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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

### Changes

- **An n8n MCP Client Tool that exposes every tool of its server is read as doing so.** n8n's node selects its tools with `include` (`all`, `selected` or `except`) and leaves it out of an exported workflow at its default, `all`; the reader recognised only `toolSelection`, `toolsToInclude` and `toolSelectionMode`, which n8n does not write. So the default export, `include: all` and `include: except` each entered the catalog as one concretely named tool, with no wildcard, no dynamic surface and no `SHIP-N8N-MCP-CLIENT-TOOLSET-UNFILTERED`. Each is now one `<node>.*` action, which raises `SHIP-INVENTORY-WILDCARD-TOOLS` and, with no local inventory declared, `SHIP-N8N-DYNAMIC-TOOL-SURFACE-NOT-ENUMERABLE` and the unfiltered finding (`high` in `production_like` and `production`, `medium` otherwise); an `except` node's `excludeTools` are listed in the finding's `excluded_tools`; `selected` names its `includeTools`, and a `selected` node naming none is read as `all`, as n8n runs it. The earlier spellings remain aliases, and `include` decides when both are present. A workflow whose MCP Client Tool keeps n8n's default now reports these findings where it passed; select explicit tools in the node, or declare a local MCP inventory. Migration: [STABILITY.md](STABILITY.md#mcp-reader-conformance-1025). (#1025)
- **`SHIP-MCP-ENV-SECRET-PASSTHROUGH` counts a secret an MCP server forwards by name, not only an `env` entry.** A secret-like variable in Codex's `env_vars` list or `env_http_headers`, any `bearer_token_env_var`, and a `${NAME}` reference in a `headers` value (`.mcp.json`) now fire the finding (an editor's `${env:NAME}` is read as `NAME` there and in `env`, and a prompted `${input:…}` names no variable) and add to the server's risk score as an `env` entry always did. A credential-named header (`Authorization`) counts the variable it carries whatever that variable is called, and a value written where a variable name belongs is redacted in the evidence, not echoed. (#1025)
- **A `codex_config` source names the other hosts' MCP config files it does not read.** Its coverage said it read `.mcp.json`, `.codex/config.toml` "and the other host MCP config files" under its root; it reads only the first two. A `.cursor/mcp.json` or `.vscode/mcp.json` there is now named in a source warning, so a workspace whose servers live only in one no longer passes silently, and the coverage text in [`docs/determinism-boundary.md`](docs/determinism-boundary.md) names exactly the files read and the files named. Reading those files is #663. (#1025)
- **`verify` stops when it has no base, and a deleted manifest is a removed gate.** With neither `--base` nor `--no-base`, a run whose detection found nothing (a default branch other than `main` or `master` with no `origin/HEAD`, or a repository with no remote) read no committed change, published that empty change set as `complete` and granted merge: a pull request deleting `.github/workflows/agents-shipgate.yml` on a `develop` repository exited 0 as `mergeable`, and exited 20 as `blocked` once `--base origin/develop` was named. Detection keeps every base it found before and otherwise follows `check`'s ladder (#649): a default branch already at the head is an empty comparison, and a repository with no remote uses its local `main` or `master`. When nothing resolves, the run exits 2 with `base_status: ref_missing`, a `diff_status` of `unavailable`/`refs_missing` whose detail begins `base_unresolved`, a `fetch_base` next action naming `--base` and `--no-base`, and every permission denied. A change deleting the configured manifest that its merge base (or `HEAD`, for a working-tree run without a base) holds is a human `stop` naming the removal, exit 2, instead of the `configure` and `init --write` route; a missing manifest whose base cannot be resolved fails closed as above, one whose base holds it but cannot be compared (a shallow merge base), or whose named `--base` is not available locally (the Action's `diff_base: target` without `fetch-depth: 0`), fails closed with that diff-input `fetch_base` stop in both CI modes, and a repository that never had one keeps the setup route (with an unavailable named base, only where complete history, neither shallow nor grafted, shows the manifest never existed; a depth-1 checkout with an unfetched `--base` therefore stops in advisory mode too, where it used to exit 0 on the host route). `SHIP-VERIFY-CI-GATE-REMOVED` resolves changed paths against the evaluated tree's root instead of the manifest's directory, so `apps/agent/shipgate.yaml` no longer turns a comment in the root workflow into a critical blocker, nor lets a nested copy hide a root deletion. Every Git archive packs the commit's tree rather than its history, so a depth-1 checkout archives `--head` instead of failing an integrity check that agent mode routed to editing `shipgate.yaml`. `check` leaves verify's default reports directory out of a working-tree change set when it holds only uncommitted Shipgate artifacts, so a check, verify, check loop no longer counts the reports as changed files. **A run that used to report `mergeable` with no base now exits 2**: pass `--base` (in the Action, `diff_base: target` with `fetch-depth: 0`, or `base_ref`) or `--no-base`. No schema, contract or check ID moves. See the `verify stops without a base` migration note in `STABILITY.md`. (#1023)
- **`attest`, `org bundle` and the Action validate the verification receipt of current verify output.** All three gated receipt validation on version literals the engine had stopped emitting (verifier `0.5`–`0.7`, `shipgate.verify_run/v3`). On every completed `1.x` verify run, `attest --from agents-shipgate-reports/verifier.json` and the Action's `attestation: true` step crashed with a traceback and exit 1, `org bundle` published a rewritten `receipt_id` or `decision_id` with exit 0, and the Action exported empty `request_id`, `receipt_id`, `decision_id` and `artifact_set_id` beside a `verification_receipt_json` nothing had checked. One predicate, `requires_verification_receipt` in `agents_shipgate.schemas.verification_identity`, now decides for all three: it names the verifier versions written before receipts (`0.1`–`0.4`) and reads every other version, newer ones included, as owing a receipt whenever the verifier states an outcome; the Action, which also publishes `report.json`'s decision, owes one whenever `verifier.json` names a `report.json` as its own, and publishes nothing from a report it does not name (the manifest-free host route leaves an earlier run's in place). A `0.1`–`0.4` label on a payload carrying a receipt-era identity field (`request_id`, `decision_id` and the four others verifier `0.5` added) is not believed. A failed or not-run execution that states none (a missing manifest, the manifest-free host route) writes no receipt, and still publishes, with empty identities. A receipt that is present is always validated: its content addressing, every artifact it binds and the request and decision `verifier.json` names. A missing, tampered or mismatched one exits `3` in `attest` and `org bundle` with one line naming the receipt (plus the agent-mode `next_action` line), and fails the Action's output step. `org bundle` also refuses an `attestation.json` whose receipt identity is not the validated receipt's, or that names one when none was validated. The Action's `verification_receipt_json` is empty unless the receipt validated. No schema version changes. See the [migration note](STABILITY.md#receipt-predicate-1022). (#1022)
- **An OpenAPI operation behind a path-item `$ref` is read, and one that cannot be read is named.** The loader skipped every key of a path item that was not an HTTP method, so `/accounts/{id}: {$ref: '#/components/pathItems/Account'}` contributed nothing: its `deleteAccount` was absent from `report.json` and a strict scan passed. A `$ref` to another path item in the same document is now followed (chains included, with the fields written beside the `$ref` read first), and each operation's `source_pointer` and line name where it is written, such as `/components/pathItems/Account/delete`. A path item whose `$ref` leaves the document (a split spec's `./paths/users.yaml`, which is reported and never opened), names nothing, or loops is a source warning and an `adapter_parse` row (`unresolved_path_item_ref`) in `surface_exclusions`, so the scan no longer passes; a referenced operation replaced by one of the same method beside the `$ref` is one too (`shadowed_path_item_operation`). Schema `$ref`s the document resolves no longer warn: the resolver also descends `additionalProperties`, `patternProperties`, `not`, `prefixItems` and list-form `items`, follows a schema that is only a reference, and an unresolved reference is reported once per target rather than once per tool, so four read operations sharing an `additionalProperties` reference no longer read as `insufficient_evidence`. Unquoted YAML status codes (`200:`) select the lowest 2xx response as the output schema instead of the first response listed, and the DELETE proof's pure-Python YAML parse runs only when a document has a DELETE, stopping once the document is past its node budget. **Tool inventories can grow** where a path-item `$ref` hid operations, and a verdict that rested on the hidden part can move from `passed` to `review_required` or `blocked`; one held back only by false unresolved-reference warnings can move to `passed`. The DELETE proof still refuses a path item written as a `$ref`. (#1021)
Expand Down
55 changes: 55 additions & 0 deletions STABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,61 @@ the Action tag) for reproducible CI.

---

<a id="mcp-reader-conformance-1025"></a>

## Migration Note: Unreleased — n8n's `include`, MCP secrets forwarded by name, and the host MCP files a `codex_config` source does not read (#1025)

**What was wrong.** Three readers did not match what the tools they read
write. n8n's MCP Client Tool selects its tools with `include` (`all`,
`selected` or `except`) and leaves it out of an export at its default, `all`;
the n8n reader read only `toolSelection`, `toolsToInclude` and
`toolSelectionMode`, so a node exposing every tool of its server was one
concretely named tool and `SHIP-N8N-MCP-CLIENT-TOOLSET-UNFILTERED` never fired
on a real export. `SHIP-MCP-ENV-SECRET-PASSTHROUGH` read only a server's `env`
table, not a secret forwarded by name. And a `codex_config` source's coverage
said it read "the other host MCP config files" while it read only `.mcp.json`
and `.codex/config.toml`, so a workspace whose servers lived in
`.cursor/mcp.json` or `.vscode/mcp.json` passed with nothing said.

