agents-shipgate
ThreeMoonsLab/agents-shipgate/llms-full.txt
Single-fetch concatenation of the canonical agent-facing reference material. AI search engines and coding agents that prefer one document over chasing links should fetch this file. The short index is at llms.txt; machine-readable triggers are at docs/triggers.json. Generated by scripts/build-llms-full.py from the source files below. Do not edit by hand — re-run the script to update. Authoritative instructions for AI coding agents (Claude Code, Codex, Cursor, Aider, Cline, Windsurf, Devin, or any other harness — see docs/agents/any-coding-agent.md) working with this repository…
- Installs packages
- Commits and pushes
# Agents Shipgate · Long-Form Agent Reference (llms-full.txt)
> Single-fetch concatenation of the canonical agent-facing reference
> material. AI search engines and coding agents that prefer one document
> over chasing links should fetch this file. The short index is at
> [`llms.txt`](llms.txt); machine-readable triggers are at
> [`docs/triggers.json`](docs/triggers.json).
>
> Generated by `scripts/build-llms-full.py` from the source files below.
> Do not edit by hand — re-run the script to update.
## Sources (in order)
- [`AGENTS.md`](AGENTS.md)
- [`docs/agent-recipes.md`](docs/agent-recipes.md)
- [`docs/agent-contract-current.md`](docs/agent-contract-current.md)
- [`docs/checks.md`](docs/checks.md)
- [`docs/concepts.md`](docs/concepts.md)
- [`docs/autofix-policy.md`](docs/autofix-policy.md)
---
<!-- ===== source: AGENTS.md ===== -->
# Agents Shipgate · Agent Instructions
Authoritative instructions for AI coding agents (Claude Code, Codex, Cursor, Aider, Cline, Windsurf, Devin, or any other harness — see [`docs/agents/any-coding-agent.md`](docs/agents/any-coding-agent.md)) working **with** this repository or a project that uses Agents Shipgate.
> If you are a human, the README and the [wiki](https://github.com/ThreeMoonsLab/agents-shipgate/wiki) are the right places to start. This file is optimized for agent ingest: short, copy-pasteable, machine-friendly.
---
## What this project is
The deterministic merge gate for AI-generated agent capability changes. Reads `shipgate.yaml` plus tool sources (MCP exports, MCP server source (TypeScript/Go registration idioms), OpenAPI specs, OpenAI Agents SDK Python files, Anthropic Messages API tool/prompt artifacts, Google ADK Python/config files, LangChain/LangGraph Python files, CrewAI Python files, OpenAI API artifacts, Codex repo config, Codex plugin packages and marketplaces, n8n workflow JSON/stubs, Conductor OSS workflow JSON) and produces deterministic findings. Local-first and static by default — no agent execution, tool calls, LLM calls, or network access.
- **Host-configuration review:** `agents-shipgate diff` shows what a change does to declared coding-agent configuration — `.claude/settings.json`, `.mcp.json`, hooks, Codex, Cursor and VS Code MCP config, GitHub workflow permissions — with no `shipgate.yaml`, saved baseline or skill. It compares against the base already in Git history, names recognized sources it could not read, and implies no verdict. A no-change answer covers only what both sides read. It names a changed input a bounded candidate list recognises but no reader reads — a plugin's `mcp.json`, `.cursor/hooks.json`, a nested `.claude/settings.json`, an external marketplace source — as `added`, `removed` or `changed, not read by this entry`, and says nothing about the other [surfaces `diff` does not read](docs/host-boundary-support.md#known-unread-surfaces), such as hook scripts, composite actions and subagent hooks; see **Run (canonical)** and [`docs/host-boundary-support.md`](docs/host-boundary-support.md) for what it reads.
- **Inputs:** MCP · MCP server source · OpenAPI · OpenAI Agents SDK · Anthropic Messages API · Google ADK · LangChain/LangGraph · CrewAI · OpenAI API · Codex config · Codex plugin · n8n · Conductor OSS workflow JSON
- **Outputs:** Markdown · JSON · SARIF
- **Trust:** Static-by-default. No agent execution, tool calls, LLM calls, or network access.
- **Marketing site:** [threemoonslab.com](https://threemoonslab.com/) — human-readable companion pages. **If you are an agent working inside this repo, use the in-tree [`.well-known/agents-shipgate.json`](.well-known/agents-shipgate.json) (current `main` contract, may be ahead of the site's released copy) for schema-version and gating-signal decisions.**
---
## Naming (canonical)
Use exactly one form depending on context. Mixing them in user-visible copy is an adoption cost.
| Form | When to use |
|---|---|
| **Agents Shipgate** | Display name. Prose, headings, marketing copy, social cards, slide titles, blog posts. |
| **`agents-shipgate`** | Package, CLI binary, repo, GitHub Action, PyPI distribution name, env-var prefix (`AGENTS_SHIPGATE_*`), import path (`agents_shipgate`). Always lowercase, kebab-case. |
| **`shipgate`** | Short alias for the CLI binary only. Acceptable in shell snippets where brevity helps; never as the project name. |
Do **not** use any of: `Agent Shipgate` (singular), `Agent Shipcheck`, `agents shipgate` (display lowercase), `Agents-Shipgate` (display kebab). When in doubt: prose → `Agents Shipgate`; code → `agents-shipgate`.
The canonical tagline is:
> The deterministic merge gate for AI-generated agent capability changes.
This single sentence is the source of truth for the GitHub repo description, [README.md](README.md), the [wiki Home page](https://github.com/ThreeMoonsLab/agents-shipgate/wiki/Home), and the [marketing site](https://threemoonslab.com/) `<meta name="description">`. Keep them in sync; the website's `.well-known` discovery file is pinned to the latest released tag and refreshes at each release.
Use **Tool-Use Readiness** in Title Case when naming the product/category or
the **Tool-Use Readiness Report** artifact. Use **tool-use readiness** in
sentence case when describing the general concept.
---
## Install (canonical)
```bash
pipx install agents-shipgate
```
Alternatives if `pipx` is unavailable:
```bash
python -m pip install agents-shipgate # global pip
uv tool install agents-shipgate # via uv
python -m agents_shipgate --help # run from a pip install without PATH
```
The CLI binary is `agents-shipgate`. A short alias `shipgate` is also installed.
---
## Run (canonical)
Handling a capability change right now? Start with **Local control** below —
run First-time setup only when the repo has no `shipgate.yaml` yet.
**First-time setup** — in a repo that contains an agent and its tools:
```bash
agents-shipgate init --workspace . --write
agents-shipgate scan -c shipgate.yaml
```
Reports land at `agents-shipgate-reports/report.{md,json}`.
**What did this change do to the agent's authority?** — one row per host
grant, no manifest and no committed baseline required:
```bash
shipgate diff --workspace .
```
It compares the detected default branch's merge base with the working tree,
materialising the base tree and reading it with the same host readers, then
handing both inventories to the comparator `audit --host --drift` already
uses. Rows carry before, after, direction, why it matters and the engine's
severity; `⚠` marks the changes the engine called expansions of authority.
`--json` emits the same rows. It publishes no verdict: static configuration
is what the files permit, not what the agent did.
**Local control for coding agents** — before reporting an agent-capability
change complete, run the local control loop and parse stdout JSON:
```bash
shipgate check --agent codex --workspace . --format agent-boundary-json
shipgate check --agent claude-code --workspace . --format agent-boundary-json
shipgate check --agent cursor --workspace . --format agent-boundary-json
```
`--agent` identifies the caller; it never selects host coverage. Every
recognized changed Codex, Claude Code, Cursor, VS Code MCP, shared trust-root,
and GitHub workflow surface is evaluated on every run.
**What a flagless run compares.** The detected default branch's merge base
against the working tree, so committed branch work and uncommitted edits are
one comparison. `subject.base` names the ref that was used. `--base` and
`--head` are independent: `--base <ref>` compares that ref's merge base with
the working tree, `--head <ref>` compares the detected base against it, and
both together are the committed `base...head` range. `--base HEAD` restricts
the run to uncommitted changes. On the default branch, where there is no
other base, the working-tree comparison is the whole answer. Where no base
can be detected at all — no remote, no `main` or `master` — the run stops and
names `--base` rather than reporting a pass it did not establish.
Read the single stdout object as `shipgate.agent_boundary_result/v1`. Switch on
`control.state`; inspect `input_coverage`, `host_coverage`, `affected_hosts`,
`policies`, `violations`, and `issues`; then follow `control.next_action`,
`control.allowed_next_commands`, and `control.human_review`. Treat `decision`
as diagnostic context, never as the operational control signal, and never
infer control from Markdown, PR comments, or prose. If
`control.state=complete`, summarize the result and finish. If
`control.state=agent_action_required`, perform only the exact coding-agent
action authorized by `control.next_action`, then rerun the command. If
`control.state=review_publishable`, a human must approve the merge — surface
the JSON result, and note that you may still commit, push, and update the pull
request so that review can happen. If `control.state=human_review_required`,
stop and surface the JSON result to a human. `control.permissions` states the
authority exactly: updating a PR is never merging it, and
`permissions.merge`/`permissions.report_complete` are false on every state but
`complete`. Conversation-level acknowledgement never changes control state;
only a new verifier artifact can clear it.
**Before editing a protected release surface** — ask the proactive static
planner first:
```bash
agents-shipgate preflight --workspace . --plan - --json
agents-shipgate preflight --changed-files changed.txt --json
agents-shipgate preflight --capability-request request.json --json
```
Switch on `control.state`. If it is `human_review_required`, stop and route the
change to a human. If it is `agent_action_required`, perform only the exact
coding-agent route in `control.next_action`. The plan form accepts `changed_files[]`,
`diff_text`, `capability_requests[]`, `host_permission_requests[]`, and
`context.{agent,task}`; prefer it whenever the agent can describe the planned
change as one JSON object. Protected surfaces include
`shipgate.yaml`, `.github/workflows/agents-shipgate.yml`,
`AGENTS.md`/`CLAUDE.md`/Cursor rules, policy packs, baselines, waivers,
suppressions, Codex hooks/config, Codex plugin manifests, `.mcp.json`,
`.app.json`, and `SKILL.md`. Preflight is a routing/projection surface only;
`release_decision.decision` remains the release gate.
Contract v32 makes instruction protection conditional on parsed structure.
For a prose edit, supply the complete proposed `diff_text` to preflight; a
path-only plan still routes to review. Only an explicit
`protected_surface_touches[].instruction_structure_unchanged: true` on that
exact path proves the supported structure did not change. Unknown/malformed
frontmatter, inline preprocessing changes, registration moves and configured
manifest/policy edits retain their review route. This does not judge prompt
safety or grant edit/merge authority. Verifier v0.17 and handoff v9 publish
`conditional_file_edits` separately from unconditional `forbidden_file_edits`;
follow current `control` as before. See the
[instruction structure boundary](docs/engineering/instruction-structure-boundary.md).
**PR / reviewer evidence** — for committed PR/CI refs, run the deterministic
verifier on the diff. Make the base ref available first because `verify` never
fetches:
```bash
agents-shipgate verify --workspace . --config shipgate.yaml \
--base origin/main --head HEAD --ci-mode advisory --format json
```
For local uncommitted verifier work, omit `--base`/`--head` so the working tree
is scanned. Read `agents-shipgate-reports/current-control.json`
first — it names which run is current — then validate the
`verification-receipt.json` it binds, then read
`agents-shipgate-reports/agent-handoff.json` and lead
with `control.state`, then `gate.merge_verdict`
(`mergeable | human_review_required | insufficient_evidence | blocked |
unknown`), `gate.can_merge_without_human`, `next_action`,
`fix_task`, and `capability_review.top_changes[]`. Fall back to
`agents-shipgate-reports/verifier.json` only when the installed CLI contract is
older than v6. Then read
`agents-shipgate-reports/report.json.release_decision.decision`
(`blocked | review_required | insufficient_evidence | passed`), which remains
the release gate. Do not report completion unless `control.state` is
`complete`. A human-review route keeps merge and completion denied until a new
verifier artifact changes the control state; conversation-level acceptance is
not a gate
override.
Read the pointer with:
```bash
agents-shipgate agent control --workspace . --reports-dir agents-shipgate-reports
```
That returns `shipgate.agent_control/v1`, the compact control envelope: the
control state, the `permissions` vector, the next actor, the exact next action,
and the path and sha256 of every artifact `current-control.json` binds, in one
object. It is the
whole routing answer — an agent that switches on `permissions` and
`next_action` from it does not need the artifact walk above. Read
`execution` and `exit_code` as what they are: whether the tool ran, and whether
the CI gate failed. Neither is merge authority; `permissions.merge` is. Pass
`--format pointer` for the raw `current-control.json`, and use
`agents-shipgate verify --format control` to get the same envelope directly
from a run you just performed.
A zero exit means the printed answer was validated against every artifact it
binds, still describes the repository as it stands right now, and did not move
while it was read. Byte consistency is not generation consistency: one commit
is enough to make an intact artifact set describe a workspace that has moved,
so the read compares the pointer's HEAD, tree, and worktree overlay against the
live repository and refuses on drift. A non-zero exit means no control identity
is current here — you hold no authority, and a remembered result does
not substitute for one. Re-read it after any human or external-tool action,
after commit, rebase, checkout, pull, or any worktree change, after any
agents-shipgate command returns, before enforcing a cached `must_stop`, before
commit/push/PR update, before merge or release, and before declaring the task
complete. If `current_control_id` changed, discard every cached control state
and restart from the new identity. This runs in both directions: a cached stop
must not survive a newer complete run, and a cached completion must not survive
a changed workspace.
Do not bypass the verifier by suppressing findings, lowering severity,
expanding baselines or waivers, removing Shipgate CI, or weakening agent
instructions. Verify-mode `SHIP-VERIFY-*` checks make those trust-root edits
release-visible and route them to human review.
Never invent or auto-fill an action effect or action authority declaration —
including the shared `tool_sources[].authority` block, which is the same
authority claim made once for a whole source rather than once per action.
Never invent or auto-fill `agent_bindings` root, tool, or handoff declarations,
or the `tool_sources[].binding` block that makes the same closed-world claim
once for a whole published tool surface; they are reviewed claims about
deployed wiring.
Contract v14 publishes these boundaries as `action_effect`, `action_authority`,
and `agent_binding` in `do_not_auto_assert[]`; route binding and semantic next
actions to a human and rerun verification after the reviewed declaration is supplied.
There is exactly one exception, and it is narrow by construction (contract v26,
report v0.41). When `control.next_action.kind` is `confirm_declarations`, run
the command it names and nothing else. That command writes only the rows the
report itself tags `next_action.authorable_by: "coding_agent"` — rows whose
declaration the scan filled in completely from its own evidence, which today
means an effect it read directly. Everything else stays exactly as above: a row
tagged `"human"`, any authority or `agent_bindings` block, an `override`, and a
`declaration_drift` row asking a person to re-confirm an answer are never yours
to write, and you may never fill a blank the scan left or weaken a declaration
the manifest already carries. Do not reconstruct the edit by hand if the route
is absent — its absence is the answer.
`action_surface.actions[].basis` is the one field in that block that is not a
human assertion: it is a digest of the evidence the scan read for the action,
published in the row's own `declaration_template`, and it re-opens the question
as `declaration_drift` when that evidence moves. Copy it verbatim from the
report when carrying a reviewed declaration forward; it can never make an action
pass-eligible, and it does not make an effect you invented any more declared.
A `declaration_drift` row still routes to a human — it asks a person to re-read
the evidence, not a machine to restamp the digest.
To reproduce the verify-native blocked refund PR demo without writing YAML:
```bash
agents-shipgate fixture run ai_generated_refund_pr
```
To verify your install on the older static scan fixture:
```bash
agents-shipgate fixture run support_refund_agent
```
---
## First-adoption helper flow (v0.6+)
For coding agents adopting Shipgate end-to-end in one turn:
```bash
agents-shipgate detect --json
agents-shipgate init --write --ci --json
agents-shipgate scan -c shipgate.yaml --suggest-patches --format json
agents-shipgate apply-patches --from agents-shipgate-reports/report.json \
--confidence high --apply
```
Or chain all four in one call:
```bash
agents-shipgate bootstrap --json
```
`bootstrap` runs `detect → init --write --ci → scan --suggest-patches → apply-patches --confidence high` against the current workspace, stopping on the first non-recoverable error and emitting a structured per-step summary. Use it for first-time adoption; for ongoing CI keep using the GitHub Action. Flags: `--workspace`, `--confidence`, `--no-ci`, `--no-apply`, `--json`.
- **`detect`** — read-only; classifies the workspace. `is_agent_project: false`
is **not** on its own a reason to stop. It is false for every artifact-only
and Codex-plugin-only workspace, which are adoptable, and it is unsafe to
read at all when the parse was cut short. Stop only when the whole published
stop condition holds: `is_agent_project: false` **and** `suggested_sources`
empty **and** `codex_plugin_candidates` empty **and**
`host_boundary_candidates` empty **and** `host_discovery_incomplete_paths`
empty **and** `python_parse_truncated: false`. Host candidates are filenames
only: follow `control.next_action` to `audit --host`; no manifest is needed
and no grants have been verified. A path the census could not see through —
a link it does not follow, a directory it could not read — can conceal nested
host configuration, so an incomplete host census is never a terminal
negative. It is not a failed classification either: the framework, source
and scope answers stand beside it.
`python_parse_truncated: true` means the
Python parse stopped at `max_python_files`, so the negative describes the
files that were read rather than the repository — re-run with
`--max-python-files <workspace_signals.python_file_total>`, which is a bound
that cannot hit the cap again. `init --write` takes the same flag and refuses
without it while the parse is truncated, rather than declaring an agent name
and tool surface read from part of the tree. `agent_scope` says whether one manifest can
describe this workspace at all: `"ambiguous"` means agents live in several self-contained
projects (`agent_project_candidates[]` lists them, and the manifest belongs
in one of them rather than at the workspace root); `"unknown"` means
discovery was capped before it could tell, so raise `--max-python-files` or
name the project directly. `agent_scope_truncated: true` says the candidate
list itself is a lower bound — the parse stopped at its cap, so any project
in the part of the tree that was not read is missing from it. Never read
absence from a truncated list as an answer; raise the cap first. On
`agent_scope: "ambiguous"` with a complete parse, `next_actions[]` ranks the
decision first (`kind: "review"`, no command) and then carries one exact
`init --workspace <candidate> --write --json` per candidate — the list
`init --write` publishes when it refuses the same workspace, minus the setup
flags, which `detect` was not asked for and does not invent: if you want
`--ci` or `--agent-instructions`, add them, or take the command from `init`'s
own refusal, which repeats what you asked for. A candidate that already
carries a manifest gets `doctor --config <that manifest> --json` instead —
`init --write` there refuses a file it will not overwrite — or, when you asked
for setup it still owes, an `init` carrying those flags. Every candidate gets
an entry, the workspace root as a `review` rather than a command; the ten-item
cap is on the human summary only. Choosing the project is the only work left. A truncated parse outranks that, in `detect` and in `init`
alike: rank 1 is then the higher-cap rerun and no candidate commands are
offered, because the list they would be built from is a lower bound.
- **`init`** — auto-detects by default. `--ci` writes
`.github/workflows/agents-shipgate.yml`; orthogonal to `--write`. Use
`--minimal` for the pre-v0.6 CHANGE_ME-heavy template.
`--agent-instructions=default` renders the recommended downstream kit
(`AGENTS.md`, `CLAUDE.md`, `.cursor/rules/agents-shipgate.mdc`,
`.claude/commands/shipgate.md`, and `.shipgate/agent-contract.json`).
Use `--ci` to write advisory CI. `--agent-instructions=all` means every
supported target. A comma-separated subset can name any target:
`agents-md,claude-md,cursor,claude-command,local-contract,codex-skill,claude-code-skill,pr-template`.
Combined with `--write`, managed-block hosts are idempotently updated and
full-file / skill-bundle targets use safe-update checks. The `codex-skill` and
`claude-code-skill` targets remain explicit opt-ins and write multi-file skill
bundles under `.agents/skills/agents-shipgate/` and
`.claude/skills/agents-shipgate/` respectively. Strict CI and baselines remain
opt-in human decisions; generated CI stays advisory by default.
`--write` **refuses** a workspace whose manifest scope is unresolved —
`manifest_status: "refused_unresolved_scope"`, exit `2`, nothing written at
all (no manifest, no workflow, no snippets, no `.gitignore` block) — rather
than adopting the first `Agent(name=…)` literal it parsed for a manifest that
would cover unrelated agents. That covers both `agent_scope` values that are
not `"single"`. Re-run with `--workspace` pointed at one of
`auto_detected.agent_project_candidates[].path` (the emitted
`next_actions[]` commands carry the setup flags you passed);
`--allow-unresolved-scope` overrides when one agent surface genuinely spans
the workspace, and `--minimal` (which adopts no detected name or tool
surface) is never scope-gated. With `--ci`, the workflow is written at the
repository root with a repo-relative `config:` — GitHub loads workflows from
nowhere else — so a scoped adoption still gets a gate that runs.
- **`scan --suggest-patches`** — attaches Patch objects to every active
finding. `Finding.patches` is absent without the flag.
- **`apply-patches`** — file-grouped, dry-run by default. Containment-
checked against `report.manifest_dir`. v0.6 default `--confidence high`
applies only manifest stale-removals; scope-coverage appends require
`--confidence medium`. Trace approval/confirmation findings are
always `ManualPatch` — never auto-applied (flipping the trace patches
the evidence, not the agent's runtime gate).
---
## Agent mode
Every command supports JSON output for programmatic consumption:
```bash
agents-shipgate detect --workspace . --json
agents-shipgate preflight --workspace . --plan - --json
agents-shipgate init --workspace . --write --json
agents-shipgate scan -c shipgate.yaml # already produces report.json
agents-shipgate apply-patches --from agents-shipgate-reports/report.json --json
agents-shipgate doctor --json
agents-shipgate contract --json
agents-shipgate explain SHIP-POLICY-APPROVAL-MISSING --json
agents-shipgate list-checks --json
agents-shipgate self-check --json
agents-shipgate fixture list --json
```
Errors carry a structured `next_action` (single string, back-compat) and `next_actions` (ranked list) when agent mode is active. Agent mode auto-enables inside a known coding-agent harness (Claude Code exports `CLAUDECODE=1`, Cursor `CURSOR_TRACE_ID`); set `AGENTS_SHIPGATE_AGENT_MODE=1` to force it on elsewhere, or `=0` to force it off:
```bash
$ AGENTS_SHIPGATE_AGENT_MODE=1 agents-shipgate scan -c missing.yaml
Config error: Config file not found: missing.yaml
{"error": "config_error", "message": "...", "next_action": "agents-shipgate detect --workspace . --json", "next_actions": [{"kind": "command", "command": "agents-shipgate detect --workspace . --json", "why": "..."}, {"kind": "command", "command": "agents-shipgate init --workspace . --write", "why": "..."}]}
```
The full set of error kinds emitted in agent mode: `config_error`, `config_already_exists`, `input_parse_error`, `unknown_check_id`, `unknown_fingerprint`, `other_error`, `internal_error`, `malformed_patch`, `environment_error`, `objects_missing`. `unknown_fingerprint` is emitted by `explain-finding` when the fingerprint doesn't match any entry in the supplied report; the payload includes `suggestion` (a close-match fingerprint, when one exists) and `source_report`. `environment_error` is the one kind emitted before Agents Shipgate is running — the interpreter is unsupported, or it cannot import the package or its dependencies — so it carries the `environment` block described below instead of a `control` envelope. `objects_missing` is `diff` refusing, with exit `2`, a partial clone (`git clone --filter=...`) that never fetched the base tree's objects; its `next_actions` are the `git fetch --refetch --no-filter <remote>` commands that hydrate the clone, one per promisor remote its configuration names, or one `review` when no remote can be named. Shipgate never runs that fetch itself.
The machine-readable catalog of error kinds — exit codes, typical causes, additional fields per kind, recovery hints — lives at [`docs/errors.json`](docs/errors.json). Pre-fetch it once and pattern-match the `error` field instead of re-deriving the recovery vocabulary from this prose.
`detect --json` and each `doctor --json` payload also carry `diagnostics: [...]` and `next_actions: [...]` fields. `next_action` (single string) remains the rank-1 action projected to a string; `next_actions` is the ranked list with `kind`, `command|path`, `why`, `expects`, and the structured `executable[]` / `args[]` pair. See [docs/diagnostics.md](docs/diagnostics.md) for the full catalog and schema.
### Which Shipgate answered: `environment`
Every `doctor --json` payload carries an `environment` block, and so does every `doctor` agent-mode error line — including the one where no manifest could be found and no payload is printed at all. Read it before concluding that a fix did not take or that a subcommand does not exist:
| Field | What it answers |
| ----- | --------------- |
| `interpreter` | `executable`, `version`, `minimum_supported`, `supported`. |
| `launcher` | `source` (`console_script` / `module` / `override` / `fallback`, the invocation policy above), `executable[]`, and `console_scripts[]` — each `agents-shipgate` / `shipgate` wrapper found on `PATH` with the interpreter it ultimately runs (the `exec` target when the wrapper is a `#!/bin/sh` trampoline, as `pip` writes for interpreter paths containing spaces), whether that interpreter still exists, and whether it is the running one. `null` when the wrapper names no interpreter — a compiled Windows wrapper, `#!/usr/bin/env python`, or an unrecognised handoff. |
| `import_source` | `package_path`, `root`, and `kind` (`source_checkout` / `installed` / `unknown`) — where the code that just ran came from. |
| `installed_version` / `imported_version` | What `pip` records for this interpreter, and what actually got imported. `null` installed version is normal on a source checkout. |
| `source_tree` | The enclosing Agents Shipgate checkout, if any: `root`, `version`, `launcher`, and `contains_import`. |
| `mismatches[]` | `code`, `severity` (`error` / `warning`), `detail`, and — when one exists — a runnable `command` spelled for this invocation. Empty is the normal state. |
`mismatches[]` codes: `interpreter_unsupported`, `import_outside_source_tree`, `source_tree_version_differs`, `installed_version_differs`, `console_script_interpreter_missing`, `console_script_runs_other_interpreter`. Nothing here executes an interpreter or a console script to find out — a stale wrapper is identified by reading it, because a wrapper that cannot start is exactly the one that cannot report on itself.
### One control vocabulary across the commands
`detect --json`, `init --json`, and each `doctor --json` payload carry a
`control` field holding the same `shipgate.agent_control/v1` envelope that
`verify --format control`, `check --format agent-control-json`, and
`agents-shipgate agent control` emit. Switch on `control.control_state` and
`control.permissions` for the whole adoption walk instead of learning a
different result shape per command; `control.next_action` is the one typed
rank-1 step, and `next_actions[]` beside it keeps the ranked alternatives.
When that step is a file edit, `control.next_action` is
`{"kind": "edit", "path": …, "expects": …, "command": null}` and
**`control.next_action.path` is the file to open** — exact, never normalized.
The kind is setup-only: `verify`, `check`, and `scan` never emit it, and both
schema layers reject it on those operations.
All six commands publish it; what differs is where, because a command that
publishes a control *pointer* names the envelope from that pointer rather than
carrying it on its own result:
| Command | Where the envelope is |
| ------- | --------------------- |
| `detect` | `--json` payload, `control` |
| `init` | `--json` payload, `control` |
| `doctor` | each `--json` payload, `control` |
| `check` | `--format agent-control-json` (the document *is* the envelope) |
| `verify` | `--format control` (same), or `agents-shipgate agent control` after a `--json` run |
| `scan` | `agents-shipgate agent control` after the run |
`scan`'s answer is the one that withholds a verdict: it reports
`decision: null` with a `reason` saying so. A scan pointer binds no
reconfirmable snapshot of the inputs it read, so no artifact in that directory
can show its verdict still describes the workspace. Run `verify` for one that
can.
Read `control.decision_source` before `control.decision`. Setup commands run
before a release decision exists, so they report `setup` and a verdict from
`setup_complete | setup_incomplete | setup_not_applicable`, never a release
verdict; `release_decision` means `report.json`'s
`release_decision.decision` and nothing else. The two cannot be confused: the
published schema requires a setup source to come from `detect`/`init`/`doctor`
and requires those operations to report no other source.
**Setup authorizes nothing.** Every field of `permissions` is `false` on every
setup envelope, no setup envelope binds an artifact or a `current_control_id`,
and `control_state: "complete"` is unreachable for these operations in the
schema. Running `init` successfully is not permission to commit, merge, or
report the task done — only a verifier run can grant that. When a manifest still
holds an unresolved `declared_purpose`, policy, or permission placeholder,
`control_state` is `human_review_required` and the action names the exact file,
line, and field: those are declarations a person makes, and an agent must never
supply them.
`next_action` may be `kind: "edit"` on these commands — a typed coding-agent
step with `path` and `expects` and no command. It appears only on setup output:
`verify`, `check`, and `agent control` cannot return it, and the published schema
rejects it on any other operation. `permissions.edit` is `false` beside it, which
is not a contradiction — a setup route authorizes only its own `next_action`.
`next_action` and `next_actions[0]` are **derived from the same selected route**
as `control.next_action`, so the compact envelope and the ranked list can never
send you to different work. Where the route is human-owned, that list holds
exactly one action and no command: an alternative would be a way around the
obligation.
**Agent-mode error lines from `detect`, `init`, and `doctor` carry
`control` too**
(contract v27). A setup command that could not finish publishes the same
envelope on stderr that it would have published on stdout, so one routing rule
covers both documented streams; `next_action` / `next_actions[]` are unchanged
beside it. Every such envelope reports `decision_source: "setup"`, a `decision`
from the setup vocabulary, `permissions` all false, and never
`control_state: "complete"`.
**Do not use `execution` to tell an error line from an answer** — the `error`
field does that. `execution` says whether the command reached an answer about
the workspace, so an error line carries `"failed"` when it could not (a flag
value it could not parse, discovery it could not bound, a manifest it could not
open) and `"succeeded"` with a non-zero `exit_code` when it did and the answer
is a refusal it can route past (`config_already_exists`, the unresolved-scope
`config_error`). Both authorize nothing.
The one setup line with no `control` is the shared `--workspace` refusal, which
fires before the workspace exists and therefore has no setup subject to
describe: it carries `next_action`/`next_actions` only. Error lines from
`scan`, `verify`, and `check` also carry no `control`: the first two answer
through their control pointer (`agents-shipgate agent control`), and `check`
through `--format agent-control-json`.
Every emitted command names the entry point that started the running process, so it is runnable where it was produced: a console-script run emits `agents-shipgate …`, and a `python -m agents_shipgate` run emits `<sys.executable> -m agents_shipgate …`. Set `AGENTS_SHIPGATE_CLI` to name the entry point explicitly; it wins over detection. **On `next_actions[]`, run `[*executable, *args]` (contract v23+) rather than parsing `command`** — it needs no shell and is computed from `command`, so it cannot disagree with it; it is omitted, never `null`, when the command has no faithful argv form. The operational control contracts (`control.next_action`, `allowed_next_commands`, verifier repairs) carry the string only: recover argv there with `shlex.split(command)`, which is exact on every platform because every emitted command is POSIX-rendered. Never use `shell=True`, and do not paste `command` into `cmd.exe` or PowerShell. Durable artifacts (`report.json`, `packet.*`) stay canonical so that same inputs still produce the same report. See [docs/diagnostics.md](docs/diagnostics.md#invocation-policy).
### Doctor behavior change for unresolved tool_sources
When a required `tool_sources[].path` does not resolve under the manifest directory (file missing OR resolves outside the manifest dir):
- `agents-shipgate doctor --json` exits **0** with a `SHIP-DIAG-MISSING-SOURCE-FILE` diagnostic and an `unresolved_sources: [{id, declared_path, line, reason}]` field in the payload, so an agent can route to a fix without parsing the error message. `reason` is `"missing"` or `"outside_manifest_dir"`.
- `agents-shipgate doctor` (no `--json`) prints the same `unresolved_sources` + diagnostic block in human-readable form and **exits 3**, preserving the pre-feature loud failure for interactive users.
- `agents-shipgate scan` is unchanged — it still raises `InputParseError(3)` regardless of `--json`. Once you're past doctor, missing sources are real scan failures. One precondition enforces this for every source type before any adapter runs, off the resolver doctor uses, so the answer does not depend on which reader the source would have gone to. `optional: true` sources are not covered: they keep their source warning and recovery evidence, and the scan completes.
- `verify` applies the same precondition to each tree it scans. A base commit whose manifest declares a path that tree does not contain yields `base_status: "scan_failed"` with the reason in `base_notes` and no capability delta; the head gate is unchanged.
### Missing vs invalid manifests
`config_error` covers two distinct shapes — agent-mode emits a different rank-1 action for each:
- **Missing**: file does not exist → `SHIP-DIAG-MISSING-MANIFEST`, rank-1 is `agents-shipgate detect --workspace <dir> --json` (then `init --write`).
- **Invalid**: file exists but the loader rejected it (invalid YAML, schema validation, unsupported version) → `SHIP-DIAG-INVALID-MANIFEST`, rank-1 is `edit <path>` with the loader error in `why`. Do **not** re-run `init` — it refuses to overwrite an existing file.
---
## When to use this tool
Use it when the user is:
- Reviewing a pull request that changes a coding agent's declared configuration (`.claude/settings.json`, `.mcp.json`, hooks, Codex, Cursor or VS Code MCP config, GitHub workflow permissions): run `agents-shipgate diff`, which needs no `shipgate.yaml`, baseline or skill
- Writing or modifying an AI agent that calls tools (MCP servers, OpenAPI APIs, `@function_tool`-decorated Python, Google ADK, LangChain/LangGraph, CrewAI, n8n, Conductor OSS)
- Publishing or modifying a Codex plugin package or marketplace entry that declares skills, apps, MCP servers, or hooks
- Adding a new tool, scope, or policy to an existing agent
- Preparing an agent for production promotion
- Wiring a release gate into PR CI
Do NOT use it for:
- General code linting (ruff, eslint, etc.)
- Runtime monitoring (use observability/tracing)
- Behavior testing (use evals)
- Stopping a tool call in flight (use a gateway or guardrail)
### Should I run Shipgate on this PR?
| Trigger in this PR | Run Shipgate? |
|---|---|
| Adds/changes MCP exports, OpenAPI specs, or `tools/*openai*tools*.json` | Yes |
| Adds/changes an MCP tool registration written in TypeScript, Go or Python source (`static toolName`, `.registerTool(`, `MustTool(`, `NewTool(`, `mcp.Tool{`, `@mcp.tool`) | Yes |
| Adds/changes Codex repo config, hooks, or permission profiles | Yes |
| Adds/changes coding-agent host config, hooks, permissions, MCP servers, or workflows | Yes |
| Adds/changes Codex plugin manifests, marketplace files, `.app.json`, `.mcp.json`, or `SKILL.md` files | Yes |
| Adds/changes `@function_tool`/`@tool` decorators (LangChain, CrewAI, OpenAI Agents SDK) | Yes |
| Adds/changes a Google ADK `Agent`/`LlmAgent` `tools=[...]` list | Yes |
| Adds/changes n8n workflow JSON, credential stubs, or n8n tool inventories | Yes |
| Adds/changes Conductor OSS workflow JSON with AI/MCP tasks | Yes |
| Edits `prompts/`, `policies/`, or `permissions.scopes` in `shipgate.yaml` | Yes |
| Adds/edits `.github/workflows/agents-shipgate.yml` or related CI | Yes |
| Pure read-only doc/test changes with no manifest impact | Skip |
| Refactor with no behavior change to tools or policies | Skip (or dry-run only) |
One known gap in the Google ADK row: an edit that *modifies* a tools list on the `Agent` alias (rather than `LlmAgent`) is not matched, because a bare `Agent(..., tools=[...])` hunk with no ADK import in it cannot be distinguished from CrewAI's by diff text alone. `LlmAgent` changes and whole-file additions in either spelling are covered.
`prompts/` and `policies/` in that row match at any depth and case-insensitively: an edit under `services/foo/policies/` or `enterprise/lib/captain/Prompts/` routes exactly like a repo-root one. That is parity with the verifier, whose trust-root classification has always read those two surfaces as `**/policies/**` and `**/prompts/**` and has always tolerated the case variant a case-insensitive filesystem resolves to the canonical name. The catalog's `glob` and `none_match_glob` predicates match the same way, so a path cannot be a trust root to the verifier and a `no_match` to the router; the Tier B checks (`SHIP-VERIFY-POLICY-WEAKENED`, `SHIP-VERIFY-CI-GATE-REMOVED`, the retained non-emitting agent-instruction weakening ID, trigger-catalog drift) select their changed files the same way too, so a case variant cannot be a trust root in Tier A and invisible to the specialized check that carries the severity. `every_file_matches` is deliberately the exception and stays case-sensitive: it is the docs-only rule's own classifier, and `skip_shipgate` beats `run_shipgate`, so folding it would read `src/TEST_agent.py` — a production module on a case-sensitive filesystem — as a test file and skip a PR that adds a tool beside it. The rule is to fold the predicates that can only add evaluation, never the one that can subtract it. The three surfaces that copy this routing — the pre-commit `files:` regex, the `.cursor/rules/agents-shipgate.mdc` activation globs, and the documented copy-paste hook snippets — follow, so a nested governance edit also activates the host instructions and stages the local hook.
`shipgate.yaml` matches at any depth for the same reason. A monorepo keeps one manifest per project directory, so an edit to `services/refund/shipgate.yaml` — the file that declares that project's agent, purpose, and tool surface — routes exactly like a root-level one; a root-only rule reported it as `no_match`. A nested manifest is also an opt-in: `verify --preview` treats the changed project's own `shipgate.yaml` as the repo-already-adopted signal and routes verification to that manifest rather than to a root one governing a different boundary.
Two implicit triggers also fire even when no row above matches:
- **Repo already opted in (shipgate.yaml present in the workspace)** — run on every PR; the manifest's existence is the opt-in.
- **(Optional) Refactor or framework upgrade that may shift the extracted tool surface** — dry-run only; bumping `openai-agents`, `langchain`, `crewai`, `google-adk`, or `conductor-oss` can change static extraction even without app-code edits. The rule needs both halves of that evidence: the package token **and** a changed dependency manifest. A bare token — a README that mentions `google-adk`, a sample that imports it — is not a version bump and no longer routes as one. The manifest set is `DEPENDENCY_MANIFEST_GLOBS` (`agents_shipgate.core.dependency_manifests`), projected into `triggers.json` and pinned by the contract test; it covers Python (`pyproject.toml`, `requirements*.{txt,in}`, `constraints*.{txt,in}`, `poetry.lock`, `uv.lock`, `pdm.lock`, `pylock*.toml`, `Pipfile*`, conda), Node (`package.json`, npm/pnpm/yarn/bun locks), and the JVM (`pom.xml`, Gradle build files and version catalogs).
A machine-readable mirror of these triggers lives at [`docs/triggers.json`](docs/triggers.json). Coding agents that have not yet adopted Shipgate can fetch the file (raw URL: `https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/triggers.json`), apply the rules to a PR diff, and decide whether to propose `agents-shipgate detect`. The catalog is stable for `0.x` and pinned by the public-surface contract test against this prose table — if you change a row above, update `triggers.json` in the same commit. To evaluate a diff locally, use the first-class `trigger` subcommand:
```bash
# From a list of changed paths (and optional diff body for diff_contains rules):
agents-shipgate trigger --changed-files changed.txt --diff pr.diff --json
# Or straight from git (the ONLY mode that shells out to git):
agents-shipgate trigger --base origin/main --head HEAD --json
agents-shipgate trigger --list-rules --json
```
The command emits a stable JSON verdict: `should_run` (alias of `run_shipgate`), `force_run`, `dry_run_recommended`, `skip_reason`, `matched_rules`, `changed_files`, `diff_tokens`, plus `input_status`, `evaluation_status`, and `surface_exclusions` (catalog schema `0.4`). A *skip* verdict is withheld in two states, and `should_run`/`run_shipgate`/`skip`/`skip_reason` are `null` in both — never read `null` as `false`. `evaluation_status: not_evaluated` means the diff could not be read in full, so an unread diff is never reported as `no_match`; `next_action.kind` is `input_required`. `evaluation_status: unclassified` means the diff *was* read in full and no rule classified some or all of the changed files — a fact about the catalog, not about the PR — so the skip is withheld and `next_action` routes forward to the scan; `surface_exclusions.entries[]` names the files nobody classified. A matched `run_shipgate`/`force_run` rule also overrides the stop block, because a capability match in the diff is evidence the whole-workspace negative did not account for; read `stop_conditions_terminal` to see whether a fired stop actually decided. A *run* verdict is still published, because rule matching is monotone — evidence that already matched cannot be un-matched by the bytes that are missing — and it arrives as `evaluation_status: evaluated` with `should_run: true`. That evidence may be a rule matched on the change set or `force_run` from a manifest that is present regardless of the diff, so read `matched_rules` before attributing it. Branch on `evaluation_status`, not on `should_run` alone. The developer entry point `python -m agents_shipgate.triggers shipgate.yaml prompts/refund.md` is preserved.
**Stop conditions.** Stop and do not run `init` only when **all** of these hold:
- `agents-shipgate detect --json` returns `is_agent_project: false`, AND
- `suggested_sources` is empty (no MCP/OpenAPI hits flowing in as `mcp` or `openapi`), AND
- `codex_plugin_candidates` is empty (no Codex plugin package or marketplace hits), AND
- `host_boundary_candidates` is empty (no recognized host config paths), AND
- `host_discovery_incomplete_paths` is empty (no unresolved traversal that can hide host config), AND
- `python_parse_truncated` is `false` — every negative above is a claim about the whole workspace, and a run whose Python parse stopped at its cap read only part of one, AND (this is the raw parse bit, not `agent_scope_truncated`: that one additionally requires more than one candidate scope, so a single-scope workspace whose only agent sits past the cap leaves it false)
- no `shipgate.yaml` already exists in the workspace, AND
- the user did not explicitly request a scan.
A `detect` payload that does not carry every one of those keys leaves the block unevaluable: `trigger` reports `stop_conditions_evaluated: false` and infers no stop. Re-run `detect` with the current CLI rather than reading an absent key as `false`.
Otherwise follow `control.next_action`. Host-only repositories route to
`audit --host` without a manifest; `host_boundary_candidates[].file_type`
describes only the observed pathname, never parsed permission evidence.
Configuration paths that are directories, and unrecognized links that could
conceal nested configuration, route to inspection. `init` (including `--ci`
and agent-instruction options) and `bootstrap` hand off without writing setup
files on this host-only route. Explicit `init --minimal` retains its template
behavior. MCP/OpenAPI tool-surface and Codex plugin repositories still use
`init`; their candidates never become host grants. The trigger table above is
the authoritative go/no-go.
---
## Five common agent tasks
### Task 1 · Add the gate to an existing repo
```bash
pipx install agents-shipgate
agents-shipgate init --workspace . --write --json
# resolve the placeholders init reports, by owner (below), then:
agents-shipgate scan -c shipgate.yaml
```
`init --json` reports the placeholders two ways, and only one of them routes. **`placeholders[]` is a location list** — each entry is `path`, `current` and `line`, and carries no owner. **`control.next_action.actor` routes the turn**, not individual fields:
- `actor: "human"` — *any* human-owned value is still unresolved. `permissions.edit` is `false`: surface the whole thing and stop. Do not edit the manifest, and do not split the array.
- `actor: "coding_agent"` — every human-owned value has been supplied and only fields you own are left. `why` names the one to replace.
**Never infer ownership from absence in `why`.** That sentence is fitted to the envelope's prose budget: with seven unresolved declarations it names three paths and then `and 4 more in placeholders[]`, and the four it dropped are human-owned too. It tells you where to start, not what is yours. The rule behind the split:
- **You own** what you can read out of the repository — `agent.name`, `project.name`, the `tool_sources[]` rows. Escalating these stops a turn for work you own.
- **A person owns** every *declaration*: purpose, prohibited actions, effect, authority, binding, approval, confirmation, idempotency, safeguards, accepted debt and its owner/reason/expiry, and the manifest blocks that are declarations end to end (`action_surface`, `permissions`, `policies`, `agent_bindings`, `tool_identity`, `checks`, `baseline`, `human_ack`, `risk_overrides`, `validation`, `organization`). While one is unresolved, `init` returns `control.next_action.actor: "human"` and `permissions.edit: false`. These values must be supplied by a human, because Shipgate never invents a declaration nobody made — a purpose or authority claim you lifted out of a prompt or README is a declaration nobody made, and the engine will treat it as evidence.
Those names are examples of the rule, not the rule. `placeholders[]` is authoritative; when it disagrees with this list, it is right.
### Task 2 · Read findings programmatically
Always parse `agents-shipgate-reports/report.json`, not the markdown.
The canonical field list — `release_decision`, `capability_facts` / `declared_intentions` / `misalignments` / `release_consequence` / `suggested_scenarios`, `tool_surface_facts` / `tool_surface_diff`, and `action_surface_facts` / `action_surface_diff` — lives in [`docs/agent-contract-current.md`](docs/agent-contract-current.md#read-these-first-for-release-gating). It updates first when the contract bumps; this file links to it instead of restating the field set.
Other stable top-level fields (full history and semantics live in
[`docs/agent-contract-current.md`](docs/agent-contract-current.md); never
restate version archaeology here):
- `summary.{critical_count, high_count, medium_count, status}` (legacy,
baseline-blind — do not gate on it)
- `findings[].{id, fingerprint, check_id, severity, tool_name, evidence, recommendation, suppressed}`
- `findings[].{autofix_safe, requires_human_review, suggested_patch_kind, docs_url, provenance_kind, blocks_release}`
- `findings[].policy_routing` (policy-pack owner/reviewer/approval routing metadata only; non-enforcing and not part of `evidence`)
- `findings[].patches[]` (only when scan ran with `--suggest-patches`)
- `baseline.{matched_count, new_count, resolved_count}` · root-reachable `tool_inventory[]` · full `tool_catalog[]` · `codex_plugin_surface`
- `action_surface_facts` / `action_surface_diff`
- `release_decision.evidence_coverage.{binding_coverage,semantic_coverage,evidence_gaps}`
- Audit envelopes: `release_decision.contribution_rules[]`, `policy_audit`,
`privacy_audit`, `heuristics_filter` — explanatory, never a second gate
The current schema is [`docs/report-schema.v1.0.json`](docs/report-schema.v1.0.json), frozen at `1.0`, superseding `0.43`. Emitted reports carry `report_schema_version: "1.0"`; `surface_exclusions` records every subject a stage removed from the analysed surface and whether the release decision saw it, typed predicate support prevents heuristic evidence from being upgraded by policy severity or block metadata, and verify-native reports bind the content-addressed request and decision. A `passed` result requires a complete static binding graph from its entry points plus complete, conflict-free identity, effect, authority, and applicable-policy evidence for every reachable action. Every release decision explicitly carries `static_analysis_only: true`, `runtime_behavior_verified: false`, and `static_verdict_disclaimer`; packet §1 mirrors them. Binding, semantic, and policy-applicability gaps are not Findings and cannot be suppressed or baselined. See [`docs/passed-verdict-contract.md`](docs/passed-verdict-contract.md), [`docs/verification-reproducibility.md`](docs/verification-reproducibility.md), and [`docs/agent-contract-current.md`](docs/agent-contract-current.md). v0.43, the last pre-freeze version, remains frozen at [`docs/report-schema.v0.43.json`](docs/report-schema.v0.43.json). `1.x` is additive-only over `1.0`; a change that cannot be expressed additively needs `2.0`. Historical `0.x` reports stay published for reading archived artifacts, but are no longer accepted as engine input — see [`docs/report-1-0-contract.md`](docs/report-1-0-contract.md).
**Release gating signal**: prefer `release_decision.decision` (`"blocked" | "review_required" | "insufficient_evidence" | "passed"`) over `summary.status`. The new field is **baseline-aware** — a baseline-matched critical surfaces in `release_decision.review_items` (accepted debt), not `release_decision.blockers`. `summary.status` stays baseline-blind for v0.7 compatibility, so a baseline-matched-only critical produces both `summary.status = "release_blockers_detected"` AND `release_decision.decision = "review_required"` (intentional divergence — see [STABILITY.md](STABILITY.md#release_decisiondecision-vs-summarystatus)). `insufficient_evidence` (added v0.14) signals that the scan saw too many low-confidence tools or source-loader warnings to be trustworthy; consumers that switch on the enum must fall back to `review_required` for unknown future values.
For a step-by-step reader's primer with anti-patterns and concrete code rewrites, see [`docs/report-reading-for-agents.md`](docs/report-reading-for-agents.md).
### Task 3 · Suppress a finding with a reason
```yaml
# shipgate.yaml
checks:
ignore:
- check_id: SHIP-DOC-MISSING-DESCRIPTION
tool: legacy_search
reason: tool deprecated 2026-Q2
```
`reason` is required and non-empty; the manifest fails validation otherwise.
Suppressions apply to Findings only. They cannot accept, hide, or close a
semantic evidence gap.
### Task 4 · Save a baseline before enabling strict CI
```bash
agents-shipgate baseline save -c shipgate.yaml --out .agents-shipgate/baseline.json \
--owner <human> --reason "<why accepted>" --expires <YYYY-MM-DD>
```
`--owner`/`--reason`/`--expires` (v0.13+) record who accepted the debt, why,
and the review-by date on newly-accepted entries. They are human-declared
values: an agent must ask the user, never invent them, and blank values are
rejected. `--apply-to-existing` fills the fields into existing entries that
lack them without overwriting previously-set values.
Then in CI:
```bash
agents-shipgate scan -c shipgate.yaml \
--baseline .agents-shipgate/baseline.json \
--ci-mode strict --fail-on critical,high
```
Strict mode fails CI only on **new** findings (those not in the baseline).
`agents-shipgate baseline status --json` reports accepted-debt aging
(owner, age, expiry); with `--require-owner` / `--require-expiry` /
`--max-age-days N` it exits `20` on violations (advisory exit `0` without
gate flags) — parse `violations[]` from the JSON, then route to a human:
acknowledging debt is a human decision.
### Task 5 · Explain a check or a specific finding
For static catalog metadata about a check ID (rationale, fires-when, recommendation):
```bash
agents-shipgate explain SHIP-POLICY-APPROVAL-MISSING --json
```
Returns the full `CheckMetadata` with `id`, `category`, `default_severity`, `description`, `rationale`, `fires_when`, `evidence_fields`, `recommendation`.
For a contextual explanation tied to a specific finding from a real scan (catalog metadata + the finding's evidence + a 3–5 sentence templated prose summary):
```bash
agents-shipgate explain-finding fp_<fingerprint> \
--from agents-shipgate-reports/report.json --json
```
Returns the canonical Finding fields plus `metadata` (CheckMetadata for the check_id) and `explanation` — a deterministic prose summary suitable for direct quotation in a PR comment or chat reply. The companion prompt is [`prompts/explain-finding-to-user.md`](prompts/explain-finding-to-user.md).
---
## Agent FAQ
### Where is the manifest schema?
Use [`docs/manifest-v0.1.json`](docs/manifest-v0.1.json) for machine
validation and [`docs/manifest-v0.1.md`](docs/manifest-v0.1.md) for prose.
### Where is the report schema?
Parse `agents-shipgate-reports/report.json` and validate against
[`docs/report-schema.v1.0.json`](docs/report-schema.v1.0.json) (current, frozen at `1.0`; it supersedes `0.43`).
Every published schema stays published, so an archived report
(`report_schema_version: "0.10"`) still validates against the frozen
[`docs/report-schema.v0.10.json`](docs/report-schema.v0.10.json) — but a
pre-freeze report is no longer accepted as *input* to this engine. It is
refused by name with a regeneration route rather than reinterpreted under the
current model's defaults; see [`docs/report-1-0-contract.md`](docs/report-1-0-contract.md).
Do not scrape Markdown when JSON is available.
### How do I add a new check?
Follow [`docs/architecture.md`](docs/architecture.md) and update the check
registry, tests, [`docs/checks.md`](docs/checks.md), and
[`docs/checks.json`](docs/checks.json). Check IDs must not change after
publication.
### How do I add a new framework adapter?
Start with [`docs/framework-adapter-checklist.md`](docs/framework-adapter-checklist.md).
Adapters must be static by default: no user-code import, no network access, no
agent execution.
### Where are runnable examples?
Use [`samples/README.md`](samples/README.md) for sample agents and
[`docs/examples.md`](docs/examples.md) for a narrative overview. The fastest
fixture is `agents-shipgate fixture run support_refund_agent`.
### What vocabulary should I use in user-facing copy?
Use the [canonical names](#canonical-names) table above and the website
glossary: https://threemoonslab.com/glossary/.
---
## Schemas
For the short, current statement of "which fields to read", see [`docs/agent-contract-current.md`](docs/agent-contract-current.md). It is the single file that updates first when the contract bumps; the table below lists the underlying schemas.
| What | Path | Stable |
|---|---|---|
| Manifest schema | [`docs/manifest-v0.1.json`](docs/manifest-v0.1.json) | `0.1` |
| Report schema (current) | [`docs/report-schema.v1.0.json`](docs/report-schema.v1.0.json) | `1.0` |
| Report schema (v0.43 frozen reference) | [`docs/report-schema.v0.43.json`](docs/report-schema.v0.43.json) | `0.43` |
| Report schema (v0.38 frozen reference) | [`docs/report-schema.v0.38.json`](docs/report-schema.v0.38.json) | `0.38` |
| Report schema (v0.37 frozen reference) | [`docs/report-schema.v0.37.json`](docs/report-schema.v0.37.json) | `0.37` |
| Report schema (v0.34 frozen reference) | [`docs/report-schema.v0.34.json`](docs/report-schema.v0.34.json) | `0.34` |
| Report schema (v0.33 frozen reference) | [`docs/report-schema.v0.33.json`](docs/report-schema.v0.33.json) | `0.33` |
| Report schema (v0.32 frozen reference) | [`docs/report-schema.v0.32.json`](docs/report-schema.v0.32.json) | `0.32` |
| Report schema (v0.31 frozen reference) | [`docs/report-schema.v0.31.json`](docs/report-schema.v0.31.json) | `0.31` |
| Report schema (v0.30 frozen reference) | [`docs/report-schema.v0.30.json`](docs/report-schema.v0.30.json) | `0.30` |
| Report schema (v0.29 frozen reference) | [`docs/report-schema.v0.29.json`](docs/report-schema.v0.29.json) | `0.29` |
| Report schema (v0.28 frozen reference) | [`docs/report-schema.v0.28.json`](docs/report-schema.v0.28.json) | `0.28` |
| Report schema (v0.27 frozen reference) | [`docs/report-schema.v0.27.json`](docs/report-schema.v0.27.json) | `0.27` |
| Report schema (v0.26 frozen reference) | [`docs/report-schema.v0.26.json`](docs/report-schema.v0.26.json) | `0.26` |
| Report schema (v0.25 frozen reference) | [`docs/report-schema.v0.25.json`](docs/report-schema.v0.25.json) | `0.25` |
| Verify-run schema | [`docs/verify-run-schema.v3.json`](docs/verify-run-schema.v3.json) | `shipgate.verify_run/v3` |
| Verification plan schema | [`docs/verification-plan-schema.v1.json`](docs/verification-plan-schema.v1.json) | `shipgate.verification_plan/v1` |
| Verification unit result schema | [`docs/verification-unit-result-schema.v1.json`](docs/verification-unit-result-schema.v1.json) | `shipgate.verification_unit_result/v1` |
| Verification artifact manifest schema | [`docs/verification-artifact-manifest-schema.v1.json`](docs/verification-artifact-manifest-schema.v1.json) | `shipgate.verification_artifact_manifest/v1` |
| Verification receipt schema | [`docs/verification-receipt-schema.v1.json`](docs/verification-receipt-schema.v1.json) | `shipgate.verification_receipt/v1` |
| Agent handoff schema | [`docs/agent-handoff-schema.v5.json`](docs/agent-handoff-schema.v5.json) | `shipgate.agent_handoff/v5` |
| Agent boundary result schema | [`docs/agent-boundary-result-schema.v1.json`](docs/agent-boundary-result-schema.v1.json) | `shipgate.agent_boundary_result/v1` |
| Codex boundary result schema (deprecated frozen projection) | [`docs/codex-boundary-result-schema.v2.json`](docs/codex-boundary-result-schema.v2.json) | `shipgate.codex_boundary_result/v2` |
| Report schema (v0.24 frozen reference) | [`docs/report-schema.v0.24.json`](docs/report-schema.v0.24.json) | `0.24` |
| Report schema (v0.23 frozen reference) | [`docs/report-schema.v0.23.json`](docs/report-schema.v0.23.json) | `0.23` |
| Report schema (v0.22 frozen reference) | [`docs/report-schema.v0.22.json`](docs/report-schema.v0.22.json) | `0.22` |
| Report schema (v0.21 frozen reference) | [`docs/report-schema.v0.21.json`](docs/report-schema.v0.21.json) | `0.21` |
| Report schema (v0.20 frozen reference) | [`docs/report-schema.v0.20.json`](docs/report-schema.v0.20.json) | `0.20` |
| Report schema (v0.19 frozen reference) | [`docs/report-schema.v0.19.json`](docs/report-schema.v0.19.json) | `0.19` |
| Report schema (v0.18 frozen reference) | [`docs/report-schema.v0.18.json`](docs/report-schema.v0.18.json) | `0.18` |
| Report schema (v0.17 frozen reference) | [`docs/report-schema.v0.17.json`](docs/report-schema.v0.17.json) | `0.17` |
| Report schema (v0.16 frozen reference) | [`docs/report-schema.v0.16.json`](docs/report-schema.v0.16.json) | `0.16` |
| Report schema (v0.15 frozen reference) | [`docs/report-schema.v0.15.json`](docs/report-schema.v0.15.json) | `0.15` |
| Report schema (v0.14 frozen reference) | [`docs/report-schema.v0.14.json`](docs/report-schema.v0.14.json) | `0.14` |
| Report schema (v0.13 frozen reference) | [`docs/report-schema.v0.13.json`](docs/report-schema.v0.13.json) | `0.13` |
| Report schema (v0.12 frozen reference) | [`docs/report-schema.v0.12.json`](docs/report-schema.v0.12.json) | `0.12` |
| Report schema (v0.11 frozen reference) | [`docs/report-schema.v0.11.json`](docs/report-schema.v0.11.json) | `0.11` |
| Report schema (v0.10 frozen reference) | [`docs/report-schema.v0.10.json`](docs/report-schema.v0.10.json) | `0.10` |
| Report schema (v0.9 frozen reference) | [`docs/report-schema.v0.9.json`](docs/report-schema.v0.9.json) | `0.9` |
| Report schema (v0.8 frozen reference) | [`docs/report-schema.v0.8.json`](docs/report-schema.v0.8.json) | `0.8` |
| Report schema (v0.7 frozen reference) | [`docs/report-schema.v0.7.json`](docs/report-schema.v0.7.json) | `0.7` |
| Report schema (v0.6 frozen reference) | [`docs/report-schema.v0.6.json`](docs/report-schema.v0.6.json) | `0.6` |
| Packet schema (Release Evidence Packet, latest) | [`docs/packet-schema.v0.18.json`](docs/packet-schema.v0.18.json) | `0.18` |
| Agent result schema (current) | [`docs/agent-result-schema.v3.json`](docs/agent-result-schema.v3.json) | `agent_result_v3` |
| Verifier schema (current) | [`docs/verifier-schema.v0.21.json`](docs/verifier-schema.v0.21.json) | `0.21` |
| Agent handoff schema (current) | [`docs/agent-handoff-schema.v9.json`](docs/agent-handoff-schema.v9.json) | `shipgate.agent_handoff/v9` |
| Preflight schema (current) | [`docs/preflight-schema.v0.5.json`](docs/preflight-schema.v0.5.json) | `0.5` |
| Host-grants inventory schema | [`docs/host-grants-inventory-schema.v0.7.json`](docs/host-grants-inventory-schema.v0.7.json) | `0.7` |
| Host-grants baseline schema | [`docs/host-grants-baseline-schema.v0.7.json`](docs/host-grants-baseline-schema.v0.7.json) | `0.7` |
| Host-grants drift schema | [`docs/host-grants-drift-schema.v0.7.json`](docs/host-grants-drift-schema.v0.7.json) | `0.7` |
| Capability standard | [`docs/capability-standard.md`](docs/capability-standard.md) | `0.5` |
| Capability lock schema | [`docs/capability-lock-schema.v0.8.json`](docs/capability-lock-schema.v0.8.json) | `0.8` |
| Capability lock diff schema | [`docs/capability-lock-diff-schema.v0.9.json`](docs/capability-lock-diff-schema.v0.9.json) | `0.9` |
| Capability payload schema (frozen) | [`docs/capability-payload-schema.v1.json`](docs/capability-payload-schema.v1.json) | `shipgate.capability_payload/v1` |
| Capability delta attestation (frozen) | [`docs/capability-delta-attestation-schema.v1.json`](docs/capability-delta-attestation-schema.v1.json) | `shipgate.capability_delta_attestation/v1` |
| Governance benchmark catalog schema | [`docs/governance-benchmark-catalog-schema.v0.2.json`](docs/governance-benchmark-catalog-schema.v0.2.json) | `0.2` |
| Governance benchmark result schema | [`docs/governance-benchmark-result-schema.v0.2.json`](docs/governance-benchmark-result-schema.v0.2.json) | `0.2` |
| Check catalog | [`docs/checks.json`](docs/checks.json) | regenerated each release |
| Anti-patterns (what NOT to write) | [`samples/_anti_patterns/`](samples/_anti_patterns/) | reference |
| Minimal manifest example | [`docs/manifest-v0.1.example.minimal.yaml`](docs/manifest-v0.1.example.minimal.yaml) | reference |
For VS Code / Cursor live YAML validation, every manifest produced by `init` includes:
```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/manifest-v0.1.json
```
---
## Stable command surface
Promised to not break in `0.x` minor versions. See [STABILITY.md](STABILITY.md) for the full contract.
| Command | Stable flags |
|---|---|
| `agents-shipgate scan` | `-c`, `--out`, `--format`, `--ci-mode`, `--fail-on`, `--baseline`, `--diff-from`, `--changed-files`, `--no-plugins`, `--no-heuristics`, `--verbose`, `--packet`/`--no-packet`, `--packet-format` |
| `agents-shipgate evidence-packet` | `--from`, `--out`, `--format`, `--json` |
| `agents-shipgate init` | `--workspace`, `--write`, `--json` |
| `agents-shipgate doctor` | `-c`, `--workspace`, `--json`, `--verbose` |
| `agents-shipgate contract` | `--json` |
| `agents-shipgate explain` | `<check_id>`, `--no-plugins`, `--json` |
| `agents-shipgate explain-finding` | `<fingerprint>`, `--from`, `--no-plugins`, `--json` |
| `agents-shipgate findings` | `--from`, `--provenance-kind`, `--include-suppressed`, `--json` |
| `agents-shipgate trigger` | `--workspace`, `--changed-files`, `--diff`, `--base`, `--head`, `--manifest-present`/`--no-manifest-present`, `--user-requested`, `--list-rules`, `--json` |
| `agents-shipgate bootstrap` | `--workspace`, `--confidence`, `--no-ci`, `--no-apply`, `--json` |
| `agents-shipgate list-checks` | `--json`, `--no-plugins` |
| `agents-shipgate baseline save` | `-c`, `--out`, `--owner`, `--reason`, `--expires`, `--apply-to-existing` |
| `agents-shipgate baseline status` | `--baseline`, `--as-of`, `--require-owner`, `--require-expiry`, `--max-age-days`, `--json` (gate flags exit `20` on violations) |
| `agents-shipgate fixture` | `list`, `run`, `copy`, `verify` |
| `agents-shipgate self-check` | `--json` |
| `agents-shipgate agent handoff` | `--from`, `--report`, `--verify-run`, `--out`, `--json` |
Newer commands (stable intent, flags may still evolve):
| Command | Purpose |
|---|---|
| `shipgate audit --host` | Zero-config, read-only static inventory of coding-agent host grants with per-host coverage; deterministic repository scope by default, optional `--scope local-static`. Works without `shipgate.yaml`. |
| `agents-shipgate mcp-serve` | Local read-only stdio MCP server (`[mcp]` extra) exposing `shipgate.check`, `shipgate.preflight`, `shipgate.explain`, `shipgate.capabilities`, and `shipgate.handoff`. See [`docs/mcp-server.md`](docs/mcp-server.md). |
| `agents-shipgate org status` | Local organization governance projection over exception hygiene, policy-pack pins, host-grant drift, and registry readiness; `--json` available and governance violations exit `20`. |
| `agents-shipgate registry` | `ingest --attestation <file>` / `query` / `report --bypass` — local capability-release ledger over attestations. |
| `agents-shipgate install-hooks` | Claude Code hooks: PreToolUse trust-root boundary (`ask`/`deny`), PostToolUse trigger nudge, Stop verify. |
### Release Evidence Packet (v0.16)
`scan` emits a reviewer-shaped Release Evidence Packet alongside
`report.{md,json}` by default; outputs land at
`agents-shipgate-reports/packet.{md,json,html}` (and `packet.pdf` with the
`[pdf]` extras). The packet is derived from the report JSON, is a local
artifact only, and never gates — §1's verdict derives from
`release_decision.decision` alone, and §10 always lists what the packet did
NOT prove. Use `--no-packet` / `--packet-format` on `scan`, and
`agents-shipgate evidence-packet --from <packet.json|report.json>` to
re-render. The full packet contract (fixed sections, disclaimers,
`evidence_matrix` rules) lives in
[STABILITY.md §Release Evidence Packet](STABILITY.md#release-evidence-packet-v018)
and [`docs/agent-contract-current.md`](docs/agent-contract-current.md#read-these-for-release-review).
Exit codes (stable):
| Code | Meaning |
|---|---|
| `0` | Pass (advisory or strict-no-blockers) |
| `2` | Manifest config error |
| `3` | Input parse error (file missing, malformed, path traversal blocked, file too large) |
| `4` | Other Agents Shipgate error |
| `20` | Strict-mode gate failure |
---
## What you can't do (intentionally)
This section is the **CLI's** invariants. For the **agent's** behavioral boundary — what an agent driving Shipgate may assert in PR comments and review summaries — see [`docs/agent-autofix-boundary.md`](docs/agent-autofix-boundary.md).
- The CLI does not modify user code; it only reads.
- The CLI does not connect to MCP servers; it reads exported JSON only.
- Tool sources outside the manifest directory are rejected (path traversal containment).
- Files larger than 10 MB are rejected.
- Plugins are off by default (`AGENTS_SHIPGATE_ENABLE_PLUGINS=1` to enable; `--no-plugins` to force off).
---
## When you make changes to this repo
**Run the CLI as `./shipgate …` from the repository root** — `python shipgate …`
on Windows, which does not read a shebang. That is the one canonical command
here, for contributors and coding agents alike, and it is what every example in
`CONTRIBUTING.md` uses. Emitted commands name whichever spelling starts it, so
follow `next_actions[].executable` rather than assuming one token. `./shipgate scan -c shipgate.yaml`
is `agents-shipgate scan -c shipgate.yaml`, with three differences that matter
in a checkout: it runs *this* tree's `src/` rather than whatever copy `PATH`
resolves to, it selects a supported interpreter (`AGENTS_SHIPGATE_PYTHON`, else
the project virtualenv — the main checkout's, if this is a `git worktree`), and
it needs no installation and no `PYTHONPATH`. Recovery commands it prints name
the launcher, so they are runnable exactly as printed.
Use a bare `agents-shipgate` only to check what an *installed* build does. If a
command behaves as though your edit never happened, run
`./shipgate doctor --config shipgate.yaml --json` and read `environment`
(above): it states which interpreter ran, which package was imported, which
checkout you are standing in, and what disagrees.
The launcher stops at the repository boundary, and so should edits that spread
it. Everything written *into another repo* — the sections above, the adoption
kits, `.cursorrules`, `.claude/commands/`, `skills/`, `.agents/skills/`, the
snippets in `docs/target-repo-agent-snippets.md`, and every block
`init --write --agent-instructions=…` renders — keeps saying
`agents-shipgate`, because those run where the package is installed and there
is no launcher. It is also the same reason durable artifacts stay canonical:
those bytes are pinned by render hashes, and an absolute path from one machine
does not belong in them.
- Run `python -m ruff check .` and `python -m pytest` before committing.
- Bumping a check's behavior requires updating the test suite and any golden fixtures under `samples/*/expected/`.
- New checks must include: code in `src/agents_shipgate/checks/<category>.py` plus a `BUILTIN_CHECKS` entry in `checks/registry.py`, metadata in `docs/checks/<category>.yaml` (loaded into `CHECK_METADATA` at registry import time by `agents_shipgate.checks._metadata_loader`), a test in `tests/`, and a row in `docs/checks.md`. After editing YAML, regenerate `docs/checks.json` with `python scripts/generate_schemas.py`.
- Do not change check IDs in published versions; always add new ones.
- If you regenerate the JSON schemas, run `python scripts/generate_schemas.py` and commit every changed file under `docs/`.
---
## Reusable prompts
Prebuilt prompts for common workflows live in [`prompts/`](prompts/):
- [`decide-shipgate-relevance.md`](prompts/decide-shipgate-relevance.md) — apply [`docs/triggers.json`](docs/triggers.json) to decide whether Shipgate should run at all
- [`add-shipgate-to-repo.md`](prompts/add-shipgate-to-repo.md) — bootstrap a repo
- [`fix-top-finding.md`](prompts/fix-top-finding.md) — iterate on a single finding
- [`recommend-fixes.md`](prompts/recommend-fixes.md) — walk all active findings and surface targeted fix recommendations across the four autofix-policy classes
- [`explain-finding-to-user.md`](prompts/explain-finding-to-user.md) — translate one finding into 3–5 sentences of user-facing prose; companion to `agents-shipgate explain-finding`
- [`stabilize-strict-mode.md`](prompts/stabilize-strict-mode.md) — tune → baseline → promote
- [`triage-false-positive.md`](prompts/triage-false-positive.md) — override vs suppress decision
- [`upgrade-shipgate-version.md`](prompts/upgrade-shipgate-version.md) — bump agents-shipgate version safely (regenerate baseline if needed)
For downstream repos, use [`docs/target-repo-agent-snippets.md`](docs/target-repo-agent-snippets.md)
to copy Shipgate trigger rules into `AGENTS.md`, `CLAUDE.md`, Cursor rules,
PR templates, and advisory CI. Use
[`docs/agent-adoption-harness.md`](docs/agent-adoption-harness.md) to evaluate
whether coding agents discover and use Shipgate without being prompted by name.
### Editor / agent integrations
Per-agent install guides for dropping Shipgate into your own agent project:
- [`docs/agents/use-with-claude-code.md`](docs/agents/use-with-claude-code.md) — install the `/shipgate` slash command and `agents-shipgate` auto-discoverable skill. Source surfaces ship at [`.claude/commands/shipgate.md`](.claude/commands/shipgate.md) and [`skills/agents-shipgate/`](skills/agents-shipgate/) (named `agents-shipgate` to avoid colliding with the slash command — Claude Code lets a same-named skill preempt a command). The skill bundles the recipes in [`skills/agents-shipgate/prompts/`](skills/agents-shipgate/prompts/) and a starter advisory CI workflow at [`skills/agents-shipgate/ci-recipes/advisory-pr-comment.yml`](skills/agents-shipgate/ci-recipes/advisory-pr-comment.yml); when you change anything in [`prompts/`](prompts/) or `examples/github-actions/01-advisory-pr-comment.yml`, sync the bundled copy.
- [`docs/agents/use-with-codex.md`](docs/agents/use-with-codex.md) — install the canonical `AGENTS.md` snippet plus repo-scoped Codex skill. Source surfaces ship at [`.agents/skills/agents-shipgate/`](.agents/skills/agents-shipgate/) and are generated into downstream repos with `agents-shipgate init --write --agent-instructions=agents-md,codex-skill`. The default `all` kit does not install skill bundles. The skill is Codex-optimized: concise `SKILL.md`, on-demand references, and an advisory CI template.
- [`docs/agents/use-with-cursor.md`](docs/agents/use-with-cursor.md) — drop the canonical `.cursor/rules/agents-shipgate.mdc` auto-attach rule (from [`docs/target-repo-agent-snippets.md`](docs/target-repo-agent-snippets.md)) into your repo. The rule fires whenever a chat touches `shipgate.yaml`, an MCP/OpenAPI spec, a tool JSON, or a `.py` file.
---
## Verification
After you (the agent) complete a task involving Agents Shipgate, verify:
1. `agents-shipgate self-check --json` returns `"ready": true`.
2. `agents-shipgate contract --json` matches the installed CLI contract you expect.
3. The user's `shipgate.yaml` has no `CHANGE_ME` placeholders.
4. A scan completes with exit code 0 (advisory mode) and writes `report.json`.
5. The user's repo `.gitignore` includes `agents-shipgate-reports/` (do not commit reports).
<!-- ===== source: docs/agent-recipes.md ===== -->
# Agent recipes
Copy-pasteable workflows for AI coding agents (Claude Code, Codex, Cursor,
Aider) that need to drive `agents-shipgate` end-to-end without prompting
the user. Every command is read-only or schema-validated;
static-by-default, with audited exceptions pinned in
[`tests/test_adapter_static_only.py::ALLOWED_EXCEPTIONS`](../tests/test_adapter_static_only.py).
> If you are a human, [`quickstart.md`](quickstart.md) is the friendlier
> entry point. This page is structured for agents that consume `--json`.
---
## Recipe 0 · Verify an agent-related PR
Use this before claiming completion on a PR or local diff that changes tools,
MCP/OpenAPI surfaces, prompts, permissions, policies, release gates, or
`shipgate.yaml`.
```bash
agents-shipgate verify --preview --json
agents-shipgate preflight --workspace . --plan - --json
agents-shipgate verify --workspace . --config shipgate.yaml \
--base origin/main --head HEAD --ci-mode advisory --format json
```
For local uncommitted work, omit `--base`/`--head`. For committed PR/CI refs,
make the base ref available first because `verify` never fetches. `--preview`
additionally wants `--head` **checked out**: it reads project markers from the
working tree, because that is the tree the `init` it recommends would write to.
Previewing some other ref establishes no project and returns
`agent_action_required` with a `fetch_base` action whose `expects` names the
missing input as a **commit id**, together with the pinned `--base`/`--head` to
re-run with — read them from `expects`, which is never truncated, because a
revision expression re-resolves against the new `HEAD`. Plain `verify` reads
`--head` from the object database and needs no checkout. Read
`agents-shipgate-reports/agent-handoff.json` first and lead with
`control.state`, `gate.merge_verdict`, `gate.can_merge_without_human`,
`next_action`, `fix_task`, and `capability_review.top_changes[]`. Fall back to
`verifier.json` only for older installed CLIs that do not report runtime
contract 14.
Then read `report.json.release_decision.decision`, which remains the only
release gate.
Before editing `shipgate.yaml`, Shipgate CI, AGENTS/CLAUDE/Cursor rules,
policy packs, baselines, waivers, suppressions, Codex hooks/config, Codex
plugin manifests, `.mcp.json`, `.app.json`, or `SKILL.md`, run
`agents-shipgate preflight --workspace . --plan - --json` with a
`PreflightPlanV1` object. Legacy `--changed-files` remains available. Switch on
`control.state`. If it is `review_publishable`, a human must approve the merge
and you may still commit, push, and update the PR; if it is
`human_review_required`, stop for a human; if it is
`agent_action_required`, perform only the exact coding-agent action in
`control.next_action` — its `command` when it names one, and otherwise the
input its `expects` names, which is the shape a `fetch_base` route carries.
Do not claim completion unless `control.state` is `complete`. Conversation-level
acknowledgement never changes control state; only a newly generated verifier
artifact can clear an obligation.
## Recipe 1 · First adoption helper
Use this when a repo doesn't yet have `shipgate.yaml` and the user wants a
scan-oriented first pass. The verifier-first path is
`verify --preview --json` →
`init --write --json` →
`verify --base origin/main --head HEAD`. The helper below remains useful when a
coding agent should also apply high-confidence manifest cleanup in the same
turn. Ongoing PR work should use Recipe 0.
```bash
agents-shipgate detect --json
agents-shipgate init --write --ci --json
agents-shipgate scan -c shipgate.yaml --suggest-patches --format json
agents-shipgate apply-patches \
--from agents-shipgate-reports/report.json \
--confidence high --apply
```
### Step 1 · `detect --json` (read-only)
Consume the response to decide whether to proceed. Key fields:
- Detection silently skips common fixture corpus directories such as
`fixtures/`, `_fixtures/`, `__fixtures__/`, `testdata/`, `test_data/`,
`test-fixtures/`, `test_fixtures/`, `golden/`, and `goldens/` when they
are below the selected workspace. Point `--workspace` directly at a
fixture project if you intentionally want to classify that fixture itself.
- `is_agent_project` — `true` when at least one Python framework
scored ≥ 2.0 with a strong signal.
- `frameworks[]` — per-framework scores + evidence + candidate file
paths.
- `agent_name_candidates[]` — ranked best-first, each
`{value, source, role, path, rank_score, selectable, rationale[]}`.
**Take the first entry whose `selectable` is `true`** — that is the value
`init` writes as `agent.name`. When none is selectable the manifest keeps
its `CHANGE_ME` placeholder rather than asserting an identity nothing
reliably declares. Ordering is decided by, in effect:
- `role` — `root_agent` (bound as `App(root_agent=…)`, or assigned to the
conventional `root_agent` symbol) outranks `agent`, which outranks
`sub_agent` (named inside another agent's `sub_agents=[…]` /
`handoffs=[…]`). `workspace_dir` is the directory-name fallback and is
never selectable.
- `path` — a name declared in product code outranks one that is not the
product's: test code, which names fixtures, and a scaffolding template
under a `resources/templates/` directory, which names an example. This
dominates: either one building an `App(root_agent=…)` still ranks below
a plain agent the shipped code declares.
- corroboration — a value the project name independently agrees with
ranks above one only a single site declares.
- a quality floor — values under three significant characters, and
generic scaffolding names (`agent`, `foo`, `test`, …), are ranked last
and marked `selectable: false`.
One rule overrides all four: if a **project** declares an application root
whose name cannot be resolved statically — a dynamic expression, a factory
call, a symbol bound more than once — then nothing that project declares
is selectable, and the `rationale[]` names the project and says why.
Anything still ranked there is by construction not the root, so writing it
would declare a worker as the reviewed identity. The scope is the project,
not the repository: on a monorepo, agents in the other entries of
`agent_project_candidates[]` stay selectable. A name two projects declare
is rejected when either one is blocked.
"Which project" means an entry of `agent_project_candidates[]`, not the
nearest directory carrying a project marker. A marker directory that holds
no agent evidence — a utilities package with its own `pyproject.toml` — is
not a manifest scope, and a name found there belongs to the scope that
encloses it.
The same product/not-product split above decides *which* roots block: a
root declared only by test code or by a scaffolding template is not the
application a project ships, so it does not disable selection for the
rest of the project (#398).
`rationale[]` states which of those applied, so a ranking change is
visible in the output rather than silently changing what the manifest
claims. `name=` values that come from a module constant or an
`os.environ.get("…", "…")` default in the same package are resolved
statically (one hop, no code executed) and say so in `rationale[]`.
All of this reads Python's binding rules or declines — a spelling is never
taken as provenance. `Agent`/`LlmAgent`/`App` are resolved through the
binding that reaches *the call site*: a framework constructor imported
under an alias is recognised, and one shadowed by a local `def`/`class`,
bound only after the call, bound conditionally, or replaced through an
attribute (`adk.Agent = fake`) is not. Dotted spellings are held to the
same standard — the head must prove a framework module.
Left unresolved rather than guessed: a symbol bound more than once
anywhere in the file; one assigned under an `if`/`try`/loop; one rebound
in an enclosing scope (a function body executes when it is called, not
where it is written);
one whose import could resolve to two different in-workspace modules; an
`os.getenv` spelling that is not a provably unshadowed stdlib import; and
anything at all in a file carrying `from x import *` — until a later
explicit binding re-establishes what a spelling means. Bindings that carry
no assignment count too: `del`, `class`, `except … as`, `case`, and a
`global`/`nonlocal` store routed to another scope all retire the agent a
name used to hold. Scopes follow Python's own — comprehensions have their
own, definition headers (defaults, decorators, annotations, class bases)
are evaluated in the enclosing one, and a root declared inside a
conditionally defined function is contingent on that branch.
- `project_name_candidates[]` — `{value, source}` only. Project names have
no hierarchy to rank, so they carry none of the fields above. The
`pyproject` source seeds `project.name`, never `agent.name`.
- `suggested_sources[]` — MCP/OpenAPI files matched by glob AND accepted
by the real input adapters, so `init` never writes a `tool_sources`
entry that `scan` rejects at parse time. These do NOT bump
`is_agent_project` on their own.
- `excluded_sources[]` — `{type, path, reason}` for glob matches the
input adapters reject (e.g. an `mcpServers`-style host config such as
a Cursor plugin `mcp.json`, or a Swagger 2.0 document). Do not add
these to `tool_sources`; the `reason` says what `scan` would fail on.
- `codex_plugin_candidates[]` — Codex plugin package or marketplace
artifacts matched by convention. These also do NOT bump
`is_agent_project` on their own.
- `host_boundary_candidates[]` — `{path, hosts, file_type}` from the existing
bounded host census, including ignored settings. Filenames only; no config
contents, grants or runtime authority have been verified. Host-only input
routes to `audit --host` without a manifest.
- `host_discovery_incomplete_paths[]` — paths the bounded census could not
see through: a link it does not follow (only when that link resolves to a
directory — one pointing at a file conceals nothing and is not listed), and
a directory it could not read or that exhausted the entry bound. Empty
candidates beside this field do not prove absence. Follow the inspection
route; a directory at a config filename is also an input defect requiring
inspection. A census that stops publishes no candidates at all, and never
refuses the rest of the classification — the framework, tool-source and
scope answers stand, and only the product-wide negative is withheld.
- `next_actions[]` — the ranked route. On `agent_scope: "ambiguous"` rank 1 is
the decision (`kind: "review"`, `command: null`) and every entry below it is
one exact `init --workspace <candidate> --write --json`, with `executable`
and `args`, in candidate order — the list `init --write` publishes when it
refuses the same workspace, minus the setup flags: `detect` asked for no
setup, so it promises none. Add `--ci` or `--agent-instructions` yourself if
you want them, or take the command from `init`'s own refusal, which repeats
the flags the run asked for. Match on the path rather than the ordering.
Every candidate gets an entry; the workspace root is never offered, since it
is the scope `init` refuses. A candidate that already carries a manifest gets
`doctor --config <that manifest> --json` rather than an `init` that would
refuse to overwrite it — unless you asked for setup it still owes: with
`--agent-instructions` the full `init --write` is the advertised refresh and
exits 0, and with `--ci` the command drops `--write` so the workflow is
installed and the manifest is left alone. The workspace root, when listed, is
a `review` entry rather than a command.
**Stop condition.** Stop and skip `init` only when ALL of:
- `is_agent_project` is `false`, AND
- `suggested_sources` is empty, AND
- `codex_plugin_candidates` is empty, AND
- `host_boundary_candidates` is empty, AND
- `host_discovery_incomplete_paths` is empty, AND
- `python_parse_truncated` is `false` — each negative above is a claim about
the whole workspace, and a run whose Python parse stopped at its cap read
only part of one. This is the raw parse bit, not `agent_scope_truncated`,
which additionally requires more than one candidate scope, AND
- no `shipgate.yaml` already exists, AND
- the user did not explicitly request a scan.
A payload that does not carry every one of those keys leaves the condition
unevaluable — `trigger` reports `stop_conditions_evaluated: false` and infers
no stop. Re-run `detect` with the current CLI rather than reading an absent
key as `false`.
Otherwise proceed. MCP/OpenAPI-only tool-surface repos and Codex plugin
package repos surface as `is_agent_project: false` but should still be
onboarded — their sources will land in `tool_sources` during `init`.
Host-only repositories instead follow the emitted audit route. `init` and
`bootstrap` hand off without creating setup files; even `init --ci` cannot
create a manifest-based workflow for a repository needing only host review.
The hand-off covers every detection-driven mode — `--ci`, `--claude-code`,
`--agent-instructions`, `--local-review` — because each renders its manifest
from that classification. `init --minimal` is the exception: it never
classifies the workspace, so it still writes the manual template on request.
### Step 2 · `init --write --ci --json`
Auto-detection runs again inside `init` and writes:
- `shipgate.yaml` with `tool_sources` populated per detected framework
candidate file.
- `.github/workflows/agents-shipgate.yml` (if `--ci` is set; refuses
to overwrite an existing workflow file, or one where a sibling
workflow already uses the `ThreeMoonsLab/agents-shipgate` action at
any ref).
Key response fields:
- `manifest_status`: `"written"` | `"skipped_existing"` |
`"refused_unresolved_scope"` | `"not_attempted"` |
`"not_applicable_host_review"` — the last one is the host-only hand-off:
no manifest, workflow or instruction file was written, and
`control.next_action` names the read-only host audit instead.
- `workflow.status` (when `--ci`): `"written"` | `"skipped_existing_target"`
| `"skipped_cross_reference"`.
- `placeholders[]` — entries the template intentionally leaves as
`CHANGE_ME` because no high-confidence signal was available. Each has
a `path` (YAML-pointer-ish location) and `current` value. Replace
these before scanning. Every field the template chose without evidence is
in here, `tool_sources[].type` included (#441).
- `tool_surface_origin`: `"detected"` | `"scaffold"` | `null`. `"scaffold"`
means this render read no tool surface and the `tool_sources` block is a
placeholder — `id`, `type`, and `path` are all `CHANGE_ME` and all three
are in `placeholders[]`. `null` means this run's render reached neither disk
nor this payload, so it describes no file you can open: `skipped_existing`
and `refused_unresolved_scope` report it, on the same authority rule
`placeholders` follows. `manifest_message` always states the same fact in
prose; `control.reason` states it only where init's own reason is the
envelope's, because on a freshly written manifest the human-owned
`declared_purpose` declaration outranks it. Do **not** run `scan` on a
scaffold — fill in the block first; until then there is nothing for the gate
to read.
In the default (auto) mode a `"scaffold"` also means full discovery found
nothing, and `detect` on the same workspace agrees: `is_agent_project: false`
with no suggested sources. Under `--minimal` it means only that the minimal
template skips framework detection entirely — `detect` may well report a
framework for the same workspace — and the summary it emits says so.
- `auto_detected.agent_name` — the value the manifest carries
(`null` when the template fell back to `CHANGE_ME`; matches the YAML
exactly).
- `auto_detected.agent_scope`: `"single"` | `"ambiguous"` | `"unknown"`,
with `auto_detected.agent_project_candidates[]` naming every self-contained
project (project-marker directory) that defines an agent. `"unknown"` means
discovery hit its Python-file cap in a workspace with several project roots,
so the verdict would otherwise have depended on which files were read
first.
- `auto_detected.python_parse_truncated`: whether the Python parse stopped at
its cap at all. Every whole-workspace negative — `is_agent_project: false`
included — is unsafe to act on while this is `true`, and `--write` refuses,
because the agent name and tool surface a manifest would declare were read
from part of the tree. The recovery is mechanical and the emitted
`next_actions[0]` carries it: the *same command you ran*, plus
`--max-python-files <workspace_signals.python_file_total>` — a bound that
covers every Python file and so cannot hit the cap again. From `detect` that
is a `detect`; from `init --write` it is an `init --write`, carrying the
setup flags the run asked for, so one step both settles the scan and
completes the setup.
- `auto_detected.agent_scope_truncated`: whether that candidate list is an
enumeration or a lower bound. `true` means the Python parse stopped at its
cap in a workspace holding more than one candidate project scope, so any
project in the part of the tree that was not read is missing from the list —
do **not** conclude a project is absent from it. Re-run
`detect --max-python-files <n> --json` first.
- `auto_detected.workspace_signals.project_root_count` bounds that claim: an
uncapped, filename-only census of the directories that could be a manifest
scope (every project-marker directory, plus the workspace root itself, which
is a candidate whether or not it carries a marker). `init` emits the same
block `detect` does, so the number its refusal message quotes is readable
structurally.
`--ci` is orthogonal to `--write`: each gets its own overwrite-refusal.
Exit code is the max of per-action outcomes; manifest-error and
workflow-skip can co-occur. The workflow lands at the repository root —
GitHub loads workflows from nowhere else — named `agents-shipgate.yml` for a
root manifest and `agents-shipgate-<project>.yml` for a scoped one, because
the action takes a single `config` scalar and one shared file would leave
every project after the first ungated. Read `workflow.path`.
`refused_unresolved_scope` (exit `2`) is the one outcome where **nothing**
is written — not the manifest, not the workflow, not the agent-instruction
snippets, not the reports `.gitignore` block. It fires when agents live in
more than one project under this workspace (because one `agent.name` and one
`declared_purpose` cannot describe them all) and when discovery was capped
before it could tell. Re-run with `--workspace` pointed at one of
`agent_project_candidates[].path` rather than retrying the same command — the
emitted `next_actions[]` commands repeat whatever setup flags you passed.
`--allow-unresolved-scope` accepts a single manifest for the workspace as a
whole, and `--minimal` is never scope-gated because it adopts no detected name
or tool surface.
### Step 3 · `scan -c shipgate.yaml --suggest-patches --format json`
Writes to `agents-shipgate-reports/report.json`. Read it, walk
`findings[]` filtering on `suppressed`. Per-finding fields you can rely
on today:
- `check_id`, `title`, `severity`, `category`, `evidence`,
`confidence`, `recommendation`.
- `patches[]` (only when `--suggest-patches` is set) — list of
patch objects with `kind` ∈ `{set_pointer, append_pointer,
remove_pointer, manual}`. Non-manual patches additionally carry
`confidence` ∈ `{low, medium, high}`, `target_file`, `pointer`,
`target_format`, `rationale`, `target_sha256`.
- `manifest_dir` (top-level on the report) — absolute path to the
directory containing `shipgate.yaml`. `apply-patches` enforces a
containment check against this.
When `--suggest-patches` is set, every active (unsuppressed) finding
has at least one patch. Manual-only findings (e.g. trace approval
flips, per-check policy decisions) carry a single `ManualPatch` with
`instructions` instead of a machine-applicable patch.
Optional dynamic-validation handoff:
```bash
agents-shipgate scenario suggest \
--from agents-shipgate-reports/report.json \
--out agents-shipgate-reports/suggested-scenarios.yaml
```
This YAML is a concrete per-finding/per-tool fan-out of
`report.json.suggested_scenarios[]`, not a separate scenario engine.
Suppressed findings are omitted; baseline-matched findings remain because
they are accepted debt, not resolved risk.
### Step 4 · `apply-patches --confidence high --apply`
Default `--confidence high` only auto-applies patches whose `confidence`
field is `"high"`. Today that's the 3 stale-manifest removals
(`SHIP-MANIFEST-STALE-{SUPPRESSION,POLICY,RISK-OVERRIDE}`). Scope
coverage appends ship at `medium` and require explicit
`--confidence medium` to apply.
`apply-patches` is dry-run by default — `--apply` is required to
mutate files. Containment-checked: any `target_file` outside
`report.manifest_dir` aborts with exit code 5 before SHA verification.
### Step 5 (optional) · Summarize for the user
When the flow completes, summarize `report.json`:
- `release_decision.decision` (`"blocked" | "review_required" | "insufficient_evidence" | "passed"`)
— the v0.8+ release-gate signal (`insufficient_evidence` added v0.14).
Prefer this over `summary.status`, which stays baseline-blind for
backwards compat. Switch on the value with a `review_required`
fallback for unknown future values.
- `release_decision.reason` (one-sentence explanation).
- Top 3 active critical/high findings with their `check_id`,
`tool_name` (when present), and `recommendation`.
- Whether any patches were applied (count from
`apply-patches --json` output's `files`).
Link findings back to [`docs/checks.md#<id>`](checks.md) so the user
can read full check rationale.
---
## Recipe 2 · Add Shipgate to a repo that already has tool surfaces
Same as Recipe 1, but `detect` may report `is_agent_project: false`
when the repo only ships MCP exports or OpenAPI specs. Per the soft
stop rule above, proceed anyway when `suggested_sources` is non-empty.
`init` will populate `tool_sources` from those globs. The rest of the
flow (steps 2-5) is identical.
### First-real-repo recovery rules
When the first repo scan does not produce useful tools, follow these
rules before changing code:
- If `detect --json` has MCP/OpenAPI `suggested_sources`, continue to
`init` even when `is_agent_project` is `false`.
- If `doctor` shows zero tools, inspect `tool_sources[].path`, MCP
`tools[]`, OpenAPI `paths`, optional source warnings, and dynamic
ADK/MCP warnings.
- If tools are created by factories, wrappers, runtime imports, or
dynamic ADK/MCP toolsets, provide an explicit MCP export, OpenAPI
spec, or local tool inventory artifact.
- Resolve the placeholders `init --json` reports before scanning, and
switch on **`control.next_action.actor`**, which routes the turn
rather than individual fields. `placeholders[]` is a location list
(`path`, `current`, `line`) and carries no owner. `actor: "human"`
selects a human review or stop with `permissions.edit: false`:
surface the required review and stop. Its `why` is fitted to a
prose budget and may end `and N more in placeholders[]`, so absence
from it never makes a field yours. `actor: "coding_agent"` authorizes
only the exact `control.next_action`: read its `kind`, `path` or
`command`, perform that step, and rerun the stated check. A blocking
setup repair can take precedence while human-owned declarations
remain unresolved; the actor is not proof that all remaining
placeholders are yours. Every
declaration — purpose, prohibited actions, effect, authority,
binding, approval, confirmation, idempotency, safeguards, accepted
debt — must be supplied by a human, because Shipgate never invents a
declaration nobody made. Do not derive one from a prompt, main agent
file or README.
- Agents Shipgate requires Python 3.12+. If the project runtime is
older, install the CLI outside the project env with `pipx` or `uv`.
- Ensure `agents-shipgate-reports/` is listed in `.gitignore`.
---
## Recipe 3 · Re-scan after editing the manifest
When the user has already replaced `CHANGE_ME` placeholders or added
policies:
```bash
agents-shipgate scan -c shipgate.yaml --suggest-patches --format json
agents-shipgate apply-patches \
--from agents-shipgate-reports/report.json \
--confidence high --apply
```
`run_id` is deterministic for the same input — if the report's
`run_id` is unchanged from the previous run, nothing semantic about
the manifest+tool-surface changed.
---
## Recipe 4 · Suppress a check or finding
When a finding is a known false positive, edit `shipgate.yaml`:
```yaml
checks:
ignore:
- check_id: SHIP-DOC-MISSING-DESCRIPTION
tool: support_lookup_v2 # optional; omit to suppress for ALL tools
reason: "Tool description matches the upstream OpenAPI summary."
```
`reason` is required — empty reasons fail manifest validation. Re-run
`scan` to confirm the finding is gone (it will appear in `findings[]`
with `suppressed: true` rather than disappearing from the report).
If you suppress a check that no longer fires, the next scan emits
`SHIP-MANIFEST-STALE-SUPPRESSION` — auto-removable via
`apply-patches`.
---
## Recipe 5 · Add Shipgate to CI without changing existing workflows
```bash
agents-shipgate init --workspace . --ci # no --write
```
Without `--write`, the manifest is printed to stdout (don't write a
new one). With `--ci`, the workflow file is still written orthogonally
unless an existing workflow already references the action — in which
case `workflow.status: "skipped_cross_reference"` and the path of the
existing workflow is reported in `cross_reference_path`.
---
## Output handling
- Always pass `--json` (where supported) and parse the result. The
human-readable stdout is unstable; the JSON shape is the contract.
- `scan` does not have `--json`; instead pass `--format json` and read
`agents-shipgate-reports/report.json`.
- Errors emit a structured `next_action` JSON line on stderr when
`AGENTS_SHIPGATE_AGENT_MODE=1` is set. Surface that path to the user
rather than scraping prose.
## Pre-flight reminder
`agents-shipgate-reports/` is a local artifact directory. Before
committing, ensure it's listed in `.gitignore`:
```gitignore
agents-shipgate-reports/
```
`init` does not touch `.gitignore` — leave that to the user or follow
up with an explicit edit.
---
## Reference
- [`docs/agent-autofix-boundary.md`](agent-autofix-boundary.md) — what
an agent may do mechanically vs. what must defer to a human reviewer.
- [`docs/report-reading-for-agents.md`](report-reading-for-agents.md) —
reader's primer for `agents-shipgate-reports/report.json`.
- [`docs/checks.md`](checks.md) — full check catalog with rationale
- [`docs/autofix-policy.md`](autofix-policy.md) — which findings are
safe to apply, which need review, and how `apply-patches --confidence`
filters them
- [`docs/minimal-real-configs.md`](minimal-real-configs.md) —
framework-specific minimal manifests
- [`AGENTS.md`](../AGENTS.md) — top-level agent instructions, install,
trigger table
<!-- ===== source: docs/agent-contract-current.md ===== -->
# Current Agent Contract
Runtime contract v41, unreleased, names the changed inputs a host comparison
does not read (#821). A zero-row comparison used to print "No static
host-grant changes detected" for a pull request that added a Cursor plugin's
`mcp.json` or moved a marketplace plugin's pinned `sha`, exactly as for a
docs-only one. Verifier `0.21` and `shipgate diff --json` (capability diff
`0.4`) now add, inside the existing `coverage` block, a `changed_not_read`
item for each path in the comparison's own changed-file set that a bounded,
documented candidate rule names and no reader of this entry read, with its
`candidate` rule (`plugin_mcp_config`, `plugin_manifest_mcp_servers`,
`plugin_manifest_hooks`, `plugin_hook_file`, `unparsed_plugin_manifest`,
`cursor_project_hooks`, `nested_host_settings`, `external_plugin_source`) and,
for an external plugin source, the source it names as `detail`, never fetched.
`read_sources_only` is `false` while such an item is named, and
`unread_candidates` (`examined`, `not_examined`, or `null` for not recorded)
with `unread_candidates_not_examined` say whether the change set was examined
and how many candidates were not, past the bound or because a file their rule
needed was not read or did not parse. An item is never a row, a widening, a
loading claim or a `check` violation. The one route it moves: a manifest-free
`verify` whose only host-relevant change is such an input, or a changed
candidate it counts as not examined, publishes that comparison instead of the
setup route, and on `verify --preview` the next
action for that change is `discover` (`audit --host`) with the comparison
published, where it was `initialize` (`init --write`) with none, in an
agent-related workspace too, as it already was when the change edited a host
file this entry reads. `minimum_control_contract_version`
stays `21`, and a `0.20` verifier reads with the search not recorded. See
[the migration note](../STABILITY.md#unread-changed-inputs-821).
Still contract v41, unreleased: a plugin directory a host comparison cannot
compare no longer hides the changes outside it (#808). Where every blocking
limit that refused the comparison is a plugin-reference limit bounded by its
plugin directory, and no compared source depends on that directory, verifier
`0.21` and `shipgate diff --json` (capability diff `0.4`) publish
`comparison_status: partial`: the same `incomparable_reasons`, the `rows`,
`review` and `unchanged_limits` established outside the directory, and the
directory as `scope` on each such `blocking_limit` coverage item, which
`0.20` reserved as always `null`. Treat `partial` as `incomparable` for any
decision: every control state, permission, next action and `check` decision
is the refusal's, the control envelope's `capability_rows` projects it as
`incomparable` with no rows, and `check`, whose boundary result cannot name a
directory, still refuses its comparison. A `0.20` verifier claiming a partial
comparison or a `scope` is refused. See
[the migration note](../STABILITY.md#partial-host-comparison-808).
Runtime contract v41, extended in place, also reads how a coding agent is
launched inside a workflow job (#823). Host-grants `0.6` shipped in 1.1.0, so
host-grants inventory, baseline and drift schemas move to `0.7`, and a workflow
grant adds `agent_launches[]`, `unread_agent_runs[]` and `checkout_refs[]`,
each omitted when empty. An agent launch is a step whose `uses:` is a
documented agent action (`anthropics/claude-code-action`,
`anthropics/claude-code-base-action`, `openai/codex-action`) with the
permission inputs it declares, or a `run:` that is one line of plain words
running `claude -p` / `codex exec` under `bash` or `sh`, with its documented
permission flags; its `job`, `step`, `agent`, `form` (`read`, or `unresolved`
with `inputs_not_a_mapping`), `settings[]` (`name`, `value`,
`unresolved_reason`, `holds_expression`), `widening_rules[]` (`rule`,
`setting`) and `job_secrets[]`. Shell is not parsed: any other `run:` that
mentions `claude` or `codex` is an `unread_agent_runs[]` entry (`job`, `step`,
`agent`), a named non-blocking limit that publishes none of its text, is never
compared and gives no row; and `claude_args` / `codex-args` are read only as a
plain list of words, any other value being `unread_arguments`, compared by a
digest and read for no rule. A checkout ref is each `actions/checkout` step's
`with.ref`, `null` for the default. Values are compared as text and never
executed. A JSON object in a `settings` or `mcp_config` input publishes its
shape and none of its free text — key names, with each string a
`<withheld:…>` digest except those a host reader publishes (a permission rule,
a documented setting's value, an MCP server's command name and URL host) — so
an MCP server's arguments and a hook's command are compared but never
published; a URL publishes its scheme and host. Only a documented rule a job's
launches gain — bypassed permission checks (a flag, or JSON settings whose
`defaultMode` is `bypassPermissions`), a bypassed or `danger-full-access`
sandbox (`permission-profile: :danger-full-access` included, and in a
`codex exec` step without `--sandbox` a `--config` override of `sandbox_mode`
or `default_permissions` that selects it), `safety-strategy: unsafe`, or a
user gate opened to `*` — raises `workflow_agent_widened_<added|changed>` and
makes the row `widened`. A rule is read only from text this audit reads
exactly, and one a launch already met in a job it left, one where an unread
step of the job became a read launch, or one where the job's launch before
held an expression or an unread argument input the rule is read from, is named
and not claimed. Every other edit is `changed`, and a workflow row that runs an
agent ends its `why` with the job facts beside each agent step. An action whose
`with:` is not a mapping is `unresolved` and a named non-blocking limit; a
setting holding credential-shaped text, prose included, is published redacted,
compared as published and a named non-blocking limit, and a checkout ref
holding it is a blocking limit, as a redacted step reference is. A `0.4`–`0.6`
baseline holding a workflow grant is incomparable
(`baseline_workflow_agent_launches_unavailable`); one without a workflow stays
comparable. It moves neither #821's verifier `0.21` nor its capability diff
`0.4`, and `minimum_control_contract_version` stays `21`. See
[the migration note](../STABILITY.md#workflow-agent-launches-contract-v41-823).
The same unreleased runtime contract v41 also names what changed in a hook and
in an MCP server's launch arguments (#819). Host-grants inventory, baseline and
drift schemas move to `0.7`: a hook grant adds `handlers[]` (each handler's
group `matcher`, its `command` as `{executable, sha256}` and its `timeout`)
and `omitted_handlers`, and an MCP server grant adds `package` and
`args_sha256`. No command or argument text is published: a command is its
executable's name, when that is a plain token, and a digest; the arguments are
one package specification of a strict shape and a digest of the rest. A hook
row names the changed field, `PostToolUse: matcher Edit → Edit|Write|Bash` or
`command changed` with both digests, and a version pin moving to `@latest` is
a `package` difference, in the text and in `review.changes[].change`. The
members display what `config_sha256` already binds, so grant equality and the
inventory digests leave them out: they move no row value, row count, verifier
or capability-diff schema, a `0.6` baseline without workflow grants stays comparable with no new row
or reason, and `minimum_control_contract_version` stays `21`. A saved baseline
holds none of the members. See
[the migration note](../STABILITY.md#hook-mcp-detail-fields-819).
Previous runtime contract v40 reads the action reference each workflow step declares
(#771). Host-grants inventory, baseline and drift schemas move to `0.6`, and a
workflow grant adds `step_actions[]`: the job, the step (`id`, else `name`,
else `steps[N]`), the declared `uses`, and its `form` — `remote`, `docker`, or
`unresolved` with a reason. Moving a step from a pinned SHA to `@main` is a
`changed` row naming the job and step, with `expands: false`: a reference
names different code, not new token scopes. Local `./` actions stay unread
(#701). A `0.4` or `0.5` baseline holding a workflow grant is incomparable
(`baseline_workflow_step_actions_unavailable`); one without a workflow stays
comparable, and `minimum_control_contract_version` stays `21`. See
[the migration note](../STABILITY.md#workflow-step-action-references-contract-v40-771).
The same contract and `0.6` schemas, new in 1.1.0, also read the named secrets a
job passes to a reusable workflow (#693): a reusable call adds
`secret_mappings[]` (the destination, the source name from
`${{ secrets.NAME }}`, `form` and `unresolved_reason`) and `uses_redacted`.
Pointing a destination at a different source is a `changed` row with
`expands: false`; a reusable target or secret name that redacts is a blocking
limit rather than a comparison, while a literal or another expression is a
named non-blocking limit that leaves coverage complete. See
[the migration note](../STABILITY.md#reusable-workflow-secret-mappings-contract-v40-693).
The same contract and `0.6` schemas, new in 1.1.0, publish every workflow label
redacted (#802): a job id, a step's `id` or `name`, a trigger and a
permission scope name shaped like a credential (`ghp_…`, `AKIA…`) or holding
userinfo after `scheme://` are redacted in every field, row and `check`
evidence that names them, and `config_sha256` is computed over the redacted
labels. Ordinary names are unchanged. One redacted label still compares. Two
distinct job ids or triggers in a workflow, or scope names in one
`permissions` mapping, that publish alike are a blocking limit: while they
exist, `check`, `audit --host --save-baseline` and drift refuse on every run.
See [the migration note](../STABILITY.md#workflow-label-redaction-contract-v40-802).
New in 1.1.0, still contract v40: every host comparison says what it
established (#812). Verifier `0.20` adds `host_comparison.coverage`, and
`shipgate diff --json` (capability diff `0.3`) the same block: a capped list of sources with
their `side` (`base`, `head`, `both`), a `status` (`compared` with its `rows`,
zero on both sides only for a file proven byte-identical,
`changed_without_grant_change`, `changed_without_rows`, `unchanged_not_proven`,
or on an incomparable result `blocking_limit` with its `limit` kind) and
`omitted_items`. It is evidence beside the rows; it moves no
row, reason, control state, permission or next action, and `null` means not
recorded. A `0.19` verifier reads with `coverage: null`. See
[the migration note](../STABILITY.md#host-comparison-coverage-812).
New in 1.1.0, still contract v40: the same two versions publish what the text
says about those rows, so a machine consumer and a reader cannot disagree
(#795). A row adds `disposition` (`allow`, `ask`, `deny`, or `null` for
another grant kind) on every route that publishes rows, and
`host_comparison.review`, the same block `diff --json` carries, adds the
changes the text prints — each with the `row_indexes` it stands for, a
`direction` that may be `widened`, `narrowed` or `moved`, its cells, `why` and
one `expands` — plus `summary` (`{rows, changes, widenings}`), `question` and
`reproduce_command`. A joined change whose two sides read alike is refused, so
`check` and a provided diff publish rows alone. Every row value and the row
count are unchanged, and `null` means not recorded. See
[the migration note](../STABILITY.md#host-diff-review-json-795).
Previous runtime contract v39 reads through an in-tree link at a boundary path (#700).
A link such as `CLAUDE.md -> AGENTS.md` or `.claude/skills -> ../.agents/skills`
that resolves inside the repository is read at its target and published under
its own path. Host-grants inventory, baseline and drift schemas move to `0.5`,
which adds `artifacts[].resolved_through`, the in-tree paths the read
followed, present only on such an artifact. An external, escaping or dangling
target, a link inside a linked directory, a chain past eight hops, and a
directory link that could only hide a `**/` match still refuse. A `0.4`
baseline stays comparable, and `minimum_control_contract_version` stays `21`.
See
[the migration note](../STABILITY.md#link-read-through-at-boundary-paths-contract-v39-700).
Previous runtime contract v38 lets the control envelope name what host capability
changed (#662). `shipgate.agent_control/v1` gains an optional
`capability_rows` block on `check --format agent-control-json`,
`verify --format control` and `agent control`. It holds up to five rows,
the rows the engine called an expansion first, plus `omitted_rows`,
`comparison_status`, `incomparable_reasons` and `unchanged_limit_count`.
It is evidence beside the control and moves no state, permission or route.
It is omitted when no host comparison ran, so those envelopes are
byte-identical. The envelope object is closed, so a reader that validates
against the v37 schema rejects an envelope carrying the block. That break
is deliberate. `minimum_control_contract_version` stays `21`. See
[the migration note](../STABILITY.md#host-capability-rows-in-the-control-envelope-contract-v38-662).
Previous runtime contract v37 names the limits a host comparison compared past (#721).
Verifier `0.19` adds `host_comparison.unchanged_limits`, and `shipgate diff`
(capability diff `0.2`) carries the same list. A surface that is partial
(`unsupported`, `parse_failed`) or experimental on both sides, and
byte-identical between them, no longer refuses the whole comparison: the rest
is compared and each limit is named. A limit that changed, appears on one side
only, or is `unreadable` still refuses. `check`'s boundary result cannot name
limits yet and keeps refusing. See
[the migration note](../STABILITY.md#unchanged-comparison-limits-contract-v37-721).
Previous runtime contract v36 freezes the report contract at `1.0` (#569). No field is
added, renamed, retyped or removed: the emitted shape is exactly the one v35
advertised as report `0.43`, so a consumer written against `0.43` reads a
`1.0` report unchanged. What moved is the promise. `1.x` is additive-only; a
change that cannot be expressed additively needs `2.0`; a deprecation cycle is
counted in shipped releases, never in time on unreleased `main`; and every
published schema URL keeps its bytes forever. The production qualification
policy's `required_report_schema_version` moves with the engine to `1.0`, and
issuance of the `pre_1_0` qualification tier is retired — existing `pre_1_0`
artifacts stay readable, nameable and scoreable, they are simply not produced
any more.
Pre-freeze `0.x` reports are no longer accepted as *input* to this engine.
Every external report boundary — `--diff-from`, `explain-finding`, `findings`,
`scenario suggest`, and `packet` from a report — refuses one by name and names
the regeneration route, instead of validating it against a model whose
defaults would stand in for blocks it never recorded. Every superseded schema
stays published for reading archived artifacts. Operational control shapes are
byte-identical, so `minimum_control_contract_version` stays at `21`. See
[`docs/report-1-0-contract.md`](report-1-0-contract.md).
Previous runtime contract v35 adds manifest-free host comparison evidence in verifier `0.18` and named boundary rows in `shipgate.agent_boundary_result/v3`. Comparison health and captured input identity remain separate from release and control authority. See [the migration note](../STABILITY.md#manifest-free-host-review-contract-v35-684).
Previous runtime contract v34 advertises host inventory, baseline and drift schema `0.4`: workflow permission projections and reusable secret recipients. Operational control is unchanged; older baselines cannot prove that unrecorded recipients were absent. See [the migration note](../STABILITY.md#workflow-capability-comparison-contract-v34-685).
Runtime contract v33 adds `host_boundary_candidates[]` and
`host_discovery_incomplete_paths[]` to discovery. They describe recognized
configuration paths and traversal the bounded census could not see through,
never parsed grants. An incomplete census withholds the product-wide negative
and publishes no candidates; it does not fail the classification. A host-only
workspace follows `audit --host` without creating a manifest. `init` and
`bootstrap` hand off without setup writes; incomplete input cannot publish a
product-wide negative. Existing operational control and persisted evidence
schemas are unchanged; all setup permissions remain false.
Runtime contract v32 separates instruction prose from supported parsed permission
structure across verification, preflight, host drift and generated edit hooks.
It publishes verifier v0.17, handoff v9, preflight v0.5 and host evidence v0.3.
Raw identity still changes on prose edits; legacy evidence is never upgraded to
a new permission claim. `conditional_file_edits` is a standing routing rule with
`grants_authority: false`, separate from unconditional `forbidden_file_edits`.
See [the comparison and migration contract](engineering/instruction-structure-boundary.md).
Runtime contract v31 adds a read-only
[external review decision evaluator](human-review-decision.md) for the bounded
[`human-review-request.json`](human-review-request.md) class. It verifies current
scope, host key trust, reviewer eligibility and expiry and returns separate
evaluation evidence. It writes no artifact and grants no merge or completion
authority. The shared control union and all existing persisted schema
identifiers are unchanged; the minimum control contract stays at 21.
GitHub acquisition and persistence remain an integration obligation.
The single, current statement of what AI coding agents and CI integrations should read from Agents Shipgate output. When the contract changes, update [STABILITY.md](../STABILITY.md) first, then this file. Other agent-facing surfaces (`AGENTS.md`, `llms.txt`, `.well-known/agents-shipgate.json`, the slash command, the skill, the FAQ) link here instead of restating field lists.
For contributors changing the contract: regenerate schemas with
`python scripts/generate_schemas.py`, then run
`python scripts/regenerate_goldens.py` and its `--check` mode using the
[committed sample recipe](../CONTRIBUTING.md#sample-goldens). Review actual
artifact changes and preserve behavioral assertions; sample regeneration does
not freeze a release contract or qualify a candidate wheel.
## Current versions
Verify the installed CLI contract locally before relying on hard-coded docs:
```bash
agents-shipgate contract --json
```
### 1.0.0 declaration-review migration
Runtime contract v29 coordinates the public artifacts that carry changed
action declarations: report v0.43 (now frozen at `1.0`), packet v0.18, and verifier v0.16.
`minimum_control_contract_version` remains 21 because the operational control
envelope is unchanged.
Read
`release_decision.evidence_coverage.semantic_coverage.declaration_review` for
the base-vs-head reviewer projection. It includes added, removed, and
semantically modified declaration rows. A requested comparison that could not
run is distinct from an available comparison with no changes; missing or
conflicting identity and semantic evidence gaps cannot earn an
`evidence_consistent` status. Report Markdown, packets, PR comments,
annotations, and the GitHub summary consume the same bounded projection.
See the
[declaration-review migration note](../STABILITY.md#migration-note-unreleased-declaration-review).
### Earlier projection-only migration
That change moved no schema or runtime-contract version; the v0.42 report
schema gained the typed `unattested_surface` gap and optional
`EvidenceGap.policy_id`.
Completed blocked verifier runs now append the deterministically selected worst
blocker to the plain headline as well as the adoption/self-approval branches;
that same headline feeds `control.reason` and `control.next_action.why`. Runs
with no blocker do not gain the clause. A genuinely incomplete enumeration
remains `incomplete_surface`; a lower-confidence extraction without an
enumeration defect is the distinct `unattested_surface` gap, and only an explicit
`surface: enumerated` adapter fact earns positive enumeration wording. Its
remedy asks for reviewed attestation. Policy gaps retain their exact identity
in structured `policy_id`, while
policy-evidence gap prose no longer prefixes `why` with an engine-owned
`builtin-*` policy id. Public and organization-defined check ids remain stable
labels. Branch on the typed gap kind and `policy_id`, never on the explanatory
sentences. See the
[verifier-explanation migration note](../STABILITY.md#migration-note-unreleased-verifier-explanations).
Standalone trigger evaluation is unchanged. Inside a verifier artifact,
however, its generic route has already been consumed. Embedded
`trigger.next_action` therefore preserves the evaluated kind while clearing
its command and adding `authoritative: false` and
`authoritative_path: "control.next_action"`; the self-referential preview
command is not repeated. Commands on embedded `matched_rules[]` are cleared as
well. In verifier, verify-run, and preview output, follow
only `control.next_action` and `control.allowed_next_commands`. See the
[embedded-trigger migration note](../STABILITY.md#migration-note-unreleased-embedded-trigger-routing).
### Emitted next actions terminate
Following `control.next_action.command` — or the human `Next:` line, which is
the same routing rendered for a person — reaches a terminal. Three properties
hold, and `tests/test_next_action_chains_terminate.py` walks every entry
command on a host-only and a manifest repository to keep them holding:
- **No cycle.** No chain returns to a question it already asked. Output
format is not part of a step's identity: `audit --host` and
`audit --host --json` ask the same thing, and treating them as different
steps is what once hid a three-command loop
(`verify --preview` → `init --write` → `audit --host` → `verify --preview`).
- **No workspace loss.** Every emitted command names the same `--workspace`
it was produced for. A step that drops it does not continue the walk; it
starts a different one against the caller's current directory, which is
the silent retargeting the [invocation policy](diagnostics.md#invocation-policy)
exists to prevent.
- **Every step is runnable.** An emitted command names a Shipgate command
that accepts it. Prose that names no command is a terminal: it hands the
reader a decision, not another lap.
A run that evaluated no gate says so. `verify --preview` on a repository with
nothing to gate reports `execution: "not_run"` and
`applicability: "not_evaluated"`, and the human line reads
`Agents Shipgate verify: not evaluated` rather than `failed`.
Runtime contract v28 publishes the capability delta as a **standalone
attestation**. `verify` writes
`agents-shipgate-reports/capability-delta-attestation.json`: an
[in-toto](https://github.com/in-toto/attestation) Statement whose
`predicateType` is
`https://threemoonslab.com/agents-shipgate/capability-delta/v1` and whose
predicate carries the frozen `shipgate.capability_payload/v1` **delta view**
unchanged. The contract now names the predicate type, both schema versions,
both schema paths, and the artifact, so a consumer can discover the format
without reading our source. Full specification:
[`docs/capability-delta-attestation.md`](capability-delta-attestation.md).
Read it when you want *what the agent can do after this change* as a portable
fact — a runtime gateway, a policy engine, a dashboard, another CI system.
Nothing about the gate moves: the attestation carries no verdict, no severity
and no release impact, and `release_decision.decision` remains the only release
decision signal.
Two things a consumer must branch on. The attestation is written **only for a
committed-tree subject** — a worktree run publishes a note on
`verifier.base_notes[]` and no file, because the bytes it evaluated are not in
any tree object. And `predicate.verification.status` is `bound` or `unbound`:
only `bound` chains to `verification-receipt.json` through `input_set_id`, and
a consumer that needs that chain checks the status rather than probing for
absent fields. Everything else is unchanged from v27.
Runtime contract v27 finishes the v24 rollout on the stream v24 left out.
**Every** agent-mode error line from `detect`, `init`, and `doctor` now carries
the same `shipgate.agent_control/v1` object those commands publish on
`--json`. Before, `doctor`'s failure routes carried one and `detect`'s and five
of `init`'s did not, so whether a caller that routes on `control` could
route at all depended on which setup command had failed and on which of its
failures — a caller cannot branch on that, and the run that most needs a route
is the one that printed no payload to carry it.
No field is removed. `next_action` (single string) and `next_actions[]` (ranked
array) are unchanged on those lines, and are derived from the same selected
route as `control.next_action`, so the three cannot send you to different work.
Three *command values* do move, and they are listed at the end of this section.
**What every setup error line guarantees**, in the published schema and not
only in the producer: `decision_source: "setup"`, a `decision` from
`setup_complete | setup_incomplete | setup_not_applicable`, every field of
`permissions` false, and `control_state` never `complete`. Setup authorizes
nothing, on either stream.
**Do not read `execution` as "this is an error line".** The `error` field says
that. `execution` answers whether the command reached an answer about the
workspace, and both values appear on error lines: `"failed"` when it could not
(an unparseable flag value, discovery that could not be bounded, a manifest it
could not open), and `"succeeded"` with a non-zero `exit_code` when it did and
the answer is a refusal it can route past — `config_already_exists` from
`init --write` declining to overwrite, and the unresolved-scope `config_error`.
The second kind still carries `permissions` all false, because it is still a
setup envelope.
**One stated exception.** The shared `--workspace` refusal (`config_error`,
exit 2, emitted by every command that takes a `--workspace`) carries
`next_action`/`next_actions[]` and no envelope. It fires because the workspace
does not exist, so there is no setup subject for `input_id` to address and no
setup facts for a state to be derived from. Treat an error line with no
`control` as "this is not a setup answer", not as "route on something else".
`scan`, `verify`, and `check` are unchanged and are deliberately not in this.
`scan` and `verify` publish a control *pointer*, so their envelope is the
promoted read `agents-shipgate agent control` — or, for `verify`, `--format
control` directly. `check` publishes no pointer at all and binds its authority
to `input_id` instead; its envelope is `--format agent-control-json`.
`init --write` over a manifest that already exists also publishes a different
route. It reported `next_action.kind: "edit"` on `shipgate.yaml` with
`expects: "The manifest reflects the desired tool sources, agent
declared_purpose, and policies"` — a postcondition the file already satisfied,
because a manifest that does *not* load is claimed by the repair route above
it. On this contract `next_action` **is** the step, so that route could not
change the answer: an envelope-only caller opened the file, found nothing to
change, re-ran, and got the identical action back. It is now the `doctor`
invocation for the manifest on disk, which reports what that manifest still
owes. The exit code (2) and the "already exists — edit it directly or remove
it" sentence are unchanged.
**Unless the request was not applied.** `init --write --control-pack <id>` over
a manifest that selects a different pack keeps a reconciliation route naming
`policies.control_pack` and the exact value. Both onward routes would otherwise
advance under the pack that is *there*, and the request would be lost with
nothing saying so — a recovery that completes with less than the caller asked
for reports success for work it did not do. The same reconciliation is
published for a scoped **candidate** whose manifest selects a different pack,
where a bare `doctor` had the same effect one directory down.
"Asked for" is read from the parser, not inferred: an explicit
`--control-pack default` over a `financial-strict` manifest **is** a request,
and it is the one that can only weaken.
**Who owns that edit is decided by the direction, not by who typed the flag.**
A governed coding agent composes its own argv, so process arguments are not
authenticated human provenance. A transition that keeps at least every
obligation the manifest has today is `agent_action_required` with a typed
`edit`; one that drops any obligation — or names a pack this build cannot
resolve, where the direction is unprovable — is `human_review_required` with no
command, naming the effects and controls it would remove. That is the same
direction `verify_policy` raises `control_pack_weakened` for, computed by the
same function.
**Two command values also move.** Every recovery `init` publishes now repeats
the whole invocation with only the invalid value corrected — `--minimal`,
`--allow-unresolved-scope`, `--agent-instructions-kit`, and a non-default
`--max-python-files` ride along, where before they were dropped; and the
`internal_error` fallback names this invocation with `--minimal` added instead
of a bare `agents-shipgate init --minimal` that ran in the process directory.
These are `command` value changes on `next_action`, `next_actions[0]`, and
`control.next_action` alike, which are one route.
Both the `AgentControl` union and `shipgate.agent_control/v1` are
byte-identical to v26 — the same envelope reaches one more place — so
`minimum_control_contract_version` stays at `21`.
Runtime contract v26 adds the **declaration continuation**: a receipt
``apply-patches`` writes beside the report it applied from
(``declaration-continuation.json``, ``shipgate.declaration_continuation/v1``),
and the one situation in which a **blocked** release decision may authorize
publication. The receipt pins the manifest by byte digest on both sides of the
write; the run additionally compares the two manifests and requires the delta
to be added ``action_surface.actions`` rows and nothing else. On that proof the
control is ``review_publishable`` — ``edit``/``commit``/``push``/``update_pr``
so the drafted proposal reaches a person, ``merge`` and ``report_complete``
still denied. Without it a blocked decision authorizes nothing, exactly as
before. `verifier.json`, `agent-handoff.json` and `verify-run.json` each carry
the resulting `declaration_continuation` boolean, which is why their schema
versions move to `0.15`, `v8` and `v5`.
Runtime contract v25 added one route to the control envelope:
`next_action.kind: "confirm_declarations"`, published by `verify` on a
**working-tree** run whose verdict is `insufficient_evidence`, with no blockers
and no policy weakening, when at least one open declaration question is one the
scan can answer from its own evidence. It is not published on a ref-bound
(`--base`/`--head`) run: `apply-patches` mutates the checkout, so the exact
rerun would re-scan the commit the edit is not in yet — the same precondition
every mechanical repair route carries.
Weakening here is `capability_review.policy_weakening_proven`, plus the
fail-closed `capability_review.policy_weakened` **unless this diff introduces
the gate it is judged by**. That exemption is a separate, narrow, proven fact —
the configured manifest is in the diff, absent at the comparison ref, the diff
removes or renames away no YAML file, and every policy surface it touches is
that manifest — so there is no prior version of this gate the change could have
loosened. It exists because `policy_weakened` stays raised whenever the
direction could not be established, and establishing it means proving no file
in the tree parses as a manifest under any name: one blob past the probe's read
bound ends that proof, which is the normal case on a real repository. Gating
the route on the flag alone made a first adoption — the run with every question
still open — the one run that could never be offered it. Nothing else moves:
the flag, the verdict, the adoption wording, and the human route are all
unchanged, and a proven weakening still refuses.
- **What it carries.** The exact `apply-patches` command that writes those
answers, plus `questions[]` — open declaration questions, each tagged
`authorable_by: "coding_agent" | "human"` — and the two counts
`agent_authorable` / `human_authorable`, which are the real totals. The list
is a prefix capped at six rows, ordered human-owned first (a drafted row is
answered by the command whether or not it is printed; a human-owned row is
what you have to hand a person), each half keeping the report's ranking. Read
`release_decision.evidence_coverage.semantic_coverage.declaration_questions.open_questions[]`
for the unabridged list.
- **What it authorizes.** Exactly the named command, plus whatever
`permissions` says — publish-only on this route: `edit`, `commit`, `push`,
`update_pr` true; `merge` and `report_complete` false. Writing declarations
into `shipgate.yaml` touches the trust root, so the change still reaches the
gate only through a human merge. The route is a proposal step, never a
completion.
- **The content rule.** A row is `authorable_by: "coding_agent"` only when the
scan filled every blank in its `declaration_template` — from the closed
effect vocabulary, never weaker than any reading it observed — *and* the
question is not one that asks a person to look again: a `declaration_drift`
row restates a confirmed answer beside a moved pin and stays `"human"`,
because an agent re-stamping the pin would close the very request the row is
for. A template still carrying a `<REVIEW_REQUIRED>` blank (every authority
block, every override) stays `"human"` too. Authorship is decided by content,
never by who is running: an agent may propose what the evidence supports, and
only a human may assert against it.
- **What the patch may write.** `next_action.patch` is exactly
`declaration_template`, split into the keys that name the action and the
fields that are written — the schema rejects any other pairing, so a row
cannot advertise an evidence-derived tag beside a patch that writes something
else. Its `target_path` is relative to `manifest_dir`, so the row means the
same thing in the packet, the SARIF file, and a cached base scan.
`apply-patches` writes only into fields the manifest leaves silent, and
refuses — exit 5, nothing written — when a row already answers one
differently, when two equally compatible rows name the same tool, or when the
manifest has changed since the scan.
- **After it, in this order: rerun, then publish.** The command edits
`shipgate.yaml`, so the moment it succeeds this control is stale —
`agents-shipgate agent control` refuses with `workspace_changed` ("the
working tree carries 1 uncommitted change this decision never saw"), and the
v20 refresh rule requires that read before any commit, push, or PR update.
The permissions printed beside the route were computed against a manifest
that no longer exists and cannot authorize publishing what the command just
wrote, which is why `expects` promises the write and the supersession rather
than a commit. Re-run verification, then act on what *that* run authorizes.
It is a fresh decision, and a declaration whose whole purpose is to make a
risk judgeable is exactly what can move the verdict to one only a person may
clear. If questions remain, they are the ones tagged `"human"`, and the human
route names them rather than asking for generic review.
- **When it is withheld.** `report.json` publishes every open question with
`authorable_by` resolved whether or not the route is published, so a control
that offered nothing and explained nothing read as an invitation to write the
manifest without the route. The headline — and therefore `control.reason` and
`control.next_action.why` — now carries one sentence naming the cause, for
example `3 declaration(s) this scan could draft are withheld: a blocker is
open, and that decision is a person's.` It is produced by the same pass that
withholds the route, so the cause published is the cause that acted, and only
a comparison that actually ran may say the gate was weakened. It appears only
where a drafting route existed to publish and something refused it; a run
with no agent-authorable question withheld nothing and says nothing.
**It is an explanation, not a machine field.** Like every headline context
clause it shares the 400-byte prose budget with the verdict, the worst
blocker, the gap-provenance clause and the reserved human-review
requirement, and it is dropped whole — after gap provenance, which names a
subject that left the analysed surface and outranks it — rather than
truncated. A run whose verdict and blocker have already spent the budget
publishes no context at all. Do not branch on its presence.
The `AgentControl` union is unchanged again, so
`minimum_control_contract_version` stays at `21`: the control holds the step as
the `repair` command it truthfully is, and the envelope publishes the richer
form — the same split `SetupEditAction` uses.
Runtime contract v24 rolls the control envelope across the setup commands.
`detect --json`, `init --json`, and every `doctor --json` payload now carry a
`control` field holding the same `shipgate.agent_control/v1` object that
`verify`, `check`, and `agent control` emit, so one vocabulary answers "what may
I do next" for the whole adoption walk. Two things keep the setup family
distinguishable from the gate:
- **Setup names its own source.** These commands run before a release decision
exists, so they report `decision_source: "setup"` and a `decision` from the
closed setup vocabulary `setup_complete | setup_incomplete |
setup_not_applicable`. The published schema enforces the pairing both ways: a
setup source can only come from `detect`/`init`/`doctor`, and those operations
can report no other source. `decision_source: "release_decision"` still means,
and only means, `release_decision.decision`.
- **Setup authorizes nothing.** No setup command reads a diff, so every field of
`permissions` is `false` on every setup envelope, no setup envelope binds an
artifact or a `current_control_id`, and `control_state: "complete"` is
unreachable for these operations in the schema itself. Setup routes; it never
finishes a task.
`agent control` on a `scan` generation publishes **no** release verdict. `scan`
reaches one, but its pointer records no HEAD, no worktree overlay, and no input
set, so nothing about that verdict can be reconfirmed against the workspace as it
stands: editing the manifest, a `tools.json` it references, a policy pack, or a
baseline leaves the pointer reading cleanly. What the envelope carries instead is
`reason`, stating why there is no verdict — which is what keeps it
distinguishable from an envelope produced before any engine ran, and is the
ambiguity #323 set out to remove. Run `verify` for a verdict a reader can check.
The `AgentControl` union is **unchanged**, and `minimum_control_contract_version`
stays at `21`. That union is embedded by the verifier, the handoff, preflight,
the agent result, the boundary result, and verify-run, so widening it would widen
six durable published schemas under unchanged identifiers — and five of those
artifacts record no `contract_version`, so a consumer holding a stored payload
could not use the floor to tell which shape it has.
A setup step that needs a file changed is still *typed*, though: the envelope
publishes `next_action.kind: "edit"` with `path` and `expects`, as
`SetupEditAction` — declared on the envelope, which is stdout-only, and rejected
in both layers on any non-setup operation. Routing such a step as the command
that merely *checks* the edit was tried and is wrong: an envelope-only consumer
executing it re-ran `doctor` against an unchanged file forever.
What v24 widens is `shipgate.agent_control/v1` itself, which is emitted on
stdout and never written as an artifact: there are no stored envelopes to
disambiguate, and its new operations cannot appear in anything a v21 consumer
holds.
A human-owned manifest declaration is never published as a coding-agent edit.
When `shipgate.yaml` still holds an unresolved `declared_purpose`, policy, or
permission placeholder, the setup control state is `human_review_required` and
the action names the exact file, line, and field a person must fill in. The
command-specific `next_action` / `next_actions[]` fields are unchanged, and
remain supported.
Runtime contract v23 spells every emitted command for the invocation that
produced it. A run started with `python -m agents_shipgate` now proposes
`<sys.executable> -m agents_shipgate ...` instead of a console script its
environment may not have; a console-script run is unchanged. Actions with
`kind="command"` in `next_actions[]` also carry `executable[]` and `args[]` —
**the authoritative runnable form on every platform**; run them as
`[*executable, *args]` with no shell. The pair is computed from `command` and
cannot be supplied, so the two forms cannot disagree, and it is omitted (not
`null`) whenever the command has no faithful argv form. `command` itself is a
POSIX rendering for display and POSIX shells. When the rank-1 action is a
command, the legacy `next_action` string is that command verbatim. Set `AGENTS_SHIPGATE_CLI` to
name the entry point explicitly. Durable evidence artifacts (`report.json`,
`packet.*`) stay canonical: "same inputs, same report" outranks runnability
there, and process-entry spelling is not an input.
Runtime contract v22 publishes `shipgate.agent_control/v1`, the compact control
envelope. It is a **projection of the control state, not a second decision**:
every field is copied from a producer that already published it, and the
schema's validators only assert that the copies cannot contradict each other.
One object answers the whole routing question — tool execution status, the
release or boundary decision and which engine made it, the control state, the
six-way `permissions` vector, who acts next, the exact next action, and the
content-addressed path and hash of every artifact `current-control.json`
binds — not every file a run writes, and none at all from `check`, which
publishes no pointer. It is emitted by
`agents-shipgate verify --format control`, `agents-shipgate check --format
agent-control-json`, and `agents-shipgate agent control` (now its default
output; `--format pointer` returns the raw pointer). It publishes a size *budget*,
`agent_control_budget_bytes` (6144), that representative output meets and that
is pinned by tests; it is not a hard maximum, because a long required-reviewer
list or an unusually long exact command must never be truncated to hit a size
target. Free-text fields are capped at 400 UTF-8 bytes; commands, paths, hashes,
and reviewer names never are. It is never written to disk — the artifacts it
names stay where they are.
The envelope is a **discriminated union on `control_state`**, like the
`AgentControl` union it projects, so the published JSON Schema — not only
Pydantic — rejects a contradictory payload: `execution: "failed"` beside
`control_state: "complete"`, a coding-agent route on a stopping state, a
`review_publishable` that denies publication, or merge authority outside
`complete`.
Three separations are structural, not documentary. `execution` says whether the
tool ran; `decision` says what the gate decided; `permissions` says what the
agent may do. A failed run can never authorize completion, but a *succeeded* one
carries no implication at all.
Both entry points apply one currency test. `verify --format control` reads the
pointer it just published through the generation-safe protocol, validated
against the live workspace, and withholds authority when the workspace has moved
past what the run evaluated; the route comes from the verifier bytes captured
inside that read, so a pointer is never reported beside another generation's
decision. `artifacts[].path` is relative to the directory the command was
invoked from. `exit_code` reports the CI gate signal, which is
mode-dependent — in advisory mode a `blocked` decision still exits 0 — so
`permissions.merge` is the only field that answers "may I merge". The full
`verifier.json` remains the authoritative substrate and is unchanged;
`verify --json` still emits it.
Runtime contract v20 adds `agents-shipgate-reports/current-control.json`,
the one atomic entry point that says which control identity is current. It is a
pointer, not a second decision: it binds identities and hashes the receipt,
handoff, verifier, and report already published. Every run replaces it with a
non-terminal `unavailable` marker before touching any other artifact, and
publishes the terminal pointer atomically, last. Consumers must re-read it at
every boundary in `agent_refresh_triggers` — after any human or external-tool
action, after any worktree change, after any command returns, before enforcing a
cached `must_stop`, before commit/push/PR update, before merge, and before
declaring the task complete. A control state remembered from earlier in a
conversation never outranks the pointer, in either direction: it can neither
keep blocking after a newer complete run exists, nor authorize action after the
workspace moved.
Runtime contract v21 separates publish authority from merge authority.
`control.permissions` is a required object on every state with the exact
booleans `edit`, `commit`, `push`, `update_pr`, `merge`, `report_complete`,
fixed by the state and never set independently, and the new
`control.state: "review_publishable"` means "a human must approve the merge,
and the agent may still commit, push, and update the pull request to obtain
that review". `human_review_required` keeps its exact old meaning — nothing is
authorized — and is now reserved for results Shipgate cannot vouch for: a
`block` decision, a failed run, unreadable or unbindable diff input, an
undeclared surface with no discovery route, and preflight protected-surface
touches. `merge` and `report_complete` always equal `completion_allowed`, so
human review never becomes self-approvable.
Runtime contract v19 grades the LOCAL boundary stop: a `require_review`
violation set that is entirely low/medium risk projects
`control.state: "agent_action_required"` with the exact verify command, and
the review obligation is carried in the additive
`pending_review[]` field on the agent-boundary result instead of ending the
turn. Block actions, critical risk, gate-weakening rules
(`CODEX-AGENTS-SHIPGATE-REQUIREMENT-REMOVED`), unparseable content,
incomplete input, experimental surfaces, and every gate-governing trust-root
class (manifest, policy, ci_gate, shipgate_state) keep the human route; as of
v20 an evaluated `require_review` route is `review_publishable` and a `block`
route is `human_review_required`. PR-time `release_decision` semantics are
unchanged. It retains the v18 human-authorization overlay, the v17
content-addressed verification identity,
v16 typed policy-evidence, v15 host-neutral
boundary, v14 unambiguous `AgentControl`, and v13 root-reachable binding
contracts. v18 added a signed, externally rooted human-authorization overlay for
one exact post-review coding-agent action. Agents
switch on `control.state`; `decision` remains diagnostic and
`release_decision.decision` remains the release gate. Contract v14 requires
`completion_allowed == (state == "complete")` and
`must_stop == (state == "human_review_required")`. The report, packet,
verifier, verify-run, and handoff projections all bind to the same request
and decision IDs; their current versions are listed once under *Current
schema versions* above rather than restated here, where the list went stale. The terminal receipt hashes the complete
artifact set; see [Verification Identity and Reproduction](verification-reproducibility.md).
The runtime contract also exposes the local agent command spec:
`primary_commands{}`, `commands{}`, `default_paths{}`, `artifacts{}`,
`agent_read_order[]`, `verifier_read_order[]`, `merge_verdicts[]`,
`release_decisions[]`, `do_not_auto_assert[]`, `verifier_schema_version`,
`verify_run_schema_version`, `verification_plan_schema_version`,
`verification_unit_result_schema_version`,
`verification_artifact_manifest_schema_version`,
`verification_receipt_schema_version`,
`human_authorization_request_schema_version`,
`human_authorization_schema_version`,
`human_authorization_evaluation_schema_version`,
`human_authorization_trust_policy_schema_version`,
`human_authorization_trust_policy_default_path`,
`human_authorization_schema_path`, `agent_handoff_schema_version`,
`agent_handoff_schema_path`, `agent_handoff_artifact`,
`agent_boundary_result_schema_version`, the deprecated
`codex_boundary_result_schema_version`, `attestation_schema_version`,
`registry_schema_version`, `org_evidence_bundle_schema_version`,
`host_grants_inventory_schema_version`, `host_grants_baseline_schema_version`,
`host_grants_drift_schema_version`, `trigger_catalog_schema_version`,
`agent_interface_operations[]`,
`exit_code_policy`, `mcp_tools[]`, `minimum_control_contract_version`,
`agent_control_fields[]`, and `agent_control_states[]`. The legacy
`agent_result_*` fields are retained only for older protocol readers.
`primary_commands{}` is the prominent
entry surface and contains only `shipgate check`, `agents-shipgate verify`, and
`shipgate audit --host` flows; `commands{}` is compatibility/supporting metadata
and retains local verify commands for older consumers.
The short `shipgate verify` alias remains invokable for compatibility, but it is
not the promoted PR-gate spelling in `primary_commands{}`.
Contract v11 adds `action_effect` and `action_authority` to
`do_not_auto_assert[]`. They are reviewed human claims that can close semantic
evidence gaps; an agent may route the structured next action but must never
invent or auto-fill either declaration.
Downstream repos generated with
`init --agent-instructions=default` get the minimal local copy at
`.shipgate/agent-contract.json`.
- Latest release: `v1.1.0`
- In-tree runtime: `1.1.0` — see [pyproject.toml](../pyproject.toml)
- Runtime contract: `41` (minimum control contract: `21`)
- Current report schema: `1.0`, frozen, superseding `0.43` — [`docs/report-schema.v1.0.json`](report-schema.v1.0.json); the `1.x` rules are in [`docs/report-1-0-contract.md`](report-1-0-contract.md)
- Current packet schema: `0.18` — [`docs/packet-schema.v0.18.json`](packet-schema.v0.18.json)
- Current shared agent result schema: `agent_result_v3` — [`docs/agent-result-schema.v3.json`](agent-result-schema.v3.json)
- Current verifier schema: `0.21` — [`docs/verifier-schema.v0.21.json`](verifier-schema.v0.21.json) (`0.20` and earlier stay frozen; `0.20` says what each host comparison established, source by source, and `0.21` also names the changed inputs it does not read)
- Current verify-run schema: `shipgate.verify_run/v5` — [`docs/verify-run-schema.v5.json`](verify-run-schema.v5.json)
- Current verification identity schemas: [`plan v1`](verification-plan-schema.v1.json), [`unit result v1`](verification-unit-result-schema.v1.json), [`artifact manifest v1`](verification-artifact-manifest-schema.v1.json), and [`terminal receipt v1`](verification-receipt-schema.v1.json)
- Current control pointer schema: `shipgate.current_control/v1` — [`docs/current-control-schema.v1.json`](current-control-schema.v1.json)
- Current agent control envelope schema: `shipgate.agent_control/v1` — [`docs/agent-control-schema.v1.json`](agent-control-schema.v1.json)
- Current human-authorization schemas: request, signed grant, verifier evaluation, and external trust policy v1 — [`docs/human-authorization-schema.v1.json`](human-authorization-schema.v1.json)
- Current agent handoff schema: `shipgate.agent_handoff/v9` — [`docs/agent-handoff-schema.v9.json`](agent-handoff-schema.v9.json)
- Current agent boundary result schema: `shipgate.agent_boundary_result/v3` — [`docs/agent-boundary-result-schema.v3.json`](agent-boundary-result-schema.v3.json)
- Frozen deprecated Codex projection: `shipgate.codex_boundary_result/v2` — [`docs/codex-boundary-result-schema.v2.json`](codex-boundary-result-schema.v2.json)
- Current preflight schema: `0.5` — [`docs/preflight-schema.v0.5.json`](preflight-schema.v0.5.json)
- Current downstream local agent contract schema: `10`
- Current capability standard: `0.5` — [`docs/capability-standard.md`](capability-standard.md)
- Current capability lock schema: `0.8` — [`docs/capability-lock-schema.v0.8.json`](capability-lock-schema.v0.8.json)
- Current capability lock diff schema: `0.9` — [`docs/capability-lock-diff-schema.v0.9.json`](capability-lock-diff-schema.v0.9.json)
- Current capability payload schema: `shipgate.capability_payload/v1` — [`docs/capability-payload-schema.v1.json`](capability-payload-schema.v1.json), specified in [`docs/capability-payload.md`](capability-payload.md)
- Current capability delta attestation: `shipgate.capability_delta_attestation/v1`, predicate type `https://threemoonslab.com/agents-shipgate/capability-delta/v1` — [`docs/capability-delta-attestation-schema.v1.json`](capability-delta-attestation-schema.v1.json), specified in [`docs/capability-delta-attestation.md`](capability-delta-attestation.md)
- Current attestation schema: `0.5` — [`docs/attestation-schema.v0.5.json`](attestation-schema.v0.5.json)
- Current registry schema: `0.4` — [`docs/registry-schema.v0.4.json`](registry-schema.v0.4.json)
- Current org evidence bundle schema: `shipgate.org_evidence_bundle/v2` — [`docs/org-evidence-bundle-schema.v2.json`](org-evidence-bundle-schema.v2.json)
- Current host-grants inventory, baseline, and drift schemas: `0.7` — [`inventory`](host-grants-inventory-schema.v0.7.json), [`baseline`](host-grants-baseline-schema.v0.7.json), [`drift`](host-grants-drift-schema.v0.7.json)
- Current trigger catalog schema: `0.4` — [`docs/triggers.json`](triggers.json)
- Current governance benchmark catalog schema: `0.2` — [`docs/governance-benchmark-catalog-schema.v0.2.json`](governance-benchmark-catalog-schema.v0.2.json)
- Current governance benchmark result schema: `0.2` — [`docs/governance-benchmark-result-schema.v0.2.json`](governance-benchmark-result-schema.v0.2.json)
- Frozen-reference report schemas: frozen [`v0.33`](report-schema.v0.33.json), frozen [`v0.32`](report-schema.v0.32.json), frozen [`v0.31`](report-schema.v0.31.json), frozen [`v0.30`](report-schema.v0.30.json), and older versions listed in [`docs/INDEX.md`](INDEX.md#reference)
- Frozen-reference packet schemas live in [`docs/INDEX.md`](INDEX.md#reference).
- Boundary v1, verifier v0.1–v0.5, verify-run v1/v2, handoff v1–v5, and preflight
v0.1/v0.2 remain frozen references for legacy readers.
- Frozen experimental capability lock and governance benchmark result schemas live in [`docs/INDEX.md`](INDEX.md#reference).
## Two read entry points
Both start at `agents-shipgate-reports/current-control.json` (`agents-shipgate
agent control --workspace .`), which names the run that is current. Everything
below it describes *a* run; only the pointer says *which* run. A non-zero exit
from the reader means no control identity is current here and the caller holds
no authority — not that the previous answer still stands.
Without `--reports-dir`, the reader looks where a default `verify --workspace`
publishes: `agents-shipgate-reports` under `--workspace`, so `agent control
--workspace <repo>` finds that run from any directory. An explicit
`--reports-dir` is read as given — a relative path resolves against the current
directory — and no other directory is searched in its place. An explicit
`verify --out` follows the same rule, as do `scan --out` and `audit --host
--out`, so `verify --out <dir>` and `agent control --reports-dir <dir>` typed in
one shell name one directory. Where a relative `--out` names a different
directory than the `1.0.0` rule did (the Git root for `verify`, the manifest's
directory for `scan`), a `note:` line on stderr names both. A refusal names
the absolute directory it searched, and its recovery command writes back to it,
naming it as an absolute `--out` even where the producing run's recorded command
carried a relative one; a `verify` command a run emits names `--out` absolutely
whenever it carries one.
The reports directory is left out of the change set the reader checks, just as
`verify` leaves its output directory out of the change set it decides on, so
it must hold nothing else Git would report. A directory inside the repository
that holds a committed path (at `HEAD`, or at the merge base a worktree pointer
records and since removed), a staged or untracked unignored path that is not a
Shipgate artifact, or any trust-root path whatever its name, or that lies
inside a trust root Git does not ignore, is refused as `workspace_unverifiable`
whatever the pointer's state, and `verify` refuses to write into one. The
recovery for that refusal is the producing run's own verification command with
only `--out` removed, publishing into the default directory with the same
`--config`, base and policy options, or a review step when the default
directory is the one refused (recognized by physical identity). A gitignored
directory, one outside the repository, and one holding only uncommitted
Shipgate artifacts outside any trust root are read as before.
**The promoted read for a coding-agent control loop is one command.**
`agents-shipgate agent control --workspace .` runs the currency protocol and
returns the `shipgate.agent_control/v1` envelope, which already carries the
route the pointer deliberately omits. An agent that routes on `permissions` and
`next_action` from that one object never needs the artifact walk below; the walk
remains the contract for consumers that want the forensic detail, for CI, and
for anything reading the artifacts directly. A run can skip the second command
entirely with `agents-shipgate verify --format control`, which emits the same
envelope for the run it just performed.
The compact reader validates **every entry in `current-control.json.artifacts`**.
It does not follow the terminal receipt into optional artifacts absent from that
map. For example, a successful read does not validate `human-review-request.json`
merely because the bound receipt references it. Changed, missing, oversized, or
symlinked receipt-only files leave the compact read unchanged; those same
conditions on pointer-bound files make it refuse.
For API consumers, `read_current_control(..., capture=...)` returns selected
bytes from the same pass that validated every pointer entry. `capture` neither
reduces validation nor adds optional files: a requested unbound key is absent
from the returned `artifacts`. Consume these captured bytes instead of reopening
their paths. A consumer that needs a receipt-only artifact must also call
`load_validated_receipt_artifacts`, compare its returned receipt with the
pointer-captured `verification_receipt`, and consume the returned closure bytes.
The full loader rejects invalid optional files. It proves a coherent artifact
snapshot, not current workspace state or permission to act; retain the currency
checks below and revalidate before consequential actions. See the
[consumer boundary and resource bounds](verification-reproducibility.md#current-control-and-receipt-closure)
for the exact distinction. Standalone renderers such as `agent handoff` do not
implicitly perform this current-control protocol.
Byte consistency is not generation consistency. A pointer whose artifacts all
still hash correctly can describe a workspace that one commit has moved past, so
the reader compares the bound `workspace_identity` against the live repository —
repository, HEAD commit, and HEAD tree — and refuses on any drift. Completion
authority is never returned without that comparison: a reader that cannot
resolve the workspace reports it as unverified rather than passing. Nor is any
other state of a pointer that binds a Git identity (a HEAD, a base, a merge
base, an overlay or a snapshot kind): when `agent control`, `verify --format
control` or the human-review decision reader cannot observe the workspace at
all — `.git` is missing or unreadable, or Git refuses the checkout — that
pointer is refused with exit `4`, `workspace_unverifiable`
(`workspace_unverified` for a `complete` pointer), whatever its state. A
pointer that binds no Git identity, such as `verify --preview` run outside a
Git checkout, has nothing to compare and still reads.
Every refusal caused by a workspace that could not be read leads with why, in
one bounded, redacted sentence that names the Git configuration key or path
and never a configuration value or file content, and `agent control`'s
`next_actions[0]` follows that cause:
- **Git configuration the worktree readers refuse**: an executable
`filter.<driver>.clean`/`smudge`/`process` in the repository's own Git
config, a `filter=` attribute on a tracked path (any repository with Git
LFS-tracked files has one), repository-local `diff.*` configuration (git-crypt sets
`diff.git-crypt.textconv`), or a non-empty `.git/info/attributes`. The
action is `kind: "review"` for a human. Re-running `verify`, with or without
`--head`, does not change the answer: the refresh reads the working tree
under that configuration for every pointer, a committed-tree one included,
so **no `verify` or `agent control` answer in such a repository reads as
current while the configuration is in place**. Shipgate never advises
removing it. `agents-shipgate diff` still reports the host-grant changes,
read-only and without authority.
- **A reports directory that holds repository content**: the recovery above,
away from that directory.
- **An uncommitted change beyond a static read bound, or a Git timeout**: the
producing `verify` command, after committing or shrinking the change.
- **Anything else**, including a workspace that is not a Git checkout: the
producing `verify` command, as before.
Global and system Git configuration are not read, so an LFS filter configured
only globally (`git lfs install`) does not trigger the first cause; a tracked
file carrying the `filter=lfs` attribute does.
When the decision named a base, that base is compared too. A decision about
`base...HEAD` is a decision about that range, and advancing the base — a merge,
or a fetch moving `origin/main` — can empty the range without touching HEAD or
the working tree, leaving every HEAD-based check satisfied while the evidence
underneath has gone. The pointer therefore carries `base_ref`,
`base_commit_sha`, and `merge_base_sha`, and the reader resolves the ref live.
Uncommitted work is checked according to what the decision actually covered:
- A **worktree** decision (`snapshot_kind: "worktree_overlay"`) is re-checked
two ways. Every path it covered must still hash to the overlay it committed
to, and no path *outside* that set may differ from HEAD now — anything outside
it was identical to HEAD when the decision was made, so a live change the plan
never recorded is evidence the decision never saw. That second test is a
subset test, not equality: `plan.inputs.changed_paths` is the union of
`base...HEAD` and the worktree, not the uncommitted set, so requiring equality
would refuse a clean workspace the moment the run that produced it finished.
- A **committed-tree** decision (`snapshot_kind: "committed_tree"`) stops at
HEAD, so any uncommitted change appearing afterwards invalidates it — in both
directions. A stale `complete` must not authorize work the decision never
covered, and a stale `human_review_required` must not keep enforcing a
pre-change stop. Re-running the same archived `--head` verification cannot
clear that, so the refusal routes to a worktree verification instead.
- A **preview** (`operation: "preview"`) decided nothing about the inputs a
plan names, so its pointer binds no `verification_plan` — also in a
repository with a manifest, where the preview still records the plan a
`verify` would run and `verify-run.json` embeds it. Its path set is
therefore not recorded: the overlay of every path that differs from HEAD is
recomputed live and compared with the one it was read from, so any tracked
edit, new untracked file or removal refuses it as `workspace_changed`, and
restoring the tree it read makes it current again. A configured and a
manifest-free preview are read the same way, whether their reports are in
the default directory, another directory inside the repository or beside
it (#807). Inside a repository such a pointer always declares the worktree
snapshot, even when its overlay could not be read, so under Git
configuration the worktree readers refuse it is refused with the cause like
every other pointer; one published over an overlay it could not read is
current, once the tree can be read, only over a tree identical to HEAD.
A pointer that does bind a plan is read against that plan's
recorded inputs, and one whose plan lacks its input-directory census is
refused as `workspace_unverifiable`, as before; see
[the census](verification-reproducibility.md).
An overlay row carries content *and* the two metadata axes Git itself tracks:
entry kind and the executable bit. Content alone is not the capability —
flipping a tool script from `100755` to `100644` changes no bytes, and swapping
a regular file for a symlink to an identical in-repo file changes no bytes
either. Full mode is deliberately not recorded: it varies with umask and would
make the identity depend on noise Git does not track.
Given a current pointer, there are two correct "read first" paths; which one
applies depends on who is reading. They are not two decisions — they are two
entry points into the same one decision engine.
- **PR / controller flow** — an autonomous coding agent deciding *continue,
repair, or stop*. Prefer
validate `agents-shipgate-reports/verification-receipt.json`, then read
`agents-shipgate-reports/agent-handoff.json` for the compact
`shipgate.agent_handoff/v9` view: lead with `control.state`, then read
`control.next_action`, `gate.merge_verdict`, and `reproducibility.run_id` for the
content-addressed verify identity. `verifier.json` remains the authoritative
controller substrate and `verify-run.json` remains the detailed run
projection; finally
confirm `report.json.release_decision.decision` for the release gate.
`.well-known/agents-shipgate.json` → `agent_read_order` is the
machine-readable cross-artifact order. `verifier_read_order` remains the
intra-`verifier.json` field order.
- **Gate / CI flow** — deciding pass/fail, or any raw `report.json` consumer.
Read `agents-shipgate-reports/report.json` → `release_decision.decision` (the
next section). `.well-known` → `gating_signal` names this signal.
`merge_verdict` is a deterministic projection of `release_decision.decision`, so
the two can never disagree.
### Command-scoped artifact lifecycle
Choose the read path from the command that just completed, not merely from
filenames already present in the output directory:
- After standalone `scan`, `report.json.release_decision.decision` is
authoritative. `scan` writes report, advisory scaffold, and configured
packet formats; it does not produce a verifier handoff or terminal receipt.
- After `verify`, validate `verification-receipt.json`, then read
`agent-handoff.json` and the supporting verifier artifacts in the order
above. The receipt and handoff retain the content-addressed identity of that
exact verify run.
`current-control.json` records which of those two just happened in its
`operation` field, so the choice does not have to be inferred from filenames at
all. Only an `operation: "verify"` pointer can carry `control.state:
"complete"`, and only when it also binds a `verification_receipt` whose
`request_id` and `decision_id` are the ones the pointer records — the assembler
accepts any `--out` name under its artifacts root, so an older canonical receipt
must not be mistaken for the one a run just closed. A `scan` or `preview`
pointer is structurally incapable of authorizing completion or merge, and each
binds only the artifacts it actually wrote: a `scan --format markdown` after a
verify does not claim that verifier's `report.json`. A `preview` binds its
verifier route and never the `verification-plan.json` a manifest lets it
record, because it read none of that plan's inputs. While a run is in flight the pointer reads
`lifecycle_state: "in_progress"` with `control.state: "unavailable"`,
`must_stop: true`, so an interrupted or crashed run leaves a directory that
denies cached control rather than one that still authorizes it. Consumers built
before the pointer existed fall back through
`current_control_fallback_read_order`; the pointer's absence is evidence of an
older producer, never permission.
When standalone `scan` replaces a report set in the same output directory, it
removes the complete prior verifier route and its identity support:
`verifier.json`, `agent-handoff.json`, `pr-comment.md`, `verify-run.json`,
`verification-plan.json`, `verification-input.diff`,
`verification-base-report.json`, `verification-unit-result.json`,
`verification-artifacts.json`, `verification-receipt.json`, and
`human-authorization.json`. Their absence is intentional: an older controller
substrate, route, or receipt must never appear to authorize or describe the
newer scan. `verify` calls the same scan pipeline internally and writes a fresh,
mutually consistent verifier artifact set afterward. Supporting commands that
only need an in-memory report, including `baseline save`, scan into an isolated
temporary directory so they preserve the current report and verifier evidence.
## Reader strings are in the reader's language
`headline`, `summary`, `control.reason` and every `why` are read by a
person. They carry no field paths and no internal nouns —
`tests/test_reader_vocabulary.py` runs every shipped fixture and fails on
`control.state`, `input_set_id`, `binding graph`, `Route H`,
`exclusion ledger`, or an enum identifier written into a sentence.
The enum *values* are untouched: `release_decision.decision` is still
`insufficient_evidence`, because a machine consumer gates on it. What the
guard forbids is that identifier appearing in prose written for a person.
"The agent's tool binding graph is incomplete" became "some tools cannot be
traced to an agent that can call them" — the thing to fix, rather than the
model it is fixed in.
Root `--help` lists the six commands that carry a reader from a fresh
checkout to an answer: `diff`, `check`, `verify`, `audit`, `init`,
`doctor`. `--help-all` lists every command. Prominence is a reading aid,
never a claim about what exists.
## Primary vs supporting surfaces
Primary gates are intentionally narrow. CI gates on
`report.json.release_decision.decision`. Coding agents handling committed PRs
read `agent-handoff.json.control.state` first, with
`verifier.json.control`, `execution`, `applicability`, and `merge_verdict` as the
authoritative detailed substrate. Everything else in the
verifier/report/packet family is supporting review evidence or a convenience
projection.
Treat legacy `agent_result_v1` / `agent-result.json` compatibility surfaces,
runtime trace/evidence fields, the Release Evidence Packet, `reviewer_summary`,
`verifier_summary`, `capability_review`, non-gating capability diff
projections, and `agents-shipgate skill ...` review output as
supporting/provisional surfaces. They may be useful for routing and review, but
they do not replace the gate above and must not introduce a second verdict.
`agents-shipgate preflight --workspace . --plan - --json` remains a supporting
proactive routing surface for coding agents before edits. It accepts a single
`PreflightPlanV1` object with `changed_files[]`, optional `diff_text`,
`capability_requests[]`, `host_permission_requests[]`, and
`context.{agent,task}`. The emitted `PreflightResultV5` reports protected
surfaces, forbidden shortcut actions, required evidence for proposed high-risk
capabilities, host-grant drift when a host baseline is present, deterministic
`signals[]`, `control`, `requires_verify`, `verification_command`,
`allowed_next_commands[]`, and `plan_summary`. A concrete, resolvable diff that
only appends valid built-in `tool_sources` rows may mark that manifest touch
`requires_human_review=false` and route the coding agent to verification. This
authorizes proposal authorship only: existing rows and all other manifest
values must be unchanged, authority-bearing fields and custom adapters are
excluded, and the resulting trust-root diff still requires human review. It is
not a second gate; it must never be read as passed or mergeable. The release
gate remains `release_decision.decision`.
## Read these first for release gating
In `agents-shipgate-reports/report.json`:
- `release_decision.decision` — `"blocked"` / `"review_required"` / `"insufficient_evidence"` / `"passed"`. Baseline-aware. **This is the gating signal.** Precedence is `blocked` → `review_required` (active high/critical named concern) → `insufficient_evidence` → `review_required` (known review concern) → `passed`. Starting in v0.29, `passed` means every in-scope action has complete, conflict-free static surface, effect, and authority evidence, all applicable controls were evaluated, and no policy condition requires review. It does not prove runtime behavior or enforcement. Any required semantic dimension that is unknown, inferred-only, protocol-defaulted, partial, conflicting, invalid, or incomplete prevents `passed`, even when every other action is healthy. Existing extraction thresholds remain: low-confidence tools at least `max(1, ceil(tool_count × 0.5))` or more than three source-loader warnings also degrade evidence. `insufficient_evidence` means the scan cannot confidently gate release from the available static evidence; it does not prove the agent is unsafe. Switch on the enum with a `review_required` fallback for unknown future values.
- `release_decision.blockers[]` — items that block release on this run.
- `release_decision.review_items[]` — items the human reviewer should look at; includes baseline-matched accepted debt.
- `release_decision.{static_analysis_only,runtime_behavior_verified,static_verdict_disclaimer}` (v0.29+) — the machine-readable verdict boundary. Emitted values are `true`, `false`, and the canonical static-verdict disclaimer respectively. Packet §1 mirrors them exactly. Preserve these fields in agent summaries; `passed` must never be rewritten as runtime verification or safety proof.
- `release_decision.{blockers,review_items}[].capability_refs` (v0.24+) — stable capability IDs copied from the originating finding when a policy or policy-pack rule matched a `CapabilityFactV1`. Empty for findings that are not capability-policy matches. This is audit metadata only; `release_decision.decision` remains the gate.
- `release_decision.{blockers,review_items}[].capability_trace_refs` (v0.25+) — stable local trace-evidence IDs copied from the originating finding when an existing trace/evidence check used declared local trace artifacts. Empty when no local trace row is relevant. This is audit metadata only; `release_decision.decision` remains the gate.
- `release_decision.evidence_coverage.semantic_coverage` (v0.29+) — `{total_actions, pass_eligible_actions, gap_count, review_concern_count, reason_counts}`. A non-zero semantic `gap_count` prevents `passed`; a non-zero `review_concern_count` prevents an automatic pass and routes known review concerns to human review — unscoped/ambient authority, and (v0.36+) `acknowledged_effect_override`, a declared effect a reviewer acknowledged as weaker than the evidence inferred for it. Read `reason_counts` for which; the count is of concerns, so one action can contribute more than one. Semantic gaps are not Findings and cannot be suppressed, baselined, severity-overridden, waived by `--no-heuristics`, or satisfied by `human_ack`.
- `release_decision.evidence_coverage.semantic_coverage.declaration_questions` (v0.37+) — the same action surface counted as a questionnaire: `{total, answered, open, open_by_dimension, open_questions[]}`. A *question* is one `(action, dimension)` a reviewed `action_surface.actions` row has to answer, and only `effect` and `authority` are counted — an action whose effect the scan established by itself (an OpenAPI method, an MCP annotation) was never asked and is not in `total`, and an inventory or `agent_bindings` declaration has no per-action counterfactual to score against. `answered` is exact rather than optimistic: it counts dimensions that gap when the same action is re-resolved *without* its declaration. `total == answered + open`, and `open_by_dimension` sums to `open`. `open_questions[]` is the answer order and joins to `evidence_gaps[].subject_id`. v0.38 ranks it by the ceiling of what an answer can establish: the actions nothing has bounded first — no effect evidence, a protocol default standing in for its absence, or only a heuristic reading the scan may not act on — then the actions a reviewed declaration or policy-eligible source evidence established, strongest-acting first, with `effect` before `authority` within one action. Position is not severity: the action at the top is the one *least* is known about. Nothing here gates; it is a projection of counts the decision already made, published so a coding agent (and the generated questionnaire) can report progress instead of a gap tally.
- `release_decision.evidence_coverage.policy_gap_count` and top-level `policy_evidence_gaps[]` (v0.33+) — policy applicability that is heuristic-only, mixed, unknown, or conflicting. These rows are outside Findings and cannot be suppressed, baselined, severity-overridden, acknowledged, or removed by `--no-heuristics`; any row prevents `passed`.
- `release_decision.evidence_coverage.identity_coverage` (v0.30+) — `{total_observations, canonical_tools, bound_tools, pass_eligible_tools, ambiguous_name_count, gap_count, reason_counts}`. Provider-scoped observations remain separate unless an exact reviewed `tool_identity.bindings[]` entry joins them. Any ambiguous selector, invalid binding, or conflicting identity prevents `passed`.
- `release_decision.evidence_coverage.evidence_gaps[]` (v0.26+;
semantic kinds added v0.29) — one structured row per measurable gap:
`{kind, subject, source_type, source_ref, policy_id, why, next_action}`.
`policy_id` is optional and present on policy-applicability rows; it retains
exact machine identity even when an engine-owned id is intentionally omitted
from adopter prose. In addition to `low_confidence_tool` and
`source_warning`, semantic rows include `incomplete_surface` for enumeration
failure and `unattested_surface` when lower-confidence extraction lacks
reviewed inventory attestation without an enumeration defect. Only an exact
adapter `surface: enumerated` fact earns positive enumeration wording. Other
semantic kinds include `missing_effect_evidence`, `inferred_effect_only`,
`conflicting_effect_evidence`, `missing_authority_evidence`,
`partial_authority_evidence`, `conflicting_authority_evidence`, and
`invalid_semantic_annotation`.
v0.36 adds `declaration_below_inferred_evidence`: the declared effect is
weaker than evidence this scan inferred for the same action. The declaration
still stands as the operative effect — heuristics never drive a verdict —
but the action is not evidence-backed-pass until a reviewer raises the
declared effect or adds `action_surface.actions[].override` with the
`evidence` they checked and the `reason` it does not apply. An acknowledged
override keeps the action pass-eligible and is reported as one semantic
review concern, so the run can never read `passed`. Each acknowledgement is
also emitted in
`release_decision.evidence_coverage.semantic_coverage.acknowledged_overrides[]`
naming the action, both readings, the hint source, any source evidence that
agrees, and the reviewer's evidence and reason. The packet's §1 and the PR
comment render it, and policy applicability consumes it, so applying an
override reaches review rather than trading one gap for another.
Semantic next actions use `declare_action_effect`,
`declare_action_authority`, `declare_tool_inventory`,
`provide_complete_inventory`, or `resolve_semantic_conflict`, include
accepted values and exact source/manifest pointers, and are human-routed.
v0.37 adds `next_action.observed_readings[]` on effect rows —
`{effect, sources[], observed}` — so the row can be answered without opening
`action_surface_facts`. Where those readings support one conservative answer,
`next_action.declaration_template` carries it pre-filled instead of a
`<REVIEW_REQUIRED>` blank. The value is a proposal, never an assertion: it
comes from the closed `ActionEffect` vocabulary, is never weaker than any
reading, and is offered only where something was observed. A protocol
default standing in for absent evidence, or a heuristic `read`, keeps the
blank. The placeholders carry `auto_apply=false` and
`requires_human_review=true`; only a reviewed manifest edit makes one
operative.
v0.41 adds `next_action.authorable_by` (`coding_agent` | `human`, default
`human`) — who may write the first draft. It is `coding_agent` only where the
scan filled every blank in `declaration_template` and the gap is not one that
asks a person to look again. `declaration_drift` therefore stays human-owned.
An agent-authorable row carries `suggested_patch_kind: "declare_action"` and
a `next_action.patch` exactly matching the template, split into the action
selector and fields written; the schema rejects any other pairing.
`target_path` is relative to `manifest_dir`. `auto_apply` stays false and
`requires_human_review` stays true for every row, and the patch remains
outside `apply-patches --kinds` by default; only the
`confirm_declarations` route proposes it.
v0.37 also re-routes `partial_authority_evidence`: it is raised when source
authority evidence is ambiguous or incomplete, and persists whatever the
manifest declares. Its action is `provide_source` with no declaration
template, not a declaration that could not close the row, and it is excluded
from `declaration_questions` for the same reason. Work the rows in order
instead of guessing; Agents Shipgate never auto-asserts effect or authority.
- `loaded_policy_packs[].{source,sha256,sha256_status,owner}` (v0.27+) — policy-pack distribution and ownership metadata for organization audit. `sha256_status` is `"verified"` only when the manifest pin matched; otherwise it is `"unpinned"`. This is report metadata; normal pack matching and release gating still come from deterministic rules and `release_decision.decision`.
- `findings[].support` (v0.33+) — typed predicate support with status, effective confidence, policy/block eligibility, claim IDs, evidence bases, predicate rows, and `support_hash`. Rule confidence and `block: true` are ceilings/requests; they cannot upgrade the support. Baseline matching for supported findings requires the same support hash.
- `findings[].policy_routing` (v0.28+) — optional policy-pack owner, reviewers, and approval-routing metadata. This is non-enforcing reviewer/audit metadata, not `Finding.evidence`; it does not affect fingerprints, suppressions, baselines, `blocks_release`, or `release_decision`.
- `release_decision.fail_policy.would_fail_ci` — `true`/`false`. Matches what
the CI process will exit with. For a semantic evidence gap, strict mode emits
the consistent tuple `decision="insufficient_evidence"`,
`would_fail_ci=true`, `exit_code=20`; advisory mode keeps exit `0` while
preserving the same non-pass decision.
- `release_decision.reason` — one-sentence explanation suitable for a PR comment.
- `release_decision.contribution_rules[]` (v0.17+) — deterministic per-finding audit explaining how each `report.findings` entry was classified. Exactly one row per finding (including suppressed). In v0.33, `unsupported_evidence` records a finding that cannot contribute because its typed support is not policy-eligible. Reading the contribution rule is sufficient to predict the gate outcome without re-deriving the decision logic.
- `privacy_audit` (v0.18+) — confirms the default redaction pass ran before public artifacts were written. Read `enabled`, `rules_version`, `sensitive_field_inventory_version`, `redacted_occurrence_count`, `redacted_paths[]`, and `output_surfaces[]`. `redacted_paths[]` contains structural paths and counts only, never raw values or raw hashes.
- `reviewer_summary` (v0.20+) — deterministic projection of the reviewer lens surfaces and audit envelopes; the reviewer-side parallel to `agent_summary`. Read this block first when triaging a scan for a human reviewer. Carries `verdict` (mirrors `release_decision.decision`), `headline` (≤200 chars, PR-comment-friendly), per-lens activity counts (`tool_surface_changes`, `capability_misalignments`, `action_surface_changes`, `evidence_matrix_gaps`), per-audit-envelope counts (`severity_overrides_applied`, `severity_overrides_tier_crossed`, `privacy_redactions`, `baseline_integrity_issues`), and `first_recommended_surface: ReviewerSurfacePointer | None` — a deterministic pointer naming which lens/audit to open first (`{kind, name, path, why}` where `kind` ∈ `{release_decision, lens, audit, evidence_matrix}` and `name` ∈ `{tool_surface_diff, capability_intent_diff, action_surface_diff, evidence_matrix, policy_audit, privacy_audit, baseline_integrity, release_decision}`). Same inputs always produce the same output; this block cannot disagree with the underlying lens/audit data.
- `heuristics_filter` (v0.21+) — top-level audit envelope describing the `--no-heuristics` CLI filter pass. Always present, even when the flag is unset (`enabled: False` with zero counts), so the report shape is stable. Carries `enabled: bool`, `excluded_provenance_kinds: list[str]` (`["keyword_heuristic", "regex_heuristic"]`), `filtered_finding_count: int`, and `filtered_by_kind: dict[str, int]` (per-kind breakdown). When `enabled: True`, findings whose `provenance_kind` is in the excluded list have been marked `suppressed=True` with `suppression_reason="filtered by --no-heuristics"` BEFORE the release decision was built — they remain in `findings[]` for transparency but no longer gate release. The filter never un-suppresses a finding; manifest-driven suppression reasons are preserved when they overlap with the filter. Useful for security/GRC reviewers who want declared-only findings.
- `verifier_summary` (v0.22+) — top-level **composition** for one-fetch controller consumption (the AI-coding-workflow verifier surface). It derives **no independent verdict**: `verdict` mirrors `release_decision.decision` exactly (Principle: one decision engine). Carries `by_severity: dict[str,int]` and `by_reason_code: dict[str,int]` (active-finding histograms — the complete per-code map), `capability_delta_summary: {added, removed, broadened, narrowed}` (equal by construction to the `capability_change` member-list lengths), `protected_surface_touched: bool`, `policy_weakened: bool`, `human_ack_required: bool`, `human_ack_satisfied: bool`, and `top_reason_codes: list[{reason_code, count}]` — the ranked top-five highlight (severity desc → count desc → code asc; the full set stays in `by_reason_code`). This block cannot introduce a finding-independent blocker.
In `findings[]`, v0.24 adds capability-native policy evidence for built-in
policy checks and policy packs:
- `capability_refs: list[str]` — stable `CapabilityFactV1.id` values that
matched the rule. It is emitted as an empty list for findings that are not
capability-policy matches.
- `capability_policy_evidence | null` — optional typed audit metadata with the
matched capability identity, effect, authority, controls, semantic hashes,
matched predicates, and source provenance. It is explanatory only and is not
included in finding fingerprint inputs.
- `policy_routing | null` — optional policy-pack routing metadata with
`owner`, `reviewers`, and `approval.{required,teams,min_approvals,enforced}`.
`approval.enforced` is always `false`; Shipgate validates declared team names
but does not verify external approval systems or make release decisions from
these fields.
Deterministic match and gating `finding.evidence` keys remain stable for legacy
readers. Policy-pack routing keys that used to live in `Finding.evidence` now
live in `policy_routing`; old baseline fingerprints are still matched during
baseline comparison. Policy matching is capability-native internally, but
policy-pack behavior, suppressions, severity overrides, baselines, SARIF,
Markdown, and GitHub Action outputs remain compatible.
In `findings[]`, v0.25 adds opt-in trace/provenance references for existing
trace/evidence checks:
- `capability_trace_refs: list[str]` — stable IDs from the top-level
`capability_runtime_evidence` block. It is emitted as an empty list for
findings that are not linked to a local trace row.
- `provenance_kind: "runtime_trace"` — used only for findings derived from
declared local trace artifacts. It is not filtered by `--no-heuristics`.
The top-level `capability_runtime_evidence` block is a deterministic audit
projection over local trace artifacts declared in `openai_api.trace_samples`,
`google_adk.trace_samples`, `validation.evidence.approval_traces`, and
`validation.evidence.agent_traces`. It carries summary counts, matched and
unmatched `CapabilityTraceEvidenceV1` rows, source provenance, and notes. Trace
normalization keeps only allowlisted scalar fields and discards prompts,
messages, tool arguments, tool outputs, and arbitrary payload bodies. The block
is empty when no trace inputs are declared. It is not part of capability locks,
fingerprints, baselines, run IDs, or release gating.
The remaining v0.22 verifier blocks are reviewer-facing projections / declared inputs — none gates independently (`release_decision.decision` stays the only gate). They populate with real values only under `verify` mode (a `VerificationContext` from `agents-shipgate verify` or an equivalent scan context); a plain `scan` emits their stable empty shape:
- `capability_change` (v0.22+, semantic metadata v0.23+) — the diff-derived capability delta, grouped into `{enabled, added, removed, broadened, narrowed}` member lists over `action_surface_diff` / `tool_surface_diff`. Each `CapabilityChangeMember` carries `{id, direction, subject_kind, tool, action, scope, before_scope, after_scope, before_capability_id, after_capability_id, changed_hashes, semantic_direction, semantic_changes, risk_tags, release_impact, provenance_kind, confidence, rationale, related_finding_ids}`. `broadened` = more effective capability (wider scope, escalated effect, removed control); `narrowed` = less (removed scope, added control). `semantic_direction` explains the proven capability-level movement (`added | removed | broadened | narrowed | mixed | unknown | evidence_only`), and `semantic_changes[]` gives field-level reasons when a base action snapshot is available. `enabled: false` when no base diff is available. A member with an empty `tool` and `subject_kind: "scope"` whose `scope` reads `"<agent> -> MCP binding <slot> <axis>"` is an agent's **remote binding** — the endpoint, credential reference, transport or tool filter of a remote MCP server it mounts (#538). `tool` is empty because no remote leaf was enumerated and none is invented; `before_scope` / `after_scope` carry the two sides. Only the tool-filter axis claims a direction; for the others `broadened` is the block's documented opaque-direction bucket and the `rationale` says so, because a host name or an environment variable name establishes no privilege level.
- `protected_surface_changes` (v0.22+) — list of touched release trust roots, each `{path, kind, glob, related_finding_ids}`. Derived from the active `SHIP-VERIFY-*` findings, so every row's `related_finding_ids` resolves to a real `findings[]` entry and the rollup can never disagree with the gate. A row means "a protected file was touched"; purely-semantic weakenings with no file path stay in `findings[]` and surface via `verifier_summary` flags.
- `effective_policy` (v0.22+) — normalized (not text-diff) snapshot of the release-policy surface for base-vs-head weakening comparison: `{ci_mode, fail_on[], suppressed_check_ids[], waiver_scopes[], severity_overrides{}, baseline_integrity_mode, baseline_fingerprints[], ci_gate_present}`. Every list/dict is sorted for byte-stable output; derived purely from the manifest (plus accepted-debt fingerprints). It describes the policy the repository **declares**, not the policy this invocation runs under: `--ci-mode` / `--fail-on` move `ci_mode` / `fail_on` at the top level of the report but never here, so two runs of the same tree produce the same snapshot and a base-vs-head comparison stays repository-vs-repository.
- `human_ack` (v0.22+) — declared human-acknowledgement state, `{required, satisfied, acks[], outstanding[]}`. Within the static boundary, acknowledgement is **declared evidence only — never inferred** (human authority cannot be synthesized). A trust-root weakening (`SHIP-VERIFY-POLICY-WEAKENED`, `-POLICY-BASE-ABSENT`, `-CI-GATE-REMOVED`, `-BASELINE-OR-WAIVER-EXPANDED`) makes a surface `required`; it is `satisfied` only by a matching `human_ack` entry in `shipgate.yaml` (owner + reason + affected surface, optional expiry). `required == (acks-covering-required) + outstanding`. The acknowledgement section lives in `shipgate.yaml` — itself a trust root — so a coding agent cannot add its own ack without tripping `SHIP-VERIFY-TRUST-ROOT-TOUCHED`.
New `SHIP-VERIFY-*` reason codes (v0.22+, category `verify` — suppression-immune and floor-protected; emit only under `verify` mode): `SHIP-VERIFY-POLICY-WEAKENED` (base-vs-head policy weakened), `SHIP-VERIFY-POLICY-BASE-ABSENT` (0.16+; a policy trust root changed with no base snapshot to compare against — split out of `-POLICY-WEAKENED` so a first adoption no longer reports a weakening that could not have happened; evidence `kind` is `manifest_introduced` or `base_snapshot_unavailable`, and only the former reports `policy_weakened: false`), `SHIP-VERIFY-BASELINE-OR-WAIVER-EXPANDED` (suppression/waiver/baseline broadened), `SHIP-VERIFY-CI-GATE-REMOVED` (Shipgate CI workflow deleted), `SHIP-VERIFY-AGENT-INSTRUCTIONS-WEAKENED` (deprecated compatibility ID; no findings; supported structural changes remain covered by the shared trust-root comparison), `SHIP-VERIFY-TRIGGER-CATALOG-DRIFT` (trigger catalog changed). They are ordinary `Finding`s routed through `release_decision` — never a second verdict.
The action exposes these as outputs `decision`, `blocker_count`, `review_item_count`, `ci_would_fail` (v0.8+).
For verifier-cycle PR workflows it also exposes additive outputs
`should_run`, `trigger_action`, `trigger_rule_ids`, `verifier_verdict`,
`verifier_json`, `verify_run_json`, `run_id`, `merge_verdict`,
`can_merge_without_human`, `agent_control_state`, `agent_control_reason`,
`agent_controller_must_stop`,
`agent_controller_stop_reason`, `agent_controller_completion_allowed`,
`trust_root_touched`,
`policy_weakened`, `capability_changes_added`,
`capability_changes_modified`, and `capability_changes_removed`. These are
review and routing aids only. `trust_root_touched` and `policy_weakened`
mirror `verifier_summary`; the capability counts mirror
`capability_change` (`modified` is `broadened + narrowed`). Keep using
`decision` as the release-gating output and `agent_control_state` as the
coding-agent operational output.
When the action is asked to emit organization-governance artifacts, it also
exposes `attestation_json`, `org_evidence_bundle_json`, `host_grants_json`, and
`org_status_json` as artifact paths. These are ingestion and audit surfaces for
platform teams; they never create a second verdict.
For ongoing PR workflows, prefer:
```bash
agents-shipgate verify --workspace . --config shipgate.yaml \
--base origin/main --head HEAD --ci-mode advisory --format json
```
`verify` writes `verifier.json`, `verify-run.json`, `agent-handoff.json`, and
`pr-comment.md` alongside the head scan artifacts. `agent-handoff.json` is the
compact coding-agent projection over the verifier, verify-run, and report
artifacts; it does not gate independently. After a successful head scan it also writes the head static
capability lock to `agents-shipgate-reports/capabilities.lock.json`. When
`--base` is provided and the base scan can be materialized, verify writes
`agents-shipgate-reports/base.capabilities.lock.json`,
`agents-shipgate-reports/capability-lock-diff.json` and
`agents-shipgate-reports/capability-lock-diff.md`. The packet artifact is
intentionally `packet.json` only; use `scan` for manifest-driven packet
Markdown/HTML/PDF rendering. Read
`verifier.json.base_status` to understand whether base diff enrichment ran;
do not use it as a release verdict. The release gate is still
`report.json.release_decision.decision`. `verify` never fetches, so CI callers
must make the base ref available before invocation. Supplying `--head` makes
verify scan an isolated archive of that ref; omitting it scans the checked-out
workspace. If an explicit `--base` ref or PR diff cannot be inspected, verify
skips a head-only scan; `verifier.json.merge_verdict` is `unknown` and the
command exits 2.
`agents-shipgate verify --preview --json` is a lightweight relevance check — no
scan, no manifest required, exits 0 for every workspace it evaluates. A
`--workspace` that does not exist is not a workspace it evaluates: it is
refused as an invocation error (`config_error`, exit 2) before any directory
is created, on preview and on every other command that takes the option. It
emits a `verifier.json` with
`mode: "preview"`, `execution: "not_run"`,
`applicability: "not_evaluated"`, and
`control.state: "agent_action_required"`. `control.next_action` carries the
next recommended action: an exact
`init --workspace <workspace> --write --json`
command for unconfigured repos, or an exact `verify` command for configured
repos using the supplied workspace/config/base/head/out arguments.
`verify --preview --format control` prints that same state and command, and an
`agent control` refresh of the pointer it published returns them until the
working tree moves. Where the working tree cannot be read, both withhold them
and lead with the cause instead
([how a preview is read](#two-read-entry-points)).
Use it as the
first touch before a full scan. To evaluate just the run/skip trigger, run
`agents-shipgate trigger --base origin/main --head HEAD --json`.
`agents-shipgate verify` and `verify --preview` also write
`agents-shipgate-reports/verify-run.json` whenever the output directory can be
created. It carries `schema_version: "shipgate.verify_run/v5"`, the exact
verification plan, executor, unit-result IDs, decision ID, outcome projection,
and artifact references. `request_id` is the content-addressed run identity;
the deprecated `run_id` remains for one compatibility cycle as its exact alias,
never as a separately derived identity. It has no wall-clock timestamp and is
not a second gate.
`agents-shipgate-reports/agent-handoff.json` carries
`schema_version: "shipgate.agent_handoff/v9"` and top-level sections
`gate`, `control`, `fix_task`, `blocked_by[]`,
`remediation_plan[]`, `capability_review`, `authorization`, `reproducibility`,
and `artifacts`.
`gate.decision` mirrors `release_decision.decision`; `gate.merge_verdict`
mirrors `verifier.json.merge_verdict`; and
`gate.{static_analysis_only,runtime_behavior_verified,static_verdict_disclaimer}`
mirrors the report/verifier static-only boundary. The values are locked to
`true`, `false`, and the canonical disclaimer. `control` is byte-identical to
the verifier/verify-run control object, and `can_merge_without_human` is true
only for a verified `passed` result or a completed deterministic
`not_applicable` skip. `authorization` is the byte-equivalent verifier
evaluation; the handoff cannot grant a command independently. Re-render it
from existing artifacts with:
```bash
agents-shipgate agent handoff --from agents-shipgate-reports/verifier.json --json
```
In `agents-shipgate-reports/verifier.json`, read the fields below (full
schema [`docs/verifier-schema.v0.21.json`](verifier-schema.v0.21.json)). **Lead
with `control.state`.** Every release and merge field below is a mirror or
deterministic projection of `report.json`; the authorization evaluation is an
operational overlay and cannot change those fields.
`release_decision.decision` remains the gate.
- `control` — the discriminated `complete | agent_action_required |
review_publishable | human_review_required` operational projection. Its
variant fixes `completion_allowed`, `must_stop`, `permissions`,
`human_review`, and the actor/action shape; generated schemas enforce the
variants with `oneOf`. Only a new verifier artifact can clear a pending
control obligation.
- `execution` — `"not_run" | "succeeded" | "skipped" | "failed"`.
- `diff_status` — whether the compared change set was read at all.
`completeness` is `"complete"` / `"partial"` / `"unavailable"`; `reason` is
`null` only when complete, and otherwise `not_attempted`, `refs_missing`,
`merge_base_missing` (shallow checkout — deepening restores the merge base),
`unrelated_histories` (no common ancestor exists; no fetch can create one),
`objects_missing`, `metadata_limit_exceeded`, `body_limit_exceeded`,
`git_timeout`, or `git_failed`. `remediation` names
the repair and `fetch_repairable` says whether fetching can perform it.
**Only `"complete"` licenses reading a negative `trigger` result**; anything
else means the diff was not read, which is never evidence that a PR is
unrelated to agent capabilities. `null` means a pre-v0.7 artifact — unknown,
not complete.
- `trigger` — the run/skip evaluation. Read `evaluation_status` first; two of
its three values withhold the verdict, and in both `should_run` /
`run_shipgate` / `skip` / `skip_reason` are `null`. Never read `null` as
`false`. The embedded `next_action` is diagnostic only: it preserves the
evaluated kind but carries no command, sets `authoritative: false`, and
points at `control.next_action`, which is the route to follow. Embedded
`matched_rules[]` carry no commands either. The standalone
`agents-shipgate trigger --json` command retains its own actionable
`next_action`.
- `"not_evaluated"` — the diff could not be read. Repair the input through
`control.next_action`; `skip_reason` is never `"no_match"` for inputs that
were not fully read.
- `"unclassified"` — the diff *was* read in full and no rule classified some
or all of the changed files. That is a fact about the catalog, not about
the PR, so the skip is withheld and `control.next_action` routes forward
to the scan. `surface_exclusions.entries[]` lists the unclassified files.
`stop_conditions_fired` is the raw block result; `stop_conditions_terminal`
says whether it decided. A matched `run_shipgate`/`force_run` rule overrides
a fired stop, because a capability match in the diff is evidence the
whole-workspace negative did not account for. `"evaluated"` on an
incomplete `diff_status` is not a contradiction: only *skip* verdicts are
withheld, so a `should_run: true` reached from evidence that did not depend
on the missing bytes is authoritative and must not be overridden. Read
`matched_rules` to see what carried it — a `force_run` match rests on the
manifest, not on the diff.
- `merge_verdict` — `"mergeable"` / `"human_review_required"` /
`"insufficient_evidence"` / `"blocked"` / `"unknown"`. Deterministic projection
of `release_decision.decision` (`passed`→`mergeable`,
`review_required`→`human_review_required`,
`insufficient_evidence`→`insufficient_evidence`, `blocked`→`blocked`, missing
decision→`unknown`). It cannot disagree with the gate; switch on the enum with
an `unknown`/`human_review_required` fallback for future values.
- `static_analysis_only`, `runtime_behavior_verified`, and
`static_verdict_disclaimer` — locked to `true`, `false`, and the canonical
non-runtime disclaimer. When a release decision is embedded, construction
rejects any disagreement between these top-level values and the decision.
- `applicability` — `"not_evaluated"` / `"verified"` /
`"not_applicable"` / `"failed"`.
Disambiguates a `mergeable` verdict: `"verified"` means Shipgate evaluated the
change and produced a release decision; `"not_applicable"` means the head scan
was skipped (nothing to gate — do **not** read this as "verified safe");
`"failed"` means the scan could not complete. Orthogonal to `merge_verdict`;
additive and locked to `"verified"` whenever a `release_decision` is present.
- `can_merge_without_human` — `bool`.
- `decision` — mirror of `release_decision.decision` (or `null` when no scan ran).
- `headline` — single-sentence, PR-comment-friendly summary (or `null`). Every
completed blocked run names the deterministically selected worst blocker as
`Most severe: <title>.`, ordered by severity, check id, then title. A run
with no blocker adds no cause clause.
- `authorization` — the
`shipgate.human_authorization_evaluation/v1` result. Only `accepted` can
expose a command, and that command must exactly match both
`control.next_action.command` and the sole entry in
`control.allowed_next_commands`. `rejected`, `not_requested`, and
`not_applicable` carry no command authority.
- `control.human_review` and `control.next_action` are the serialized route for
the current verifier state; when authorization is accepted, the signed
evaluation is the provenance for the exact coding-agent next action.
- `AgentController`, `VerifierNextAction`, and `VerifierHumanReview` remain
importable only as deprecated v0.1/v0.2 reader models. Verifier v0.6 does not
emit or invoke the retired `build_agent_controller` projector.
- `fix_task` — `{actor, safe_to_attempt, instructions[], allowed_repairs[],
forbidden_repairs[], forbidden_shortcuts[], verification_command, patches[]}` or `null`.
This is the deterministic repair boundary: `actor: coding_agent` with
`safe_to_attempt: true` means the agent may attempt only the listed mechanical
`allowed_repairs[]` and rerun `verification_command`; `actor: human` means the
agent must not invent action effect, action authority, approval,
idempotency, policy, waiver, baseline, or trust-root evidence to make the
gate pass. `forbidden_repairs[]` explicitly
lists reward-hacking moves such as suppressing findings, lowering severity,
expanding baselines/waivers, weakening CI or policy, adding human ack, or
inventing action-effect/action-authority/approval/idempotency evidence.
`patches[]` (v0.13+) carries
`{finding_id, check_id, patch}` rows with the
machine-applicable suggested patches for the gating findings — populated
only when verify ran with `--suggest-patches` and the task routes to the
coding agent; repair aids, never gate inputs.
- `trust_root_touched` — `bool`; `true` when the PR changed a release-gate trust
root (`shipgate.yaml`, the Shipgate CI workflow, `AGENTS.md`/`CLAUDE.md`,
policy packs, prompts, baselines, waivers, etc.). Backed by the
`SHIP-VERIFY-TRUST-ROOT-TOUCHED` check.
- `capability_review` — reviewer-facing projection of `capability_change` with
`{trust_root_touched, policy_weakened, policy_weakening_proven,
capability_changes_added, capability_changes_removed,
capability_changes_modified, top_changes[]}`. Gate on `policy_weakened` (the
fail-closed flag, raised even when no base policy existed to compare
against); say the policy was weakened only when `policy_weakening_proven`
(0.16+) is also true — that one means a base-vs-head comparison actually ran.
`top_changes[]` carries the highest-signal capability deltas with
`{id, change_type, change_bucket, subject_kind, subject, impact, rationale,
source_path, source_start_line, related_finding_ids}`. `impact` mirrors the
gate (`blocks_release`, `review_required`, `insufficient_evidence`, or
informational values) and never introduces a finding-independent blocker.
- `mode` — `"advisory"` / `"strict"` / `"skipped"` / `"preview"`.
### Trusted human authorization for one exact command
Authorization changes operational routing, never the static release verdict.
The flow is deliberately two-pass:
1. Run `agents-shipgate verify --no-plugins` and validate the resulting
terminal receipt. Authorization requires the plan's exact effective plugin
mode to be false; the protected executor never loads third-party plugin or
adapter entry points.
`agents-shipgate authorization request --receipt <receipt>
--artifacts-root <root> --destination-ref <full-ref>
--expected-lease-oid <oid> --out <request>` constructs the unsigned
`shipgate.human_authorization_request/v1` from that receipt's current
request, subject, decision, source receipt/artifact-set/engine/executor and
tree identities, the complete ordered
review set, and one typed Git-push operation. This command creates a
challenge, not authority.
2. The host authenticates the human and signs the canonical request with an
Ed25519 key kept outside coding-agent reach. Agents Shipgate supplies no
private key and no command that signs or approves a request. The v1 trust
policy must be stored outside the evaluated workspace and protected from
writes by the agent. On POSIX, Agents Shipgate reads it only from the OS
account home's fixed path
`~/.config/agents-shipgate/human-authorization-trust-policy.json`; `HOME`
and `XDG_CONFIG_HOME` do not redirect that lookup.
3. Rerun `agents-shipgate verify --no-plugins --authorization
<external-grant>`. The
verifier recomputes the current identities and validates the signature,
principal, repository scope, TTL, request, subject, trees, decision, full
review set, and operation before publishing any command authority.
The only v1 operation is an exact force-with-lease Git push. It binds the exact
evaluated commit, a canonical credential-free HTTPS destination whose
repository identity equals the verified repository, a full destination
`refs/heads/...` ref, and the expected remote OID. A synthetic PR merge receipt
cannot authorize pushing a different parent commit. Authorization is eligible
only when execution
succeeded and the release decision is `review_required`. An accepted grant
changes `control.state` from `human_review_required` to
`agent_action_required` for that exact command, while all release facts remain
unchanged: `release_decision.decision="review_required"`,
`merge_verdict="human_review_required"`, `can_merge_without_human=false`, and
`completion_allowed=false`. The coding agent may perform only the serialized
guarded `agents-shipgate authorization execute` command. That consumer
revalidates the current receipt, trust root, clock, repository, and commit and
isolates Git configuration and hooks before issuing the internal typed push;
the raw Git command is never operational authority. The agent must rerun
verification afterward.
The signer must authenticate the source closure: content addressing is
integrity, not provenance. It must rerun verification in a trusted worker or
verify trusted-CI attestation over the bound source receipt/artifact-set IDs.
The request exposes the evaluated base commit and merge base, and the source
commit transitively binds its full parent graph. The signer must review that
complete ancestry and reachable history rather than relying only on the final
tree diff. Execution enforces a 512 MiB graph-pack ceiling and a 120-second
process timeout; the host broker should impose tighter deployment quotas. The
compressed pack ceiling does not bound expanded-object indexing memory or CPU,
so production brokers need cgroup, container, or equivalent host resource
limits.
Execution also requires a host-protected broker with a sanitized environment,
external trust policy, interpreter, entire virtual environment and
`site-packages` tree (including startup `.pth` files), dependencies,
credentials, and separately installed Agents Shipgate distribution. Same-UID
file permissions alone are insufficient, and an
editable install rooted in the authorized workspace is ineligible. If the host
cannot enforce those boundaries, authorization remains disabled. The guarded
executor is POSIX-only in v1 and authorization remains disabled on Windows. V1 is
push-only and does not authorize the coding agent to apply reviewed protected
patches.
Malformed, untrusted, expired, not-yet-valid, incomplete-review-set,
wrong-tree, wrong-request, wrong-ref, or wrong-lease grants fail closed with
zero allowed commands. Plain JSON in the repository, a PR comment, or
conversation-level approval is not equivalent to a signed grant. This release
defines the protocol and verifier consumer; it does not claim a current Codex,
Claude Code, or other coding-host UI signing integration. Such a host adapter
must be implemented separately. A grant replayed after the remote ref advances
cannot overwrite that ref: Git enforces the signed command's explicit expected
lease OID.
`verifier.json` also carries `trigger`, `base_status`, `head_status`, `base_ref`,
`head_ref`, `changed_files`, `base_notes`, the embedded `release_decision`, and an
`artifacts` map. In `verifier.json` each artifact path (and `head_report_json`,
`base_report_json`) inside the repository is relative to `workspace`, the Git
root, and one outside it is absolute. `verify --format json` prints the same
object with those relative paths spelled for the invoking shell: relative to
its current directory when beneath it, absolute otherwise, so they open as
printed; from the Git root the two are identical. When present,
`artifacts.capability_lock_json`,
`artifacts.base_capability_lock_json`,
`artifacts.capability_lock_diff_json`, and
`artifacts.capability_lock_diff_markdown` are review artifacts only; they do not
change the gate. The matching GitHub Action outputs are `agent_control_state`,
`agent_control_reason`, `merge_verdict`, `can_merge_without_human`, and the
compatibility mirrors `agent_controller_must_stop`,
`agent_controller_stop_reason`, `agent_controller_completion_allowed`,
`trust_root_touched`, and
`capability_changes_{added,modified,removed}` (the original `decision`,
`blocker_count`, `review_item_count`, `ci_would_fail` outputs are preserved). See
[STABILITY.md §Verify Orchestrator](../STABILITY.md#verify-orchestrator) for the
authoritative contract.
The default Action PR comment style for the verifier-cycle minor is
`capability-review`: exactly two reviewer sections, a human summary and a
fenced JSON agent instruction block. The human summary leads with
`merge_verdict`, `can_merge_without_human`, capability delta, next actor, and
artifact links, including the semantic capability-lock diff summary when a base
lock is available. The agent block carries `control` and `fix_task` for
coding-agent routing. Existing adopters that need the v1
findings-oriented comment during migration can set `pr_comment_style: findings`
for one minor release cycle.
The GitHub Action emits source-backed GitHub Actions job annotations by default
for active blockers and review items. `check_annotations: "false"` disables the
projection; `check_annotation_limit` caps the number emitted. The helper also
writes `agents-shipgate-reports/check-annotations.json` for audit/debug.
`verify` writes non-gating capability artifacts when static extraction succeeds:
`agents-shipgate-reports/capabilities.lock.json` for head, and when a base ref
is available, `base.capabilities.lock.json` plus `capability-lock-diff.json`.
These artifacts are review/integration surfaces only and cannot introduce a
second verdict.
## Read this for local boundary control
`shipgate check --agent <codex|claude-code|cursor> --workspace . --format
agent-boundary-json` is the local static multi-host boundary command. The
`--agent` value is caller identity, never a coverage selector. The command emits
exactly one stdout JSON object using
`schema_version: "shipgate.agent_boundary_result/v3"` and the schema in
[`agent-boundary-result-schema.v3.json`](agent-boundary-result-schema.v3.json).
The old `codex-boundary-json` spelling remains a deprecated compatibility
projection of the same assessment, kept through `1.x`.
Read `input_coverage`, `host_coverage[]`, `affected_hosts[]`, `policies[]`,
`issues[]`, `pending_review[]`, and `excluded_scopes[]` before relying on the
result. `complete`
means complete only within the declared static input scope; it is not proof of
session grants, runtime enforcement, or tool behavior. `pending_review[]` is
non-empty only alongside `agent_action_required`: those are review obligations
the graded mapping carried forward instead of stopping the turn, and an agent
must name them when summarizing the change. The detailed matrix is
[`host-boundary-support.md`](host-boundary-support.md).
Coding agents switch on `control.state`, then follow `control.next_action` and
`control.allowed_next_commands`. `decision` is diagnostic only. A pending
verification obligation produces `agent_action_required`; it can never coexist
with `completion_allowed=true`. An evaluated human route produces
`review_publishable`, `must_stop=false`, a human next action, and
`permissions` that authorize publishing but not merging. An unsafe or
unbindable one produces `human_review_required`, `must_stop=true`, and
permissions that authorize nothing.
Do not derive control from Markdown, PR comments, natural language, or a
conversation-level acknowledgement. Only a new verifier artifact can clear
the obligation. Do not confuse this local boundary result with
`agents-shipgate verify`: verify writes
`agent-handoff.json`, `verifier.json`, and `verify-run.json`, and
`report.json` remains the full CI/reviewer substrate.
## Read these for release review
`agents-shipgate contract --json` exposes `manual_review_signals[]` as the
installed CLI's stable list of report/packet fields to inspect for human review
work. `findings[].provenance_kind` is included there as a filter/review signal
only; it never changes the release decision, severity, fingerprints, baselines,
or CI exit behavior.
The runtime contract also exposes stable non-gating integration fields:
`agent_handoff_schema_version`, `agent_handoff_schema_path`,
`agent_handoff_artifact`, `agent_interface_operations[]`, `exit_code_policy`,
`mcp_tools[]`,
`capability_lock_schema_version`, `capability_lock_diff_schema_version`,
`capability_payload_schema_version`, `capability_payload_schema_path`,
`capability_delta_attestation_schema_version`,
`capability_delta_attestation_schema_path`,
`capability_delta_predicate_type`, `capability_delta_attestation_artifact`,
`capability_standard_version`,
`governance_benchmark_catalog_schema_version`,
`governance_benchmark_result_schema_version`, and
`external_integration_surfaces[]`. These advertise capability lock/diff and
benchmark artifacts for integrations and research. They do not change the gate:
`release_decision.decision` remains the only release decision signal.
The capability/intent diff fields (v0.9+), used by reviewers to spot misalignment between declared agent intent and actual tool surface:
- `capability_facts[]` — every capability surfaced from the tool inventory. In v0.29 each newly emitted fact carries `semantic_assessment`, the normalized effect/authority claims, issues, conservative effect, and pass-eligibility state consumed by the gate.
- `declared_intentions[]` — what the manifest says the agent is supposed to do.
- `misalignments[]` — where capabilities exceed (or fall short of) declared intent.
- `release_consequence` — capability-aware roll-up of the release decision.
- `suggested_scenarios[]` — dynamic-validation scenarios derived from misalignments and findings.
The Action Surface Diff fields (v0.16+), reviewer-facing PR/release delta:
- `action_surface_facts.actions[]` — deterministic snapshot of the current agent action surface: action id, operation, effect, normalized risk tags, scopes, approval policy, safeguards, evidence, hashes, and (v0.29+) the same `semantic_assessment` projected onto the corresponding capability fact. `semantic_assessment.conservative_effect`, `action.effect`, and the capability effect must agree.
- `action_surface_diff.{enabled, base, summary, added, removed, modified, notes}` — what changed vs. a base report or v0.4 baseline. Policy findings generated from this diff can set `findings[].blocks_release=true` and appear in `release_decision.blockers`.
- `findings[].blocks_release` and `release_decision.{blockers,review_items}[].blocks_release` — explicit release-policy blockers from Action Surface Diff policies and policy-pack rules with `block: true`. Advisory CI may still exit 0; strict CI exits nonzero when an active unbaselined release blocker is present.
The tool-surface diff fields (v0.10+), lower-level explanatory data:
- `tool_surface_facts.{tools, scopes, controls, policies}` — current static facts about the tool surface.
- `tool_surface_diff.{enabled, base, summary, tools, high_risk_effects, scopes, controls, metadata_changes, policy_drift, finding_deltas, notes}` — what changed vs. a base ref. Disabled diffs render as `enabled: false` with a `notes` reason.
Source provenance fields on `findings[].source` (v0.11+), additive and optional:
- `path`, `start_line`, `end_line`, `start_column`, `pointer` — manifest-relative file path, 1-based line/column, and RFC 6901 JSON pointer for the offending tool. Populated for OpenAPI, MCP, OpenAI tool artifacts, and Anthropic tool artifacts when the source is YAML. JSON inputs carry `path` and `pointer` but no line in v0.11.
Per-finding `agent_action` enum (v0.12+), deterministic projection — read this **first** when deciding what to do with a finding so you don't have to synthesize an action from `patches`/`autofix_safe`/`requires_human_review`/`suggested_patch_kind`:
- `auto_apply` — `apply-patches --confidence high` will resolve cleanly. Every patch is non-manual and high-confidence.
- `propose_patch_for_review` — at least one non-manual patch is attached and machine-applicable, but the full patch set is not auto-safe. Two shapes land here: (a) every non-manual patch is medium- or low-confidence, and (b) a high-confidence non-manual patch sits alongside one or more `ManualPatch` siblings (the non-manual is safe to apply, but the manual instructions still need a human). In both cases the agent should ask the user before `--apply` and surface any manual instructions verbatim.
- `escalate_to_human` — no machine-applicable patch. Either every patch is `ManualPatch`, or `patches` is empty/absent and the check requires human review.
- `suppress_with_reason` — reserved for future check classes that explicitly mark themselves as suppressible. Not emitted by the v0.12 deterministic projection; the schema accepts it so callers can extend.
- `informational` — no action required (suppressed finding or non-actionable advisory).
Top-level `agent_summary` block (v0.12+), one-fetch summary shaped for direct agent consumption — read this when you want the headline numbers without traversing arrays:
- `verdict` — mirrors `release_decision.decision`.
- `headline` — single-sentence verdict + counts; suitable for a PR comment lead. The headline uses `needs_human_review` (action-driven) for "require human review" wording, so a `review_required` verdict with only auto-applicable findings reads honestly as "auto-applicable; none require human input" rather than falsely claiming N findings need review.
- `blocker_count` — mirrors `len(release_decision.blockers)`.
- `review_item_count` — mirrors `len(release_decision.review_items)`; **severity-driven** (medium-and-up severity findings that aren't blockers, plus baseline-matched accepted debt). Use this when reporting release-review debt to the human reviewer.
- `auto_appliable_patches` — number of active findings with `agent_action == "auto_apply"`.
- `needs_human_review` — **action-driven**: number of active findings with `agent_action ∈ {"escalate_to_human", "propose_patch_for_review"}`. Both kinds need explicit human attention before any change applies — full escalations have no machine path, and proposed patches ship at medium/low confidence and require an explicit `--apply` after the user confirms. Use this when reasoning about what work an agent must do.
- **`review_item_count` and `needs_human_review` track different populations and can diverge.** A medium-severity stale-suppression finding lands in `release_decision.review_items` (severity rule) but its `agent_action` is `auto_apply` (high-confidence patch attached), so it's counted in `review_item_count` and `auto_appliable_patches` but **not** in `needs_human_review`.
- `first_recommended_action` — `{kind, command|null, why}`; deterministic next step. `kind: "command"` carries an actual CLI invocation; `kind: "info"` is a "surface this to the user" hint with no command. The agent_summary block is a deterministic projection — same inputs, same output, no agent-side aggregation needed.
- **Evidence-gap actionability (v0.16+).** One **selected gap** feeds every short-form surface that names one: the first `release_decision.evidence_coverage.evidence_gaps[]` row that is *addressable*, falling back to the first row when none is. `Improve evidence:` (CLI and step summary, printed only on an `insufficient_evidence` verdict) always renders the selected gap's action.
**A row is addressable when it names a visible target or carries a publishable command.** `next_action.path` and `next_action.command` are independently nullable, and either one alone is enough:
- *Visible target* — the path contains at least one character that actually renders. Whitespace, control characters, and Unicode Default_Ignorable code points (U+200B ZWSP, U+200E/U+202E bidi marks, U+FE0F VS16, U+034F CGJ, …) render as nothing, so a path made only of those names no surface and is not addressable.
- *Publishable command* — the command is safe to run **exactly as authored**. A command containing any control, bidi, or invisible code point, or any whitespace other than U+0020, is suppressed entirely rather than cleaned up: deleting a zero-width character from `r␣m -rf` would author a different program. Only leading/trailing U+0020 is trimmed, which cannot change `argv[0]`.
Two guarantees, scoped to exactly what holds:
- **Alignment, on `insufficient_evidence` with an addressable gap.** When the verdict is `insufficient_evidence` *and* at least one gap is addressable, `release_decision.reason`, `Improve evidence:`, and `first_recommended_action.why` name the **same** gap and the same target (or, for a command-only row, the same command). The reason leads with that gap and reports the source-warning / low-confidence counts as `Context:`.
- **No false dead end, on every verdict.** The phrase *"no machine-applicable fix is available"* is never emitted in `first_recommended_action.why` while any gap is addressable. When you do see it, no gap names a surface to open or a command to run, and the next step really is a human gathering evidence.
Outside that first case the three surfaces answer different questions, by design — do **not** read alignment into them:
- `insufficient_evidence` with **no** addressable gap: `reason` keeps the `Evidence coverage below threshold (…)` wording and `first_recommended_action` routes to gathering deeper sources, while `Improve evidence:` still renders the first gap's `expects`. That line is a remediation hint, not a restatement of the reason.
- `review_required`: `reason` is severity/findings-driven and never names a gap. Whether `first_recommended_action` names one depends on which branch of the action picker fires, and **auto-apply wins on sub-threshold evidence**: when the scan carries auto-applicable patches and evidence is *not* below the `insufficient_evidence` threshold, the action is the `apply-patches` command even if an addressable gap exists (the gap is still called out in the `why`). `first_recommended_action` names the selected gap on the evidence-first branches only — evidence below the threshold, or evidence-driven review with no findings to walk. Read `evidence_coverage.evidence_gaps[]` directly if you need the gap on this verdict; do not infer it from the action.
**Rendering never rewrites what it renders.** Values reaching these one-line surfaces are repository-derived — a gap subject is a tool name, a policy pack authors `expects`, a semantic gap's `path` embeds a tool name — and they are made line-safe without being altered otherwise. Characters that could break a line or reorder it (controls, U+2028/U+2029, bidi marks) become a visible `<U+XXXX>` escape; **nothing is deleted**. Identity-bearing invisibles survive, so `agents/👩💻.yaml` and identifiers carrying ZWNJ are named as they actually are. Paths and commands are additionally never whitespace-folded — `configs/foo␣␣bar.yaml` keeps both spaces, and `python -c 'print("a␣␣b")'` stays the program that was written — while prose (subjects, `why`/`expects`, loader warnings) does fold whitespace, because there a stray newline is better read as a space. An affordance is published only when it exists: a suppressed or absent command produces no `Run:` line and a `null` repair command, and accepted values with nothing visible in them are dropped.
**Authority comes from the action's own fields, never from its `kind` or its path.** Every evidence-gap row published today is `requires_human_review: true` and `auto_apply: false` — including the `provide_source` row that regenerates a stale `--diff-from` comparison base, for which `verify` emits `fix_task.actor: "human"` and `safe_to_attempt: false`. Since v0.41 the one row that carries a machine-applicable patch says so in its own fields too: `authorable_by: "coding_agent"` with `suggested_patch_kind: "declare_action"`. That is still not a licence to apply it unasked — it is applied by the `confirm_declarations` route that proposes it, and the human merge it needs is unchanged. A `command` on such a row tells a human exactly what to run; it does not make the row agent-owned. A coding agent acts mechanically only where `fix_task.actor == "coding_agent"` and `safe_to_attempt` is true, and then only within `allowed_repairs[]`. Separately, `first_recommended_action.kind` stays `"info"` on the evidence-first actions — a statement about the summary projection, not a claim that no gap row ever carries a command, and not a promise about every `review_required` action (the auto-apply branch returns `kind: "command"`).
Codex plugin surface block (v0.13+), explanatory only — never a release-gate
input by itself:
- `codex_plugin_surface.{plugins, marketplaces, skills, apps, mcp_server_stubs, hook_stubs, mcp_inventory_files, component_path_issues, warnings}` — local static plugin package and marketplace facts.
- Only explicit MCP inventory tools from `codex_plugins.mcp_tool_inventories` appear in `tool_inventory[]`; apps, hooks, skills, and MCP server declarations stay in `codex_plugin_surface`.
Per-finding `provenance_kind` enum (v0.15+), additive classification — read this when you want to filter findings by the kind of rule that fired, independent of `confidence` (sureness):
- `static_declaration` — declared metadata: manifest, MCP export, OpenAPI schema, ADK YAML agent config, LangChain/CrewAI inventory JSON. High-trust structural facts.
- `ast_extraction` — Tool parsed from user Python source by a framework extractor (LangChain function/structured tools, CrewAI function/class tools, ADK Python toolsets). Subject to extraction errors; agents that distrust AST quality may filter these as a class.
- `keyword_heuristic` — matched a keyword list (broad-scope tokens, read-only/approval prompt terms, free-text parameter names). Higher false-positive risk than declarative facts.
- `regex_heuristic` — matched a regex (secret-like values in descriptions, prompt-injection patterns). Highest false-positive risk; pair with the recommendation before acting.
- `policy_pack` — emitted by an external policy pack rule after its predicates have authoritative typed support. Rule confidence can lower, but never raise, evidence confidence.
- `runtime_trace` — derived from declared local trace artifacts. Audit evidence only; never filtered by `--no-heuristics`.
Provenance generally follows the rule's own trigger (e.g., a rule that checks for a declared manifest field is `static_declaration` even when the underlying Tool was AST-extracted). For framework checks that fire across both AST and declarative tool sources (ADK's per-tool checks against `google_adk_function` AND `google_adk_config` tools), the label tracks the underlying tool's source. Third-party plugin checks that don't yet set the field land at `static_declaration` by default — pre-v0.15 plugins continue to validate against the v0.15 wire schema. Use `findings[].source.type` for the precise underlying tool source.
To filter operationally, use:
```bash
agents-shipgate findings --from agents-shipgate-reports/report.json \
--provenance-kind keyword_heuristic,regex_heuristic --json
```
The command reads active findings by default; add `--include-suppressed` when a
reviewer needs suppressed entries in the same provenance summary.
For reviewer-shaped output, also read the **Release Evidence Packet** at
`agents-shipgate-reports/packet.{md,json,html}` (and `packet.pdf` when the
`[pdf]` extras are installed). The packet is a supporting/provisional reviewer
projection, not a second gate. Packet outputs are redacted by the same default
privacy layer as the report. The packet has fixed reviewer sections governed by
[`docs/packet-schema.v0.18.json`](packet-schema.v0.18.json) — see
[STABILITY.md §Release Evidence Packet](../STABILITY.md#release-evidence-packet-v018).
Packet schema `0.9` carries the report's evidence-backed semantic coverage and
gap remediation contract. Packet §1 also mirrors
`static_analysis_only=true`, `runtime_behavior_verified=false`, and
`static_verdict_disclaimer` from the report release decision. Frozen packet
schema `0.7` added capability-linked
trace summary and trace refs under `human_in_the_loop`; frozen schema `0.6`
preserved the v0.5
`action_surface_diff` section and added two independent additive extensions:
- `evidence_matrix` (PR #104) — a compact packet-only review aid
derived from public `report.json` fields. The matrix never contributes
to `release_decision`, CI exit behavior, severity, suppression,
baseline matching, or `agent_summary`; its blocker and review-item
cells are copied from `release_decision`.
- `ReleaseDecisionItem.source` and `ReleaseDecisionItem.policy_evidence_source`
(PR #103) — packet §1 / §2 re-renders carry the same dual-source
provenance that `Finding.source` / `Finding.policy_evidence_source`
expose in the report.
It preserves every v0.5 field
(`human_in_the_loop.runtime_control_disclaimer`,
`human_in_the_loop.source_provenance[]`, `action_surface_diff`). The
`release_decision.verdict` label includes `INSUFFICIENT EVIDENCE` when
the report decision is insufficient evidence.
## Don't use for new gating
- `summary.status` — preserved for v0.7 callers, **baseline-blind**. A baseline-matched critical flips this to `release_blockers_detected` even though `release_decision.decision` correctly classifies it as `review_required`. New consumers should not gate on `summary.status`. See [STABILITY.md §`release_decision.decision` vs `summary.status`](../STABILITY.md#release_decisiondecision-vs-summarystatus).
## Per-finding contextual explanation (v0.12+)
For prose summaries of a single finding (PR comments, chat replies, commit messages), use:
```bash
agents-shipgate explain-finding <FINGERPRINT> \
--from agents-shipgate-reports/report.json --json
```
The payload is the full `Finding` shape (every field on `findings[]` in `report.json`, including `source`, `patches`, `confidence`, `agent_id`, etc.) overlaid with three derived fields:
- `metadata` — full `CheckMetadata` for the check_id (rationale, fires_when, evidence_fields, docs_url, `mvp_tier`) when the check is in the catalog; null for unknown ids (third-party plugins, future checks). `mvp_tier` is display/triage metadata only and never affects gating.
- `explanation` — a deterministic 3–5 sentence prose summary suitable for direct quotation. Names the affected tool, the severity, the recommended fix, and an action-aware closing sentence keyed to `agent_action`. Same inputs always produce the same output.
- `source_report` — **absolute** path (always; relative `--from` values are resolved before serialization) to the report file the explanation was sourced from; round-trippable for caching and audit.
`explain-finding` requires `report_schema_version >= 0.12` because the action-aware explanation depends on per-finding `agent_action`. Pre-v0.12 reports are rejected with `input_parse_error` and a `next_action` pointing at the canonical scan command. The Pydantic `ReadinessReport` model is intentionally looser than this command's contract (so test fixtures can construct minimal findings); the version gate is what enforces v0.12 semantics on emitted reports.
Companion prompt: [`prompts/explain-finding-to-user.md`](../prompts/explain-finding-to-user.md). Use it when you need to translate a finding for a human who has never read the Shipgate docs. Keep `agents-shipgate explain <CHECK_ID>` for static catalog metadata (no specific finding); use `explain-finding` whenever you have a fingerprint and want the evidence-tied prose.
## Authoritative references
- [STABILITY.md](../STABILITY.md) — full alpha stability contract. Source of truth for everything above.
- [AGENTS.md](../AGENTS.md) — agent-facing instructions: install, run, single-turn flow, error semantics.
- [`docs/report-schema.v1.0.json`](report-schema.v1.0.json) — machine-validatable JSON Schema for the current report.
- [`docs/privacy.md`](privacy.md) and [`docs/report-sensitive-fields.json`](report-sensitive-fields.json) — default redaction behavior and sensitive-field inventory.
- [`docs/packet-schema.v0.18.json`](packet-schema.v0.18.json) — machine-validatable JSON Schema for the current packet.
- [`docs/checks.json`](checks.json) — check catalog, including `mvp_tier` for MVP/readiness triage.
## See also
- [`report-reading-for-agents.md`](report-reading-for-agents.md) — reader's primer that walks the JSON in the order a new consumer should read it; complements this field index.
- [`agent-autofix-boundary.md`](agent-autofix-boundary.md) — what an agent may assert mechanically vs. what must defer to a human reviewer when surfacing findings from `report.json`.
<!-- ===== source: docs/checks.md ===== -->
# Check Catalog
Agents Shipgate checks are deterministic static checks. They do not certify safety, run agents, call tools, call LLMs, or verify runtime routing.
## Severity Contract
- `critical`: strict CI exits `20` unless the finding is explicitly suppressed
with a reason (suppression-immune checks remain active).
- `high`: requires human review but does not fail CI by default.
- `medium`: review during release hardening.
- `low` and `info`: informational.
Among Findings, only unsuppressed `critical` findings block strict mode by
default. Independently, report v0.29 semantic `insufficient_evidence` also
exits `20` in strict mode; semantic gaps are not Findings and cannot be
suppressed. Suppressed Findings remain in JSON with `suppressed: true` and are
excluded from active severity counts.
## Evidence Coverage
- `static`: all enumerated tools came from high-confidence static sources.
- `mixed`: at least one enumerated tool came from lower-confidence enrichment, such as SDK AST extraction.
Suppressions do not change evidence coverage.
## Baselines
v0.2 adds local baseline gating. `agents-shipgate baseline save` writes active,
unsuppressed findings to `.agents-shipgate/baseline.json`. A later
`agents-shipgate scan --baseline .agents-shipgate/baseline.json --ci-mode strict`
marks findings as `matched` or `new` and fails only on new findings that match
the active fail policy. Resolved baseline findings are counted in the report
baseline summary and do not fail CI.
## Checks
| Check ID | Severity | Meaning |
| --- | --- | --- |
| `SHIP-INVENTORY-NOT-ENUMERABLE` | high | No tool surface could be enumerated from the manifest inputs. |
| `SHIP-INVENTORY-WILDCARD-TOOLS` | high | A source exposes wildcard/all tools instead of an explicit allowlist. |
| `SHIP-INVENTORY-TOOL-SURFACE-TOO-LARGE` | medium | The normalized tool count exceeds the MVP review threshold. |
| `SHIP-DOC-MISSING-DESCRIPTION` | medium | A tool has no description or a description too short for reliable review. |
| `SHIP-DOC-INJECTION-RISK` | medium/high | A tool description contains instruction-override style language. High only when multiple patterns match on a write/high-risk tool. |
| `SHIP-DOC-SECRET-IN-DESCRIPTION` | medium/high | A tool description contains a secret-like token or credential value. High only when multiple patterns match on a write/high-risk tool. |
| `SHIP-SCHEMA-BROAD-FREE-TEXT` | high | A write/action-like tool accepts broad `action`, `body`, `command`, `updates`, or similar free-form input. |
| `SHIP-SCHEMA-MISSING-BOUNDS` | high | A risky numeric parameter such as `amount`, `count`, or `quantity` lacks a maximum. |
| `SHIP-SCHEMA-FREEFORM-OUTPUT` | medium | A tool returns free-form string output that may later be placed in model context. |
| `SHIP-AUTH-MISSING-SCOPE` | high | A write-like tool has no declared auth scope metadata. |
| `SHIP-AUTH-MANIFEST-BROAD-SCOPE` | high | The manifest declares broad scopes such as `*`, `admin`, or `service:*`. |
| `SHIP-AUTH-TOOL-BROAD-SCOPE` | high | A tool declares broad scopes such as `*`, `admin`, or `service:*`. |
| `SHIP-AUTH-SCOPE-COVERAGE-MISSING` | high | A tool requires scopes that are not covered by `permissions.scopes`. |
| `SHIP-SCOPE-TOOL-OUTSIDE-PURPOSE` | high | A write-capable tool contradicts a read-only declared purpose. |
| `SHIP-SCOPE-PROHIBITED-TOOL-PRESENT` | high | A tool appears to overlap with a manifest `prohibited_actions` entry. |
| `SHIP-POLICY-APPROVAL-MISSING` | critical | A high-risk tool lacks a manifest approval policy. |
| `SHIP-POLICY-CONFIRMATION-MISSING` | high | A destructive, external-write, or customer-communication tool lacks a confirmation policy. |
| `SHIP-ACTION-UNDECLARED` | high | A loaded tool lacks explicit action-surface metadata when explicit actions are required. |
| `SHIP-ACTION-POLICY-VIOLATION` | high | A user-declared action-surface policy, or a selected control pack's obligation, is not satisfied. |
| `SHIP-ACTION-FINANCIAL-WRITE-CONTROL-MISSING` | critical | A newly added financial write action lacks approval, audit, or idempotency controls. |
| `SHIP-ACTION-DESTRUCTIVE-ROLLBACK-MISSING` | critical | A newly added destructive action lacks approval or rollback controls. |
| `SHIP-ACTION-EXTERNAL-COMMUNICATION-AUDIT-MISSING` | high | A newly added external communication action lacks audit evidence. |
| `SHIP-ACTION-WILDCARD-SCOPE` | critical | An action declares or expands into a wildcard/admin-like scope. |
| `SHIP-ACTION-EFFECT-ESCALATED` | critical | An action effect escalated compared with the base surface. |
| `SHIP-ACTION-EFFECT-DOWNGRADE-DECLARED` | high | An action declaration weakens the effect inferred from the loaded tool surface. |
| `SHIP-ACTION-CONTROL-DOWNGRADE` | high | An action declaration weakens an inherited approval or safeguard control. |
| `SHIP-ACTION-APPROVAL-REMOVED` | critical | An existing action approval policy was removed. |
| `SHIP-ACTION-SAFEGUARD-REMOVED` | high | An existing action safeguard was removed. |
| `SHIP-EVIDENCE-APPROVAL-TRACE-MISSING` | high | Local HITL approval trace evidence is missing or incomplete for an approval-required tool. |
| `SHIP-EVIDENCE-OVERRIDE-REASON-MISSING` | high | Local HITL override reason evidence is missing or incomplete. |
| `SHIP-EVIDENCE-HIGH-RISK-EXCLUSION-MISSING` | high | Local high-risk auto-approval exclusion evidence is missing or incomplete. |
| `SHIP-EVIDENCE-HITL-PROMOTION-CRITERIA-MISSING` | high | Local HITL promotion criteria evidence is missing or incomplete. |
| `SHIP-SIDEFX-IDEMPOTENCY-MISSING` | critical/high | A risky write tool lacks idempotency evidence. Critical only when retry behavior is known. |
| `SHIP-API-FUNCTION-SCHEMA-STRICTNESS` | high/medium | An OpenAI API function schema is missing strictness, required fields, or bounded risky fields. |
| `SHIP-API-STRUCTURED-OUTPUT-READINESS` | high/medium | An OpenAI API response format is missing or too broad for downstream decisions. |
| `SHIP-API-PROMPT-TOOL-SCOPE-MISMATCH` | high/medium | Prompt language contradicts the enabled OpenAI API tool surface or lacks approval/confirmation instructions. |
| `SHIP-API-RETRY-POLICY-MISSING` | medium | High-risk OpenAI API tools are enabled without retry policy metadata. |
| `SHIP-API-TIMEOUT-MISSING` | medium | High-risk OpenAI API tools are enabled without timeout metadata. |
| `SHIP-API-TEST-CASES-MISSING` | medium | High-risk OpenAI API tools are enabled without declared test cases. |
| `SHIP-API-TOOL-OUTPUT-SCHEMA-MISSING` | medium | A high-risk OpenAI API tool lacks success/failure output modeling. |
| `SHIP-API-RETRY-WITHOUT-IDEMPOTENCY` | high | A risky OpenAI API write tool may be retried without idempotency evidence. |
| `SHIP-API-TRACE-APPROVAL-MISSING` | medium | A trace sample shows a policy-controlled tool call without approval. |
| `SHIP-API-TRACE-CONFIRMATION-MISSING` | medium | A trace sample shows a policy-controlled tool call without confirmation. |
| `SHIP-API-OPERATIONAL-READINESS` | medium | Deprecated v0.3 compatibility alias for the v0.4 atomic OpenAI API operational readiness checks. |
| `SHIP-ADK-DYNAMIC-TOOLSET-NOT-ENUMERABLE` | high | A Google ADK toolset cannot be statically enumerated and no explicit inventory is declared. |
| `SHIP-ADK-MCP-TOOLSET-UNFILTERED` | high/medium | A Google ADK `McpToolset` has no static `tool_filter`. |
| `SHIP-ADK-FUNCTION-TOOL-METADATA-MISSING` | medium | A Google ADK function/config tool lacks static description or parameter metadata. |
| `SHIP-ADK-LONGRUNNING-CONTRACT-MISSING` | high | A Google ADK long-running tool lacks operation-id and status/progress contract evidence. |
| `SHIP-ADK-GUARDRAIL-EVIDENCE-MISSING` | high | High-risk Google ADK tools lack callback/plugin or policy guardrail evidence. |
| `SHIP-ADK-EVAL-COVERAGE-MISSING` | medium | Production-like Google ADK inputs are present without declared eval files. |
| `SHIP-LANGCHAIN-DYNAMIC-TOOL-SURFACE-NOT-ENUMERABLE` | high | A LangChain/LangGraph tool surface cannot be statically enumerated and no explicit inventory is declared. |
| `SHIP-LANGCHAIN-FUNCTION-TOOL-METADATA-MISSING` | medium | A LangChain/LangGraph function tool lacks static description or parameter metadata. |
| `SHIP-CREWAI-DYNAMIC-TOOL-SURFACE-NOT-ENUMERABLE` | high | A CrewAI tool surface cannot be statically enumerated and no explicit inventory is declared. |
| `SHIP-CREWAI-FUNCTION-TOOL-METADATA-MISSING` | medium | A CrewAI function/class tool lacks static description or parameter metadata. |
| `SHIP-CODEX-PLUGIN-METADATA-MISSING` | medium | A Codex plugin package has incomplete or ambiguous identity metadata. |
| `SHIP-CODEX-PLUGIN-COMPONENT-PATH-MISSING` | high | A declared Codex plugin component path is missing or outside the package/workspace. |
| `SHIP-CODEX-PLUGIN-MARKETPLACE-POLICY-MISSING` | medium | A Codex plugin marketplace entry lacks installation/authentication policy metadata. |
| `SHIP-CODEX-PLUGIN-MCP-SERVER-NOT-ENUMERABLE` | high | A Codex plugin MCP server is declared without a local enumerable tool inventory. |
| `SHIP-CODEX-PLUGIN-APP-SURFACE-NOT-ENUMERABLE` | medium | A Codex plugin connector app surface is not statically enumerable from local metadata. |
| `SHIP-CODEX-PLUGIN-SKILL-METADATA-MISSING` | medium | A Codex plugin skill lacks unique name/description frontmatter. |
| `SHIP-CODEX-BOUNDARY-CONFIG-PARSE-FAILED` | medium | Codex project configuration could not be parsed. |
| `SHIP-CODEX-BOUNDARY-UNKNOWN-PERMISSION-KEY` | medium | Codex permissions contain an unknown high-risk key. |
| `SHIP-CODEX-BOUNDARY-NETWORK-WILDCARD` | high | Codex network permissions allow a wildcard domain. |
| `SHIP-CODEX-BOUNDARY-NETWORK-EXPANDED` | high | Codex network access expanded. |
| `SHIP-CODEX-BOUNDARY-DANGER-FULL-ACCESS` | critical | Codex full-access sandbox is selected. |
| `SHIP-CODEX-BOUNDARY-MCP-AUTO-APPROVE-WRITE` | critical | Codex auto-approves a write or destructive MCP/app tool. |
| `SHIP-CODEX-BOUNDARY-MCP-AUTO-APPROVE-UNKNOWN` | high | Codex auto-approves an MCP server whose tool surface is not statically enumerable. |
| `SHIP-CODEX-BOUNDARY-APP-AUTO-APPROVE` | high | Codex app connector tool approval changed to approve. |
| `SHIP-CODEX-BOUNDARY-AGENTS-SHIPGATE-REQUIREMENT-REMOVED` | medium | Deprecated; historical ID, no current findings. |
| `SHIP-CODEX-BOUNDARY-CI-GATE-REMOVED` | critical | Shipgate GitHub Action no longer invokes the gate. |
| `SHIP-CODEX-BOUNDARY-POLICY-WEAKENED` | critical | Codex boundary policy was weakened. |
| `SHIP-CODEX-BOUNDARY-HOOK-COMMAND-CHANGED` | high | A Codex executable hook changed. |
| `SHIP-CODEX-BOUNDARY-SKILL-COMMAND-CHANGED` | medium | A Codex skill gained command-bearing instructions. |
| `SHIP-AGENT-BOUNDARY-PROTECTED-SURFACE-UNCLASSIFIED` | medium | A protected coding-agent surface lacks a safe static classification. |
| `SHIP-AGENT-BOUNDARY-EXPERIMENTAL-SURFACE-CHANGED` | high | An experimental coding-agent boundary surface changed. |
| `SHIP-AGENT-BOUNDARY-STATIC-REQUIREMENTS-CHANGED` | high | Static host requirements changed. |
| `SHIP-AGENT-BOUNDARY-INPUT-INCOMPLETE` | medium | Boundary input could not be evaluated completely. |
| `SHIP-HOST-BOUNDARY-CONFIG-PARSE-FAILED` | medium | A coding-agent host configuration file could not be parsed. |
| `SHIP-HOST-BOUNDARY-MCP-SERVER-ADDED` | high | A new MCP server was declared for the coding-agent host. |
| `SHIP-HOST-BOUNDARY-MCP-SERVER-CHANGED` | high | An existing MCP server declaration changed its command, URL, args, or env keys. |
| `SHIP-HOST-BOUNDARY-PERMISSION-WILDCARD-ALLOW` | critical | A Claude Code allow rule grants a wildcard surface over a tool that can execute, reach the network, or write, or a setting removes a prompt wholesale. |
| `SHIP-HOST-BOUNDARY-PERMISSION-ALLOW-EXPANDED` | high | The Claude Code permission allowlist expanded, by a scoped rule or a read-only wildcard, or a modelled setting changed, at the setting's rating. |
| `SHIP-HOST-BOUNDARY-PERMISSION-DENY-REMOVED` | high | A Claude Code permission deny rule was removed. |
| `SHIP-HOST-BOUNDARY-HOOK-CHANGED` | high | Claude Code hooks changed. |
| `SHIP-HOST-BOUNDARY-WORKFLOW-WRITE-ALL` | critical | A GitHub workflow grants write-all permissions. |
| `SHIP-HOST-BOUNDARY-WORKFLOW-PERMISSIONS-EXPANDED` | high | GitHub workflow permissions expanded. |
| `SHIP-HOST-BOUNDARY-PULL-REQUEST-TARGET-ADDED` | critical | A GitHub workflow gained a pull_request_target trigger. |
| `LINT-SPEC-002` | high | A skill has invalid YAML frontmatter. |
| `LINT-SPEC-003` | high | A skill is missing required `name` frontmatter. |
| `LINT-SPEC-004` | high | A skill is missing required `description` frontmatter. |
| `LINT-DESC-001` | high | A skill description is too vague to route reliably. |
| `LINT-DESC-003` | medium | A skill description is overbroad and may false-trigger. |
| `LINT-BODY-001` | medium | A skill body lacks a step-by-step procedure. |
| `LINT-BODY-003` | medium | A skill body lacks an output contract. |
| `LINT-BODY-004` | medium | A skill body lacks verification criteria. |
| `LINT-SCRIPT-001` | medium | A skill script lacks documented `--help` usage. |
| `LINT-SCRIPT-004` | medium | A stateful skill script lacks dry-run support. |
| `SEC-PI-001` | critical | A skill artifact contains instruction override language. |
| `SEC-PI-003` | high | A skill artifact tells the agent to hide behavior. |
| `SEC-SECRET-001` | critical | A skill artifact contains a hardcoded secret-like value. |
| `SEC-SECRET-003` | high | A skill artifact instructs credential or secret-file access. |
| `SEC-SCRIPT-001` | critical | Remote content is piped to a shell or interpreter. |
| `SEC-SCRIPT-002` | critical | A destructive or stateful command lacks guardrails. |
| `SEC-REMOTE-001` | medium | A skill fetches remote instruction content. |
| `SEC-REMOTE-002` | critical | Remote content is fetched and executed. |
| `SEC-TOOL-001` | high | A skill pre-approves shell or bash without justification. |
| `SEC-FLOW-004` | medium | A skill lacks data and instruction separation guidance. |
| `SEC-PROV-001` | high | A third-party skill lacks provenance metadata. |
| `SEC-MISMATCH-001` | high | A skill declares read-only behavior but bundled scripts mutate state. |
| `SHIP-N8N-DYNAMIC-TOOL-SURFACE-NOT-ENUMERABLE` | high | An n8n tool surface uses runtime, unresolved, wildcard, or uninventoried custom exposure. |
| `SHIP-N8N-MCP-CLIENT-TOOLSET-UNFILTERED` | high/medium | An n8n MCP Client Tool exposes `All` or `All Except` tools without an explicit inventory. |
| `SHIP-N8N-AI-TOOL-METADATA-MISSING` | medium | An n8n AI-exposed tool lacks static description or parameter metadata. |
| `SHIP-N8N-CREDENTIAL-EVIDENCE-MISSING` | high | Production-like n8n workflows reference credentials without declared credential stubs. |
| `SHIP-N8N-EVAL-COVERAGE-MISSING` | medium | Production-like n8n workflows are present without declared eval files. |
| `SHIP-N8N-SECRET-IN-WORKFLOW-PARAMETER` | high | n8n workflow JSON contains a secret-like value; evidence is redacted. |
| `SHIP-CONDUCTOR-DYNAMIC-TOOL-SURFACE-NOT-ENUMERABLE` | high | A Conductor OSS workflow uses a dynamic or unresolved tool-capability surface. |
| `SHIP-MANIFEST-STALE-SUPPRESSION` | medium | A suppression references a missing check ID or missing tool. |
| `SHIP-MANIFEST-STALE-POLICY` | medium | An approval, confirmation, or idempotency policy references a missing tool. |
| `SHIP-MANIFEST-STALE-RISK-OVERRIDE` | medium | A risk override references a missing tool. |
| `SHIP-MANIFEST-HIGH-RISK-OWNER-MISSING` | high | A high-risk production or production-like tool lacks owner metadata. |
| `SHIP-MANIFEST-UNUSED-SCOPE` | medium/high | `permissions.scopes` contains a scope unused by any loaded tool; broad unused scopes are high. |
| `SHIP-VERIFY-LOCAL-REVIEW-PROVISIONAL` | medium | Verification uses an ephemeral local-review manifest; the result is provisional and cannot carry release authority. |
| `SHIP-VERIFY-TRUST-ROOT-TOUCHED` | medium | A PR changed a release trust-root file; emitted only when a verification context (changed files) is supplied. |
| `SHIP-VERIFY-POLICY-WEAKENED` | high | Base-vs-head effective policy weakened (CI mode downgraded, fail-on loosened, a severity override lowered across a tier, or the control pack moved to one that requires less). |
| `SHIP-VERIFY-POLICY-BASE-ABSENT` | medium | A policy or manifest trust root changed and no base policy could be compared — no base report, a first adoption, or a control pack this build cannot resolve; routed to human review without a weakening claim. |
| `SHIP-VERIFY-BASELINE-OR-WAIVER-EXPANDED` | high | The PR broadens what the gate forgives — a new suppression, a widened waiver scope, or a larger accepted-debt baseline — versus the base. |
| `SHIP-VERIFY-CI-GATE-REMOVED` | critical | The PR deletes the Shipgate CI workflow from an opted-in repo, which would stop the release gate from running. |
| `SHIP-VERIFY-AGENT-INSTRUCTIONS-WEAKENED` | medium | Deprecated; retained for compatibility, with no findings emitted by current scans. |
| `SHIP-VERIFY-TRIGGER-CATALOG-DRIFT` | medium | The PR changes the trigger catalog that decides when Shipgate runs; routed to human review to rule out gate evasion. |
| `SHIP-VERIFY-CAPABILITY-SCOPE-BROADENED` | critical | The PR removes or broadens a dynamically-loaded toolkit's least-privilege configuration bound (e.g. a `stripe_agent_toolkit` allowlist), silently expanding the toolkit surface; blocks rather than degrading to insufficient_evidence. |
| `SHIP-CAP-CONFIG-BINDING-REMOVED` | high | The PR removes the config binding from a dynamic toolkit factory present on both sides of the diff, so the toolkit may fall back to its everything-enabled defaults; routed to human review instead of silent insufficient_evidence parity. |
| `SHIP-CAP-CONFIG-BINDING-CHANGED` | medium | The PR changes the config file that binds a dynamic toolkit's authority; static analysis cannot diff the effective tool list, so the config delta is routed to review. |
## Check Details
### SHIP-INVENTORY-NOT-ENUMERABLE
The scanner could not enumerate any tools from required manifest inputs. Add a local MCP JSON or OpenAPI source before relying on the report.
### SHIP-INVENTORY-WILDCARD-TOOLS
A source exposes wildcard or all-tools access. Replace it with an explicit allowlist so review can reason about the actual release surface.
### SHIP-INVENTORY-TOOL-SURFACE-TOO-LARGE
The normalized tool count exceeds the MVP review threshold. Split or reduce the surface when the report becomes too broad to review.
### SHIP-INVENTORY-LOW-CONFIDENCE-PRODUCTION-SURFACE
A production target depends on lower-confidence extraction, such as SDK AST enrichment. Declare the tools through manifest, MCP, or OpenAPI inputs.
### SHIP-DOC-MISSING-DESCRIPTION
A tool has no description or a description too short for reliable review. Add a concise capability description.
On `mcp_server_source`, a description written in a form the reader cannot resolve, such as a variable, an f-string or a template literal with a substitution, is recorded as `extraction.description: unresolved` and is not reported as missing (#658).
### SHIP-DOC-INJECTION-RISK
A tool description contains instruction-override-like language. Rewrite it as neutral metadata.
Purely heuristic matches default to `medium`; multiple matches on write/high-risk tools are `high`.
### SHIP-DOC-SECRET-IN-DESCRIPTION
A tool description contains a secret-like token or credential value. Remove it and rotate the exposed secret.
Purely heuristic matches default to `medium`; multiple matches on write/high-risk tools are `high`.
### SHIP-SCHEMA-BROAD-FREE-TEXT
A write/action-like tool accepts broad free-form input. Constrain the field with structured schema or enums.
### SHIP-SCHEMA-MISSING-BOUNDS
A risky numeric parameter lacks a maximum. Add a maximum or equivalent policy limit.
### SHIP-SCHEMA-FREEFORM-OUTPUT
A tool returns free-form string output that may later be placed in model context. Prefer structured output for model-consumed tool results.
### SHIP-AUTH-MISSING-SCOPE
A write or sensitive-data tool has no auth scope metadata. Declare scopes in OpenAPI, MCP, or manifest metadata.
### SHIP-AUTH-MANIFEST-BROAD-SCOPE
The manifest declares broad permission scopes such as wildcard or admin scopes. Replace them with operation-specific scopes.
### SHIP-AUTH-TOOL-BROAD-SCOPE
A tool declares broad auth scopes. Use narrower tool scopes where possible.
### SHIP-AUTH-SCOPE-COVERAGE-MISSING
A tool requires scopes that are not covered by `permissions.scopes`. Reconcile the manifest with the tool requirements.
### SHIP-SCOPE-TOOL-OUTSIDE-PURPOSE
A write-capable tool contradicts a read-only declared purpose. Remove the tool or update the declared release scope.
### SHIP-SCOPE-PROHIBITED-TOOL-PRESENT
A tool appears to overlap with a manifest `prohibited_actions` entry. Remove or narrow the tool, or revise policy/scope text.
### SHIP-SCOPE-TOOLKIT-UNBOUNDED
A recognized agent-toolkit constructor (e.g. `stripe_agent_toolkit`) is mounted with no `configuration` allowlist, so the full toolkit surface — which static extraction cannot enumerate — is granted. Routes to human review instead of passing silently. Pass an explicit `configuration` allowlist (resource:verb actions) to bound the mounted tools.
### SHIP-POLICY-APPROVAL-MISSING
A high-risk tool lacks a declared approval policy. Add an approval policy or remove the tool from the release.
### SHIP-POLICY-CONFIRMATION-MISSING
A destructive, external-write, or customer-communication tool lacks a confirmation policy. Add confirmation policy or remove the tool.
### SHIP-ACTION-UNDECLARED
`action_surface.require_explicit_actions` is true, but a loaded tool has no
matching `action_surface.actions[]` declaration. Add action metadata for the
tool or disable the explicit-action requirement.
### SHIP-ACTION-POLICY-VIOLATION
A user-declared `action_surface.policies[]` rule matched an action, and one or
more required dot-path values were absent or different. Satisfy the policy
requirements or narrow/remove the action.
This id also carries two built-in rules, distinguished by
`evidence.policy_id`. `builtin-high-impact-approval` is the approval a
production operation or code execution requires. `control-pack:<pack>:<effect>`
is an obligation the manifest's selected control pack
(`policies.control_pack`) states about an effect with no control check of its
own — a plain write, a privileged read, or identity access. Both are mandatory
current-surface controls: a `checks.ignore` entry records the exception but
does not waive the blocker.
### SHIP-ACTION-FINANCIAL-WRITE-CONTROL-MISSING
A newly added action is classified as `financial_write` and is missing
`approval.required`, `safeguards.audit_log`, or `safeguards.idempotency`.
Declare the required controls before releasing the action.
### SHIP-ACTION-DESTRUCTIVE-ROLLBACK-MISSING
A newly added destructive action is missing `approval.required` or
`safeguards.rollback`. Declare the approval and rollback controls, or remove
the destructive action from the release surface.
### SHIP-ACTION-EXTERNAL-COMMUNICATION-AUDIT-MISSING
A newly added external communication action lacks `safeguards.audit_log`.
Declare audit evidence so reviewers can trace outbound side effects.
### SHIP-ACTION-WILDCARD-SCOPE
An added action declares a broad scope, or a modified action expands into a
broad scope such as wildcard/admin access. Replace it with operation-specific
scopes.
### SHIP-ACTION-EFFECT-ESCALATED
An action changed to a higher-risk effect, such as read to write or write to
destructive. Add reviewer approval for the escalation or reduce the effect.
### SHIP-ACTION-EFFECT-DOWNGRADE-DECLARED
An `action_surface.actions[]` declaration sets a lower-risk effect than
Shipgate inferred from the loaded tool metadata. Align the declared effect
with the inferred operation or remove the weaker declaration.
### SHIP-ACTION-CONTROL-DOWNGRADE
An `action_surface.actions[]` declaration sets an inherited approval or
safeguard control from `true` to `false`. Keep the inherited control enabled
or remove the weakening declaration.
### SHIP-ACTION-APPROVAL-REMOVED
The base action required approval, but the current action no longer does.
Restore `approval.required` or document a reviewed override.
### SHIP-ACTION-SAFEGUARD-REMOVED
An existing action lost a safeguard such as audit logging, idempotency,
rollback, or dry-run support. Restore the safeguard or document a reviewed
override.
### SHIP-EVIDENCE-APPROVAL-TRACE-MISSING
`validation.required_evidence.approval_trace_required` is true, but local
validation evidence does not show `approved: true` for an approval-required
tool. Add local approval trace evidence produced by runtime middleware or
change the declared review posture. Agents Shipgate reads this evidence; it
does not produce or certify it. Missing local evidence does not prove the
runtime approval control is absent.
### SHIP-EVIDENCE-OVERRIDE-REASON-MISSING
`validation.required_evidence.override_reason_required` is true, but override
logs are absent, empty, or include normalized `override`, `bypass`, or
`auto_approve` events without a non-empty `reason`. Record reviewer-visible
reasons in the local override log. Missing local evidence does not prove the
runtime override control is absent.
### SHIP-EVIDENCE-HIGH-RISK-EXCLUSION-MISSING
`validation.required_evidence.high_risk_auto_approval_exclusion_required` is
true, and a high-risk tool with declared approval policy is not listed under
`high_risk_auto_approval_exclusions`. This is separate from
`SHIP-POLICY-APPROVAL-MISSING`: it only fires after approval policy is already
declared, because it checks the local evidence that the tool is excluded from
auto-approval review posture. Missing local evidence does not prove the
runtime exclusion control is absent.
### SHIP-EVIDENCE-HITL-PROMOTION-CRITERIA-MISSING
`validation.target_review_posture` is `limited_auto_approval`, but local
promotion criteria evidence is missing or the canonical required-evidence
flags are not true in the manifest and criteria file. Finding evidence includes
`reason: file_missing` or `reason: flags_missing` so reviewers can distinguish
an absent local source from incomplete criteria. Missing local evidence does
not prove runtime controls are absent.
### SHIP-SIDEFX-IDEMPOTENCY-MISSING
A risky write tool lacks idempotency evidence. Add an idempotency key, idempotent annotation, or declared idempotency policy.
### SHIP-API-FUNCTION-SCHEMA-STRICTNESS
An OpenAI API function schema is not strict enough for reliable tool calls. The check flags missing `strict: true`, missing object parameters, `additionalProperties` not set to `false`, properties omitted from `required`, broad free-text action fields, and risky numeric fields without bounds or enums.
### SHIP-API-STRUCTURED-OUTPUT-READINESS
An OpenAI API response format is missing or under-specified. The check flags missing response schemas for high-risk API tools, broad response objects, decision/status fields without enums, missing `refusal` / `needs_review` / `error` modeling, and missing `downstream_critical_fields`.
### SHIP-API-PROMPT-TOOL-SCOPE-MISMATCH
Prompt files contradict the enabled API tool surface. The check flags prompts that say "advise only" or "read-only" while write/high-risk tools are enabled, and high-risk tools whose prompts do not mention approval and confirmation expectations.
### OpenAI API Operational Readiness Checks
v0.4 splits the former `SHIP-API-OPERATIONAL-READINESS` bundle into atomic
check IDs so suppressions, severity overrides, SARIF rules, and baselines can
target one missing contract at a time. The split checks use `model_config`,
`policy_rules`, simple test cases, and trace samples to flag missing retry
policy, missing timeouts, missing test cases, non-idempotent high-risk tools
with retry evidence, missing success/failure tool-output modeling, and trace
samples that show required approval or confirmation missing.
The old bundled check ID remains as a deprecated compatibility alias through at
least one minor release. v0.4 does not emit new findings with
`SHIP-API-OPERATIONAL-READINESS`, but existing suppressions, severity overrides,
baseline entries, `explain`, `list-checks`, and stale-suppression validation
continue to recognize it. New configs should use the specific v0.4 ID that
represents the condition.
### SHIP-API-OPERATIONAL-READINESS
Deprecated compatibility alias for the v0.3 OpenAI API operational readiness
bundle. Migrate suppressions, severity overrides, and baselines to the specific
v0.4 `SHIP-API-*` readiness checks when you touch the config.
### SHIP-API-RETRY-POLICY-MISSING
A high-risk OpenAI API tool flow runs without declared retry policy metadata.
Reviewers cannot reason about duplicate side effects when retry behavior is
unspecified. Declare `retry_policy` in `openai_api.policy_rules` or
`openai_api.model_config`.
### SHIP-API-TIMEOUT-MISSING
A high-risk OpenAI API tool flow runs without declared timeout metadata.
Without an explicit timeout, failure behavior and tool-call continuation
become ambiguous. Declare a tool-call timeout in policy rules or model
config.
### SHIP-API-TEST-CASES-MISSING
High-risk OpenAI API tools exist with no declared test cases. Tool-call flows
that approve refunds, send mail, or modify state should ship with simple test
cases as release evidence. Add cases under `openai_api.test_cases`.
### SHIP-API-TOOL-OUTPUT-SCHEMA-MISSING
A high-risk OpenAI API tool lacks declared success/failure output modeling.
Reviewers depend on `success_fields` and `failure_fields` to reason about
downstream failure handling. Declare them in policy rules.
### SHIP-API-RETRY-WITHOUT-IDEMPOTENCY
A retry policy is declared and a risky write tool lacks idempotency evidence.
Retries against non-idempotent writes can duplicate financial, destructive, or
external side effects. Either add idempotency evidence or remove the retry
policy for this tool.
### SHIP-API-TRACE-APPROVAL-MISSING
A trace sample shows a policy-controlled tool call with `approved: false` for
a tool that has approval policy evidence elsewhere in the manifest. Implement
the runtime approval gate; **do not edit the trace recording** to flip
`approved` — that patches the evidence, not the agent's behavior.
### SHIP-API-TRACE-CONFIRMATION-MISSING
A trace sample shows a policy-controlled tool call with `confirmed: false`
for a tool that has confirmation policy evidence. Implement the runtime
confirmation gate; **do not edit the trace recording** to flip `confirmed`
— same anti-pattern as the approval-missing finding above.
### SHIP-ADK-DYNAMIC-TOOLSET-NOT-ENUMERABLE
A Google ADK `OpenAPIToolset`, `McpToolset`, or dynamic tools expression could
not be enumerated statically. Provide explicit local OpenAPI, MCP, or ADK tool
inventory inputs before relying on the release report.
### SHIP-ADK-MCP-TOOLSET-UNFILTERED
An ADK `McpToolset` has no static `tool_filter`. Add a narrow filter and an
explicit inventory file so reviewers can see the intended runtime surface.
### SHIP-ADK-FUNCTION-TOOL-METADATA-MISSING
An ADK function or Agent Config tool reference lacks description or parameter
metadata. Add docstrings, type annotations, or explicit local inventory
metadata.
### SHIP-ADK-LONGRUNNING-CONTRACT-MISSING
An ADK `LongRunningFunctionTool` lacks static evidence for operation id and
status/progress fields. Google-style `name` plus `done`, `state`, `phase`,
`metadata`, or `result` fields count as contract evidence; tools may also carry
`annotations.long_running_contract: true` in explicit inventory metadata.
Document the handoff and completion contract before promotion.
### SHIP-ADK-GUARDRAIL-EVIDENCE-MISSING
High-risk ADK tools are present without static callback/plugin or manifest
policy evidence. ADK callbacks and plugins count only as static evidence of
intent; they are not proof that runtime enforcement works.
### SHIP-ADK-EVAL-COVERAGE-MISSING
Google ADK inputs target `production_like` or `production` without declared eval
files. Add eval artifacts that cover expected responses and tool-use
trajectories.
### SHIP-LANGCHAIN-DYNAMIC-TOOL-SURFACE-NOT-ENUMERABLE
A LangChain/LangGraph tool list, binding, or graph node could not be enumerated
statically. Provide an explicit local inventory when tools are produced by
factories, comprehensions, loop-built lists, unresolved imports, or other
runtime-only code. This ID uses `TOOL-SURFACE` instead of ADK's `TOOLSET`
because LangChain exposes ad hoc tool lists and model/graph bindings rather
than a consistent toolset abstraction.
### SHIP-LANGCHAIN-FUNCTION-TOOL-METADATA-MISSING
A LangChain/LangGraph `@tool` function or `StructuredTool.from_function(...)`
surface lacks a static description or parameter metadata. Add docstrings,
function annotations, or same-file Pydantic `args_schema` metadata.
### SHIP-CREWAI-DYNAMIC-TOOL-SURFACE-NOT-ENUMERABLE
A CrewAI agent or crew tool surface could not be enumerated statically. Provide
an explicit local inventory when tools are produced by factories,
comprehensions, loop-built lists, unresolved imports, or other runtime-only
code. This ID uses `TOOL-SURFACE` instead of ADK's `TOOLSET` because CrewAI
agents bind ad hoc tool lists rather than a consistent toolset abstraction.
### SHIP-CREWAI-FUNCTION-TOOL-METADATA-MISSING
A CrewAI `@tool` function or `BaseTool` subclass lacks a static description or
parameter metadata. Add descriptions, `_run` annotations, or same-file Pydantic
`args_schema` metadata.
### SHIP-CODEX-PLUGIN-METADATA-MISSING
A Codex plugin package has incomplete or ambiguous identity metadata. Fill
`name`, `version`, and `description`; keep the plugin name aligned with the
package root; and avoid duplicate plugin names across scanned package roots.
### SHIP-CODEX-PLUGIN-COMPONENT-PATH-MISSING
A Codex plugin component path for skills, MCP servers, apps, or hooks could not
be loaded. Paths must resolve inside both the plugin package and the manifest
directory.
### SHIP-CODEX-PLUGIN-MARKETPLACE-POLICY-MISSING
A marketplace entry lacks `policy.installation`, `policy.authentication`, or
`category`. Add those fields so coding agents can see installation and
authentication posture before adoption.
### SHIP-CODEX-PLUGIN-MCP-SERVER-NOT-ENUMERABLE
A plugin declares an MCP server in `.mcp.json`, but Agents Shipgate does not
execute MCP commands to discover tools. Provide a local MCP tools inventory via
`codex_plugins.mcp_tool_inventories`.
### SHIP-CODEX-PLUGIN-APP-SURFACE-NOT-ENUMERABLE
A plugin declares a connector app in `.app.json`. Connector-backed capabilities
are externally mediated and are review items unless a local inventory or policy
artifact documents the effective surface.
### SHIP-CODEX-PLUGIN-SKILL-METADATA-MISSING
A `skills/**/SKILL.md` file is missing parseable `name` or `description`
frontmatter, or duplicates another skill name in the same plugin. Give every
skill a unique routing name and clear description.
### SHIP-CODEX-BOUNDARY-CONFIG-PARSE-FAILED
A changed repo-local `.codex/config.toml` or `.codex/hooks.json` cannot be
parsed. Fix the malformed config or have a human review the boundary change.
### SHIP-CODEX-BOUNDARY-UNKNOWN-PERMISSION-KEY
A changed `.codex/config.toml` contains an unknown key below a permissions
profile or permissions network table. Review the key before trusting the local
Codex boundary.
### SHIP-CODEX-BOUNDARY-NETWORK-WILDCARD
A changed Codex permission profile allows a wildcard domain. Replace wildcard
network access with explicit domains or get human approval.
### SHIP-CODEX-BOUNDARY-NETWORK-EXPANDED
Codex workspace-write network access or full network mode was enabled. Have a
human approve the expanded local execution boundary.
### SHIP-CODEX-BOUNDARY-DANGER-FULL-ACCESS
Codex `sandbox_mode` or `default_permissions` selects `danger-full-access`.
Use a narrower permission profile or get explicit human approval.
### SHIP-CODEX-BOUNDARY-MCP-AUTO-APPROVE-WRITE
A changed MCP or app tool approval mode is `approve` for a write or
destructive-looking tool. Do not auto-approve write or destructive tools.
### SHIP-CODEX-BOUNDARY-MCP-AUTO-APPROVE-UNKNOWN
Codex auto-approves an MCP server whose tool surface is not statically
enumerable. Enumerate MCP tools or keep the approval mode at `prompt`.
### SHIP-CODEX-BOUNDARY-APP-AUTO-APPROVE
A Codex app connector tool approval changed to `approve`. Review connector
approval changes before local automation.
### SHIP-CODEX-BOUNDARY-AGENTS-SHIPGATE-REQUIREMENT-REMOVED
Deprecated in 1.1.0. The ID remains available for historical reports and
configuration; current runs do not judge removed or softened prose. Complete
parsed structure and unresolved-input review replace the word heuristic.
Structured host readers remain active (#545, #516).
### SHIP-CODEX-BOUNDARY-CI-GATE-REMOVED
The Shipgate GitHub Actions workflow was deleted or no longer contains a
Shipgate invocation. Restore the workflow or get human approval to remove it.
### SHIP-CODEX-BOUNDARY-POLICY-WEAKENED
The Codex boundary policy was deleted or downgraded. Restore the stricter
policy or get human approval before weakening the local boundary gate.
### SHIP-CODEX-BOUNDARY-HOOK-COMMAND-CHANGED
A changed Codex hooks source contains a command hook. Review executable hooks
before relying on them.
### SHIP-CODEX-BOUNDARY-SKILL-COMMAND-CHANGED
Deprecated in 1.1.0. The ID remains readable but current runs emit no command
claim from prose words. Supported skill frontmatter and preprocessing
declarations remain in structural comparison; unknown or malformed structure
remains a coverage limitation (#545, #516).
### SHIP-AGENT-BOUNDARY-PROTECTED-SURFACE-UNCLASSIFIED
A recognized host, instruction, policy, state, or workflow surface changed
without a specialized safe classification. The change routes to human review at
PR time because absence of a risk finding is not evidence of non-broadening
behavior. In the local `shipgate check` loop this medium row is graded: unless
the path is a gate-governing trust root (manifest, policy, CI gate, or
`.agents-shipgate/` state), the coding agent may finish its turn with the
obligation carried in `pending_review[]` rather than stopping.
### SHIP-AGENT-BOUNDARY-EXPERIMENTAL-SURFACE-CHANGED
An adapter marked experimental observed a boundary change. Experimental
coverage cannot authorize coding-agent completion without human review.
### SHIP-AGENT-BOUNDARY-STATIC-REQUIREMENTS-CHANGED
The repository's `.codex/requirements.toml` changed. This is a reviewer-owned
host trust root for approvals, sandboxing, and network constraints.
### SHIP-AGENT-BOUNDARY-INPUT-INCOMPLETE
A relevant diff or file was truncated, malformed, unsafe to read, traversing,
binary, oversized, or otherwise unresolved. Supply coherent input and rerun.
### SHIP-HOST-BOUNDARY-CONFIG-PARSE-FAILED
A changed `.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`,
`.claude/settings(.local).json`, or `.github/workflows` file cannot be parsed,
or its content cannot be resolved from the diff. Fix the malformed host config
or have a human review the change.
A settings file that parses but changes a top-level key outside the
host-boundary rule allow-list, such as `outputStyle`, also raises this check,
with evidence kind `unknown_host_config_key` and the key's name: the file was
read and the key's meaning is not modelled. A Claude Code setting the host
inventory models is not an unknown key. `enableAllProjectMcpServers`,
`skipDangerousModePermissionPrompt`, `enabledMcpjsonServers`,
`disableAllHooks` and the managed-only switches are rated by the setting table
and raise `SHIP-HOST-BOUNDARY-PERMISSION-WILDCARD-ALLOW` or
`SHIP-HOST-BOUNDARY-PERMISSION-ALLOW-EXPANDED` instead (#827).
### SHIP-HOST-BOUNDARY-MCP-SERVER-ADDED
A changed MCP server declaration file adds a server key that did not exist
before. Have a human review the new MCP server before the agent can use it.
### SHIP-HOST-BOUNDARY-MCP-SERVER-CHANGED
An existing MCP server changed its `command`, `args`, `url`, `serverUrl`, or
`env` keys. Review the change — env var values never appear in evidence, key
names only.
### SHIP-HOST-BOUNDARY-PERMISSION-WILDCARD-ALLOW
A changed `.claude/settings.json` or `.claude/settings.local.json` adds an
allow rule that is `*`, a bare tool name, or a wildcard-shaped rule such as
`Bash(*)`, **and** the tool it grants can reach beyond reading. Do not allow
wildcard tool permissions over execution, network access or writes; scope the
rule to specific commands.
A documented [arbitrary-code launcher prefix](engineering/exec-equivalent-permissions.md)
followed by ` *` or `:*`, such as `Bash(python3 -c *)` or `Bash(npx *)`,
also follows this critical/block route (#824), and so does a rule wider than
one, such as `Bash(python3 *)` or `Bash(npx*)`. Exact commands do not enter
that tier. This rating does not change rule containment or deny precedence.
An MCP rule that names tools within one server, such as
`mcp__github__get_issue`, is scoped: it raises
`SHIP-HOST-BOUNDARY-PERMISSION-ALLOW-EXPANDED`. `mcp__github` and
`mcp__github__*` grant every tool the server offers and still raise this
check. Before #816 every MCP rule, being a bare token, reached it.
A wildcard grant over a read tool — `Read(**)`, `Glob`, `Grep(**)` — raises
`SHIP-HOST-BOUNDARY-PERMISSION-ALLOW-EXPANDED` instead. It is still an
expansion and still reviewed, but it is not a release blocker: the agent
already has the workspace checked out, and turning that read into an
exfiltration needs a network or write grant, which is its own finding. Before
v0.17 every wildcard shape reached this check, so the ordinary coding-agent
configuration blocked the release at `critical` (#657).
A Claude Code setting that removes a prompt wholesale raises this check too,
at `critical`: `defaultMode: bypassPermissions` skips every permission prompt,
`skipDangerousModePermissionPrompt: true` skips the confirmation before that
mode starts, and `enableAllProjectMcpServers: true` approves every MCP server
the project's `.mcp.json` declares. The rating comes from one table in the
engine that also rates the setting's `audit --host` grant and its `diff`,
`verify` and `check` rows, so every surface gives the value one severity
(#827). `defaultMode` evidence names the `mode`; every other setting's names
the `setting` and its `value`. Either is the value the setting's
`audit --host` grant publishes, with credentials redacted, cut to at most 200
characters. `defaultMode: dontAsk` no longer reaches this
check: Claude Code documents that it denies whatever no allow rule permits
rather than running it, so it raises
`SHIP-HOST-BOUNDARY-PERMISSION-ALLOW-EXPANDED` at `medium`.
### SHIP-HOST-BOUNDARY-PERMISSION-ALLOW-EXPANDED
A changed Claude Code settings file adds a `permissions.allow` entry that is
either scoped (`Bash(npm test:*)`, `mcp__github__get_issue`) or a wildcard over
a read-only tool (`Read(**)`). Have a human approve the new permission allow
rule.
An added rule that an allow rule the file already had covers grants nothing
new and does not fire (#661). `Bash(npm:*)` and `Bash(npm *)` are two
spellings of one rule, which also allows the bare `npm`, so replacing either
with `Bash(npm test:*)` or `Bash(npm test)` does not fire (#816). The space
before the star is part of the rule, so `Bash(npm run test:*)` does not cover
`npm run test:unit`: adding that command fires.
It also fires when a change sets any other value of a Claude Code setting the
host inventory models, at the rating the engine's setting table gives that
value — the rating the setting's grant and host-diff row carry (#827):
| Setting and value | Rating | Why |
| --- | --- | --- |
| `defaultMode: acceptEdits` | high | accepts file edits without a prompt |
| `defaultMode: auto` | high | lets Claude Code approve tool calls itself instead of prompting |
| a `defaultMode` Claude Code does not document | high | what it permits is not established |
| an `enabledMcpjsonServers` entry | high | approves that project `.mcp.json` server without a prompt |
| an `enabledMcpjsonServers` entry that names no server (an object, a number, a blank string) | high | what it permits is not established |
| `defaultMode: dontAsk` | medium | denies every tool call no allow rule already permits, instead of prompting |
| `defaultMode: plan`, `defaultMode: default` | medium | lets nothing run without a prompt that an allow rule does not already permit |
| `disableBypassPermissionsMode`, `disableAllHooks`, `allowManagedPermissionRulesOnly`, `allowManagedHooksOnly`, and `false` for `enableAllProjectMcpServers` or `skipDangerousModePermissionPrompt` | medium | changes which prompts, hooks or rules apply without letting anything run without a prompt that an allow rule does not permit |
A rating is of the value, not of the change: whether a value is wider than the
one it replaced is not modelled, so every value a change sets is reviewed.
Moving a value between `permissions` and the top level sets it, in either
direction, and raises this check or `SHIP-HOST-BOUNDARY-PERMISSION-WILDCARD-ALLOW`
at the value's rating: the host inventory reads a setting from either place,
but Claude Code documents `defaultMode` under `permissions` and
`enableAllProjectMcpServers` at the top level, so moving a top-level
`defaultMode: bypassPermissions` into `permissions` turns the mode on without
changing its value. Removing a setting raises nothing of its own, as removing
`defaultMode` never did. A host-boundary policy may raise this rule, and then
raises these ratings with it; it cannot lower them.
### SHIP-HOST-BOUNDARY-PERMISSION-DENY-REMOVED
A changed Claude Code settings file drops an entry from `permissions.deny`.
Have a human confirm the removed deny rule is no longer needed.
### SHIP-HOST-BOUNDARY-HOOK-CHANGED
A changed Claude Code settings file adds, modifies, or removes hook handlers.
Review executable hook changes before the agent relies on them.
### SHIP-HOST-BOUNDARY-WORKFLOW-WRITE-ALL
A changed `.github/workflows` file sets `permissions: write-all` at the top
level or for a job. Replace write-all with the minimal explicit permission
scopes.
### SHIP-HOST-BOUNDARY-WORKFLOW-PERMISSIONS-EXPANDED
A changed `.github/workflows` file grants an explicit write scope that the
base did not grant, at the top level or per job. Have a human approve the
expanded workflow write permission.
### SHIP-HOST-BOUNDARY-PULL-REQUEST-TARGET-ADDED
A changed `.github/workflows` file adds `pull_request_target` to its triggers.
Review it carefully — `pull_request_target` runs with secrets on fork PRs.
### LINT-BODY-001
A `SKILL.md` body lacks an ordered Procedure, Steps, or Workflow section. Add
step-by-step instructions an agent can follow.
### LINT-BODY-003
A `SKILL.md` body does not define the expected output artifact, response shape,
or completion criteria. Add an Output section.
### LINT-BODY-004
A `SKILL.md` body lacks verification or acceptance criteria. Add checks the
agent can run or cite before reporting completion.
### LINT-DESC-001
A skill description is too short or generic to route reliably. Rewrite it with
concrete trigger conditions and domain-specific nouns.
### LINT-DESC-003
A skill description is broad enough to false-trigger. Narrow it to the specific
workflow, file type, or domain where the skill applies.
### LINT-SCRIPT-001
A bundled script lacks documented `--help` usage. Document safe invocation in
`SKILL.md` and make usage discoverable without interaction.
### LINT-SCRIPT-004
A bundled script appears stateful or destructive but lacks dry-run support. Add
`--dry-run` or document a non-mutating preview path.
### LINT-SPEC-002
`SKILL.md` frontmatter is not parseable YAML mapping data. Fix the frontmatter
before relying on skill discovery.
### LINT-SPEC-003
`SKILL.md` is missing required `name` frontmatter. Add a stable routing name.
### LINT-SPEC-004
`SKILL.md` is missing required `description` frontmatter. Add a clear trigger
description that tells agents when to load the skill.
### SEC-FLOW-004
A skill combines untrusted content with outbound or secret-access behavior but
lacks instruction/data separation guidance. State that untrusted content is data
and must not supply agent instructions.
### SEC-MISMATCH-001
A skill declares read-only or review-only behavior but bundled scripts mutate
files or external state. Update the declared purpose or remove the mutation.
### SEC-PI-001
A skill or related artifact tells agents to ignore or override higher-priority
instructions. Remove the instruction-override language.
### SEC-PI-003
A skill or related artifact tells agents to hide behavior from users, reviewers,
or logs. Remove concealment instructions.
### SEC-PROV-001
A skill marked as third-party lacks `metadata.shipgate.source` provenance. Add
source, source reference, owner, and review metadata.
### SEC-REMOTE-001
A skill fetches mutable remote prompt, instruction, or skill content at runtime.
Avoid remote instruction fetch or document source trust and pinning.
### SEC-REMOTE-002
A skill sources or executes content fetched from a remote URL. Remove the
runtime remote-code execution or pin, verify, and sandbox the content.
### SEC-SCRIPT-001
A skill artifact pipes remote content to a shell or interpreter. Vendor the
script or verify pinned content instead of executing mutable remote bytes.
### SEC-SCRIPT-002
A skill artifact contains destructive or stateful command patterns without
dry-run, confirmation, or path validation. Add guardrails or remove the command.
### SEC-SECRET-001
A skill artifact contains a hardcoded secret-like value. Remove it and rotate the
exposed credential; reports redact the value by kind and location.
### SEC-SECRET-003
A skill artifact instructs agents to read broad credential files or secret-bearing
environment variables. Replace with scoped placeholder guidance.
### SEC-TOOL-001
A skill pre-approves shell or bash-like tools without reviewed-script or sandbox
justification. Remove preapproval or document the review boundary.
### SHIP-N8N-DYNAMIC-TOOL-SURFACE-NOT-ENUMERABLE
An n8n workflow uses a runtime expression in a tool name, an unresolved
Call-Workflow target, wildcard MCP Server/Client exposure, or an uninventoried
community/custom tool node. Provide a local n8n/MCP inventory or replace the
dynamic exposure with a static allowlist. This is high severity in every
environment because static release evidence cannot prove the actual tool
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
release review. The severity is environment-sensitive because the selector is
easy to narrow before production, while production-like use increases blast
radius.
### SHIP-N8N-AI-TOOL-METADATA-MISSING
An n8n AI-exposed tool lacks a static description or parameter metadata. Add
tool descriptions, `$fromAI()` metadata, workflow input schemas, or explicit
inventory metadata.
### SHIP-N8N-CREDENTIAL-EVIDENCE-MISSING
Production-like n8n workflows reference credentials but no local credential
stubs are declared. Declare source-control credential stubs so reviewers can
see credential types without seeing secret values.
### SHIP-N8N-EVAL-COVERAGE-MISSING
n8n workflows target `production_like` or `production` without declared eval
files. Add eval artifacts that cover expected responses and tool-use
trajectories.
### SHIP-N8N-SECRET-IN-WORKFLOW-PARAMETER
An n8n workflow parameter, node note, `pinData` entry, or `staticData` entry
contains a secret-like value. Evidence includes only the source reference,
stable pointer, and secret kind; it never includes the matched secret value or
a verifier hash for that value.
### SHIP-CONDUCTOR-DYNAMIC-TOOL-SURFACE-NOT-ENUMERABLE
A Conductor OSS workflow uses a dynamic or missing MCP method/server, dynamic
LLM tool advertisement, runtime-generated task, or unresolved sub-workflow.
Make the target static and locally reviewable before release. The adapter does
not connect to Conductor, MCP servers, or model providers to fill the gap.
### SHIP-TRUST-MANIFEST-UNPROTECTED
The manifest declares `ci.mode: strict`, so this repository states the strongest
posture it can — and no CODEOWNERS file GitHub would read assigns an owner to the
manifest that decides it. Add a rule covering the manifest path and require
review on the branch it merges to, so changing what the gate enforces takes a
named human's approval.
Two halves are required, and the finding says which one is missing. A rule set
must cover the manifest **and cover itself**: one that owns `shipgate.yaml` but
not `CODEOWNERS` describes a protection one edit deep, because the same pull
request can delete the rule. `* @team` satisfies both.
It reads the file GitHub would read, and fails closed where it would not: outside
a git checkout there is no pull request for a rule to gate, a file of 3 MB or
more is ignored by GitHub entirely (with no fallback to a lower-precedence
location), and a token GitHub does not accept as an owner assigns nobody — which
matters because last-rule-wins means a narrower rule with a typo *removes* the
ownership a broader rule granted.
Guidance only: it never becomes a blocker or a review item, and it never moves a
verdict. Branch protection lives in repository settings that no file in a
checkout can read, so this check reports the CODEOWNERS half and says so rather
than inferring the rest. Silent on `ci.mode: advisory`, where the manifest is not
yet enforcing anything — `agents-shipgate doctor` names manifest protection as a
step from adoption rung 2 to rung 3.
### SHIP-MANIFEST-STALE-SUPPRESSION
A suppression references an unknown check ID or a tool that is not loaded in the
current scan. Remove stale suppressions so reviewers can trust the suppression
list as current release intent.
### SHIP-MANIFEST-STALE-POLICY
A policy entry references a tool that is not loaded. Remove or update stale
approval, confirmation, or idempotency policies so release policy matches the
actual tool surface.
### SHIP-MANIFEST-STALE-RISK-OVERRIDE
`risk_overrides.tools` references a tool that is not loaded. Remove stale
overrides or update them to the current tool names.
### SHIP-MANIFEST-HIGH-RISK-OWNER-MISSING
A high-risk tool in `production_like` or `production` has no owner metadata.
Declare an owner in the tool source or `risk_overrides.tools` so reviewers know
who is accountable for remediation.
### SHIP-MANIFEST-UNUSED-SCOPE
`permissions.scopes` includes a scope not required by any loaded tool. Remove
unused scopes or add tool metadata showing why the permission is needed. Broad
unused write/admin scopes are `high`; other unused scopes are `medium`.
### SHIP-BASELINE-INTEGRITY-MISMATCH
Baseline file integrity check failed. Emitted when the baseline JSON has been
edited outside `agents-shipgate baseline save` (hash mismatch against the
audit log), when the audit log is missing or empty for a non-empty baseline,
when the audit log is malformed, when an entry's `provenance.run_id` is not
present in the audit log, or when an entry pre-dates the v0.5 provenance
contract. In
`baseline.integrity_mode: strict` the finding carries `blocks_release=true`
and `agents-shipgate baseline verify --strict` exits with code 6.
Re-run `agents-shipgate baseline save` to refresh the baseline alongside its
audit row; investigate the diff before accepting.
### SHIP-BASELINE-ENTRY-EXPIRED
A baseline entry's reviewer-set `provenance.expires` date is past today.
Renewable consent is a deliberate choice: accepted technical debt should
need re-review on a schedule, not a silent extension. Re-review the entry
and either remove it, fix the underlying finding, or extend
`provenance.expires` with a new `reason`.
### SHIP-BASELINE-ENTRY-STALE
A baseline entry no longer corresponds to an active finding or check ID.
Two sub-kinds, both `low` severity:
- `deprecated_check_id` — entry references an alias in `LEGACY_CHECK_ID_ALIASES`.
Update the entry to the canonical replacement check IDs (re-running
`baseline save` does not rewrite check IDs).
- `resolved_not_pruned` — entry matched no active scan finding. Re-run
`agents-shipgate baseline save` to drop the entry from the baseline.
### SHIP-VERIFY-LOCAL-REVIEW-PROVISIONAL
`verify` loaded a manifest Git could not prove is a committed repository input.
The reserved `.agents-shipgate-local-review.yaml` path is always
`local_review`; another untracked custom path is `uncommitted`; Git inspection
failure is `unknown`. The finding goes through the ordinary
`release_decision.decision` engine at review-required severity; it is not a
second verdict. Verifier notes, the verification plan, and handoff blockers
record the exact provenance, while the terminal receipt denies merge
authority. Adopt `shipgate.yaml` through `init --write`, verify it from the
evaluated Git tree, and follow any resulting human-review route before using
the policy as release evidence. Git presence alone does not prove human review.
### SHIP-VERIFY-TRUST-ROOT-TOUCHED
A PR changed a file that defines the release gate's trust spine — the
manifest (`shipgate.yaml`), `.agents-shipgate/` state (baselines,
waivers), `policies/`, `prompts/`, the Shipgate CI gate
(`.github/workflows/agents-shipgate.yml`), agent instructions
(`AGENTS.md`, `CLAUDE.md`, `.claude/`, `.cursor/rules/`,
`.agents/skills/`, `.codex/`), Codex plugin packages (`.codex-plugin/`),
or tool-surface declarations (`.app.json`, `.mcp.json`, `SKILL.md`).
This is Tier A trust-root protection: pure path/glob classification of
the changed files. It is the cheap half of the reward-hacking guard — a
coding agent told to make CI pass can weaken the gate instead of fixing
the readiness issue, so touching a trust root must require human review.
The finding fires only when a verification context (changed files) is
supplied (`agents-shipgate scan --changed-files ...` or, later, `verify`);
a plain `scan` emits nothing. It is one ordinary `Finding` at `medium`
severity routed through `release_decision` — never a second verdict.
A host settings change that can only take authority away does not fire
(#661). That holds when `.claude/settings.json`, `.claude/settings.local.json`
or `.cursor/cli.json` changes nothing but its rule lists, every added allow
rule is subsumed by an allow rule the file already had, and no deny rule, nor
for Claude Code an ask rule, was removed. `check` records such a change as the
`host_settings_narrowed` diagnostic. A hook, any other key, a rule list that is
not a list of rules, or an added rule the permission lattice cannot decide
still fires.
### SHIP-VERIFY-POLICY-WEAKENED
Tier B trust-root protection: instead of classifying *which* files
changed, it compares the normalized effective-policy snapshot of the base
report (supplied via `--diff-from`) against the head manifest and fires
when the gate moved toward *less* review or *less* blocking — CI mode
downgraded (e.g. `strict` → `advisory`), the fail-on severity set lost a
tier, a check's severity override dropped across a tier boundary, or
`policies.control_pack` moved to a pack that requires *less* of some action
effect (`kind: control_pack_weakened`). The comparison is semantic, not a text
diff, so it is robust to reformatting.
The first three kinds answer "does the same finding still block?"; the control
pack answers "does the same action still produce the finding?", which is the
other way a gate gets weaker. A pack move is one changed line, so it is one
finding: `evidence.removed_controls` carries `{effect, controls}` for every
effect that lost something, and the sentence names a bounded prefix plus how
many it is not naming.
Where either side names a control pack this build cannot resolve, no
comparison is possible in either direction. That routes to
`SHIP-VERIFY-POLICY-BASE-ABSENT` with `kind: control_pack_unrecognized` rather
than reading as no weakening.
The claim is base-relative, so this check fires only when a base snapshot
exists to compare against. The no-base fail-safe is its own reason code,
`SHIP-VERIFY-POLICY-BASE-ABSENT`. Category `verify` (suppression-immune,
floor `high`); never a second verdict.
### SHIP-VERIFY-POLICY-BASE-ABSENT
The fail-safe for "there is no base policy to compare against". It fires in
verify mode when the PR touched a policy/manifest trust root
(`**/shipgate.yaml`, `**/policies/**`, `**/.agents-shipgate/**`, or the
configured manifest under any name) and no base effective-policy snapshot
was available — either because the base carries no Shipgate manifest at all
(a first adoption) or because no base report was obtainable (no
`--diff-from`, a pre-v0.22 base, or a base scan that produced none).
It makes no weakening claim in either direction, which is the point: on a
first adoption the base had no gate, so a weakening definitionally could not
have happened, and reporting one told every new adopter that their first PR
had loosened something. It is equally not a pass — a reward-hacker must not
be able to dodge review by breaking the base scan — so the change is routed
to a human at `medium` under this reason code instead.
Two evidence kinds separate the two conditions:
- `manifest_introduced` — git proves the base carries no manifest under any
name and the diff removes none, so this PR adopts the gate. This is the
one case that reports `verifier_summary.policy_weakened: false`.
- `base_snapshot_unavailable` — the comparison simply could not be made. The
direction is unprovable, so `policy_weakened` stays raised; a
rename-and-loosen diff cannot clear the alarm by hiding the base.
Category `verify` (suppression-immune, floor `medium`). Like the rest of the
family it requires a declared `human_ack` for the `policy` surface and is
routed through the one decision engine — never a second verdict.
### SHIP-VERIFY-BASELINE-OR-WAIVER-EXPANDED
Tier B: detects a PR that broadens what the gate forgives — a new entry in
`checks.ignore`, a widened waiver scope (e.g. one tool widened to `*`), or
a larger accepted-debt baseline — by a base-vs-head superset comparison of
the effective-policy snapshot. Suppressing or baselining a finding instead
of fixing it is a classic reward hack; this makes the expansion
release-visible. Requires a base snapshot (touching the files alone is
already covered by `SHIP-VERIFY-TRUST-ROOT-TOUCHED`). Category `verify`,
floor `high`.
### SHIP-VERIFY-CI-GATE-REMOVED
Tier B: fires when, in verify mode, a Shipgate CI workflow path
(`.github/workflows/agents-shipgate.yml`/`.yaml`) appears in the changed
files **and** that file no longer exists on disk — i.e. the PR deleted the
gate. Detectable without a base snapshot. Emitted at `critical` (floor
`high`): removing CI enforcement from an opted-in repo is the strongest
weakening signal in the family.
### SHIP-VERIFY-AGENT-INSTRUCTIONS-WEAKENED
Deprecated in 1.1.0 (#516). The registered ID, severity metadata and historical
report compatibility remain for at least one minor version cycle. Current scans
emit no findings for this ID: a path change does not establish that
natural-language instructions weakened a control.
Existing structured permission, MCP, CI and hook readers remain active,
including changes beside prose in the same directory. The local word-based
instruction and skill emitters are also retired while their IDs remain readable.
The shared comparison in #545 clears only complete unchanged supported instruction
structure; unknown or malformed inputs retain explicit coverage/review routes.
`SHIP-VERIFY-TRUST-ROOT-TOUCHED` remains active for structural trust roots.
Neither deprecation nor preflight substitutes for a current verifier result.
### SHIP-VERIFY-TRIGGER-CATALOG-DRIFT
Tier B: the trigger catalog (`docs/triggers.json` or an
`.agents-shipgate` trigger config) decides *when* Shipgate runs. Editing
it can carve out paths so the gate stops firing — a gate-evasion one level
up from suppressing findings. Fires on changed-file membership in verify
mode at `medium`; the human confirms the change does not create a path
that evades the release gate.
### SHIP-VERIFY-CAPABILITY-SCOPE-BROADENED
Tier B: some agent toolkits load their tools through a runtime factory
(`*toolkit.get_tools()`) the static extractor cannot enumerate, so the only
statically-provable least-privilege signal is the *constructor's* permission
allowlist (`configuration={"actions": {...}}`). This check diffs that bound
base-vs-head and fires `critical` when the head removes it (mounting the full
toolkit surface — e.g. refund / cancel / dispute on a customer-support agent)
or adds permissions the base allowlist did not. Without it the opaque factory
would degrade the verdict to `insufficient_evidence` instead of `blocked`. A
coding agent cannot self-approve the broadening; a human must re-apply a
least-privilege configuration or explicitly approve the expanded surface. A
narrowing (bound added or tightened) emits nothing.
### SHIP-CAP-CONFIG-BINDING-REMOVED
Config-bound capability detection: a dynamic toolkit factory whose
authority-bearing constructor argument is *bound from a config read*
(`json`/`yaml`/`toml` load, `os.environ`, pydantic settings) hides its
effective tool surface from static enumeration on both sides of a verify
diff. When the same factory site appears on both sides and the head removes
the binding entirely, the toolkit may fall back to its everything-enabled
defaults — authority expands while looking like cleanup, and the generic
capability diff sees nothing. Fires `high` in verify mode and routes to
human review instead of silent `insufficient_evidence` parity. Fail-safe by
design: a head binding that is merely *unreadable* (`unknown`) never fires
this check — it degrades to a source warning; a head that moves to a
literal allowlist is the safe direction and emits nothing.
### SHIP-CAP-CONFIG-BINDING-CHANGED
Companion review item: the same factory site is config-bound on both sides
of the diff and the PR changes the bound config file itself (e.g. adding
`"refund"` to an actions list). Static analysis cannot diff the effective
tool list, so the config delta is routed to review at `medium` with the
inventory remedy. Fires only when the binding's literal config path matches
a changed file — env- or settings-bound factories never guess a match.
### SHIP-MCP-ENV-SECRET-PASSTHROUGH
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.
### SHIP-MCP-AUTO-APPROVE-SIDE-EFFECT
Fires when an MCP tool classified as write, destructive, external,
financial, or production is configured with approval mode `approve`. This is
an explicit release blocker because auto-approved side-effecting MCP tools
can let an agent act outside the review boundary.
### SHIP-MCP-ANNOTATION-CONTRADICTION
Fires when an explicit MCP narrowing annotation conflicts with independent
side-effect evidence on the same tool: `readOnlyHint: true` beside write or
destructive evidence, or `destructiveHint: false` beside destructive evidence.
Absent annotations follow the MCP defaults and never trigger this check.
Policy-eligible protocol, provider, or scope evidence outranks a conflicting
keyword or pattern inference: for example, an OpenAPI `GET` does not become a
contradiction merely because its description mentions a later delete. Structural
side-effect evidence still triggers the check.
Only source observations independent of the published MCP annotation contribute.
Reviewed `action_surface.actions[].effect` declarations are deliberately excluded:
they are human trust-root assertions, not a second observation of the server.
Likewise, a source that publishes both `readOnlyHint: true` and
`destructiveHint: true` remains a `conflicting_effect_evidence` semantic gap; this
check does not relabel that same-source contradiction as independent evidence.
On `mcp_server_source` the published annotation is a literal hint read from the
server's own Python registration (`annotations=` on a FastMCP decorator). There
it is only ever the claim under review: it is not effect evidence, so it can
neither corroborate a read that would suppress a conflict nor lower the tool's
effect by itself. The source route reads no function body, so a tool whose only
side-effect evidence is its body — `process_account` calling `_ITEMS.clear()`
under `readOnlyHint: True` — does not fire; the same body behind a name such as
`purge_account` does, on the keyword.
Supported structural instances route to review because MCP clients may use these
advisory hints to reduce confirmation prompts. Inferred-only instances remain
visible in `report.findings` with `support.policy_eligible: false` and
`agent_action: informational`; they also produce an evidence gap, but are excluded
from blockers, named review items, and release contribution. The check never
changes the engine's conservative permission verdict.
With a base report, `annotation_surface_changed` compares the exact complete
annotation map (including otherwise unrecognized keys and list order), while
`independent_evidence_unchanged` compares the independent semantic evidence. The
finding uses `form: delta` and fills `annotation_changes` only when reconstruction
proves the exact narrowing-hint flip against the base hash; unrelated co-changes
remain the static form. This semantic check runs in the normal
`scan`/`check`/`verify` path. It is intentionally not duplicated in the narrower
`mcp audit` host/config-diff rule table.
### SHIP-MCP-UNKNOWN-TOOL-SCHEMA
Fires for new or changed MCP capabilities whose static metadata cannot prove
the callable surface and side effect, including wildcard server exposure.
Provide an explicit local MCP inventory/schema or keep the server behind
human review. Unknown side effects route to review by default; they become
blockers only when paired with a separate blocking condition such as
auto-approved side-effecting access.
### SHIP-MCP-PERMISSION-EXPANDED
Fires when the MCP capability diff broadens scope, effect, risk tags,
controls, or accepted input schema. Narrowing and pure least-privilege
downgrades do not block by default.
### SHIP-MCP-READONLY-SERVER-ADDED
Fires as a low-severity warning when a new local documentation MCP server is
classified as read-only. It keeps benign expansion visible in review
artifacts while remaining non-blocking by default.
Risk tags are hints, not findings by themselves. Checks consume tags with confidence thresholds.
Common tags:
- `read_only`
- `write`
- `destructive`
- `external_write`
- `financial_action`
- `customer_communication`
- `sensitive_data_access`
- `infrastructure_change`
- `code_execution`
Manual `risk_overrides` in `shipgate.yaml` are treated as high-confidence evidence. Use `remove_tags` to subtract heuristic tags that are known to be wrong for a specific tool.
## Listing Checks
Use the CLI to inspect the built-in catalog:
```bash
agents-shipgate list-checks
agents-shipgate list-checks --json
agents-shipgate explain SHIP-POLICY-APPROVAL-MISSING
```
The JSON catalog includes `mvp_tier` for display and triage:
| `mvp_tier` | Meaning |
|---|---|
| `core` | Core Tool-Use Readiness MVP signal. |
| `adapter` | Framework or provider-specific readiness signal. |
| `evidence` | Validation, trace, or HITL evidence signal. |
| `lifecycle` | Baseline, diff, or action-surface evolution signal. |
| `hygiene` | Useful quality or maintenance signal, not core MVP positioning. |
`mvp_tier` never changes check execution, severity, fingerprints, baselines,
`release_decision`, or CI exit behavior.
Third-party packages can register checks through the `agents_shipgate.checks` Python entry-point group. Plugins are disabled by default because loading them imports third-party Python modules. Set `AGENTS_SHIPGATE_ENABLE_PLUGINS=1` to opt in, or pass `--no-plugins` to force them off for a scan or catalog command. Reports include `loaded_plugins` provenance for every third-party check entry point Shipgate discovered — including ones that failed validation. A plugin check should expose a callable with the same `ScanContext -> list[Finding]` shape as built-ins and attach `AGENTS_SHIPGATE_METADATA` as either a `CheckMetadata` instance or a compatible dictionary. Adapter artifacts are available through `context.framework_artifacts` or `context.artifact("openai_api", OpenAIApiArtifacts)`. Legacy `context.*_artifacts` read-only properties remain available for v0.11 plugin compatibility, raise `TypeError` on artifact type mismatch, and are scheduled for removal in v0.12.
**Plugin validation (v0.17+; six gates v0.18+).** Shipgate runs six load-time gates against every entry point — load, signature, metadata, dynamic-default-not-supported (v0.18+), ID-collision, and floor-consistency — before letting it produce findings. Metadata may use either `id` or `check_id` as the identifier key (the alias is symmetric with `Finding.check_id`); both names map to `CheckMetadata.id`. The `dynamic_default_not_supported` gate (v0.18+) rejects plugins declaring `AGENTS_SHIPGATE_METADATA.dynamic_default=True`: plugins have no path to wire into `core/dynamic_defaults.py:dynamic_check_defaults`, so a swing check would never receive a manifest-effective default and would be silently bypassable. This gate runs **before** `_coerce_metadata` so a plugin declaring `dynamic_default=True` without `floor_severity` lands here under a precise status rather than being mis-classified as `bad_floor`. Plugins that fail validation surface in `loaded_plugins[]` with a non-`valid` `validation_status` and human-readable `validation_errors`, and they do not run. At runtime, findings whose `check_id` does not match the declared plugin metadata are dropped and recorded under `loaded_plugins[].runtime_errors` — a plugin cannot smuggle findings under another check ID. Default behavior is lenient (record failures, continue scanning). Pass `--strict-plugins` to exit non-zero (code 4) when any plugin has a non-`valid` status or non-empty `runtime_errors`. See [STABILITY.md § Trust-model invariants](../STABILITY.md#trust-model-invariants) and [STABILITY.md § Severity-override floor](../STABILITY.md#severity-override-floor) (for the dynamic-default contract) for the full contracts.
## Declarative Policy Packs
v0.4 adds local YAML policy packs for organization-specific release rules.
Policy packs are static data and are safe to enable by default when declared in
`checks.policy_packs` or passed with `scan --policy-pack`. External rule IDs
must use a non-`SHIP-*` namespace such as `ORG-*`; `SHIP-*` is reserved for
built-in checks. Pack findings behave like built-ins for suppressions, severity
overrides, baselines, Markdown, JSON, and SARIF. Python plugins remain a
separate opt-in extension mechanism.
## OpenAI Agents SDK Static Extraction
SDK extraction is optional enrichment. Agents Shipgate detects Python functions decorated directly with `@function_tool`, `@function_tool(...)`, `@agents.function_tool`, `@openai_agents.function_tool`, or simple import aliases such as `from agents import function_tool as ft`, for example:
```python
@function_tool
def search_customer(customer_id: str) -> str:
...
```
When `tool_sources[].path` points at a directory, the extractor scans immediate
`*.py` files in sorted order; it does not recurse into nested packages. The
static extractor does not execute user code and intentionally does not detect
dynamic wrappers, factory-created tools, `Tool.from_fn()` style objects, runtime
imports, or dynamic tool lists. Declare those tools through MCP/OpenAPI inputs or
manifest metadata.
## Google ADK Static Extraction
Google ADK extraction is optional static enrichment. Agents Shipgate detects
Python `Agent` / `LlmAgent` definitions, literal function tools,
`FunctionTool`, `LongRunningFunctionTool`, `OpenAPIToolset`, `McpToolset`,
callbacks, plugins, sub-agents, and Agent Config YAML references where those
values are statically knowable.
The ADK extractor does not import user modules, run `adk`, connect to MCP
servers, fetch OpenAPI specs over the network, call tools, or call models.
Dynamic ADK toolsets produce source warnings and one ADK finding per unresolved
toolset unless explicit local MCP/OpenAPI/tool inventory inputs are provided.
## LangChain And CrewAI Static Extraction
LangChain/LangGraph and CrewAI extraction are optional static enrichment.
Agents Shipgate detects supported Python tool definitions, wrappers, agent
bindings, and local inventory files where those values are statically knowable.
CrewAI `BaseTool` class metadata may use literal strings or Pydantic-style
`Field(default="...")` assignments for `name` and `description`.
The extractors do not import user modules, import framework packages, run
agents, run graphs, run crews, connect to MCP servers, fetch specs over the
network, call tools, call models, or execute framework subprocesses. Dynamic
tool surfaces produce source warnings and framework findings unless explicit
local tool inventory inputs are provided. CrewAI prebuilt `crewai_tools.*Tool()`
references are emitted as low-confidence stubs and warnings; they do not by
themselves produce the dynamic-tools finding.
## n8n Static Extraction
n8n extraction reads only local workflow JSON exports/source-control files and
optional local stubs or evidence artifacts declared under `n8n:`. It does not
call a live n8n instance, run `n8n`, execute workflows, decrypt credentials,
connect to MCP endpoints, execute code nodes, or fetch network resources.
The adapter enumerates AI Agent tool sub-nodes, MCP Client Tool selections,
MCP Server Trigger exposed tools, Call n8n Workflow Tool entrypoints, Custom
Code Tool nodes, HTTP Request Tool nodes, and explicit inventories when those
surfaces are statically visible. Workflow triggers such as Webhook and Chat
Trigger are recorded as ingress evidence, not as tools.
Inactive workflows (`active: false`) are recorded as workflow evidence but are
not normalized as live tool or ingress surfaces; their workflow JSON is still
scanned for secret-like values. Workflow tags, error-workflow settings, and
node execution controls such as retry/continue-on-fail are preserved as
review metadata when present.
Credential names, workflow/node names, code bodies, request bodies, headers,
pinned data, static data, node notes, variable values, execution payloads, and
detected secrets are redacted or omitted from reports. Credential types and
credential IDs may be preserved as local release evidence.
## Conductor OSS Static Extraction
Conductor OSS extraction reads only local `schemaVersion: 2` workflow JSON
declared through `tool_sources[].type: conductor`. The MCP-core v1 adapter
normalizes literal `CALL_MCP_TOOL.method` call sites, records
`LIST_MCP_TOOLS`, `LLM_CHAT_COMPLETE`, `HUMAN`, and sub-workflow facts, and
recursively traverses switch/decision, loop, and fork containers.
The adapter never starts Conductor, executes expressions or inline code,
imports workers, connects to MCP/model endpoints, or treats runtime discovery
as a local inventory. Dynamic MCP bindings and unresolved sub-workflows produce
the Conductor finding above. HTTP/custom-worker/A2A/provider-native execution
is recorded as an unsupported capability and source warning in v1, so partial
coverage cannot silently produce `passed`. A `HUMAN` task is structural pause
evidence only; it does not prove reviewer identity or approval.
<!-- ===== source: docs/concepts.md ===== -->
# Concepts
The mental model behind Agents Shipgate, the deterministic merge gate for
AI-generated agent capability changes — a local-first, static Tool-Use
Readiness review.
For the product-level definition of a Tool-Use Readiness release gate, see
[`category.md`](category.md). For the agent-facing
walkthrough, see [`AGENTS.md`](../AGENTS.md).
## Tool-use readiness
**Tool-use readiness** is the static check that an agent's tool surface
is ready for promotion. It is *not* "did the tool call succeed" (a
runtime concern) or "did the model pick the right tool" (an eval
concern). It is the question a release reviewer answers at PR time:
> Given the tool surface declared in this PR, do we have explicit
> approval policies, scope coverage, idempotency evidence, and review
> readiness for every action — *before* promotion?
Tool-use readiness has seven dimensions. agents-shipgate produces
findings against each one.
| Dimension | What it asks | Evidence in the manifest |
|---|---|---|
| **Inventory** | What tools can the agent call? | A complete, named list — no wildcards, no "whatever this MCP server returns" |
| **Schema** | What inputs does each tool accept? | Strict JSON schema — `additionalProperties: false`, complete `required`, bounded numeric fields |
| **Auth** | What scopes does each tool need? | Declared per-tool or in `permissions.scopes` — narrower than the service account's actual scopes |
| **Approval** | Who reviews destructive actions before they fire? | `policies.require_approval_for_tools: [...]` for every write/destructive/financial action |
| **Side effects** | What does this tool change in the world? | Risk tags on the tool: `write`, `destructive`, `external_write`, `financial_action`, `customer_communication` |
| **Idempotency** | Can it be retried safely? | Idempotency key in the schema, documented retry policy, or explicit "do not retry" |
| **Blast radius** | If this tool fires unexpectedly, how bad is it? | Owner declared, prohibited actions enumerated, scope of resources bounded |
## Tool surface
The **tool surface** is the set of named, schemaed actions an agent can
invoke at runtime. It is declared via:
- Model Context Protocol (MCP) exports
- OpenAPI specs
- Framework-specific code (OpenAI Agents SDK Python, Google ADK, LangChain/LangGraph, CrewAI)
- API-specific artifacts (Anthropic Messages API tools.json, OpenAI
Agents API function schemas)
The tool surface is a **release artifact** in the same sense as a
service deployment's binary or an API contract: it's a checked-in,
diff-able statement of what the agent can do, and it should be reviewed
on every PR.
## Manifest-first
agents-shipgate is **manifest-first**: the canonical claim about an
agent's surface lives in a single `shipgate.yaml` checked into the
repo. Every tool source the manifest references is reviewed at scan
time. There is one place to look for "what does this agent ship with."
This is intentional. Implicit configurations (e.g. "use whatever the
MCP registry returns") fail the inventory dimension above. The manifest
is what makes the release gate reviewable.
## Static vs dynamic
agents-shipgate is **static**. It does not run the agent, invoke the
model, call MCP servers, or make any network calls by default. Every
finding is derived from the artifact diff alone.
Static analysis covers the Tool-Use Readiness release slice. Dynamic concerns —
behavior under unusual inputs, runtime tool routing, latency,
hallucination — belong in evals, observability, and runtime guardrails.
agents-shipgate is additive to those, not a replacement.
## Where this fits in the wider stack
| Guard | When it runs | What it catches |
|---|---|---|
| Tests | CI on every PR | Code paths in the agent's *code* |
| Evals | On a schedule or per release | Model behavior on curated inputs |
| **agents-shipgate** | CI on every PR | Tool surface, scopes, policies, prompt/surface alignment |
| Runtime guardrails / gateway | At call time | Per-call policy enforcement |
| Observability | Runtime | What actually happened in production |
Each catches something the others can't. Removing any of them is a
regression.
## Related reading
- [`category.md`](category.md) — the product-level "what is an agent release gate"
- [`checks.md`](checks.md) — every check the scanner runs
- [`manifest-v0.1.md`](manifest-v0.1.md) — full manifest schema
- [`trust-model.md`](trust-model.md) — local-only guarantees and disclosure process
- [`glossary.md`](glossary.md) — category vocabulary
<!-- ===== source: docs/autofix-policy.md ===== -->
# Autofix policy
Which Agents Shipgate findings are safe to apply automatically, which
need human review, and how the per-finding metadata in `report.json`
maps to `apply-patches --confidence` flag semantics.
> **Audience.** AI coding agents driving verify-first PR checks or first-adoption helper flows
> (see [`agent-recipes.md`](agent-recipes.md)) and CI integrators
> deciding what to gate on.
---
## Semantic evidence gaps are outside the patch system
Starting with report v0.30, effect and authority evidence gaps live under
`release_decision.evidence_coverage.evidence_gaps[]`; they are not Findings and
never carry a `Patch`. Their `next_action` may show a reviewed declaration
template, accepted values, manifest path, and rerun command, but always carries
`suggested_patch_kind: "manual"`, `auto_apply: false`, and
`requires_human_review: true`.
An agent may route `declare_action_effect`, `declare_action_authority`,
`provide_complete_inventory`, or `resolve_semantic_conflict` to a human. It
must never invent or auto-fill the declaration. These two assertions are
published as `action_effect` and `action_authority` in contract v12's
`do_not_auto_assert[]`. Baselines, suppressions, severity overrides,
`--no-heuristics`, and human acknowledgement cannot close a semantic gap.
The Finding classes below apply only after this separate evidence-coverage
boundary has been checked.
---
## The four classes
Every active finding falls into one of four classes. The class is
encoded by the `autofix_safe` and `requires_human_review` fields on
each Finding, plus the `kind` and `confidence` fields on each
attached Patch.
| Class | Finding fields | Patch shape | v0.7 examples |
|---|---|---|---|
| **Safe auto-fix** | `autofix_safe: true`, `requires_human_review: false` | All patches non-manual AND high confidence | The 3 stale-manifest removals (`SHIP-MANIFEST-STALE-{SUPPRESSION,POLICY,RISK-OVERRIDE}`) when the match is unique |
| **Medium-confidence config fix** | `autofix_safe: false`, `requires_human_review: true`, `suggested_patch_kind: append_pointer/set_pointer` | Non-manual patch but at `medium` confidence | `SHIP-AUTH-SCOPE-COVERAGE-MISSING` scope appends |
| **Manual source/policy fix** | `autofix_safe: false`, `requires_human_review: true`, `suggested_patch_kind: manual` | `ManualPatch` with curated `instructions` | All other ~30 active checks (documentation, schema bounds, owner gaps, ADK/LangChain/CrewAI metadata, …) |
| **Never auto-fix** | `autofix_safe: false`, `requires_human_review: true`, `suggested_patch_kind: manual` | `ManualPatch` with explicit anti-pattern language | `SHIP-API-TRACE-{APPROVAL,CONFIRMATION}-MISSING` (flipping the trace patches the *evidence*, not the agent's runtime gate) |
Class four is a deliberate subset of class three — the distinction is
that an agent must NEVER attempt to "auto-fix" a trace finding by
editing the trace recording, even if the user asks. The
`ManualPatch.instructions` for these checks spell out the
anti-pattern in prose so even a curious operator gets the message.
---
## Catalog vs. Finding (the dual-source contract)
Two sources describe per-check remediation policy, and they answer
different questions:
| Source | Endpoint | What it answers |
|---|---|---|
| **CheckMetadata** | `agents-shipgate list-checks --json`, `agents-shipgate explain <ID> --json`, `docs/checks.json` | What an agent should *assume* when it has only the catalog and no scan output. Conservative across the board. |
| **Finding** | `agents-shipgate-reports/report.json` (per-finding) | What this *specific* instance produced. Can be more permissive than the catalog when the generator emitted clean high-confidence patches. |
**Catalog `autofix_safe` and `requires_human_review` describe the
worst-case per-check outcome.** A check whose generator USUALLY emits
a safe non-manual patch but falls back to `ManualPatch` in edge
cases (e.g. ambiguous duplicate matches in the stale-manifest
generators) keeps the safe-closed defaults at the catalog level. The
per-Finding fields tell the truth for that instance.
`suggested_patch_kind` at the catalog level is **informational** —
it documents the kind the generator *targets* when conditions are
clean, not what the report carries. An agent that sees
`suggested_patch_kind: "remove_pointer"` in `list-checks --json`
should still consult `Finding.patches` (or the per-Finding
`suggested_patch_kind`) to know whether this particular instance
actually produced one.
When in doubt, **trust the per-Finding fields over the catalog**
for any specific finding. The catalog is for static planning
("which check IDs *might* yield safe fixes"); the report is for
acting on a specific scan.
---
## Strict derivation rule
When a scan runs with `--suggest-patches`, every active finding
gets one or more attached patches and the four per-Finding fields
are derived from those patches with this rule:
```text
autofix_safe = True iff EVERY patch is non-manual AND has confidence == "high"
```
That is: a single `ManualPatch` mixed in, or a single `medium`/`low`
confidence patch mixed in, drops the entire finding to safe-closed.
The earlier "at least one safe patch wins" rule was unsafe — it
would have marked a `[high_remove, manual]` combination
auto-fixable while a ManualPatch still required review.
`suggested_patch_kind` is the kind of the **first non-manual patch**
even when ManualPatches are also present. (If ALL patches are
manual: `"manual"`. If the patches list is empty: `"none"`.)
`requires_human_review` is always the inverse of `autofix_safe`.
`docs_url` always comes from `CheckMetadata.docs_url`. Patches
don't carry per-instance documentation URLs.
### Catalog-driven escalation override
The strict derivation rule above can be **forced safe-closed** by a
per-check policy flag. When `CheckMetadata.requires_human_review_regardless_of_patch`
is `True`, `annotate_remediation` sets `autofix_safe=False` and
`requires_human_review=True` regardless of the per-patch derivation,
so `agent_action` lands at `propose_patch_for_review` (the patch is
still surfaced) instead of `auto_apply`. This catches the
approval/confirmation/idempotency, broad-scope, prohibited-action,
runtime-trace and HITL-evidence categories listed in
[`agent-autofix-boundary.md`](agent-autofix-boundary.md) §"Check-ID
mapping"; even a third-party patch generator emitting a clean
high-confidence non-manual patch on one of those check IDs cannot
auto-apply. The catalog is the contract: those check IDs always
escalate, regardless of how the patches were derived.
### Three patch states
| `Finding.patches` | Source of derived fields |
|---|---|
| `None` (scan ran without `--suggest-patches`) | CheckMetadata, with safe-closed fallback for unknown check IDs |
| `[]` (scan ran WITH `--suggest-patches` but generator emitted nothing) | Safe-closed shape, `suggested_patch_kind: "none"`. Does NOT fall back to catalog — the report carries no patches, so reporting a catalog-level kind would mislead. |
| Non-empty | Strict derivation rule above |
### Unknown check IDs (policy packs and third-party plugins)
A finding whose `check_id` isn't in the loaded catalog (a policy
pack rule, a third-party plugin emitted while plugins are disabled)
gets the safe-closed fallback when patches are absent:
```text
autofix_safe: false
requires_human_review: true
suggested_patch_kind: "manual"
docs_url: null
```
The fallback only applies when patches are absent. A high-confidence
non-manual patch from a policy pack still derives correctly.
---
## How `apply-patches --confidence` filters
`apply-patches` reads the report, filters patches by `--confidence`
and `--kinds`, and applies the survivors. Default flags:
```bash
agents-shipgate apply-patches \
--from agents-shipgate-reports/report.json \
--confidence high \
--kinds set_pointer,append_pointer,remove_pointer \
--apply
```
| Flag | Default | What it accepts |
|---|---|---|
| `--confidence` | `high` | Minimum patch confidence. Patches below this are skipped. |
| `--kinds` | `set_pointer,append_pointer,remove_pointer` | Patch kinds to include. ManualPatch is filtered out unconditionally — even with `--kinds manual`. |
| `--apply` | (off) | Without this, dry-run only. Always preview before mutating. |
So in v0.7 with the default flags:
- The 3 stale-manifest removals (when unambiguous) auto-apply.
- `SHIP-AUTH-SCOPE-COVERAGE-MISSING` scope appends are **skipped**
(medium confidence). Pass `--confidence medium` to opt in — but
read the appended scopes before merging, since adding scopes can
encode policy choices.
- Trace approval/confirmation findings are **never** applied —
ManualPatch is filtered out.
- Everything else with a ManualPatch is **never** applied.
`apply-patches` enforces a **containment check**: every patch's
`target_file` must resolve under `report.manifest_dir`. Anything
outside aborts with exit code 5 before any SHA verification.
---
## Decision tree for agents
When walking `findings[]` from a `--suggest-patches` report:
```text
for finding in active_findings:
if finding.suggested_patch_kind == "manual":
# Manual source/policy fix or never-auto-fix.
# Read finding.patches[0].instructions and surface to user.
# Do NOT attempt to auto-edit, especially for trace findings.
surface_to_user(finding)
continue
if finding.suggested_patch_kind == "none":
# Scan ran with --suggest-patches but the generator emitted
# nothing for this finding (empty patches list — see "Three
# patch states" above). There's nothing to apply via
# apply-patches at any confidence level. Surface for human
# triage instead.
surface_to_user(finding)
continue
if finding.autofix_safe is True:
# Safe to include in the next `apply-patches --confidence high`.
plan_to_apply(finding)
continue
# Medium-confidence non-manual patch (e.g. scope coverage).
# Surface as "review and run apply-patches --confidence medium"
# but do not auto-apply on the high-confidence path.
surface_for_medium_review(finding)
```
After running `apply-patches --apply`, re-run `scan` to confirm the
fixed findings are gone. The `run_id` will only change if the
manifest or tool surface actually changed — patches are excluded
from the hash so toggling `--suggest-patches` doesn't shift it.
---
## See also
- [`agent-autofix-boundary.md`](agent-autofix-boundary.md) — the
*behavioral* counterpart to this *mechanical* page. What an agent may
assert in a PR comment or review summary, beyond which patches
`apply-patches` will run.
- [`agent-recipes.md`](agent-recipes.md) — copy-pasteable AI-agent
workflows, including the soft-stop rule for `detect`.
- [`report-reading-for-agents.md`](report-reading-for-agents.md) —
reader's primer for `report.json`.
- [`checks.md`](checks.md) — full check catalog with rationale.
- [`minimal-real-configs.md`](minimal-real-configs.md) — per-framework
minimal manifests to build from.
- [`report-schema.v1.0.json`](report-schema.v1.0.json) — current JSON
Schema for `report.json`, frozen at `1.0`.
- [`AGENTS.md`](../AGENTS.md) — top-level agent instructions, install,
trigger table.
- [`STABILITY.md`](../STABILITY.md) — what won't break across `0.x`.
Discussion
Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.
No one has posted yet. Be the first.

