Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Changes

- **An MCP row names the `env_vars` entries that changed.** Replaying openai/codex-security#1281, a `.mcp.json` that added `CODEX_SECURITY_PLUGIN_ROOT` to a server's `env_vars` array and changed nothing else was a `changed` row reading `no difference in the command name launch_codex_security_mcp, launch arguments, env key names or header key names; the change is in a detail this output does not show, such as the command's path or another setting`, which pointed at the command's path for a change to a variable name. An MCP server grant now publishes `env_var_names` (host-grants `0.9`, extended in place): the plain names its `env_vars` list declares, in declared order, never a `NAME=value` entry, an object, a token-shaped string or an entry the digest's redaction rewrites. The row reads `codex-security: env_vars names +CODEX_SECURITY_PLUGIN_ROOT` on `diff` (text and `--json`), `verify`, the PR comment and `check`, beside `env keys` and `header keys` (`env keys +API_BASE -DEBUG; env_vars names +X -Y`); the same names in another order read `env_vars names in a different order`, and an added or removed server's cell lists them. The label is the key the list is declared under, so no claim is made that a variable is set or read. No value is published or printed, and rotating an `env` value is still no row, because `config_sha256`'s input redacts every `env` value. The field is display-only, left out of grant equality, the inventory digests and saved baselines, so no row, direction, `expands`, severity, expansion signal or `check` decision moves; a change confined to an unpublished entry still says it is not shown. See [STABILITY.md](STABILITY.md#mcp-env-var-names-795). (#795)
- **An OpenShell policy row names the declarations that changed.** Replaying openshift-kni/ai-sandbox#29, a selected policy that removed `/sandbox/.uv/python` from `filesystem_policy.read_only`, changed binary selectors and added GitHub release destinations printed `authored, OpenShell 0.1.2, 6 endpoint(s), facts <digest> → …7 endpoint(s), facts <digest>`, which names none of them. `diff` (text and `--json`), `verify`, `check` and the PR comment now print the difference: `filesystem read_only -/sandbox/.uv/python`, `destination aiplatform.googleapis.com:443 (l4) binaries +/tmp/**/node_modules/…/claude -/usr/local/bin/claude`, `removed destination oauth2.googleapis.com:443 (l4) via /usr/local/bin/claude` and `added destinations objects.githubusercontent.com:443, release-assets.githubusercontent.com:443 (l4) via …`, with changed request declarations, `include_workdir`, Landlock compatibility and process identity named the same way. Each list names its first entries and counts the rest. Only values the OpenShell inventory already publishes are named, and a value that would change under redaction prints as `<redacted>`. Query and parameter constraints and endpoint options appear by field name, not value. A policy selected or removed whole lists what it declares and which sections and inspected endpoints took an upstream default, as a description and not a proof that authority grew. Display only: the published `rows[]`, direction, `expands`, severity, expansion signals and `check` decisions are unchanged, the existing unknown-direction and runtime-limit wording stays in each row's reason, and no schema changes. (#968)
- **`diff` classifies a large base tree about eight times faster.** Translating each boundary glob into a regular expression was repeated for every path in the tree and was 74–86% of first-run `diff` time on two large repositories (#698's measurement). The compiled pattern is now cached per pattern; which paths match is unchanged, and a test checks the cached matcher against the previous implementation on over 10,000 random pattern and path pairs. (#698)
- **`diff --application` leads with one finding per changed agent.** The text opens with a `Findings:` line and, for each agent that has a row, a finding whose first line stands alone: the tools the change added (`+`), removed (`-`) or changed (`~`), or only candidates (`?`); what they newly reach (the outbound call or library effect, its host, the credentials it sends by name, the parameters the model controls) or, for a changed tool, what it still reaches; and where reading stopped (`not read beyond memory_bank.py:531`), with the first unread hop and a count of the rest per tool. A finding with a binding or a reach it did not read is `partial`, though `comparison_status` still speaks of bindings only. A question specific to the change follows ("should `agent` reach `POST …` through `act`, sending `env WEATHER_TOKEN`, with the model controlling `city`?"; the same call newly taking a model-controlled argument; a removed tool; a changed one and what changed). Rows that are `not_established` for the same reason are one line with a count and one example, however many tools and agents share it, and limits no row states are one line per agent; effect evidence is counted once instead of `write (provisional: unknown effect)` per row. Per-row text follows under `Detail:` for up to 10 rows; above that the text says how many it did not print and `--json` carries every row, gap and reach limit. The JSON gains a top-level `summary` with the same content (`counts`, `findings`, `effects`, `causes`, `not_read`), each entry naming the rows it summarizes by index; it states nothing a row does not, and no row, status, direction, gap, scope or exit code changes. `application_comparison_schema_version` stays `0.4`, unreleased, with `summary` added. `comparison_id` digests it with the rows; `benchmark/application-q2/summarize.py` leaves it out of `answer_id`, so a hand score keeps judging the answer it read. On the 49-PR development corpus (`benchmark/application-q2/`) against `main` at `e703b41b`, `summarize.py` counts the same statuses and rows and every answer has the same `answer_id`. VidulaWickramasinghe/O.R.I.O.N#126's text goes from 11,926 lines to 24, jpka/attest#3's findings fit in 7 lines before its rows and Tiendat2703/MIS_TALENT#7's in 10; the committed goldens in `benchmark/application-q2/goldens/` hold six real answers. (#914)
Expand Down
52 changes: 52 additions & 0 deletions STABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,58 @@ unknown`. A rotation the digest's input already redacts is still no row

---

<a id="mcp-env-var-names-795"></a>

## Migration Note: Unreleased — an MCP row names the `env_vars` that changed (host-grants `0.9`, #795)

**What was wrong.** An MCP server grant published the keys of its `env` map
and of its `headers` map, and nothing for an `env_vars` list. Replaying
openai/codex-security#1281, a `.mcp.json` that added
`CODEX_SECURITY_PLUGIN_ROOT` to a server's `env_vars` array and changed
nothing else was a `changed` row reading `no difference in the command name
…, launch arguments, env key names or header key names; the change is in a
detail this output does not show, such as the command's path or another
setting`, which pointed at the command's path for a change to a variable
name.

**What changes.** See [MCP environment-variable names](docs/host-boundary-support.md#mcp-env-var-names).

- Host-grants inventory `0.9`, unpublished before this release, is extended in
place. An `mcp_server` grant adds `env_var_names`: the entries of its
`env_vars` list that are plain environment-variable names, in declared
order, and `[]` when none are. An entry is published only when it matches
`[A-Za-z_][A-Za-z0-9_]{0,79}` and neither the redaction `config_sha256`'s
input applies nor the label redaction rewrites it, so a `NAME=value` entry,
an object, a token-shaped string and the entry after a credential word are
not published. It is display-only, like `package` and `args_sha256`: out of
grant equality, the inventory digests and saved baselines.
- A changed MCP row names the names the list gained and lost beside the
existing `env keys` and `header keys`: `codex-security: env_vars names
+CODEX_SECURITY_PLUGIN_ROOT`, `env keys +API_BASE -DEBUG; env_vars names +X
-Y`. The same names in another order read `env_vars names in a different
order`, because `config_sha256` reads the list in order. An added or removed
server's cell lists them (`docs (command name npx; env_vars names A B)`).
When no published field differs and either side lists a name, the `no
difference …` sentence names `env_vars names` among what was compared. A
change confined to an entry that is not published still says it is not
shown.
- `diff` (text and `--json`), `verify`'s host comparison, the PR comment and
`check`'s rows print the same entry.

**Who must act.** Nobody. A reader that matched the old sentence's text, or
that validates `0.9` MCP grants against a schema generated before this change,
updates to the new one.

**What does not change.** No value is published or printed: `env` and
`headers` values are redacted whole in `config_sha256`'s input, so rotating
one is still no row, and no per-key value digest is added. The fact is
display-only, so no row, row value, direction, `expands`, severity,
expansion signal or `check` decision moves, and an MCP edit stays `authority
direction is unknown`. No check ID, CLI flag, control state or permission is
added. `minimum_control_contract_version` stays `21`.

---

<a id="url-permission-rules-922"></a>

## Migration Note: Unreleased — a URL in a permission rule keeps its rule (#922)
Expand Down
2 changes: 1 addition & 1 deletion docs/distribution-surfaces.md

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions docs/host-boundary-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,36 @@ into the npm cache only when the project does not depend on it
`package.json`, lockfile, `node_modules` or cache is read to decide which
applies, and the row's direction, `expands` and severity do not move.

<a id="mcp-env-var-names"></a>

An MCP server that passes environment variables through by name declares them
in an `env_vars` list, apart from the `env` map whose keys `env_keys`
publishes. A change confined to that list used to read as `no difference in
the command name …, launch arguments, env key names or header key names; the
change is in a detail this output does not show, such as the command's path or
another setting` (openai/codex-security#1281 added
`CODEX_SECURITY_PLUGIN_ROOT` to it, #795). The grant now publishes
`env_var_names` (host-grants `0.9`), and the row names the names the list
gained and lost, `codex-security: env_vars names +CODEX_SECURITY_PLUGIN_ROOT`,
beside `env keys` and `header keys`: `env keys +API_BASE -DEBUG; env_vars names
+X -Y`. The label is the key the list is declared under, so the row claims no
more for a name than that it is listed there: it does not say a variable is
set, available to the server or read by it, or that either host honours the
field. An entry is published only when it is a plain name,
`[A-Za-z_][A-Za-z0-9_]{0,79}`, that neither the redaction `config_sha256`'s
input applies nor the label redaction rewrites; a `NAME=value` entry, an
object, a token-shaped string and the entry after a credential word such as
`api_key` are not published, and a change confined to them still reads as a
detail this output does not show. The list is digested in order, so the same
names in another order are a row, and it reads `env_vars names in a different
order`. No value is ever published: `env` and `headers` values are redacted
whole in the digest's input, so rotating one is no row, and this entry adds no
per-key value digest. Other fields a host may honour, such as `cwd`, a bearer
token's variable name or a header taken from an environment variable, are not
published. `env_var_names` is display-only: out of grant equality, the
inventory digests and saved baselines, so no row, direction, `expands`,
severity or `check` decision moves.

The boundary is intentionally fail-closed above the adapters' specialized
semantics. Most instruction, policy, skill, and workflow edits therefore route
to human review unless a dedicated rule can prove the change safe. This can be
Expand Down
7 changes: 7 additions & 0 deletions docs/host-grants-inventory-schema.v0.9.json
Original file line number Diff line number Diff line change
Expand Up @@ -1480,6 +1480,13 @@
"title": "Env Keys",
"type": "array"
},
"env_var_names": {
"items": {
"type": "string"
},
"title": "Env Var Names",
"type": "array"
},
"grant_id": {
"title": "Grant Id",
"type": "string"
Expand Down
67 changes: 57 additions & 10 deletions src/agents_shipgate/core/capability_diff_rows.py
Original file line number Diff line number Diff line change
Expand Up @@ -1354,6 +1354,7 @@ def _mcp_cell(value: str, grant: dict[str, Any] | None) -> str:
_mcp_launch(grant),
f"package {grant['package']}" if grant.get("package") else None,
"env keys " + _names(_key_names(grant["env_keys"])) if grant.get("env_keys") else None,
f"{_ENV_VARS_LABEL} " + _names(_key_names(grant["env_var_names"])) if grant.get("env_var_names") else None,
"header keys " + _names(_key_names(grant["header_keys"])) if grant.get("header_keys") else None,
)
if fact
Expand Down Expand Up @@ -1395,7 +1396,9 @@ def _mcp_change(name: str, before: dict[str, Any], after: dict[str, Any]) -> str
confined to them says what was compared and that the change is
elsewhere, rather than ``name → name`` or a claim that the command is
unchanged. Two different endpoints that print alike, such as two URLs
neither of which is printed, read ``url changed (not shown)``.
neither of which is printed, read ``url changed (not shown)``. The names
an ``env_vars`` list gained and lost are named when both readings publish
them (#795); a value is never read.
"""

if any(key not in grant for grant in (before, after) for key in _MCP_FIELDS):
Expand All @@ -1417,19 +1420,62 @@ def _mcp_change(name: str, before: dict[str, Any], after: dict[str, Any]) -> str
kind = "url" if after.get("transport") == "url" else "command name"
parts.append(f"{kind} changed ({_URL_NOT_SHOWN})")
parts.extend(_mcp_args_change(before, after))
for field, label in (("env_keys", "env keys"), ("header_keys", "header keys")):
old, new = set(before[field] or []), set(after[field] or [])
added, removed = sorted(new - old), sorted(old - new)
if added or removed:
tokens = [f"+{key}" for key in _key_names(added)] + [f"-{key}" for key in _key_names(removed)]
parts.append(f"{label} {_names(tokens)}")
parts.extend(_key_set_change(before, after, "env_keys", "env keys"))
env_vars = _env_vars_compared(before, after)
if env_vars:
parts.extend(_key_set_change(before, after, "env_var_names", _ENV_VARS_LABEL, ordered=True))
parts.extend(_key_set_change(before, after, "header_keys", "header keys"))
if not parts:
compared = all("args_sha256" in grant for grant in (before, after))
return f"{name}: {_mcp_unshown_change(after, args_compared=compared)}"
return f"{name}: {_mcp_unshown_change(after, args_compared=compared, env_vars=env_vars)}"
return f"{name}: " + "; ".join(parts)


def _mcp_unshown_change(grant: dict[str, Any], *, args_compared: bool = False) -> str:
#: What a row calls a server's ``env_vars`` list: the key it is declared under,
#: so no meaning is claimed for a name beyond that it is listed there (#795).
_ENV_VARS_LABEL = "env_vars names"


def _env_vars_compared(before: dict[str, Any], after: dict[str, Any]) -> bool:
"""Whether both readings publish ``env_var_names`` and either declares one.

A saved baseline, or a grant from an earlier schema, publishes none, so
nothing is claimed about the names it cannot show. Two readings that
declare none have nothing to say about them.
"""

return all("env_var_names" in grant for grant in (before, after)) and bool(
before["env_var_names"] or after["env_var_names"]
)


def _key_set_change(
before: dict[str, Any], after: dict[str, Any], field: str, label: str, *, ordered: bool = False
) -> list[str]:
"""The names a published list gained and lost, never a value (#795).

Names print through the #802 label redaction. ``ordered`` is for a list
whose order is part of its declaration (``env_vars``): the same names in
another order are said to be, rather than called no difference.
"""

old_list, new_list = list(before[field] or []), list(after[field] or [])
old, new = set(old_list), set(new_list)
added, removed = sorted(new - old), sorted(old - new)
if added or removed:
tokens = [f"+{key}" for key in _key_names(added)] + [f"-{key}" for key in _key_names(removed)]
return [f"{label} {_names(tokens)}"]
if ordered and old_list != new_list:
# The same set of names: the declaration still differs, in its order
# or in how many times a name is listed.
same = sorted(old_list) == sorted(new_list)
return [f"{label} in a different order" if same else f"{label} listed a different number of times"]
return []


def _mcp_unshown_change(
grant: dict[str, Any], *, args_compared: bool = False, env_vars: bool = False
) -> str:
"""A change confined to what the grant does not publish, in the words of what was compared.

Only the command's name, or the URL's recorded value, the launch
Expand Down Expand Up @@ -1457,8 +1503,9 @@ def _mcp_unshown_change(grant: dict[str, Any], *, args_compared: bool = False) -
else:
compared = launch or "command name"
unshown = "the command's path or another setting" if arguments else "the command's path or arguments"
names = f"env key names, {_ENV_VARS_LABEL} or header key names" if env_vars else "env key names or header key names"
return (
f"no difference in the {compared}, {arguments}env key names or header key names; the "
f"no difference in the {compared}, {arguments}{names}; the "
f"change is in a detail this output does not show, such as {unshown}"
)

Expand Down
Loading
Loading