**What changes.**

- **Check ids.** None is added, removed or renamed. Per
[Check IDs](#check-ids), the conditions under which these fire broaden, and
this note records it:
`SHIP-N8N-MCP-CLIENT-TOOLSET-UNFILTERED`,
`SHIP-N8N-DYNAMIC-TOOL-SURFACE-NOT-ENUMERABLE` and
`SHIP-INVENTORY-WILDCARD-TOOLS` fire for an MCP Client Tool with no
`include`, `include: all`, `include: except`, or `include: selected` naming
no `includeTools`; `SHIP-MCP-ENV-SECRET-PASSTHROUGH` fires for a
secret-like name in `env_vars` or `env_http_headers` (or one a credential
header carries), any `bearer_token_env_var`, and a `${NAME}` reference in
`headers`.
- **Fingerprints.** Such an MCP Client Tool's action is `<node>.*` rather than
the node's name, so a finding keyed on the old tool name (for example
`SHIP-N8N-AI-TOOL-METADATA-MISSING`) reads as new. An
`SHIP-MCP-ENV-SECRET-PASSTHROUGH` finding on a server that also forwards a
secret in one of the new forms lists more `env_secret_names`, so it reads as
new too. An unfiltered finding on an `except` node gains `excluded_tools`
only when the node names one, so an existing finding with none keeps its
fingerprint.
- **Decisions.** A workflow whose MCP Client Tool keeps n8n's default, and a
`codex_config` workspace holding `.cursor/mcp.json` or `.vscode/mcp.json`
under its root, no longer reach `passed`: the first raises the findings
above, and the second a source warning naming each such file.

