From 9881b352e2ef7bd458bb9bfdfd53db114bbe3632 Mon Sep 17 00:00:00 2001 From: iperev Date: Wed, 2 Sep 2026 22:47:39 +0200 Subject: [PATCH 1/9] feat: add bounded agent routing packets --- .github/actions/setup-verified-npm/action.yml | 6 +- .github/workflows/ci.yml | 22 +- .github/workflows/codeql.yml | 8 +- .github/workflows/osv-scanner.yml | 6 +- .github/workflows/release.yml | 38 +-- .github/workflows/scorecard-publish.yml | 4 +- .github/workflows/scorecard.yml | 6 +- .github/workflows/semantic-diff.yml | 6 +- BACKLOG.md | 2 +- README.md | 2 +- docs/proofkit-contract-map.md | 33 ++- .../specs/proofkit-agent-workflow/overview.md | 13 +- .../requirements.v1.json | 2 +- .../proofkit-spec-proof-core/overview.md | 5 + .../requirements.v1.json | 13 + .../requirements.v1.json | 4 +- go.mod | 29 +- go.sum | 44 +-- internal/app/agent_route_command.go | 6 +- internal/app/agent_route_version_edge_test.go | 137 ++++++++++ internal/app/app.go | 7 +- internal/app/app_test.go | 42 ++- internal/app/cli_abi_test.go | 134 ++++++++++ internal/app/cli_contract_test.go | 4 +- .../app/cli_output_witness_contract_test.go | 23 ++ internal/app/command_contract_generated.go | 8 +- internal/app/command_descriptors.go | 2 +- internal/app/invocation_profile_test.go | 2 +- internal/app/planning_commands.go | 15 ++ .../app/testdata/v0.6-wire-observations.json | 28 ++ internal/command/agentroute/agentroute.go | 75 +++--- .../command/agentroute/agentroute_test.go | 25 +- internal/command/agentroute/brief.go | 170 ++++++++++++ internal/command/agentroute/brief_test.go | 253 ++++++++++++++++++ internal/command/agentroute/envelope.go | 46 +++- .../stackpreset/preset_ids_generated.go | 2 +- .../kernel/releaseplatform/releaseplatform.go | 4 +- .../releaseplatform/releaseplatform_test.go | 8 +- .../testdata/unsafe-scalar-ranges.v1.json | 4 +- .../kernel/unicodepolicy/unicodepolicy.go | 4 +- .../unicodepolicy/unicodepolicy_test.go | 2 +- .../tools/commandoracle/corpus_coordinates.go | 2 +- internal/tools/commandoracle/events.go | 4 +- internal/tools/commandoracle/events_test.go | 9 + internal/tools/coveragemetrics/main.go | 22 ++ internal/tools/packagepack/main.go | 34 ++- internal/tools/packagepack/main_test.go | 51 ++++ internal/tools/packageverify/main.go | 128 +++++++-- internal/tools/packageverify/main_test.go | 14 +- internal/tools/pythonpackage/build.go | 2 - internal/tools/pythonpackage/verify.go | 66 +++++ internal/tools/releasechange/record_test.go | 29 +- .../tools/releasecloseoutinput/main_test.go | 18 +- package-lock.json | 36 +-- package.json | 8 +- proofkit/cli-contract.v2.json | 230 +++++++++++----- proofkit/requirement-bindings.json | 96 ++++++- release/change-record.v2.json | 35 ++- scripts/browser-proof-inputs.test.mjs | 2 +- scripts/stable-json.test.mjs | 2 +- scripts/stable_json_selector_test.go | 4 +- scripts/workflow_package_gate_oracle_test.go | 14 +- .../workflow_runtime_preconditions_test.go | 8 +- scripts/workflow_source_oracles_test.go | 2 +- 64 files changed, 1713 insertions(+), 347 deletions(-) create mode 100644 internal/app/agent_route_version_edge_test.go create mode 100644 internal/app/testdata/v0.6-wire-observations.json create mode 100644 internal/command/agentroute/brief.go create mode 100644 internal/command/agentroute/brief_test.go create mode 100644 internal/tools/packagepack/main_test.go diff --git a/.github/actions/setup-verified-npm/action.yml b/.github/actions/setup-verified-npm/action.yml index 580cdf3..23eaa4a 100644 --- a/.github/actions/setup-verified-npm/action.yml +++ b/.github/actions/setup-verified-npm/action.yml @@ -10,14 +10,14 @@ runs: set -euo pipefail npm_version="$(node -p "require('./package.json').packageManager.split('@').at(-1)")" - expected_version="11.18.0" + expected_version="12.0.2" if [ "$npm_version" != "$expected_version" ]; then echo "packageManager npm version drift: expected $expected_version, got $npm_version" >&2 exit 1 fi - tarball_url="https://registry.npmjs.org/npm/-/npm-11.18.0.tgz" - tarball_integrity="sha512-T67M4L5wNm0cZ7EBLErcEkY1SmzEW/WJ+SADBzsFUY1UdAPfFHXFQtZ6SEXiK0+vzXysCvAsepbMaBTwnrAD+w==" + tarball_url="https://registry.npmjs.org/npm/-/npm-12.0.2.tgz" + tarball_integrity="sha512-uIXokLlBj6FpNUTQX1PmT5pz7BlIN9QlixX+zdaSNHsd0qUXsbDLr50xzY6Sw7cJVr0uzHKDOle0swmPW/p5Qw==" tool_root="$RUNNER_TEMP/proofkit-verified-npm" archive="$tool_root/npm.tgz" bin_dir="$tool_root/bin" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb3af76..2e07815 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,15 +25,15 @@ jobs: timeout-minutes: 15 steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 2 persist-credentials: false - name: Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24.18.0 + node-version: 26.8.1 package-manager-cache: false - name: Install verified npm @@ -49,9 +49,9 @@ jobs: cache: true - name: Setup Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: '3.14.6' + python-version: '3.14.7' - name: Verify diff hygiene run: | @@ -132,14 +132,14 @@ jobs: timeout-minutes: 12 steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24.18.0 + node-version: 26.8.1 package-manager-cache: false - name: Install verified npm @@ -163,14 +163,14 @@ jobs: timeout-minutes: 20 steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24.18.0 + node-version: 26.8.1 package-manager-cache: false - name: Install verified npm diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c9362d4..5d670e7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,7 +29,7 @@ jobs: timeout-minutes: 20 steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -40,14 +40,14 @@ jobs: cache: true - name: Initialize CodeQL - uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 + uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: languages: go build-mode: autobuild - name: Analyze id: analyze - uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 + uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: output: codeql-results upload: never @@ -89,7 +89,7 @@ jobs: echo "path=$sarif_file" >> "$GITHUB_OUTPUT" - name: Upload CodeQL SARIF - uses: github/codeql-action/upload-sarif@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 + uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: sarif_file: ${{ steps.sarif.outputs.path }} category: codeql-go diff --git a/.github/workflows/osv-scanner.yml b/.github/workflows/osv-scanner.yml index bb1932c..bf7f39c 100644 --- a/.github/workflows/osv-scanner.yml +++ b/.github/workflows/osv-scanner.yml @@ -25,7 +25,7 @@ jobs: timeout-minutes: 15 steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -38,7 +38,7 @@ jobs: - name: Run OSV source scan run: | set -euo pipefail - go install github.com/google/osv-scanner/v2/cmd/osv-scanner@v2.4.0 + go install github.com/google/osv-scanner/v2/cmd/osv-scanner@v2.5.1 scanner_bin="$(go env GOPATH)/bin/osv-scanner" mkdir -p artifacts/osv set +e @@ -82,7 +82,7 @@ jobs: path: artifacts/osv - name: Upload OSV SARIF - uses: github/codeql-action/upload-sarif@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 + uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: sarif_file: artifacts/osv/osv-results.sarif category: osv-source-advisory diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 93d2a9f..2e75366 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,14 +42,14 @@ jobs: contents: read steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24.18.0 + node-version: 26.8.1 registry-url: ${{ env.REGISTRY_URL }} package-manager-cache: false @@ -214,7 +214,7 @@ jobs: contents: read steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false @@ -279,14 +279,14 @@ jobs: id-token: write steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24.18.0 + node-version: 26.8.1 registry-url: ${{ env.REGISTRY_URL }} package-manager-cache: false @@ -541,7 +541,7 @@ jobs: id-token: write steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -602,7 +602,7 @@ jobs: - name: Publish to PyPI if: steps.pypi-preflight.outputs.skip_publish != 'true' - uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1 + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 with: packages-dir: artifacts/pypi-dist print-hash: true @@ -642,7 +642,7 @@ jobs: contents: read steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -653,9 +653,9 @@ jobs: cache: true - name: Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24.18.0 + node-version: 26.8.1 package-manager-cache: false - name: Install verified npm @@ -740,7 +740,7 @@ jobs: id-token: write steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -763,17 +763,17 @@ jobs: path: artifacts/release - name: Attest release artifact provenance - uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-checksums: artifacts/release/checksums.sha256 - name: Attest release metadata provenance - uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-checksums: artifacts/release/metadata-checksums.sha256 - name: Attest release SBOM - uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1 + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-checksums: artifacts/release/sbom-subjects.sha256 sbom-path: artifacts/release/sbom.cdx.json @@ -842,14 +842,14 @@ jobs: contents: write steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24.18.0 + node-version: 26.8.1 package-manager-cache: false - name: Setup Go diff --git a/.github/workflows/scorecard-publish.yml b/.github/workflows/scorecard-publish.yml index d8228be..acd9a51 100644 --- a/.github/workflows/scorecard-publish.yml +++ b/.github/workflows/scorecard-publish.yml @@ -28,12 +28,12 @@ jobs: pull-requests: read steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Publish Scorecard results - uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 with: publish_results: true results_file: scorecard-public-results.json diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 971df68..9bb9e23 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -27,12 +27,12 @@ jobs: pull-requests: read steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Run Scorecard - uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 with: publish_results: false results_file: scorecard-results.sarif @@ -65,7 +65,7 @@ jobs: path: artifacts/scorecard - name: Upload Scorecard SARIF - uses: github/codeql-action/upload-sarif@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1 + uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: sarif_file: artifacts/scorecard/scorecard-results.sarif category: scorecard-repository-hygiene diff --git a/.github/workflows/semantic-diff.yml b/.github/workflows/semantic-diff.yml index 5e23fdd..69c53f3 100644 --- a/.github/workflows/semantic-diff.yml +++ b/.github/workflows/semantic-diff.yml @@ -30,11 +30,11 @@ jobs: SEM_LOCAL: "1" SEM_NO_TELEMETRY: "1" SEM_NO_UPDATE_CHECK: "1" - SEM_VERSION: v0.20.0 - SEM_LINUX_X86_64_SHA256: cf7249d797bed05d59e16cfa1b417e872087d558611e9f675a13810edba33dbf + SEM_VERSION: v0.24.0 + SEM_LINUX_X86_64_SHA256: 90f31b67a351aa2ec6492167d35c5636e9128bee9c528529219423c146831a4e steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false diff --git a/BACKLOG.md b/BACKLOG.md index ad9258e..b87c339 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -55,4 +55,4 @@ records, generated release manifests, or the owning docs named above. | DEFERRED | VALUE-01 | Admit exact value-evidence comparisons only after a real producer and downstream consumer establish the public record boundary; detailed candidate contract is retained in [issue #65](https://github.com/research-engineering/agentic-proofkit/issues/65). | A real execution-receipt projection, baseline producer, and downstream consumer prove an exact producer-output-to-admission round trip plus compact/full-graph inclusion or an intentional omission non-claim; otherwise no public command is added. | | BLOCKED | RELOCATION-01 | Add provenance-bounded witness relocation candidates without introducing a second binding path or trusting a caller-authored prior digest; detailed candidate contract is retained in [issue #66](https://github.com/research-engineering/agentic-proofkit/issues/66). | An owner-admitted content-addressed baseline binds witness id, prior path and digest, source revision, evidence class, authentication non-claims, and freshness non-claims; the scanner then proves the zero/one/many match partition while remaining non-current until fresh execution evidence exists. | | BLOCKED | RELEASE-01 | Prove signed protected-tag release policy as provider-side release governance, not source-only intent. | Repository tag protection/ruleset and release workflow variables require signed annotated release tags; the next public release records provider-side evidence or the row is explicitly retired as an accepted non-claim. | -| BLOCKED | RELEASE-02 | Retire the inaccurate PyPI `0.1.159` wheel compatibility and license projection without mutating immutable release history. | After a public replacement release proves macOS 12.0 wheel tags, embedded MIT license identity, npm/PyPI/GitHub byte closure, and installed-package smoke, yank PyPI `0.1.159` with an exact compatibility-and-license reason and retain provider evidence of the yank. | +| BLOCKED | RELEASE-02 | Retire the inaccurate PyPI `0.1.159` wheel compatibility and license projection without mutating immutable release history. | After a public replacement release proves that each advertised macOS wheel minimum is no lower than its embedded Mach-O minimum, embedded MIT license identity, npm/PyPI/GitHub byte closure, and installed-package smoke, yank PyPI `0.1.159` with an exact compatibility-and-license reason and retain provider evidence of the yank. | diff --git a/README.md b/README.md index 1c85fc6..e3940d6 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ CLI, not as a Python SDK. Python projects should still treat CLI/JSON records, exit codes, and package metadata as the public contract. -Supported binary targets are macOS 12 or later on arm64 or x64. +Supported binary targets are macOS 13 or later on arm64 or x64. Linux manylinux 2.17 or later is supported on arm64 or x64. Windows is unsupported. The Python runner requires Python 3.9 or later and wraps the same Go CLI; it is not a Python SDK. diff --git a/docs/proofkit-contract-map.md b/docs/proofkit-contract-map.md index e321005..8657da4 100644 --- a/docs/proofkit-contract-map.md +++ b/docs/proofkit-contract-map.md @@ -94,11 +94,16 @@ Route ambiguous modernization work through the smallest matching family: ## Agent Decision Procedure -Agents should use `agent-route` for executable routing and -`agent-route --agent-envelope` when a bounded work packet is needed. The command -returns deterministic JSON from explicit caller-owned facts; the envelope is an -opt-in derived projection over the same report. This map explains the route -families without becoming an execution, freshness, or merge decision. +Agents should use `agent-route` for the complete deterministic route report and +`agent-route --agent-envelope` for the default bounded brief packet. Explicit +`--agent-envelope-mode brief` is byte-identical to the bare envelope form; +`--agent-envelope-mode full` preserves the complete generic agent envelope for +debugging or demand-loaded detail. Every brief binds its source report ID and +stable digest in one top-level `detailAccess` record, so callers retain the +original admitted input and recompute report or full detail instead of adding a +detail reference to every item. All three forms are derived from explicit +caller-owned facts. This map explains route families without becoming an +execution, freshness, or merge decision. The exact route input vocabulary is machine-readable in `proofkit/cli-contract.v2.json` under `agent-route.inputContract`; the Go admission implementation owns nested and semantic behavior, while the shipped @@ -110,7 +115,7 @@ Formal rule: goal plus caller-owned state -> smallest matching command family -> explicit required input - -> deterministic report or bounded envelope + -> deterministic report, bounded brief, or explicit full envelope -> caller-owned execution, proof freshness, and merge decision ``` @@ -154,14 +159,18 @@ Semantic context routes are `requirement-context-compose`, 2. Start from this map when a human or agent only needs the correct command family. 3. Use `agent-route` when a coding agent needs a deterministic next-command - packet from explicit current state. Use `agent-route --agent-envelope` when - the agent needs compact context refs, blockers, command refs, and non-claims - instead of a plain route report. Treat `blocked_*` states as stop signals, - not as permission to guess missing inputs. `knownChangedPaths` are + report from explicit current state. Use `agent-route --agent-envelope` when + the agent needs exactly one next action or terminal state, bounded blockers, + caller-owned context refs, exact omission counts, and stable boundary-policy + refs instead of the full route report. Resolve further detail from the + original input only after checking the brief's source digest; request + `--agent-envelope-mode full` only when the generic envelope is actually + needed. Treat `blocked_*` states as stop signals, not as permission to guess + missing inputs. `knownChangedPaths` are diagnostic-only until the caller supplies a `changed_path_set`; browser server startup requires explicit `browserMode: "serve_local_view"`. -4. Use agent-envelope output only when a coding agent needs bounded context; - do not expand whole proof graphs into chat. +4. Prefer the brief packet for agent context; do not expand full route reports, + full envelopes, or proof graphs into chat without a specific detail need. 5. Treat generated views and rendered HTML as presentation only. They never replace the structured source record. 6. Escalate to the consuming repository's owner policy whenever Proofkit reports diff --git a/docs/specs/proofkit-agent-workflow/overview.md b/docs/specs/proofkit-agent-workflow/overview.md index 72037ff..5f9dee5 100644 --- a/docs/specs/proofkit-agent-workflow/overview.md +++ b/docs/specs/proofkit-agent-workflow/overview.md @@ -20,11 +20,12 @@ The public capability is deliberately small: 3. Existing descriptors, dispatch, command families, root-shape CLI contracts, agent envelopes, and package gates provide public-surface closure. -Neither command scans a repository. Both command cores are stateless pure -projections with no filesystem, Git, process, environment, clock, random, -network, container, or provider dependency. No setup facade, agent-route extension, -external prompt resource, persisted experiment state, or second source codec is -introduced. +Neither workflow command scans a repository. Both command cores are stateless +pure projections with no filesystem, Git, process, environment, clock, random, +network, container, or provider dependency. Neither workflow command adds a +setup facade, hidden route policy, external prompt resource, persisted +experiment state, or second source codec. Agent-route brief and full +projections remain independently owned by the spec-proof-core package. ## Requirements @@ -48,7 +49,7 @@ introduced. limits. - `REQ-PROOFKIT-WORKFLOW-006`: only existing launcher and presentation capabilities outside explicit input, with no ambient repository authority, - setup facade, hidden policy, or agent-route extension. + setup facade, hidden policy, or ownership of agent-route projections. - `REQ-PROOFKIT-WORKFLOW-007`: pure deterministic repository-neutral native-evidence guidance from one versioned typed table with five closed applicability classes, explicit absent-channel decisions, and finite diff --git a/docs/specs/proofkit-agent-workflow/requirements.v1.json b/docs/specs/proofkit-agent-workflow/requirements.v1.json index 356cef9..323c86c 100644 --- a/docs/specs/proofkit-agent-workflow/requirements.v1.json +++ b/docs/specs/proofkit-agent-workflow/requirements.v1.json @@ -73,7 +73,7 @@ { "requirementId": "REQ-PROOFKIT-WORKFLOW-006", "ownerId": "proofkit.agent-workflow", - "invariant": "After existing bounded CLI transport, immutable installed-launcher profile admission, and immutable presentation-capability capture, the semantic planner accepts no repository root, caller prompt, caller-selected bound, command, environment, receipt body, or external authority field and has no filesystem, Git, process, environment, clock, random, network, container, or provider dependency; the CLI reads only the explicit input transport, the existing launcher-profile and optional Python-executable transport capabilities, and startup TTY/NO_COLOR presence needed by requested presentation, and the capability adds no setup facade, ambient scan, hidden repository policy, consumer-specific route, or agent-route extension.", + "invariant": "After existing bounded CLI transport, immutable installed-launcher profile admission, and immutable presentation-capability capture, the semantic planner accepts no repository root, caller prompt, caller-selected bound, command, environment, receipt body, or external authority field and has no filesystem, Git, process, environment, clock, random, network, container, or provider dependency; the CLI reads only the explicit input transport, the existing launcher-profile and optional Python-executable transport capabilities, and startup TTY/NO_COLOR presence needed by requested presentation, and this two-command workflow capability adds no setup facade, ambient scan, hidden repository policy, consumer-specific route, or ownership of the independently specified agent-route projections.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], diff --git a/docs/specs/proofkit-spec-proof-core/overview.md b/docs/specs/proofkit-spec-proof-core/overview.md index 8793330..d6a9f3d 100644 --- a/docs/specs/proofkit-spec-proof-core/overview.md +++ b/docs/specs/proofkit-spec-proof-core/overview.md @@ -168,6 +168,11 @@ execution receipts, and merge policy. selected owner boundary without migrating current sources, exposing a new public CLI, proving open-world absence of undeclared equivalent parsers, or claiming that no future owner-approved grammar can be added. +- `REQ-PROOFKIT-SPEC-026`: agent-route v3 emits a deterministic 3072-byte + brief packet by default with one canonical action or typed blockers, exact + omission accounting, stable source-report identity, one top-level detail + access record, and explicit full-envelope retrieval without repeating policy + prose or promoting route guidance to authority. ## Non-Claims diff --git a/docs/specs/proofkit-spec-proof-core/requirements.v1.json b/docs/specs/proofkit-spec-proof-core/requirements.v1.json index 1f4f566..86d9bb4 100644 --- a/docs/specs/proofkit-spec-proof-core/requirements.v1.json +++ b/docs/specs/proofkit-spec-proof-core/requirements.v1.json @@ -581,6 +581,19 @@ "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, "deferral": null, "updatePolicy": {"reviewOwnerId": "proofkit.spec-proof-core", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-SPEC-026", + "ownerId": "proofkit.spec-proof-core", + "invariant": "Agent-route output contract v3 derives every report, brief packet, and full envelope from one fully admitted immutable route input and one command-owned route report: route artifact refs identify materialized caller-owned artifacts and never use the stdin transport sentinel; a bare --agent-envelope and explicit brief mode emit the same deterministic schema-v1 proofkit.agent-route.brief root while explicit full mode preserves the prior generic envelope; a routed brief selects exactly the first canonical nextCommands entry as its sole nextAction, retains its exact command and argv identity when the 3072-byte pretty stable-JSON bound permits, otherwise removes only inline argv and marks argvState detail_required, while every blocked state emits null nextAction; blockers are projected in deterministic required-input then sorted non-passed observed-report order, retain at most four typed entries, and report the exact omitted remainder; contextRefs refer to all and only caller-owned artifact operands of the selected command through resolvable source-report JSON pointers; one top-level detailAccess record binds the source report ID and stable digest, advertises report and full retrieval from the original input, and no per-item detail reference is added; omissionSummary exactly counts unselected available commands, source-omitted commands, and omitted blockers; fixed boundary-policy IDs replace repeated policy prose; the complete brief is at most 3072 bytes, and unsupported modes, invalid flag values, repeated single-value flags, or a detail mode without --agent-envelope fail before input is read. Reports and envelopes remain derived route guidance and never become requirement, proof, execution, merge, release, rollout, deployment, or readiness authority.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-SPEC-026"], + "nonClaims": ["The 3072-byte packet bound and stable source-report digest do not prove provider token consumption, semantic sufficiency, source freshness, command execution, native witness truth, or policy approval; detail retrieval requires the caller to retain the original admitted input."], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.spec-proof-core", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} } ], "nonClaims": [ diff --git a/docs/specs/proofkit-supply-chain-quality/requirements.v1.json b/docs/specs/proofkit-supply-chain-quality/requirements.v1.json index 96b1df2..24fc3fd 100644 --- a/docs/specs/proofkit-supply-chain-quality/requirements.v1.json +++ b/docs/specs/proofkit-supply-chain-quality/requirements.v1.json @@ -337,7 +337,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-026", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Every repository-owned stable JSON implementation used by a public or retained-evidence path accepts only Unicode 15.0.0 scalar strings, rejecting malformed UTF-8 byte strings and unpaired UTF-16 surrogates as applicable, emits the same deterministic semantic-preserving JSON escapes for the pinned Cc, Cf, Zl, and Zp range table, preserves ordinary decoded values, and remains byte-compatible with one versioned owner-authored literal range corpus whose Go projection is exhaustively checked against the Unicode 15.0.0 category oracle and whose JavaScript and generated TypeScript projections are exhaustively checked over the scalar domain.", + "invariant": "Every repository-owned stable JSON implementation used by a public or retained-evidence path accepts only Unicode 17.0.0 scalar strings, rejecting malformed UTF-8 byte strings and unpaired UTF-16 surrogates as applicable, emits the same deterministic semantic-preserving JSON escapes for the pinned Cc, Cf, Zl, and Zp range table, preserves ordinary decoded values, and remains byte-compatible with one versioned owner-authored literal range corpus whose Go projection is exhaustively checked against the Unicode 17.0.0 category oracle and whose JavaScript and generated TypeScript projections are exhaustively checked over the scalar domain.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], @@ -350,7 +350,7 @@ { "requirementId": "REQ-PROOFKIT-QUALITY-027", "ownerId": "proofkit.supply-chain-quality", - "invariant": "Every report-visible diagnostic and structural-text sanitizer accepts only Unicode 15.0.0 scalar strings, recognizes one explicit Unicode whitespace alphabet plus the pinned Cc, Cf, Zl, and Zp range table and contiguous or control-split secret-shaped values through one shared taxonomy, replaces any rejected caller-owned value as a whole with one fixed command-owned label without emitting its bytes, code points, prefix, suffix, or caller-controlled replacement text, and bounds admitted diagnostic projection to 512 Unicode scalars; every Go and JavaScript repository tool entrypoint routes dynamic errors through that owner, failed child-process stderr that enters this diagnostic boundary is captured under a fixed byte limit before whole-value sanitization while successful child stderr is not projected, direct child stderr wiring is rejected, and the installed-carrier smoke adapter bounds stdout, stderr, time, and cleanup; on supported Unix hosts, a successful installed-carrier result requires confirmed process-group absence after the command parent is reaped, while cleanup timeout or probe failure is an explicit failed operation rather than a successful terminal result.", + "invariant": "Every report-visible diagnostic and structural-text sanitizer accepts only Unicode 17.0.0 scalar strings, recognizes one explicit Unicode whitespace alphabet plus the pinned Cc, Cf, Zl, and Zp range table and contiguous or control-split secret-shaped values through one shared taxonomy, replaces any rejected caller-owned value as a whole with one fixed command-owned label without emitting its bytes, code points, prefix, suffix, or caller-controlled replacement text, and bounds admitted diagnostic projection to 512 Unicode scalars; every Go and JavaScript repository tool entrypoint routes dynamic errors through that owner, failed child-process stderr that enters this diagnostic boundary is captured under a fixed byte limit before whole-value sanitization while successful child stderr is not projected, direct child stderr wiring is rejected, and the installed-carrier smoke adapter bounds stdout, stderr, time, and cleanup; on supported Unix hosts, a successful installed-carrier result requires confirmed process-group absence after the command parent is reaped, while cleanup timeout or probe failure is an explicit failed operation rather than a successful terminal result.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], diff --git a/go.mod b/go.mod index b2b6c7a..5082d09 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/research-engineering/agentic-proofkit -go 1.26 +go 1.27 -toolchain go1.26.7 +toolchain go1.27.1 tool ( github.com/rhysd/actionlint/cmd/actionlint @@ -10,13 +10,11 @@ tool ( honnef.co/go/tools/cmd/staticcheck ) -require go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect - require ( - github.com/mattn/go-isatty v0.0.22 + github.com/mattn/go-isatty v0.0.24 go.yaml.in/yaml/v3 v3.0.5 - golang.org/x/mod v0.37.0 - golang.org/x/tools v0.47.0 + golang.org/x/mod v0.40.0 + golang.org/x/tools v0.49.0 ) require ( @@ -26,14 +24,15 @@ require ( github.com/fatih/color v1.19.0 // indirect github.com/google/renameio v1.0.1 // indirect github.com/mattn/go-colorable v0.1.15 // indirect - github.com/mattn/go-runewidth v0.0.24 // indirect - github.com/mattn/go-shellwords v1.0.13 // indirect + github.com/mattn/go-runewidth v0.0.29 // indirect + github.com/mattn/go-shellwords v1.0.14 // indirect github.com/rhysd/actionlint v1.7.12 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect - golang.org/x/exp/typeparams v0.0.0-20260611194520-c48552f49976 // indirect - golang.org/x/sync v0.21.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/telemetry v0.0.0-20260626140120-b709645a9e92 // indirect - golang.org/x/vuln v1.5.0 // indirect - honnef.co/go/tools v0.7.0 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect + golang.org/x/exp/typeparams v0.0.0-20260824195058-e88cd73687aa // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/telemetry v0.0.0-20260902144106-3ef544be8421 // indirect + golang.org/x/vuln v1.7.0 // indirect + honnef.co/go/tools v0.8.1 // indirect ) diff --git a/go.sum b/go.sum index 9b309f0..a5966a4 100644 --- a/go.sum +++ b/go.sum @@ -14,12 +14,12 @@ github.com/google/renameio v1.0.1 h1:Lh/jXZmvZxb0BBeSY5VKEfidcbcbenKjZFzM/q0fSeU github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= -github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= -github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= -github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXNtDcf4= -github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/mattn/go-runewidth v0.0.29 h1:3oGF3R/S2N9DQ3ptftzVIvg2eicmojCzlwBEmqEPDfQ= +github.com/mattn/go-runewidth v0.0.29/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= +github.com/mattn/go-shellwords v1.0.14 h1:yUKzIgsCnosndOASY6/enly1EAuaXeFSQ7cdyA3OuYg= +github.com/mattn/go-shellwords v1.0.14/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/rhysd/actionlint v1.7.12 h1:vQ4GeJN86C0QH+gTUQcs8McmK62OLT3kmakPMtEWYnY= github.com/rhysd/actionlint v1.7.12/go.mod h1:krOUhujIsJusovkaYzQ/VNH8PFexjNKqU0q5XI/4w+g= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= @@ -28,23 +28,23 @@ go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go= go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= -golang.org/x/exp/typeparams v0.0.0-20260611194520-c48552f49976 h1:GTD/WuaexTazIG/SxLOz4rEKZPDVilmVVC2nz4xhwfE= -golang.org/x/exp/typeparams v0.0.0-20260611194520-c48552f49976/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260626140120-b709645a9e92 h1:NqOK030/LCJf9q8SYF7NAtorXKpwpV8DmjBJcstScnU= -golang.org/x/telemetry v0.0.0-20260626140120-b709645a9e92/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/exp/typeparams v0.0.0-20260824195058-e88cd73687aa h1:0crASk1XE9p9RP0n9EbRutyWZoOkzTSuwTp3mqwppb0= +golang.org/x/exp/typeparams v0.0.0-20260824195058-e88cd73687aa/go.mod h1:qkMnvgP8C7+xkci04txL2SJFc/0PGqnkf6gs5JUC43E= +golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= +golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260902144106-3ef544be8421 h1:o5Q1WWgqIjOriF3xZyV3Y32kRr4lbgdHDbg2Wdw8/i0= +golang.org/x/telemetry v0.0.0-20260902144106-3ef544be8421/go.mod h1:/KSYFnLndIrA1A+Rs5r6vSifQhuFz75BPNN3J/hvzN0= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= -golang.org/x/vuln v1.5.0 h1:jGVVuNZ7NrBJlFB7IBkZ/R9c8gYCja+SWqrHpBCYJZA= -golang.org/x/vuln v1.5.0/go.mod h1:Ujq+7kg+6B5HsCgDFbMmP0+gAV1zGf05mkh4uF5YEXY= -honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU= -honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc= +golang.org/x/vuln v1.7.0 h1:4MQBuhmXbz2uepNJrf3v+aaZLGDqw1JluwYboegA1qg= +golang.org/x/vuln v1.7.0/go.mod h1:Xw7zvU3e1bsCYYBXu+w4wcn2Kgn27f34WBCTw8LL5Us= +honnef.co/go/tools v0.8.1 h1:+JKf3xJ1ni4CwrhVg4/pqsfPGP6vNAXcKbMXJodYx3w= +honnef.co/go/tools v0.8.1/go.mod h1:XA+OnlRA9EDh/ukGvXMNSZNKGwFQJ+5dER0ioUkOxks= diff --git a/internal/app/agent_route_command.go b/internal/app/agent_route_command.go index 802461d..0999f55 100644 --- a/internal/app/agent_route_command.go +++ b/internal/app/agent_route_command.go @@ -34,7 +34,11 @@ func runAgentRoute(args []string, stdin io.Reader, stdout io.Writer, stderr io.W } } if options.agentEnvelope { - output, exitCode, err := agentroute.BuildEnvelopeWithRenderer(input, renderer) + mode := agentroute.EnvelopeModeBrief + if options.agentEnvelopeMode != "" { + mode = agentroute.EnvelopeMode(options.agentEnvelopeMode) + } + output, exitCode, err := agentroute.BuildEnvelopeModeWithRenderer(input, renderer, mode) if err != nil { return writeJSON(agentenvelope.InvalidInput(diagnosticMessage(err)), 1, nil, stdout, stderr) } diff --git a/internal/app/agent_route_version_edge_test.go b/internal/app/agent_route_version_edge_test.go new file mode 100644 index 0000000..5682c8d --- /dev/null +++ b/internal/app/agent_route_version_edge_test.go @@ -0,0 +1,137 @@ +package app + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "path/filepath" + "slices" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" +) + +const agentRouteVersionEdgePath = "internal/app/testdata/v0.6-wire-observations.json" + +type agentRouteVersionEdge struct { + AdditionChangeIDs []string `json:"additionChangeIds"` + BreakingChangeIDs []string `json:"breakingChangeIds"` + ChangedCommandContract agentRouteChangedCommandContract `json:"changedCommandContract"` + CurrentPublicABISHA256 string `json:"currentPublicAbiSha256"` + EdgeID string `json:"edgeId"` + EvidenceClass string `json:"evidenceClass"` + MigrationSteps []string `json:"migrationSteps"` + NonClaims []string `json:"nonClaims"` + PreviousPublicABISHA256 string `json:"previousPublicAbiSha256"` + PreviousVersion string `json:"previousVersion"` + SchemaVersion int `json:"schemaVersion"` + Version string `json:"version"` +} + +type agentRouteChangedCommandContract struct { + Command string `json:"command"` + CurrentInputContractSHA256 string `json:"currentInputContractSha256"` + CurrentOutputContractSHA256 string `json:"currentOutputContractSha256"` + PreviousInputContractSHA256 string `json:"previousInputContractSha256"` + PreviousOutputContractSHA256 string `json:"previousOutputContractSha256"` +} + +func TestAgentRouteVersionEdgeClosesBriefDefaultMigration(t *testing.T) { + record := readAgentRouteVersionEdge(t) + if err := validateAgentRouteVersionEdge(record); err != nil { + t.Fatal(err) + } + + mutants := []struct { + name string + mutate func(*agentRouteVersionEdge) + }{ + {name: "current ABI", mutate: func(value *agentRouteVersionEdge) { value.CurrentPublicABISHA256 += "0" }}, + {name: "previous ABI", mutate: func(value *agentRouteVersionEdge) { value.PreviousPublicABISHA256 = value.CurrentPublicABISHA256 }}, + {name: "command", mutate: func(value *agentRouteVersionEdge) { value.ChangedCommandContract.Command = "help" }}, + {name: "current input contract", mutate: func(value *agentRouteVersionEdge) { value.ChangedCommandContract.CurrentInputContractSHA256 += "0" }}, + {name: "current output contract", mutate: func(value *agentRouteVersionEdge) { value.ChangedCommandContract.CurrentOutputContractSHA256 += "0" }}, + {name: "previous input contract", mutate: func(value *agentRouteVersionEdge) { value.ChangedCommandContract.PreviousInputContractSHA256 += "0" }}, + {name: "previous output contract", mutate: func(value *agentRouteVersionEdge) { value.ChangedCommandContract.PreviousOutputContractSHA256 += "0" }}, + {name: "breaking owner", mutate: func(value *agentRouteVersionEdge) { value.BreakingChangeIDs[0] += ".drift" }}, + {name: "addition owner", mutate: func(value *agentRouteVersionEdge) { value.AdditionChangeIDs[0] += ".drift" }}, + {name: "migration", mutate: func(value *agentRouteVersionEdge) { value.MigrationSteps[0] += " Drift." }}, + } + for _, mutant := range mutants { + t.Run(mutant.name, func(t *testing.T) { + value := cloneAgentRouteVersionEdge(record) + mutant.mutate(&value) + if err := validateAgentRouteVersionEdge(value); err == nil { + t.Fatal("version-edge mutant was admitted") + } + }) + } +} + +func readAgentRouteVersionEdge(t *testing.T) agentRouteVersionEdge { + t.Helper() + content, err := os.ReadFile(filepath.Join(repoRoot(t), agentRouteVersionEdgePath)) + if err != nil { + t.Fatal(err) + } + value, err := admission.DecodeJSON(bytes.NewReader(content), int64(len(content))) + if err != nil { + t.Fatal(err) + } + root, ok := value.(map[string]any) + if !ok { + t.Fatal("agent-route version edge must be an object") + } + assertExactObjectKeys(t, root, []string{"additionChangeIds", "breakingChangeIds", "changedCommandContract", "currentPublicAbiSha256", "edgeId", "evidenceClass", "migrationSteps", "nonClaims", "previousPublicAbiSha256", "previousVersion", "schemaVersion", "version"}, "agent-route version edge") + contract, ok := root["changedCommandContract"].(map[string]any) + if !ok { + t.Fatal("agent-route version edge changedCommandContract must be an object") + } + assertExactObjectKeys(t, contract, []string{"command", "currentInputContractSha256", "currentOutputContractSha256", "previousInputContractSha256", "previousOutputContractSha256"}, "agent-route changed command contract") + var decoded agentRouteVersionEdge + if err := json.Unmarshal(content, &decoded); err != nil { + t.Fatal(err) + } + return decoded +} + +func validateAgentRouteVersionEdge(record agentRouteVersionEdge) error { + if record.SchemaVersion != 1 || record.EdgeID != "proofkit.public-wire.0.5.1-to-0.6.0" || record.EvidenceClass != "owner_authored_frozen_version_edge_observation" { + return fmt.Errorf("agent-route version-edge identity is invalid") + } + if record.PreviousVersion != "0.5.1" || record.Version != "0.6.0" { + return fmt.Errorf("agent-route version-edge release identity is stale") + } + if record.PreviousPublicABISHA256 != "sha256:9ecd2c3d2f3f360088409f7e91cce406fc1d1d6edda1b404fce119985c4fb623" || record.CurrentPublicABISHA256 != "sha256:39f9f4314eec9e4d5baad8baf0eecbed5063d3830f074977df73ab934cafb277" || record.PreviousPublicABISHA256 == record.CurrentPublicABISHA256 { + return fmt.Errorf("agent-route version-edge ABI identity is invalid") + } + wantContract := agentRouteChangedCommandContract{ + Command: "agent-route", + PreviousInputContractSHA256: "sha256:6b5af8287f2972bbef4c68c247f43fb16d0f0d8739e5e6d3a66543af20d2644d", + CurrentInputContractSHA256: "sha256:285eaeb48845d41357cd3fc131ebb16fe11cd876bca92e9eba94dad14268acfd", + PreviousOutputContractSHA256: "sha256:44ec313a43360b6138ad6c3ae5de4abd51bbf312060880c108a6351606695915", + CurrentOutputContractSHA256: "sha256:1718051d01ebae24922baac191c9e43b281007b4ee502b9a391dfd0aa63b0039", + } + if record.ChangedCommandContract != wantContract { + return fmt.Errorf("agent-route version-edge changed command contract is not exact") + } + if !slices.Equal(record.BreakingChangeIDs, []string{"proofkit.agent-route.brief-default"}) || !slices.Equal(record.AdditionChangeIDs, []string{"proofkit.agent-route.envelope-detail-mode"}) { + return fmt.Errorf("agent-route version-edge change owners are not exact") + } + if !slices.Equal(record.MigrationSteps, []string{"Consumers that require the former generic agent-route envelope must add --agent-envelope-mode full after --agent-envelope; consumers that accept bounded route guidance may keep bare --agent-envelope."}) { + return fmt.Errorf("agent-route version-edge migration is not exact") + } + if !slices.Equal(record.NonClaims, []string{"This owner-authored version-edge observation binds reviewed public contract identities; it does not authenticate Git history, registry publication, provider ingestion, native witness truth, rollout, or production readiness."}) { + return fmt.Errorf("agent-route version-edge non-claims are not exact") + } + return nil +} + +func cloneAgentRouteVersionEdge(record agentRouteVersionEdge) agentRouteVersionEdge { + record.AdditionChangeIDs = append([]string(nil), record.AdditionChangeIDs...) + record.BreakingChangeIDs = append([]string(nil), record.BreakingChangeIDs...) + record.MigrationSteps = append([]string(nil), record.MigrationSteps...) + record.NonClaims = append([]string(nil), record.NonClaims...) + return record +} diff --git a/internal/app/app.go b/internal/app/app.go index b81a15b..0bb14e1 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -264,9 +264,10 @@ type requirementBrowserArgs struct { } type planningArgs struct { - agentEnvelope bool - inputPath string - inputPointer string + agentEnvelope bool + agentEnvelopeMode string + inputPath string + inputPointer string } type testEvidenceInventoryArgs struct { diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 3ad378e..c386ffc 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -334,18 +334,18 @@ func TestAgentRouteCLIOutputUsesVersionedRouteFamilyFields(t *testing.T) { if err := json.Unmarshal(stdout.Bytes(), &report); err != nil { t.Fatalf("stdout must be a JSON route report: %v", err) } - if report["schemaVersion"] != float64(2) || report["selectedRouteFamily"] != "requirement_source" { - t.Fatalf("agent-route output does not satisfy schema v2 route-family contract: %#v", report) + if report["schemaVersion"] != float64(3) || report["selectedRouteFamily"] != "requirement_source" { + t.Fatalf("agent-route output does not satisfy schema v3 route-family contract: %#v", report) } if _, legacy := report["selectedFamily"]; legacy { - t.Fatalf("agent-route schema v2 retained selectedFamily: %#v", report) + t.Fatalf("agent-route schema v3 retained selectedFamily: %#v", report) } guidance := report["guidanceSlice"].(map[string]any) if guidance["routeFamily"] != report["selectedRouteFamily"] { t.Fatalf("guidance routeFamily=%v selectedRouteFamily=%v", guidance["routeFamily"], report["selectedRouteFamily"]) } if _, legacy := guidance["family"]; legacy { - t.Fatalf("agent-route schema v2 retained guidanceSlice.family: %#v", guidance) + t.Fatalf("agent-route schema v3 retained guidanceSlice.family: %#v", guidance) } } @@ -360,6 +360,37 @@ func TestAgentRouteAgentEnvelopeCLIABI(t *testing.T) { if stderr.Len() != 0 { t.Fatalf("stderr must be empty: %s", stderr.String()) } + var packet map[string]any + if err := json.Unmarshal(stdout.Bytes(), &packet); err != nil { + t.Fatalf("stdout must be JSON brief packet: %v", err) + } + if packet["packetId"] != "consumer.route.requirement_source.agent-brief" || packet["packetKind"] != "proofkit.agent-route.brief" { + t.Fatalf("unexpected brief identity: %#v", packet) + } + if packet["state"] != "routed" || packet["routeFamily"] != "requirement_source" { + t.Fatalf("unexpected brief route state: %#v", packet) + } + action := packet["nextAction"].(map[string]any) + if action["commandRef"] != "requirement-source-admission" || action["argvState"] != "inline" { + t.Fatalf("unexpected brief next action: %#v", action) + } + if _, duplicated := packet["nonClaims"]; duplicated { + t.Fatalf("brief must cite policy instead of duplicating caller prose: %#v", packet) + } + detail := packet["detailAccess"].(map[string]any) + if detail["commandRef"] != "agent-route" || detail["requiresOriginalInput"] != true { + t.Fatalf("brief does not expose explicit detail access: %#v", detail) + } +} + +func TestAgentRouteFullAgentEnvelopeCLIABI(t *testing.T) { + input := `{"schemaVersion":1,"routeId":"consumer.route.requirement_source","goal":"validate_requirement_source","mode":"observe","availableInputs":[{"kind":"requirement_source","ref":"docs/specs/module/requirements.v1.json"}],"nonClaims":["Caller route fixture is not merge proof."]}` + var stdout bytes.Buffer + var stderr bytes.Buffer + status := Run(t.Context(), []string{"agent-route", "--input", "-", "--agent-envelope", "--agent-envelope-mode", "full"}, strings.NewReader(input), &stdout, &stderr) + if status != 0 || stderr.Len() != 0 { + t.Fatalf("agent-route full envelope failed status=%d stdout=%s stderr=%s", status, stdout.String(), stderr.String()) + } var envelope map[string]any if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil { t.Fatalf("stdout must be JSON envelope: %v", err) @@ -585,6 +616,9 @@ func TestCLIRejectsUnadvertisedFlagsWithoutStdout(t *testing.T) { {args: []string{"test-evidence-inventory", "--input", "-", "--projection", "unknown"}, wantStderrHas: "test-evidence-inventory --projection must be proof-binding-derived or discovery-draft"}, {args: []string{"test-evidence-inventory", "--input", "-", "--projection"}, wantStderrHas: "test-evidence-inventory --projection requires proof-binding-derived or discovery-draft"}, {args: []string{"selective-gate-obligation-decision-input", "--input", "missing.json", "--agent-envelope"}, wantStderrHas: "unsupported argument for selective-gate-obligation-decision-input: --agent-envelope"}, + {args: []string{"agent-route", "--input", "-", "--agent-envelope-mode", "full"}, wantStderrHas: "--agent-envelope-mode requires --agent-envelope"}, + {args: []string{"agent-route", "--input", "-", "--agent-envelope", "--agent-envelope-mode", "expanded"}, wantStderrHas: "--agent-envelope-mode requires one of: brief, full"}, + {args: []string{"agent-route", "--input", "-", "--agent-envelope", "--agent-envelope-mode", "brief", "--agent-envelope-mode", "full"}, wantStderrHas: "--agent-envelope-mode may be specified only once"}, } for _, item := range cases { t.Run(strings.Join(item.args, " "), func(t *testing.T) { diff --git a/internal/app/cli_abi_test.go b/internal/app/cli_abi_test.go index 055171a..43aac94 100644 --- a/internal/app/cli_abi_test.go +++ b/internal/app/cli_abi_test.go @@ -16,7 +16,9 @@ import ( "testing" "time" + "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" "github.com/research-engineering/agentic-proofkit/internal/kernel/compactproofcontract" + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" "github.com/research-engineering/agentic-proofkit/internal/testsupport/browserfixture" "github.com/research-engineering/agentic-proofkit/internal/testsupport/commandcoverage" "github.com/research-engineering/agentic-proofkit/internal/testsupport/compactfixture" @@ -2344,6 +2346,138 @@ func cliImpactDemo(demoID string, stackDiverse bool) map[string]any { } } +func TestAgentRouteEnvelopeModesUseExactRootShapes(t *testing.T) { + valid := `{"schemaVersion":1,"routeId":"proofkit.cli.agent-route","goal":"validate_requirement_source","mode":"observe","availableInputs":[{"kind":"requirement_source","ref":"docs/specs/example/requirements.v1.json"}]}` + unknown := `{"schemaVersion":1,"routeId":"proofkit.cli.agent-route.unknown","goal":"unknown","mode":"observe"}` + invalid := `{"schemaVersion":1,"routeId":"proofkit.cli.agent-route.invalid","goal":"validate_requirement_source","mode":"observe","availableInputs":[{"kind":"requirement_source","ref":"../requirements.v1.json"}]}` + cases := []struct { + name string + args []string + input string + wantStatus int + rootVariant string + rootCondition string + assert func(t *testing.T, value map[string]any) + }{ + { + name: "route report", + args: []string{"agent-route", "--input", "-"}, + input: valid, + wantStatus: 0, + rootVariant: "04-routed", + rootCondition: "without --agent-envelope; known goal", + assert: func(t *testing.T, value map[string]any) { + assertStringField(t, value, "reportKind", "proofkit.agent-route") + assertStringField(t, value, "state", "routed") + }, + }, + { + name: "unknown goal report", + args: []string{"agent-route", "--input", "-"}, + input: unknown, + wantStatus: 1, + rootVariant: "05-unknown-goal", + rootCondition: "without --agent-envelope; goal=unknown", + assert: func(t *testing.T, value map[string]any) { + assertStringField(t, value, "state", "blocked_unknown_goal") + }, + }, + { + name: "bare brief", + args: []string{"agent-route", "--input", "-", "--agent-envelope"}, + input: valid, + wantStatus: 0, + rootVariant: "01-agent-brief", + rootCondition: "--agent-envelope=present --agent-envelope-mode=absent|brief; admitted route input", + assert: func(t *testing.T, value map[string]any) { + assertStringField(t, value, "packetKind", "proofkit.agent-route.brief") + assertStringField(t, value, "state", "routed") + }, + }, + { + name: "explicit brief", + args: []string{"agent-route", "--input", "-", "--agent-envelope", "--agent-envelope-mode", "brief"}, + input: valid, + wantStatus: 0, + rootVariant: "01-agent-brief", + rootCondition: "--agent-envelope=present --agent-envelope-mode=absent|brief; admitted route input", + assert: func(t *testing.T, value map[string]any) { + assertStringField(t, value, "packetKind", "proofkit.agent-route.brief") + }, + }, + { + name: "explicit full envelope", + args: []string{"agent-route", "--input", "-", "--agent-envelope", "--agent-envelope-mode", "full"}, + input: valid, + wantStatus: 0, + rootVariant: "02-agent-envelope-full", + rootCondition: "--agent-envelope=present --agent-envelope-mode=full; admitted route input", + assert: func(t *testing.T, value map[string]any) { + assertStringField(t, value, "envelopeId", "proofkit.cli.agent-route.agent-envelope") + }, + }, + { + name: "invalid input repair envelope", + args: []string{"agent-route", "--input", "-", "--agent-envelope"}, + input: invalid, + wantStatus: 1, + rootVariant: "03-invalid-input-repair", + rootCondition: "--agent-envelope=present; route input or input-pointer admission failed", + assert: func(t *testing.T, value map[string]any) { + assertStringField(t, value, "envelopeId", "proofkit.agent-envelope.invalid-input") + }, + }, + } + outputs := map[string][]byte{} + for _, item := range cases { + t.Run(item.name, func(t *testing.T) { + var stdout bytes.Buffer + var stderr bytes.Buffer + status := Run(t.Context(), item.args, strings.NewReader(item.input), &stdout, &stderr) + if status != item.wantStatus || stderr.Len() != 0 { + t.Fatalf("status=%d want=%d stdout=%s stderr=%s", status, item.wantStatus, stdout.String(), stderr.String()) + } + var value any + if err := json.Unmarshal(stdout.Bytes(), &value); err != nil { + t.Fatalf("stdout must be one JSON value: %v\n%s", err, stdout.String()) + } + assertPublicCLIRootVariant(t, "agent-route", "output", item.rootVariant, value) + assertPublicCLIRootVariantCondition(t, "agent-route", "output", item.rootVariant, item.rootCondition) + item.assert(t, jsonObject(t, value)) + outputs[item.name] = append([]byte(nil), stdout.Bytes()...) + }) + } + if !bytes.Equal(outputs["bare brief"], outputs["explicit brief"]) { + t.Fatal("bare and explicit brief modes emitted different bytes") + } + var report map[string]any + var brief map[string]any + var full map[string]any + reportValue, err := admission.DecodeJSON(bytes.NewReader(outputs["route report"]), int64(len(outputs["route report"]))) + if err != nil { + t.Fatal(err) + } + report = jsonObject(t, reportValue) + if err := json.Unmarshal(outputs["bare brief"], &brief); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(outputs["explicit full envelope"], &full); err != nil { + t.Fatal(err) + } + detail := jsonObject(t, brief["detailAccess"]) + wantDigest, err := digest.StableJSONSHA256Ref(report) + if err != nil { + t.Fatal(err) + } + if detail["sourceReportDigest"] != wantDigest || detail["sourceReportId"] != report["reportId"] { + t.Fatalf("brief detail access does not resolve the exact route report: %#v", detail) + } + sourceReport := jsonObject(t, full["sourceReport"]) + if sourceReport["reportId"] != report["reportId"] { + t.Fatalf("full envelope does not resolve the same route report: %#v", sourceReport) + } +} + const ( cliPilotWitnessSelector = "internal/proofkit/witness_test.go::TestWitness" cliPilotFalsificationWitnessSelector = "internal/proofkit/witness_test.go::TestWitnessFalsification" diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index 2d9ffcf..e9fef60 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -23,7 +23,7 @@ import ( ) const ( - cliContractPublicABISHA256 = "9ecd2c3d2f3f360088409f7e91cce406fc1d1d6edda1b404fce119985c4fb623" + cliContractPublicABISHA256 = "39f9f4314eec9e4d5baad8baf0eecbed5063d3830f074977df73ab934cafb277" maxAggregateFileReadBytesForContractTest = 64 << 20 maxPackageManifestBytesForContractTest = 256 << 10 maxSourceFileBytesForContractTest = 8 << 20 @@ -1040,7 +1040,7 @@ func TestCLIContractModeSpecificPromises(t *testing.T) { assertScopeClass(t, commands["json-report-cli-adapter-source"], commandScopeBuiltInPackageCatalog) assertScopeClass(t, commands["typescript-public-api-surfaces"], commandScopeExplicitFileSystemScan) assertCommand(t, commands["adoption-contract-envelope"], "required", []string{"--agent-envelope", "--checked-scope", "--guidance-mode", "--input", "--materialization-manifest", "--mode", "--pilot", "--touched-rule-id"}, []string{"json"}) - assertCommand(t, commands["agent-route"], "required", []string{"--agent-envelope", "--input", "--input-pointer"}, []string{"json"}) + assertCommand(t, commands["agent-route"], "required", []string{"--agent-envelope", "--agent-envelope-mode", "--input", "--input-pointer"}, []string{"json"}) assertCommand(t, commands["capability-map-admission"], "required", []string{"--input", "--input-pointer"}, []string{"json"}) assertCommand(t, commands["conformance-profile"], "required", []string{"--format", "--input", "--input-pointer", "--list", "--profile", "--verify"}, []string{"json", "markdown"}) assertCommand(t, commands["requirement-coverage-input-compose"], "required", []string{"--input", "--input-pointer"}, []string{"json"}) diff --git a/internal/app/cli_output_witness_contract_test.go b/internal/app/cli_output_witness_contract_test.go index 3363d66..e3683b8 100644 --- a/internal/app/cli_output_witness_contract_test.go +++ b/internal/app/cli_output_witness_contract_test.go @@ -370,6 +370,14 @@ func rootDistinctOutputContractExpectations() []rootDistinctOutputContractExpect SelectorTest: "TestAdoptionContractEnvelopeCLIABI", ExecutableCommand: "go test ./internal/app -run '^TestAdoptionContractEnvelopeCLIABI$'", }, + { + Command: "agent-route", + NativeSourceForm: "nativeSources", + NativeSourcePaths: []string{"internal/app", "internal/command/agentroute"}, + SelectorPath: "internal/app/cli_abi_test.go", + SelectorTest: "TestAgentRouteEnvelopeModesUseExactRootShapes", + ExecutableCommand: "go test ./internal/app -run '^TestAgentRouteEnvelopeModesUseExactRootShapes$'", + }, { Command: "pilot-admission", NativeSourceForm: "nativeSources", @@ -404,6 +412,11 @@ func rootDistinctOutputBindingMappings() []rootDistinctOutputBindingMapping { ScenarioID: "proofkit.package-boundary.cli-output-root-witnesses", SelectorTest: "TestAdoptionContractEnvelopeCLIABI", }, + { + RequirementID: "REQ-PROOFKIT-PACKAGE-002", + ScenarioID: "proofkit.package-boundary.cli-output-root-witnesses", + SelectorTest: "TestAgentRouteEnvelopeModesUseExactRootShapes", + }, { RequirementID: "REQ-PROOFKIT-PACKAGE-002", ScenarioID: "proofkit.package-boundary.cli-output-root-witnesses", @@ -424,6 +437,11 @@ func rootDistinctOutputBindingMappings() []rootDistinctOutputBindingMapping { ScenarioID: "proofkit.supply-chain-quality.cli-abi-golden", SelectorTest: "TestAdoptionContractEnvelopeCLIABI", }, + { + RequirementID: "REQ-PROOFKIT-QUALITY-004", + ScenarioID: "proofkit.supply-chain-quality.cli-abi-golden", + SelectorTest: "TestAgentRouteEnvelopeModesUseExactRootShapes", + }, { RequirementID: "REQ-PROOFKIT-QUALITY-004", ScenarioID: "proofkit.supply-chain-quality.cli-abi-golden", @@ -444,6 +462,11 @@ func rootDistinctOutputBindingMappings() []rootDistinctOutputBindingMapping { ScenarioID: "proofkit.spec-proof-core.adoption-contract-envelope-cli-abi", SelectorTest: "TestAdoptionContractEnvelopeCLIABI", }, + { + RequirementID: "REQ-PROOFKIT-SPEC-026", + ScenarioID: "proofkit.spec-proof-core.agent-route-brief-cli-abi", + SelectorTest: "TestAgentRouteEnvelopeModesUseExactRootShapes", + }, } } diff --git a/internal/app/command_contract_generated.go b/internal/app/command_contract_generated.go index cc65674..e66519a 100644 --- a/internal/app/command_contract_generated.go +++ b/internal/app/command_contract_generated.go @@ -1,7 +1,7 @@ // Code generated by internal/tools/commandcontractgen; DO NOT EDIT. package app -const commandContractSourceSHA256 = "3e1289cbe709b62aa5cb76dd71d39ad0c40c70f6fcebd2e991544fac43a51d1c" +const commandContractSourceSHA256 = "3a21c10a7bc1dc7246c420096dd07b6d9783384e386c5c03c214902c6cf3ce5b" type generatedCommandContractMetadata struct { InputContractSHA256 string @@ -15,7 +15,7 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "adoption-contract-envelope": {InputContractSHA256: "sha256:c310214676ff4b6f536a5bc9d687f681a7e71f73d7a03ac932707d8cd3905cdf", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.adoption-contract-envelope.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3efb2c5161fee16fd8ac6a40dcb6d9c41fbc23e468f60621436ae9e8076e0950", FlagChoices: map[string][]string{}}, "adoption-doctor": {InputContractSHA256: "sha256:efa9acfe32bff07f56d9dc9902530df2979794289bc2f7f547f7a108a7dd0f35", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.adoption-doctor.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:8fdfc6608f197e633f042f20031ae1014872a90aa3daa66885ffcaddca994766", FlagChoices: map[string][]string{}}, "adoption-workflow-plan": {InputContractSHA256: "sha256:b32ae67179d7b6dcf1ea66cb6b2b2691c8367ce2e2be367619b65973166da55c", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.adoption-workflow-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:8d64cb53ebd0307e3cebc3435286a3d2a1ee8a0ad6f7514fc0fb3285db0f565b", FlagChoices: map[string][]string{}}, - "agent-route": {InputContractSHA256: "sha256:6b5af8287f2972bbef4c68c247f43fb16d0f0d8739e5e6d3a66543af20d2644d", InputSchemaSummary: []string{"availableInputs", "browserMode", "goal", "knownChangedPaths", "mode", "nonClaims", "observedReports", "openBrowser", "routeId", "schemaVersion", "root-shape-only definition proofkit.agent-route.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:44ec313a43360b6138ad6c3ae5de4abd51bbf312060880c108a6351606695915", FlagChoices: map[string][]string{}}, + "agent-route": {InputContractSHA256: "sha256:285eaeb48845d41357cd3fc131ebb16fe11cd876bca92e9eba94dad14268acfd", InputSchemaSummary: []string{"availableInputs", "browserMode", "goal", "knownChangedPaths", "mode", "nonClaims", "observedReports", "openBrowser", "routeId", "schemaVersion", "root-shape-only definition proofkit.agent-route.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:1718051d01ebae24922baac191c9e43b281007b4ee502b9a391dfd0aa63b0039", FlagChoices: map[string][]string{}}, "binding-partition": {InputContractSHA256: "sha256:366ad082045af52b2ac6604f18626d0f285b2db73b45d9a82687b8d3b0d2b3fd", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.binding-partition.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:52840879e13a00ef9a4abaad6cdb33000511674d5f9003fb56f387fdf58fadc8", FlagChoices: map[string][]string{}}, "branch-authority": {InputContractSHA256: "sha256:8a3ed74978898593fbdbf1f7fa684dae450fbd9019edcd60d07f818d63363ed4", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.branch-authority.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3c7dc74842299b92cd5baf57cc8666e9415963091359e5faf654e28da89561f1", FlagChoices: map[string][]string{}}, "capability-map-admission": {InputContractSHA256: "sha256:efe87497cf6a32cfd1f9c9c191393cac8781d1e9211914a8a7b6aaa44e7f371d", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.capability-map-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:bfb32535fd51bed168111912acb1651527897fe59a592119dc0032ce9286c9db", FlagChoices: map[string][]string{}}, @@ -40,7 +40,7 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "native-evidence-guidance": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:c1d23df574e948ea7160931f53790a6d133ae12eeceefe9e5fa15430d653ff7e", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}}, "obligation-decision": {InputContractSHA256: "sha256:1dea2ed5c5066451d6d49b815cea99df2cdae2ef05d42fed16c8aeb45eb7f445", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.obligation-decision.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:96dc074f611bcc12e511bc803c548e4df623e2de869d3add29a3ea6386e04330", FlagChoices: map[string][]string{}}, "package-runtime-dependency-admission": {InputContractSHA256: "sha256:fc85887af9b8fcd899d245f0db30b2f2f68609822fc268126bf999082bb4115f", InputSchemaSummary: []string{"schemaVersion=1", "reportId", "expectedDependencySpec", "expectedLockfileIntegrity", "expectedPackageName", "expectedPackageVersion", "admissibleLocations{}", "packageResolution{}", "nonClaims[]", "root-shape-only definition proofkit.package-runtime-dependency-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:c012032e8c8212fd50bc2e85669cc610609ca2124ebc992c9e88f44a1ad2d5fc", FlagChoices: map[string][]string{}}, - "pilot-admission": {InputContractSHA256: "sha256:a1d9116ce619f7d705349ff4ae44c0f4399a281ebaa9e7d62ea304ac57af59ba", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.pilot-admission.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:c136592e6921acba1fdf50280a9de6945983fc3cfc7454e32f33b7c894cb1774", FlagChoices: map[string][]string{}}, + "pilot-admission": {InputContractSHA256: "sha256:a1d9116ce619f7d705349ff4ae44c0f4399a281ebaa9e7d62ea304ac57af59ba", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.pilot-admission.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:8f51b18472c74894155816877ccb9f55d8ef4ce34fbe2856effc8de67685ea88", FlagChoices: map[string][]string{}}, "producer-policy-self-proof": {InputContractSHA256: "sha256:d48e18826000c8d415f3c44b6c686e1da6ed962ef7ca36c9f705de8c68d034f9", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.producer-policy-self-proof.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:e82a3989a743f8babc6069f7af82b1dd1ea62bad8dbb18d95e105b36f74e4276", FlagChoices: map[string][]string{}}, "proof-obligation-algebra": {InputContractSHA256: "sha256:4f176b6bc9bdbd0d96d65c071d66447d246665bda7a23269e7927f1d0b80b043", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.proof-obligation-algebra.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:f9ee9e56b349756c55856a2dab198e1ad85db70a468c38e3aeca73cfe2ed66f6", FlagChoices: map[string][]string{}}, "proof-receipt-admission": {InputContractSHA256: "sha256:7cb4c4fb60c8b5a37109bbd8c00d567749f7d181bbc905d8bc58155f139c44cb", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.proof-receipt-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3f802ac3fac6762ede51f0e0a151f16dc10b4a20344a3887b3ee8bae43ce94f2", FlagChoices: map[string][]string{}}, @@ -78,7 +78,7 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "selective-gate-evidence": {InputContractSHA256: "sha256:8aa178ab7ca7c475c23707bc4e15fd3f9f8d57acf6f6dcf279677e7769a45586", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.selective-gate-evidence.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:723569262bb85d9674b2a78d3bcb6e9f4cab229b71e8c784ff1b804a7fcade71", FlagChoices: map[string][]string{}}, "selective-gate-obligation-decision-input": {InputContractSHA256: "sha256:85761fcbc0ea94239d55bf379d0592a6ca814e6612a2d609a651f6cdaf8ca10a", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.selective-gate-obligation-decision-input.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:ab9dddabe975238d7019266c43350afa2df1a61d4c2eb7bc23afd520b588a2da", FlagChoices: map[string][]string{}}, "selective-gate-plan": {InputContractSHA256: "sha256:5293a5a4c7d8426cf637e6f8d252095ca0eb1714365bb89bec83307b778c678a", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.selective-gate-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:d7bffed853af5595af08b03859be01c283a3bdff1b3502d94ddc190889977647", FlagChoices: map[string][]string{}}, - "self-check": {InputContractSHA256: "sha256:a4baba76aa97d331e1a64677f56b659f30510be9a918079e745bb62c54dffe24", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.self-check.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:cda2f97a369e783782b00b466b7fd0f6d7c31d17cbe69cbc8ac25d7e73d56a8c", FlagChoices: map[string][]string{}}, + "self-check": {InputContractSHA256: "sha256:e47635cbb0b058d386fc55e7e58ef2f340bfd02bb1ec0bc44fa0e78977b41d5a", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.self-check.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:62cfd4f4663c4bb0535ad3699e3b166bb2f206651b3d733c027b71e9eb8ec374", FlagChoices: map[string][]string{}}, "spec-overview-claims": {InputContractSHA256: "sha256:2490dcd34ba7485e13f8f33e8a288a0463c4c52cc6b0d82c57777466927e49a4", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.spec-overview-claims.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:554f3a7020e9820ccb90672629fd769c52b2f298f356040aa3b0a817666cbfbf", FlagChoices: map[string][]string{}}, "spec-proof-bundle-admission": {InputContractSHA256: "sha256:6b6c2875b6476e63a1911e7d6112d9999df2babbee969f84abc4c9e4b470c933", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.spec-proof-bundle-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:e9e0eb66cebca3b99fe5036fb2e7327a9284934ed76f58818d18094d0546fc52", FlagChoices: map[string][]string{}}, "stack-preset": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:f495e9ade4e1e7af7a8f8b2059f7611cc016e6080363afccf76d8dfc2dbc6d2d", FlagChoices: map[string][]string{"--preset": []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"}}}, diff --git a/internal/app/command_descriptors.go b/internal/app/command_descriptors.go index 3ddcd4c..e673145 100644 --- a/internal/app/command_descriptors.go +++ b/internal/app/command_descriptors.go @@ -95,7 +95,7 @@ var commandDescriptors = []commandDescriptor{ command("adoption-contract-envelope", commandInputRequired, flags("--agent-envelope", "--checked-scope", "--guidance-mode", "--input", "--materialization-manifest", "--mode", "--pilot", "--touched-rule-id"), modes("json"), ownerDirs("adoptioncontract"), withRunner(commandRunnerAdoptionContractEnvelope), withAgentEnvelope(), withRequiredFlags("--mode")), command("adoption-doctor", commandInputRequired, flags("--agent-envelope", "--input", "--input-pointer"), modes("json"), ownerDirs("adoptiondoctor"), withRunner(commandRunnerAdoptionDoctor), withSemanticAppTests("TestAdoptionDoctorCLIABI"), withAgentEnvelope()), command("adoption-workflow-plan", commandInputRequired, flags("--agent-envelope", "--contract-envelope", "--input", "--input-pointer"), modes("json"), ownerDirs("adoptionworkflow"), withRunner(commandRunnerAdoptionWorkflow), withAgentEnvelope(), withContractEnvelope()), - command("agent-route", commandInputRequired, flags("--agent-envelope", "--input", "--input-pointer"), modes("json"), ownerDirs("agentroute"), withRunner(commandRunnerAgentRoute), withAgentEnvelope()), + command("agent-route", commandInputRequired, flags("--agent-envelope", "--agent-envelope-mode", "--input", "--input-pointer"), modes("json"), ownerDirs("agentroute"), withRunner(commandRunnerAgentRoute), withAgentEnvelope(), withFlagChoices("--agent-envelope-mode", "brief", "full"), withFlagPresenceRequirement("--agent-envelope-mode", "--agent-envelope"), withSingleOccurrenceFlags("--agent-envelope", "--agent-envelope-mode", "--input", "--input-pointer")), command("binding-partition", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("bindingpartition")), command("branch-authority", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("branchauthority")), command("capability-map-admission", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("capabilitymapadmission")), diff --git a/internal/app/invocation_profile_test.go b/internal/app/invocation_profile_test.go index 5986ac8..7ec0705 100644 --- a/internal/app/invocation_profile_test.go +++ b/internal/app/invocation_profile_test.go @@ -133,7 +133,7 @@ func TestGeneratedCommandInvocationProfileRouteClosure(t *testing.T) { invocationProfileAssertArgvInventory(t, renderer, agentRoute, agentRouteInventory) invocationProfileAssertArgvInventoryMutants(t, renderer, agentRoute, agentRouteInventory) - agentRouteEnvelope := invocationProfileRun(t, renderer, []string{"agent-route", "--input", "-", "--agent-envelope"}, invocationProfileEncode(t, agentRouteInput)) + agentRouteEnvelope := invocationProfileRun(t, renderer, []string{"agent-route", "--input", "-", "--agent-envelope", "--agent-envelope-mode", "full"}, invocationProfileEncode(t, agentRouteInput)) agentRouteEnvelopeInventory := map[string]int{"$.commands[*].argv": 1} invocationProfileAssertArgvInventory(t, renderer, agentRouteEnvelope, agentRouteEnvelopeInventory) invocationProfileAssertArgvInventoryMutantsAt(t, renderer, agentRouteEnvelope, agentRouteEnvelopeInventory, "commands") diff --git a/internal/app/planning_commands.go b/internal/app/planning_commands.go index 9a000aa..9224156 100644 --- a/internal/app/planning_commands.go +++ b/internal/app/planning_commands.go @@ -78,6 +78,18 @@ func parsePlanningArgs(command string, args []string) (planningArgs, error) { return planningArgs{}, fmt.Errorf("unsupported argument for %s: %s", command, args[index]) } options.agentEnvelope = true + case "--agent-envelope-mode": + if command != "agent-route" { + return planningArgs{}, fmt.Errorf("unsupported argument for %s: %s", command, args[index]) + } + if options.agentEnvelopeMode != "" || index+1 >= len(args) { + return planningArgs{}, fmt.Errorf("--agent-envelope-mode requires brief or full") + } + options.agentEnvelopeMode = args[index+1] + if options.agentEnvelopeMode != "brief" && options.agentEnvelopeMode != "full" { + return planningArgs{}, fmt.Errorf("--agent-envelope-mode requires brief or full") + } + index++ default: return planningArgs{}, fmt.Errorf("unsupported argument for %s: %s", command, args[index]) } @@ -85,5 +97,8 @@ func parsePlanningArgs(command string, args []string) (planningArgs, error) { if options.inputPath == "" { return planningArgs{}, fmt.Errorf("%s requires --input ", command) } + if options.agentEnvelopeMode != "" && !options.agentEnvelope { + return planningArgs{}, fmt.Errorf("--agent-envelope-mode requires --agent-envelope") + } return options, nil } diff --git a/internal/app/testdata/v0.6-wire-observations.json b/internal/app/testdata/v0.6-wire-observations.json new file mode 100644 index 0000000..bbf438b --- /dev/null +++ b/internal/app/testdata/v0.6-wire-observations.json @@ -0,0 +1,28 @@ +{ + "schemaVersion": 1, + "edgeId": "proofkit.public-wire.0.5.1-to-0.6.0", + "previousVersion": "0.5.1", + "version": "0.6.0", + "evidenceClass": "owner_authored_frozen_version_edge_observation", + "previousPublicAbiSha256": "sha256:9ecd2c3d2f3f360088409f7e91cce406fc1d1d6edda1b404fce119985c4fb623", + "currentPublicAbiSha256": "sha256:39f9f4314eec9e4d5baad8baf0eecbed5063d3830f074977df73ab934cafb277", + "changedCommandContract": { + "command": "agent-route", + "previousInputContractSha256": "sha256:6b5af8287f2972bbef4c68c247f43fb16d0f0d8739e5e6d3a66543af20d2644d", + "currentInputContractSha256": "sha256:285eaeb48845d41357cd3fc131ebb16fe11cd876bca92e9eba94dad14268acfd", + "previousOutputContractSha256": "sha256:44ec313a43360b6138ad6c3ae5de4abd51bbf312060880c108a6351606695915", + "currentOutputContractSha256": "sha256:1718051d01ebae24922baac191c9e43b281007b4ee502b9a391dfd0aa63b0039" + }, + "breakingChangeIds": [ + "proofkit.agent-route.brief-default" + ], + "additionChangeIds": [ + "proofkit.agent-route.envelope-detail-mode" + ], + "migrationSteps": [ + "Consumers that require the former generic agent-route envelope must add --agent-envelope-mode full after --agent-envelope; consumers that accept bounded route guidance may keep bare --agent-envelope." + ], + "nonClaims": [ + "This owner-authored version-edge observation binds reviewed public contract identities; it does not authenticate Git history, registry publication, provider ingestion, native witness truth, rollout, or production readiness." + ] +} diff --git a/internal/command/agentroute/agentroute.go b/internal/command/agentroute/agentroute.go index cd1a34a..ab42141 100644 --- a/internal/command/agentroute/agentroute.go +++ b/internal/command/agentroute/agentroute.go @@ -463,7 +463,7 @@ func InputContract() map[string]any { "default": []any{}, "item": map[string]any{ "kind": map[string]any{"enum": sortedKeys(inputKindValues)}, - "ref": map[string]any{"format": "safe repo-relative caller-owned file, report ref, or scanner root ref; scanner root refs may be ."}, + "ref": map[string]any{"format": "safe repo-relative materialized caller-owned file, report ref, or scanner root ref; stdin sentinel - is forbidden and scanner root refs may be ."}, }, "uniqueBy": "kind", }, @@ -497,42 +497,50 @@ func InputContract() map[string]any { func OutputContract() map[string]any { return map[string]any{ - "contractId": "proofkit.agent-route.output.v2", - "schemaVersion": 2, - "authority": "deterministic route report derived from admitted agent-route input", - "requiredFields": []any{ - "guidanceSlice", - "reportId", - "reportKind", - "schemaVersion", - "selectedRouteFamily", - "state", - }, - "fields": map[string]any{ - "schemaVersion": map[string]any{"value": 2}, - "selectedRouteFamily": map[string]any{ - "enum": routeFamilyContractValues(), + "contractId": "proofkit.agent-route.output.v3", + "schemaVersion": 3, + "authority": "deterministic route report, brief packet, full envelope, or invalid-input repair packet selected from the admitted invocation", + "briefPacketContract": map[string]any{ + "contractId": "proofkit.agent-route.brief.v1", + "schemaVersion": 1, + "maxPrettyJSONBytes": maxAgentBriefBytes, + "maxBlockers": maxBriefBlockerItems, + "requiredFields": []any{ + "blockers", + "boundaryPolicyRefs", + "contextRefs", + "detailAccess", + "nextAction", + "omissionSummary", + "packetId", + "packetKind", + "routeFamily", + "schemaVersion", + "state", }, - "guidanceSlice": map[string]any{ - "requiredFields": []any{"routeFamily"}, - "routeFamilyRule": "must equal selectedRouteFamily", + "fieldRules": map[string]any{ + "blockers": "required-input blockers followed by sorted non-passed observed-report blockers; retain at most four and count the exact remainder", + "boundaryPolicyRefs": "exactly NC-PROOFKIT-SPEC-005 and NC-PROOFKIT-SPEC-026; policy prose is not duplicated", + "contextRefs": "only caller-owned artifact operands of the selected next command, each addressed by an RFC 6901 pointer into the source report", + "detailAccess": "binds source report ID and stable digest and advertises report and full retrieval from the original admitted input", + "nextAction": "null for blocked states; otherwise the first canonical nextCommands entry with exact command identity and inline argv unless the byte bound requires argvState=detail_required", + "omissionSummary": "exact counts for unselected available commands, source-omitted commands, and blockers omitted by the packet bound", + "packetKind": "proofkit.agent-route.brief", + "schemaVersion": "1", + }, + "nonClaims": []any{ + "The brief packet is derived route guidance, not requirement, proof, execution, merge, release, rollout, deployment, or readiness authority.", + "The packet byte bound does not claim tokenizer-specific token consumption or semantic sufficiency.", }, }, - "changesFromV1": []any{ - "selectedFamily is replaced by selectedRouteFamily", - "guidanceSlice.family is replaced by guidanceSlice.routeFamily", + "changesFromV2": []any{ + "bare --agent-envelope emits proofkit.agent-route.brief", + "--agent-envelope-mode full preserves the prior generic envelope projection", + "route report schemaVersion advances to 3 without changing its semantic fields", }, } } -func routeFamilyContractValues() []any { - values := map[string]struct{}{string(routeFamilyUnknown): {}} - for _, spec := range routeSpecs { - values[string(spec.RouteFamily)] = struct{}{} - } - return sortedKeys(values) -} - func admitInput(raw any) (routeInput, error) { record, ok := raw.(map[string]any) if !ok { @@ -655,6 +663,9 @@ func admitAvailableInputs(raw any) (map[string]string, error) { } func admitAvailableInputRef(kind string, value string, context string) (string, error) { + if value == "-" { + return "", fmt.Errorf("%s must identify a materialized caller-owned artifact, not the stdin transport sentinel", context) + } if (kind == "typescript_public_api_repo_root" || kind == "requirement_context_repo_root") && value == "." { return value, nil } @@ -839,7 +850,7 @@ func buildUnknownGoal(input routeInput) map[string]any { "reportId": input.RouteID, "reportKind": "proofkit.agent-route", "requiredInputs": []any{}, - "schemaVersion": 2, + "schemaVersion": 3, "selectedRouteFamily": string(routeFamilyUnknown), "state": "blocked_unknown_goal", "stopConditions": []any{"Stop before running a Proofkit command until the caller supplies a known goal."}, @@ -873,7 +884,7 @@ func buildReport(input routeInput, spec routeSpec, state string, missing []map[s "reportId": input.RouteID, "reportKind": "proofkit.agent-route", "requiredInputs": requiredInputReports(missing), - "schemaVersion": 2, + "schemaVersion": 3, "selectedRouteFamily": string(spec.RouteFamily), "state": state, "stopConditions": toAnySlice(spec.StopConditions), diff --git a/internal/command/agentroute/agentroute_test.go b/internal/command/agentroute/agentroute_test.go index ec94e81..496b19f 100644 --- a/internal/command/agentroute/agentroute_test.go +++ b/internal/command/agentroute/agentroute_test.go @@ -36,8 +36,8 @@ func TestBuildRoutesRequirementSourceAndBlocksUnknownGoal(t *testing.T) { if state := report["state"]; state != "routed" { t.Fatalf("state = %v, want routed", state) } - if schemaVersion := report["schemaVersion"]; schemaVersion != 2 { - t.Fatalf("schemaVersion = %v, want 2 for selectedRouteFamily output", schemaVersion) + if schemaVersion := report["schemaVersion"]; schemaVersion != 3 { + t.Fatalf("schemaVersion = %v, want 3 for selectedRouteFamily output", schemaVersion) } if family := report["selectedRouteFamily"]; family != "requirement_source" { t.Fatalf("selectedRouteFamily = %v, want requirement_source", family) @@ -77,14 +77,31 @@ func TestBuildRoutesRequirementSourceAndBlocksUnknownGoal(t *testing.T) { if state := blocked["state"]; state != "blocked_unknown_goal" { t.Fatalf("unknown state = %v, want blocked_unknown_goal", state) } - if schemaVersion := blocked["schemaVersion"]; schemaVersion != 2 { - t.Fatalf("unknown schemaVersion = %v, want 2", schemaVersion) + if schemaVersion := blocked["schemaVersion"]; schemaVersion != 3 { + t.Fatalf("unknown schemaVersion = %v, want 3", schemaVersion) } if commands := blocked["nextCommands"].([]any); len(commands) != 0 { t.Fatalf("unknown nextCommands length = %d, want 0", len(commands)) } } +func TestBuildRejectsStdinTransportSentinelAsArtifactReference(t *testing.T) { + t.Parallel() + + _, exitCode, err := Build(map[string]any{ + "schemaVersion": jsonNumber("1"), + "routeId": "consumer.route.stdin-sentinel", + "goal": "validate_requirement_source", + "mode": "observe", + "availableInputs": []any{ + map[string]any{"kind": "requirement_source", "ref": "-"}, + }, + }) + if err == nil || exitCode != 1 || !strings.Contains(err.Error(), "stdin transport sentinel") { + t.Fatalf("stdin sentinel exit=%d error=%v", exitCode, err) + } +} + func TestBuildRejectsUnknownAdoptionMode(t *testing.T) { t.Parallel() diff --git a/internal/command/agentroute/brief.go b/internal/command/agentroute/brief.go new file mode 100644 index 0000000..5323e98 --- /dev/null +++ b/internal/command/agentroute/brief.go @@ -0,0 +1,170 @@ +package agentroute + +import ( + "fmt" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" + "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" +) + +type EnvelopeMode string + +const ( + EnvelopeModeBrief EnvelopeMode = "brief" + EnvelopeModeFull EnvelopeMode = "full" + + maxAgentBriefBytes = 3072 + maxBriefBlockerItems = 4 +) + +func AgentBrief(report map[string]any) (map[string]any, error) { + reportID := stringFromMap(report, "reportId") + if reportID == "" { + reportID = "proofkit.agent-route.unknown" + } + reportDigest, err := digest.StableJSONSHA256Ref(report) + if err != nil { + return nil, fmt.Errorf("digest agent-route report: %w", err) + } + + nextCommands := mapsFromAny(report["nextCommands"]) + blockers, omittedBlockerCount := briefBlockers(report, reportID) + packet := map[string]any{ + "blockers": blockers, + "boundaryPolicyRefs": []any{"NC-PROOFKIT-SPEC-005", "NC-PROOFKIT-SPEC-026"}, + "contextRefs": briefContextRefs(nextCommands, reportID), + "detailAccess": map[string]any{ + "availableModes": []any{"full", "report"}, + "commandRef": "agent-route", + "requiresOriginalInput": true, + "sourceReportDigest": reportDigest, + "sourceReportId": reportID, + }, + "nextAction": briefNextAction(nextCommands, reportID), + "omissionSummary": briefOmissionSummary(report, nextCommands, omittedBlockerCount), + "packetId": reportID + ".agent-brief", + "packetKind": "proofkit.agent-route.brief", + "routeFamily": stringFromMap(report, "selectedRouteFamily"), + "schemaVersion": 1, + "state": stringFromMap(report, "state"), + } + encoded, err := stablejson.Marshal(packet) + if err != nil { + return nil, fmt.Errorf("serialize agent-route brief: %w", err) + } + if len(encoded) > maxAgentBriefBytes { + compactBriefAction(packet) + encoded, err = stablejson.Marshal(packet) + if err != nil { + return nil, fmt.Errorf("serialize compacted agent-route brief: %w", err) + } + } + if len(encoded) > maxAgentBriefBytes { + return nil, fmt.Errorf("agent-route brief exceeds %d bytes", maxAgentBriefBytes) + } + return packet, nil +} + +func briefNextAction(commands []map[string]any, reportID string) any { + if len(commands) == 0 { + return nil + } + command := commands[0] + return map[string]any{ + "actionId": reportID + ".action." + commandRefSuffix(command), + "argv": command["argv"], + "argvState": "inline", + "commandId": commandID(reportID, command), + "commandRef": stringFromMap(command, "command"), + "owner": "consumer_repository", + } +} + +func briefContextRefs(commands []map[string]any, reportID string) []any { + if len(commands) == 0 { + return []any{} + } + argv := stringsFromAny(commands[0]["argv"]) + refs := []any{} + for index := 0; index < len(argv)-1; index++ { + role := "" + switch argv[index] { + case "--input": + role = "caller_owned_input" + case "--repo-root": + role = "caller_owned_repo_root" + default: + continue + } + ref := argv[index+1] + refs = append(refs, map[string]any{ + "refId": inputRefID(reportID, argv[index]+"\x00"+ref), + "role": role, + "sourceReportPointer": fmt.Sprintf("/nextCommands/0/argv/%d", index+1), + }) + index++ + } + return refs +} + +func briefBlockers(report map[string]any, reportID string) ([]any, int) { + blockers := []any{} + for index, item := range mapsFromAny(report["requiredInputs"]) { + blockers = append(blockers, map[string]any{ + "blockerId": fmt.Sprintf("%s.blocker.required-input.%02d", reportID, index+1), + "kind": "missing_input", + "sourceReportPointer": fmt.Sprintf("/requiredInputs/%d", index), + "subject": requiredInputLabel(item), + }) + } + for index, item := range mapsFromAny(report["observedReports"]) { + state := stringFromMap(item, "state") + if state == "passed" { + continue + } + blockers = append(blockers, map[string]any{ + "blockerId": fmt.Sprintf("%s.blocker.observed-report.%02d", reportID, index+1), + "kind": "observed_report", + "sourceReportPointer": fmt.Sprintf("/observedReports/%d", index), + "state": state, + "subject": stringFromMap(item, "kind"), + }) + } + if len(blockers) == 0 && stringFromMap(report, "state") == "blocked_unknown_goal" { + blockers = append(blockers, map[string]any{ + "blockerId": reportID + ".blocker.unknown-goal", + "kind": "unknown_goal", + }) + } + if len(blockers) <= maxBriefBlockerItems { + return blockers, 0 + } + return blockers[:maxBriefBlockerItems], len(blockers) - maxBriefBlockerItems +} + +func briefOmissionSummary(report map[string]any, commands []map[string]any, omittedBlockerCount int) map[string]any { + alternativeCount := len(commands) + if alternativeCount > 0 { + alternativeCount-- + } + return map[string]any{ + "availableAlternativeCommandCount": alternativeCount, + "detailClasses": []any{ + "explanatory_prose", + "full_action_plan", + "policy_non_claim_text", + "route_questions", + }, + "omittedBlockerCount": omittedBlockerCount, + "sourceOmittedCommandCount": len(mapsFromAny(report["omitted"])), + } +} + +func compactBriefAction(packet map[string]any) { + action, ok := packet["nextAction"].(map[string]any) + if !ok { + return + } + delete(action, "argv") + action["argvState"] = "detail_required" +} diff --git a/internal/command/agentroute/brief_test.go b/internal/command/agentroute/brief_test.go new file mode 100644 index 0000000..30ee5eb --- /dev/null +++ b/internal/command/agentroute/brief_test.go @@ -0,0 +1,253 @@ +package agentroute + +import ( + "strings" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" + "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" +) + +func TestAgentBriefIsBoundedAndFullEnvelopeRemainsAvailable(t *testing.T) { + t.Parallel() + + input := map[string]any{ + "schemaVersion": jsonNumber("1"), + "routeId": "consumer.route.render", + "goal": "render_human_view", + "mode": "observe", + "availableInputs": []any{ + map[string]any{"kind": "proof_binding", "ref": "proofkit/requirement-bindings.json"}, + map[string]any{"kind": "requirement_source", "ref": "docs/specs/example/requirements.v1.json"}, + }, + } + report, reportExitCode, err := Build(input) + if err != nil || reportExitCode != 0 { + t.Fatalf("Build() exit=%d error=%v", reportExitCode, err) + } + brief, exitCode, err := buildBriefEnvelope(input) + if err != nil || exitCode != 0 { + t.Fatalf("BuildEnvelope() exit=%d error=%v", exitCode, err) + } + full, fullExitCode, err := BuildEnvelope(input) + if err != nil || fullExitCode != 0 { + t.Fatalf("buildFullEnvelope() exit=%d error=%v", fullExitCode, err) + } + + if brief["packetKind"] != "proofkit.agent-route.brief" || brief["schemaVersion"] != 1 { + t.Fatalf("unexpected brief identity: %#v", brief) + } + if brief["state"] != "routed" || brief["routeFamily"] != "rendered_views" { + t.Fatalf("unexpected brief route state: %#v", brief) + } + action := brief["nextAction"].(map[string]any) + if action["commandRef"] != "requirement-source-view" || action["argvState"] != "inline" { + t.Fatalf("brief did not retain the first canonical action: %#v", action) + } + if _, ok := brief["nonClaims"]; ok { + t.Fatalf("brief duplicated policy prose: %#v", brief) + } + if _, ok := brief["routeQuestions"]; ok { + t.Fatalf("brief duplicated route questions: %#v", brief) + } + omissions := brief["omissionSummary"].(map[string]any) + if omissions["availableAlternativeCommandCount"] != 3 || omissions["sourceOmittedCommandCount"] != 6 { + t.Fatalf("brief omission accounting drifted: %#v", omissions) + } + detail := brief["detailAccess"].(map[string]any) + wantDigest, err := digest.StableJSONSHA256Ref(report) + if err != nil { + t.Fatal(err) + } + if detail["sourceReportDigest"] != wantDigest || detail["requiresOriginalInput"] != true { + t.Fatalf("brief detail access is not source-bound: %#v", detail) + } + + briefBytes, err := stablejson.Marshal(brief) + if err != nil { + t.Fatal(err) + } + fullBytes, err := stablejson.Marshal(full) + if err != nil { + t.Fatal(err) + } + if len(briefBytes) > maxAgentBriefBytes || len(briefBytes) >= len(fullBytes) { + t.Fatalf("brief bytes=%d full bytes=%d limit=%d", len(briefBytes), len(fullBytes), maxAgentBriefBytes) + } + rebuilt, _, err := buildBriefEnvelope(input) + if err != nil { + t.Fatal(err) + } + rebuiltBytes, err := stablejson.Marshal(rebuilt) + if err != nil { + t.Fatal(err) + } + if string(briefBytes) != string(rebuiltBytes) { + t.Fatal("brief projection is not deterministic") + } +} + +func TestAgentBriefClosesEverySelectedCommandInputReference(t *testing.T) { + t.Parallel() + + brief, exitCode, err := buildBriefEnvelope(map[string]any{ + "schemaVersion": jsonNumber("1"), + "routeId": "consumer.route.typescript-api", + "goal": "verify_typescript_public_api", + "mode": "observe", + "availableInputs": []any{ + map[string]any{"kind": "typescript_public_api_manifest", "ref": "proofkit/typescript-public-api.json"}, + map[string]any{"kind": "typescript_public_api_repo_root", "ref": "."}, + }, + }) + if err != nil || exitCode != 0 { + t.Fatalf("brief exit=%d error=%v", exitCode, err) + } + refs := brief["contextRefs"].([]any) + if len(refs) != 2 { + t.Fatalf("context ref count=%d want 2: %#v", len(refs), refs) + } + want := []struct { + role string + pointer string + }{ + {role: "caller_owned_repo_root", pointer: "/nextCommands/0/argv/3"}, + {role: "caller_owned_input", pointer: "/nextCommands/0/argv/5"}, + } + for index, expectation := range want { + ref := refs[index].(map[string]any) + if ref["role"] != expectation.role || ref["sourceReportPointer"] != expectation.pointer { + t.Fatalf("context ref %d=%#v want role=%s pointer=%s", index, ref, expectation.role, expectation.pointer) + } + } +} + +func TestAgentBriefNamesCompleteInputBundleBlocker(t *testing.T) { + t.Parallel() + + brief, exitCode, err := buildBriefEnvelope(map[string]any{ + "schemaVersion": jsonNumber("1"), + "routeId": "consumer.route.context", + "goal": "inspect_requirement_context", + "mode": "observe", + }) + if err != nil || exitCode != 1 { + t.Fatalf("brief exit=%d error=%v", exitCode, err) + } + blocker := brief["blockers"].([]any)[0].(map[string]any) + want := "requirement_context_catalog_and_requirement_context_repo_root_or_requirement_context_slice_input_or_requirement_workspace_input" + if blocker["subject"] != want { + t.Fatalf("bundle blocker subject=%v want %s", blocker["subject"], want) + } +} + +func TestBuildEnvelopePreservesBlockedStateWithoutExecutableAction(t *testing.T) { + t.Parallel() + + brief, exitCode, err := buildBriefEnvelope(map[string]any{ + "schemaVersion": jsonNumber("1"), + "routeId": "consumer.route.blocked", + "goal": "inventory_tests", + "mode": "observe", + }) + if err != nil || exitCode != 1 { + t.Fatalf("BuildEnvelope() exit=%d error=%v", exitCode, err) + } + if brief["nextAction"] != nil { + t.Fatalf("blocked brief exposed executable action: %#v", brief["nextAction"]) + } + blockers := brief["blockers"].([]any) + if len(blockers) != 1 || blockers[0].(map[string]any)["kind"] != "missing_input" { + t.Fatalf("blocked brief lost typed blocker: %#v", blockers) + } + if contextRefs := brief["contextRefs"].([]any); len(contextRefs) != 0 { + t.Fatalf("blocked brief exposed action context: %#v", contextRefs) + } +} + +func TestBuildEnvelopeCompactsOversizedArgvWithoutLosingActionIdentity(t *testing.T) { + t.Parallel() + + longRef := "docs/specs/" + strings.Repeat("a", 5000) + ".json" + brief, exitCode, err := buildBriefEnvelope(map[string]any{ + "schemaVersion": jsonNumber("1"), + "routeId": "consumer.route.long-ref", + "goal": "validate_requirement_source", + "mode": "observe", + "availableInputs": []any{ + map[string]any{"kind": "requirement_source", "ref": longRef}, + }, + }) + if err != nil || exitCode != 0 { + t.Fatalf("BuildEnvelope() exit=%d error=%v", exitCode, err) + } + action := brief["nextAction"].(map[string]any) + if action["commandRef"] != "requirement-source-admission" || action["commandId"] == "" { + t.Fatalf("compacted action lost identity: %#v", action) + } + if action["argvState"] != "detail_required" { + t.Fatalf("oversized argv was not deferred to detail access: %#v", action) + } + if _, ok := action["argv"]; ok { + t.Fatalf("compacted action retained oversized argv: %#v", action) + } + encoded, err := stablejson.Marshal(brief) + if err != nil { + t.Fatal(err) + } + if len(encoded) > maxAgentBriefBytes || strings.Contains(string(encoded), longRef) { + t.Fatalf("compacted brief bytes=%d leaked oversized ref", len(encoded)) + } +} + +func TestBuildEnvelopeCapsBlockersAndCountsOmittedDetails(t *testing.T) { + t.Parallel() + + reports := make([]any, 0, 6) + for index := 0; index < 6; index++ { + reports = append(reports, map[string]any{ + "kind": "requirement_source", + "ref": "artifacts/proofkit/source-report-" + string(rune('a'+index)) + ".json", + "state": "warning", + }) + } + brief, exitCode, err := buildBriefEnvelope(map[string]any{ + "schemaVersion": jsonNumber("1"), + "routeId": "consumer.route.many-blockers", + "goal": "validate_requirement_source", + "mode": "observe", + "availableInputs": []any{ + map[string]any{"kind": "requirement_source", "ref": "docs/specs/example/requirements.v1.json"}, + }, + "observedReports": reports, + }) + if err != nil || exitCode != 1 { + t.Fatalf("BuildEnvelope() exit=%d error=%v", exitCode, err) + } + if got := len(brief["blockers"].([]any)); got != maxBriefBlockerItems { + t.Fatalf("blocker count=%d want=%d", got, maxBriefBlockerItems) + } + omissions := brief["omissionSummary"].(map[string]any) + if omissions["omittedBlockerCount"] != 2 { + t.Fatalf("omitted blocker count drifted: %#v", omissions) + } +} + +func TestBuildEnvelopeModeRejectsUnknownMode(t *testing.T) { + t.Parallel() + + _, exitCode, err := BuildEnvelopeModeWithRenderer(map[string]any{ + "schemaVersion": jsonNumber("1"), + "routeId": "consumer.route.mode", + "goal": "unknown", + "mode": "observe", + }, cliexec.PathRenderer(), EnvelopeMode("expanded")) + if err == nil || exitCode != 1 { + t.Fatalf("unknown mode exit=%d error=%v", exitCode, err) + } +} + +func buildBriefEnvelope(raw any) (map[string]any, int, error) { + return BuildEnvelopeModeWithRenderer(raw, cliexec.PathRenderer(), EnvelopeModeBrief) +} diff --git a/internal/command/agentroute/envelope.go b/internal/command/agentroute/envelope.go index cc2d111..ce01f6a 100644 --- a/internal/command/agentroute/envelope.go +++ b/internal/command/agentroute/envelope.go @@ -19,10 +19,29 @@ func BuildEnvelopeWithRenderer(raw any, renderer cliexec.Renderer) (map[string]a if err != nil { return nil, 1, err } - return AgentEnvelope(report), exitCode, nil + return FullAgentEnvelope(report), exitCode, nil } -func AgentEnvelope(report map[string]any) map[string]any { +func BuildEnvelopeModeWithRenderer(raw any, renderer cliexec.Renderer, mode EnvelopeMode) (map[string]any, int, error) { + report, exitCode, err := BuildWithRenderer(raw, renderer) + if err != nil { + return nil, 1, err + } + switch mode { + case EnvelopeModeBrief: + packet, err := AgentBrief(report) + if err != nil { + return nil, 1, err + } + return packet, exitCode, nil + case EnvelopeModeFull: + return FullAgentEnvelope(report), exitCode, nil + default: + return nil, 1, fmt.Errorf("unsupported agent-route envelope mode: %s", mode) + } +} + +func FullAgentEnvelope(report map[string]any) map[string]any { routeState := stringFromMap(report, "state") sourceState := "failed" if routeState == "routed" { @@ -316,6 +335,29 @@ func requiredInputLabel(item map[string]any) string { sort.Strings(parts) return strings.Join(parts, "_or_") } + if rawBundles, ok := item["oneOfBundles"].([]any); ok { + bundles := make([]string, 0, len(rawBundles)) + for _, rawBundle := range rawBundles { + values, ok := rawBundle.([]any) + if !ok { + continue + } + parts := make([]string, 0, len(values)) + for _, value := range values { + if text, ok := value.(string); ok { + parts = append(parts, text) + } + } + sort.Strings(parts) + if len(parts) > 0 { + bundles = append(bundles, strings.Join(parts, "_and_")) + } + } + sort.Strings(bundles) + if len(bundles) > 0 { + return strings.Join(bundles, "_or_") + } + } return "caller_owned_input" } diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go index 7ed4271..1c89bd1 100644 --- a/internal/command/stackpreset/preset_ids_generated.go +++ b/internal/command/stackpreset/preset_ids_generated.go @@ -1,6 +1,6 @@ // Code generated by internal/tools/commandcontractgen; DO NOT EDIT. package stackpreset -const presetContractSourceSHA256 = "3e1289cbe709b62aa5cb76dd71d39ad0c40c70f6fcebd2e991544fac43a51d1c" +const presetContractSourceSHA256 = "3a21c10a7bc1dc7246c420096dd07b6d9783384e386c5c03c214902c6cf3ce5b" var presetIDs = []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"} diff --git a/internal/kernel/releaseplatform/releaseplatform.go b/internal/kernel/releaseplatform/releaseplatform.go index 60fcd02..e25b656 100644 --- a/internal/kernel/releaseplatform/releaseplatform.go +++ b/internal/kernel/releaseplatform/releaseplatform.go @@ -21,8 +21,8 @@ type Target struct { } var targets = []Target{ - target("darwin", "arm64", "arm64", "darwin-arm64", "macosx_12_0_arm64"), - target("darwin", "amd64", "x64", "darwin-x64", "macosx_12_0_x86_64"), + target("darwin", "arm64", "arm64", "darwin-arm64", "macosx_13_0_arm64"), + target("darwin", "amd64", "x64", "darwin-x64", "macosx_13_0_x86_64"), target("linux", "arm64", "arm64", "linux-arm64", "manylinux_2_17_aarch64"), target("linux", "amd64", "x64", "linux-x64", "manylinux_2_17_x86_64"), } diff --git a/internal/kernel/releaseplatform/releaseplatform_test.go b/internal/kernel/releaseplatform/releaseplatform_test.go index c778f78..8aef04a 100644 --- a/internal/kernel/releaseplatform/releaseplatform_test.go +++ b/internal/kernel/releaseplatform/releaseplatform_test.go @@ -17,8 +17,8 @@ func TestTargetsOwnExactReleaseMatrix(t *testing.T) { NPMOS: "darwin", PackageTarEntry: "package/dist/platform/darwin-arm64/agentic-proofkit", PlatformSuffix: "darwin-arm64", - PlatformTag: "macosx_12_0_arm64", - WheelTag: "py3-none-macosx_12_0_arm64", + PlatformTag: "macosx_13_0_arm64", + WheelTag: "py3-none-macosx_13_0_arm64", }, { BinaryPath: "dist/platform/darwin-x64/agentic-proofkit", @@ -28,8 +28,8 @@ func TestTargetsOwnExactReleaseMatrix(t *testing.T) { NPMOS: "darwin", PackageTarEntry: "package/dist/platform/darwin-x64/agentic-proofkit", PlatformSuffix: "darwin-x64", - PlatformTag: "macosx_12_0_x86_64", - WheelTag: "py3-none-macosx_12_0_x86_64", + PlatformTag: "macosx_13_0_x86_64", + WheelTag: "py3-none-macosx_13_0_x86_64", }, { BinaryPath: "dist/platform/linux-arm64/agentic-proofkit", diff --git a/internal/kernel/unicodepolicy/testdata/unsafe-scalar-ranges.v1.json b/internal/kernel/unicodepolicy/testdata/unsafe-scalar-ranges.v1.json index 9ae4b19..e479680 100644 --- a/internal/kernel/unicodepolicy/testdata/unsafe-scalar-ranges.v1.json +++ b/internal/kernel/unicodepolicy/testdata/unsafe-scalar-ranges.v1.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, - "unicodeVersion": "15.0.0", - "tableSha256": "16873769cae138cd0129781932e487ed558510e52a0237dcfcb558081994cfd9", + "unicodeVersion": "17.0.0", + "tableSha256": "3d7664f62e9abe69851726e858e1214a860dbe2ab0a2adbd6b3c79cbb4ff2649", "ranges": [ {"category": "Cc", "start": 0, "end": 31, "step": 1}, {"category": "Cc", "start": 127, "end": 159, "step": 1}, diff --git a/internal/kernel/unicodepolicy/unicodepolicy.go b/internal/kernel/unicodepolicy/unicodepolicy.go index cb98d8c..2791888 100644 --- a/internal/kernel/unicodepolicy/unicodepolicy.go +++ b/internal/kernel/unicodepolicy/unicodepolicy.go @@ -7,8 +7,8 @@ import ( ) const ( - UnicodeVersion = "15.0.0" - TableSHA256 = "16873769cae138cd0129781932e487ed558510e52a0237dcfcb558081994cfd9" + UnicodeVersion = "17.0.0" + TableSHA256 = "3d7664f62e9abe69851726e858e1214a860dbe2ab0a2adbd6b3c79cbb4ff2649" ) type scalarRange struct { diff --git a/internal/kernel/unicodepolicy/unicodepolicy_test.go b/internal/kernel/unicodepolicy/unicodepolicy_test.go index b5bde41..6ea3a4a 100644 --- a/internal/kernel/unicodepolicy/unicodepolicy_test.go +++ b/internal/kernel/unicodepolicy/unicodepolicy_test.go @@ -88,7 +88,7 @@ func TestUnsafeScalarClassification(t *testing.T) { } } -func TestUnsafeScalarTableMatchesEveryUnicode15Scalar(t *testing.T) { +func TestUnsafeScalarTableMatchesEveryUnicode17Scalar(t *testing.T) { if unicode.Version != UnicodeVersion { t.Fatalf("Go Unicode version = %s, want %s", unicode.Version, UnicodeVersion) } diff --git a/internal/tools/commandoracle/corpus_coordinates.go b/internal/tools/commandoracle/corpus_coordinates.go index 26a91dd..8b7490f 100644 --- a/internal/tools/commandoracle/corpus_coordinates.go +++ b/internal/tools/commandoracle/corpus_coordinates.go @@ -63,7 +63,7 @@ func syntheticRecordValue() (map[string]any, error) { CounterfeitCorpusDigest: strings.Repeat("2", 64), Entries: entries, ExecutionCommands: executionCommands(candidates), - GoVersion: "go1.26.7", + GoVersion: "go1.27.1", NonClaims: RecordNonClaims(), Platform: "darwin/arm64", SchemaVersion: SchemaVersion, diff --git a/internal/tools/commandoracle/events.go b/internal/tools/commandoracle/events.go index 4ab176c..edcc88d 100644 --- a/internal/tools/commandoracle/events.go +++ b/internal/tools/commandoracle/events.go @@ -125,7 +125,7 @@ func admitEvent(line []byte) (testEvent, error) { if !ok { return testEvent{}, decision("event.object_required") } - if err := admit.KnownKeys(record, []string{"Action", "Elapsed", "FailedBuild", "Key", "Output", "Package", "Test", "Time", "Value"}, "go test event"); err != nil { + if err := admit.KnownKeys(record, []string{"Action", "Elapsed", "FailedBuild", "Key", "Output", "OutputType", "Package", "Path", "Test", "Time", "Value"}, "go test event"); err != nil { return testEvent{}, decision("event.unknown_field") } action, ok := record["Action"].(string) @@ -135,7 +135,7 @@ func admitEvent(line []byte) (testEvent, error) { if !validEventAction(action) { return testEvent{}, decision("event.action_unknown") } - for _, key := range []string{"FailedBuild", "Key", "Package", "Test", "Value"} { + for _, key := range []string{"FailedBuild", "Key", "OutputType", "Package", "Path", "Test", "Value"} { if value, exists := record[key]; exists { if _, ok := value.(string); !ok { return testEvent{}, decision("event.field_type_invalid") diff --git a/internal/tools/commandoracle/events_test.go b/internal/tools/commandoracle/events_test.go index 24fe044..1f0dd3b 100644 --- a/internal/tools/commandoracle/events_test.go +++ b/internal/tools/commandoracle/events_test.go @@ -11,6 +11,8 @@ func TestAdmitEventRejectsUnknownActionAndInvalidAuxiliaryTypes(t *testing.T) { {name: "unknown action", line: `{"Action":"counterfeit","Package":"example.test/p"}`, want: "event.action_unknown"}, {name: "elapsed string", line: `{"Action":"start","Elapsed":"0","Package":"example.test/p"}`, want: "event.field_type_invalid"}, {name: "output object", line: `{"Action":"output","Output":{},"Package":"example.test/p"}`, want: "event.field_type_invalid"}, + {name: "output type object", line: `{"Action":"output","Output":"x","OutputType":{},"Package":"example.test/p"}`, want: "event.field_type_invalid"}, + {name: "path object", line: `{"Action":"output","Output":"x","Package":"example.test/p","Path":{}}`, want: "event.field_type_invalid"}, {name: "time number", line: `{"Action":"start","Package":"example.test/p","Time":1}`, want: "event.field_type_invalid"}, } { t.Run(testCase.name, func(t *testing.T) { @@ -21,6 +23,13 @@ func TestAdmitEventRejectsUnknownActionAndInvalidAuxiliaryTypes(t *testing.T) { } } +func TestAdmitEventAcceptsGo127OutputMetadata(t *testing.T) { + line := []byte(`{"Action":"output","Output":"x","OutputType":"frame","Package":"example.test/p","Path":"internal/sample/sample_test.go"}`) + if _, err := admitEvent(line); err != nil { + t.Fatalf("admitEvent() error = %v", err) + } +} + func TestEventLedgerRejectsPackageAndTestActionContextDrift(t *testing.T) { candidates := syntheticCandidates() imports := map[string]string{"./internal/sample": "example.test/proofkit/internal/sample"} diff --git a/internal/tools/coveragemetrics/main.go b/internal/tools/coveragemetrics/main.go index bef9083..87a142f 100644 --- a/internal/tools/coveragemetrics/main.go +++ b/internal/tools/coveragemetrics/main.go @@ -284,6 +284,7 @@ func validateRequiredBindingWitnessSelectors(bindings bindingFile) error { {"REQ-PROOFKIT-PACKAGE-002", "proofkit.package-boundary.generated-command-caller-preservation"}: {"TestBootstrapPreservesCallerDisplayCommandInGuidancePayload"}, {"REQ-PROOFKIT-PACKAGE-002", "proofkit.package-boundary.cli-output-root-witnesses"}: { "TestAdoptionContractEnvelopeCLIABI", + "TestAgentRouteEnvelopeModesUseExactRootShapes", "TestRequirementAuthoringPlanOutputUsesVersionedRootShape", "TestSelfCheckOutputUsesExactRootShape", "TestStandaloneMultiVariantCommandsUseExactRootShapes", @@ -305,6 +306,7 @@ func validateRequiredBindingWitnessSelectors(bindings bindingFile) error { }, {"REQ-PROOFKIT-QUALITY-004", "proofkit.supply-chain-quality.cli-abi-golden"}: { "TestAdoptionContractEnvelopeCLIABI", + "TestAgentRouteEnvelopeModesUseExactRootShapes", "TestRequiredInputCommandsRouteStructuralErrorsByMode", "TestRequirementAuthoringPlanOutputUsesVersionedRootShape", "TestRequirementBrowserOneShotCLIOutputVariants", @@ -507,6 +509,22 @@ func validateRequiredBindingWitnessSelectors(bindings bindingFile) error { {"REQ-PROOFKIT-SPEC-012", "proofkit.spec-proof-core.requirement-authoring-ref-provenance"}: { "TestBuildPreservesDigestBoundAuthoringRefIdentity", }, + {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-brief-cli-abi"}: { + "TestAgentRouteEnvelopeModesUseExactRootShapes", + }, + {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-brief-projection"}: { + "TestAgentBriefClosesEverySelectedCommandInputReference", + "TestAgentBriefIsBoundedAndFullEnvelopeRemainsAvailable", + "TestAgentBriefNamesCompleteInputBundleBlocker", + "TestBuildEnvelopeCapsBlockersAndCountsOmittedDetails", + "TestBuildEnvelopeCompactsOversizedArgvWithoutLosingActionIdentity", + }, + {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-brief-version-edge"}: { + "TestAgentRouteVersionEdgeClosesBriefDefaultMigration", + }, + {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-materialized-ref-admission"}: { + "TestBuildRejectsStdinTransportSentinelAsArtifactReference", + }, {"REQ-PROOFKIT-RETIRE-006", "proofkit.consumer-infra-retirement.migration-parity-admission"}: { "TestBuildProjectsEveryCallerDeclaredStatusAndSummaryField", }, @@ -583,6 +601,10 @@ func validateRequiredBindingWitnessSelectors(bindings bindingFile) error { {"REQ-PROOFKIT-SPEC-006", "proofkit.spec-proof-core.test-inventory-and-coverage-view"}: "internal/command/requirementcoverageview/output_closure_test.go", {"REQ-PROOFKIT-SPEC-006", "proofkit.spec-proof-core.declared-route-mapping-without-assurance"}: "internal/command/requirementcoverageview/requirementcoverageview_test.go", {"REQ-PROOFKIT-SPEC-012", "proofkit.spec-proof-core.requirement-authoring-ref-provenance"}: "internal/command/requirementauthoringplan/requirement_authoring_plan_test.go", + {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-brief-cli-abi"}: "internal/app/cli_abi_test.go", + {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-brief-projection"}: "internal/command/agentroute/brief_test.go", + {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-brief-version-edge"}: "internal/app/agent_route_version_edge_test.go", + {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-materialized-ref-admission"}: "internal/command/agentroute/agentroute_test.go", } if len(requiredPaths) != len(required) { return fmt.Errorf("required selector path inventory=%d, selector inventory=%d", len(requiredPaths), len(required)) diff --git a/internal/tools/packagepack/main.go b/internal/tools/packagepack/main.go index 7ddd376..32fda86 100644 --- a/internal/tools/packagepack/main.go +++ b/internal/tools/packagepack/main.go @@ -54,16 +54,36 @@ func run() error { } func npmPack(packageRoot string) ([]packRecord, error) { - output, err := exec.Command("npm", "pack", "--json", "--pack-destination", filepath.Join("artifacts", "package"), packageRoot).CombinedOutput() + command := exec.Command("npm", "--silent", "pack", "--json", "--pack-destination", filepath.Join("artifacts", "package"), packageRoot) + stderr := diagnostic.NewStderrCapture() + command.Stderr = stderr + output, err := command.Output() if err != nil { - return nil, fmt.Errorf("npm pack %s: %w\n%s", packageRoot, err, output) + if childErr := stderr.Failure("npm pack child stderr"); childErr != nil { + return nil, fmt.Errorf("npm pack %s: %w; %s", packageRoot, err, childErr) + } + return nil, fmt.Errorf("npm pack %s: %w", packageRoot, err) } - records, err := admission.DecodeTypedJSON[[]packRecord](bytes.NewReader(output), int64(len(output))) + records, err := decodeNPM12PackOutput(output) if err != nil { - return nil, fmt.Errorf("parse npm pack output for %s: %w\n%s", packageRoot, err, output) - } - if len(records) != 1 { - return nil, fmt.Errorf("npm pack %s must return exactly one record", packageRoot) + return nil, fmt.Errorf("parse npm pack output for %s: %w", packageRoot, err) } return records, nil } + +func decodeNPM12PackOutput(output []byte) ([]packRecord, error) { + keyed, err := admission.DecodeTypedJSON[map[string]packRecord](bytes.NewReader(output), int64(len(output))) + if err != nil { + return nil, err + } + if len(keyed) != 1 { + return nil, fmt.Errorf("npm pack output must contain exactly one keyed record") + } + for key, record := range keyed { + if key != record.Name { + return nil, fmt.Errorf("npm pack output key must equal the record package name") + } + return []packRecord{record}, nil + } + return nil, fmt.Errorf("npm pack output must contain exactly one keyed record") +} diff --git a/internal/tools/packagepack/main_test.go b/internal/tools/packagepack/main_test.go new file mode 100644 index 0000000..459ffd7 --- /dev/null +++ b/internal/tools/packagepack/main_test.go @@ -0,0 +1,51 @@ +package main + +import ( + "strings" + "testing" +) + +func TestDecodeNPM12PackOutputRequiresOneIdentityBoundRecord(t *testing.T) { + valid := []byte("{\"@research-engineering/agentic-proofkit\":{\"filename\":\"package.tgz\",\"integrity\":\"sha512-value\",\"name\":\"@research-engineering/agentic-proofkit\",\"shasum\":\"abc\",\"version\":\"1.2.3\"}}") + records, err := decodeNPM12PackOutput(valid) + if err != nil { + t.Fatalf("decodeNPM12PackOutput(valid) error = %v", err) + } + if len(records) != 1 || records[0].Name != "@research-engineering/agentic-proofkit" { + t.Fatalf("decodeNPM12PackOutput(valid) = %#v", records) + } + + for _, test := range []struct { + name string + value string + want string + }{ + { + name: "legacy array root", + value: "[{\"filename\":\"package.tgz\",\"integrity\":\"sha512-value\",\"name\":\"@research-engineering/agentic-proofkit\",\"shasum\":\"abc\",\"version\":\"1.2.3\"}]", + want: "cannot unmarshal array", + }, + { + name: "mismatched key", + value: "{\"other\":{\"filename\":\"package.tgz\",\"integrity\":\"sha512-value\",\"name\":\"@research-engineering/agentic-proofkit\",\"shasum\":\"abc\",\"version\":\"1.2.3\"}}", + want: "key must equal", + }, + { + name: "multiple records", + value: "{\"first\":{\"name\":\"first\"},\"second\":{\"name\":\"second\"}}", + want: "exactly one", + }, + { + name: "duplicate key", + value: "{\"first\":{\"name\":\"first\"},\"first\":{\"name\":\"first\"}}", + want: "duplicate object key", + }, + } { + t.Run(test.name, func(t *testing.T) { + _, err := decodeNPM12PackOutput([]byte(test.value)) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("decodeNPM12PackOutput() error = %v, want %q", err, test.want) + } + }) + } +} diff --git a/internal/tools/packageverify/main.go b/internal/tools/packageverify/main.go index 33e6fd0..01e410b 100644 --- a/internal/tools/packageverify/main.go +++ b/internal/tools/packageverify/main.go @@ -629,8 +629,8 @@ func verifyRootManifestBoundary(artifact rootPackageArtifact) error { if manifest.License != "MIT" { return fmt.Errorf("root package license must be MIT, got %s", manifest.License) } - if manifest.PackageManager != "npm@11.18.0" { - return fmt.Errorf("root package packageManager must be npm@11.18.0, got %s", manifest.PackageManager) + if manifest.PackageManager != "npm@12.0.2" { + return fmt.Errorf("root package packageManager must be npm@12.0.2, got %s", manifest.PackageManager) } if manifest.Type != "module" { return fmt.Errorf("root package type must be module, got %s", manifest.Type) @@ -638,7 +638,7 @@ func verifyRootManifestBoundary(artifact rootPackageArtifact) error { if manifest.SideEffects { return fmt.Errorf("root package sideEffects must be false") } - expectedDevDependencies := map[string]string{"@playwright/test": "1.62.0", "axe-core": "4.12.1", "typescript": "7.0.2"} + expectedDevDependencies := map[string]string{"@playwright/test": "1.62.1", "axe-core": "4.13.0", "typescript": "7.0.2"} if !maps.Equal(manifest.DevDependencies, expectedDevDependencies) { return fmt.Errorf("root package devDependencies must equal the source-only browser proof toolchain") } @@ -1300,25 +1300,27 @@ func verifyCLIContractSourceClassifications(content string, entries map[string]s return err } if err := verifyClosedReferenceInventory("CLI contract", value, map[string]string{ - "/processContract/helpGrammar/helpCatalogFormsSource": "package_public", - "/commands/*/inputContract/fields/availableInputs/item/ref": "runtime_field", - "/commands/*/inputContract/fields/knownChangedPaths": "runtime_field", - "/commands/*/inputContract/fields/observedReports/item/ref": "runtime_field", - "/commands/*/inputContract/nativeAdmissionWitnessSelector": "source_checkout_selector", - "/commands/*/inputContract/nativeAdmissionWitnessSelector/path": "source_checkout", - "/commands/*/inputContract/nativeSource/path": "source_checkout", - "/commands/*/inputContract/nativeSources/*/path": "source_checkout", - "/commands/*/inputContract/ownerRequirementRefs": "requirement_identifier", - "/commands/*/inputContract/rootDefinitionRef": "schema_identifier", - "/commands/*/outputContract/nativeOutputWitnessSelector": "source_checkout_selector", - "/commands/*/outputContract/nativeOutputWitnessSelector/path": "source_checkout", - "/commands/*/outputContract/nativeSource/path": "source_checkout", - "/commands/*/outputContract/nativeSources/*/path": "source_checkout", - "/commands/*/outputContract/ownerRequirementRefs": "requirement_identifier", - "/commands/*/outputContract/qualityFindingFields/evidenceRefs": "runtime_field", - "/commands/*/outputContract/records/dependencyRef": "runtime_field", - "/commands/*/outputContract/rootDefinitionRef": "schema_identifier", - "/contractDefinitions/*/definitionRefs": "schema_identifier", + "/processContract/helpGrammar/helpCatalogFormsSource": "package_public", + "/commands/*/inputContract/fields/availableInputs/item/ref": "runtime_field", + "/commands/*/inputContract/fields/knownChangedPaths": "runtime_field", + "/commands/*/inputContract/fields/observedReports/item/ref": "runtime_field", + "/commands/*/inputContract/nativeAdmissionWitnessSelector": "source_checkout_selector", + "/commands/*/inputContract/nativeAdmissionWitnessSelector/path": "source_checkout", + "/commands/*/inputContract/nativeSource/path": "source_checkout", + "/commands/*/inputContract/nativeSources/*/path": "source_checkout", + "/commands/*/inputContract/ownerRequirementRefs": "requirement_identifier", + "/commands/*/inputContract/rootDefinitionRef": "schema_identifier", + "/commands/*/outputContract/nativeOutputWitnessSelector": "source_checkout_selector", + "/commands/*/outputContract/nativeOutputWitnessSelector/path": "source_checkout", + "/commands/*/outputContract/nativeSource/path": "source_checkout", + "/commands/*/outputContract/nativeSources/*/path": "source_checkout", + "/commands/*/outputContract/ownerRequirementRefs": "requirement_identifier", + "/commands/*/outputContract/briefPacketContract/fieldRules/boundaryPolicyRefs": "contract_field_description", + "/commands/*/outputContract/briefPacketContract/fieldRules/contextRefs": "contract_field_description", + "/commands/*/outputContract/qualityFindingFields/evidenceRefs": "runtime_field", + "/commands/*/outputContract/records/dependencyRef": "runtime_field", + "/commands/*/outputContract/rootDefinitionRef": "schema_identifier", + "/contractDefinitions/*/definitionRefs": "schema_identifier", }); err != nil { return err } @@ -2447,16 +2449,94 @@ func verifyInstalledJSONABI(consumer string) error { if err := verifyJSONAdapterSourceSmoke(consumer); err != nil { return err } + if err := verifyInstalledAgentRouteEnvelopeModes(consumer); err != nil { + return err + } if err := workflowsmoke.VerifyProcess(context.Background(), workflowsmoke.ProcessCarrier{ Directory: consumer, Executable: "npm", - Prefix: []string{"exec", "--offline", "--", "agentic-proofkit"}, + Prefix: []string{"--silent", "exec", "--offline", "--", "agentic-proofkit"}, }); err != nil { return fmt.Errorf("outside consumer agent-workflow smoke failed: %w", err) } return nil } +func verifyInstalledAgentRouteEnvelopeModes(consumer string) error { + input := []byte(`{"schemaVersion":1,"routeId":"proofkit.package-smoke.agent-route","goal":"validate_requirement_source","mode":"observe","availableInputs":[{"kind":"requirement_source","ref":"docs/specs/example/requirements.v1.json"}]}` + "\n") + bareBrief, err := runInstalledWithInput(consumer, input, "agent-route", "--input", "-", "--agent-envelope") + if err != nil { + return fmt.Errorf("outside consumer bare agent-route brief smoke failed to run: %w", err) + } + if err := verifyInstalledAgentRouteBrief(bareBrief); err != nil { + return fmt.Errorf("outside consumer bare agent-route brief smoke failed: %w", err) + } + explicitBrief, err := runInstalledWithInput(consumer, input, "agent-route", "--input", "-", "--agent-envelope", "--agent-envelope-mode", "brief") + if err != nil { + return fmt.Errorf("outside consumer explicit agent-route brief smoke failed to run: %w", err) + } + if err := verifyInstalledAgentRouteBrief(explicitBrief); err != nil { + return fmt.Errorf("outside consumer explicit agent-route brief smoke failed: %w", err) + } + if !bytes.Equal(bareBrief.Stdout, explicitBrief.Stdout) { + return fmt.Errorf("outside consumer bare and explicit agent-route brief outputs differ") + } + full, err := runInstalledWithInput(consumer, input, "agent-route", "--input", "-", "--agent-envelope", "--agent-envelope-mode", "full") + if err != nil { + return fmt.Errorf("outside consumer full agent-route envelope smoke failed to run: %w", err) + } + if err := verifyInstalledAgentRouteFullEnvelope(full); err != nil { + return fmt.Errorf("outside consumer full agent-route envelope smoke failed: %w", err) + } + return nil +} + +func verifyInstalledAgentRouteBrief(result installedCommandResult) error { + if result.ExitCode != 0 { + return fmt.Errorf("exit code %d, want 0; stdout=%s stderr=%s", result.ExitCode, result.Stdout, result.Stderr) + } + if len(result.Stderr) != 0 { + return fmt.Errorf("stderr must be empty, got %q", string(result.Stderr)) + } + packet, err := admission.DecodeTypedJSON[map[string]any](bytes.NewReader(result.Stdout), 1<<20) + if err != nil { + return fmt.Errorf("stdout must be one strict JSON object: %w", err) + } + if packet["packetKind"] != "proofkit.agent-route.brief" || packet["state"] != "routed" { + return fmt.Errorf("unexpected brief identity or state") + } + if _, ok := packet["nextAction"].(map[string]any); !ok { + return fmt.Errorf("brief nextAction must be an object") + } + if len(result.Stdout) > 3072 { + return fmt.Errorf("brief output is %d bytes, want at most 3072", len(result.Stdout)) + } + return nil +} + +func verifyInstalledAgentRouteFullEnvelope(result installedCommandResult) error { + if result.ExitCode != 0 { + return fmt.Errorf("exit code %d, want 0; stdout=%s stderr=%s", result.ExitCode, result.Stdout, result.Stderr) + } + if len(result.Stderr) != 0 { + return fmt.Errorf("stderr must be empty, got %q", string(result.Stderr)) + } + envelope, err := admission.DecodeTypedJSON[map[string]any](bytes.NewReader(result.Stdout), 8<<20) + if err != nil { + return fmt.Errorf("stdout must be one strict JSON object: %w", err) + } + if envelope["envelopeId"] != "proofkit.package-smoke.agent-route.agent-envelope" { + return fmt.Errorf("unexpected full envelope identity") + } + if _, ok := envelope["actionPlan"].([]any); !ok { + return fmt.Errorf("full envelope actionPlan must be an array") + } + if _, brief := envelope["packetKind"]; brief { + return fmt.Errorf("full envelope must not use the brief packet root") + } + return nil +} + func verifyJSONAdapterSourceSmoke(consumer string) error { result, err := runInstalledWithInput(consumer, nil, "json-report-cli-adapter-source", "--language", "typescript") if err != nil { @@ -2542,7 +2622,7 @@ func runWithInput(dir string, name string, input []byte, args ...string) (instal } func runInstalledWithInput(dir string, input []byte, args ...string) (installedCommandResult, error) { - npmArgs := append([]string{"exec", "--offline", "--", "agentic-proofkit"}, args...) + npmArgs := append([]string{"--silent", "exec", "--offline", "--", "agentic-proofkit"}, args...) return runWithInput(dir, "npm", input, npmArgs...) } diff --git a/internal/tools/packageverify/main_test.go b/internal/tools/packageverify/main_test.go index a494e50..6b2eee1 100644 --- a/internal/tools/packageverify/main_test.go +++ b/internal/tools/packageverify/main_test.go @@ -238,13 +238,13 @@ func TestVerifyRootManifestBoundaryRejectsDevDependencyDrift(t *testing.T) { { name: "missing dependency", patch: func(manifest string) string { - return strings.Replace(manifest, " \"axe-core\": \"4.12.1\",\n", "", 1) + return strings.Replace(manifest, " \"axe-core\": \"4.13.0\",\n", "", 1) }, }, { name: "wrong dependency version", patch: func(manifest string) string { - return strings.Replace(manifest, "\"axe-core\": \"4.12.1\"", "\"axe-core\": \"4.12.0\"", 1) + return strings.Replace(manifest, "\"axe-core\": \"4.13.0\"", "\"axe-core\": \"4.12.1\"", 1) }, }, { @@ -253,7 +253,7 @@ func TestVerifyRootManifestBoundaryRejectsDevDependencyDrift(t *testing.T) { return strings.Replace( manifest, " \"devDependencies\": {\n", - " \"devDependencies\": {\n \"@axe-core/playwright\": \"4.12.1\",\n", + " \"devDependencies\": {\n \"@axe-core/playwright\": \"4.13.0\",\n", 1, ) }, @@ -1385,7 +1385,7 @@ func packageReferenceClosureFixture() map[string]string { "package/proofkit/witness-plan.json": `{"commands":[],"policies":[]}`, "package/proofkit/command-families.v1.json": `{"families":[]}`, "package/proofkit/receipt-producer-policy.json": `{"producers":[{"producerId":"local.developer","evidenceRefs":["docs/specs/example/requirements.v1.json"]}]}`, - "package/proofkit/cli-contract.v2.json": `{"processContract":{"helpGrammar":{"helpCatalogFormsSource":"proofkit/command-families.v1.json"}},"commands":[{"command":"fixture","inputContract":{"nativeSource":{"path":"internal/tools/packageverify/main.go","evidenceClass":"source_checkout"}}}]}`, + "package/proofkit/cli-contract.v2.json": `{"processContract":{"helpGrammar":{"helpCatalogFormsSource":"proofkit/command-families.v1.json"}},"commands":[{"command":"fixture","inputContract":{"nativeSource":{"path":"internal/tools/packageverify/main.go","evidenceClass":"source_checkout"}},"outputContract":{"briefPacketContract":{"fieldRules":{"boundaryPolicyRefs":"policy field description","contextRefs":"runtime field description"}},"nativeSource":{"path":"internal/tools/packageverify/main.go","evidenceClass":"source_checkout"}}}]}`, } } @@ -1757,12 +1757,12 @@ func packageManifestFixture(repositoryURL string) string { "name": "@research-engineering/agentic-proofkit", "version": "1.2.3", "license": "MIT", - "packageManager": "npm@11.18.0", + "packageManager": "npm@12.0.2", "type": "module", "sideEffects": false, "devDependencies": { - "@playwright/test": "1.62.0", - "axe-core": "4.12.1", + "@playwright/test": "1.62.1", + "axe-core": "4.13.0", "typescript": "7.0.2" }, "repository": { diff --git a/internal/tools/pythonpackage/build.go b/internal/tools/pythonpackage/build.go index 33e14cb..b1d5044 100644 --- a/internal/tools/pythonpackage/build.go +++ b/internal/tools/pythonpackage/build.go @@ -247,9 +247,7 @@ func writeWheel(path string, entries []wheelEntry) error { Method: zip.Deflate, Modified: time.Date(1980, 1, 1, 0, 0, 0, 0, time.UTC), CRC32: crc32.ChecksumIEEE(entry.Content), - CompressedSize: uint32(len(compressed)), CompressedSize64: uint64(len(compressed)), - UncompressedSize: uint32(len(entry.Content)), UncompressedSize64: uint64(len(entry.Content)), } header.SetMode(entry.Mode) diff --git a/internal/tools/pythonpackage/verify.go b/internal/tools/pythonpackage/verify.go index dd91632..dda2576 100644 --- a/internal/tools/pythonpackage/verify.go +++ b/internal/tools/pythonpackage/verify.go @@ -601,6 +601,9 @@ func verifyInstalledPythonHelpAndAgentRouteContinuity(consumer string, environme if err := writeJSONFixture(agentRoutePath, agentRoute); err != nil { return err } + if err := verifyInstalledPythonAgentRouteEnvelopeModes(consumer, environment, renderer, agentRoutePath); err != nil { + return err + } routeOutput, err := runArgvWithEnvironment(consumer, environment, renderer.Argv("agent-route", "--input", agentRoutePath)) if err != nil { return fmt.Errorf("installed Python wheel agent route failed: %w\n%s", err, routeOutput) @@ -636,6 +639,69 @@ func verifyInstalledPythonHelpAndAgentRouteContinuity(consumer string, environme return requirePythonPassedJSON(commandOutput, "installed Python wheel emitted agent-route argv") } +func verifyInstalledPythonAgentRouteEnvelopeModes(consumer string, environment []string, renderer cliexec.Renderer, inputPath string) error { + bareBrief, err := runArgvWithEnvironment(consumer, environment, renderer.Argv("agent-route", "--input", inputPath, "--agent-envelope")) + if err != nil { + return fmt.Errorf("installed Python wheel bare agent-route brief failed: %w\n%s", err, bareBrief) + } + if err := verifyInstalledPythonAgentRouteBrief(bareBrief); err != nil { + return fmt.Errorf("installed Python wheel bare agent-route brief is invalid: %w", err) + } + explicitBrief, err := runArgvWithEnvironment(consumer, environment, renderer.Argv("agent-route", "--input", inputPath, "--agent-envelope", "--agent-envelope-mode", "brief")) + if err != nil { + return fmt.Errorf("installed Python wheel explicit agent-route brief failed: %w\n%s", err, explicitBrief) + } + if err := verifyInstalledPythonAgentRouteBrief(explicitBrief); err != nil { + return fmt.Errorf("installed Python wheel explicit agent-route brief is invalid: %w", err) + } + if !bytes.Equal(bareBrief, explicitBrief) { + return fmt.Errorf("installed Python wheel bare and explicit agent-route brief outputs differ") + } + full, err := runArgvWithEnvironment(consumer, environment, renderer.Argv("agent-route", "--input", inputPath, "--agent-envelope", "--agent-envelope-mode", "full")) + if err != nil { + return fmt.Errorf("installed Python wheel full agent-route envelope failed: %w\n%s", err, full) + } + value, err := admission.DecodeJSON(bytes.NewReader(full), 8<<20) + if err != nil { + return fmt.Errorf("installed Python wheel full agent-route envelope must be one strict JSON value: %w", err) + } + envelope, ok := value.(map[string]any) + if !ok { + return fmt.Errorf("installed Python wheel full agent-route envelope must be an object") + } + if envelope["envelopeId"] != "proofkit.python.consumer.route.agent-envelope" { + return fmt.Errorf("installed Python wheel full agent-route envelope has unexpected identity") + } + if _, ok := envelope["actionPlan"].([]any); !ok { + return fmt.Errorf("installed Python wheel full agent-route envelope actionPlan must be an array") + } + if _, brief := envelope["packetKind"]; brief { + return fmt.Errorf("installed Python wheel full agent-route envelope uses the brief packet root") + } + return nil +} + +func verifyInstalledPythonAgentRouteBrief(output []byte) error { + value, err := admission.DecodeJSON(bytes.NewReader(output), 1<<20) + if err != nil { + return fmt.Errorf("output must be one strict JSON value: %w", err) + } + packet, ok := value.(map[string]any) + if !ok { + return fmt.Errorf("output must be an object") + } + if packet["packetKind"] != "proofkit.agent-route.brief" || packet["state"] != "routed" { + return fmt.Errorf("output has unexpected brief identity or state") + } + if _, ok := packet["nextAction"].(map[string]any); !ok { + return fmt.Errorf("output nextAction must be an object") + } + if len(output) > 3072 { + return fmt.Errorf("output is %d bytes, want at most 3072", len(output)) + } + return nil +} + func exactDisplayedRouteOperands(output []byte, prefix string, context string) ([]string, error) { decoded, err := unicodepolicy.DecodeUTF8(output) if err != nil { diff --git a/internal/tools/releasechange/record_test.go b/internal/tools/releasechange/record_test.go index b855e64..db9cef5 100644 --- a/internal/tools/releasechange/record_test.go +++ b/internal/tools/releasechange/record_test.go @@ -194,13 +194,20 @@ func TestCurrentChangeRecordNamesReviewedSemanticChanges(t *testing.T) { assertCurrentChangeRecordNotesRejected(t, "appended duplicate change section", record, notes+"## Breaking Contract Changes\n\n- `proofkit.surplus.section`: Surplus section.\n") } -var currentBreakingChanges = []Change{} +var currentBreakingChanges = []Change{ + {ChangeID: "proofkit.agent-route.brief-default", Summary: "Change bare agent-route --agent-envelope output from the generic full envelope to the bounded proofkit.agent-route.brief packet."}, + {ChangeID: "proofkit.platform.macos-13", Summary: "Raise published Darwin package compatibility to macOS 13.0 so wheel tags remain truthful for binaries built with Go 1.27.1."}, +} var currentAdditions = []Change{ - {ChangeID: "proofkit.requirement-source.codec-selection", Summary: "Select the private bounded grouped-JSON requirement-source v2 codec from byte-bound V3 screen evidence, with exact collection-limit ownership, nondisclosing source diagnostics, lexical source maps, decision-metric closure, and owner-scoped grammar identity proof."}, + {ChangeID: "proofkit.agent-route.envelope-detail-mode", Summary: "Add --agent-envelope-mode brief|full so agents can request bounded default guidance or the complete generic envelope explicitly."}, + {ChangeID: "proofkit.toolchain.currentness", Summary: "Update the verified build and CI baseline to Go 1.27.1 with Unicode 17.0.0, Node 26.8.1, npm 12.0.2, Python 3.14.7, and the current stable repository-owned dependency and action pins."}, } -var currentMigrationSteps = []string{} +var currentMigrationSteps = []string{ + "Consumers that require the former generic agent-route envelope must add --agent-envelope-mode full after --agent-envelope; consumers that accept bounded route guidance may keep bare --agent-envelope.", + "Darwin consumers must use macOS 13.0 or later; contributors that build from source must use Go 1.27.1.", +} func validateCurrentChangeRecord(record Record, notes string) error { if !slices.Equal(record.BreakingChanges, currentBreakingChanges) { @@ -220,11 +227,10 @@ func validateCurrentChangeRecord(record Record, notes string) error { func currentExpectedReleaseNotes() string { lines := []string{ - "# @research-engineering/agentic-proofkit 0.5.1", + "# @research-engineering/agentic-proofkit 0.6.0", "", "## Breaking Contract Changes", "", - "- None.", } for _, change := range currentBreakingChanges { lines = append(lines, currentChangeBullet(change)) @@ -240,17 +246,22 @@ func currentExpectedReleaseNotes() string { "", "## Migration", "", - "No consumer migration is required.", + "Migration is required:", + "", ) + for _, step := range currentMigrationSteps { + lines = append(lines, "- "+step) + } lines = append(lines, "", "## Platform Requirements", "", - "- Published Darwin package binaries require macOS 12.0 or later on arm64 and x86_64.", + "- Published Darwin package binaries require macOS 13.0 or later on arm64 and x86_64.", "", "## Known Limitations", "", "- Agent workflow plans, prompts, text, and envelopes are derived guidance and do not execute agents, repository mutations, native witnesses, CI, release, rollout, or production operations.", + "- Brief agent-route packets cap pretty JSON at 3072 bytes and may defer oversized argv to explicit full detail; the bound does not claim tokenizer-specific token counts.", "- Complete nested public structural contracts remain blocked under SCHEMA-01; current CLI contracts own exact root variants only.", "- The selected requirement-source v2 codec remains internal; current requirement sources are not migrated and no source cutover is claimed.", "- TSX source parsing remains unsupported.", @@ -260,7 +271,7 @@ func currentExpectedReleaseNotes() string { "Primary npm channel:", "", "```bash", - "npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.5.1", + "npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.6.0", "```", "", "Pre-1.0 npm consumers must keep this dependency exact-pinned.", @@ -271,7 +282,7 @@ func currentExpectedReleaseNotes() string { "", "## Rollback", "", - "- Pin npm consumers to the previous admitted version 0.5.0 with `npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.5.0`.", + "- Pin npm consumers to the previous admitted version 0.5.1 with `npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.5.1`.", "- Treat local package artifacts as candidates until registry identity is proven.", ) return strings.Join(lines, "\n") + "\n" diff --git a/internal/tools/releasecloseoutinput/main_test.go b/internal/tools/releasecloseoutinput/main_test.go index 589fe0f..09b8ab9 100644 --- a/internal/tools/releasecloseoutinput/main_test.go +++ b/internal/tools/releasecloseoutinput/main_test.go @@ -989,15 +989,15 @@ func producerReachableCommandRouteMetricsFixture() coverageCommandRouteMetrics { missingDeclared := []string{"proofkit.cli.a", "proofkit.cli.b"} commands := []string{"proofkit.cli.a", "proofkit.cli.b"} return coverageCommandRouteMetrics{ - AdmittedInventoryEntryCount: testIntPointer(3), - CommandCount: testIntPointer(2), - Commands: &commands, - CommandWithoutProofRouteCandidateCount: testIntPointer(0), - CommandsWithoutProofRouteCandidate: &empty, - ContractOnlyCommandCount: testIntPointer(0), - ContractOnlyCommands: &empty, - CommandWithoutDeclaredSemanticFalsifierRouteCount: testIntPointer(2), - CommandsWithoutDeclaredSemanticFalsifierRoute: &missingDeclared, + AdmittedInventoryEntryCount: testIntPointer(3), + CommandCount: testIntPointer(2), + Commands: &commands, + CommandWithoutProofRouteCandidateCount: testIntPointer(0), + CommandsWithoutProofRouteCandidate: &empty, + ContractOnlyCommandCount: testIntPointer(0), + ContractOnlyCommands: &empty, + CommandWithoutDeclaredSemanticFalsifierRouteCount: testIntPointer(2), + CommandsWithoutDeclaredSemanticFalsifierRoute: &missingDeclared, RouteCount: testIntPointer(3), RouteOnlyCommandCount: testIntPointer(0), RouteOnlyCommands: &empty, diff --git a/package-lock.json b/package-lock.json index a1cd681..c98c25d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@research-engineering/agentic-proofkit", - "version": "0.5.1", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@research-engineering/agentic-proofkit", - "version": "0.5.1", + "version": "0.6.0", "cpu": [ "arm64", "x64" @@ -20,19 +20,19 @@ "agentic-proofkit": "dist/agentic-proofkit" }, "devDependencies": { - "@playwright/test": "1.62.0", - "axe-core": "4.12.1", + "@playwright/test": "1.62.1", + "axe-core": "4.13.0", "typescript": "7.0.2" } }, "node_modules/@playwright/test": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.0.tgz", - "integrity": "sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", + "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.62.0" + "playwright": "1.62.1" }, "bin": { "playwright": "cli.js" @@ -382,9 +382,9 @@ } }, "node_modules/axe-core": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", - "integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz", + "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", "dev": true, "license": "MPL-2.0", "engines": { @@ -407,13 +407,13 @@ } }, "node_modules/playwright": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz", - "integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.62.0" + "playwright-core": "1.62.1" }, "bin": { "playwright": "cli.js" @@ -426,9 +426,9 @@ } }, "node_modules/playwright-core": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz", - "integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index de40bd5..0d8791c 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "@research-engineering/agentic-proofkit", "description": "Reusable proof profile, report, graph, and witness-planning primitives.", - "version": "0.5.1", + "version": "0.6.0", "type": "module", "license": "MIT", "sideEffects": false, - "packageManager": "npm@11.18.0", + "packageManager": "npm@12.0.2", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" @@ -74,8 +74,8 @@ "x64" ], "devDependencies": { - "@playwright/test": "1.62.0", - "axe-core": "4.12.1", + "@playwright/test": "1.62.1", + "axe-core": "4.13.0", "typescript": "7.0.2" } } diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index d25de1a..f26b0a0 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -347,9 +347,30 @@ ], "allowedFlags": [ "--agent-envelope", + "--agent-envelope-mode", "--input", "--input-pointer" ], + "singleOccurrenceFlags": [ + "--agent-envelope", + "--agent-envelope-mode", + "--input", + "--input-pointer" + ], + "flagChoices": { + "--agent-envelope-mode": [ + "brief", + "full" + ] + }, + "flagPresenceRequirements": [ + { + "flag": "--agent-envelope-mode", + "requiredFlags": [ + "--agent-envelope" + ] + } + ], "inputContract": { "contractId": "proofkit.agent-route.input.v1", "schemaVersion": 1, @@ -399,9 +420,9 @@ "witness_command_catalog" ] }, - "ref": { - "format": "safe repo-relative caller-owned file, report ref, or scanner root ref; scanner root refs may be ." - } + "ref": { + "format": "safe repo-relative materialized caller-owned file, report ref, or scanner root ref; stdin sentinel - is forbidden and scanner root refs may be ." + } }, "uniqueBy": "kind" }, @@ -511,7 +532,7 @@ "rootDefinitionDigest": "sha256:fd1121285d1b357ed848febc248700a75417d06619e22ed36991631d7526375b", "nativeSource": { "path": "internal/command/agentroute", - "canonicalDigest": "sha256:2fc5d42c6126069aad7cad9a18acf9683289ce54be3a0d2f3aef6f429e2bcbdf", + "canonicalDigest": "sha256:c4957046177c5cc8192deae96efd6b492a8f2a1a0c3d02da4dd4b3f4f97ca1cf", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -540,72 +561,81 @@ ] }, "outputContract": { - "contractId": "proofkit.agent-route.output.v2", - "schemaVersion": 2, - "authority": "deterministic route report derived from admitted agent-route input", - "requiredFields": [ - "guidanceSlice", - "reportId", - "reportKind", - "schemaVersion", - "selectedRouteFamily", - "state" - ], - "fields": { - "schemaVersion": { - "value": 2 - }, - "selectedRouteFamily": { - "enum": [ - "adoption", - "migration", - "release_and_deployment", - "rendered_views", - "repository_structure", - "requirement_proof_binding", - "requirement_source", - "selective_evidence", - "selective_planning", - "test_inventory_and_coverage", - "unknown" - ] + "contractId": "proofkit.agent-route.output.v3", + "schemaVersion": 3, + "authority": "deterministic route report, brief packet, full envelope, or invalid-input repair packet selected from the admitted invocation", + "briefPacketContract": { + "contractId": "proofkit.agent-route.brief.v1", + "schemaVersion": 1, + "maxPrettyJSONBytes": 3072, + "maxBlockers": 4, + "requiredFields": [ + "blockers", + "boundaryPolicyRefs", + "contextRefs", + "detailAccess", + "nextAction", + "omissionSummary", + "packetId", + "packetKind", + "routeFamily", + "schemaVersion", + "state" + ], + "fieldRules": { + "blockers": "required-input blockers followed by sorted non-passed observed-report blockers; retain at most four and count the exact remainder", + "boundaryPolicyRefs": "exactly NC-PROOFKIT-SPEC-005 and NC-PROOFKIT-SPEC-026; policy prose is not duplicated", + "contextRefs": "only caller-owned artifact operands of the selected next command, each addressed by an RFC 6901 pointer into the source report", + "detailAccess": "binds source report ID and stable digest and advertises report and full retrieval from the original admitted input", + "nextAction": "null for blocked states; otherwise the first canonical nextCommands entry with exact command identity and inline argv unless the byte bound requires argvState=detail_required", + "omissionSummary": "exact counts for unselected available commands, source-omitted commands, and blockers omitted by the packet bound", + "packetKind": "proofkit.agent-route.brief", + "schemaVersion": "1" }, - "guidanceSlice": { - "requiredFields": [ - "routeFamily" - ], - "routeFamilyRule": "must equal selectedRouteFamily" - } + "nonClaims": [ + "The brief packet is derived route guidance, not requirement, proof, execution, merge, release, rollout, deployment, or readiness authority.", + "The packet byte bound does not claim tokenizer-specific token consumption or semantic sufficiency." + ] }, - "changesFromV1": [ - "selectedFamily is replaced by selectedRouteFamily", - "guidanceSlice.family is replaced by guidanceSlice.routeFamily" + "changesFromV2": [ + "bare --agent-envelope emits proofkit.agent-route.brief", + "--agent-envelope-mode full preserves the prior generic envelope projection", + "route report schemaVersion advances to 3 without changing its semantic fields" ], "rootType": "object", "closed": true, - "rootDefinitionRef": "proofkit.agent-route.output.v2.root-shape", - "rootDefinitionDigest": "sha256:db7d5b29083a939eea0f23253632394212b0d73b5f1b6206d0811d236d6a84ca", - "nativeSource": { - "path": "internal/command/agentroute", - "canonicalDigest": "sha256:2fc5d42c6126069aad7cad9a18acf9683289ce54be3a0d2f3aef6f429e2bcbdf", - "evidenceClass": "source_checkout" - }, + "rootDefinitionRef": "proofkit.agent-route.output.v3.root-shape", + "rootDefinitionDigest": "sha256:3a48ba6391239e9bce6b6dfcc1c19831fb1029b4cdbb4b19e3af4d944f8275ce", + "nativeSources": [ + { + "path": "internal/app", + "canonicalDigest": "sha256:3fb162add7aad5988f19c88016571f113af948190c0042f74016b7a688625386", + "evidenceClass": "source_checkout" + }, + { + "path": "internal/command/agentroute", + "canonicalDigest": "sha256:c4957046177c5cc8192deae96efd6b492a8f2a1a0c3d02da4dd4b3f4f97ca1cf", + "evidenceClass": "source_checkout" + } + ], "nativeOutputWitnessSelector": { - "path": "internal/command/agentroute/agentroute_test.go", - "test": "TestBuildRoutesRequirementSourceAndBlocksUnknownGoal", - "command": "go test ./internal/command/agentroute -run '^TestBuildRoutesRequirementSourceAndBlocksUnknownGoal$'", + "path": "internal/app/cli_abi_test.go", + "test": "TestAgentRouteEnvelopeModesUseExactRootShapes", + "command": "go test ./internal/app -run '^TestAgentRouteEnvelopeModesUseExactRootShapes$'", "evidenceClass": "source_checkout" }, "compatibilitySummary": [ "guidanceSlice", - "schemaVersion", "selectedRouteFamily", - "root-shape-only definition proofkit.agent-route.output.v2.root-shape; nested fields, types, and cardinalities are non-claims" + "schemaVersion=3", + "brief agent packet by default with explicit full envelope detail mode", + "nested proofkit.agent-route.brief.v1 contract plus root-shape definition proofkit.agent-route.output.v3.root-shape" ], "ownerRequirementRefs": [ "REQ-PROOFKIT-PACKAGE-002", "REQ-PROOFKIT-QUALITY-004", - "REQ-PROOFKIT-SPEC-005" + "REQ-PROOFKIT-SPEC-005", + "REQ-PROOFKIT-SPEC-026" ] } }, @@ -2421,7 +2451,7 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:af43c7dae19e019887fd3c43ddef4d453966b517880dc3fb76be5f3ed048281d", + "canonicalDigest": "sha256:3fb162add7aad5988f19c88016571f113af948190c0042f74016b7a688625386", "evidenceClass": "source_checkout" }, { @@ -5663,7 +5693,7 @@ "rootDefinitionDigest": "sha256:3c842174dff5361e7f83166469b832805e05aa314b073c16234b5b64e346281e", "nativeSource": { "path": "internal/app", - "canonicalDigest": "sha256:af43c7dae19e019887fd3c43ddef4d453966b517880dc3fb76be5f3ed048281d", + "canonicalDigest": "sha256:3fb162add7aad5988f19c88016571f113af948190c0042f74016b7a688625386", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -5692,7 +5722,7 @@ "rootDefinitionDigest": "sha256:0ea95e277ebe44cd2de42c29b47c38686ac0b6b390d8965367437b3fe138e209", "nativeSource": { "path": "internal/app", - "canonicalDigest": "sha256:af43c7dae19e019887fd3c43ddef4d453966b517880dc3fb76be5f3ed048281d", + "canonicalDigest": "sha256:3fb162add7aad5988f19c88016571f113af948190c0042f74016b7a688625386", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { @@ -7706,7 +7736,7 @@ "canonicalDigest": "sha256:fd1121285d1b357ed848febc248700a75417d06619e22ed36991631d7526375b" }, { - "definitionId": "proofkit.agent-route.output.v2.root-shape", + "definitionId": "proofkit.agent-route.output.v3.root-shape", "schemaVersion": 1, "rootType": "object", "closed": true, @@ -7718,6 +7748,39 @@ "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." ], "variants": [ + { + "allowedFields": [ + "blockers", + "boundaryPolicyRefs", + "contextRefs", + "detailAccess", + "nextAction", + "omissionSummary", + "packetId", + "packetKind", + "routeFamily", + "schemaVersion", + "state" + ], + "requiredFields": [ + "blockers", + "boundaryPolicyRefs", + "contextRefs", + "detailAccess", + "nextAction", + "omissionSummary", + "packetId", + "packetKind", + "routeFamily", + "schemaVersion", + "state" + ], + "rootKind": "object", + "variantId": "01-agent-brief", + "when": [ + "--agent-envelope=present --agent-envelope-mode=absent|brief; admitted route input" + ] + }, { "allowedFields": [ "actionPlan", @@ -7752,9 +7815,48 @@ "sourceReport" ], "rootKind": "object", - "variantId": "01-agent-envelope", + "variantId": "02-agent-envelope-full", "when": [ - "--agent-envelope" + "--agent-envelope=present --agent-envelope-mode=full; admitted route input" + ] + }, + { + "allowedFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "requiredFields": [ + "actionPlan", + "blockedPreconditions", + "bounds", + "clarificationQuestions", + "commands", + "contextRefs", + "costContract", + "envelopeId", + "nonClaims", + "omitted", + "receiptRefs", + "routeQuestions", + "schemaVersion", + "sourceReport" + ], + "rootKind": "object", + "variantId": "03-invalid-input-repair", + "when": [ + "--agent-envelope=present; route input or input-pointer admission failed" ] }, { @@ -7795,7 +7897,7 @@ "summary" ], "rootKind": "object", - "variantId": "02-routed", + "variantId": "04-routed", "when": [ "without --agent-envelope; known goal" ] @@ -7836,14 +7938,14 @@ "summary" ], "rootKind": "object", - "variantId": "03-unknown-goal", + "variantId": "05-unknown-goal", "when": [ "without --agent-envelope; goal=unknown" ] } ] }, - "canonicalDigest": "sha256:db7d5b29083a939eea0f23253632394212b0d73b5f1b6206d0811d236d6a84ca" + "canonicalDigest": "sha256:3a48ba6391239e9bce6b6dfcc1c19831fb1029b4cdbb4b19e3af4d944f8275ce" }, { "definitionId": "proofkit.binding-partition.input.v1.root-shape", diff --git a/proofkit/requirement-bindings.json b/proofkit/requirement-bindings.json index e3b3ef2..5496f57 100644 --- a/proofkit/requirement-bindings.json +++ b/proofkit/requirement-bindings.json @@ -704,6 +704,14 @@ "proofState": "witness_backed", "nonClaims": ["This private codec does not migrate or rewrite current requirement sources, expose a public source extension or CLI command, retain a normalized mirror, authenticate requirement meaning or derivation provenance, generalize the frozen formatter screen beyond its byte-bound corpus, prove the open-world absence of undeclared semantically equivalent parsers, prevent a future owner-approved grammar from being added, execute native witnesses, approve merge or release, or establish rollout or production readiness."] }, + { + "requirementId": "REQ-PROOFKIT-SPEC-026", + "ownerId": "proofkit.spec-proof-core", + "specPath": "docs/specs/proofkit-spec-proof-core/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": ["The 3072-byte packet bound and stable source-report digest do not prove provider token consumption, semantic sufficiency, source freshness, command execution, native witness truth, or policy approval; detail retrieval requires the caller to retain the original admitted input."] + }, { "requirementId": "REQ-PROOFKIT-WORKFLOW-001", "ownerId": "proofkit.agent-workflow", @@ -1108,6 +1116,10 @@ "selector": "TestAdoptionContractEnvelopeCLIABI", "command": "go test ./internal/app -run '^TestAdoptionContractEnvelopeCLIABI$'" }, + { + "selector": "TestAgentRouteEnvelopeModesUseExactRootShapes", + "command": "go test ./internal/app -run '^TestAgentRouteEnvelopeModesUseExactRootShapes$'" + }, { "selector": "TestRequirementAuthoringPlanOutputUsesVersionedRootShape", "command": "go test ./internal/app -run '^TestRequirementAuthoringPlanOutputUsesVersionedRootShape$'" @@ -2323,6 +2335,10 @@ "selector": "TestAdoptionContractEnvelopeCLIABI", "command": "go test ./internal/app -run '^TestAdoptionContractEnvelopeCLIABI$'" }, + { + "selector": "TestAgentRouteEnvelopeModesUseExactRootShapes", + "command": "go test ./internal/app -run '^TestAgentRouteEnvelopeModesUseExactRootShapes$'" + }, { "selector": "TestRequirementAuthoringPlanOutputUsesVersionedRootShape", "command": "go test ./internal/app -run '^TestRequirementAuthoringPlanOutputUsesVersionedRootShape$'" @@ -3866,8 +3882,8 @@ "command": "go test ./internal/kernel/unicodepolicy -run '^TestUnsafeScalarTableMatchesVersionedOwnerCorpus$'" }, { - "selector": "TestUnsafeScalarTableMatchesEveryUnicode15Scalar", - "command": "go test ./internal/kernel/unicodepolicy -run '^TestUnsafeScalarTableMatchesEveryUnicode15Scalar$'" + "selector": "TestUnsafeScalarTableMatchesEveryUnicode17Scalar", + "command": "go test ./internal/kernel/unicodepolicy -run '^TestUnsafeScalarTableMatchesEveryUnicode17Scalar$'" }, { "selector": "TestValidScalarStringRejectsMalformedUTF8", @@ -4907,6 +4923,82 @@ "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, + { + "requirementId": "REQ-PROOFKIT-SPEC-026", + "scenarioId": "proofkit.spec-proof-core.agent-route-brief-cli-abi", + "witnessId": "proofkit.agent-route-brief.cli-abi-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/cli_abi_test.go", + "witnessSelectors": [ + { + "selector": "TestAgentRouteEnvelopeModesUseExactRootShapes", + "command": "go test ./internal/app -run '^TestAgentRouteEnvelopeModesUseExactRootShapes$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-SPEC-026", + "scenarioId": "proofkit.spec-proof-core.agent-route-brief-projection", + "witnessId": "proofkit.agent-route-brief.projection-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/agentroute/brief_test.go", + "witnessSelectors": [ + { + "selector": "TestAgentBriefIsBoundedAndFullEnvelopeRemainsAvailable", + "command": "go test ./internal/command/agentroute -run '^TestAgentBriefIsBoundedAndFullEnvelopeRemainsAvailable$'" + }, + { + "selector": "TestAgentBriefClosesEverySelectedCommandInputReference", + "command": "go test ./internal/command/agentroute -run '^TestAgentBriefClosesEverySelectedCommandInputReference$'" + }, + { + "selector": "TestAgentBriefNamesCompleteInputBundleBlocker", + "command": "go test ./internal/command/agentroute -run '^TestAgentBriefNamesCompleteInputBundleBlocker$'" + }, + { + "selector": "TestBuildEnvelopeCompactsOversizedArgvWithoutLosingActionIdentity", + "command": "go test ./internal/command/agentroute -run '^TestBuildEnvelopeCompactsOversizedArgvWithoutLosingActionIdentity$'" + }, + { + "selector": "TestBuildEnvelopeCapsBlockersAndCountsOmittedDetails", + "command": "go test ./internal/command/agentroute -run '^TestBuildEnvelopeCapsBlockersAndCountsOmittedDetails$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-SPEC-026", + "scenarioId": "proofkit.spec-proof-core.agent-route-materialized-ref-admission", + "witnessId": "proofkit.agent-route.materialized-ref-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/agentroute/agentroute_test.go", + "witnessSelectors": [ + { + "selector": "TestBuildRejectsStdinTransportSentinelAsArtifactReference", + "command": "go test ./internal/command/agentroute -run '^TestBuildRejectsStdinTransportSentinelAsArtifactReference$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-SPEC-026", + "scenarioId": "proofkit.spec-proof-core.agent-route-brief-version-edge", + "witnessId": "proofkit.agent-route-brief.version-edge-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/agent_route_version_edge_test.go", + "witnessSelectors": [ + { + "selector": "TestAgentRouteVersionEdgeClosesBriefDefaultMigration", + "command": "go test ./internal/app -run '^TestAgentRouteVersionEdgeClosesBriefDefaultMigration$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, { "requirementId": "REQ-PROOFKIT-SPEC-023", "scenarioId": "proofkit.spec-proof-core.requirement-traceability-topology-closure", diff --git a/release/change-record.v2.json b/release/change-record.v2.json index 959e934..01e2edb 100644 --- a/release/change-record.v2.json +++ b/release/change-record.v2.json @@ -1,24 +1,41 @@ { "schemaVersion": 2, - "previousVersion": "0.5.0", - "version": "0.5.1", - "changeClass": "compatible", - "breakingChanges": [], + "previousVersion": "0.5.1", + "version": "0.6.0", + "changeClass": "breaking", + "breakingChanges": [ + { + "changeId": "proofkit.agent-route.brief-default", + "summary": "Change bare agent-route --agent-envelope output from the generic full envelope to the bounded proofkit.agent-route.brief packet." + }, + { + "changeId": "proofkit.platform.macos-13", + "summary": "Raise published Darwin package compatibility to macOS 13.0 so wheel tags remain truthful for binaries built with Go 1.27.1." + } + ], "additions": [ { - "changeId": "proofkit.requirement-source.codec-selection", - "summary": "Select the private bounded grouped-JSON requirement-source v2 codec from byte-bound V3 screen evidence, with exact collection-limit ownership, nondisclosing source diagnostics, lexical source maps, decision-metric closure, and owner-scoped grammar identity proof." + "changeId": "proofkit.agent-route.envelope-detail-mode", + "summary": "Add --agent-envelope-mode brief|full so agents can request bounded default guidance or the complete generic envelope explicitly." + }, + { + "changeId": "proofkit.toolchain.currentness", + "summary": "Update the verified build and CI baseline to Go 1.27.1 with Unicode 17.0.0, Node 26.8.1, npm 12.0.2, Python 3.14.7, and the current stable repository-owned dependency and action pins." } ], "migration": { - "required": false, - "steps": [] + "required": true, + "steps": [ + "Consumers that require the former generic agent-route envelope must add --agent-envelope-mode full after --agent-envelope; consumers that accept bounded route guidance may keep bare --agent-envelope.", + "Darwin consumers must use macOS 13.0 or later; contributors that build from source must use Go 1.27.1." + ] }, "platformRequirements": [ - "Published Darwin package binaries require macOS 12.0 or later on arm64 and x86_64." + "Published Darwin package binaries require macOS 13.0 or later on arm64 and x86_64." ], "knownLimitations": [ "Agent workflow plans, prompts, text, and envelopes are derived guidance and do not execute agents, repository mutations, native witnesses, CI, release, rollout, or production operations.", + "Brief agent-route packets cap pretty JSON at 3072 bytes and may defer oversized argv to explicit full detail; the bound does not claim tokenizer-specific token counts.", "Complete nested public structural contracts remain blocked under SCHEMA-01; current CLI contracts own exact root variants only.", "The selected requirement-source v2 codec remains internal; current requirement sources are not migrated and no source cutover is claimed.", "TSX source parsing remains unsupported." diff --git a/scripts/browser-proof-inputs.test.mjs b/scripts/browser-proof-inputs.test.mjs index 2eb114d..fca3df6 100644 --- a/scripts/browser-proof-inputs.test.mjs +++ b/scripts/browser-proof-inputs.test.mjs @@ -29,7 +29,7 @@ test("browser accessibility harness closes direct audit topology", async () => { snapshots: true, sources: true, }); - assert.equal(axeDistributionVersion, "4.12.1"); + assert.equal(axeDistributionVersion, "4.13.0"); assert(axeDistributionSource.length > 0); const exactConfigureOptions = { allowedOrigins: [""], diff --git a/scripts/stable-json.test.mjs b/scripts/stable-json.test.mjs index f2bb09d..778c4a8 100644 --- a/scripts/stable-json.test.mjs +++ b/scripts/stable-json.test.mjs @@ -63,7 +63,7 @@ test("stable JSON Unicode corpus", () => { test("stable JSON Unicode table classifies every scalar and complement", () => { assert.equal(unicodePolicyCorpus.schemaVersion, 1); - assert.equal(unicodePolicyCorpus.unicodeVersion, "15.0.0"); + assert.equal(unicodePolicyCorpus.unicodeVersion, "17.0.0"); const ranges = unicodePolicyCorpus.ranges.map(({start, end, step}) => [start, end, step]); const expected = new Set(); for (const [start, end, step] of ranges) { diff --git a/scripts/stable_json_selector_test.go b/scripts/stable_json_selector_test.go index bbc664a..3192ada 100644 --- a/scripts/stable_json_selector_test.go +++ b/scripts/stable_json_selector_test.go @@ -9,6 +9,8 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/testsupport/nodetestselector" ) +const stableJSONSelectorTimeout = 45 * time.Second + func TestStableJSONJavaScriptUnicodePredicates(t *testing.T) { runStableJSONJavaScriptSelectors(t, []string{ "stable JSON Unicode corpus", @@ -29,7 +31,7 @@ func runStableJSONJavaScriptSelectors(t *testing.T, names []string) { if err != nil { t.Fatalf("locate node: %v", err) } - ctx, cancel := context.WithTimeout(t.Context(), 15*time.Second) + ctx, cancel := context.WithTimeout(t.Context(), stableJSONSelectorTimeout) defer cancel() if err := nodetestselector.RunSet(ctx, nodePath, ".", "stable-json.test.mjs", names); err != nil { t.Fatalf("run exact JavaScript selector set: %v", err) diff --git a/scripts/workflow_package_gate_oracle_test.go b/scripts/workflow_package_gate_oracle_test.go index 8e392eb..77b2f16 100644 --- a/scripts/workflow_package_gate_oracle_test.go +++ b/scripts/workflow_package_gate_oracle_test.go @@ -19,10 +19,10 @@ import ( const mergeSatisfyingProducerEnv = "PROOFKIT_MERGE_SATISFYING_PRODUCER" const requiredPlatformSmokeOwnerCommand = "go run ./internal/tools/packagebuild current && ./dist/agentic-proofkit --help >/dev/null && go run ./internal/tools/pythonpackage build-current && go run ./internal/tools/pythonpackage verify-current" -const setupVerifiedNPMActionSHA256 = "ead7e280f6430a9e83a544d5200217efaa36bf7aaedc879f417141fddfb20e8e" -const ciSourceQualityStepInventorySHA256 = "90143666d13b499059937564e0829ecb1799946edb2975f187759cf5ef246da0" -const ciBrowserRuntimeStepInventorySHA256 = "4880405e46ad4daad339117e76174a579e77cdfb70dde9c18d4afc7873f30aa4" -const releaseCandidateStepInventorySHA256 = "12e2b229f711fb83e7a4230a455f764f03b5e36abf17ac0e53539037718b9e65" +const setupVerifiedNPMActionSHA256 = "73e760391a9f93b95034aec8edd12af081051ce6859d3114e3561c4d255d061f" +const ciSourceQualityStepInventorySHA256 = "08f0a637b09bc8071ae771d57cf7a74493fd392aa10a2e952e9e73de00e591c7" +const ciBrowserRuntimeStepInventorySHA256 = "75774851de2c5d6a020d2ee9aa4d1c877d4d9d879b4272e715bdaaf7f7da9cec" +const releaseCandidateStepInventorySHA256 = "32077f0273db85606716b297776fec5c1777e96cf25b695995846d5b259ce0aa" type packageGateWorkflowExpectation struct { label string @@ -1528,14 +1528,14 @@ func validateExactPlatformSmokeSteps(job githubJob) error { expected := []githubStep{ { Name: "Checkout", - Uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0", + Uses: "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1", With: map[string]any{"persist-credentials": false}, }, { Name: "Setup Node", - Uses: "actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e", + Uses: "actions/setup-node@820762786026740c76f36085b0efc47a31fe5020", With: map[string]any{ - "node-version": "24.18.0", + "node-version": "26.8.1", "package-manager-cache": false, }, }, diff --git a/scripts/workflow_runtime_preconditions_test.go b/scripts/workflow_runtime_preconditions_test.go index ded69ce..049a8cc 100644 --- a/scripts/workflow_runtime_preconditions_test.go +++ b/scripts/workflow_runtime_preconditions_test.go @@ -29,10 +29,10 @@ func TestCISourceQualityInstallsPythonBeforeLifecycleTests(t *testing.T) { t.Fatalf("Setup Python index=%d must precede Go tests index=%d", setupIndex, testIndex) } setup := job.Steps[setupIndex] - if setup.Uses != "actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1" { - t.Fatalf("Setup Python uses=%q, want pinned actions/setup-python v6.3.0", setup.Uses) + if setup.Uses != "actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97" { + t.Fatalf("Setup Python uses=%q, want pinned actions/setup-python v7.0.0", setup.Uses) } - if got := withString(setup.With, "python-version"); got != "3.14.6" { - t.Fatalf("Setup Python python-version=%q, want 3.14.6", got) + if got := withString(setup.With, "python-version"); got != "3.14.7" { + t.Fatalf("Setup Python python-version=%q, want 3.14.7", got) } } diff --git a/scripts/workflow_source_oracles_test.go b/scripts/workflow_source_oracles_test.go index 6eebce3..990da43 100644 --- a/scripts/workflow_source_oracles_test.go +++ b/scripts/workflow_source_oracles_test.go @@ -241,7 +241,7 @@ func TestWorkflowExternalActionsUseFullCommitSHAs(t *testing.T) { t.Fatal(err) } base := readWorkflowForTest(t, filepath.Join("..", ".github", "workflows", "ci.yml")) - for _, reference := range []string{"actions/checkout@v7", "actions/checkout@9c091bb"} { + for _, reference := range []string{"actions/checkout@v7", "actions/checkout@3d3c42e"} { t.Run(reference, func(t *testing.T) { workflow := cloneWorkflow(t, base) job := workflow.Jobs["source-quality"] From 1a288cf25db6b11fb663328fc1993026ffc471b8 Mon Sep 17 00:00:00 2001 From: iperev Date: Wed, 2 Sep 2026 23:52:07 +0200 Subject: [PATCH 2/9] fix: close agent routing release invariants --- .github/workflows/release.yml | 19 +---- docs/proofkit-contract-map.md | 7 +- .../requirements.v1.json | 2 +- internal/app/agent_route_version_edge_test.go | 15 ++-- internal/app/app_test.go | 23 ++++++ internal/app/cli_abi_test.go | 21 ++++++ internal/app/cli_contract_test.go | 2 +- internal/app/command_contract_generated.go | 4 +- .../app/testdata/v0.6-wire-observations.json | 12 ++-- internal/command/agentroute/agentroute.go | 16 +++-- .../command/agentroute/agentroute_test.go | 32 ++++++--- internal/command/agentroute/brief.go | 60 ++++++++++------ internal/command/agentroute/brief_test.go | 63 +++++++++++++++++ .../stackpreset/preset_ids_generated.go | 2 +- internal/tools/coveragemetrics/main.go | 1 + internal/tools/npmpack/report.go | 49 +++++++++++++ internal/tools/npmpack/report_test.go | 56 +++++++++++++++ internal/tools/npmregistry/main.go | 47 ++++++++++++- internal/tools/npmregistry/main_test.go | 45 ++++++++++++ internal/tools/packagepack/main.go | 36 ++-------- internal/tools/packagepack/main_test.go | 51 -------------- internal/tools/packageverify/main.go | 53 +++++++++++++- internal/tools/packageverify/main_test.go | 18 ++++- internal/tools/releasechange/record_test.go | 2 + proofkit/cli-contract.v2.json | 70 ++++++++++--------- proofkit/requirement-bindings.json | 19 +++++ release/change-record.v2.json | 5 ++ .../validate-self-hosting-receipts_test.go | 5 +- 28 files changed, 545 insertions(+), 190 deletions(-) create mode 100644 internal/tools/npmpack/report.go create mode 100644 internal/tools/npmpack/report_test.go delete mode 100644 internal/tools/packagepack/main_test.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2e75366..a39a954 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -388,22 +388,9 @@ jobs: sleep 10 done done < artifacts/registry/package-names.txt - node <<'NODE' - const { readFileSync, readdirSync, writeFileSync } = require("node:fs"); - - const files = readdirSync("artifacts/registry") - .filter((file) => file.startsWith("pack-") && file.endsWith(".json")) - .sort(); - const records = files.map((file) => { - const parsed = JSON.parse(readFileSync(`artifacts/registry/${file}`, "utf8")); - if (!Array.isArray(parsed) || parsed.length !== 1) { - throw new Error(`registry npm pack report ${file} must contain exactly one record`); - } - return parsed[0]; - }); - records.sort((left, right) => left.name.localeCompare(right.name)); - writeFileSync("artifacts/registry/npm-pack.json", `${JSON.stringify(records, null, 2)}\n`); - NODE + go run ./internal/tools/npmregistry capture-pack-reports \ + artifacts/registry/npm-pack.json \ + artifacts/registry/pack-*.json npm run npm:registry-evidence - name: Verify root-only registry install and signatures diff --git a/docs/proofkit-contract-map.md b/docs/proofkit-contract-map.md index 8657da4..a4702cb 100644 --- a/docs/proofkit-contract-map.md +++ b/docs/proofkit-contract-map.md @@ -161,9 +161,10 @@ Semantic context routes are `requirement-context-compose`, 3. Use `agent-route` when a coding agent needs a deterministic next-command report from explicit current state. Use `agent-route --agent-envelope` when the agent needs exactly one next action or terminal state, bounded blockers, - caller-owned context refs, exact omission counts, and stable boundary-policy - refs instead of the full route report. Resolve further detail from the - original input only after checking the brief's source digest; request + caller-owned context refs, exact omission counts, and uniquely resolvable + shipped boundary-policy requirement refs instead of the full route report. + Resolve further detail with the packet's exact output-argument suffixes and + retained original input only after checking the brief's source digest; request `--agent-envelope-mode full` only when the generic envelope is actually needed. Treat `blocked_*` states as stop signals, not as permission to guess missing inputs. `knownChangedPaths` are diff --git a/docs/specs/proofkit-spec-proof-core/requirements.v1.json b/docs/specs/proofkit-spec-proof-core/requirements.v1.json index 86d9bb4..a28409b 100644 --- a/docs/specs/proofkit-spec-proof-core/requirements.v1.json +++ b/docs/specs/proofkit-spec-proof-core/requirements.v1.json @@ -585,7 +585,7 @@ { "requirementId": "REQ-PROOFKIT-SPEC-026", "ownerId": "proofkit.spec-proof-core", - "invariant": "Agent-route output contract v3 derives every report, brief packet, and full envelope from one fully admitted immutable route input and one command-owned route report: route artifact refs identify materialized caller-owned artifacts and never use the stdin transport sentinel; a bare --agent-envelope and explicit brief mode emit the same deterministic schema-v1 proofkit.agent-route.brief root while explicit full mode preserves the prior generic envelope; a routed brief selects exactly the first canonical nextCommands entry as its sole nextAction, retains its exact command and argv identity when the 3072-byte pretty stable-JSON bound permits, otherwise removes only inline argv and marks argvState detail_required, while every blocked state emits null nextAction; blockers are projected in deterministic required-input then sorted non-passed observed-report order, retain at most four typed entries, and report the exact omitted remainder; contextRefs refer to all and only caller-owned artifact operands of the selected command through resolvable source-report JSON pointers; one top-level detailAccess record binds the source report ID and stable digest, advertises report and full retrieval from the original input, and no per-item detail reference is added; omissionSummary exactly counts unselected available commands, source-omitted commands, and omitted blockers; fixed boundary-policy IDs replace repeated policy prose; the complete brief is at most 3072 bytes, and unsupported modes, invalid flag values, repeated single-value flags, or a detail mode without --agent-envelope fail before input is read. Reports and envelopes remain derived route guidance and never become requirement, proof, execution, merge, release, rollout, deployment, or readiness authority.", + "invariant": "Agent-route output contract v3 derives every report, brief packet, and full envelope from one fully admitted immutable route input and one command-owned route report: route artifact refs in both available inputs and observed reports identify materialized caller-owned artifacts and never use the stdin transport sentinel; a bare --agent-envelope and explicit brief mode emit the same deterministic schema-v1 proofkit.agent-route.brief root while explicit full mode preserves the prior generic envelope; a routed brief selects exactly the first canonical nextCommands entry as its sole nextAction, retains its exact command and argv identity when the 3072-byte pretty stable-JSON bound permits, otherwise removes only inline argv and marks argvState detail_required, while every blocked state emits null nextAction; blockers are projected in deterministic required-input then sorted non-passed observed-report order, retain at most four typed entries, and report the exact omitted remainder; contextRefs refer to all and only caller-owned artifact operands of the selected command through resolvable source-report JSON pointers; one top-level detailAccess record binds the source report ID and stable digest, provides exact report and full output-argument suffixes for replay with the original input, and no per-item detail reference is added; omissionSummary exactly counts unselected available commands, source-omitted commands, and omitted blockers; fixed uniquely resolvable shipped requirement IDs replace repeated boundary-policy prose; the complete brief is at most 3072 bytes, and unsupported modes, invalid flag values, repeated single-value flags, or a detail mode without --agent-envelope fail before input is read. Reports and envelopes remain derived route guidance and never become requirement, proof, execution, merge, release, rollout, deployment, or readiness authority.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], diff --git a/internal/app/agent_route_version_edge_test.go b/internal/app/agent_route_version_edge_test.go index 5682c8d..e5df835 100644 --- a/internal/app/agent_route_version_edge_test.go +++ b/internal/app/agent_route_version_edge_test.go @@ -55,8 +55,10 @@ func TestAgentRouteVersionEdgeClosesBriefDefaultMigration(t *testing.T) { {name: "previous input contract", mutate: func(value *agentRouteVersionEdge) { value.ChangedCommandContract.PreviousInputContractSHA256 += "0" }}, {name: "previous output contract", mutate: func(value *agentRouteVersionEdge) { value.ChangedCommandContract.PreviousOutputContractSHA256 += "0" }}, {name: "breaking owner", mutate: func(value *agentRouteVersionEdge) { value.BreakingChangeIDs[0] += ".drift" }}, + {name: "missing breaking owner", mutate: func(value *agentRouteVersionEdge) { value.BreakingChangeIDs = value.BreakingChangeIDs[:1] }}, {name: "addition owner", mutate: func(value *agentRouteVersionEdge) { value.AdditionChangeIDs[0] += ".drift" }}, {name: "migration", mutate: func(value *agentRouteVersionEdge) { value.MigrationSteps[0] += " Drift." }}, + {name: "missing migration", mutate: func(value *agentRouteVersionEdge) { value.MigrationSteps = value.MigrationSteps[:1] }}, } for _, mutant := range mutants { t.Run(mutant.name, func(t *testing.T) { @@ -103,23 +105,26 @@ func validateAgentRouteVersionEdge(record agentRouteVersionEdge) error { if record.PreviousVersion != "0.5.1" || record.Version != "0.6.0" { return fmt.Errorf("agent-route version-edge release identity is stale") } - if record.PreviousPublicABISHA256 != "sha256:9ecd2c3d2f3f360088409f7e91cce406fc1d1d6edda1b404fce119985c4fb623" || record.CurrentPublicABISHA256 != "sha256:39f9f4314eec9e4d5baad8baf0eecbed5063d3830f074977df73ab934cafb277" || record.PreviousPublicABISHA256 == record.CurrentPublicABISHA256 { + if record.PreviousPublicABISHA256 != "sha256:9ecd2c3d2f3f360088409f7e91cce406fc1d1d6edda1b404fce119985c4fb623" || record.CurrentPublicABISHA256 != "sha256:3dc6dead6d21c6a463426594c2e10e2b9dceffd0d16a08ed0fb9ebe2e679e0c0" || record.PreviousPublicABISHA256 == record.CurrentPublicABISHA256 { return fmt.Errorf("agent-route version-edge ABI identity is invalid") } wantContract := agentRouteChangedCommandContract{ Command: "agent-route", PreviousInputContractSHA256: "sha256:6b5af8287f2972bbef4c68c247f43fb16d0f0d8739e5e6d3a66543af20d2644d", - CurrentInputContractSHA256: "sha256:285eaeb48845d41357cd3fc131ebb16fe11cd876bca92e9eba94dad14268acfd", + CurrentInputContractSHA256: "sha256:60bafcd877e0bd26bb9fbd37343ba7e19d61d314afbd7062754c8574ffd36c41", PreviousOutputContractSHA256: "sha256:44ec313a43360b6138ad6c3ae5de4abd51bbf312060880c108a6351606695915", - CurrentOutputContractSHA256: "sha256:1718051d01ebae24922baac191c9e43b281007b4ee502b9a391dfd0aa63b0039", + CurrentOutputContractSHA256: "sha256:e160ea2e5531b6675f7c4edfb01a2fb0d32fd7c74189e9c38774c41a211dc29e", } if record.ChangedCommandContract != wantContract { return fmt.Errorf("agent-route version-edge changed command contract is not exact") } - if !slices.Equal(record.BreakingChangeIDs, []string{"proofkit.agent-route.brief-default"}) || !slices.Equal(record.AdditionChangeIDs, []string{"proofkit.agent-route.envelope-detail-mode"}) { + if !slices.Equal(record.BreakingChangeIDs, []string{"proofkit.agent-route.brief-default", "proofkit.agent-route.materialized-artifact-refs"}) || !slices.Equal(record.AdditionChangeIDs, []string{"proofkit.agent-route.envelope-detail-mode"}) { return fmt.Errorf("agent-route version-edge change owners are not exact") } - if !slices.Equal(record.MigrationSteps, []string{"Consumers that require the former generic agent-route envelope must add --agent-envelope-mode full after --agent-envelope; consumers that accept bounded route guidance may keep bare --agent-envelope."}) { + if !slices.Equal(record.MigrationSteps, []string{ + "Consumers that require the former generic agent-route envelope must add --agent-envelope-mode full after --agent-envelope; consumers that accept bounded route guidance may keep bare --agent-envelope.", + "Consumers that used the stdin transport sentinel as an agent-route availableInputs or observedReports ref must materialize that artifact and pass its safe repo-relative path instead.", + }) { return fmt.Errorf("agent-route version-edge migration is not exact") } if !slices.Equal(record.NonClaims, []string{"This owner-authored version-edge observation binds reviewed public contract identities; it does not authenticate Git history, registry publication, provider ingestion, native witness truth, rollout, or production readiness."}) { diff --git a/internal/app/app_test.go b/internal/app/app_test.go index c386ffc..5565140 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -638,6 +638,29 @@ func TestCLIRejectsUnadvertisedFlagsWithoutStdout(t *testing.T) { } } +func TestAgentRouteModeAdmissionPrecedesInputRead(t *testing.T) { + cases := []struct { + name string + args []string + want string + }{ + {name: "mode without envelope", args: []string{"agent-route", "--input", "-", "--agent-envelope-mode", "full"}, want: "--agent-envelope-mode requires --agent-envelope"}, + {name: "invalid mode", args: []string{"agent-route", "--input", "-", "--agent-envelope", "--agent-envelope-mode", "expanded"}, want: "--agent-envelope-mode requires one of: brief, full"}, + {name: "repeated mode", args: []string{"agent-route", "--input", "-", "--agent-envelope", "--agent-envelope-mode", "brief", "--agent-envelope-mode", "full"}, want: "--agent-envelope-mode may be specified only once"}, + {name: "unsupported flag", args: []string{"agent-route", "--input", "-", "--format", "json"}, want: "unsupported argument for agent-route: --format"}, + } + for _, item := range cases { + t.Run(item.name, func(t *testing.T) { + var stdout bytes.Buffer + var stderr bytes.Buffer + status := Run(t.Context(), item.args, panicReader{}, &stdout, &stderr) + if status != 1 || stdout.Len() != 0 || !strings.Contains(stderr.String(), item.want) { + t.Fatalf("status=%d stdout=%q stderr=%q want=%q", status, stdout.String(), stderr.String(), item.want) + } + }) + } +} + func TestCLIDiagnosticsRedactSecretLikeCallerLabels(t *testing.T) { secret := "ghp_FAKEFAKE1234567890" cases := []struct { diff --git a/internal/app/cli_abi_test.go b/internal/app/cli_abi_test.go index 43aac94..c7f9397 100644 --- a/internal/app/cli_abi_test.go +++ b/internal/app/cli_abi_test.go @@ -2472,6 +2472,27 @@ func TestAgentRouteEnvelopeModesUseExactRootShapes(t *testing.T) { if detail["sourceReportDigest"] != wantDigest || detail["sourceReportId"] != report["reportId"] { t.Fatalf("brief detail access does not resolve the exact route report: %#v", detail) } + outputArgs := jsonObject(t, detail["outputArgs"]) + for outputKind, outputName := range map[string]string{"report": "route report", "full": "explicit full envelope"} { + rawArgs, ok := outputArgs[outputKind].([]any) + if !ok { + t.Fatalf("detail outputArgs.%s must be an array: %#v", outputKind, outputArgs[outputKind]) + } + args := []string{"agent-route", "--input", "-"} + for _, rawArg := range rawArgs { + arg, ok := rawArg.(string) + if !ok { + t.Fatalf("detail outputArgs.%s contains non-string argument", outputKind) + } + args = append(args, arg) + } + var replayStdout bytes.Buffer + var replayStderr bytes.Buffer + status := Run(t.Context(), args, strings.NewReader(valid), &replayStdout, &replayStderr) + if status != 0 || replayStderr.Len() != 0 || !bytes.Equal(replayStdout.Bytes(), outputs[outputName]) { + t.Fatalf("detail outputArgs.%s did not reproduce %s: status=%d stdout=%s stderr=%s", outputKind, outputName, status, replayStdout.String(), replayStderr.String()) + } + } sourceReport := jsonObject(t, full["sourceReport"]) if sourceReport["reportId"] != report["reportId"] { t.Fatalf("full envelope does not resolve the same route report: %#v", sourceReport) diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index e9fef60..eef6526 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -23,7 +23,7 @@ import ( ) const ( - cliContractPublicABISHA256 = "39f9f4314eec9e4d5baad8baf0eecbed5063d3830f074977df73ab934cafb277" + cliContractPublicABISHA256 = "3dc6dead6d21c6a463426594c2e10e2b9dceffd0d16a08ed0fb9ebe2e679e0c0" maxAggregateFileReadBytesForContractTest = 64 << 20 maxPackageManifestBytesForContractTest = 256 << 10 maxSourceFileBytesForContractTest = 8 << 20 diff --git a/internal/app/command_contract_generated.go b/internal/app/command_contract_generated.go index e66519a..bcd735e 100644 --- a/internal/app/command_contract_generated.go +++ b/internal/app/command_contract_generated.go @@ -1,7 +1,7 @@ // Code generated by internal/tools/commandcontractgen; DO NOT EDIT. package app -const commandContractSourceSHA256 = "3a21c10a7bc1dc7246c420096dd07b6d9783384e386c5c03c214902c6cf3ce5b" +const commandContractSourceSHA256 = "28c8f4128ba07378e705693640d42e7c0867043b4c053b3f8874401b5d5d3284" type generatedCommandContractMetadata struct { InputContractSHA256 string @@ -15,7 +15,7 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "adoption-contract-envelope": {InputContractSHA256: "sha256:c310214676ff4b6f536a5bc9d687f681a7e71f73d7a03ac932707d8cd3905cdf", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.adoption-contract-envelope.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3efb2c5161fee16fd8ac6a40dcb6d9c41fbc23e468f60621436ae9e8076e0950", FlagChoices: map[string][]string{}}, "adoption-doctor": {InputContractSHA256: "sha256:efa9acfe32bff07f56d9dc9902530df2979794289bc2f7f547f7a108a7dd0f35", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.adoption-doctor.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:8fdfc6608f197e633f042f20031ae1014872a90aa3daa66885ffcaddca994766", FlagChoices: map[string][]string{}}, "adoption-workflow-plan": {InputContractSHA256: "sha256:b32ae67179d7b6dcf1ea66cb6b2b2691c8367ce2e2be367619b65973166da55c", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.adoption-workflow-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:8d64cb53ebd0307e3cebc3435286a3d2a1ee8a0ad6f7514fc0fb3285db0f565b", FlagChoices: map[string][]string{}}, - "agent-route": {InputContractSHA256: "sha256:285eaeb48845d41357cd3fc131ebb16fe11cd876bca92e9eba94dad14268acfd", InputSchemaSummary: []string{"availableInputs", "browserMode", "goal", "knownChangedPaths", "mode", "nonClaims", "observedReports", "openBrowser", "routeId", "schemaVersion", "root-shape-only definition proofkit.agent-route.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:1718051d01ebae24922baac191c9e43b281007b4ee502b9a391dfd0aa63b0039", FlagChoices: map[string][]string{}}, + "agent-route": {InputContractSHA256: "sha256:43f38f48909d4d99496c90086d271e796a1a90c6fcd9984f7a22728c39538394", InputSchemaSummary: []string{"availableInputs", "browserMode", "goal", "knownChangedPaths", "mode", "nonClaims", "observedReports", "openBrowser", "routeId", "schemaVersion", "root-shape-only definition proofkit.agent-route.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:75a1313265c401babf4b67065767f7ac75339762b19a1dc557b4c4a66d9f9d02", FlagChoices: map[string][]string{}}, "binding-partition": {InputContractSHA256: "sha256:366ad082045af52b2ac6604f18626d0f285b2db73b45d9a82687b8d3b0d2b3fd", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.binding-partition.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:52840879e13a00ef9a4abaad6cdb33000511674d5f9003fb56f387fdf58fadc8", FlagChoices: map[string][]string{}}, "branch-authority": {InputContractSHA256: "sha256:8a3ed74978898593fbdbf1f7fa684dae450fbd9019edcd60d07f818d63363ed4", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.branch-authority.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3c7dc74842299b92cd5baf57cc8666e9415963091359e5faf654e28da89561f1", FlagChoices: map[string][]string{}}, "capability-map-admission": {InputContractSHA256: "sha256:efe87497cf6a32cfd1f9c9c191393cac8781d1e9211914a8a7b6aaa44e7f371d", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.capability-map-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:bfb32535fd51bed168111912acb1651527897fe59a592119dc0032ce9286c9db", FlagChoices: map[string][]string{}}, diff --git a/internal/app/testdata/v0.6-wire-observations.json b/internal/app/testdata/v0.6-wire-observations.json index bbf438b..21f6884 100644 --- a/internal/app/testdata/v0.6-wire-observations.json +++ b/internal/app/testdata/v0.6-wire-observations.json @@ -5,22 +5,24 @@ "version": "0.6.0", "evidenceClass": "owner_authored_frozen_version_edge_observation", "previousPublicAbiSha256": "sha256:9ecd2c3d2f3f360088409f7e91cce406fc1d1d6edda1b404fce119985c4fb623", - "currentPublicAbiSha256": "sha256:39f9f4314eec9e4d5baad8baf0eecbed5063d3830f074977df73ab934cafb277", + "currentPublicAbiSha256": "sha256:3dc6dead6d21c6a463426594c2e10e2b9dceffd0d16a08ed0fb9ebe2e679e0c0", "changedCommandContract": { "command": "agent-route", "previousInputContractSha256": "sha256:6b5af8287f2972bbef4c68c247f43fb16d0f0d8739e5e6d3a66543af20d2644d", - "currentInputContractSha256": "sha256:285eaeb48845d41357cd3fc131ebb16fe11cd876bca92e9eba94dad14268acfd", + "currentInputContractSha256": "sha256:60bafcd877e0bd26bb9fbd37343ba7e19d61d314afbd7062754c8574ffd36c41", "previousOutputContractSha256": "sha256:44ec313a43360b6138ad6c3ae5de4abd51bbf312060880c108a6351606695915", - "currentOutputContractSha256": "sha256:1718051d01ebae24922baac191c9e43b281007b4ee502b9a391dfd0aa63b0039" + "currentOutputContractSha256": "sha256:e160ea2e5531b6675f7c4edfb01a2fb0d32fd7c74189e9c38774c41a211dc29e" }, "breakingChangeIds": [ - "proofkit.agent-route.brief-default" + "proofkit.agent-route.brief-default", + "proofkit.agent-route.materialized-artifact-refs" ], "additionChangeIds": [ "proofkit.agent-route.envelope-detail-mode" ], "migrationSteps": [ - "Consumers that require the former generic agent-route envelope must add --agent-envelope-mode full after --agent-envelope; consumers that accept bounded route guidance may keep bare --agent-envelope." + "Consumers that require the former generic agent-route envelope must add --agent-envelope-mode full after --agent-envelope; consumers that accept bounded route guidance may keep bare --agent-envelope.", + "Consumers that used the stdin transport sentinel as an agent-route availableInputs or observedReports ref must materialize that artifact and pass its safe repo-relative path instead." ], "nonClaims": [ "This owner-authored version-edge observation binds reviewed public contract identities; it does not authenticate Git history, registry publication, provider ingestion, native witness truth, rollout, or production readiness." diff --git a/internal/command/agentroute/agentroute.go b/internal/command/agentroute/agentroute.go index ab42141..3be958e 100644 --- a/internal/command/agentroute/agentroute.go +++ b/internal/command/agentroute/agentroute.go @@ -476,7 +476,7 @@ func InputContract() map[string]any { "default": []any{}, "item": map[string]any{ "kind": map[string]any{"enum": sortedKeys(reportKindValues)}, - "ref": map[string]any{"format": "safe repo-relative report ref"}, + "ref": map[string]any{"format": "safe repo-relative materialized caller-owned report ref; stdin sentinel - is forbidden"}, "state": map[string]any{"enum": sortedKeys(reportStateValues)}, }, "blockingSemantics": "any observed report state other than passed blocks command emission", @@ -501,6 +501,7 @@ func OutputContract() map[string]any { "schemaVersion": 3, "authority": "deterministic route report, brief packet, full envelope, or invalid-input repair packet selected from the admitted invocation", "briefPacketContract": map[string]any{ + "boundaryPolicyRefs": briefBoundaryPolicyRefs(), "contractId": "proofkit.agent-route.brief.v1", "schemaVersion": 1, "maxPrettyJSONBytes": maxAgentBriefBytes, @@ -520,9 +521,9 @@ func OutputContract() map[string]any { }, "fieldRules": map[string]any{ "blockers": "required-input blockers followed by sorted non-passed observed-report blockers; retain at most four and count the exact remainder", - "boundaryPolicyRefs": "exactly NC-PROOFKIT-SPEC-005 and NC-PROOFKIT-SPEC-026; policy prose is not duplicated", + "boundaryPolicyRefs": "exactly the shipped and uniquely resolvable requirement IDs REQ-PROOFKIT-SPEC-005 and REQ-PROOFKIT-SPEC-026; policy prose is not duplicated", "contextRefs": "only caller-owned artifact operands of the selected next command, each addressed by an RFC 6901 pointer into the source report", - "detailAccess": "binds source report ID and stable digest and advertises report and full retrieval from the original admitted input", + "detailAccess": "binds source report ID and stable digest and provides exact outputArgs suffixes for report and full retrieval from the original admitted input", "nextAction": "null for blocked states; otherwise the first canonical nextCommands entry with exact command identity and inline argv unless the byte bound requires argvState=detail_required", "omissionSummary": "exact counts for unselected available commands, source-omitted commands, and blockers omitted by the packet bound", "packetKind": "proofkit.agent-route.brief", @@ -663,10 +664,15 @@ func admitAvailableInputs(raw any) (map[string]string, error) { } func admitAvailableInputRef(kind string, value string, context string) (string, error) { + allowRepoRoot := kind == "typescript_public_api_repo_root" || kind == "requirement_context_repo_root" + return admitMaterializedRouteRef(value, context, allowRepoRoot) +} + +func admitMaterializedRouteRef(value string, context string, allowRepoRoot bool) (string, error) { if value == "-" { return "", fmt.Errorf("%s must identify a materialized caller-owned artifact, not the stdin transport sentinel", context) } - if (kind == "typescript_public_api_repo_root" || kind == "requirement_context_repo_root") && value == "." { + if allowRepoRoot && value == "." { return value, nil } return admit.SafeRepoRelativePath(value, context) @@ -701,7 +707,7 @@ func admitObservedReports(raw any) ([]observedReport, error) { if err != nil { return nil, err } - ref, err := admit.SafeRepoRelativePath(refText, fmt.Sprintf("agent route observedReports[%d].ref", index)) + ref, err := admitMaterializedRouteRef(refText, fmt.Sprintf("agent route observedReports[%d].ref", index), false) if err != nil { return nil, err } diff --git a/internal/command/agentroute/agentroute_test.go b/internal/command/agentroute/agentroute_test.go index 496b19f..343aaa3 100644 --- a/internal/command/agentroute/agentroute_test.go +++ b/internal/command/agentroute/agentroute_test.go @@ -88,17 +88,27 @@ func TestBuildRoutesRequirementSourceAndBlocksUnknownGoal(t *testing.T) { func TestBuildRejectsStdinTransportSentinelAsArtifactReference(t *testing.T) { t.Parallel() - _, exitCode, err := Build(map[string]any{ - "schemaVersion": jsonNumber("1"), - "routeId": "consumer.route.stdin-sentinel", - "goal": "validate_requirement_source", - "mode": "observe", - "availableInputs": []any{ - map[string]any{"kind": "requirement_source", "ref": "-"}, - }, - }) - if err == nil || exitCode != 1 || !strings.Contains(err.Error(), "stdin transport sentinel") { - t.Fatalf("stdin sentinel exit=%d error=%v", exitCode, err) + for _, test := range []struct { + name string + field string + value []any + }{ + {name: "available input", field: "availableInputs", value: []any{map[string]any{"kind": "requirement_source", "ref": "-"}}}, + {name: "observed report", field: "observedReports", value: []any{map[string]any{"kind": "requirement_source", "ref": "-", "state": "warning"}}}, + } { + t.Run(test.name, func(t *testing.T) { + input := map[string]any{ + "schemaVersion": jsonNumber("1"), + "routeId": "consumer.route.stdin-sentinel", + "goal": "validate_requirement_source", + "mode": "observe", + test.field: test.value, + } + _, exitCode, err := Build(input) + if err == nil || exitCode != 1 || !strings.Contains(err.Error(), "stdin transport sentinel") { + t.Fatalf("stdin sentinel exit=%d error=%v", exitCode, err) + } + }) } } diff --git a/internal/command/agentroute/brief.go b/internal/command/agentroute/brief.go index 5323e98..0b14f75 100644 --- a/internal/command/agentroute/brief.go +++ b/internal/command/agentroute/brief.go @@ -13,11 +13,35 @@ const ( EnvelopeModeBrief EnvelopeMode = "brief" EnvelopeModeFull EnvelopeMode = "full" - maxAgentBriefBytes = 3072 - maxBriefBlockerItems = 4 + briefBoundaryPolicyDerivedView = "REQ-PROOFKIT-SPEC-005" + briefBoundaryPolicyRoutePacket = "REQ-PROOFKIT-SPEC-026" + maxAgentBriefBytes = 3072 + maxBriefBlockerItems = 4 ) func AgentBrief(report map[string]any) (map[string]any, error) { + packet, err := projectAgentBrief(report) + if err != nil { + return nil, err + } + encoded, err := stablejson.Marshal(packet) + if err != nil { + return nil, fmt.Errorf("serialize agent-route brief: %w", err) + } + if len(encoded) > maxAgentBriefBytes { + compactBriefAction(packet) + encoded, err = stablejson.Marshal(packet) + if err != nil { + return nil, fmt.Errorf("serialize compacted agent-route brief: %w", err) + } + } + if len(encoded) > maxAgentBriefBytes { + return nil, fmt.Errorf("agent-route brief exceeds %d bytes", maxAgentBriefBytes) + } + return packet, nil +} + +func projectAgentBrief(report map[string]any) (map[string]any, error) { reportID := stringFromMap(report, "reportId") if reportID == "" { reportID = "proofkit.agent-route.unknown" @@ -29,13 +53,13 @@ func AgentBrief(report map[string]any) (map[string]any, error) { nextCommands := mapsFromAny(report["nextCommands"]) blockers, omittedBlockerCount := briefBlockers(report, reportID) - packet := map[string]any{ + return map[string]any{ "blockers": blockers, - "boundaryPolicyRefs": []any{"NC-PROOFKIT-SPEC-005", "NC-PROOFKIT-SPEC-026"}, + "boundaryPolicyRefs": briefBoundaryPolicyRefs(), "contextRefs": briefContextRefs(nextCommands, reportID), "detailAccess": map[string]any{ - "availableModes": []any{"full", "report"}, "commandRef": "agent-route", + "outputArgs": briefDetailOutputArgs(), "requiresOriginalInput": true, "sourceReportDigest": reportDigest, "sourceReportId": reportID, @@ -47,22 +71,18 @@ func AgentBrief(report map[string]any) (map[string]any, error) { "routeFamily": stringFromMap(report, "selectedRouteFamily"), "schemaVersion": 1, "state": stringFromMap(report, "state"), + }, nil +} + +func briefBoundaryPolicyRefs() []any { + return []any{briefBoundaryPolicyDerivedView, briefBoundaryPolicyRoutePacket} +} + +func briefDetailOutputArgs() map[string]any { + return map[string]any{ + "full": []any{"--agent-envelope", "--agent-envelope-mode", "full"}, + "report": []any{}, } - encoded, err := stablejson.Marshal(packet) - if err != nil { - return nil, fmt.Errorf("serialize agent-route brief: %w", err) - } - if len(encoded) > maxAgentBriefBytes { - compactBriefAction(packet) - encoded, err = stablejson.Marshal(packet) - if err != nil { - return nil, fmt.Errorf("serialize compacted agent-route brief: %w", err) - } - } - if len(encoded) > maxAgentBriefBytes { - return nil, fmt.Errorf("agent-route brief exceeds %d bytes", maxAgentBriefBytes) - } - return packet, nil } func briefNextAction(commands []map[string]any, reportID string) any { diff --git a/internal/command/agentroute/brief_test.go b/internal/command/agentroute/brief_test.go index 30ee5eb..dd94b61 100644 --- a/internal/command/agentroute/brief_test.go +++ b/internal/command/agentroute/brief_test.go @@ -1,6 +1,7 @@ package agentroute import ( + "slices" "strings" "testing" @@ -63,6 +64,13 @@ func TestAgentBriefIsBoundedAndFullEnvelopeRemainsAvailable(t *testing.T) { if detail["sourceReportDigest"] != wantDigest || detail["requiresOriginalInput"] != true { t.Fatalf("brief detail access is not source-bound: %#v", detail) } + outputArgs := detail["outputArgs"].(map[string]any) + if !slices.Equal(stringsFromAny(outputArgs["full"]), []string{"--agent-envelope", "--agent-envelope-mode", "full"}) || len(outputArgs["report"].([]any)) != 0 { + t.Fatalf("brief detail access is not executable: %#v", detail) + } + if !slices.Equal(stringsFromAny(brief["boundaryPolicyRefs"]), []string{briefBoundaryPolicyDerivedView, briefBoundaryPolicyRoutePacket}) { + t.Fatalf("brief boundary policy refs are not exact: %#v", brief["boundaryPolicyRefs"]) + } briefBytes, err := stablejson.Marshal(brief) if err != nil { @@ -201,6 +209,61 @@ func TestBuildEnvelopeCompactsOversizedArgvWithoutLosingActionIdentity(t *testin } } +func TestAgentBriefCompactsAtDeclaredByteBoundary(t *testing.T) { + t.Parallel() + + var boundaryReport map[string]any + var unboundedBytes int + for refRunes := 800; refRunes <= 2400; refRunes += 8 { + report, exitCode, err := Build(map[string]any{ + "schemaVersion": jsonNumber("1"), + "routeId": "consumer.route.boundary", + "goal": "validate_requirement_source", + "mode": "observe", + "availableInputs": []any{ + map[string]any{"kind": "requirement_source", "ref": "docs/specs/" + strings.Repeat("a", refRunes) + ".json"}, + }, + }) + if err != nil || exitCode != 0 { + t.Fatalf("Build() exit=%d error=%v", exitCode, err) + } + unbounded, err := projectAgentBrief(report) + if err != nil { + t.Fatal(err) + } + encoded, err := stablejson.Marshal(unbounded) + if err != nil { + t.Fatal(err) + } + if len(encoded) > maxAgentBriefBytes && len(encoded) <= maxAgentBriefBytes+1024 { + boundaryReport = report + unboundedBytes = len(encoded) + break + } + } + if boundaryReport == nil { + t.Fatal("failed to construct an admitted brief immediately above the declared byte bound") + } + brief, err := AgentBrief(boundaryReport) + if err != nil { + t.Fatal(err) + } + action := brief["nextAction"].(map[string]any) + if action["argvState"] != "detail_required" { + t.Fatalf("unbounded brief bytes=%d was not compacted at limit=%d: %#v", unboundedBytes, maxAgentBriefBytes, action) + } + if _, retained := action["argv"]; retained { + t.Fatal("boundary-compacted brief retained inline argv") + } + encoded, err := stablejson.Marshal(brief) + if err != nil { + t.Fatal(err) + } + if len(encoded) > maxAgentBriefBytes { + t.Fatalf("compacted brief bytes=%d exceed limit=%d", len(encoded), maxAgentBriefBytes) + } +} + func TestBuildEnvelopeCapsBlockersAndCountsOmittedDetails(t *testing.T) { t.Parallel() diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go index 1c89bd1..98ba02e 100644 --- a/internal/command/stackpreset/preset_ids_generated.go +++ b/internal/command/stackpreset/preset_ids_generated.go @@ -1,6 +1,6 @@ // Code generated by internal/tools/commandcontractgen; DO NOT EDIT. package stackpreset -const presetContractSourceSHA256 = "3a21c10a7bc1dc7246c420096dd07b6d9783384e386c5c03c214902c6cf3ce5b" +const presetContractSourceSHA256 = "28c8f4128ba07378e705693640d42e7c0867043b4c053b3f8874401b5d5d3284" var presetIDs = []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"} diff --git a/internal/tools/coveragemetrics/main.go b/internal/tools/coveragemetrics/main.go index 87a142f..53f9231 100644 --- a/internal/tools/coveragemetrics/main.go +++ b/internal/tools/coveragemetrics/main.go @@ -514,6 +514,7 @@ func validateRequiredBindingWitnessSelectors(bindings bindingFile) error { }, {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-brief-projection"}: { "TestAgentBriefClosesEverySelectedCommandInputReference", + "TestAgentBriefCompactsAtDeclaredByteBoundary", "TestAgentBriefIsBoundedAndFullEnvelopeRemainsAvailable", "TestAgentBriefNamesCompleteInputBundleBlocker", "TestBuildEnvelopeCapsBlockersAndCountsOmittedDetails", diff --git a/internal/tools/npmpack/report.go b/internal/tools/npmpack/report.go new file mode 100644 index 0000000..404a55e --- /dev/null +++ b/internal/tools/npmpack/report.go @@ -0,0 +1,49 @@ +package npmpack + +import ( + "fmt" + "io" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" +) + +type Record struct { + Filename string `json:"filename"` + ID string `json:"id,omitempty"` + Integrity string `json:"integrity"` + Name string `json:"name"` + Shasum string `json:"shasum"` + Version string `json:"version"` +} + +func DecodeNPM12Report(reader io.Reader, maxBytes int64) (Record, error) { + keyed, err := admission.DecodeTypedJSON[map[string]Record](reader, maxBytes) + if err != nil { + return Record{}, err + } + if len(keyed) != 1 { + return Record{}, fmt.Errorf("npm pack report must contain exactly one keyed record") + } + for key, record := range keyed { + if key != record.Name { + return Record{}, fmt.Errorf("npm pack report key must equal the record package name") + } + for _, field := range []struct { + name string + value string + }{ + {name: "filename", value: record.Filename}, + {name: "integrity", value: record.Integrity}, + {name: "name", value: record.Name}, + {name: "shasum", value: record.Shasum}, + {name: "version", value: record.Version}, + } { + if _, err := admit.NonEmptyText(field.value, "npm pack report "+field.name); err != nil { + return Record{}, err + } + } + return record, nil + } + return Record{}, fmt.Errorf("npm pack report must contain exactly one keyed record") +} diff --git a/internal/tools/npmpack/report_test.go b/internal/tools/npmpack/report_test.go new file mode 100644 index 0000000..d5d8d2c --- /dev/null +++ b/internal/tools/npmpack/report_test.go @@ -0,0 +1,56 @@ +package npmpack + +import ( + "strings" + "testing" +) + +func TestDecodeNPM12ReportRequiresOneIdentityBoundRecord(t *testing.T) { + valid := `{"@research-engineering/agentic-proofkit":{"filename":"package.tgz","integrity":"sha512-value","name":"@research-engineering/agentic-proofkit","shasum":"abc","version":"1.2.3"}}` + record, err := DecodeNPM12Report(strings.NewReader(valid), int64(len(valid))) + if err != nil { + t.Fatalf("DecodeNPM12Report(valid) error = %v", err) + } + if record.Name != "@research-engineering/agentic-proofkit" { + t.Fatalf("DecodeNPM12Report(valid) = %#v", record) + } + + for _, test := range []struct { + name string + value string + want string + }{ + { + name: "legacy array root", + value: `[{"filename":"package.tgz","integrity":"sha512-value","name":"@research-engineering/agentic-proofkit","shasum":"abc","version":"1.2.3"}]`, + want: "cannot unmarshal array", + }, + { + name: "mismatched key", + value: `{"other":{"filename":"package.tgz","integrity":"sha512-value","name":"@research-engineering/agentic-proofkit","shasum":"abc","version":"1.2.3"}}`, + want: "key must equal", + }, + { + name: "multiple records", + value: `{"first":{"filename":"first.tgz","integrity":"sha512-first","name":"first","shasum":"first","version":"1.0.0"},"second":{"filename":"second.tgz","integrity":"sha512-second","name":"second","shasum":"second","version":"1.0.0"}}`, + want: "exactly one", + }, + { + name: "duplicate key", + value: `{"first":{"name":"first"},"first":{"name":"first"}}`, + want: "duplicate object key", + }, + { + name: "missing required identity", + value: `{"first":{"filename":"first.tgz","integrity":"sha512-first","name":"first","shasum":"first"}}`, + want: "version must be non-empty text", + }, + } { + t.Run(test.name, func(t *testing.T) { + _, err := DecodeNPM12Report(strings.NewReader(test.value), int64(len(test.value))) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("DecodeNPM12Report() error = %v, want %q", err, test.want) + } + }) + } +} diff --git a/internal/tools/npmregistry/main.go b/internal/tools/npmregistry/main.go index f907e7d..595d499 100644 --- a/internal/tools/npmregistry/main.go +++ b/internal/tools/npmregistry/main.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -14,6 +15,7 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/kernel/releasechannel" "github.com/research-engineering/agentic-proofkit/internal/kernel/trustedpublisher" "github.com/research-engineering/agentic-proofkit/internal/kernel/unicodepolicy" + "github.com/research-engineering/agentic-proofkit/internal/tools/npmpack" ) const ( @@ -43,12 +45,55 @@ type registryArtifactSet struct { } func main() { - if err := run("."); err != nil { + var err error + if len(os.Args) > 1 && os.Args[1] == "capture-pack-reports" { + if len(os.Args) < 4 { + err = fmt.Errorf("capture-pack-reports requires an output path and at least one input report") + } else { + err = capturePackReports(os.Args[2], os.Args[3:]) + } + } else if len(os.Args) != 1 { + err = fmt.Errorf("unsupported npm registry evidence argument") + } else { + err = run(".") + } + if err != nil { diagnostic.WriteError(os.Stderr, err) os.Exit(1) } } +func capturePackReports(outputPath string, inputPaths []string) error { + records := make([]npmpack.Record, 0, len(inputPaths)) + seenNames := map[string]struct{}{} + seenFiles := map[string]struct{}{} + for _, inputPath := range inputPaths { + file, err := os.Open(inputPath) + if err != nil { + return err + } + record, decodeErr := npmpack.DecodeNPM12Report(file, maxPackRecordBytes) + closeErr := file.Close() + err = errors.Join(decodeErr, closeErr) + if err != nil { + return fmt.Errorf("admit npm pack report: %w", err) + } + if _, duplicate := seenNames[record.Name]; duplicate { + return fmt.Errorf("npm pack reports contain duplicate package name") + } + if _, duplicate := seenFiles[record.Filename]; duplicate { + return fmt.Errorf("npm pack reports contain duplicate package filename") + } + seenNames[record.Name] = struct{}{} + seenFiles[record.Filename] = struct{}{} + records = append(records, record) + } + sort.Slice(records, func(left int, right int) bool { + return records[left].Name < records[right].Name + }) + return writeJSON(outputPath, records) +} + func run(root string) error { local, err := readPackRecords(filepath.Join(root, "artifacts", "package", "npm-pack.json"), "local npm package evidence") if err != nil { diff --git a/internal/tools/npmregistry/main_test.go b/internal/tools/npmregistry/main_test.go index 98f3589..fefa721 100644 --- a/internal/tools/npmregistry/main_test.go +++ b/internal/tools/npmregistry/main_test.go @@ -52,6 +52,51 @@ func TestRunRejectsRegistryPackageSetSubstitution(t *testing.T) { } } +func TestCapturePackReportsNormalizesNPM12KeyedOutput(t *testing.T) { + root := t.TempDir() + first := filepath.Join(root, "pack-first.json") + second := filepath.Join(root, "pack-second.json") + output := filepath.Join(root, "npm-pack.json") + writeFixture(t, first, `{"second":{"filename":"second.tgz","integrity":"sha512-second","name":"second","shasum":"second","version":"1.0.0"}}`) + writeFixture(t, second, `{"first":{"filename":"first.tgz","integrity":"sha512-first","name":"first","shasum":"first","version":"1.0.0"}}`) + if err := capturePackReports(output, []string{first, second}); err != nil { + t.Fatalf("capturePackReports() error=%v", err) + } + content, err := os.ReadFile(output) + if err != nil { + t.Fatal(err) + } + var records []map[string]any + if err := json.Unmarshal(content, &records); err != nil { + t.Fatal(err) + } + if len(records) != 2 || records[0]["name"] != "first" || records[1]["name"] != "second" { + t.Fatalf("normalized records=%#v", records) + } +} + +func TestCapturePackReportsRejectsLegacyAndAmbiguousOutput(t *testing.T) { + for _, test := range []struct { + name string + value string + want string + }{ + {name: "legacy array", value: `[{"name":"first"}]`, want: "cannot unmarshal array"}, + {name: "multiple keyed records", value: `{"first":{"name":"first"},"second":{"name":"second"}}`, want: "exactly one"}, + {name: "key mismatch", value: `{"other":{"filename":"first.tgz","integrity":"sha512-first","name":"first","shasum":"first","version":"1.0.0"}}`, want: "key must equal"}, + } { + t.Run(test.name, func(t *testing.T) { + root := t.TempDir() + input := filepath.Join(root, "pack.json") + writeFixture(t, input, test.value) + err := capturePackReports(filepath.Join(root, "normalized.json"), []string{input}) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("capturePackReports() error=%v want=%q", err, test.want) + } + }) + } +} + func writeFixture(t *testing.T, path string, content string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { diff --git a/internal/tools/packagepack/main.go b/internal/tools/packagepack/main.go index 32fda86..5c90310 100644 --- a/internal/tools/packagepack/main.go +++ b/internal/tools/packagepack/main.go @@ -9,19 +9,10 @@ import ( "path/filepath" "sort" - "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" "github.com/research-engineering/agentic-proofkit/internal/kernel/diagnostic" + "github.com/research-engineering/agentic-proofkit/internal/tools/npmpack" ) -type packRecord struct { - Filename string `json:"filename"` - ID string `json:"id,omitempty"` - Integrity string `json:"integrity"` - Name string `json:"name"` - Shasum string `json:"shasum"` - Version string `json:"version"` -} - func main() { if err := run(); err != nil { diagnostic.WriteError(os.Stderr, err) @@ -37,7 +28,7 @@ func run() error { if err := os.MkdirAll(packageDir, 0o755); err != nil { return err } - records := []packRecord{} + records := []npmpack.Record{} rootRecords, err := npmPack(".") if err != nil { return err @@ -53,7 +44,7 @@ func run() error { return os.WriteFile(filepath.Join(packageDir, "npm-pack.json"), append(content, '\n'), 0o644) } -func npmPack(packageRoot string) ([]packRecord, error) { +func npmPack(packageRoot string) ([]npmpack.Record, error) { command := exec.Command("npm", "--silent", "pack", "--json", "--pack-destination", filepath.Join("artifacts", "package"), packageRoot) stderr := diagnostic.NewStderrCapture() command.Stderr = stderr @@ -64,26 +55,9 @@ func npmPack(packageRoot string) ([]packRecord, error) { } return nil, fmt.Errorf("npm pack %s: %w", packageRoot, err) } - records, err := decodeNPM12PackOutput(output) + record, err := npmpack.DecodeNPM12Report(bytes.NewReader(output), int64(len(output))) if err != nil { return nil, fmt.Errorf("parse npm pack output for %s: %w", packageRoot, err) } - return records, nil -} - -func decodeNPM12PackOutput(output []byte) ([]packRecord, error) { - keyed, err := admission.DecodeTypedJSON[map[string]packRecord](bytes.NewReader(output), int64(len(output))) - if err != nil { - return nil, err - } - if len(keyed) != 1 { - return nil, fmt.Errorf("npm pack output must contain exactly one keyed record") - } - for key, record := range keyed { - if key != record.Name { - return nil, fmt.Errorf("npm pack output key must equal the record package name") - } - return []packRecord{record}, nil - } - return nil, fmt.Errorf("npm pack output must contain exactly one keyed record") + return []npmpack.Record{record}, nil } diff --git a/internal/tools/packagepack/main_test.go b/internal/tools/packagepack/main_test.go deleted file mode 100644 index 459ffd7..0000000 --- a/internal/tools/packagepack/main_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "strings" - "testing" -) - -func TestDecodeNPM12PackOutputRequiresOneIdentityBoundRecord(t *testing.T) { - valid := []byte("{\"@research-engineering/agentic-proofkit\":{\"filename\":\"package.tgz\",\"integrity\":\"sha512-value\",\"name\":\"@research-engineering/agentic-proofkit\",\"shasum\":\"abc\",\"version\":\"1.2.3\"}}") - records, err := decodeNPM12PackOutput(valid) - if err != nil { - t.Fatalf("decodeNPM12PackOutput(valid) error = %v", err) - } - if len(records) != 1 || records[0].Name != "@research-engineering/agentic-proofkit" { - t.Fatalf("decodeNPM12PackOutput(valid) = %#v", records) - } - - for _, test := range []struct { - name string - value string - want string - }{ - { - name: "legacy array root", - value: "[{\"filename\":\"package.tgz\",\"integrity\":\"sha512-value\",\"name\":\"@research-engineering/agentic-proofkit\",\"shasum\":\"abc\",\"version\":\"1.2.3\"}]", - want: "cannot unmarshal array", - }, - { - name: "mismatched key", - value: "{\"other\":{\"filename\":\"package.tgz\",\"integrity\":\"sha512-value\",\"name\":\"@research-engineering/agentic-proofkit\",\"shasum\":\"abc\",\"version\":\"1.2.3\"}}", - want: "key must equal", - }, - { - name: "multiple records", - value: "{\"first\":{\"name\":\"first\"},\"second\":{\"name\":\"second\"}}", - want: "exactly one", - }, - { - name: "duplicate key", - value: "{\"first\":{\"name\":\"first\"},\"first\":{\"name\":\"first\"}}", - want: "duplicate object key", - }, - } { - t.Run(test.name, func(t *testing.T) { - _, err := decodeNPM12PackOutput([]byte(test.value)) - if err == nil || !strings.Contains(err.Error(), test.want) { - t.Fatalf("decodeNPM12PackOutput() error = %v, want %q", err, test.want) - } - }) - } -} diff --git a/internal/tools/packageverify/main.go b/internal/tools/packageverify/main.go index 01e410b..a4e56c7 100644 --- a/internal/tools/packageverify/main.go +++ b/internal/tools/packageverify/main.go @@ -949,7 +949,11 @@ func verifyPackagePublicReferenceClosure(artifact rootPackageArtifact, entries m if err := verifyReceiptPolicyReferences(textEntries["package/proofkit/receipt-producer-policy.json"], entries); err != nil { return err } - return verifyCLIContractSourceClassifications(textEntries["package/proofkit/cli-contract.v2.json"], entries) + cliContract := textEntries["package/proofkit/cli-contract.v2.json"] + if err := verifyCLIContractSourceClassifications(cliContract, entries); err != nil { + return err + } + return verifyCLIContractBoundaryPolicyClosure(cliContract, textEntries) } const readmePreOneExactPinPolicy = "Pre-1.0 releases may contain owner-declared breaking changes, so npm consumers\nmust retain the exact saved version instead of replacing it with a version\nrange." @@ -1315,6 +1319,7 @@ func verifyCLIContractSourceClassifications(content string, entries map[string]s "/commands/*/outputContract/nativeSource/path": "source_checkout", "/commands/*/outputContract/nativeSources/*/path": "source_checkout", "/commands/*/outputContract/ownerRequirementRefs": "requirement_identifier", + "/commands/*/outputContract/briefPacketContract/boundaryPolicyRefs": "requirement_identifier", "/commands/*/outputContract/briefPacketContract/fieldRules/boundaryPolicyRefs": "contract_field_description", "/commands/*/outputContract/briefPacketContract/fieldRules/contextRefs": "contract_field_description", "/commands/*/outputContract/qualityFindingFields/evidenceRefs": "runtime_field", @@ -1395,6 +1400,52 @@ func verifyCLIContractSourceClassifications(content string, entries map[string]s return nil } +func verifyCLIContractBoundaryPolicyClosure(content string, textEntries map[string]string) error { + contract, err := decodePackageJSONObject(content, "CLI contract") + if err != nil { + return err + } + requirementIDCounts := map[string]int{} + for entry, source := range textEntries { + if !strings.HasPrefix(entry, "package/docs/specs/") || !strings.HasSuffix(entry, "/requirements.v1.json") { + continue + } + value, err := decodePackageJSONObject(source, entry) + if err != nil { + return err + } + requirements, _ := value["requirements"].([]any) + for _, rawRequirement := range requirements { + requirement, ok := rawRequirement.(map[string]any) + if !ok { + return fmt.Errorf("package %s requirement must be an object", entry) + } + if requirementID, _ := requirement["requirementId"].(string); requirementID != "" { + requirementIDCounts[requirementID]++ + } + } + } + commands, _ := contract["commands"].([]any) + for _, rawCommand := range commands { + command, _ := rawCommand.(map[string]any) + output, _ := command["outputContract"].(map[string]any) + brief, _ := output["briefPacketContract"].(map[string]any) + if brief == nil { + continue + } + refs := stringArrayField(brief, "boundaryPolicyRefs") + if len(refs) == 0 { + return fmt.Errorf("package CLI brief contract boundaryPolicyRefs must be non-empty") + } + for _, requirementID := range refs { + if requirementIDCounts[requirementID] != 1 { + return fmt.Errorf("package CLI brief boundary policy ref must resolve to exactly one shipped requirement") + } + } + } + return nil +} + func verifyClosedReferenceInventory(label string, value any, classifications map[string]string) error { var walk func(any, []string) error walk = func(current any, route []string) error { diff --git a/internal/tools/packageverify/main_test.go b/internal/tools/packageverify/main_test.go index 6b2eee1..4a5f4c4 100644 --- a/internal/tools/packageverify/main_test.go +++ b/internal/tools/packageverify/main_test.go @@ -736,6 +736,20 @@ func TestPackagePublicReferenceClosure(t *testing.T) { }, want: "dangling package-public route MISSING.json", }, + { + name: "dangling brief boundary policy requirement", + mutate: func(entries map[string]string) { + entries["package/proofkit/cli-contract.v2.json"] = strings.Replace(entries["package/proofkit/cli-contract.v2.json"], "REQ-PROOFKIT-SPEC-026", "REQ-MISSING-026", 1) + }, + want: "boundary policy ref must resolve to exactly one shipped requirement", + }, + { + name: "ambiguous brief boundary policy requirement", + mutate: func(entries map[string]string) { + entries["package/docs/specs/example/requirements.v1.json"] = strings.Replace(entries["package/docs/specs/example/requirements.v1.json"], `]}`, `,{"requirementId":"REQ-PROOFKIT-SPEC-026"}]}`, 1) + }, + want: "boundary policy ref must resolve to exactly one shipped requirement", + }, { name: "dangling binding witness path", mutate: func(entries map[string]string) { @@ -1380,12 +1394,12 @@ func packageReferenceClosureFixture() map[string]string { "package/ADOPTION.md": "Adoption.\n", "package/docs/proofkit-contract-map.md": "Contract map.\n", "package/docs/specs/example/overview.md": "Example.\n", - "package/docs/specs/example/requirements.v1.json": `{"specPackagePath":"docs/specs/example","overviewPath":"docs/specs/example/overview.md","requirementsPath":"docs/specs/example/requirements.v1.json","requirements":[]}`, + "package/docs/specs/example/requirements.v1.json": `{"specPackagePath":"docs/specs/example","overviewPath":"docs/specs/example/overview.md","requirementsPath":"docs/specs/example/requirements.v1.json","requirements":[{"requirementId":"REQ-PROOFKIT-SPEC-005"},{"requirementId":"REQ-PROOFKIT-SPEC-026"}]}`, "package/proofkit/requirement-bindings.json": `{"requirements":[{"specPath":"docs/specs/example/requirements.v1.json"}],"bindings":[{"witnessPath":"internal/tools/packageverify/main_test.go","witnessSelectors":[{"selector":"TestPackagePublicReferenceClosure","command":"go test ./internal/tools/packageverify -run '^TestPackagePublicReferenceClosure$'"}]}]}`, "package/proofkit/witness-plan.json": `{"commands":[],"policies":[]}`, "package/proofkit/command-families.v1.json": `{"families":[]}`, "package/proofkit/receipt-producer-policy.json": `{"producers":[{"producerId":"local.developer","evidenceRefs":["docs/specs/example/requirements.v1.json"]}]}`, - "package/proofkit/cli-contract.v2.json": `{"processContract":{"helpGrammar":{"helpCatalogFormsSource":"proofkit/command-families.v1.json"}},"commands":[{"command":"fixture","inputContract":{"nativeSource":{"path":"internal/tools/packageverify/main.go","evidenceClass":"source_checkout"}},"outputContract":{"briefPacketContract":{"fieldRules":{"boundaryPolicyRefs":"policy field description","contextRefs":"runtime field description"}},"nativeSource":{"path":"internal/tools/packageverify/main.go","evidenceClass":"source_checkout"}}}]}`, + "package/proofkit/cli-contract.v2.json": `{"processContract":{"helpGrammar":{"helpCatalogFormsSource":"proofkit/command-families.v1.json"}},"commands":[{"command":"fixture","inputContract":{"nativeSource":{"path":"internal/tools/packageverify/main.go","evidenceClass":"source_checkout"}},"outputContract":{"briefPacketContract":{"boundaryPolicyRefs":["REQ-PROOFKIT-SPEC-005","REQ-PROOFKIT-SPEC-026"],"fieldRules":{"boundaryPolicyRefs":"policy field description","contextRefs":"runtime field description"}},"nativeSource":{"path":"internal/tools/packageverify/main.go","evidenceClass":"source_checkout"}}}]}`, } } diff --git a/internal/tools/releasechange/record_test.go b/internal/tools/releasechange/record_test.go index db9cef5..d0fee8e 100644 --- a/internal/tools/releasechange/record_test.go +++ b/internal/tools/releasechange/record_test.go @@ -196,6 +196,7 @@ func TestCurrentChangeRecordNamesReviewedSemanticChanges(t *testing.T) { var currentBreakingChanges = []Change{ {ChangeID: "proofkit.agent-route.brief-default", Summary: "Change bare agent-route --agent-envelope output from the generic full envelope to the bounded proofkit.agent-route.brief packet."}, + {ChangeID: "proofkit.agent-route.materialized-artifact-refs", Summary: "Reject the stdin transport sentinel as an available-input or observed-report artifact identity; route references must name materialized caller-owned artifacts."}, {ChangeID: "proofkit.platform.macos-13", Summary: "Raise published Darwin package compatibility to macOS 13.0 so wheel tags remain truthful for binaries built with Go 1.27.1."}, } @@ -206,6 +207,7 @@ var currentAdditions = []Change{ var currentMigrationSteps = []string{ "Consumers that require the former generic agent-route envelope must add --agent-envelope-mode full after --agent-envelope; consumers that accept bounded route guidance may keep bare --agent-envelope.", + "Consumers that used the stdin transport sentinel as an agent-route availableInputs or observedReports ref must materialize that artifact and pass its safe repo-relative path instead.", "Darwin consumers must use macOS 13.0 or later; contributors that build from source must use Go 1.27.1.", } diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index f26b0a0..c267b8f 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -420,9 +420,9 @@ "witness_command_catalog" ] }, - "ref": { - "format": "safe repo-relative materialized caller-owned file, report ref, or scanner root ref; stdin sentinel - is forbidden and scanner root refs may be ." - } + "ref": { + "format": "safe repo-relative materialized caller-owned file, report ref, or scanner root ref; stdin sentinel - is forbidden and scanner root refs may be ." + } }, "uniqueBy": "kind" }, @@ -435,7 +435,6 @@ "nonClaim": "serve_local_view only adds --serve to requirement-browser-server routes; it does not create a public server or approve rendered artifacts." }, "goal": { - "required": true, "enum": [ "admit_receipts", "adopt_repository", @@ -456,7 +455,8 @@ "unknown", "validate_requirement_source", "verify_typescript_public_api" - ] + ], + "required": true }, "knownChangedPaths": { "default": [], @@ -464,20 +464,21 @@ "nonClaim": "knownChangedPaths is diagnostic-only; selective routing requires a caller-owned changed_path_set input ref." }, "mode": { - "required": true, "enum": [ "enforce-all", "enforce-touched", "observe", "warn" - ] + ], + "required": true }, "nonClaims": { "default": [], - "maxItems": 8, - "maxItemRunes": 240 + "maxItemRunes": 240, + "maxItems": 8 }, "observedReports": { + "blockingSemantics": "any observed report state other than passed blocks command emission", "default": [], "item": { "kind": { @@ -493,7 +494,7 @@ ] }, "ref": { - "format": "safe repo-relative report ref" + "format": "safe repo-relative materialized caller-owned report ref; stdin sentinel - is forbidden" }, "state": { "enum": [ @@ -505,7 +506,6 @@ ] } }, - "blockingSemantics": "any observed report state other than passed blocks command emission", "uniqueBy": "ref" }, "openBrowser": { @@ -513,9 +513,9 @@ "nonClaim": "openBrowser only adds --open when browserMode is serve_local_view." }, "routeId": { - "required": true, "format": "safe stable id", - "maxRunes": 96 + "maxRunes": 96, + "required": true }, "schemaVersion": { "required": true, @@ -532,7 +532,7 @@ "rootDefinitionDigest": "sha256:fd1121285d1b357ed848febc248700a75417d06619e22ed36991631d7526375b", "nativeSource": { "path": "internal/command/agentroute", - "canonicalDigest": "sha256:c4957046177c5cc8192deae96efd6b492a8f2a1a0c3d02da4dd4b3f4f97ca1cf", + "canonicalDigest": "sha256:3164cabec37654591d8e3c88444734d1584647d07b12e149ae17a95ad5b27d8a", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -565,10 +565,27 @@ "schemaVersion": 3, "authority": "deterministic route report, brief packet, full envelope, or invalid-input repair packet selected from the admitted invocation", "briefPacketContract": { + "boundaryPolicyRefs": [ + "REQ-PROOFKIT-SPEC-005", + "REQ-PROOFKIT-SPEC-026" + ], "contractId": "proofkit.agent-route.brief.v1", - "schemaVersion": 1, - "maxPrettyJSONBytes": 3072, + "fieldRules": { + "blockers": "required-input blockers followed by sorted non-passed observed-report blockers; retain at most four and count the exact remainder", + "boundaryPolicyRefs": "exactly the shipped and uniquely resolvable requirement IDs REQ-PROOFKIT-SPEC-005 and REQ-PROOFKIT-SPEC-026; policy prose is not duplicated", + "contextRefs": "only caller-owned artifact operands of the selected next command, each addressed by an RFC 6901 pointer into the source report", + "detailAccess": "binds source report ID and stable digest and provides exact outputArgs suffixes for report and full retrieval from the original admitted input", + "nextAction": "null for blocked states; otherwise the first canonical nextCommands entry with exact command identity and inline argv unless the byte bound requires argvState=detail_required", + "omissionSummary": "exact counts for unselected available commands, source-omitted commands, and blockers omitted by the packet bound", + "packetKind": "proofkit.agent-route.brief", + "schemaVersion": "1" + }, "maxBlockers": 4, + "maxPrettyJSONBytes": 3072, + "nonClaims": [ + "The brief packet is derived route guidance, not requirement, proof, execution, merge, release, rollout, deployment, or readiness authority.", + "The packet byte bound does not claim tokenizer-specific token consumption or semantic sufficiency." + ], "requiredFields": [ "blockers", "boundaryPolicyRefs", @@ -582,20 +599,7 @@ "schemaVersion", "state" ], - "fieldRules": { - "blockers": "required-input blockers followed by sorted non-passed observed-report blockers; retain at most four and count the exact remainder", - "boundaryPolicyRefs": "exactly NC-PROOFKIT-SPEC-005 and NC-PROOFKIT-SPEC-026; policy prose is not duplicated", - "contextRefs": "only caller-owned artifact operands of the selected next command, each addressed by an RFC 6901 pointer into the source report", - "detailAccess": "binds source report ID and stable digest and advertises report and full retrieval from the original admitted input", - "nextAction": "null for blocked states; otherwise the first canonical nextCommands entry with exact command identity and inline argv unless the byte bound requires argvState=detail_required", - "omissionSummary": "exact counts for unselected available commands, source-omitted commands, and blockers omitted by the packet bound", - "packetKind": "proofkit.agent-route.brief", - "schemaVersion": "1" - }, - "nonClaims": [ - "The brief packet is derived route guidance, not requirement, proof, execution, merge, release, rollout, deployment, or readiness authority.", - "The packet byte bound does not claim tokenizer-specific token consumption or semantic sufficiency." - ] + "schemaVersion": 1 }, "changesFromV2": [ "bare --agent-envelope emits proofkit.agent-route.brief", @@ -614,7 +618,7 @@ }, { "path": "internal/command/agentroute", - "canonicalDigest": "sha256:c4957046177c5cc8192deae96efd6b492a8f2a1a0c3d02da4dd4b3f4f97ca1cf", + "canonicalDigest": "sha256:3164cabec37654591d8e3c88444734d1584647d07b12e149ae17a95ad5b27d8a", "evidenceClass": "source_checkout" } ], @@ -1148,7 +1152,7 @@ "rootDefinitionDigest": "sha256:b343c1093476cd9f50970f58608e43c1a6ae681a2c06b17c217ab4f22ea454e5", "nativeSource": { "path": "internal/command/conformanceprofile", - "canonicalDigest": "sha256:fba2ae58bf66cfc43834368dec84ca85abd11e08a541786dc59eafaf7628c322", + "canonicalDigest": "sha256:fba2ae58bf66cfc43834368dec84ca85abd11e08a541786dc59eafaf7628c322", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -1175,7 +1179,7 @@ "rootDefinitionDigest": "sha256:5817a9d259deb8ff48d8dc6298b46164e7b132304ca1d865967c611d147b1793", "nativeSource": { "path": "internal/command/conformanceprofile", - "canonicalDigest": "sha256:fba2ae58bf66cfc43834368dec84ca85abd11e08a541786dc59eafaf7628c322", + "canonicalDigest": "sha256:fba2ae58bf66cfc43834368dec84ca85abd11e08a541786dc59eafaf7628c322", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { diff --git a/proofkit/requirement-bindings.json b/proofkit/requirement-bindings.json index 5496f57..9ed12e3 100644 --- a/proofkit/requirement-bindings.json +++ b/proofkit/requirement-bindings.json @@ -4961,6 +4961,10 @@ "selector": "TestBuildEnvelopeCompactsOversizedArgvWithoutLosingActionIdentity", "command": "go test ./internal/command/agentroute -run '^TestBuildEnvelopeCompactsOversizedArgvWithoutLosingActionIdentity$'" }, + { + "selector": "TestAgentBriefCompactsAtDeclaredByteBoundary", + "command": "go test ./internal/command/agentroute -run '^TestAgentBriefCompactsAtDeclaredByteBoundary$'" + }, { "selector": "TestBuildEnvelopeCapsBlockersAndCountsOmittedDetails", "command": "go test ./internal/command/agentroute -run '^TestBuildEnvelopeCapsBlockersAndCountsOmittedDetails$'" @@ -4969,6 +4973,21 @@ "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, + { + "requirementId": "REQ-PROOFKIT-SPEC-026", + "scenarioId": "proofkit.spec-proof-core.agent-route-flag-pre-read-admission", + "witnessId": "proofkit.agent-route.flag-pre-read-admission-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/app_test.go", + "witnessSelectors": [ + { + "selector": "TestAgentRouteModeAdmissionPrecedesInputRead", + "command": "go test ./internal/app -run '^TestAgentRouteModeAdmissionPrecedesInputRead$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, { "requirementId": "REQ-PROOFKIT-SPEC-026", "scenarioId": "proofkit.spec-proof-core.agent-route-materialized-ref-admission", diff --git a/release/change-record.v2.json b/release/change-record.v2.json index 01e2edb..dd1513a 100644 --- a/release/change-record.v2.json +++ b/release/change-record.v2.json @@ -8,6 +8,10 @@ "changeId": "proofkit.agent-route.brief-default", "summary": "Change bare agent-route --agent-envelope output from the generic full envelope to the bounded proofkit.agent-route.brief packet." }, + { + "changeId": "proofkit.agent-route.materialized-artifact-refs", + "summary": "Reject the stdin transport sentinel as an available-input or observed-report artifact identity; route references must name materialized caller-owned artifacts." + }, { "changeId": "proofkit.platform.macos-13", "summary": "Raise published Darwin package compatibility to macOS 13.0 so wheel tags remain truthful for binaries built with Go 1.27.1." @@ -27,6 +31,7 @@ "required": true, "steps": [ "Consumers that require the former generic agent-route envelope must add --agent-envelope-mode full after --agent-envelope; consumers that accept bounded route guidance may keep bare --agent-envelope.", + "Consumers that used the stdin transport sentinel as an agent-route availableInputs or observedReports ref must materialize that artifact and pass its safe repo-relative path instead.", "Darwin consumers must use macOS 13.0 or later; contributors that build from source must use Go 1.27.1." ] }, diff --git a/scripts/validate-self-hosting-receipts_test.go b/scripts/validate-self-hosting-receipts_test.go index 1afcc46..ffe7da7 100644 --- a/scripts/validate-self-hosting-receipts_test.go +++ b/scripts/validate-self-hosting-receipts_test.go @@ -927,10 +927,13 @@ func TestReleaseWorkflowDelegatesNPMRegistryEvidenceToRepositoryOwner(t *testing t.Fatalf("find npm registry evidence step: index=%d error=%v", stepIndex, err) } run := publish.Steps[stepIndex].Run + if strings.Count(run, "go run ./internal/tools/npmregistry capture-pack-reports") != 1 { + t.Fatalf("npm registry evidence step must normalize npm 12 keyed reports through its repository owner exactly once: %s", run) + } if strings.Count(run, "npm run npm:registry-evidence") != 1 { t.Fatalf("npm registry evidence step must invoke its repository owner exactly once: %s", run) } - for _, forbidden := range []string{"proofkit.published-registry-artifact-set.v1", "authorityValidator", "registry_release"} { + for _, forbidden := range []string{"proofkit.published-registry-artifact-set.v1", "authorityValidator", "registry_release", "Array.isArray(parsed)", "JSON.parse(readFileSync"} { if strings.Contains(run, forbidden) { t.Fatalf("workflow duplicates typed npm registry authority field %q", forbidden) } From 844546a432f6cf21af73eff33006840499bbb00c Mon Sep 17 00:00:00 2001 From: iperev Date: Thu, 3 Sep 2026 00:28:28 +0200 Subject: [PATCH 3/9] fix: close agent route review findings --- docs/proofkit-contract-map.md | 11 +- .../requirements.v1.json | 4 +- internal/app/agent_route_version_edge_test.go | 13 ++- internal/app/app_test.go | 6 +- internal/app/cli_contract_test.go | 39 ++++++- internal/app/command_contract_generated.go | 4 +- .../app/testdata/v0.6-wire-observations.json | 10 +- internal/command/agentroute/agentroute.go | 93 +++++++++++----- internal/command/agentroute/brief.go | 89 +++++++++++---- internal/command/agentroute/brief_test.go | 105 +++++++++++++++++- .../stackpreset/preset_ids_generated.go | 2 +- internal/tools/coveragemetrics/main.go | 11 ++ internal/tools/packageverify/main.go | 7 +- internal/tools/packageverify/main_test.go | 15 +++ internal/tools/releasechange/record_test.go | 6 +- internal/tools/releasepreflight/main.go | 24 +++- internal/tools/releasepreflight/main_test.go | 38 ++++++- proofkit/cli-contract.v2.json | 59 ++++++++-- proofkit/requirement-bindings.json | 41 +++++-- release/change-record.v2.json | 12 +- 20 files changed, 491 insertions(+), 98 deletions(-) diff --git a/docs/proofkit-contract-map.md b/docs/proofkit-contract-map.md index a4702cb..d912ac6 100644 --- a/docs/proofkit-contract-map.md +++ b/docs/proofkit-contract-map.md @@ -99,10 +99,11 @@ Agents should use `agent-route` for the complete deterministic route report and `--agent-envelope-mode brief` is byte-identical to the bare envelope form; `--agent-envelope-mode full` preserves the complete generic agent envelope for debugging or demand-loaded detail. Every brief binds its source report ID and -stable digest in one top-level `detailAccess` record, so callers retain the -original admitted input and recompute report or full detail instead of adding a -detail reference to every item. All three forms are derived from explicit -caller-owned facts. This map explains route families without becoming an +stable digest and admitted launcher profile in one top-level `detailAccess` +record, so callers retain the original admitted input and launcher context and +recompute report or full detail instead of adding a detail reference to every +item. All three forms are derived from explicit caller-owned facts. This map +explains route families without becoming an execution, freshness, or merge decision. The exact route input vocabulary is machine-readable in `proofkit/cli-contract.v2.json` under `agent-route.inputContract`; the Go @@ -164,7 +165,7 @@ Semantic context routes are `requirement-context-compose`, caller-owned context refs, exact omission counts, and uniquely resolvable shipped boundary-policy requirement refs instead of the full route report. Resolve further detail with the packet's exact output-argument suffixes and - retained original input only after checking the brief's source digest; request + retained original input and launcher context only after checking the brief's source digest and launcher profile; request `--agent-envelope-mode full` only when the generic envelope is actually needed. Treat `blocked_*` states as stop signals, not as permission to guess missing inputs. `knownChangedPaths` are diff --git a/docs/specs/proofkit-spec-proof-core/requirements.v1.json b/docs/specs/proofkit-spec-proof-core/requirements.v1.json index a28409b..7c9ed18 100644 --- a/docs/specs/proofkit-spec-proof-core/requirements.v1.json +++ b/docs/specs/proofkit-spec-proof-core/requirements.v1.json @@ -585,12 +585,12 @@ { "requirementId": "REQ-PROOFKIT-SPEC-026", "ownerId": "proofkit.spec-proof-core", - "invariant": "Agent-route output contract v3 derives every report, brief packet, and full envelope from one fully admitted immutable route input and one command-owned route report: route artifact refs in both available inputs and observed reports identify materialized caller-owned artifacts and never use the stdin transport sentinel; a bare --agent-envelope and explicit brief mode emit the same deterministic schema-v1 proofkit.agent-route.brief root while explicit full mode preserves the prior generic envelope; a routed brief selects exactly the first canonical nextCommands entry as its sole nextAction, retains its exact command and argv identity when the 3072-byte pretty stable-JSON bound permits, otherwise removes only inline argv and marks argvState detail_required, while every blocked state emits null nextAction; blockers are projected in deterministic required-input then sorted non-passed observed-report order, retain at most four typed entries, and report the exact omitted remainder; contextRefs refer to all and only caller-owned artifact operands of the selected command through resolvable source-report JSON pointers; one top-level detailAccess record binds the source report ID and stable digest, provides exact report and full output-argument suffixes for replay with the original input, and no per-item detail reference is added; omissionSummary exactly counts unselected available commands, source-omitted commands, and omitted blockers; fixed uniquely resolvable shipped requirement IDs replace repeated boundary-policy prose; the complete brief is at most 3072 bytes, and unsupported modes, invalid flag values, repeated single-value flags, or a detail mode without --agent-envelope fail before input is read. Reports and envelopes remain derived route guidance and never become requirement, proof, execution, merge, release, rollout, deployment, or readiness authority.", + "invariant": "Agent-route output contract v3 derives every report, brief packet, and full envelope from one fully admitted immutable route input, one admitted launcher profile, and one command-owned route report: route artifact refs in both available inputs and observed reports identify materialized caller-owned artifacts and never use the stdin transport sentinel; the nested report contract preserves the exact required route-report fields, schemaVersion 3, closed route-family vocabulary, guidance-to-selected-family relation, pre-suppression available-command count, and launcher profile; a bare --agent-envelope and explicit brief mode emit the same deterministic schema-v1 proofkit.agent-route.brief root while explicit full mode preserves the prior generic envelope; a routed brief selects exactly the first canonical nextCommands entry as its sole nextAction, retains its exact command and argv identity when the 3072-byte pretty stable-JSON bound permits, otherwise removes only inline argv and marks argvState detail_required, while every blocked state emits null nextAction; blockers are projected in deterministic unknown-goal, required-input, then sorted non-passed observed-report order, retain at most four typed entries without materializing omitted blocker maps, and report the exact omitted remainder; contextRefs refer to all and only caller-owned artifact operands of the selected command through resolvable source-report JSON pointers; one top-level detailAccess record binds the source report ID, stable digest, and launcher profile, provides exact report and full output-argument suffixes for replay with the original input and launcher context, and no per-item detail reference is added; omissionSummary exactly counts pre-suppression unselected available commands, source-omitted commands, and omitted blockers; fixed uniquely resolvable shipped requirement IDs replace repeated boundary-policy prose; the complete brief is at most 3072 bytes, and unsupported modes, invalid flag values, repeated single-value flags, or a detail mode without --agent-envelope fail before input is read. Reports and envelopes remain derived route guidance and never become requirement, proof, execution, merge, release, rollout, deployment, or readiness authority.", "claimLevel": "blocking", "riskClass": "high", "proofBindingRefs": ["proofkit/requirement-bindings.json"], "nonClaimRefs": ["NC-PROOFKIT-SPEC-026"], - "nonClaims": ["The 3072-byte packet bound and stable source-report digest do not prove provider token consumption, semantic sufficiency, source freshness, command execution, native witness truth, or policy approval; detail retrieval requires the caller to retain the original admitted input."], + "nonClaims": ["The 3072-byte packet bound and stable source-report digest do not prove provider token consumption, semantic sufficiency, source freshness, command execution, native witness truth, or policy approval; detail retrieval requires the caller to retain the original admitted input and launcher context."], "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, "deferral": null, "updatePolicy": {"reviewOwnerId": "proofkit.spec-proof-core", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} diff --git a/internal/app/agent_route_version_edge_test.go b/internal/app/agent_route_version_edge_test.go index e5df835..aad9eec 100644 --- a/internal/app/agent_route_version_edge_test.go +++ b/internal/app/agent_route_version_edge_test.go @@ -105,24 +105,29 @@ func validateAgentRouteVersionEdge(record agentRouteVersionEdge) error { if record.PreviousVersion != "0.5.1" || record.Version != "0.6.0" { return fmt.Errorf("agent-route version-edge release identity is stale") } - if record.PreviousPublicABISHA256 != "sha256:9ecd2c3d2f3f360088409f7e91cce406fc1d1d6edda1b404fce119985c4fb623" || record.CurrentPublicABISHA256 != "sha256:3dc6dead6d21c6a463426594c2e10e2b9dceffd0d16a08ed0fb9ebe2e679e0c0" || record.PreviousPublicABISHA256 == record.CurrentPublicABISHA256 { + if record.PreviousPublicABISHA256 != "sha256:9ecd2c3d2f3f360088409f7e91cce406fc1d1d6edda1b404fce119985c4fb623" || record.CurrentPublicABISHA256 != "sha256:"+cliContractPublicABISHA256 || record.PreviousPublicABISHA256 == record.CurrentPublicABISHA256 { return fmt.Errorf("agent-route version-edge ABI identity is invalid") } + currentMetadata, ok := generatedCommandContractMetadataByName["agent-route"] + if !ok { + return fmt.Errorf("agent-route version-edge current command metadata is missing") + } wantContract := agentRouteChangedCommandContract{ Command: "agent-route", PreviousInputContractSHA256: "sha256:6b5af8287f2972bbef4c68c247f43fb16d0f0d8739e5e6d3a66543af20d2644d", - CurrentInputContractSHA256: "sha256:60bafcd877e0bd26bb9fbd37343ba7e19d61d314afbd7062754c8574ffd36c41", + CurrentInputContractSHA256: currentMetadata.InputContractSHA256, PreviousOutputContractSHA256: "sha256:44ec313a43360b6138ad6c3ae5de4abd51bbf312060880c108a6351606695915", - CurrentOutputContractSHA256: "sha256:e160ea2e5531b6675f7c4edfb01a2fb0d32fd7c74189e9c38774c41a211dc29e", + CurrentOutputContractSHA256: currentMetadata.OutputContractSHA256, } if record.ChangedCommandContract != wantContract { return fmt.Errorf("agent-route version-edge changed command contract is not exact") } - if !slices.Equal(record.BreakingChangeIDs, []string{"proofkit.agent-route.brief-default", "proofkit.agent-route.materialized-artifact-refs"}) || !slices.Equal(record.AdditionChangeIDs, []string{"proofkit.agent-route.envelope-detail-mode"}) { + if !slices.Equal(record.BreakingChangeIDs, []string{"proofkit.agent-route.brief-default", "proofkit.agent-route.materialized-artifact-refs", "proofkit.agent-route.report-schema-v3"}) || !slices.Equal(record.AdditionChangeIDs, []string{"proofkit.agent-route.envelope-detail-mode"}) { return fmt.Errorf("agent-route version-edge change owners are not exact") } if !slices.Equal(record.MigrationSteps, []string{ "Consumers that require the former generic agent-route envelope must add --agent-envelope-mode full after --agent-envelope; consumers that accept bounded route guidance may keep bare --agent-envelope.", + "Consumers of the bare agent-route report must admit schemaVersion 3; the prior route-family fields remain, and summary now binds launcherProfile and availableCommandCount.", "Consumers that used the stdin transport sentinel as an agent-route availableInputs or observedReports ref must materialize that artifact and pass its safe repo-relative path instead.", }) { return fmt.Errorf("agent-route version-edge migration is not exact") diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 5565140..4636169 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -347,6 +347,10 @@ func TestAgentRouteCLIOutputUsesVersionedRouteFamilyFields(t *testing.T) { if _, legacy := guidance["family"]; legacy { t.Fatalf("agent-route schema v3 retained guidanceSlice.family: %#v", guidance) } + summary := report["summary"].(map[string]any) + if summary["availableCommandCount"] != float64(1) || summary["launcherProfile"] != "path" { + t.Fatalf("agent-route schema v3 summary lost exact route context: %#v", summary) + } } func TestAgentRouteAgentEnvelopeCLIABI(t *testing.T) { @@ -378,7 +382,7 @@ func TestAgentRouteAgentEnvelopeCLIABI(t *testing.T) { t.Fatalf("brief must cite policy instead of duplicating caller prose: %#v", packet) } detail := packet["detailAccess"].(map[string]any) - if detail["commandRef"] != "agent-route" || detail["requiresOriginalInput"] != true { + if detail["commandRef"] != "agent-route" || detail["requiresOriginalInput"] != true || detail["requiresOriginalLauncherContext"] != true || detail["launcherProfile"] != "path" { t.Fatalf("brief does not expose explicit detail access: %#v", detail) } } diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index eef6526..554693d 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -23,7 +23,7 @@ import ( ) const ( - cliContractPublicABISHA256 = "3dc6dead6d21c6a463426594c2e10e2b9dceffd0d16a08ed0fb9ebe2e679e0c0" + cliContractPublicABISHA256 = "ae7614f6c05a84cae1d3c8eda4a8426df0ceb5696d1548fe20d9ffc6ec4a3f53" maxAggregateFileReadBytesForContractTest = 64 << 20 maxPackageManifestBytesForContractTest = 256 << 10 maxSourceFileBytesForContractTest = 8 << 20 @@ -2319,6 +2319,43 @@ func TestAgentRouteInputContractMatchesAdmission(t *testing.T) { } } +func TestAgentRouteOutputContractPreservesReportSemantics(t *testing.T) { + contract := readCLIContract(t) + var output map[string]any + for _, command := range contract.Commands { + if command.Command == "agent-route" { + output = canonicalJSONValue(t, command.OutputContract).(map[string]any) + break + } + } + if output == nil { + t.Fatal("agent-route output contract is missing") + } + report, ok := output["reportContract"].(map[string]any) + if !ok || report["contractId"] != "proofkit.agent-route.report.v3" || report["schemaVersion"] != float64(3) { + t.Fatalf("agent-route report contract identity is invalid: %#v", report) + } + assertStringSet(t, stringsFromAny(report["requiredFields"].([]any)), []string{ + "guidanceSlice", "reportId", "reportKind", "schemaVersion", "selectedRouteFamily", "state", "summary", + }, "agent-route report contract required fields") + fields := report["fields"].(map[string]any) + if fields["schemaVersion"].(map[string]any)["value"] != float64(3) { + t.Fatalf("agent-route report schema value drifted: %#v", fields["schemaVersion"]) + } + family := fields["selectedRouteFamily"].(map[string]any) + assertStringSet(t, stringsFromAny(family["enum"].([]any)), []string{ + "adoption", "migration", "release_and_deployment", "rendered_views", "repository_structure", "requirement_proof_binding", "requirement_source", "selective_evidence", "selective_planning", "test_inventory_and_coverage", "unknown", + }, "agent-route report route families") + guidance := fields["guidanceSlice"].(map[string]any) + if guidance["routeFamilyRule"] != "must equal selectedRouteFamily" { + t.Fatalf("agent-route guidance relation drifted: %#v", guidance) + } + summary := fields["summary"].(map[string]any) + if summary["availableCommandCount"] != "exact count before blocked-state command suppression" || summary["launcherProfile"] != "exact admitted command-renderer profile that affects route argv and report identity" { + t.Fatalf("agent-route report summary contract drifted: %#v", summary) + } +} + func commandOwnedContractProjection(t *testing.T, value any) any { t.Helper() record := canonicalJSONValue(t, value).(map[string]any) diff --git a/internal/app/command_contract_generated.go b/internal/app/command_contract_generated.go index bcd735e..0aedd87 100644 --- a/internal/app/command_contract_generated.go +++ b/internal/app/command_contract_generated.go @@ -1,7 +1,7 @@ // Code generated by internal/tools/commandcontractgen; DO NOT EDIT. package app -const commandContractSourceSHA256 = "28c8f4128ba07378e705693640d42e7c0867043b4c053b3f8874401b5d5d3284" +const commandContractSourceSHA256 = "d5aff223f854d6de95a612d9fe1d421015e27eaa26f0943b02b347021519a3bd" type generatedCommandContractMetadata struct { InputContractSHA256 string @@ -15,7 +15,7 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "adoption-contract-envelope": {InputContractSHA256: "sha256:c310214676ff4b6f536a5bc9d687f681a7e71f73d7a03ac932707d8cd3905cdf", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.adoption-contract-envelope.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3efb2c5161fee16fd8ac6a40dcb6d9c41fbc23e468f60621436ae9e8076e0950", FlagChoices: map[string][]string{}}, "adoption-doctor": {InputContractSHA256: "sha256:efa9acfe32bff07f56d9dc9902530df2979794289bc2f7f547f7a108a7dd0f35", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.adoption-doctor.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:8fdfc6608f197e633f042f20031ae1014872a90aa3daa66885ffcaddca994766", FlagChoices: map[string][]string{}}, "adoption-workflow-plan": {InputContractSHA256: "sha256:b32ae67179d7b6dcf1ea66cb6b2b2691c8367ce2e2be367619b65973166da55c", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.adoption-workflow-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:8d64cb53ebd0307e3cebc3435286a3d2a1ee8a0ad6f7514fc0fb3285db0f565b", FlagChoices: map[string][]string{}}, - "agent-route": {InputContractSHA256: "sha256:43f38f48909d4d99496c90086d271e796a1a90c6fcd9984f7a22728c39538394", InputSchemaSummary: []string{"availableInputs", "browserMode", "goal", "knownChangedPaths", "mode", "nonClaims", "observedReports", "openBrowser", "routeId", "schemaVersion", "root-shape-only definition proofkit.agent-route.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:75a1313265c401babf4b67065767f7ac75339762b19a1dc557b4c4a66d9f9d02", FlagChoices: map[string][]string{}}, + "agent-route": {InputContractSHA256: "sha256:b44e2273d237a4533d01ab008a52d205dc2835e87ba5b3731a30db1748ead659", InputSchemaSummary: []string{"availableInputs", "browserMode", "goal", "knownChangedPaths", "mode", "nonClaims", "observedReports", "openBrowser", "routeId", "schemaVersion", "root-shape-only definition proofkit.agent-route.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:2fb98bdbc655211644c89e160a51380d58783dd2a393befffb7ddc8dfb4f79da", FlagChoices: map[string][]string{}}, "binding-partition": {InputContractSHA256: "sha256:366ad082045af52b2ac6604f18626d0f285b2db73b45d9a82687b8d3b0d2b3fd", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.binding-partition.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:52840879e13a00ef9a4abaad6cdb33000511674d5f9003fb56f387fdf58fadc8", FlagChoices: map[string][]string{}}, "branch-authority": {InputContractSHA256: "sha256:8a3ed74978898593fbdbf1f7fa684dae450fbd9019edcd60d07f818d63363ed4", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.branch-authority.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3c7dc74842299b92cd5baf57cc8666e9415963091359e5faf654e28da89561f1", FlagChoices: map[string][]string{}}, "capability-map-admission": {InputContractSHA256: "sha256:efe87497cf6a32cfd1f9c9c191393cac8781d1e9211914a8a7b6aaa44e7f371d", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.capability-map-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:bfb32535fd51bed168111912acb1651527897fe59a592119dc0032ce9286c9db", FlagChoices: map[string][]string{}}, diff --git a/internal/app/testdata/v0.6-wire-observations.json b/internal/app/testdata/v0.6-wire-observations.json index 21f6884..bc5f7b0 100644 --- a/internal/app/testdata/v0.6-wire-observations.json +++ b/internal/app/testdata/v0.6-wire-observations.json @@ -5,23 +5,25 @@ "version": "0.6.0", "evidenceClass": "owner_authored_frozen_version_edge_observation", "previousPublicAbiSha256": "sha256:9ecd2c3d2f3f360088409f7e91cce406fc1d1d6edda1b404fce119985c4fb623", - "currentPublicAbiSha256": "sha256:3dc6dead6d21c6a463426594c2e10e2b9dceffd0d16a08ed0fb9ebe2e679e0c0", + "currentPublicAbiSha256": "sha256:ae7614f6c05a84cae1d3c8eda4a8426df0ceb5696d1548fe20d9ffc6ec4a3f53", "changedCommandContract": { "command": "agent-route", "previousInputContractSha256": "sha256:6b5af8287f2972bbef4c68c247f43fb16d0f0d8739e5e6d3a66543af20d2644d", - "currentInputContractSha256": "sha256:60bafcd877e0bd26bb9fbd37343ba7e19d61d314afbd7062754c8574ffd36c41", + "currentInputContractSha256": "sha256:b44e2273d237a4533d01ab008a52d205dc2835e87ba5b3731a30db1748ead659", "previousOutputContractSha256": "sha256:44ec313a43360b6138ad6c3ae5de4abd51bbf312060880c108a6351606695915", - "currentOutputContractSha256": "sha256:e160ea2e5531b6675f7c4edfb01a2fb0d32fd7c74189e9c38774c41a211dc29e" + "currentOutputContractSha256": "sha256:2fb98bdbc655211644c89e160a51380d58783dd2a393befffb7ddc8dfb4f79da" }, "breakingChangeIds": [ "proofkit.agent-route.brief-default", - "proofkit.agent-route.materialized-artifact-refs" + "proofkit.agent-route.materialized-artifact-refs", + "proofkit.agent-route.report-schema-v3" ], "additionChangeIds": [ "proofkit.agent-route.envelope-detail-mode" ], "migrationSteps": [ "Consumers that require the former generic agent-route envelope must add --agent-envelope-mode full after --agent-envelope; consumers that accept bounded route guidance may keep bare --agent-envelope.", + "Consumers of the bare agent-route report must admit schemaVersion 3; the prior route-family fields remain, and summary now binds launcherProfile and availableCommandCount.", "Consumers that used the stdin transport sentinel as an agent-route availableInputs or observedReports ref must materialize that artifact and pass its safe repo-relative path instead." ], "nonClaims": [ diff --git a/internal/command/agentroute/agentroute.go b/internal/command/agentroute/agentroute.go index 3be958e..3ae2479 100644 --- a/internal/command/agentroute/agentroute.go +++ b/internal/command/agentroute/agentroute.go @@ -409,11 +409,11 @@ func BuildWithRenderer(raw any, renderer cliexec.Renderer) (map[string]any, int, return nil, 1, err } if input.Goal == "unknown" { - return buildUnknownGoal(input), 1, nil + return buildUnknownGoal(input, renderer), 1, nil } spec, ok := routeSpecs[input.Goal] if !ok { - return buildUnknownGoal(input), 1, nil + return buildUnknownGoal(input, renderer), 1, nil } missing := missingRequiredInputs(spec, input) state := "routed" @@ -500,6 +500,33 @@ func OutputContract() map[string]any { "contractId": "proofkit.agent-route.output.v3", "schemaVersion": 3, "authority": "deterministic route report, brief packet, full envelope, or invalid-input repair packet selected from the admitted invocation", + "reportContract": map[string]any{ + "contractId": "proofkit.agent-route.report.v3", + "schemaVersion": 3, + "requiredFields": []any{ + "guidanceSlice", + "reportId", + "reportKind", + "schemaVersion", + "selectedRouteFamily", + "state", + "summary", + }, + "fields": map[string]any{ + "schemaVersion": map[string]any{"value": 3}, + "selectedRouteFamily": map[string]any{ + "enum": routeFamilyContractValues(), + }, + "guidanceSlice": map[string]any{ + "requiredFields": []any{"routeFamily"}, + "routeFamilyRule": "must equal selectedRouteFamily", + }, + "summary": map[string]any{ + "availableCommandCount": "exact count before blocked-state command suppression", + "launcherProfile": "exact admitted command-renderer profile that affects route argv and report identity", + }, + }, + }, "briefPacketContract": map[string]any{ "boundaryPolicyRefs": briefBoundaryPolicyRefs(), "contractId": "proofkit.agent-route.brief.v1", @@ -520,10 +547,10 @@ func OutputContract() map[string]any { "state", }, "fieldRules": map[string]any{ - "blockers": "required-input blockers followed by sorted non-passed observed-report blockers; retain at most four and count the exact remainder", + "blockers": "unknown-goal blocker when applicable, then required-input blockers, then sorted non-passed observed-report blockers; retain at most four and count the exact remainder without materializing the omitted blocker maps", "boundaryPolicyRefs": "exactly the shipped and uniquely resolvable requirement IDs REQ-PROOFKIT-SPEC-005 and REQ-PROOFKIT-SPEC-026; policy prose is not duplicated", "contextRefs": "only caller-owned artifact operands of the selected next command, each addressed by an RFC 6901 pointer into the source report", - "detailAccess": "binds source report ID and stable digest and provides exact outputArgs suffixes for report and full retrieval from the original admitted input", + "detailAccess": "binds source report ID, stable digest, and launcher profile and provides exact outputArgs suffixes for report and full retrieval from the original admitted input and launcher context", "nextAction": "null for blocked states; otherwise the first canonical nextCommands entry with exact command identity and inline argv unless the byte bound requires argvState=detail_required", "omissionSummary": "exact counts for unselected available commands, source-omitted commands, and blockers omitted by the packet bound", "packetKind": "proofkit.agent-route.brief", @@ -537,11 +564,19 @@ func OutputContract() map[string]any { "changesFromV2": []any{ "bare --agent-envelope emits proofkit.agent-route.brief", "--agent-envelope-mode full preserves the prior generic envelope projection", - "route report schemaVersion advances to 3 without changing its semantic fields", + "route report schemaVersion advances to 3 while preserving prior route-family semantics and adding exact launcher and available-command context", }, } } +func routeFamilyContractValues() []any { + values := map[string]struct{}{string(routeFamilyUnknown): {}} + for _, spec := range routeSpecs { + values[string(spec.RouteFamily)] = struct{}{} + } + return sortedKeys(values) +} + func admitInput(raw any) (routeInput, error) { record, ok := raw.(map[string]any) if !ok { @@ -835,7 +870,7 @@ func inputGroupSatisfied(group []string, available map[string]string) bool { return len(group) == 0 } -func buildUnknownGoal(input routeInput) map[string]any { +func buildUnknownGoal(input routeInput, renderer cliexec.Renderer) map[string]any { spec := routeSpec{ RouteFamily: routeFamilyUnknown, SliceSummary: "Unknown goals are deliberately not routed.", @@ -847,9 +882,10 @@ func buildUnknownGoal(input routeInput) map[string]any { "diagnostics": []any{ map[string]any{"key": "goal", "value": input.Goal}, }, - "escalations": []any{"Escalate to the consuming repository owner; do not guess a Proofkit route from an unknown goal."}, - "nextCommands": []any{}, - "nonClaims": mergedNonClaims(input.CallerNonClaims), + "escalations": []any{"Escalate to the consuming repository owner; do not guess a Proofkit route from an unknown goal."}, + "nextCommands": []any{}, + "nonClaims": mergedNonClaims(input.CallerNonClaims), + "observedReports": observedReportReports(input.ObservedReports), "omitted": []any{ map[string]any{"reason": "Unknown goals are not routed because that would create an implicit policy owner inside Proofkit."}, }, @@ -863,20 +899,23 @@ func buildUnknownGoal(input routeInput) map[string]any { "callerNonClaims": toAnySlice(input.CallerNonClaims), "guidanceSlice": guidanceSliceReport(input.Goal, spec), "summary": map[string]any{ - "browserMode": input.BrowserMode, - "availableInputCount": len(input.AvailableInputs), - "callerNonClaimCount": len(input.CallerNonClaims), - "goal": input.Goal, - "knownChangedPaths": len(input.KnownChangedPaths), - "mode": input.Mode, - "observedReportCount": len(input.ObservedReports), - "openBrowser": input.OpenBrowser, + "availableCommandCount": 0, + "browserMode": input.BrowserMode, + "availableInputCount": len(input.AvailableInputs), + "callerNonClaimCount": len(input.CallerNonClaims), + "goal": input.Goal, + "knownChangedPaths": len(input.KnownChangedPaths), + "launcherProfile": renderer.Profile(), + "mode": input.Mode, + "observedReportCount": len(input.ObservedReports), + "openBrowser": input.OpenBrowser, }, } } func buildReport(input routeInput, spec routeSpec, state string, missing []map[string]any, renderer cliexec.Renderer) map[string]any { - nextCommands := commandReports(spec.NextCommands, input, renderer) + availableCommands := commandReports(spec.NextCommands, input, renderer) + nextCommands := availableCommands if state != "routed" { nextCommands = []any{} } @@ -897,14 +936,16 @@ func buildReport(input routeInput, spec routeSpec, state string, missing []map[s "callerNonClaims": toAnySlice(input.CallerNonClaims), "guidanceSlice": guidanceSliceReport(input.Goal, spec), "summary": map[string]any{ - "browserMode": input.BrowserMode, - "availableInputCount": len(input.AvailableInputs), - "callerNonClaimCount": len(input.CallerNonClaims), - "goal": input.Goal, - "knownChangedPaths": len(input.KnownChangedPaths), - "mode": input.Mode, - "observedReportCount": len(input.ObservedReports), - "openBrowser": input.OpenBrowser, + "availableCommandCount": len(availableCommands), + "browserMode": input.BrowserMode, + "availableInputCount": len(input.AvailableInputs), + "callerNonClaimCount": len(input.CallerNonClaims), + "goal": input.Goal, + "knownChangedPaths": len(input.KnownChangedPaths), + "launcherProfile": renderer.Profile(), + "mode": input.Mode, + "observedReportCount": len(input.ObservedReports), + "openBrowser": input.OpenBrowser, }, } } diff --git a/internal/command/agentroute/brief.go b/internal/command/agentroute/brief.go index 0b14f75..1abb2fb 100644 --- a/internal/command/agentroute/brief.go +++ b/internal/command/agentroute/brief.go @@ -1,6 +1,7 @@ package agentroute import ( + "encoding/json" "fmt" "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" @@ -53,19 +54,30 @@ func projectAgentBrief(report map[string]any) (map[string]any, error) { nextCommands := mapsFromAny(report["nextCommands"]) blockers, omittedBlockerCount := briefBlockers(report, reportID) + summary := mapFromAny(report["summary"]) + availableCommandCount, ok := nonNegativeInt(summary["availableCommandCount"]) + if !ok { + return nil, fmt.Errorf("agent-route report summary availableCommandCount must be a non-negative integer") + } + launcherProfile := stringFromMap(summary, "launcherProfile") + if launcherProfile == "" { + return nil, fmt.Errorf("agent-route report summary launcherProfile must be non-empty") + } return map[string]any{ "blockers": blockers, "boundaryPolicyRefs": briefBoundaryPolicyRefs(), "contextRefs": briefContextRefs(nextCommands, reportID), "detailAccess": map[string]any{ - "commandRef": "agent-route", - "outputArgs": briefDetailOutputArgs(), - "requiresOriginalInput": true, - "sourceReportDigest": reportDigest, - "sourceReportId": reportID, + "commandRef": "agent-route", + "launcherProfile": launcherProfile, + "outputArgs": briefDetailOutputArgs(), + "requiresOriginalLauncherContext": true, + "requiresOriginalInput": true, + "sourceReportDigest": reportDigest, + "sourceReportId": reportID, }, "nextAction": briefNextAction(nextCommands, reportID), - "omissionSummary": briefOmissionSummary(report, nextCommands, omittedBlockerCount), + "omissionSummary": briefOmissionSummary(report, availableCommandCount, omittedBlockerCount), "packetId": reportID + ".agent-brief", "packetKind": "proofkit.agent-route.brief", "routeFamily": stringFromMap(report, "selectedRouteFamily"), @@ -128,21 +140,44 @@ func briefContextRefs(commands []map[string]any, reportID string) []any { } func briefBlockers(report map[string]any, reportID string) ([]any, int) { - blockers := []any{} - for index, item := range mapsFromAny(report["requiredInputs"]) { - blockers = append(blockers, map[string]any{ + blockers := make([]any, 0, maxBriefBlockerItems) + total := 0 + appendBounded := func(blocker map[string]any) { + total++ + if len(blockers) < maxBriefBlockerItems { + blockers = append(blockers, blocker) + } + } + if stringFromMap(report, "state") == "blocked_unknown_goal" { + appendBounded(map[string]any{ + "blockerId": reportID + ".blocker.unknown-goal", + "kind": "unknown_goal", + }) + } + requiredInputs, _ := report["requiredInputs"].([]any) + for index, raw := range requiredInputs { + item, ok := raw.(map[string]any) + if !ok { + continue + } + appendBounded(map[string]any{ "blockerId": fmt.Sprintf("%s.blocker.required-input.%02d", reportID, index+1), "kind": "missing_input", "sourceReportPointer": fmt.Sprintf("/requiredInputs/%d", index), "subject": requiredInputLabel(item), }) } - for index, item := range mapsFromAny(report["observedReports"]) { + observedReports, _ := report["observedReports"].([]any) + for index, raw := range observedReports { + item, ok := raw.(map[string]any) + if !ok { + continue + } state := stringFromMap(item, "state") if state == "passed" { continue } - blockers = append(blockers, map[string]any{ + appendBounded(map[string]any{ "blockerId": fmt.Sprintf("%s.blocker.observed-report.%02d", reportID, index+1), "kind": "observed_report", "sourceReportPointer": fmt.Sprintf("/observedReports/%d", index), @@ -150,21 +185,12 @@ func briefBlockers(report map[string]any, reportID string) ([]any, int) { "subject": stringFromMap(item, "kind"), }) } - if len(blockers) == 0 && stringFromMap(report, "state") == "blocked_unknown_goal" { - blockers = append(blockers, map[string]any{ - "blockerId": reportID + ".blocker.unknown-goal", - "kind": "unknown_goal", - }) - } - if len(blockers) <= maxBriefBlockerItems { - return blockers, 0 - } - return blockers[:maxBriefBlockerItems], len(blockers) - maxBriefBlockerItems + return blockers, total - len(blockers) } -func briefOmissionSummary(report map[string]any, commands []map[string]any, omittedBlockerCount int) map[string]any { - alternativeCount := len(commands) - if alternativeCount > 0 { +func briefOmissionSummary(report map[string]any, availableCommandCount int, omittedBlockerCount int) map[string]any { + alternativeCount := availableCommandCount + if stringFromMap(report, "state") == "routed" && alternativeCount > 0 { alternativeCount-- } return map[string]any{ @@ -180,6 +206,21 @@ func briefOmissionSummary(report map[string]any, commands []map[string]any, omit } } +func nonNegativeInt(raw any) (int, bool) { + switch value := raw.(type) { + case int: + return value, value >= 0 + case json.Number: + integer, err := value.Int64() + if err != nil || integer < 0 || int64(int(integer)) != integer { + return 0, false + } + return int(integer), true + default: + return 0, false + } +} + func compactBriefAction(packet map[string]any) { action, ok := packet["nextAction"].(map[string]any) if !ok { diff --git a/internal/command/agentroute/brief_test.go b/internal/command/agentroute/brief_test.go index dd94b61..012b5b2 100644 --- a/internal/command/agentroute/brief_test.go +++ b/internal/command/agentroute/brief_test.go @@ -61,14 +61,14 @@ func TestAgentBriefIsBoundedAndFullEnvelopeRemainsAvailable(t *testing.T) { if err != nil { t.Fatal(err) } - if detail["sourceReportDigest"] != wantDigest || detail["requiresOriginalInput"] != true { + if detail["sourceReportDigest"] != wantDigest || detail["requiresOriginalInput"] != true || detail["requiresOriginalLauncherContext"] != true || detail["launcherProfile"] != cliexec.ProfilePath { t.Fatalf("brief detail access is not source-bound: %#v", detail) } outputArgs := detail["outputArgs"].(map[string]any) if !slices.Equal(stringsFromAny(outputArgs["full"]), []string{"--agent-envelope", "--agent-envelope-mode", "full"}) || len(outputArgs["report"].([]any)) != 0 { t.Fatalf("brief detail access is not executable: %#v", detail) } - if !slices.Equal(stringsFromAny(brief["boundaryPolicyRefs"]), []string{briefBoundaryPolicyDerivedView, briefBoundaryPolicyRoutePacket}) { + if !slices.Equal(stringsFromAny(brief["boundaryPolicyRefs"]), []string{"REQ-PROOFKIT-SPEC-005", "REQ-PROOFKIT-SPEC-026"}) { t.Fatalf("brief boundary policy refs are not exact: %#v", brief["boundaryPolicyRefs"]) } @@ -211,6 +211,10 @@ func TestBuildEnvelopeCompactsOversizedArgvWithoutLosingActionIdentity(t *testin func TestAgentBriefCompactsAtDeclaredByteBoundary(t *testing.T) { t.Parallel() + const ownerDeclaredByteLimit = 3072 + if maxAgentBriefBytes != ownerDeclaredByteLimit { + t.Fatalf("agent brief byte limit=%d want owner-declared %d", maxAgentBriefBytes, ownerDeclaredByteLimit) + } var boundaryReport map[string]any var unboundedBytes int @@ -264,6 +268,87 @@ func TestAgentBriefCompactsAtDeclaredByteBoundary(t *testing.T) { } } +func TestAgentBriefPreservesBlockedRouteOmissionsAndUnknownReportBlockers(t *testing.T) { + t.Parallel() + + blockedInput := map[string]any{ + "schemaVersion": jsonNumber("1"), + "routeId": "consumer.route.blocked-available", + "goal": "validate_requirement_source", + "mode": "observe", + "availableInputs": []any{ + map[string]any{"kind": "requirement_source", "ref": "docs/specs/example/requirements.v1.json"}, + }, + "observedReports": []any{ + map[string]any{"kind": "requirement_source", "ref": "artifacts/proofkit/source-report.json", "state": "warning"}, + }, + } + report, reportExitCode, err := Build(blockedInput) + if err != nil || reportExitCode != 1 { + t.Fatalf("Build(blocked) exit=%d error=%v", reportExitCode, err) + } + brief, briefExitCode, err := buildBriefEnvelope(blockedInput) + if err != nil || briefExitCode != 1 { + t.Fatalf("BuildEnvelope(blocked) exit=%d error=%v", briefExitCode, err) + } + available := report["summary"].(map[string]any)["availableCommandCount"] + if available == 0 || brief["omissionSummary"].(map[string]any)["availableAlternativeCommandCount"] != available { + t.Fatalf("blocked route lost available command accounting: report=%#v brief=%#v", report["summary"], brief["omissionSummary"]) + } + + unknownInput := map[string]any{ + "schemaVersion": jsonNumber("1"), + "routeId": "consumer.route.unknown-observed", + "goal": "unknown", + "mode": "observe", + "observedReports": []any{ + map[string]any{"kind": "release", "ref": "artifacts/proofkit/release-report.json", "state": "failed"}, + }, + } + unknown, exitCode, err := buildBriefEnvelope(unknownInput) + if err != nil || exitCode != 1 { + t.Fatalf("BuildEnvelope(unknown) exit=%d error=%v", exitCode, err) + } + blockers := unknown["blockers"].([]any) + if len(blockers) != 2 || blockers[0].(map[string]any)["kind"] != "unknown_goal" || blockers[1].(map[string]any)["kind"] != "observed_report" { + t.Fatalf("unknown route did not retain ordered blockers: %#v", blockers) + } +} + +func TestAgentBriefBindsLauncherContextThatAffectsReportDigest(t *testing.T) { + t.Parallel() + + input := map[string]any{ + "schemaVersion": jsonNumber("1"), + "routeId": "consumer.route.launcher", + "goal": "validate_requirement_source", + "mode": "observe", + "availableInputs": []any{ + map[string]any{"kind": "requirement_source", "ref": "docs/specs/example/requirements.v1.json"}, + }, + } + npmRenderer, err := cliexec.AdmitLauncherProfile(cliexec.ProfileNPMOffline, "") + if err != nil { + t.Fatal(err) + } + pathBrief, _, err := BuildEnvelopeModeWithRenderer(input, cliexec.PathRenderer(), EnvelopeModeBrief) + if err != nil { + t.Fatal(err) + } + npmBrief, _, err := BuildEnvelopeModeWithRenderer(input, npmRenderer, EnvelopeModeBrief) + if err != nil { + t.Fatal(err) + } + pathDetail := pathBrief["detailAccess"].(map[string]any) + npmDetail := npmBrief["detailAccess"].(map[string]any) + if pathDetail["launcherProfile"] != cliexec.ProfilePath || npmDetail["launcherProfile"] != cliexec.ProfileNPMOffline { + t.Fatalf("brief launcher profiles are not exact: path=%#v npm=%#v", pathDetail, npmDetail) + } + if pathDetail["sourceReportDigest"] == npmDetail["sourceReportDigest"] { + t.Fatal("launcher-dependent route reports unexpectedly share a digest") + } +} + func TestBuildEnvelopeCapsBlockersAndCountsOmittedDetails(t *testing.T) { t.Parallel() @@ -297,6 +382,22 @@ func TestBuildEnvelopeCapsBlockersAndCountsOmittedDetails(t *testing.T) { } } +func TestBriefBlockerBoundDominatesMapMaterialization(t *testing.T) { + t.Parallel() + + reports := make([]any, 10_000) + for index := range reports { + reports[index] = map[string]any{"kind": "requirement_source", "state": "warning"} + } + blockers, omitted := briefBlockers(map[string]any{ + "observedReports": reports, + "state": "blocked_ambiguous_state", + }, "consumer.route.bounded-work") + if len(blockers) != 4 || cap(blockers) != 4 || omitted != len(reports)-4 { + t.Fatalf("blocker bound did not dominate materialization: len=%d cap=%d omitted=%d", len(blockers), cap(blockers), omitted) + } +} + func TestBuildEnvelopeModeRejectsUnknownMode(t *testing.T) { t.Parallel() diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go index 98ba02e..dd27278 100644 --- a/internal/command/stackpreset/preset_ids_generated.go +++ b/internal/command/stackpreset/preset_ids_generated.go @@ -1,6 +1,6 @@ // Code generated by internal/tools/commandcontractgen; DO NOT EDIT. package stackpreset -const presetContractSourceSHA256 = "28c8f4128ba07378e705693640d42e7c0867043b4c053b3f8874401b5d5d3284" +const presetContractSourceSHA256 = "d5aff223f854d6de95a612d9fe1d421015e27eaa26f0943b02b347021519a3bd" var presetIDs = []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"} diff --git a/internal/tools/coveragemetrics/main.go b/internal/tools/coveragemetrics/main.go index 53f9231..2da9a87 100644 --- a/internal/tools/coveragemetrics/main.go +++ b/internal/tools/coveragemetrics/main.go @@ -513,13 +513,22 @@ func validateRequiredBindingWitnessSelectors(bindings bindingFile) error { "TestAgentRouteEnvelopeModesUseExactRootShapes", }, {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-brief-projection"}: { + "TestAgentBriefBindsLauncherContextThatAffectsReportDigest", "TestAgentBriefClosesEverySelectedCommandInputReference", "TestAgentBriefCompactsAtDeclaredByteBoundary", "TestAgentBriefIsBoundedAndFullEnvelopeRemainsAvailable", "TestAgentBriefNamesCompleteInputBundleBlocker", + "TestAgentBriefPreservesBlockedRouteOmissionsAndUnknownReportBlockers", + "TestBriefBlockerBoundDominatesMapMaterialization", "TestBuildEnvelopeCapsBlockersAndCountsOmittedDetails", "TestBuildEnvelopeCompactsOversizedArgvWithoutLosingActionIdentity", }, + {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-flag-pre-read-admission"}: { + "TestAgentRouteModeAdmissionPrecedesInputRead", + }, + {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-report-contract-closure"}: { + "TestAgentRouteOutputContractPreservesReportSemantics", + }, {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-brief-version-edge"}: { "TestAgentRouteVersionEdgeClosesBriefDefaultMigration", }, @@ -605,7 +614,9 @@ func validateRequiredBindingWitnessSelectors(bindings bindingFile) error { {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-brief-cli-abi"}: "internal/app/cli_abi_test.go", {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-brief-projection"}: "internal/command/agentroute/brief_test.go", {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-brief-version-edge"}: "internal/app/agent_route_version_edge_test.go", + {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-flag-pre-read-admission"}: "internal/app/app_test.go", {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-materialized-ref-admission"}: "internal/command/agentroute/agentroute_test.go", + {"REQ-PROOFKIT-SPEC-026", "proofkit.spec-proof-core.agent-route-report-contract-closure"}: "internal/app/cli_contract_test.go", } if len(requiredPaths) != len(required) { return fmt.Errorf("required selector path inventory=%d, selector inventory=%d", len(requiredPaths), len(required)) diff --git a/internal/tools/packageverify/main.go b/internal/tools/packageverify/main.go index a4e56c7..8b9bd9d 100644 --- a/internal/tools/packageverify/main.go +++ b/internal/tools/packageverify/main.go @@ -18,6 +18,7 @@ import ( pathpkg "path" "path/filepath" "regexp" + "slices" "sort" "strings" "time" @@ -1434,14 +1435,14 @@ func verifyCLIContractBoundaryPolicyClosure(content string, textEntries map[stri continue } refs := stringArrayField(brief, "boundaryPolicyRefs") - if len(refs) == 0 { - return fmt.Errorf("package CLI brief contract boundaryPolicyRefs must be non-empty") - } for _, requirementID := range refs { if requirementIDCounts[requirementID] != 1 { return fmt.Errorf("package CLI brief boundary policy ref must resolve to exactly one shipped requirement") } } + if !slices.Equal(refs, []string{"REQ-PROOFKIT-SPEC-005", "REQ-PROOFKIT-SPEC-026"}) { + return fmt.Errorf("package CLI brief contract boundaryPolicyRefs must equal the exact canonical policy set") + } } return nil } diff --git a/internal/tools/packageverify/main_test.go b/internal/tools/packageverify/main_test.go index 4a5f4c4..09e1353 100644 --- a/internal/tools/packageverify/main_test.go +++ b/internal/tools/packageverify/main_test.go @@ -750,6 +750,21 @@ func TestPackagePublicReferenceClosure(t *testing.T) { }, want: "boundary policy ref must resolve to exactly one shipped requirement", }, + { + name: "incomplete brief boundary policy set", + mutate: func(entries map[string]string) { + entries["package/proofkit/cli-contract.v2.json"] = strings.Replace(entries["package/proofkit/cli-contract.v2.json"], `"REQ-PROOFKIT-SPEC-005","REQ-PROOFKIT-SPEC-026"`, `"REQ-PROOFKIT-SPEC-005"`, 1) + }, + want: "must equal the exact canonical policy set", + }, + { + name: "surplus resolvable brief boundary policy ref", + mutate: func(entries map[string]string) { + entries["package/docs/specs/example/requirements.v1.json"] = strings.Replace(entries["package/docs/specs/example/requirements.v1.json"], `]}`, `,{"requirementId":"REQ-OTHER"}]}`, 1) + entries["package/proofkit/cli-contract.v2.json"] = strings.Replace(entries["package/proofkit/cli-contract.v2.json"], `"REQ-PROOFKIT-SPEC-005","REQ-PROOFKIT-SPEC-026"`, `"REQ-PROOFKIT-SPEC-005","REQ-PROOFKIT-SPEC-026","REQ-OTHER"`, 1) + }, + want: "must equal the exact canonical policy set", + }, { name: "dangling binding witness path", mutate: func(entries map[string]string) { diff --git a/internal/tools/releasechange/record_test.go b/internal/tools/releasechange/record_test.go index d0fee8e..35c0932 100644 --- a/internal/tools/releasechange/record_test.go +++ b/internal/tools/releasechange/record_test.go @@ -197,7 +197,9 @@ func TestCurrentChangeRecordNamesReviewedSemanticChanges(t *testing.T) { var currentBreakingChanges = []Change{ {ChangeID: "proofkit.agent-route.brief-default", Summary: "Change bare agent-route --agent-envelope output from the generic full envelope to the bounded proofkit.agent-route.brief packet."}, {ChangeID: "proofkit.agent-route.materialized-artifact-refs", Summary: "Reject the stdin transport sentinel as an available-input or observed-report artifact identity; route references must name materialized caller-owned artifacts."}, + {ChangeID: "proofkit.agent-route.report-schema-v3", Summary: "Advance the bare agent-route report to schemaVersion 3 while preserving route-family semantics and adding exact launcher-profile and pre-suppression available-command context."}, {ChangeID: "proofkit.platform.macos-13", Summary: "Raise published Darwin package compatibility to macOS 13.0 so wheel tags remain truthful for binaries built with Go 1.27.1."}, + {ChangeID: "proofkit.toolchain.npm-12", Summary: "Require npm 12.0.2 for repository-owned contributor and release scripts and consume npm view and pack evidence using npm 12 wire forms."}, } var currentAdditions = []Change{ @@ -207,8 +209,10 @@ var currentAdditions = []Change{ var currentMigrationSteps = []string{ "Consumers that require the former generic agent-route envelope must add --agent-envelope-mode full after --agent-envelope; consumers that accept bounded route guidance may keep bare --agent-envelope.", + "Consumers of the bare agent-route report must admit schemaVersion 3; the prior route-family fields remain, and summary now binds launcherProfile and availableCommandCount.", "Consumers that used the stdin transport sentinel as an agent-route availableInputs or observedReports ref must materialize that artifact and pass its safe repo-relative path instead.", - "Darwin consumers must use macOS 13.0 or later; contributors that build from source must use Go 1.27.1.", + "Darwin consumers must use macOS 13.0 or later.", + "Contributors must use Go 1.27.1, Node 26.8.1, npm 12.0.2, and Python 3.14.7 for repository-owned build and release workflows.", } func validateCurrentChangeRecord(record Record, notes string) error { diff --git a/internal/tools/releasepreflight/main.go b/internal/tools/releasepreflight/main.go index f949389..19c6fc7 100644 --- a/internal/tools/releasepreflight/main.go +++ b/internal/tools/releasepreflight/main.go @@ -131,8 +131,8 @@ func run(args []string) error { if err != nil { return fmt.Errorf("decode npm expected metadata: %w", err) } - var actual npmView - if err := readJSON(options["actual-file"], &actual); err != nil { + actual, err := readNPMViewRecord[npmView](options["actual-file"]) + if err != nil { return err } return compareNPMExisting(expected, actual) @@ -145,8 +145,8 @@ func run(args []string) error { if err != nil { return err } - var latest npmReleaseIdentity - if err := readJSON(options["latest-file"], &latest); err != nil { + latest, err := readNPMViewRecord[npmReleaseIdentity](options["latest-file"]) + if err != nil { return err } return validateNPMReleaseLineage( @@ -654,6 +654,22 @@ func readJSON(path string, out any) error { return nil } +func readNPMViewRecord[T any](path string) (T, error) { + var zero T + content, err := os.ReadFile(path) + if err != nil { + return zero, fmt.Errorf("read %s: %w", path, err) + } + records, err := admission.DecodeTypedJSON[[]T](bytes.NewReader(content), maxReleaseJSONBytes) + if err != nil { + return zero, fmt.Errorf("decode %s as npm 12 view output: %w", path, err) + } + if len(records) != 1 { + return zero, fmt.Errorf("decode %s as npm 12 view output: expected exactly one record, got %d", path, len(records)) + } + return records[0], nil +} + func parseFlags(args []string, required ...string) (map[string]string, error) { admitted := map[string]struct{}{} for _, key := range required { diff --git a/internal/tools/releasepreflight/main_test.go b/internal/tools/releasepreflight/main_test.go index 53b3bd4..c9d6713 100644 --- a/internal/tools/releasepreflight/main_test.go +++ b/internal/tools/releasepreflight/main_test.go @@ -174,7 +174,7 @@ func TestRunNPMLineageUsesAdmittedRecordAndProviderIdentity(t *testing.T) { "knownLimitations": [], "rollback": {"strategy": "previous_admitted_version"} }`) - writeFile(t, latestPath, `{"name":"@research-engineering/agentic-proofkit","version":"0.2.0"}`) + writeFile(t, latestPath, `[{"name":"@research-engineering/agentic-proofkit","version":"0.2.0"}]`) args := []string{ "npm-lineage", "--change-record-file", recordPath, @@ -187,7 +187,7 @@ func TestRunNPMLineageUsesAdmittedRecordAndProviderIdentity(t *testing.T) { t.Fatalf("run(npm-lineage) error = %v", err) } - writeFile(t, latestPath, `{"name":"@research-engineering/agentic-proofkit","version":"0.1.160"}`) + writeFile(t, latestPath, `[{"name":"@research-engineering/agentic-proofkit","version":"0.1.160"}]`) if err := run(args); err == nil || !strings.Contains(err.Error(), "expected 0.2.0") { t.Fatalf("run(npm-lineage) error = %v, want lineage gap rejection", err) } @@ -219,7 +219,7 @@ func TestCompareNPMCandidateArtifactsBindsDownloadedBytes(t *testing.T) { func TestRunNPMExistingRejectsAmbiguousExpectedJSON(t *testing.T) { actualFile := filepath.Join(t.TempDir(), "npm-view.json") - if err := os.WriteFile(actualFile, []byte(`{"name":"agentic-proofkit","version":"1.2.3","dist":{"shasum":"sha","integrity":"integrity"}}`), 0o600); err != nil { + if err := os.WriteFile(actualFile, []byte(`[{"name":"agentic-proofkit","version":"1.2.3","dist":{"shasum":"sha","integrity":"integrity"}}]`), 0o600); err != nil { t.Fatalf("write npm view: %v", err) } err := run([]string{ @@ -232,6 +232,38 @@ func TestRunNPMExistingRejectsAmbiguousExpectedJSON(t *testing.T) { } } +func TestRunNPMExistingAdmitsExactlyOneNPM12ViewRecord(t *testing.T) { + root := t.TempDir() + actualFile := filepath.Join(root, "npm-view.json") + expectedJSON := `{"name":"agentic-proofkit","version":"1.2.3","filename":"agentic-proofkit.tgz","shasum":"sha","integrity":"integrity"}` + args := []string{"npm-existing", "--expected-json", expectedJSON, "--actual-file", actualFile} + + writeFile(t, actualFile, `[{"name":"agentic-proofkit","version":"1.2.3","dist":{"shasum":"sha","integrity":"integrity"}}]`) + if err := run(args); err != nil { + t.Fatalf("run(npm-existing) error=%v", err) + } + + invalid := []struct { + name string + content string + want string + }{ + {name: "legacy object", content: `{"name":"agentic-proofkit","version":"1.2.3"}`, want: "npm 12 view output"}, + {name: "empty array", content: `[]`, want: "expected exactly one record, got 0"}, + {name: "multiple records", content: `[{"name":"agentic-proofkit"},{"name":"other"}]`, want: "expected exactly one record, got 2"}, + {name: "duplicate key", content: `[{"name":"agentic-proofkit","name":"other","version":"1.2.3"}]`, want: "duplicate object key"}, + } + for _, item := range invalid { + t.Run(item.name, func(t *testing.T) { + writeFile(t, actualFile, item.content) + err := run(args) + if err == nil || !strings.Contains(err.Error(), item.want) { + t.Fatalf("run(npm-existing) error=%v, want %q", err, item.want) + } + }) + } +} + func TestComparePyPIExisting(t *testing.T) { candidates := pythonPackageSet{Packages: []wheelRecord{{Filename: "agentic_proofkit-1.2.3-py3-none-any.whl", Sha256: "sha"}}} registry := pypiResponse{URLs: []pypiFile{{Filename: "agentic_proofkit-1.2.3-py3-none-any.whl"}}} diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index c267b8f..99aea2e 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -532,7 +532,7 @@ "rootDefinitionDigest": "sha256:fd1121285d1b357ed848febc248700a75417d06619e22ed36991631d7526375b", "nativeSource": { "path": "internal/command/agentroute", - "canonicalDigest": "sha256:3164cabec37654591d8e3c88444734d1584647d07b12e149ae17a95ad5b27d8a", + "canonicalDigest": "sha256:a2359fd9f5a667a0da31e5b12331239ce77d9d997205c85d78ad8771034f0369", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -564,6 +564,49 @@ "contractId": "proofkit.agent-route.output.v3", "schemaVersion": 3, "authority": "deterministic route report, brief packet, full envelope, or invalid-input repair packet selected from the admitted invocation", + "reportContract": { + "contractId": "proofkit.agent-route.report.v3", + "schemaVersion": 3, + "requiredFields": [ + "guidanceSlice", + "reportId", + "reportKind", + "schemaVersion", + "selectedRouteFamily", + "state", + "summary" + ], + "fields": { + "schemaVersion": { + "value": 3 + }, + "selectedRouteFamily": { + "enum": [ + "adoption", + "migration", + "release_and_deployment", + "rendered_views", + "repository_structure", + "requirement_proof_binding", + "requirement_source", + "selective_evidence", + "selective_planning", + "test_inventory_and_coverage", + "unknown" + ] + }, + "guidanceSlice": { + "requiredFields": [ + "routeFamily" + ], + "routeFamilyRule": "must equal selectedRouteFamily" + }, + "summary": { + "availableCommandCount": "exact count before blocked-state command suppression", + "launcherProfile": "exact admitted command-renderer profile that affects route argv and report identity" + } + } + }, "briefPacketContract": { "boundaryPolicyRefs": [ "REQ-PROOFKIT-SPEC-005", @@ -571,10 +614,10 @@ ], "contractId": "proofkit.agent-route.brief.v1", "fieldRules": { - "blockers": "required-input blockers followed by sorted non-passed observed-report blockers; retain at most four and count the exact remainder", + "blockers": "unknown-goal blocker when applicable, then required-input blockers, then sorted non-passed observed-report blockers; retain at most four and count the exact remainder without materializing the omitted blocker maps", "boundaryPolicyRefs": "exactly the shipped and uniquely resolvable requirement IDs REQ-PROOFKIT-SPEC-005 and REQ-PROOFKIT-SPEC-026; policy prose is not duplicated", "contextRefs": "only caller-owned artifact operands of the selected next command, each addressed by an RFC 6901 pointer into the source report", - "detailAccess": "binds source report ID and stable digest and provides exact outputArgs suffixes for report and full retrieval from the original admitted input", + "detailAccess": "binds source report ID, stable digest, and launcher profile and provides exact outputArgs suffixes for report and full retrieval from the original admitted input and launcher context", "nextAction": "null for blocked states; otherwise the first canonical nextCommands entry with exact command identity and inline argv unless the byte bound requires argvState=detail_required", "omissionSummary": "exact counts for unselected available commands, source-omitted commands, and blockers omitted by the packet bound", "packetKind": "proofkit.agent-route.brief", @@ -604,12 +647,12 @@ "changesFromV2": [ "bare --agent-envelope emits proofkit.agent-route.brief", "--agent-envelope-mode full preserves the prior generic envelope projection", - "route report schemaVersion advances to 3 without changing its semantic fields" + "route report schemaVersion advances to 3 while preserving prior route-family semantics and adding exact launcher and available-command context" ], "rootType": "object", "closed": true, "rootDefinitionRef": "proofkit.agent-route.output.v3.root-shape", - "rootDefinitionDigest": "sha256:3a48ba6391239e9bce6b6dfcc1c19831fb1029b4cdbb4b19e3af4d944f8275ce", + "rootDefinitionDigest": "sha256:0881bc0765f61ea76d681a8bb2ddd915504c5a350a3a8286253e2959693be60e", "nativeSources": [ { "path": "internal/app", @@ -618,7 +661,7 @@ }, { "path": "internal/command/agentroute", - "canonicalDigest": "sha256:3164cabec37654591d8e3c88444734d1584647d07b12e149ae17a95ad5b27d8a", + "canonicalDigest": "sha256:a2359fd9f5a667a0da31e5b12331239ce77d9d997205c85d78ad8771034f0369", "evidenceClass": "source_checkout" } ], @@ -7914,6 +7957,7 @@ "guidanceSlice", "nextCommands", "nonClaims", + "observedReports", "omitted", "reportId", "reportKind", @@ -7931,6 +7975,7 @@ "guidanceSlice", "nextCommands", "nonClaims", + "observedReports", "omitted", "reportId", "reportKind", @@ -7949,7 +7994,7 @@ } ] }, - "canonicalDigest": "sha256:3a48ba6391239e9bce6b6dfcc1c19831fb1029b4cdbb4b19e3af4d944f8275ce" + "canonicalDigest": "sha256:0881bc0765f61ea76d681a8bb2ddd915504c5a350a3a8286253e2959693be60e" }, { "definitionId": "proofkit.binding-partition.input.v1.root-shape", diff --git a/proofkit/requirement-bindings.json b/proofkit/requirement-bindings.json index 9ed12e3..8e2f86e 100644 --- a/proofkit/requirement-bindings.json +++ b/proofkit/requirement-bindings.json @@ -710,7 +710,7 @@ "specPath": "docs/specs/proofkit-spec-proof-core/requirements.v1.json", "claimLevel": "blocking", "proofState": "witness_backed", - "nonClaims": ["The 3072-byte packet bound and stable source-report digest do not prove provider token consumption, semantic sufficiency, source freshness, command execution, native witness truth, or policy approval; detail retrieval requires the caller to retain the original admitted input."] + "nonClaims": ["The 3072-byte packet bound and stable source-report digest do not prove provider token consumption, semantic sufficiency, source freshness, command execution, native witness truth, or policy approval; detail retrieval requires the caller to retain the original admitted input and launcher context."] }, { "requirementId": "REQ-PROOFKIT-WORKFLOW-001", @@ -4946,28 +4946,40 @@ "witnessPath": "internal/command/agentroute/brief_test.go", "witnessSelectors": [ { - "selector": "TestAgentBriefIsBoundedAndFullEnvelopeRemainsAvailable", - "command": "go test ./internal/command/agentroute -run '^TestAgentBriefIsBoundedAndFullEnvelopeRemainsAvailable$'" + "selector": "TestAgentBriefBindsLauncherContextThatAffectsReportDigest", + "command": "go test ./internal/command/agentroute -run '^TestAgentBriefBindsLauncherContextThatAffectsReportDigest$'" }, { "selector": "TestAgentBriefClosesEverySelectedCommandInputReference", "command": "go test ./internal/command/agentroute -run '^TestAgentBriefClosesEverySelectedCommandInputReference$'" }, + { + "selector": "TestAgentBriefCompactsAtDeclaredByteBoundary", + "command": "go test ./internal/command/agentroute -run '^TestAgentBriefCompactsAtDeclaredByteBoundary$'" + }, + { + "selector": "TestAgentBriefIsBoundedAndFullEnvelopeRemainsAvailable", + "command": "go test ./internal/command/agentroute -run '^TestAgentBriefIsBoundedAndFullEnvelopeRemainsAvailable$'" + }, { "selector": "TestAgentBriefNamesCompleteInputBundleBlocker", "command": "go test ./internal/command/agentroute -run '^TestAgentBriefNamesCompleteInputBundleBlocker$'" }, { - "selector": "TestBuildEnvelopeCompactsOversizedArgvWithoutLosingActionIdentity", - "command": "go test ./internal/command/agentroute -run '^TestBuildEnvelopeCompactsOversizedArgvWithoutLosingActionIdentity$'" + "selector": "TestAgentBriefPreservesBlockedRouteOmissionsAndUnknownReportBlockers", + "command": "go test ./internal/command/agentroute -run '^TestAgentBriefPreservesBlockedRouteOmissionsAndUnknownReportBlockers$'" }, { - "selector": "TestAgentBriefCompactsAtDeclaredByteBoundary", - "command": "go test ./internal/command/agentroute -run '^TestAgentBriefCompactsAtDeclaredByteBoundary$'" + "selector": "TestBriefBlockerBoundDominatesMapMaterialization", + "command": "go test ./internal/command/agentroute -run '^TestBriefBlockerBoundDominatesMapMaterialization$'" }, { "selector": "TestBuildEnvelopeCapsBlockersAndCountsOmittedDetails", "command": "go test ./internal/command/agentroute -run '^TestBuildEnvelopeCapsBlockersAndCountsOmittedDetails$'" + }, + { + "selector": "TestBuildEnvelopeCompactsOversizedArgvWithoutLosingActionIdentity", + "command": "go test ./internal/command/agentroute -run '^TestBuildEnvelopeCompactsOversizedArgvWithoutLosingActionIdentity$'" } ], "commandIds": ["proofkit.go-test"], @@ -5003,6 +5015,21 @@ "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] }, + { + "requirementId": "REQ-PROOFKIT-SPEC-026", + "scenarioId": "proofkit.spec-proof-core.agent-route-report-contract-closure", + "witnessId": "proofkit.agent-route.report-contract-closure-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/cli_contract_test.go", + "witnessSelectors": [ + { + "selector": "TestAgentRouteOutputContractPreservesReportSemantics", + "command": "go test ./internal/app -run '^TestAgentRouteOutputContractPreservesReportSemantics$'" + } + ], + "commandIds": ["proofkit.go-test"], + "environmentClasses": ["local-go"] + }, { "requirementId": "REQ-PROOFKIT-SPEC-026", "scenarioId": "proofkit.spec-proof-core.agent-route-brief-version-edge", diff --git a/release/change-record.v2.json b/release/change-record.v2.json index dd1513a..82094fc 100644 --- a/release/change-record.v2.json +++ b/release/change-record.v2.json @@ -12,9 +12,17 @@ "changeId": "proofkit.agent-route.materialized-artifact-refs", "summary": "Reject the stdin transport sentinel as an available-input or observed-report artifact identity; route references must name materialized caller-owned artifacts." }, + { + "changeId": "proofkit.agent-route.report-schema-v3", + "summary": "Advance the bare agent-route report to schemaVersion 3 while preserving route-family semantics and adding exact launcher-profile and pre-suppression available-command context." + }, { "changeId": "proofkit.platform.macos-13", "summary": "Raise published Darwin package compatibility to macOS 13.0 so wheel tags remain truthful for binaries built with Go 1.27.1." + }, + { + "changeId": "proofkit.toolchain.npm-12", + "summary": "Require npm 12.0.2 for repository-owned contributor and release scripts and consume npm view and pack evidence using npm 12 wire forms." } ], "additions": [ @@ -31,8 +39,10 @@ "required": true, "steps": [ "Consumers that require the former generic agent-route envelope must add --agent-envelope-mode full after --agent-envelope; consumers that accept bounded route guidance may keep bare --agent-envelope.", + "Consumers of the bare agent-route report must admit schemaVersion 3; the prior route-family fields remain, and summary now binds launcherProfile and availableCommandCount.", "Consumers that used the stdin transport sentinel as an agent-route availableInputs or observedReports ref must materialize that artifact and pass its safe repo-relative path instead.", - "Darwin consumers must use macOS 13.0 or later; contributors that build from source must use Go 1.27.1." + "Darwin consumers must use macOS 13.0 or later.", + "Contributors must use Go 1.27.1, Node 26.8.1, npm 12.0.2, and Python 3.14.7 for repository-owned build and release workflows." ] }, "platformRequirements": [ From 7d98eb72ea3c4ef7d672e7e5e8c2692ea961a695 Mon Sep 17 00:00:00 2001 From: iperev Date: Thu, 3 Sep 2026 00:57:52 +0200 Subject: [PATCH 4/9] fix: close agent route proof gaps --- internal/app/agent_route_version_edge_test.go | 51 +++++---- internal/app/cli_contract_test.go | 2 +- internal/app/command_contract_generated.go | 4 +- .../app/testdata/v0.6-wire-observations.json | 21 +--- internal/command/agentroute/brief.go | 60 ++++++---- internal/command/agentroute/brief_test.go | 104 +++++++++++++----- .../stackpreset/preset_ids_generated.go | 2 +- internal/tools/coveragemetrics/main_test.go | 6 + internal/tools/packageverify/main.go | 10 +- internal/tools/packageverify/main_test.go | 20 +++- proofkit/cli-contract.v2.json | 7 +- 11 files changed, 191 insertions(+), 96 deletions(-) diff --git a/internal/app/agent_route_version_edge_test.go b/internal/app/agent_route_version_edge_test.go index aad9eec..3d505a8 100644 --- a/internal/app/agent_route_version_edge_test.go +++ b/internal/app/agent_route_version_edge_test.go @@ -2,6 +2,7 @@ package app import ( "bytes" + "crypto/sha256" "encoding/json" "fmt" "os" @@ -10,18 +11,18 @@ import ( "testing" "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/tools/releasechange" ) const agentRouteVersionEdgePath = "internal/app/testdata/v0.6-wire-observations.json" type agentRouteVersionEdge struct { - AdditionChangeIDs []string `json:"additionChangeIds"` - BreakingChangeIDs []string `json:"breakingChangeIds"` ChangedCommandContract agentRouteChangedCommandContract `json:"changedCommandContract"` + ChangeRecordRef string `json:"changeRecordRef"` + ChangeRecordSHA256 string `json:"changeRecordSha256"` CurrentPublicABISHA256 string `json:"currentPublicAbiSha256"` EdgeID string `json:"edgeId"` EvidenceClass string `json:"evidenceClass"` - MigrationSteps []string `json:"migrationSteps"` NonClaims []string `json:"nonClaims"` PreviousPublicABISHA256 string `json:"previousPublicAbiSha256"` PreviousVersion string `json:"previousVersion"` @@ -39,7 +40,8 @@ type agentRouteChangedCommandContract struct { func TestAgentRouteVersionEdgeClosesBriefDefaultMigration(t *testing.T) { record := readAgentRouteVersionEdge(t) - if err := validateAgentRouteVersionEdge(record); err != nil { + root := repoRoot(t) + if err := validateAgentRouteVersionEdge(record, root); err != nil { t.Fatal(err) } @@ -54,17 +56,14 @@ func TestAgentRouteVersionEdgeClosesBriefDefaultMigration(t *testing.T) { {name: "current output contract", mutate: func(value *agentRouteVersionEdge) { value.ChangedCommandContract.CurrentOutputContractSHA256 += "0" }}, {name: "previous input contract", mutate: func(value *agentRouteVersionEdge) { value.ChangedCommandContract.PreviousInputContractSHA256 += "0" }}, {name: "previous output contract", mutate: func(value *agentRouteVersionEdge) { value.ChangedCommandContract.PreviousOutputContractSHA256 += "0" }}, - {name: "breaking owner", mutate: func(value *agentRouteVersionEdge) { value.BreakingChangeIDs[0] += ".drift" }}, - {name: "missing breaking owner", mutate: func(value *agentRouteVersionEdge) { value.BreakingChangeIDs = value.BreakingChangeIDs[:1] }}, - {name: "addition owner", mutate: func(value *agentRouteVersionEdge) { value.AdditionChangeIDs[0] += ".drift" }}, - {name: "migration", mutate: func(value *agentRouteVersionEdge) { value.MigrationSteps[0] += " Drift." }}, - {name: "missing migration", mutate: func(value *agentRouteVersionEdge) { value.MigrationSteps = value.MigrationSteps[:1] }}, + {name: "change record reference", mutate: func(value *agentRouteVersionEdge) { value.ChangeRecordRef += ".drift" }}, + {name: "change record digest", mutate: func(value *agentRouteVersionEdge) { value.ChangeRecordSHA256 += "0" }}, } for _, mutant := range mutants { t.Run(mutant.name, func(t *testing.T) { value := cloneAgentRouteVersionEdge(record) mutant.mutate(&value) - if err := validateAgentRouteVersionEdge(value); err == nil { + if err := validateAgentRouteVersionEdge(value, root); err == nil { t.Fatal("version-edge mutant was admitted") } }) @@ -85,7 +84,7 @@ func readAgentRouteVersionEdge(t *testing.T) agentRouteVersionEdge { if !ok { t.Fatal("agent-route version edge must be an object") } - assertExactObjectKeys(t, root, []string{"additionChangeIds", "breakingChangeIds", "changedCommandContract", "currentPublicAbiSha256", "edgeId", "evidenceClass", "migrationSteps", "nonClaims", "previousPublicAbiSha256", "previousVersion", "schemaVersion", "version"}, "agent-route version edge") + assertExactObjectKeys(t, root, []string{"changedCommandContract", "changeRecordRef", "changeRecordSha256", "currentPublicAbiSha256", "edgeId", "evidenceClass", "nonClaims", "previousPublicAbiSha256", "previousVersion", "schemaVersion", "version"}, "agent-route version edge") contract, ok := root["changedCommandContract"].(map[string]any) if !ok { t.Fatal("agent-route version edge changedCommandContract must be an object") @@ -98,7 +97,7 @@ func readAgentRouteVersionEdge(t *testing.T) agentRouteVersionEdge { return decoded } -func validateAgentRouteVersionEdge(record agentRouteVersionEdge) error { +func validateAgentRouteVersionEdge(record agentRouteVersionEdge, root string) error { if record.SchemaVersion != 1 || record.EdgeID != "proofkit.public-wire.0.5.1-to-0.6.0" || record.EvidenceClass != "owner_authored_frozen_version_edge_observation" { return fmt.Errorf("agent-route version-edge identity is invalid") } @@ -122,15 +121,24 @@ func validateAgentRouteVersionEdge(record agentRouteVersionEdge) error { if record.ChangedCommandContract != wantContract { return fmt.Errorf("agent-route version-edge changed command contract is not exact") } - if !slices.Equal(record.BreakingChangeIDs, []string{"proofkit.agent-route.brief-default", "proofkit.agent-route.materialized-artifact-refs", "proofkit.agent-route.report-schema-v3"}) || !slices.Equal(record.AdditionChangeIDs, []string{"proofkit.agent-route.envelope-detail-mode"}) { - return fmt.Errorf("agent-route version-edge change owners are not exact") + if record.ChangeRecordRef != "release/change-record.v2.json" { + return fmt.Errorf("agent-route version-edge change record reference is not exact") } - if !slices.Equal(record.MigrationSteps, []string{ - "Consumers that require the former generic agent-route envelope must add --agent-envelope-mode full after --agent-envelope; consumers that accept bounded route guidance may keep bare --agent-envelope.", - "Consumers of the bare agent-route report must admit schemaVersion 3; the prior route-family fields remain, and summary now binds launcherProfile and availableCommandCount.", - "Consumers that used the stdin transport sentinel as an agent-route availableInputs or observedReports ref must materialize that artifact and pass its safe repo-relative path instead.", - }) { - return fmt.Errorf("agent-route version-edge migration is not exact") + changeRecordPath := filepath.Join(root, filepath.FromSlash(record.ChangeRecordRef)) + changeRecordContent, err := os.ReadFile(changeRecordPath) + if err != nil { + return fmt.Errorf("read agent-route version-edge change record: %w", err) + } + changeRecordDigest := sha256.Sum256(changeRecordContent) + if record.ChangeRecordSHA256 != fmt.Sprintf("sha256:%x", changeRecordDigest) { + return fmt.Errorf("agent-route version-edge change record digest is not exact") + } + changeRecord, err := releasechange.Read(changeRecordPath) + if err != nil { + return fmt.Errorf("admit agent-route version-edge change record: %w", err) + } + if changeRecord.PreviousVersion != record.PreviousVersion || changeRecord.Version != record.Version || !changeRecord.Migration.Required { + return fmt.Errorf("agent-route version-edge change record identity is inconsistent") } if !slices.Equal(record.NonClaims, []string{"This owner-authored version-edge observation binds reviewed public contract identities; it does not authenticate Git history, registry publication, provider ingestion, native witness truth, rollout, or production readiness."}) { return fmt.Errorf("agent-route version-edge non-claims are not exact") @@ -139,9 +147,6 @@ func validateAgentRouteVersionEdge(record agentRouteVersionEdge) error { } func cloneAgentRouteVersionEdge(record agentRouteVersionEdge) agentRouteVersionEdge { - record.AdditionChangeIDs = append([]string(nil), record.AdditionChangeIDs...) - record.BreakingChangeIDs = append([]string(nil), record.BreakingChangeIDs...) - record.MigrationSteps = append([]string(nil), record.MigrationSteps...) record.NonClaims = append([]string(nil), record.NonClaims...) return record } diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index 554693d..26d1d71 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -23,7 +23,7 @@ import ( ) const ( - cliContractPublicABISHA256 = "ae7614f6c05a84cae1d3c8eda4a8426df0ceb5696d1548fe20d9ffc6ec4a3f53" + cliContractPublicABISHA256 = "163f06bf6fc94f15040fecf3e352d4600a8611a227e26f35369b7fe97e90bde5" maxAggregateFileReadBytesForContractTest = 64 << 20 maxPackageManifestBytesForContractTest = 256 << 10 maxSourceFileBytesForContractTest = 8 << 20 diff --git a/internal/app/command_contract_generated.go b/internal/app/command_contract_generated.go index 0aedd87..d4f23e0 100644 --- a/internal/app/command_contract_generated.go +++ b/internal/app/command_contract_generated.go @@ -1,7 +1,7 @@ // Code generated by internal/tools/commandcontractgen; DO NOT EDIT. package app -const commandContractSourceSHA256 = "d5aff223f854d6de95a612d9fe1d421015e27eaa26f0943b02b347021519a3bd" +const commandContractSourceSHA256 = "ca3151470f502bf1c00d33c92b9d4bbce29805612342787aa89de10d36c7ad89" type generatedCommandContractMetadata struct { InputContractSHA256 string @@ -15,7 +15,7 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "adoption-contract-envelope": {InputContractSHA256: "sha256:c310214676ff4b6f536a5bc9d687f681a7e71f73d7a03ac932707d8cd3905cdf", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.adoption-contract-envelope.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3efb2c5161fee16fd8ac6a40dcb6d9c41fbc23e468f60621436ae9e8076e0950", FlagChoices: map[string][]string{}}, "adoption-doctor": {InputContractSHA256: "sha256:efa9acfe32bff07f56d9dc9902530df2979794289bc2f7f547f7a108a7dd0f35", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.adoption-doctor.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:8fdfc6608f197e633f042f20031ae1014872a90aa3daa66885ffcaddca994766", FlagChoices: map[string][]string{}}, "adoption-workflow-plan": {InputContractSHA256: "sha256:b32ae67179d7b6dcf1ea66cb6b2b2691c8367ce2e2be367619b65973166da55c", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.adoption-workflow-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:8d64cb53ebd0307e3cebc3435286a3d2a1ee8a0ad6f7514fc0fb3285db0f565b", FlagChoices: map[string][]string{}}, - "agent-route": {InputContractSHA256: "sha256:b44e2273d237a4533d01ab008a52d205dc2835e87ba5b3731a30db1748ead659", InputSchemaSummary: []string{"availableInputs", "browserMode", "goal", "knownChangedPaths", "mode", "nonClaims", "observedReports", "openBrowser", "routeId", "schemaVersion", "root-shape-only definition proofkit.agent-route.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:2fb98bdbc655211644c89e160a51380d58783dd2a393befffb7ddc8dfb4f79da", FlagChoices: map[string][]string{}}, + "agent-route": {InputContractSHA256: "sha256:4fc7b2e5ffe3ed632e5e84d20e5ae26f9ace11df614bc9aec680853e60809ebd", InputSchemaSummary: []string{"availableInputs", "browserMode", "goal", "knownChangedPaths", "mode", "nonClaims", "observedReports", "openBrowser", "routeId", "schemaVersion", "root-shape-only definition proofkit.agent-route.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:485d62afc2e5ed07c28f557b0d1069f167b3838abe0aed248e9ff94f3e25c0ad", FlagChoices: map[string][]string{}}, "binding-partition": {InputContractSHA256: "sha256:366ad082045af52b2ac6604f18626d0f285b2db73b45d9a82687b8d3b0d2b3fd", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.binding-partition.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:52840879e13a00ef9a4abaad6cdb33000511674d5f9003fb56f387fdf58fadc8", FlagChoices: map[string][]string{}}, "branch-authority": {InputContractSHA256: "sha256:8a3ed74978898593fbdbf1f7fa684dae450fbd9019edcd60d07f818d63363ed4", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.branch-authority.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:3c7dc74842299b92cd5baf57cc8666e9415963091359e5faf654e28da89561f1", FlagChoices: map[string][]string{}}, "capability-map-admission": {InputContractSHA256: "sha256:efe87497cf6a32cfd1f9c9c191393cac8781d1e9211914a8a7b6aaa44e7f371d", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.capability-map-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:bfb32535fd51bed168111912acb1651527897fe59a592119dc0032ce9286c9db", FlagChoices: map[string][]string{}}, diff --git a/internal/app/testdata/v0.6-wire-observations.json b/internal/app/testdata/v0.6-wire-observations.json index bc5f7b0..41da08f 100644 --- a/internal/app/testdata/v0.6-wire-observations.json +++ b/internal/app/testdata/v0.6-wire-observations.json @@ -4,28 +4,17 @@ "previousVersion": "0.5.1", "version": "0.6.0", "evidenceClass": "owner_authored_frozen_version_edge_observation", + "changeRecordRef": "release/change-record.v2.json", + "changeRecordSha256": "sha256:053f459a5d9d8d1568da531d566adb185541bb85167a5867c7c4074f99f8f5ec", "previousPublicAbiSha256": "sha256:9ecd2c3d2f3f360088409f7e91cce406fc1d1d6edda1b404fce119985c4fb623", - "currentPublicAbiSha256": "sha256:ae7614f6c05a84cae1d3c8eda4a8426df0ceb5696d1548fe20d9ffc6ec4a3f53", + "currentPublicAbiSha256": "sha256:163f06bf6fc94f15040fecf3e352d4600a8611a227e26f35369b7fe97e90bde5", "changedCommandContract": { "command": "agent-route", "previousInputContractSha256": "sha256:6b5af8287f2972bbef4c68c247f43fb16d0f0d8739e5e6d3a66543af20d2644d", - "currentInputContractSha256": "sha256:b44e2273d237a4533d01ab008a52d205dc2835e87ba5b3731a30db1748ead659", + "currentInputContractSha256": "sha256:4fc7b2e5ffe3ed632e5e84d20e5ae26f9ace11df614bc9aec680853e60809ebd", "previousOutputContractSha256": "sha256:44ec313a43360b6138ad6c3ae5de4abd51bbf312060880c108a6351606695915", - "currentOutputContractSha256": "sha256:2fb98bdbc655211644c89e160a51380d58783dd2a393befffb7ddc8dfb4f79da" + "currentOutputContractSha256": "sha256:485d62afc2e5ed07c28f557b0d1069f167b3838abe0aed248e9ff94f3e25c0ad" }, - "breakingChangeIds": [ - "proofkit.agent-route.brief-default", - "proofkit.agent-route.materialized-artifact-refs", - "proofkit.agent-route.report-schema-v3" - ], - "additionChangeIds": [ - "proofkit.agent-route.envelope-detail-mode" - ], - "migrationSteps": [ - "Consumers that require the former generic agent-route envelope must add --agent-envelope-mode full after --agent-envelope; consumers that accept bounded route guidance may keep bare --agent-envelope.", - "Consumers of the bare agent-route report must admit schemaVersion 3; the prior route-family fields remain, and summary now binds launcherProfile and availableCommandCount.", - "Consumers that used the stdin transport sentinel as an agent-route availableInputs or observedReports ref must materialize that artifact and pass its safe repo-relative path instead." - ], "nonClaims": [ "This owner-authored version-edge observation binds reviewed public contract identities; it does not authenticate Git history, registry publication, provider ingestion, native witness truth, rollout, or production readiness." ] diff --git a/internal/command/agentroute/brief.go b/internal/command/agentroute/brief.go index 1abb2fb..872f3f0 100644 --- a/internal/command/agentroute/brief.go +++ b/internal/command/agentroute/brief.go @@ -142,16 +142,12 @@ func briefContextRefs(commands []map[string]any, reportID string) []any { func briefBlockers(report map[string]any, reportID string) ([]any, int) { blockers := make([]any, 0, maxBriefBlockerItems) total := 0 - appendBounded := func(blocker map[string]any) { - total++ - if len(blockers) < maxBriefBlockerItems { - blockers = append(blockers, blocker) - } - } if stringFromMap(report, "state") == "blocked_unknown_goal" { - appendBounded(map[string]any{ - "blockerId": reportID + ".blocker.unknown-goal", - "kind": "unknown_goal", + appendBoundedBriefBlocker(&blockers, &total, func() map[string]any { + return map[string]any{ + "blockerId": reportID + ".blocker.unknown-goal", + "kind": "unknown_goal", + } }) } requiredInputs, _ := report["requiredInputs"].([]any) @@ -160,11 +156,13 @@ func briefBlockers(report map[string]any, reportID string) ([]any, int) { if !ok { continue } - appendBounded(map[string]any{ - "blockerId": fmt.Sprintf("%s.blocker.required-input.%02d", reportID, index+1), - "kind": "missing_input", - "sourceReportPointer": fmt.Sprintf("/requiredInputs/%d", index), - "subject": requiredInputLabel(item), + appendBoundedBriefBlocker(&blockers, &total, func() map[string]any { + return map[string]any{ + "blockerId": fmt.Sprintf("%s.blocker.required-input.%02d", reportID, index+1), + "kind": "missing_input", + "sourceReportPointer": fmt.Sprintf("/requiredInputs/%d", index), + "subject": requiredInputLabel(item), + } }) } observedReports, _ := report["observedReports"].([]any) @@ -177,17 +175,27 @@ func briefBlockers(report map[string]any, reportID string) ([]any, int) { if state == "passed" { continue } - appendBounded(map[string]any{ - "blockerId": fmt.Sprintf("%s.blocker.observed-report.%02d", reportID, index+1), - "kind": "observed_report", - "sourceReportPointer": fmt.Sprintf("/observedReports/%d", index), - "state": state, - "subject": stringFromMap(item, "kind"), + appendBoundedBriefBlocker(&blockers, &total, func() map[string]any { + return map[string]any{ + "blockerId": fmt.Sprintf("%s.blocker.observed-report.%02d", reportID, index+1), + "kind": "observed_report", + "sourceReportPointer": fmt.Sprintf("/observedReports/%d", index), + "state": state, + "subject": stringFromMap(item, "kind"), + } }) } return blockers, total - len(blockers) } +func appendBoundedBriefBlocker(blockers *[]any, total *int, build func() map[string]any) { + *total = *total + 1 + if len(*blockers) >= maxBriefBlockerItems { + return + } + *blockers = append(*blockers, build()) +} + func briefOmissionSummary(report map[string]any, availableCommandCount int, omittedBlockerCount int) map[string]any { alternativeCount := availableCommandCount if stringFromMap(report, "state") == "routed" && alternativeCount > 0 { @@ -202,8 +210,18 @@ func briefOmissionSummary(report map[string]any, availableCommandCount int, omit "route_questions", }, "omittedBlockerCount": omittedBlockerCount, - "sourceOmittedCommandCount": len(mapsFromAny(report["omitted"])), + "sourceOmittedCommandCount": omittedCommandCount(report["omitted"]), + } +} + +func omittedCommandCount(raw any) int { + count := 0 + for _, item := range mapsFromAny(raw) { + if stringFromMap(item, "command") != "" { + count++ + } } + return count } func nonNegativeInt(raw any) (int, bool) { diff --git a/internal/command/agentroute/brief_test.go b/internal/command/agentroute/brief_test.go index 012b5b2..1566c77 100644 --- a/internal/command/agentroute/brief_test.go +++ b/internal/command/agentroute/brief_test.go @@ -1,12 +1,13 @@ package agentroute import ( + "crypto/sha256" + "encoding/hex" "slices" "strings" "testing" "github.com/research-engineering/agentic-proofkit/internal/kernel/cliexec" - "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" ) @@ -57,10 +58,7 @@ func TestAgentBriefIsBoundedAndFullEnvelopeRemainsAvailable(t *testing.T) { t.Fatalf("brief omission accounting drifted: %#v", omissions) } detail := brief["detailAccess"].(map[string]any) - wantDigest, err := digest.StableJSONSHA256Ref(report) - if err != nil { - t.Fatal(err) - } + wantDigest := independentlyHashStableJSON(t, report) if detail["sourceReportDigest"] != wantDigest || detail["requiresOriginalInput"] != true || detail["requiresOriginalLauncherContext"] != true || detail["launcherProfile"] != cliexec.ProfilePath { t.Fatalf("brief detail access is not source-bound: %#v", detail) } @@ -216,9 +214,10 @@ func TestAgentBriefCompactsAtDeclaredByteBoundary(t *testing.T) { t.Fatalf("agent brief byte limit=%d want owner-declared %d", maxAgentBriefBytes, ownerDeclaredByteLimit) } - var boundaryReport map[string]any - var unboundedBytes int - for refRunes := 800; refRunes <= 2400; refRunes += 8 { + var exactReport map[string]any + var aboveReport map[string]any + var aboveBytes int + for refRunes := 800; refRunes <= 2400; refRunes++ { report, exitCode, err := Build(map[string]any{ "schemaVersion": jsonNumber("1"), "routeId": "consumer.route.boundary", @@ -239,22 +238,45 @@ func TestAgentBriefCompactsAtDeclaredByteBoundary(t *testing.T) { if err != nil { t.Fatal(err) } - if len(encoded) > maxAgentBriefBytes && len(encoded) <= maxAgentBriefBytes+1024 { - boundaryReport = report - unboundedBytes = len(encoded) + switch { + case len(encoded) == maxAgentBriefBytes: + exactReport = report + case len(encoded) > maxAgentBriefBytes && aboveReport == nil: + aboveReport = report + aboveBytes = len(encoded) + } + if exactReport != nil && aboveReport != nil { break } } - if boundaryReport == nil { - t.Fatal("failed to construct an admitted brief immediately above the declared byte bound") + if exactReport == nil || aboveReport == nil { + t.Fatalf("failed to construct exact and above-bound briefs: exact=%t above=%t", exactReport != nil, aboveReport != nil) } - brief, err := AgentBrief(boundaryReport) + exactBrief, err := AgentBrief(exactReport) + if err != nil { + t.Fatal(err) + } + exactAction := exactBrief["nextAction"].(map[string]any) + if exactAction["argvState"] != "inline" { + t.Fatalf("brief at the exact byte limit was compacted: %#v", exactAction) + } + if _, retained := exactAction["argv"]; !retained { + t.Fatal("brief at the exact byte limit lost inline argv") + } + exactBytes, err := stablejson.Marshal(exactBrief) + if err != nil { + t.Fatal(err) + } + if len(exactBytes) != maxAgentBriefBytes { + t.Fatalf("exact-bound brief bytes=%d want=%d", len(exactBytes), maxAgentBriefBytes) + } + brief, err := AgentBrief(aboveReport) if err != nil { t.Fatal(err) } action := brief["nextAction"].(map[string]any) if action["argvState"] != "detail_required" { - t.Fatalf("unbounded brief bytes=%d was not compacted at limit=%d: %#v", unboundedBytes, maxAgentBriefBytes, action) + t.Fatalf("unbounded brief bytes=%d was not compacted at limit=%d: %#v", aboveBytes, maxAgentBriefBytes, action) } if _, retained := action["argv"]; retained { t.Fatal("boundary-compacted brief retained inline argv") @@ -291,8 +313,7 @@ func TestAgentBriefPreservesBlockedRouteOmissionsAndUnknownReportBlockers(t *tes if err != nil || briefExitCode != 1 { t.Fatalf("BuildEnvelope(blocked) exit=%d error=%v", briefExitCode, err) } - available := report["summary"].(map[string]any)["availableCommandCount"] - if available == 0 || brief["omissionSummary"].(map[string]any)["availableAlternativeCommandCount"] != available { + if report["summary"].(map[string]any)["availableCommandCount"] != 1 || brief["omissionSummary"].(map[string]any)["availableAlternativeCommandCount"] != 1 { t.Fatalf("blocked route lost available command accounting: report=%#v brief=%#v", report["summary"], brief["omissionSummary"]) } @@ -310,9 +331,16 @@ func TestAgentBriefPreservesBlockedRouteOmissionsAndUnknownReportBlockers(t *tes t.Fatalf("BuildEnvelope(unknown) exit=%d error=%v", exitCode, err) } blockers := unknown["blockers"].([]any) - if len(blockers) != 2 || blockers[0].(map[string]any)["kind"] != "unknown_goal" || blockers[1].(map[string]any)["kind"] != "observed_report" { + if len(blockers) != 2 || blockers[0].(map[string]any)["kind"] != "unknown_goal" { t.Fatalf("unknown route did not retain ordered blockers: %#v", blockers) } + observed := blockers[1].(map[string]any) + if observed["kind"] != "observed_report" || observed["state"] != "failed" || observed["subject"] != "release" || observed["sourceReportPointer"] != "/observedReports/0" { + t.Fatalf("unknown route observed-report blocker drifted: %#v", observed) + } + if unknown["omissionSummary"].(map[string]any)["sourceOmittedCommandCount"] != 0 { + t.Fatalf("unknown route counted a policy omission as a command: %#v", unknown["omissionSummary"]) + } } func TestAgentBriefBindsLauncherContextThatAffectsReportDigest(t *testing.T) { @@ -341,9 +369,20 @@ func TestAgentBriefBindsLauncherContextThatAffectsReportDigest(t *testing.T) { } pathDetail := pathBrief["detailAccess"].(map[string]any) npmDetail := npmBrief["detailAccess"].(map[string]any) + pathReport, _, err := BuildWithRenderer(input, cliexec.PathRenderer()) + if err != nil { + t.Fatal(err) + } + npmReport, _, err := BuildWithRenderer(input, npmRenderer) + if err != nil { + t.Fatal(err) + } if pathDetail["launcherProfile"] != cliexec.ProfilePath || npmDetail["launcherProfile"] != cliexec.ProfileNPMOffline { t.Fatalf("brief launcher profiles are not exact: path=%#v npm=%#v", pathDetail, npmDetail) } + if pathDetail["sourceReportDigest"] != independentlyHashStableJSON(t, pathReport) || npmDetail["sourceReportDigest"] != independentlyHashStableJSON(t, npmReport) { + t.Fatalf("launcher-dependent source digests are not independently reproducible: path=%#v npm=%#v", pathDetail, npmDetail) + } if pathDetail["sourceReportDigest"] == npmDetail["sourceReportDigest"] { t.Fatal("launcher-dependent route reports unexpectedly share a digest") } @@ -385,16 +424,17 @@ func TestBuildEnvelopeCapsBlockersAndCountsOmittedDetails(t *testing.T) { func TestBriefBlockerBoundDominatesMapMaterialization(t *testing.T) { t.Parallel() - reports := make([]any, 10_000) - for index := range reports { - reports[index] = map[string]any{"kind": "requirement_source", "state": "warning"} + blockers := make([]any, 0, maxBriefBlockerItems) + total := 0 + materialized := 0 + for index := 0; index < 10_000; index++ { + appendBoundedBriefBlocker(&blockers, &total, func() map[string]any { + materialized++ + return map[string]any{"blockerId": index} + }) } - blockers, omitted := briefBlockers(map[string]any{ - "observedReports": reports, - "state": "blocked_ambiguous_state", - }, "consumer.route.bounded-work") - if len(blockers) != 4 || cap(blockers) != 4 || omitted != len(reports)-4 { - t.Fatalf("blocker bound did not dominate materialization: len=%d cap=%d omitted=%d", len(blockers), cap(blockers), omitted) + if len(blockers) != 4 || cap(blockers) != 4 || total != 10_000 || materialized != 4 { + t.Fatalf("blocker bound did not dominate materialization: len=%d cap=%d total=%d materialized=%d", len(blockers), cap(blockers), total, materialized) } } @@ -415,3 +455,13 @@ func TestBuildEnvelopeModeRejectsUnknownMode(t *testing.T) { func buildBriefEnvelope(raw any) (map[string]any, int, error) { return BuildEnvelopeModeWithRenderer(raw, cliexec.PathRenderer(), EnvelopeModeBrief) } + +func independentlyHashStableJSON(t *testing.T, value any) string { + t.Helper() + encoded, err := stablejson.Marshal(value) + if err != nil { + t.Fatal(err) + } + sum := sha256.Sum256(encoded) + return "sha256:" + hex.EncodeToString(sum[:]) +} diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go index dd27278..a869167 100644 --- a/internal/command/stackpreset/preset_ids_generated.go +++ b/internal/command/stackpreset/preset_ids_generated.go @@ -1,6 +1,6 @@ // Code generated by internal/tools/commandcontractgen; DO NOT EDIT. package stackpreset -const presetContractSourceSHA256 = "d5aff223f854d6de95a612d9fe1d421015e27eaa26f0943b02b347021519a3bd" +const presetContractSourceSHA256 = "ca3151470f502bf1c00d33c92b9d4bbce29805612342787aa89de10d36c7ad89" var presetIDs = []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"} diff --git a/internal/tools/coveragemetrics/main_test.go b/internal/tools/coveragemetrics/main_test.go index a38a1c7..108ffcd 100644 --- a/internal/tools/coveragemetrics/main_test.go +++ b/internal/tools/coveragemetrics/main_test.go @@ -634,6 +634,12 @@ func TestBindingWitnessSelectorsRequireExactCriticalInventories(t *testing.T) { "proofkit.supply-chain-quality.workflow-package-gate-oracle", "proofkit.supply-chain-quality.workflow-source-oracles", "proofkit.spec-proof-core.adoption-contract-envelope-cli-abi", + "proofkit.spec-proof-core.agent-route-brief-cli-abi", + "proofkit.spec-proof-core.agent-route-brief-projection", + "proofkit.spec-proof-core.agent-route-brief-version-edge", + "proofkit.spec-proof-core.agent-route-flag-pre-read-admission", + "proofkit.spec-proof-core.agent-route-materialized-ref-admission", + "proofkit.spec-proof-core.agent-route-report-contract-closure", "proofkit.spec-proof-core.declared-route-mapping-without-assurance", "proofkit.spec-proof-core.requirement-authoring-ref-provenance", "proofkit.spec-proof-core.requirement-browser-one-shot-cleanup", diff --git a/internal/tools/packageverify/main.go b/internal/tools/packageverify/main.go index 8b9bd9d..5a1e52a 100644 --- a/internal/tools/packageverify/main.go +++ b/internal/tools/packageverify/main.go @@ -1427,12 +1427,17 @@ func verifyCLIContractBoundaryPolicyClosure(content string, textEntries map[stri } } commands, _ := contract["commands"].([]any) + agentRouteCount := 0 for _, rawCommand := range commands { command, _ := rawCommand.(map[string]any) + if command["command"] != "agent-route" { + continue + } + agentRouteCount++ output, _ := command["outputContract"].(map[string]any) brief, _ := output["briefPacketContract"].(map[string]any) if brief == nil { - continue + return fmt.Errorf("package CLI agent-route briefPacketContract must be present") } refs := stringArrayField(brief, "boundaryPolicyRefs") for _, requirementID := range refs { @@ -1444,6 +1449,9 @@ func verifyCLIContractBoundaryPolicyClosure(content string, textEntries map[stri return fmt.Errorf("package CLI brief contract boundaryPolicyRefs must equal the exact canonical policy set") } } + if agentRouteCount != 1 { + return fmt.Errorf("package CLI contract must contain exactly one agent-route command") + } return nil } diff --git a/internal/tools/packageverify/main_test.go b/internal/tools/packageverify/main_test.go index 09e1353..516f922 100644 --- a/internal/tools/packageverify/main_test.go +++ b/internal/tools/packageverify/main_test.go @@ -743,6 +743,24 @@ func TestPackagePublicReferenceClosure(t *testing.T) { }, want: "boundary policy ref must resolve to exactly one shipped requirement", }, + { + name: "missing agent-route brief packet contract", + mutate: func(entries map[string]string) { + var contract map[string]any + if err := json.Unmarshal([]byte(entries["package/proofkit/cli-contract.v2.json"]), &contract); err != nil { + panic(err) + } + command := contract["commands"].([]any)[0].(map[string]any) + output := command["outputContract"].(map[string]any) + delete(output, "briefPacketContract") + encoded, err := json.Marshal(contract) + if err != nil { + panic(err) + } + entries["package/proofkit/cli-contract.v2.json"] = string(encoded) + }, + want: "agent-route briefPacketContract must be present", + }, { name: "ambiguous brief boundary policy requirement", mutate: func(entries map[string]string) { @@ -1414,7 +1432,7 @@ func packageReferenceClosureFixture() map[string]string { "package/proofkit/witness-plan.json": `{"commands":[],"policies":[]}`, "package/proofkit/command-families.v1.json": `{"families":[]}`, "package/proofkit/receipt-producer-policy.json": `{"producers":[{"producerId":"local.developer","evidenceRefs":["docs/specs/example/requirements.v1.json"]}]}`, - "package/proofkit/cli-contract.v2.json": `{"processContract":{"helpGrammar":{"helpCatalogFormsSource":"proofkit/command-families.v1.json"}},"commands":[{"command":"fixture","inputContract":{"nativeSource":{"path":"internal/tools/packageverify/main.go","evidenceClass":"source_checkout"}},"outputContract":{"briefPacketContract":{"boundaryPolicyRefs":["REQ-PROOFKIT-SPEC-005","REQ-PROOFKIT-SPEC-026"],"fieldRules":{"boundaryPolicyRefs":"policy field description","contextRefs":"runtime field description"}},"nativeSource":{"path":"internal/tools/packageverify/main.go","evidenceClass":"source_checkout"}}}]}`, + "package/proofkit/cli-contract.v2.json": `{"processContract":{"helpGrammar":{"helpCatalogFormsSource":"proofkit/command-families.v1.json"}},"commands":[{"command":"agent-route","inputContract":{"nativeSource":{"path":"internal/tools/packageverify/main.go","evidenceClass":"source_checkout"}},"outputContract":{"briefPacketContract":{"boundaryPolicyRefs":["REQ-PROOFKIT-SPEC-005","REQ-PROOFKIT-SPEC-026"],"fieldRules":{"boundaryPolicyRefs":"policy field description","contextRefs":"runtime field description"}},"nativeSource":{"path":"internal/tools/packageverify/main.go","evidenceClass":"source_checkout"}}}]}`, } } diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index 99aea2e..52c7fe6 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -532,7 +532,7 @@ "rootDefinitionDigest": "sha256:fd1121285d1b357ed848febc248700a75417d06619e22ed36991631d7526375b", "nativeSource": { "path": "internal/command/agentroute", - "canonicalDigest": "sha256:a2359fd9f5a667a0da31e5b12331239ce77d9d997205c85d78ad8771034f0369", + "canonicalDigest": "sha256:3256eac5777824ad0b7b9bce6537efdb42b75430579a1a3f160577cf8cdce379", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -557,7 +557,8 @@ "ownerRequirementRefs": [ "REQ-PROOFKIT-PACKAGE-002", "REQ-PROOFKIT-QUALITY-004", - "REQ-PROOFKIT-SPEC-005" + "REQ-PROOFKIT-SPEC-005", + "REQ-PROOFKIT-SPEC-026" ] }, "outputContract": { @@ -661,7 +662,7 @@ }, { "path": "internal/command/agentroute", - "canonicalDigest": "sha256:a2359fd9f5a667a0da31e5b12331239ce77d9d997205c85d78ad8771034f0369", + "canonicalDigest": "sha256:3256eac5777824ad0b7b9bce6537efdb42b75430579a1a3f160577cf8cdce379", "evidenceClass": "source_checkout" } ], From cd0aa9096efa40e6ac511fc5c1d10a1748de1cb6 Mon Sep 17 00:00:00 2001 From: iperev Date: Thu, 3 Sep 2026 01:14:22 +0200 Subject: [PATCH 5/9] test: isolate agent route proof oracles --- internal/app/agent_route_version_edge_test.go | 25 +++++++------- internal/command/agentroute/brief_test.go | 33 +++++++++++++++++++ 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/internal/app/agent_route_version_edge_test.go b/internal/app/agent_route_version_edge_test.go index 3d505a8..837c95e 100644 --- a/internal/app/agent_route_version_edge_test.go +++ b/internal/app/agent_route_version_edge_test.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "slices" + "strings" "testing" "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" @@ -48,23 +49,25 @@ func TestAgentRouteVersionEdgeClosesBriefDefaultMigration(t *testing.T) { mutants := []struct { name string mutate func(*agentRouteVersionEdge) + want string }{ - {name: "current ABI", mutate: func(value *agentRouteVersionEdge) { value.CurrentPublicABISHA256 += "0" }}, - {name: "previous ABI", mutate: func(value *agentRouteVersionEdge) { value.PreviousPublicABISHA256 = value.CurrentPublicABISHA256 }}, - {name: "command", mutate: func(value *agentRouteVersionEdge) { value.ChangedCommandContract.Command = "help" }}, - {name: "current input contract", mutate: func(value *agentRouteVersionEdge) { value.ChangedCommandContract.CurrentInputContractSHA256 += "0" }}, - {name: "current output contract", mutate: func(value *agentRouteVersionEdge) { value.ChangedCommandContract.CurrentOutputContractSHA256 += "0" }}, - {name: "previous input contract", mutate: func(value *agentRouteVersionEdge) { value.ChangedCommandContract.PreviousInputContractSHA256 += "0" }}, - {name: "previous output contract", mutate: func(value *agentRouteVersionEdge) { value.ChangedCommandContract.PreviousOutputContractSHA256 += "0" }}, - {name: "change record reference", mutate: func(value *agentRouteVersionEdge) { value.ChangeRecordRef += ".drift" }}, - {name: "change record digest", mutate: func(value *agentRouteVersionEdge) { value.ChangeRecordSHA256 += "0" }}, + {name: "current ABI", mutate: func(value *agentRouteVersionEdge) { value.CurrentPublicABISHA256 += "0" }, want: "ABI identity is invalid"}, + {name: "previous ABI", mutate: func(value *agentRouteVersionEdge) { value.PreviousPublicABISHA256 = value.CurrentPublicABISHA256 }, want: "ABI identity is invalid"}, + {name: "command", mutate: func(value *agentRouteVersionEdge) { value.ChangedCommandContract.Command = "help" }, want: "changed command contract is not exact"}, + {name: "current input contract", mutate: func(value *agentRouteVersionEdge) { value.ChangedCommandContract.CurrentInputContractSHA256 += "0" }, want: "changed command contract is not exact"}, + {name: "current output contract", mutate: func(value *agentRouteVersionEdge) { value.ChangedCommandContract.CurrentOutputContractSHA256 += "0" }, want: "changed command contract is not exact"}, + {name: "previous input contract", mutate: func(value *agentRouteVersionEdge) { value.ChangedCommandContract.PreviousInputContractSHA256 += "0" }, want: "changed command contract is not exact"}, + {name: "previous output contract", mutate: func(value *agentRouteVersionEdge) { value.ChangedCommandContract.PreviousOutputContractSHA256 += "0" }, want: "changed command contract is not exact"}, + {name: "change record reference", mutate: func(value *agentRouteVersionEdge) { value.ChangeRecordRef += ".drift" }, want: "change record reference is not exact"}, + {name: "change record digest", mutate: func(value *agentRouteVersionEdge) { value.ChangeRecordSHA256 += "0" }, want: "change record digest is not exact"}, } for _, mutant := range mutants { t.Run(mutant.name, func(t *testing.T) { value := cloneAgentRouteVersionEdge(record) mutant.mutate(&value) - if err := validateAgentRouteVersionEdge(value, root); err == nil { - t.Fatal("version-edge mutant was admitted") + err := validateAgentRouteVersionEdge(value, root) + if err == nil || !strings.Contains(err.Error(), mutant.want) { + t.Fatalf("version-edge mutant error=%v want class %q", err, mutant.want) } }) } diff --git a/internal/command/agentroute/brief_test.go b/internal/command/agentroute/brief_test.go index 1566c77..b879c42 100644 --- a/internal/command/agentroute/brief_test.go +++ b/internal/command/agentroute/brief_test.go @@ -438,6 +438,39 @@ func TestBriefBlockerBoundDominatesMapMaterialization(t *testing.T) { } } +func TestBriefBlockerBoundDominatesProductionAllocations(t *testing.T) { + reportWith := func(count int) map[string]any { + reports := make([]any, count) + for index := range reports { + reports[index] = map[string]any{ + "kind": "requirement_source", + "state": "warning", + } + } + return map[string]any{ + "observedReports": reports, + "state": "blocked_ambiguous_state", + } + } + measure := func(report map[string]any) float64 { + var blockers []any + var omitted int + allocations := testing.AllocsPerRun(20, func() { + blockers, omitted = briefBlockers(report, "consumer.route.bounded-work") + }) + if len(blockers) != maxBriefBlockerItems || omitted != len(report["observedReports"].([]any))-maxBriefBlockerItems { + t.Fatalf("allocation probe produced invalid blockers: len=%d omitted=%d", len(blockers), omitted) + } + return allocations + } + + boundedAllocations := measure(reportWith(maxBriefBlockerItems)) + largeAllocations := measure(reportWith(10_000)) + if largeAllocations > boundedAllocations+1 { + t.Fatalf("omitted blockers caused map allocation growth: bounded=%.2f large=%.2f", boundedAllocations, largeAllocations) + } +} + func TestBuildEnvelopeModeRejectsUnknownMode(t *testing.T) { t.Parallel() From 18995e66d9a7ee90915277662b914668c8efba73 Mon Sep 17 00:00:00 2001 From: iperev Date: Thu, 3 Sep 2026 01:17:46 +0200 Subject: [PATCH 6/9] test: stabilize bounded allocation oracle --- internal/command/agentroute/brief_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/command/agentroute/brief_test.go b/internal/command/agentroute/brief_test.go index b879c42..8af7a99 100644 --- a/internal/command/agentroute/brief_test.go +++ b/internal/command/agentroute/brief_test.go @@ -466,7 +466,8 @@ func TestBriefBlockerBoundDominatesProductionAllocations(t *testing.T) { boundedAllocations := measure(reportWith(maxBriefBlockerItems)) largeAllocations := measure(reportWith(10_000)) - if largeAllocations > boundedAllocations+1 { + const maxInstrumentationAllocationDelta = 32 + if largeAllocations > boundedAllocations+maxInstrumentationAllocationDelta { t.Fatalf("omitted blockers caused map allocation growth: bounded=%.2f large=%.2f", boundedAllocations, largeAllocations) } } From 69f788b8398541eba1a300c35d558edf7140fd1e Mon Sep 17 00:00:00 2001 From: iperev Date: Thu, 3 Sep 2026 01:27:17 +0200 Subject: [PATCH 7/9] test: make snapshot overflow oracle causal --- internal/tools/repositorysnapshot/snapshot_test.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/tools/repositorysnapshot/snapshot_test.go b/internal/tools/repositorysnapshot/snapshot_test.go index 99fd61b..dc5e6c5 100644 --- a/internal/tools/repositorysnapshot/snapshot_test.go +++ b/internal/tools/repositorysnapshot/snapshot_test.go @@ -185,17 +185,18 @@ func TestCaptureContextTerminatesCanceledGitProcessGroup(t *testing.T) { func TestCaptureContextTerminatesGitProcessGroupOnOutputOverflow(t *testing.T) { bin := t.TempDir() gitPath := filepath.Join(bin, "git") - script := "#!/bin/sh\n/bin/dd if=/dev/zero bs=17825792 count=1 1>&2\n/bin/sleep 10\n" + script := "#!/bin/sh\n/bin/dd if=/dev/zero bs=17825792 count=1 1>&2\n/bin/sleep 30\n" if err := os.WriteFile(gitPath, []byte(script), 0o755); err != nil { t.Fatal(err) } t.Setenv("PATH", bin) - started := time.Now() - if _, err := CaptureContext(context.Background(), t.TempDir()); err == nil || !strings.Contains(err.Error(), "output exceeds resource limit") { + ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) + defer cancel() + if _, err := CaptureContext(ctx, t.TempDir()); err == nil || !strings.Contains(err.Error(), "output exceeds resource limit") { t.Fatalf("CaptureContext() error = %v, want output-limit rejection", err) } - if elapsed := time.Since(started); elapsed > 3*time.Second { - t.Fatalf("CaptureContext() output-limit termination took %s", elapsed) + if ctx.Err() != nil { + t.Fatalf("CaptureContext() output-limit termination did not precede the context deadline: %v", ctx.Err()) } } From 2f1656f993363e964892efbe38c017c0e21fbc0b Mon Sep 17 00:00:00 2001 From: iperev Date: Thu, 3 Sep 2026 01:36:07 +0200 Subject: [PATCH 8/9] fix: prove snapshot process cleanup --- .../tools/repositorysnapshot/git_inventory.go | 19 +++++++- .../snapshot_process_group_unix_test.go | 44 +++++++++++++++++++ .../tools/repositorysnapshot/snapshot_test.go | 18 -------- 3 files changed, 62 insertions(+), 19 deletions(-) create mode 100644 internal/tools/repositorysnapshot/snapshot_process_group_unix_test.go diff --git a/internal/tools/repositorysnapshot/git_inventory.go b/internal/tools/repositorysnapshot/git_inventory.go index b041c5a..81bf22d 100644 --- a/internal/tools/repositorysnapshot/git_inventory.go +++ b/internal/tools/repositorysnapshot/git_inventory.go @@ -111,27 +111,44 @@ func gitOutput(ctx context.Context, root string, args ...string) (string, error) stderrExceeded := stderr.Exceeded() contextDone := ctx.Done() waitComplete := false + terminationRequested := false for !waitComplete { select { case waitErr = <-waitDone: waitComplete = true case <-stdoutExceeded: overflowed = true + terminationRequested = true stdoutExceeded = nil _ = processgroup.Terminate(command) case <-stderrExceeded: overflowed = true + terminationRequested = true stderrExceeded = nil _ = processgroup.Terminate(command) case <-contextDone: + terminationRequested = true contextDone = nil _ = processgroup.Terminate(command) } } + outputOverflowed := overflowed || stdout.Overflowed() || stderr.Overflowed() + if terminationRequested || ctx.Err() != nil || outputOverflowed { + if err := processgroup.TerminateAndWait(command, processWaitDelay); err != nil { + switch { + case ctx.Err() != nil: + return "", fmt.Errorf("repository snapshot operation canceled and process cleanup failed: %w", ctx.Err()) + case outputOverflowed: + return "", fmt.Errorf("git output exceeds resource limit and process cleanup failed") + default: + return "", fmt.Errorf("git process cleanup failed") + } + } + } if ctx.Err() != nil { return "", fmt.Errorf("repository snapshot operation canceled: %w", ctx.Err()) } - if overflowed || stdout.Overflowed() || stderr.Overflowed() { + if outputOverflowed { return "", fmt.Errorf("git output exceeds resource limit") } if waitErr != nil { diff --git a/internal/tools/repositorysnapshot/snapshot_process_group_unix_test.go b/internal/tools/repositorysnapshot/snapshot_process_group_unix_test.go new file mode 100644 index 0000000..d67e786 --- /dev/null +++ b/internal/tools/repositorysnapshot/snapshot_process_group_unix_test.go @@ -0,0 +1,44 @@ +//go:build darwin || linux + +package repositorysnapshot + +import ( + "context" + "errors" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" + "testing" + "time" +) + +func TestCaptureContextTerminatesGitProcessGroupOnOutputOverflow(t *testing.T) { + bin := t.TempDir() + gitPath := filepath.Join(bin, "git") + processGroupPath := filepath.Join(t.TempDir(), "process-group") + script := "#!/bin/sh\n/bin/sleep 30 &\nprintf '%s\\n' \"$$\" > \"$PROOFKIT_TEST_PROCESS_GROUP\"\n/bin/dd if=/dev/zero bs=17825792 count=1 1>&2\nwait\n" + if err := os.WriteFile(gitPath, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + t.Setenv("PATH", bin) + t.Setenv("PROOFKIT_TEST_PROCESS_GROUP", processGroupPath) + ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) + defer cancel() + if _, err := CaptureContext(ctx, t.TempDir()); err == nil || !strings.Contains(err.Error(), "output exceeds resource limit") { + t.Fatalf("CaptureContext() error = %v, want output-limit rejection", err) + } + processGroupBytes, err := os.ReadFile(processGroupPath) + if err != nil { + t.Fatalf("read process-group oracle: %v", err) + } + processGroupID, err := strconv.Atoi(strings.TrimSpace(string(processGroupBytes))) + if err != nil || processGroupID <= 0 { + t.Fatalf("invalid process-group oracle %q", processGroupBytes) + } + if err := syscall.Kill(-processGroupID, syscall.Signal(0)); !errors.Is(err, syscall.ESRCH) { + _ = syscall.Kill(-processGroupID, syscall.SIGKILL) + t.Fatalf("git process group %d survived output-limit termination: %v", processGroupID, err) + } +} diff --git a/internal/tools/repositorysnapshot/snapshot_test.go b/internal/tools/repositorysnapshot/snapshot_test.go index dc5e6c5..9dfa146 100644 --- a/internal/tools/repositorysnapshot/snapshot_test.go +++ b/internal/tools/repositorysnapshot/snapshot_test.go @@ -182,24 +182,6 @@ func TestCaptureContextTerminatesCanceledGitProcessGroup(t *testing.T) { } } -func TestCaptureContextTerminatesGitProcessGroupOnOutputOverflow(t *testing.T) { - bin := t.TempDir() - gitPath := filepath.Join(bin, "git") - script := "#!/bin/sh\n/bin/dd if=/dev/zero bs=17825792 count=1 1>&2\n/bin/sleep 30\n" - if err := os.WriteFile(gitPath, []byte(script), 0o755); err != nil { - t.Fatal(err) - } - t.Setenv("PATH", bin) - ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) - defer cancel() - if _, err := CaptureContext(ctx, t.TempDir()); err == nil || !strings.Contains(err.Error(), "output exceeds resource limit") { - t.Fatalf("CaptureContext() error = %v, want output-limit rejection", err) - } - if ctx.Err() != nil { - t.Fatalf("CaptureContext() output-limit termination did not precede the context deadline: %v", ctx.Err()) - } -} - func TestCaptureRejectsSuccessfulGitDiagnosticsWithoutEcho(t *testing.T) { bin := t.TempDir() gitPath := filepath.Join(bin, "git") From ca20fad246bd11de6b656fe95957ae4b2ee891ec Mon Sep 17 00:00:00 2001 From: iperev Date: Thu, 3 Sep 2026 01:42:19 +0200 Subject: [PATCH 9/9] test: preserve snapshot witness routing --- .../snapshot_process_group_other_test.go | 9 +++++++++ .../snapshot_process_group_unix_test.go | 2 +- internal/tools/repositorysnapshot/snapshot_test.go | 4 ++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 internal/tools/repositorysnapshot/snapshot_process_group_other_test.go diff --git a/internal/tools/repositorysnapshot/snapshot_process_group_other_test.go b/internal/tools/repositorysnapshot/snapshot_process_group_other_test.go new file mode 100644 index 0000000..08b0f26 --- /dev/null +++ b/internal/tools/repositorysnapshot/snapshot_process_group_other_test.go @@ -0,0 +1,9 @@ +//go:build !darwin && !linux + +package repositorysnapshot + +import "testing" + +func testCaptureContextTerminatesGitProcessGroupOnOutputOverflow(t *testing.T) { + t.Skip("process-group absence proof requires the supported Darwin or Linux process-group primitive") +} diff --git a/internal/tools/repositorysnapshot/snapshot_process_group_unix_test.go b/internal/tools/repositorysnapshot/snapshot_process_group_unix_test.go index d67e786..20c482c 100644 --- a/internal/tools/repositorysnapshot/snapshot_process_group_unix_test.go +++ b/internal/tools/repositorysnapshot/snapshot_process_group_unix_test.go @@ -14,7 +14,7 @@ import ( "time" ) -func TestCaptureContextTerminatesGitProcessGroupOnOutputOverflow(t *testing.T) { +func testCaptureContextTerminatesGitProcessGroupOnOutputOverflow(t *testing.T) { bin := t.TempDir() gitPath := filepath.Join(bin, "git") processGroupPath := filepath.Join(t.TempDir(), "process-group") diff --git a/internal/tools/repositorysnapshot/snapshot_test.go b/internal/tools/repositorysnapshot/snapshot_test.go index 9dfa146..2ffc404 100644 --- a/internal/tools/repositorysnapshot/snapshot_test.go +++ b/internal/tools/repositorysnapshot/snapshot_test.go @@ -182,6 +182,10 @@ func TestCaptureContextTerminatesCanceledGitProcessGroup(t *testing.T) { } } +func TestCaptureContextTerminatesGitProcessGroupOnOutputOverflow(t *testing.T) { + testCaptureContextTerminatesGitProcessGroupOnOutputOverflow(t) +} + func TestCaptureRejectsSuccessfulGitDiagnosticsWithoutEcho(t *testing.T) { bin := t.TempDir() gitPath := filepath.Join(bin, "git")