From 25efa86bbe17d605f79f00649a8dc1e8dbf7af5b Mon Sep 17 00:00:00 2001 From: Pengfei Hu Date: Fri, 6 Nov 2026 11:45:14 -0800 Subject: [PATCH 1/2] docs(changelog): backfill Unreleased entries for merged PRs without one Add one line each for the OpenShell stack (#943-#948, including the native trust and evidence schemas #959 published), the application reader increments (#963, #1010, #1016-#1019), the Action's application mode (#1015), the YAML timestamp crash in host config hashing (#1100), the slow test marker (#1006) and, under Security, the AWS path-segment and URL-safe Basic redaction gaps (#1082). Lines are appended at the end of Security and of Changes only; no existing line changes. The Unreleased section, renamed 1.3.0, still passes scripts/release_notes.py: 72,069 characters against 125,000. --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c989180c..01a0a2be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - **`bootstrap` no longer runs code committed in the repository it adopts.** Each step re-entered the CLI as `python -m agents_shipgate` with the workspace as the working directory, and `python -m` puts that directory first on `sys.path`: a committed `yaml/`, `typer/`, `pydantic/` or `agents_shipgate/` package was imported and executed before `detect` read anything. The steps now run as ` -P -m agents_shipgate`, and so does every recovery command a `python -m agents_shipgate` run prints (`next_actions[].executable` is four tokens). `-P` drops only the working directory; `PYTHONPATH` and the environment still reach the child, with relative and empty `PYTHONPATH` entries anchored where `bootstrap` was started rather than resolved inside the workspace. Regression tests plant both packages, and a `yaml/` under a relative `PYTHONPATH` entry, and require bootstrap to finish without importing any of them. (#1020) - **`capability export` and the MCP `shipgate.capabilities` tool load plugins only on the opt-in.** Both passed an explicit `plugins_enabled=True` unless `--no-plugins` was given, so a read-only tool imported every installed `agents_shipgate.adapters` entry point with `AGENTS_SHIPGATE_ENABLE_PLUGINS` unset, where `scan` and `verify` imported none. They now honour the same opt-in, `--no-plugins` still wins, and the lock's `source.plugins_enabled` records the value that took effect (`false` by default, which moves `source_set_hash`). An adopter whose lock includes tools read by a third-party adapter sets the variable for `capability export`, as `scan` already required. (#1020) - `docs/trust-model.md` lists every process Shipgate starts — the Git plumbing behind `verify`, `check`, `diff` and the Claude Code hooks, workspace discovery, `bootstrap`'s self-invocation, fixture Git, the `authorization execute` HTTPS push and the native prover — instead of saying it shells out only for Git discovery. `tests/test_adapter_static_only.py` fails when a spawn site is added, removed or moved without that table changing, when a `-m agents_shipgate` argv lacks `-P`, and now also on `asyncio.create_subprocess_*`. Migration: [STABILITY.md](STABILITY.md#exec-trust-opt-in-1020). (#1020) +- **Redaction also covers an AWS secret access key inside a path or URL segment, and a `Basic` credential in URL-safe or unpadded base64.** `path/`, `s3://bucket//obj` and `https://host/?a=1` kept the key verbatim, as did such a `Basic` token with no credential label before it; a path that reads as words is still not taken for a key. (#1082) ### Changes @@ -52,6 +53,21 @@ - **The pre-commit example pin names the published release.** `.pre-commit-hooks.yaml`'s copyable example said `rev: v0.12.0`, and `docs/integrations.md` and `examples/pre-commit/README.md` said `rev: v1.0.0` (twice), after `v1.2.0` was published. All four now name `v1.2.0`, and `.pre-commit-hooks.yaml` is a registered distribution surface (`pre_commit_hook`, `executable_pin`). A repository-wide sweep in `tests/test_distribution_surface_parity.py` fails when any `rev:` that follows this repository's URL names another tag, so the release runbook's step 8 moves them with the other pins. Hook execution and default installation are unchanged. Refs #796. - **The published verifier `0.21` and agent-boundary-result `v3` schemas now declare a capability row closed.** The locked development closure moves to pydantic 2.14, which reflects an enclosing model's `extra="forbid"` in the JSON Schema of a standard-library dataclass it contains; `CapabilityDiffRow` therefore gains `additionalProperties: false` in both files. Validation is unchanged: the same model already rejected an unknown row key under pydantic 2.13, and no emitted artifact, schema version or contract version moves. A consumer that validates a row against these schemas and relied on the object being open now rejects an unknown key, as the producing model always did. - Move the published-release pins, examples and adoption prompts to `v1.2.0` (contract 41) now that it is published, re-capture the README and quickstart `diff` answers from the published `1.2.0`, and re-measure the pilot ledger's Route H dry run on it. No schema or contract change. (#778) +- **`audit --host` inventories the OpenShell policies a repository registers in `.shipgate/openshell.json`.** Each selected YAML or JSON document is read against OpenShell 0.1.2 and policy schema 1, without running OpenShell, into an `openshell_policy` grant of filesystem, Landlock, process and network facts, and a missing, malformed or unsupported one is a blocking coverage limit; host-grants inventory, baseline and drift move `0.7` → `0.8`. See [`docs/openshell-support.md`](docs/openshell-support.md). (#943) +- **A host comparison of a selected OpenShell policy says whether its declared authority widened or narrowed.** Supported filesystem, Landlock, exact L4 and REST changes are proven expansions, narrowings or equivalent rewrites (removing `enforcement: enforce` restores OpenShell's `audit` default, an expansion), and wildcard, credential, runtime-dependent and other-protocol changes stay unknown. (#944) +- **Verification binds the OpenShell inputs a comparison read.** The registration and selected policies are read from each compared tree, whatever their filenames, and the receipt binds their bytes, link targets and missing inputs, Git-ignored files included, so a later edit or link retarget makes current control stale. (#945) +- **`check`, `preflight`, `trigger` and configured `verify` route a selected OpenShell policy change.** A proven expansion raises `SHIP-HOST-BOUNDARY-PERMISSION-ALLOW-EXPANDED`, an unknown direction takes the protected-surface review and an unread input the incomplete-input route; the trigger catalog adds `TRIGGER-OPENSHELL-BOUNDARY-CHANGED`, and the pre-commit hook matches `.shipgate/openshell.json` but not an arbitrary selected policy path. (#946) +- **A version 2 `.shipgate/openshell.json` composes a policy from local global, saved and image policies and provider profiles.** Composition follows OpenShell 0.1.2's selection and append rules without contacting a gateway, every contributor is bound to the comparison and the receipt, and missing context stays incomplete; host-grants inventory, baseline and drift move `0.8` → `0.9`. (#947) +- **`verify --openshell-proof-config ` runs OpenShell's native containment prover against an operator-supplied maximum boundary; no other command, and no default run, executes it.** The operator-owned trust file ([`docs/openshell-native-trust-schema.v1.json`](docs/openshell-native-trust-schema.v1.json)) pins the prover and boundary by hash, `openshell-native.json` ([`docs/openshell-native-evidence-schema.v1.json`](docs/openshell-native-evidence-schema.v1.json)) records the result, an exceeded boundary is the critical `SHIP-VERIFY-OPENSHELL-BOUNDARY-EXCEEDED` and requested proof that is not established `SHIP-VERIFY-OPENSHELL-PROOF-UNAVAILABLE`, and `--openshell-proof-required` makes proof mandatory; containment grants no merge. (#948) +- **`diff --application` follows a tools list a builder receives from its caller.** A module-level OpenAI Agents SDK or Google ADK builder's list argument, passed positionally, by keyword or through its default, including from a module that imports the builder, is read per call site with the caller's location on the row; a variadic or decorated builder and an unresolved call stay named limits. (#963) +- **`diff --application` keeps the readable tools of an OpenAI Agents SDK list whose constructor or returned agent it cannot prove.** Each readable function tool is a `not_established` candidate carrying the original refusal and the caller's conditions, where the list used to be dropped. (#1010) +- **`diff --application` identifies an OpenAI Agents SDK agent with a computed `name` by where it is built.** A returned or class-held construction is labelled like `build@agent.py` or `Helper.self.agent@agent.py`, stable across line changes and moves between the class's methods, where it used to be a limit on its file; two constructions that would share a label are a named limit, and an identity that changes within a file makes its rows `not_established`. (#1016) +- **`diff --application` reads a builder that constructs its agent inside `if`/`else`, and a capability field of an identified agent.** Caller arguments reach either branch, with both conditions on the row, and `base.tools`, SDK `base.handoffs` or ADK `base.sub_agents` reads the original constructor list while the field provably still holds it; loops, handlers, nested builders and a replaced, mutated or escaped field stay named limits. (#1017) +- **`diff --application` reads an OpenAI Agents SDK `agent.clone(...)` of a local agent as an agent of its own.** The clone inherits `tools`, `handoffs` and `mcp_servers` unless it overrides them, a literal tool-name filter narrows them, and a row's side gains optional `agent_settings` (a literal `tool_use_behavior` or `ModelSettings(tool_choice=...)`) within `application_comparison_schema_version` `0.4`; an imported or unidentified receiver stays unread. (#1018) +- **`diff --application` reads the synchronous Google Memory Bank client.** `google.cloud.aiplatform_v1beta1`'s `MemoryBankServiceClient` reads through `retrieve_memories`, `get_memory` and `list_memories` and writes through `generate_memories`, `create_memory`, `update_memory` and `delete_memory`, naming a literal `parent` or `name` resource; other methods and a repository wrapper such as `MemoryBank.from_env()` stay limits. (#1019) +- **The Action can publish an advisory `diff --application` comparison of a pull request.** With `application: "true"` it compares the pull request's exact base and head SHAs by default, from a full-history checkout, scoped by `application_scope` or derived from the change, posts its own sticky comment, uploads `agents-shipgate-application-review` and sets `application_status`, `application_json` and `application_markdown`; a refusal fails the job, a status fails it only when `application_fail_on` names it, and verifier inputs do not apply. (#1015) +- **`diff` no longer crashes on an unquoted YAML date or timestamp in a host file.** A workflow value such as `not-before: 2023-07-06T02:54:32Z` made `diff` exit 1 with `Object of type datetime is not JSON serializable` while hashing its config; such values are now hashed as ISO-8601 text. (#1100) +- **Pull-request CI leaves out tests marked `slow`, which run nightly and in release verification.** `CONTRIBUTING.md` says when to mark one, and `pytest -m "not perf and not slow"` reproduces what a pull request runs. (#1006) ### Packaging From 187e21570afa7c18e7fa7284f65dae2d68ca94b3 Mon Sep 17 00:00:00 2001 From: Pengfei Hu Date: Fri, 6 Nov 2026 13:57:24 -0800 Subject: [PATCH 2/2] docs(changelog): group the backfilled entries and cite their tracking issues The OpenShell feature entries (#943-#948) now precede #962's follow-up fixes to them, the application reader and Action entries sit with the other `diff --application` entries, and the #1082 redaction fix sits beside the redaction entry it extends. Entries for pull requests that reference an issue cite it first, as the section does elsewhere: (#874, #963), (#1001, #1010), (#912, #1016), (#874, #1017), (#911, #1018), (#913, #1019) and (#915, #1015). --- CHANGELOG.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01a0a2be..587d05fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,10 @@ - **A secret or prompt-injection phrase in a tool description is published as a finding, with the value redacted.** `SHIP-DOC-SECRET-IN-DESCRIPTION` and `SHIP-DOC-INJECTION-RISK` matched a live `sk_live_` key or "ignore all previous instructions" and then dropped the finding, because a regex match has no policy-eligible support: `report.json` had no finding, SARIF no result, and `report.md` said only that policy applicability was inferred, while the same secret in an n8n workflow (`SHIP-N8N-SECRET-IN-WORKFLOW-PARAMETER`) was published. All three now follow one rule and are always visible in `report.json`, `report.md`, SARIF and the packet. The two description findings stay `support.policy_eligible: false` and `agent_action: informational`: they are never a blocker or a named review item and change no exit code, and the `insufficient_evidence` gap beside them is unchanged except that its `why` now leads with the check's catalog description ("Tool description contains a secret-like value."), as every finding-derived gap now does. Whether a lexical match may move a verdict is #1055. Migration: [STABILITY.md](STABILITY.md#secret-observations-1026). (#1026) - **Redaction covers four more secret shapes, two more credential labels and a finding's extra fields.** `Authorization: Basic `, a PEM private-key block, a Google `AIza` key and an AWS secret access key written without a label appeared verbatim in `report.json`, `report.md`, SARIF and the packet wherever free text carried them, such as `agent.declared_purpose` or a tool name. The shared rules now redact all four (`basic_auth`, `private_key`, `google_api_key`, `aws_secret_access_key`), an `authorization`, `proxy-authorization` or `auth` labeled value after an optional scheme word (the scheme alternation the host-grant readers use; a `Digest` value to the end of its line), and `Bearer` followed by a tab. A `Basic` token counts only when it decodes to `user:password`, so prose such as `Provides Basic ReadOnly access` is neither redacted nor reported, and a 40-character run that reads as camelCase words or path segments (`getCustomerOrderHistoryForAccountV2Async`, `Sources/PaymentKit/Handlers/RefundTools2`) is not taken for an AWS key, so a SARIF alert keeps its file. The first three also raise the secret findings above and skill `SEC-SECRET-001`; the AWS shape and the labels only redact, because without a prefix or a credential label they are too common to report. `sanitize_model` now walks the extra fields of an `extra="allow"` model, so a value a check plugin attaches to a `Finding` is redacted before any writer sees it and counted in the privacy audit. `privacy_audit.rules_version` is `0.2`. A finding whose evidence held one of these shapes publishes a new fingerprint; re-save a baseline that recorded it. (#1026) +- **Redaction also covers an AWS secret access key inside a path or URL segment, and a `Basic` credential in URL-safe or unpadded base64.** `path/`, `s3://bucket//obj` and `https://host/?a=1` kept the key verbatim, as did such a `Basic` token with no credential label before it; a path that reads as words is still not taken for a key. (#1082) - **`bootstrap` no longer runs code committed in the repository it adopts.** Each step re-entered the CLI as `python -m agents_shipgate` with the workspace as the working directory, and `python -m` puts that directory first on `sys.path`: a committed `yaml/`, `typer/`, `pydantic/` or `agents_shipgate/` package was imported and executed before `detect` read anything. The steps now run as ` -P -m agents_shipgate`, and so does every recovery command a `python -m agents_shipgate` run prints (`next_actions[].executable` is four tokens). `-P` drops only the working directory; `PYTHONPATH` and the environment still reach the child, with relative and empty `PYTHONPATH` entries anchored where `bootstrap` was started rather than resolved inside the workspace. Regression tests plant both packages, and a `yaml/` under a relative `PYTHONPATH` entry, and require bootstrap to finish without importing any of them. (#1020) - **`capability export` and the MCP `shipgate.capabilities` tool load plugins only on the opt-in.** Both passed an explicit `plugins_enabled=True` unless `--no-plugins` was given, so a read-only tool imported every installed `agents_shipgate.adapters` entry point with `AGENTS_SHIPGATE_ENABLE_PLUGINS` unset, where `scan` and `verify` imported none. They now honour the same opt-in, `--no-plugins` still wins, and the lock's `source.plugins_enabled` records the value that took effect (`false` by default, which moves `source_set_hash`). An adopter whose lock includes tools read by a third-party adapter sets the variable for `capability export`, as `scan` already required. (#1020) - `docs/trust-model.md` lists every process Shipgate starts — the Git plumbing behind `verify`, `check`, `diff` and the Claude Code hooks, workspace discovery, `bootstrap`'s self-invocation, fixture Git, the `authorization execute` HTTPS push and the native prover — instead of saying it shells out only for Git discovery. `tests/test_adapter_static_only.py` fails when a spawn site is added, removed or moved without that table changing, when a `-m agents_shipgate` argv lacks `-P`, and now also on `asyncio.create_subprocess_*`. Migration: [STABILITY.md](STABILITY.md#exec-trust-opt-in-1020). (#1020) -- **Redaction also covers an AWS secret access key inside a path or URL segment, and a `Basic` credential in URL-safe or unpadded base64.** `path/`, `s3://bucket//obj` and `https://host/?a=1` kept the key verbatim, as did such a `Basic` token with no credential label before it; a path that reads as words is still not taken for a key. (#1082) ### Changes @@ -43,6 +43,12 @@ - **Host diff reads Claude Code permission rules the way Claude Code documents them.** One rule model, shared by `diff` (text and `--json`), `verify`'s host comparison, `check` and `audit --host --drift`, replaces set-membership comparison of `allow`, `ask` and `deny` rules, scoped to one host, settings source and disposition. A documented respelling (`Bash(git add:*)` ↔ `Bash(git add *)`, `Bash(*)` ↔ `Bash`) is one `respelled` change and no widening, for a deny rule too (#918). An added allow rule that an allow rule the same source declared at the base already matches — an unchanged `Bash(git *)` over `Bash(git merge --no-ff -X ours:*)` (#941), a removed `Bash(gh pr *)` over the four subcommands that replace it (#918), a bare `Edit` over `Edit(**/.env.example)` (#969) — stays a row and is not a widening; the removed broad rule names the rules it was narrowed to. `Edit(**/.env.example)` is a scoped rule, not "every target of this kind", and no longer blocks as a wildcard (#969). A `Read(!…)`/`Edit(!…)` deny or ask rule is described as a carve-out of the earlier rules it follows in the same source, or as excepting nothing when listed first; it widens only when it now follows a rule the source already declared, and an ordering-only edit that changes that is a row (`carves_from` on the grant, host-grants `0.9` extended in place; `check` raises `SHIP-HOST-BOUNDARY-PERMISSION-DENY-REMOVED` with evidence kind `permission_deny_carved_out`) (#974). A path-scoped `Write`, `NotebookEdit`, `Glob` or `MultiEdit` rule, which Claude Code accepts and never consults, is reported as such and is never an effective grant, a removed denial or a widening; a bare `Write` rule is unchanged (#938). Anything the model cannot prove stays a widening: shell syntax, an exec wrapper, a `**` or a `*` crossing `/`, a differently cased tool name, an unanchored allow `*` (which Claude Code skips), a deny→allow move and an addition beside a lifted restriction of the same tool. The #918 reproduction now shows 3 widenings, not 9. See the `Claude Code permission-rule model` migration note in `STABILITY.md`. (#918, #941, #969, #974, #938) - **Host diff names the host whose declaration selects a `.mcp.json`.** The boundary registry finds `.mcp.json` by its file name at any depth, and that name alone made every one Claude Code's: a replay of firecrawl/firecrawl-mcp-server#468 printed `claude-code plugins/codex/firecrawl/.mcp.json`, which only a Codex plugin manifest selects (#936), and one of xai-org/plugin-marketplace#1205 printed `claude-code external_plugins/ceraph/.mcp.json` beside a Grok plugin manifest (#970). A `.mcp.json` a `.codex-plugin/plugin.json` selects (a path its `mcpServers` names, or the plugin root's `.mcp.json` when it names none, the rule `scan`'s Codex plugin reader already applied) is now published under `codex`. One no declaration this entry reads selects, in a directory holding another host's plugin manifest (Grok, Cursor, Copilot, or a Codex manifest that names other files), is published under host `unknown`: its row names no host before the path and says `host not established: …`, and `audit --host` names the manifest beside it. The root `.mcp.json`, a Claude Code plugin's and an unselected nested copy keep `claude-code`, and a file a Claude Code and a Codex plugin both select gives a row per host. The server, command name, package, launch-source note and `⚠` are kept on every host, and every `check` decision is unchanged: routing stays the registry's, and only `check`'s `affected_hosts` and `host_coverage` follow the hosts the inventory published. `diff` text and `--json`, `verify` and the PR comment, `check` and `audit --host --drift` agree. A Claude Code or Codex manifest's `mcpServers` naming only `.mcp.json` files that were read is no longer listed as an unread input. Host-grants `0.9` is extended in place (an MCP grant, its artifact and an inventory issue may carry host `unknown`); a saved baseline that recorded such a file under `claude-code` reports its servers once as removed and re-added. Nothing is installed, fetched or run, and no loading is inferred. See the `host a .mcp.json belongs to` migration note in `STABILITY.md`. (#936, #970) - Control completion now requires an explicitly evaluated subject. Unreadable MCP configuration or policy, empty/irrelevant MCP input, and detached `check` diffs deny every permission even when their diagnostic decision is `allow` or `warn`. `mcp audit` also applies `check`'s structural diff validation to MCP source records and to records whose path cannot be parsed, so a binary or header-only record (a binary rename included), a rename without hunks that git does not report as 100% similar, an empty file's record printed without hunks, two records for one path, or an unreadable path is unread input, not an evaluated source, and the stop's `summary` gives that reason. A replayable clean checkout remains an evaluated boundary result. Verifier skip completion requires an evaluated negative trigger over complete input; provisional skipped results are no longer constructed before input failures are resolved. Stored explicit permission vectors missing any of the six fields are rejected by the shared control, compact envelope and `current-control.json` pointer runtime readers, matching their existing published schemas. No frozen schema grammar changes. See the `evaluated subject` migration note in `STABILITY.md`. (#930) +- **`audit --host` inventories the OpenShell policies a repository registers in `.shipgate/openshell.json`.** Each selected YAML or JSON document is read against OpenShell 0.1.2 and policy schema 1, without running OpenShell, into an `openshell_policy` grant of filesystem, Landlock, process and network facts, and a missing, malformed or unsupported one is a blocking coverage limit; host-grants inventory, baseline and drift move `0.7` → `0.8`. See [`docs/openshell-support.md`](docs/openshell-support.md). (#943) +- **A host comparison of a selected OpenShell policy says whether its declared authority widened or narrowed.** Supported filesystem, Landlock, exact L4 and REST changes are proven expansions, narrowings or equivalent rewrites (removing `enforcement: enforce` restores OpenShell's `audit` default, an expansion), and wildcard, credential, runtime-dependent and other-protocol changes stay unknown. (#944) +- **Verification binds the OpenShell inputs a comparison read.** The registration and selected policies are read from each compared tree, whatever their filenames, and the receipt binds their bytes, link targets and missing inputs, Git-ignored files included, so a later edit or link retarget makes current control stale. (#945) +- **`check`, `preflight`, `trigger` and configured `verify` route a selected OpenShell policy change.** A proven expansion raises `SHIP-HOST-BOUNDARY-PERMISSION-ALLOW-EXPANDED`, an unknown direction takes the protected-surface review and an unread input the incomplete-input route; the trigger catalog adds `TRIGGER-OPENSHELL-BOUNDARY-CHANGED`, and the pre-commit hook matches `.shipgate/openshell.json` but not an arbitrary selected policy path. (#946) +- **A version 2 `.shipgate/openshell.json` composes a policy from local global, saved and image policies and provider profiles.** Composition follows OpenShell 0.1.2's selection and append rules without contacting a gateway, every contributor is bound to the comparison and the receipt, and missing context stays incomplete; host-grants inventory, baseline and drift move `0.8` → `0.9`. (#947) +- **`verify --openshell-proof-config ` runs OpenShell's native containment prover against an operator-supplied maximum boundary; no other command, and no default run, executes it.** The operator-owned trust file ([`docs/openshell-native-trust-schema.v1.json`](docs/openshell-native-trust-schema.v1.json)) pins the prover and boundary by hash, `openshell-native.json` ([`docs/openshell-native-evidence-schema.v1.json`](docs/openshell-native-evidence-schema.v1.json)) records the result, an exceeded boundary is the critical `SHIP-VERIFY-OPENSHELL-BOUNDARY-EXCEEDED` and requested proof that is not established `SHIP-VERIFY-OPENSHELL-PROOF-UNAVAILABLE`, and `--openshell-proof-required` makes proof mandatory; containment grants no merge. (#948) - **OpenShell link retargets retain supported authority expansions.** A selected policy link changing targets still requires reference review, and the same change now also reports every widening established from the captured policies. This covers a linked document moving from enforced request restrictions to audit-only, and an independent policy widening beside a same-byte link retarget. Static comparison and runtime-enforcement limits are unchanged. (#942) - Configured committed verification reads selected OpenShell file-link chains from the same verified Git object store as its application snapshot, including mixed-case registrations and chains of up to eight hops. Generic archives still reject links; configured snapshots still reject unrelated, escaping, cyclic, directory and over-bound links, selected links that would not open as written (through a missing or non-directory component, or with an empty one such as a trailing slash), and submodules. `verification prepare` now captures every host dependency the boundary snapshot reads before building its plan, as configured `verify` already did. Worker replay therefore rejects a selected-link retarget even when policy bytes are identical, and portable plans now bind Claude hook scripts too, so a hook script that is a symbolic link makes the worker reject the plan. Scoped host-comparison archives now resolve in-tree link chains of up to eight hops (was seven). No schema or contract version changes. (#942, #945) - **An empty preflight plan no longer grants merge or completion.** A plan that named no changed file, capability request or host permission request returned the shared `complete` state, whose permissions include `merge` and `report_complete`, with no verifier run behind it. Preflight `0.6` answers it with the new `planning_only` state, which owes no action and authorizes nothing, and every preflight route now denies every permission in both the model and the published schema; `complete` and `review_publishable` cannot appear. Docs-only plans still route to `verify`, and protected surfaces and drift still stop for a human. `0.5` stays frozen and readable as a base preflight. Runtime contract 41 → 42; `minimum_control_contract_version` stays 21. Hooks written by `install-hooks` before this change accept only preflight `0.5`, so their instruction-structure check fails closed until `install-hooks --write` is re-run. See the `planning-only preflight` migration note in `STABILITY.md`. (#610) @@ -50,22 +56,16 @@ - **`diff --application` reads tools lists built by an expression.** An OpenAI Agents SDK or Google ADK agent's `tools=` (and an SDK agent's `handoffs=`, an ADK agent's `sub_agents=`) built by a spread, `+`, a conditional, `or`, a filter or another module's list is read member by member instead of reported as one dynamic expression. A part it cannot read — a call, a builder's parameter, `self.tools` — is named with its location on that agent, which stays incomplete, and the members read are still compared. A member held only under a condition carries `bound_when`, and a change to the condition alone is a `changed` row whose direction is not established. `application_comparison_schema_version` 0.2 → 0.3. The Google ADK dynamic-tools limit now names its file and line and its agent, a starred element the ADK reader cannot read no longer leaves the agent's list read as complete, and an imported `handoffs=` list no longer reads as one handoff named after the list. The SDK recovery reason `sdk_literal_tool_list_concatenation_unsupported` is retired: that form is read (#584). For `scan`, an ADK list that came through a name or from another module is not counted as a proven surface, so no `scan` decision moves. On the 49-PR development corpus (`benchmark/application-q2/`), pull requests with any row go from 9 to 11 and those still carrying a tools-list limit from 28 to 24, most of the rest a builder's parameter (#874); MIS_TALENT#7 now reports the `Finance_Agent` row the 2026-09-30 ledger found missing. On 131 open SDK/ADK pull requests, every status is unchanged and two gain rows, all 16 checked against source. (#909) - **`diff --application` reads tools bound as objects.** An MCP server or toolset (Google ADK `McpToolset`; OpenAI Agents SDK `MCPServerStdio`, `MCPServerSse`, `MCPServerStreamableHttp` in `mcp_servers=`), an agent exposed as a tool (SDK `agent.as_tool(...)`, ADK `AgentTool(agent=...)`, and a repository helper that returns one from its single `return`), a hosted or built-in tool (SDK `WebSearchTool`, `FileSearchTool`, `CodeInterpreterTool`, `ComputerTool`; ADK `google_search`, `built_in_code_execution`, `load_memory`) and the SDK's `function_tool(f)` are bindings of their agent, where they used to read as an unresolved tool or "assigned a value, not a function definition". A row's side carries `object`: an MCP server's transport, the URL's host or the program's file name, the names of the credentials it sends and its tool filter; the agent an agent tool wraps; a built-in's name. A changed host, command, transport, credential name, filter or wrapped agent is a `changed` row. A URL's path or query, a header value and a command's arguments are digested (`endpoint_sha256`), never printed. Identity is the import, never the spelling: a same-named local function, a shadowed, reassigned or patched import, a vendored `google/adk` package and another package's `load_memory` are not built-ins. A helper whose return cannot be resolved, an `.as_tool` on a value not identified as an agent, an object changed after it is built, and an `mcp_servers=` member that is not an MCP server stay named limits; a part of an object's identity that is not read (a host from a builder's parameter) leaves the binding present but makes a change to it `not_established`. An SDK agent's `mcp_servers=`, which the comparison did not read before, is read member by member. Read for `diff --application` only: `scan` reads what it did. `application_comparison_schema_version` 0.3 → 0.4. On the 49-PR development corpus (`benchmark/application-q2/`) against `main` at `7c160cfb`, every status is unchanged, pull requests with any row go from 9 to 10, and no row is dropped: cuppibla/adk_tutorial#9's `update_plan` is now an established `added` row (its base `google_search` was unresolved), buriro-ezekia/cinescout-ai#9 gains a `not_established` row naming the base MCP toolset's host, transport and `Authorization` source, and 24 (adk_tutorial#7) and 41 (Lling0000/OpenCMO#41) limits are gone. zendah21/capstone_project#3 is `compared` with `load_memory`, `inspect_schema` and `execute_sql` added. On 131 open SDK/ADK pull requests every status is unchanged; 7 gain rows (14 new rows, 2 of them `not_established`, and 3 candidate removals now established, all checked against source), and 2 gain a named `mcp_servers` limit where a server list was not read before, as 2 development members do. What remains a limit: a helper with more than one `return`, an agent tool whose agent has no literal name, a re-exported built-in, a pydantic settings class, and #874's caller-supplied lists. (#910, #866) - **`diff --application` names what a bound tool reaches beyond HTTP.** A side's `reach` gains `effects`: what the tool's own code, and its helpers within the same bound, reaches through a recognised database (`sqlite3`, `psycopg2`, `psycopg`, `asyncpg`, `pymysql`, SQLAlchemy, `pymongo`, `redis`), process (`subprocess`, `os.system`, `os.exec*`, `asyncio.create_subprocess_*`), file (`open`, `pathlib`, `shutil`, `os.remove` and the like), cloud SDK (`boto3`, `google.cloud` storage, firestore and bigquery, Vertex AI Memory Bank) or messaging library (`smtplib`, `slack_sdk`, `twilio`), recognised by its import and method, never by a name. Each effect names its family, operation (`read`, `write`, `execute` or `unknown`), library, call and location and, where it can be read, the program, table, bucket, collection, key or path, a SQL statement's leading keyword, the database engine or cloud service, the host and credential names read off the client's construction, and the model-supplied parameters. A SQL statement, a command's arguments and a path outside the repository are digested (`value_sha256`), never printed. A literal SQL statement or file mode decides read or write; a `SELECT` that writes into a table, runs a second statement or calls a function outside SQL's own read functions (a quoted or schema-qualified name included), or a BigQuery query given a job configuration, is not taken as a read, and one that splices in a value the read does not name is a read and a limit. boto3 operations are read off their verb. A write supports `write`, running a process `code_execution` and sending a message `external_communication`, as a new `source_library_call` claim of the same effect assessment; `read` still needs everything reached to read and nothing unresolved. A method outside the tables, a statement or mode that is not a literal, a read through a client built elsewhere or with an object the read does not see into, and a process started elsewhere are named limits; a write to a file opened elsewhere (`print(..., file=LOG)`) is named where it is made, and printing to an object the read cannot name is a limit. What a helper returns is read one call past the helper bound, so the lazily built pool a query runs on is named; the call itself stays a limit. `application_comparison_schema_version` stays `0.4`, unreleased, with `reach.effects` added. On the 49-PR development corpus (`benchmark/application-q2/`) against `main` at `2c8ecf71`, every status and row is unchanged; row sides whose effect evidence is `unknown` go from 359 to 300 of 362, and sides that name at least one call or effect from 2 to 129 (on established rows, 0 to 44 of 129), in 5 pull requests. xju2/hepagent#72's `request_slurm_interactive` names `tmux` run through `subprocess.run` (`code_execution`); Tiendat2703/MIS_TALENT#7's and #8's `load_service_catalog` name a Postgres `SELECT` through `psycopg2`, and stay `write (provisional)` because other calls are unread; JinyanSu1/Agentic-AI-Scientist#2 and VidulaWickramasinghe/O.R.I.O.N#126 name process runs and file reads and writes. Five sides become `read` (structural), each checked against source. On 131 open SDK/ADK pull requests every status, row and effect evidence is unchanged and no library effect is named; two limits are gone (TencentCloud/CubeSandbox#1508's `Path.cwd()` and the `is_file()` on it). jpka/attest#3's Memory Bank is not named: its `MemoryBank` is a repository class over its own REST client, which the read does not follow into. (#913) +- **`diff --application` follows a tools list a builder receives from its caller.** A module-level OpenAI Agents SDK or Google ADK builder's list argument, passed positionally, by keyword or through its default, including from a module that imports the builder, is read per call site with the caller's location on the row; a variadic or decorated builder and an unresolved call stay named limits. (#874, #963) +- **`diff --application` keeps the readable tools of an OpenAI Agents SDK list whose constructor or returned agent it cannot prove.** Each readable function tool is a `not_established` candidate carrying the original refusal and the caller's conditions, where the list used to be dropped. (#1001, #1010) +- **`diff --application` identifies an OpenAI Agents SDK agent with a computed `name` by where it is built.** A returned or class-held construction is labelled like `build@agent.py` or `Helper.self.agent@agent.py`, stable across line changes and moves between the class's methods, where it used to be a limit on its file; two constructions that would share a label are a named limit, and an identity that changes within a file makes its rows `not_established`. (#912, #1016) +- **`diff --application` reads a builder that constructs its agent inside `if`/`else`, and a capability field of an identified agent.** Caller arguments reach either branch, with both conditions on the row, and `base.tools`, SDK `base.handoffs` or ADK `base.sub_agents` reads the original constructor list while the field provably still holds it; loops, handlers, nested builders and a replaced, mutated or escaped field stay named limits. (#874, #1017) +- **`diff --application` reads an OpenAI Agents SDK `agent.clone(...)` of a local agent as an agent of its own.** The clone inherits `tools`, `handoffs` and `mcp_servers` unless it overrides them, a literal tool-name filter narrows them, and a row's side gains optional `agent_settings` (a literal `tool_use_behavior` or `ModelSettings(tool_choice=...)`) within `application_comparison_schema_version` `0.4`; an imported or unidentified receiver stays unread. (#911, #1018) +- **`diff --application` reads the synchronous Google Memory Bank client.** `google.cloud.aiplatform_v1beta1`'s `MemoryBankServiceClient` reads through `retrieve_memories`, `get_memory` and `list_memories` and writes through `generate_memories`, `create_memory`, `update_memory` and `delete_memory`, naming a literal `parent` or `name` resource; other methods and a repository wrapper such as `MemoryBank.from_env()` stay limits. (#913, #1019) +- **The Action can publish an advisory `diff --application` comparison of a pull request.** With `application: "true"` it compares the pull request's exact base and head SHAs by default, from a full-history checkout, scoped by `application_scope` or derived from the change, posts its own sticky comment, uploads `agents-shipgate-application-review` and sets `application_status`, `application_json` and `application_markdown`; a refusal fails the job, a status fails it only when `application_fail_on` names it, and verifier inputs do not apply. (#915, #1015) - **The pre-commit example pin names the published release.** `.pre-commit-hooks.yaml`'s copyable example said `rev: v0.12.0`, and `docs/integrations.md` and `examples/pre-commit/README.md` said `rev: v1.0.0` (twice), after `v1.2.0` was published. All four now name `v1.2.0`, and `.pre-commit-hooks.yaml` is a registered distribution surface (`pre_commit_hook`, `executable_pin`). A repository-wide sweep in `tests/test_distribution_surface_parity.py` fails when any `rev:` that follows this repository's URL names another tag, so the release runbook's step 8 moves them with the other pins. Hook execution and default installation are unchanged. Refs #796. - **The published verifier `0.21` and agent-boundary-result `v3` schemas now declare a capability row closed.** The locked development closure moves to pydantic 2.14, which reflects an enclosing model's `extra="forbid"` in the JSON Schema of a standard-library dataclass it contains; `CapabilityDiffRow` therefore gains `additionalProperties: false` in both files. Validation is unchanged: the same model already rejected an unknown row key under pydantic 2.13, and no emitted artifact, schema version or contract version moves. A consumer that validates a row against these schemas and relied on the object being open now rejects an unknown key, as the producing model always did. - Move the published-release pins, examples and adoption prompts to `v1.2.0` (contract 41) now that it is published, re-capture the README and quickstart `diff` answers from the published `1.2.0`, and re-measure the pilot ledger's Route H dry run on it. No schema or contract change. (#778) -- **`audit --host` inventories the OpenShell policies a repository registers in `.shipgate/openshell.json`.** Each selected YAML or JSON document is read against OpenShell 0.1.2 and policy schema 1, without running OpenShell, into an `openshell_policy` grant of filesystem, Landlock, process and network facts, and a missing, malformed or unsupported one is a blocking coverage limit; host-grants inventory, baseline and drift move `0.7` → `0.8`. See [`docs/openshell-support.md`](docs/openshell-support.md). (#943) -- **A host comparison of a selected OpenShell policy says whether its declared authority widened or narrowed.** Supported filesystem, Landlock, exact L4 and REST changes are proven expansions, narrowings or equivalent rewrites (removing `enforcement: enforce` restores OpenShell's `audit` default, an expansion), and wildcard, credential, runtime-dependent and other-protocol changes stay unknown. (#944) -- **Verification binds the OpenShell inputs a comparison read.** The registration and selected policies are read from each compared tree, whatever their filenames, and the receipt binds their bytes, link targets and missing inputs, Git-ignored files included, so a later edit or link retarget makes current control stale. (#945) -- **`check`, `preflight`, `trigger` and configured `verify` route a selected OpenShell policy change.** A proven expansion raises `SHIP-HOST-BOUNDARY-PERMISSION-ALLOW-EXPANDED`, an unknown direction takes the protected-surface review and an unread input the incomplete-input route; the trigger catalog adds `TRIGGER-OPENSHELL-BOUNDARY-CHANGED`, and the pre-commit hook matches `.shipgate/openshell.json` but not an arbitrary selected policy path. (#946) -- **A version 2 `.shipgate/openshell.json` composes a policy from local global, saved and image policies and provider profiles.** Composition follows OpenShell 0.1.2's selection and append rules without contacting a gateway, every contributor is bound to the comparison and the receipt, and missing context stays incomplete; host-grants inventory, baseline and drift move `0.8` → `0.9`. (#947) -- **`verify --openshell-proof-config ` runs OpenShell's native containment prover against an operator-supplied maximum boundary; no other command, and no default run, executes it.** The operator-owned trust file ([`docs/openshell-native-trust-schema.v1.json`](docs/openshell-native-trust-schema.v1.json)) pins the prover and boundary by hash, `openshell-native.json` ([`docs/openshell-native-evidence-schema.v1.json`](docs/openshell-native-evidence-schema.v1.json)) records the result, an exceeded boundary is the critical `SHIP-VERIFY-OPENSHELL-BOUNDARY-EXCEEDED` and requested proof that is not established `SHIP-VERIFY-OPENSHELL-PROOF-UNAVAILABLE`, and `--openshell-proof-required` makes proof mandatory; containment grants no merge. (#948) -- **`diff --application` follows a tools list a builder receives from its caller.** A module-level OpenAI Agents SDK or Google ADK builder's list argument, passed positionally, by keyword or through its default, including from a module that imports the builder, is read per call site with the caller's location on the row; a variadic or decorated builder and an unresolved call stay named limits. (#963) -- **`diff --application` keeps the readable tools of an OpenAI Agents SDK list whose constructor or returned agent it cannot prove.** Each readable function tool is a `not_established` candidate carrying the original refusal and the caller's conditions, where the list used to be dropped. (#1010) -- **`diff --application` identifies an OpenAI Agents SDK agent with a computed `name` by where it is built.** A returned or class-held construction is labelled like `build@agent.py` or `Helper.self.agent@agent.py`, stable across line changes and moves between the class's methods, where it used to be a limit on its file; two constructions that would share a label are a named limit, and an identity that changes within a file makes its rows `not_established`. (#1016) -- **`diff --application` reads a builder that constructs its agent inside `if`/`else`, and a capability field of an identified agent.** Caller arguments reach either branch, with both conditions on the row, and `base.tools`, SDK `base.handoffs` or ADK `base.sub_agents` reads the original constructor list while the field provably still holds it; loops, handlers, nested builders and a replaced, mutated or escaped field stay named limits. (#1017) -- **`diff --application` reads an OpenAI Agents SDK `agent.clone(...)` of a local agent as an agent of its own.** The clone inherits `tools`, `handoffs` and `mcp_servers` unless it overrides them, a literal tool-name filter narrows them, and a row's side gains optional `agent_settings` (a literal `tool_use_behavior` or `ModelSettings(tool_choice=...)`) within `application_comparison_schema_version` `0.4`; an imported or unidentified receiver stays unread. (#1018) -- **`diff --application` reads the synchronous Google Memory Bank client.** `google.cloud.aiplatform_v1beta1`'s `MemoryBankServiceClient` reads through `retrieve_memories`, `get_memory` and `list_memories` and writes through `generate_memories`, `create_memory`, `update_memory` and `delete_memory`, naming a literal `parent` or `name` resource; other methods and a repository wrapper such as `MemoryBank.from_env()` stay limits. (#1019) -- **The Action can publish an advisory `diff --application` comparison of a pull request.** With `application: "true"` it compares the pull request's exact base and head SHAs by default, from a full-history checkout, scoped by `application_scope` or derived from the change, posts its own sticky comment, uploads `agents-shipgate-application-review` and sets `application_status`, `application_json` and `application_markdown`; a refusal fails the job, a status fails it only when `application_fail_on` names it, and verifier inputs do not apply. (#1015) - **`diff` no longer crashes on an unquoted YAML date or timestamp in a host file.** A workflow value such as `not-before: 2023-07-06T02:54:32Z` made `diff` exit 1 with `Object of type datetime is not JSON serializable` while hashing its config; such values are now hashed as ISO-8601 text. (#1100) - **Pull-request CI leaves out tests marked `slow`, which run nightly and in release verification.** `CONTRIBUTING.md` says when to mark one, and `pytest -m "not perf and not slow"` reproduces what a pull request runs. (#1006)