**Who must act.** An n8n workflow author selects explicit tools in the MCP
Client Tool, or declares a local MCP inventory. A `codex_config` adopter
reviews the servers in the named file; until #663 reads those files, the
warning stays while the file is under the source's root. A suppression or
baseline entry for a finding that reads as new is re-recorded.

**What does not change.** The earlier n8n spellings are still read, as
aliases; `include` decides when both are present. A `selected` node naming
its tools lists them as before. No CLI flag, schema, control state or
permission is added, and `minimum_control_contract_version` stays `21`.

---

<a id="verify-base-unresolved-1023"></a>

## Migration Note: Unreleased — `verify` stops without a base, and a deleted manifest is a removed gate (#1023)
Expand Down
7 changes: 4 additions & 3 deletions docs/checks.json
Original file line number Diff line number Diff line change
Expand Up @@ -2514,7 +2514,7 @@
"source_id",
"env_secret_names"
],
"fires_when": "A statically parsed MCP server declares secret-like env vars or passes through secret-like host env references.",
"fires_when": "A statically parsed MCP server declares secret-like env vars, passes through secret-like host env references, or forwards a secret-like variable by name (`env_vars`, `bearer_token_env_var`, `env_http_headers`, a `headers` reference).",
"floor_severity": "medium",
"id": "SHIP-MCP-ENV-SECRET-PASSTHROUGH",
"mvp_tier": "lifecycle",
Expand Down Expand Up @@ -2686,9 +2686,10 @@
"source_ref",
"node_id",
"selection_mode",
"explicit_inventory"
"explicit_inventory",
"excluded_tools"
],
"fires_when": "An n8n MCP Client Tool uses All or All Except selection and no explicit local inventory is declared.",
"fires_when": "An n8n MCP Client Tool uses All or All Except selection (`include` of `all`, the default an export leaves out, or `except`) and no explicit local inventory is declared.",
"floor_severity": null,
"id": "SHIP-N8N-MCP-CLIENT-TOOLSET-UNFILTERED",
"mvp_tier": "adapter",
Expand Down
17 changes: 13 additions & 4 deletions docs/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -923,7 +923,11 @@ inventory.
### SHIP-N8N-MCP-CLIENT-TOOLSET-UNFILTERED

An n8n MCP Client Tool exposes `All` or `All Except` tools without a local
inventory. Select explicit MCP tools or provide a local MCP inventory for
inventory. The node selects with `include`: `all`, which is n8n's default and
is left out of an exported workflow, so a node with no `include` exposes every
tool; `except`, whose `excludeTools` the finding lists as `excluded_tools`; or
`selected`, which names its `includeTools` and does not fire this check unless
it names none. Select explicit MCP tools or provide a local MCP inventory for
release review. The severity is environment-sensitive because the selector is
easy to narrow before production, while production-like use increases blast
radius.
Expand Down Expand Up @@ -1234,9 +1238,14 @@ a changed file — env- or settings-bound factories never guess a match.

Fires when a statically parsed MCP server passes through secret-like
environment variables, such as token, password, API key, or credential
names. Secret pass-through changes the credential boundary available to the
server, so Shipgate routes the change to human review without exposing raw
secret values in evidence.
names. Besides an `env` entry, a server can forward a variable by name: in
Codex's `env_vars` list or `env_http_headers`, as a `bearer_token_env_var`
(always counted, since it holds a bearer token), or as a `${NAME}` reference in
a `headers` value. A forwarded variable counts when its name, or the header
carrying it, is secret-like. An editor's `${env:NAME}` is read as `NAME`; a
prompted `${input:…}` names no environment variable. Secret pass-through changes the credential
boundary available to the server, so Shipgate routes the change to human
review without exposing raw secret values in evidence.

### SHIP-MCP-AUTO-APPROVE-SIDE-EFFECT

Expand Down
5 changes: 3 additions & 2 deletions docs/checks/mcp_permissions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ checks:
description: MCP server passes through secret environment variables.
rationale: Secret-bearing environment pass-through expands the credential boundary
available to a tool server.
fires_when: A statically parsed MCP server declares secret-like env vars or passes
through secret-like host env references.
fires_when: A statically parsed MCP server declares secret-like env vars, passes
through secret-like host env references, or forwards a secret-like variable by
name (`env_vars`, `bearer_token_env_var`, `env_http_headers`, a `headers` reference).
evidence_fields: [source_id, env_secret_names]
recommendation: Review the secret boundary or replace pass-through with a narrower
credential mechanism.
Expand Down
6 changes: 4 additions & 2 deletions docs/checks/n8n.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,14 @@ checks:
description: n8n MCP Client Tool exposes an unfiltered toolset.
rationale: |-
All-tools and all-except MCP client selections can expose more tools than reviewers expect. The check is environment-sensitive because the selector is straightforward to narrow before production, while production-like use increases blast radius.
fires_when: An n8n MCP Client Tool uses All or All Except selection and no explicit
local inventory is declared.
fires_when: An n8n MCP Client Tool uses All or All Except selection (`include`
of `all`, the default an export leaves out, or `except`) and no explicit local
inventory is declared.
evidence_fields:
- source_ref
- node_id
- selection_mode
- explicit_inventory
- excluded_tools
recommendation: Select an explicit MCP tool allowlist in n8n or provide a local
MCP inventory.
4 changes: 2 additions & 2 deletions docs/determinism-boundary.json
Original file line number Diff line number Diff line change
Expand Up @@ -1073,7 +1073,7 @@
"SHIP-INVENTORY-WILDCARD-TOOLS",
"SHIP-N8N-MCP-CLIENT-TOOLSET-UNFILTERED"
],
"reads": "An MCP client node whose tool selection is `all`, `all_except`, or unreadable: one `<node>.*` action stands in for a selection the workflow does not enumerate.",
"reads": "An MCP client node whose `include` is `all` \u2014 n8n's default, which an export leaves out \u2014 or `except`, or whose selection is unreadable: one `<node>.*` action stands in for a selection the workflow does not enumerate, and an `except` node's `excludeTools` are recorded.",
"shape": "dynamic_construction",
"status": "extracted",
"surface": null,
Expand Down Expand Up @@ -1524,7 +1524,7 @@
"label": "Codex / MCP host config",
"manifest_section": null,
"manifest_section_role": "activates",
"reads": "`.mcp.json`, `.codex/config.toml`, and the other host MCP config files under the root a `tool_sources[]` entry names \u2014 including the servers a plugin block declares."
"reads": "`.mcp.json` and `.codex/config.toml`, at any depth under the root a `tool_sources[]` entry names \u2014 including the servers a plugin block of `.codex/config.toml` declares. Other hosts' MCP config files there (`.cursor/mcp.json` and `.vscode/mcp.json`) are not read; each one present is named in a source warning."
},
{
"adapter": "codex_plugin",
Expand Down
Loading
Loading