From 183b3c8aece27a9fa4ac6b33a95e4e2deb1d90d1 Mon Sep 17 00:00:00 2001 From: iperev Date: Sat, 5 Sep 2026 10:50:56 +0200 Subject: [PATCH 1/3] feat: add portable agent bootstrap and freshness checks --- ADOPTION.md | 61 ++ NON_CLAIMS.md | 18 + docs/proofkit-contract-map.md | 2 + .../specs/proofkit-agent-workflow/overview.md | 45 ++ .../requirements.v1.json | 51 ++ internal/app/agent_integration_command.go | 130 ++++ .../app/agent_integration_command_test.go | 217 ++++++ internal/app/app.go | 2 + internal/app/cli_contract_test.go | 4 +- internal/app/command_contract_generated.go | 16 +- internal/app/command_coverage_routes.go | 2 + internal/app/command_coverage_test.go | 13 + internal/app/command_descriptors.go | 5 + .../app/command_family_catalog_generated.go | 3 +- internal/app/integration_version_edge_test.go | 70 ++ .../project_navigation_abi_closure_test.go | 232 +----- .../project_navigation_abi_mutation_test.go | 148 +--- .../project_navigation_version_edge_test.go | 50 +- internal/app/public_abi_mutation_test.go | 167 ++++ internal/app/public_abi_observation_test.go | 330 ++++++++ .../compact-current-production-consumers.json | 1 + .../releases/v0.9.0/cli-contract.v2.json.gz | Bin 0 -> 53523 bytes .../v0.9.0/public-abi-observation.json | 268 +++++++ .../v0.9.0/release/change-record.v2.json | 49 ++ internal/command/agentintegration/check.go | 203 +++++ .../command/agentintegration/check_test.go | 720 ++++++++++++++++++ .../agentintegration/check_unix_test.go | 53 ++ internal/command/agentintegration/source.go | 189 +++++ .../command/agentintegration/source_test.go | 98 +++ .../stackpreset/preset_ids_generated.go | 2 +- .../coveragemetrics/required_inventory.go | 63 ++ internal/tools/releasechange/record_test.go | 24 +- .../tools/workflowsmoke/integration_smoke.go | 145 ++++ .../tools/workflowsmoke/workflow_smoke.go | 5 +- .../workflowsmoke/workflow_smoke_test.go | 6 + package-lock.json | 4 +- package.json | 2 +- proofkit/cli-contract.v2.json | 448 ++++++++++- proofkit/command-families.v1.json | 9 + proofkit/requirement-bindings.json | 370 +++++++++ release/change-record.v2.json | 35 +- 41 files changed, 3804 insertions(+), 456 deletions(-) create mode 100644 internal/app/agent_integration_command.go create mode 100644 internal/app/agent_integration_command_test.go create mode 100644 internal/app/integration_version_edge_test.go create mode 100644 internal/app/public_abi_mutation_test.go create mode 100644 internal/app/public_abi_observation_test.go create mode 100644 internal/app/testdata/releases/v0.9.0/cli-contract.v2.json.gz create mode 100644 internal/app/testdata/releases/v0.9.0/public-abi-observation.json create mode 100644 internal/app/testdata/releases/v0.9.0/release/change-record.v2.json create mode 100644 internal/command/agentintegration/check.go create mode 100644 internal/command/agentintegration/check_test.go create mode 100644 internal/command/agentintegration/check_unix_test.go create mode 100644 internal/command/agentintegration/source.go create mode 100644 internal/command/agentintegration/source_test.go create mode 100644 internal/tools/workflowsmoke/integration_smoke.go diff --git a/ADOPTION.md b/ADOPTION.md index 40bce60..2a22f38 100644 --- a/ADOPTION.md +++ b/ADOPTION.md @@ -196,6 +196,67 @@ the proof binding already contains safe witness command facts and the caller provides witness command vocabulary. The projection avoids duplicated command identity; native execution and command freshness remain caller-owned. +## Portable Agent Bootstrap + +`REQ-PROOFKIT-WORKFLOW-016` through `REQ-PROOFKIT-WORKFLOW-018` own the bounded +Phase5A generation/check contract. Use `integration source --tool codex` or +`integration source --tool claude` through a repository-approved, already +installed launcher. JSON is the default and includes the exact generated +content, descriptor-owned target path, materialization identity, content and +consumed-capability digests, and separate metadata/body byte counts. Text is +the exact file content. The limits are 512 metadata bytes and 4096 body bytes, +not tokenizer-specific token counts. + +The fixed repository paths are `.agents/skills/agentic-proofkit/SKILL.md` for +`codex` and `.claude/skills/agentic-proofkit/SKILL.md` for `claude`. Neither tool +is selected implicitly; selecting one does not inspect the other location. +The generator reads no repository files, writes nothing, and has no `--output` +or install option. Generated instructions contain no hooks or permission grants +and delegate policy, planning, and evidence semantics to current owners. +Resolve the approved installed launcher for each session. An absent or +ambiguous binding needs an owner decision, not an install, network fallback, +package-manager default, or persisted machine-local executable path. + +For one manual export, first inspect the explicit repository root and every +component of the selected path. Stop on symlinks, aliases, unknown ownership, +local edits, or an existing target; do not overwrite it. The example below +assumes the owner has approved the npm launcher, inspected and prepared the +ordinary parent directories, and ensured no concurrent writer can replace +them. Replace the example absolute root with that inspected root. The shell, +not Proofkit, creates the file; noclobber protects an existing final file but +does not establish race-free parent traversal or transactional installation. + +```bash +(set -o noclobber; npm exec --offline -- agentic-proofkit integration source --tool codex --format text > /absolute/inspected/repository/.agents/skills/agentic-proofkit/SKILL.md) +npm exec --offline -- agentic-proofkit integration check --tool codex --repo-root /absolute/inspected/repository +``` + +Check a failed export before treating its output as usable: shell redirection +may leave an empty or partial new file if generation or transport fails. +Resolve such a file manually under repository ownership; check never repairs it. +The checker admits flags before I/O and reads only the selected fixed path +through an application-write-free confined lease with bounded reobservation. +Exit 0 and `current` mean exact generated-byte equality; exit 2 reports +`missing`, `stale`, or `invalid`; exit 1 reports an invocation or operational +error, including denied reads, ambiguous paths, observed changes, cancellation, +or cleanup failure. It never prints observed bytes, their digest, or caller +root paths. Source exits 0 on successful generation and 1 on error. + +Generated identity binds the shared template, selected descriptor, and exact +consumed registered public invocation contracts. Package version is absent +from the generated bytes; version-only or unrelated-command changes preserve +identity when consumed projections remain unchanged. Shared native source +digests may conservatively invalidate freshness. This is materialization +freshness, not proof of every transitive runtime behavior. + +Phase5B managed install/update/remove remains open. Manual export is not an +installer, rollback protocol, or proof of host activation. Installed npm/Python +integration proof requires actual carrier execution; source-only tests do not +discharge it. Native-host file discovery, body loading, and approved-launcher calls +are separate observations requiring isolated sessions and absent/altered-file +controls; a prompt directly requesting CLI execution is not skill-use evidence. +Removing a file does not revoke instructions already loaded into host context. + ## Requirement, Contract, And Test Order The durable semantic source is the repository-owned requirement package: diff --git a/NON_CLAIMS.md b/NON_CLAIMS.md index b2b2473..29419dc 100644 --- a/NON_CLAIMS.md +++ b/NON_CLAIMS.md @@ -36,6 +36,24 @@ This repository state does not claim: Consuming repositories must keep their own specifications, proof bindings, native witnesses, CI gates, rollback policy, and rollout decisions. +Phase5A integration source/check implements only bounded portable generation +and read-only generated-byte freshness. It does not install, update, remove, +repair, activate host instructions, grant tool permissions, or authorize native +execution. Phase5B managed lifecycle remains open; manual shell export does not +close it. `current` neither proves semantic full proof nor guarantees stability +after return. Consumed registered-contract identity is not complete transitive +native-semantic identity, and byte budgets are not token counts. + +Declared witness routes do not prove execution. Installed npm/Python integration +smokes and final frozen-tree closure require actual execution against the named +source and artifacts. Native-host discovery, body loading, and approved-launcher +invocation require separate isolated evidence; direct CLI calls cannot prove +skill use. Removing an on-disk file does not revoke already-loaded instructions. +Read-only checks do not suppress filesystem-owned access metadata updates or +exclude uncooperative writers. Cancellation after the final pre-emission check +cannot retract bytes accepted by an external stdout writer, and prefix-accepting +transport failure does not imply an atomic sink. + Reference infrastructure boundary: ```text diff --git a/docs/proofkit-contract-map.md b/docs/proofkit-contract-map.md index 0599589..72db9d7 100644 --- a/docs/proofkit-contract-map.md +++ b/docs/proofkit-contract-map.md @@ -41,6 +41,7 @@ owner boundaries. It is not a second command-family inventory. | Family | Main commands | Caller provides | Proofkit owns | Consumer owns | Output authority | |---|---|---|---|---|---| +| Agent integration generation/check | `integration source`, `integration check` | explicit `claude` or `codex` tool; explicit repository root for check only | one portable bounded bootstrap, exact consumed registered-contract identity, descriptor-owned path, and confined read-only generated-byte freshness | launcher admission, manual materialization, instruction ownership, host discovery/activation, permissions, native verification, and any future managed lifecycle | generated source or missing/current/stale/invalid freshness report; neither is installation or execution authority | | Project state navigation | `status`, `next` | explicit repository root | bounded transaction-first materialized-project inspection, normalized observation identity, deterministic project-state classification, and one non-executable next action; admitted in-bound records bind exact content digests, while unread out-of-bound records intentionally identify only their invalid class | repository policy, byte identity for unread out-of-bound records, witness execution, receipt trust/currentness/scope, merge, release, deployment, rollout, and production readiness | project-status report, next-action packet, or bounded text projection | | Agent workflow planning | `change plan`, `native-evidence-guidance` | explicit checkpoint, completed stage ids, bounded context refs, governing authority ref, and required context ref ids | optional built-in `proofkit.reviewed-change.v1` checkpoint relation, reference-closed next-stage context, deterministic agent prompts, bounded text/JSON/envelope projections, and repository-neutral native-evidence guidance with closed applicability classes | custom workflow topology, repository state discovery, stage execution, native witness semantics, evidence collection, review conclusions, merge, release, deployment, and rollout authority | next-action plan, terminal workflow report, bounded agent envelope, or guidance catalog | | Adoption and scaffolding | `adopt plan`, `adopt materialize plan`, `adopt materialize apply`, `adopt materialize recover`, `repository-inventory`, `adoption-contract-envelope`, `adoption-workflow-plan`, `adoption-checklist`, `adoption-doctor`, `gradual-adoption`, `gradual-adoption-bootstrap`, `gradual-adoption-guidance`, `capability-map-admission`, `pilot-admission`, `scaffold-profile-plan`, `scaffold-project-structure`, `stack-preset` | explicit repository root, explicit fresh/code-baseline/audit-from-code intent, optional stack hint, owner-reviewed candidate packet, expected transaction and desired-state identities, recovery action, aggregate adoption contract envelope, checklist facts, target paths, owner routes, caller-extracted stale authority vocabulary facts, explicit pre-spec capability observations, and pilot records | bounded fixed-catalog root inventory, candidate-only front-door tasks, owner-closed read-only materialization plans, confined transactional apply and recovery receipts, aggregate contract-envelope admission, deterministic starter plans, checklist/report admission, bounded guidance envelopes, dry-run manifests, pre-spec trust-mode admission, adoption gap and stale-authority classification, and pilot shape admission | stack selection, arbitrary source inspection, candidate review, final requirement meaning, proof adequacy, rollout policy, text extraction, code observation extraction, and pilot truth | inventory, candidate-only plan, transaction-bound materialization plan or receipt, selected child output, report, seed packet, or agent envelope | @@ -131,6 +132,7 @@ Semantic context routes are `requirement-context-compose`, | State or goal | Next Proofkit route | Stop or escalation condition | |---|---|---| +| A repository owner requests a portable Proofkit bootstrap or its exact-byte freshness. | `integration source --tool ` generates JSON or exact file bytes with `--format text`; `integration check --tool --repo-root ` checks only the selected fixed path. Select `claude` or `codex` explicitly. | Source writes nothing; neither route accepts `--output` or install. Source exits 0/1 for success/error; check exits 0 for current, 2 for missing/stale/invalid, and 1 for invocation or operation error. Current proves only generated-byte equality. Use `ADOPTION.md` for inspected manual export; Phase5B installation/update/removal and host activation remain open. | | The repository may already contain a materialized Proofkit project. | `status --repo-root ` for the full bounded classification or `next --repo-root ` for its single action projection. | Treat `verification_required` as a request to run repository-owned verification, never as completion or approval. Stop on blocked or recovery-required states; the packet does not execute its route or own policy. | | The agent does not know where to start. | `adopt plan --mode --repo-root `, where mode is `fresh`, `code-baseline`, or `audit-from-code` | Choose the trust intent explicitly. Treat the fixed-catalog inventory and tasks as a read-only candidate plan; stop before arbitrary source inspection, writing files, or making requirements authoritative. | | An agent needs a bounded, deterministic stage transition for an engineering change. | `change plan` selects the optional built-in `proofkit.reviewed-change.v1` profile; use `--agent-envelope` for the compact work packet and `native-evidence-guidance` when the consuming repository has not yet materialized repository-specific evidence instructions. | Supply only explicit current checkpoint, completed stages, and admitted context references. Apply conditional guidance slots only when their applicability class matches a declared consumer mechanism. Stop before treating the profile, plan, or guidance as repository policy or as proof that a stage ran, evidence exists, review passed, or merge/release is authorized. | diff --git a/docs/specs/proofkit-agent-workflow/overview.md b/docs/specs/proofkit-agent-workflow/overview.md index f0e9097..7bf1d55 100644 --- a/docs/specs/proofkit-agent-workflow/overview.md +++ b/docs/specs/proofkit-agent-workflow/overview.md @@ -24,6 +24,11 @@ The public capability is deliberately small: bounded action. Admitted in-bound records bind exact content digests; unread out-of-bound records bind only their invalid class. Neither command claims native execution or proof completion. +5. `integration source --tool ` emits one portable static + bootstrap without reading repositories or writing files; + `integration check --tool --repo-root ` compares only + the selected descriptor-owned file through confined read-only inspection. + These are generation/check targets, not managed integration support. The change planner and evidence-guidance cores are stateless pure projections. Project status reads only an explicit repository root, the conventional routing @@ -92,11 +97,47 @@ projections remain independently owned by the spec-proof-core package. without claiming cancellation rollback or atomicity from an external sink, and a versioned breaking replacement of the flat change route by `change plan` across source and installed carriers. +- `REQ-PROOFKIT-WORKFLOW-016`: one private descriptor/template owner, portable + approved-launcher guidance, 512-byte metadata and 4096-byte body limits, and + identity over exactly consumed registered invocation contracts rather than + package version or the entire command universe. +- `REQ-PROOFKIT-WORKFLOW-017`: pre-I/O invocation admission, a confined + application-write-free two-observation check with an 8192-byte read bound per + observation, distinct missing/current/stale/invalid states, non-disclosing + operational failures, and independent cancellation/cleanup outcomes. +- `REQ-PROOFKIT-WORKFLOW-018`: exact source/check CLI flags, JSON root shapes, + exit semantics, one source/check-only command family, and source, generated, + and installed-carrier closure obligations. A binding is not an execution + receipt; final frozen-tree and installed-carrier integration proof requires + actual execution against the named source and artifacts. Shared stable-JSON/diagnostic hardening is owned by the supply-chain-quality spec. Typed local-reference closure is owned by the existing agent-envelope requirement. This workflow spec consumes those owners without duplicating them. +## Thin Integration Boundary + +The production owner is `internal/command/agentintegration`. The descriptor +alone selects `.agents/skills/agentic-proofkit/SKILL.md` for `codex` or +`.claude/skills/agentic-proofkit/SKILL.md` for `claude`. Source and check share +the same renderer. Common name/description frontmatter and static Markdown +delegate to the current CLI instead of embedding schemas or workflow policy. +The bootstrap persists logical routes, not machine-local executable paths; +an absent or ambiguous repository-approved installed launcher needs an owner +decision, never an implicit package manager, install, or network fallback. + +The source contract's default JSON includes exact content and its digest; +`--format text` emits those file bytes. Check returns only expected generated +identity and a bounded freshness classification, never observed content, its +digest, or caller root paths. Exit 0 means current, exit 2 means a classified +missing/stale/invalid file, and exit 1 means an invocation or operation error. +Source returns 0 on success and 1 on error. Neither accepts `--output` or an +install option. Manual export guidance is owned by `ADOPTION.md`. + +Phase5B managed installation, update, and safe removal remains explicitly open. +Host discovery, body loading, and approved-launcher invocation require separate +isolated native-host observations; direct CLI execution cannot prove skill use. + ## Non-Claims - Generated prompts are not authenticated and need not be obeyed or adequate. @@ -106,5 +147,9 @@ requirement. This workflow spec consumes those owners without duplicating them. - Least dependency closure does not prove context truth, freshness, semantic sufficiency, or global minimality. - A finite pilot does not prove universal repository fit. +- Bootstrap byte bounds are not token counts, and consumed registered-contract + identity is not complete transitive semantic proof. +- Generated-byte freshness does not install, activate, authorize, or grant + permissions. File removal does not revoke instructions already in host context. - This spec does not select a persisted requirement-source codec. - This spec does not approve merge, release, rollout, or production readiness. diff --git a/docs/specs/proofkit-agent-workflow/requirements.v1.json b/docs/specs/proofkit-agent-workflow/requirements.v1.json index 0e9799e..b1f3e78 100644 --- a/docs/specs/proofkit-agent-workflow/requirements.v1.json +++ b/docs/specs/proofkit-agent-workflow/requirements.v1.json @@ -203,6 +203,57 @@ "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, "deferral": null, "updatePolicy": {"reviewOwnerId": "proofkit.agent-workflow", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-016", + "ownerId": "proofkit.agent-workflow", + "invariant": "One private production owner in internal/command/agentintegration holds the finite tool/path descriptor table and one shared static bootstrap renderer consumed by both source and check; explicit codex selects .agents/skills/agentic-proofkit/SKILL.md and explicit claude selects .claude/skills/agentic-proofkit/SKILL.md, without a default tool or access to the other location. Generation is a deterministic repository-read-free and application-write-free projection with only name/description frontmatter, at most 512 UTF-8 metadata bytes and 4096 UTF-8 body bytes, exact file bytes in text mode, and no hooks, shell injection, permission grants, persona, background process, package installation, copied schema, state/action table, or independent workflow policy. The shared body delegates status, next, adoption/change planning, brief agent-route and existing detail retrieval, help, and native-evidence-guidance to current CLI owners. Identity binds the template, selected descriptor, and exact consumed registered routes and invocation contracts from existing app descriptors and generated input/output contract metadata; package version is absent from generated bytes, and version-only or unconsumed-command changes preserve bytes and identity when consumed registered projections are unchanged. Consumed contract or route changes invalidate identity; shared native source digests may conservatively invalidate freshness. Persist logical routes only, resolve the repository-approved already-installed launcher at invocation time, and stop for an owner decision when its binding is absent or ambiguous; tool, distribution channel, launcher profile, and concrete interpreter remain distinct coordinates without a persistent carrier option or network fallback.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-WORKFLOW-016"], + "nonClaims": [ + "Byte budgets do not prove tokenizer-specific token cost or model behavior.", + "Generation does not install instructions, activate a host skill, grant permissions, authorize execution, or prove native verification.", + "Materialization identity binds consumed registered invocation contracts, not complete schemas or every transitive native semantic behavior." + ], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.agent-workflow", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-017", + "ownerId": "proofkit.agent-workflow", + "invariant": "Integration freshness admits explicit tool, root, format, and flag cardinality before filesystem I/O, obtains expected bytes and the fixed selected path only from the integration source owner, and performs no application writes to selected, other-tool, adjacent, or global instructions. Reuse one read-only repository inspection lease and exact component-wise non-symlink regular-file traversal; check file kind before any content read and never read FIFO/device contents. Each of two complete observations reads at most 8192 bytes; reobserve the complete bounded state and bytes, verify file/route and pinned-root identity, and fail operationally on observed change. Exact entry or ordinary parent absence is missing; bounded regular UTF-8 text equal to generated bytes is current; differing bounded regular UTF-8 text, including unknown or old content, is stale; non-regular entry, symlink component, oversized file, NUL, or invalid UTF-8 is invalid. Permission, I/O, cleanup, portable-alias, changed-root/route, or cancellation failure is an operation error, never a classified success. Cancellation and file/lease cleanup outcomes participate independently in every result and dominate classification. Public reports and diagnostics disclose neither observed content, its digest, nor caller root paths; fixed descriptor-owned relative target paths and expected generated identities are public. A marker or stored digest never substitutes for checking actual bounded bytes.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-WORKFLOW-017"], + "nonClaims": [ + "Application-write-free inspection does not prevent filesystem-owned read metadata updates, exclude an uncooperative same-user writer, or identify unread oversized-file bytes beyond the bounded observation.", + "Current means exact generated-byte freshness only, not installation, host activation, approved-launcher invocation, native execution, semantic proof completion, or stability after return." + ], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.agent-workflow", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-018", + "ownerId": "proofkit.agent-workflow", + "invariant": "Exactly two no-input commands expose Phase5A: integration-source through integration source with built_in_package_catalog scope, and integration-check through integration check with explicit_filesystem_scan scope. Both require exactly one --tool from claude/codex, admit at most one --format from json/text with JSON default, reject input transport, surplus operands, color, carrier, output-file, and install options; check additionally requires exactly one --repo-root, which source rejects. Source JSON has exactly schemaVersion, kind, tool, targetPath, integrationId, content, contentDigest, capabilityDigest, metadataBytes, bodyBytes, and nonClaims; check JSON has exactly schemaVersion, kind, tool, targetPath, integrationId, expectedContentDigest, state, and nonClaims. Both schema versions are 1; kinds are proofkit.integration-source.v1 and proofkit.integration-check.v1. Source text is the exact generated content. Source exits 0 on success and 1 on error; check exits 0 only for current, 2 for missing/stale/invalid classified reports with empty stderr, and 1 for operation or invocation error. Cancellation observed before emission and serialization/cleanup failures produce no success report. Descriptor, dispatch, help, the agent-integrations source/check-only family, root-shape definitions and their hashes, CLI contracts, contract-map routes, exact native witnesses, generated metadata, and installed npm/Python carriers must close over the same bounded invocation/output relation without a second semantic owner; native source bindings include internal/command/agentintegration. Source-carrier independence and installed-carrier execution are separate obligations. Phase5B managed install/update/remove remains open and is not discharged by generation, check, manual export, or this contract closure.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-WORKFLOW-018"], + "nonClaims": [ + "Cancellation after the final pre-emission checkpoint cannot retract accepted output, and a caller-provided writer that accepts a prefix before failure is not an atomic sink.", + "Installed CLI behavior does not prove native-host file discovery, body loading, approved-launcher calls, registry publication, consumer migration, or host instruction revocation after file removal.", + "Phase5A is generation/check only, not managed installation, update, removal, host activation, permission grant, semantic full proof, merge approval, or production readiness.", + "Witness selectors declare exact verification routes, not executed or passed proof; installed npm/Python integration witnesses and final frozen-tree closure require actual execution against the named source and artifacts." + ], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.agent-workflow", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} } ], "nonClaims": [ diff --git a/internal/app/agent_integration_command.go b/internal/app/agent_integration_command.go new file mode 100644 index 0000000..62c8356 --- /dev/null +++ b/internal/app/agent_integration_command.go @@ -0,0 +1,130 @@ +package app + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "slices" + + "github.com/research-engineering/agentic-proofkit/internal/command/agentintegration" + "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" +) + +func runAgentIntegration(ctx context.Context, command string, args []string, stdout, stderr io.Writer) int { + options, err := parseAgentIntegrationArgs(command, args) + if err != nil { + writeDiagnostic(stderr, err) + return 1 + } + capabilities, err := integrationCapabilities(commandDescriptorByName, generatedCommandContractMetadataByName) + if err != nil { + writeDiagnostic(stderr, err) + return 1 + } + document, err := agentintegration.Source(options.tool, capabilities) + if err != nil { + writeDiagnostic(stderr, err) + return 1 + } + var output any = document.JSONValue() + text, exitCode := document.Content(), 0 + if command == "integration-check" { + result, err := agentintegration.Check(ctx, options.repositoryRoot, document) + if err != nil { + writeDiagnostic(stderr, err) + return 1 + } + output, text = result.JSONValue(), result.Text() + if result.State() != "current" { + exitCode = 2 + } + } + if err := ctx.Err(); err != nil { + writeDiagnosticf(stderr, "integration command cancelled before output") + return 1 + } + if options.format == "text" { + return writeText(text, exitCode, nil, stdout, stderr) + } + return writeJSON(output, exitCode, nil, stdout, stderr) +} + +type agentIntegrationArgs struct{ tool, format, repositoryRoot string } + +func parseAgentIntegrationArgs(command string, args []string) (agentIntegrationArgs, error) { + options := agentIntegrationArgs{format: "json"} + if command != "integration-source" && command != "integration-check" { + return options, fmt.Errorf("unsupported integration operation") + } + seen := map[string]bool{} + for index := 0; index < len(args); index++ { + flag := args[index] + if flag != "--tool" && flag != "--format" && (flag != "--repo-root" || command != "integration-check") { + return options, fmt.Errorf("unsupported integration argument") + } + if seen[flag] || index+1 >= len(args) || args[index+1] == "" { + return options, fmt.Errorf("integration arguments require one non-empty value per flag") + } + seen[flag] = true + index++ + switch flag { + case "--tool": + options.tool = args[index] + case "--format": + options.format = args[index] + case "--repo-root": + options.repositoryRoot = args[index] + } + } + if !slices.Contains(agentintegration.Tools(), options.tool) { + return options, fmt.Errorf("--tool requires claude or codex") + } + if options.format != "json" && options.format != "text" { + return options, fmt.Errorf("--format requires json or text") + } + if command == "integration-check" && options.repositoryRoot == "" { + return options, fmt.Errorf("integration check requires --repo-root") + } + return options, nil +} + +func integrationCapabilities(descriptors map[string]commandDescriptor, metadata map[string]generatedCommandContractMetadata) ([]agentintegration.Capability, error) { + commands := agentintegration.ConsumedCommands() + result := make([]agentintegration.Capability, len(commands)) + for index, command := range commands { + descriptor, exists := descriptors[command] + contract, hasContract := metadata[command] + if !exists || !hasContract { + return nil, fmt.Errorf("integration consumed command contract is unavailable") + } + // This projection binds existing invocation owners. It is not a second + // schema or a digest of every transitive implementation dependency. + value := map[string]any{ + "command": command, "route": descriptor.routeTokens, "input": descriptor.input, + "allowedFlags": descriptor.allowedFlags, "requiredFlags": descriptor.requiredFlags, + "exactlyOneOfFlags": descriptor.exactlyOneOfFlagGroups, "atMostOneOfFlags": descriptor.atMostOneOfFlagGroups, + "flagPresenceRequirements": descriptor.flagPresenceRequirements, "flagValueRequirements": descriptor.flagValueRequirements, + "singleOccurrenceFlags": descriptor.singleOccurrenceFlags, "flagChoices": descriptor.flagValueChoices, + "outputModes": descriptor.outputModes, "scopeClass": descriptor.scopeClass, + "agentEnvelope": descriptor.agentEnvelope, "contractEnvelope": descriptor.contractEnvelope, + "inputContractDigest": contract.InputContractSHA256, "outputContractDigest": contract.OutputContractSHA256, + } + encoded, err := json.Marshal(value) + if err != nil { + return nil, fmt.Errorf("encode integration invocation contract") + } + canonical, err := admission.DecodeJSON(bytes.NewReader(encoded), 64<<10) + if err != nil { + return nil, fmt.Errorf("admit integration invocation contract") + } + identity, err := digest.StableJSONSHA256Ref(canonical) + if err != nil { + return nil, err + } + result[index] = agentintegration.Capability{Command: command, Route: cloneStrings(descriptor.routeTokens), ContractDigest: identity} + } + return result, nil +} diff --git a/internal/app/agent_integration_command_test.go b/internal/app/agent_integration_command_test.go new file mode 100644 index 0000000..02b63f2 --- /dev/null +++ b/internal/app/agent_integration_command_test.go @@ -0,0 +1,217 @@ +package app + +import ( + "bytes" + "context" + "maps" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/command/agentintegration" + "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/testsupport/commandcoverage" +) + +func integrationDocument(t *testing.T, tool string) agentintegration.Document { + t.Helper() + capabilities, err := integrationCapabilities(commandDescriptorByName, generatedCommandContractMetadataByName) + if err != nil { + t.Fatal(err) + } + document, err := agentintegration.Source(tool, capabilities) + if err != nil { + t.Fatal(err) + } + return document +} + +func TestIntegrationSourceCLI(t *testing.T) { + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.055752742369677475265762786661152229127557861085124179111305077873855100236870") + for _, tool := range agentintegration.Tools() { + document := integrationDocument(t, tool) + for _, format := range []string{"json", "text"} { + code, output, diagnostic := executeAgentWorkflowCLI(t, []string{"integration", "source", "--tool", tool, "--format", format}, panicReader{}, PresentationCapabilities{StdoutIsTTY: true}) + if code != 0 || diagnostic != "" || strings.Contains(output, "\x1b") { + t.Fatalf("source format=%s exit=%d diagnostic=%q", format, code, diagnostic) + } + if format == "text" { + if output != document.Content() { + t.Fatal("text source differs from exact generated bytes") + } + } else { + value := decodeCLIJSON(t, output).(map[string]any) + if value["content"] != document.Content() || value["kind"] != "proofkit.integration-source.v1" || value["tool"] != tool { + t.Fatal("source command did not emit the selected generator document") + } + } + } + } + for _, args := range [][]string{ + {"integration", "source"}, + {"integration", "source", "--tool", "all"}, + {"integration", "source", "--tool", "codex", "--tool", "claude"}, + {"integration", "source", "--tool", "codex", "--repo-root", "unread"}, + {"integration", "source", "--tool", "codex", "--input", "-"}, + {"integration", "source", "--tool", "codex", "--output", "unused"}, + {"--json-layout", "compact", "integration", "source", "--tool", "codex", "--format", "text"}, + } { + code, output, diagnostic := executeAgentWorkflowCLI(t, args, panicReader{}, PresentationCapabilities{}) + if code != 1 || output != "" || diagnostic == "" { + t.Fatal("invalid generation invocation was not rejected before output") + } + } +} + +func TestIntegrationCheckCLI(t *testing.T) { + commandcoverage.SemanticRoute(t, "proofkit.command_coverage.source_oracle.v1.115408364061752394702935199140172268554813555954760274834640853529071448021786") + for _, tool := range agentintegration.Tools() { + t.Run(tool, func(t *testing.T) { + root := t.TempDir() + document := integrationDocument(t, tool) + path := filepath.Join(root, filepath.FromSlash(document.JSONValue()["targetPath"].(string))) + for _, state := range []string{"missing", "current", "stale", "invalid"} { + if state != "missing" { + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + t.Fatal(err) + } + content := document.Content() + if state == "stale" { + content += "owner edit\n" + } + if state == "invalid" { + content = "\x00" + } + if err := os.WriteFile(path, []byte(content), 0o600); err != nil { + t.Fatal(err) + } + } + before, beforeErr := os.ReadFile(path) + for _, format := range []string{"json", "text"} { + code, output, diagnostic := executeAgentWorkflowCLI(t, []string{"integration", "check", "--tool", tool, "--repo-root", root, "--format", format}, panicReader{}, PresentationCapabilities{}) + wantCode := 2 + if state == "current" { + wantCode = 0 + } + if code != wantCode || diagnostic != "" || strings.Contains(output, root) || strings.Contains(output, "owner edit") { + t.Fatalf("check state=%s format=%s exit=%d diagnosticPresent=%t", state, format, code, diagnostic != "") + } + if format == "json" { + value := decodeCLIJSON(t, output).(map[string]any) + if value["kind"] != "proofkit.integration-check.v1" || value["state"] != state || value["integrationId"] != document.JSONValue()["integrationId"] { + t.Fatal("check result does not classify selected generated file") + } + } else if !strings.Contains(output, state) || strings.Contains(output, "\x1b") { + t.Fatal("check text does not preserve state") + } + } + after, afterErr := os.ReadFile(path) + if !bytes.Equal(before, after) || (beforeErr == nil) != (afterErr == nil) { + t.Fatal("read-only check mutated the selected entry") + } + } + code, output, diagnostic := executeAgentWorkflowCLI(t, []string{"integration", "check", "--tool", "invalid", "--repo-root", filepath.Join(root, "absent")}, panicReader{}, PresentationCapabilities{}) + if code != 1 || output != "" || !strings.Contains(diagnostic, "--tool") { + t.Fatal("tool admission did not dominate repository I/O") + } + }) + } +} + +func TestIntegrationCommandsExactRootShapes(t *testing.T) { + for _, operation := range []string{"source", "check"} { + args := []string{"integration", operation, "--tool", "codex"} + keys := []string{"bodyBytes", "capabilityDigest", "content", "contentDigest", "integrationId", "kind", "metadataBytes", "nonClaims", "schemaVersion", "targetPath", "tool"} + wantCode := 0 + if operation == "check" { + args = append(args, "--repo-root", t.TempDir()) + keys = []string{"expectedContentDigest", "integrationId", "kind", "nonClaims", "schemaVersion", "state", "targetPath", "tool"} + wantCode = 2 + } + code, output, diagnostic := executeAgentWorkflowCLI(t, args, panicReader{}, PresentationCapabilities{}) + if code != wantCode || diagnostic != "" { + t.Fatalf("root shape command exit=%d", code) + } + assertExactObjectKeys(t, decodeCLIJSON(t, output).(map[string]any), keys, operation) + } +} + +func TestIntegrationSourcesAreCarrierIndependent(t *testing.T) { + for _, tool := range agentintegration.Tools() { + for _, format := range []string{"json", "text"} { + var previous string + for _, profile := range []string{cliexec.ProfilePath, cliexec.ProfileNPMOffline, cliexec.ProfilePythonModule} { + python := "" + if profile == cliexec.ProfilePythonModule { + python = "/isolated/consumer/bin/python" + } + renderer, err := cliexec.AdmitLauncherProfile(profile, python) + if err != nil { + t.Fatal(err) + } + var stdout, stderr bytes.Buffer + code := RunWithRenderer(context.Background(), []string{"integration", "source", "--tool", tool, "--format", format}, panicReader{}, &stdout, &stderr, renderer) + if code != 0 || stderr.Len() != 0 || previous != "" && previous != stdout.String() { + t.Fatal("process-local launcher changed portable source bytes") + } + previous = stdout.String() + } + } + } +} + +func TestIntegrationCapabilityIdentityScope(t *testing.T) { + base, err := integrationCapabilities(commandDescriptorByName, generatedCommandContractMetadataByName) + if err != nil { + t.Fatal(err) + } + metadata := maps.Clone(generatedCommandContractMetadataByName) + unconsumed := metadata["text-policy"] + unconsumed.OutputContractSHA256 = digest.SHA256TextRef("changed unconsumed record") + metadata["text-policy"] = unconsumed + unchanged, err := integrationCapabilities(commandDescriptorByName, metadata) + if err != nil || !reflect.DeepEqual(base, unchanged) { + t.Fatal("unconsumed metadata altered integration capabilities") + } + for _, command := range agentintegration.ConsumedCommands() { + for _, operand := range []string{"route", "flags", "contract"} { + descriptors := maps.Clone(commandDescriptorByName) + descriptor := descriptors[command].clone() + metadata := maps.Clone(generatedCommandContractMetadataByName) + switch operand { + case "route": + descriptor.routeTokens = []string{"new", "route"} + case "flags": + descriptor.requiredFlags = append(descriptor.requiredFlags, "--new-flag") + case "contract": + changed := metadata[command] + changed.OutputContractSHA256 = digest.SHA256TextRef("changed consumed record") + metadata[command] = changed + } + descriptors[command] = descriptor + changed, err := integrationCapabilities(descriptors, metadata) + if err != nil || reflect.DeepEqual(base, changed) { + t.Fatalf("consumed %s/%s change was not bound", command, operand) + } + } + } + delete(metadata, "next") + if _, err := integrationCapabilities(commandDescriptorByName, metadata); err == nil { + t.Fatal("missing consumed contract was accepted") + } +} + +func TestIntegrationCancellationBeforeOutput(t *testing.T) { + for _, format := range []string{"json", "text"} { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + var stdout, stderr bytes.Buffer + code := Run(ctx, []string{"integration", "source", "--tool", "codex", "--format", format}, panicReader{}, &stdout, &stderr) + if code != 1 || stdout.Len() != 0 || !strings.Contains(stderr.String(), "cancelled") { + t.Fatal("cancelled source command began emitting output") + } + } +} diff --git a/internal/app/app.go b/internal/app/app.go index df1b8be..8add6f8 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -140,6 +140,8 @@ func RunWithRendererAndCapabilities(ctx context.Context, args []string, stdin io }) case commandRunnerAgentWorkflow: return runAgentWorkflowCommand(args[0], args[1:], stdin, stdout, stderr, capabilities) + case commandRunnerAgentIntegration: + return runAgentIntegration(ctx, args[0], args[1:], stdout, stderr) case commandRunnerAgentRoute: return runAgentRoute(args[1:], stdin, stdout, stderr, renderer) case commandRunnerContractEnvelope: diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index a1be0f4..924cebf 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -24,7 +24,7 @@ import ( ) const ( - cliContractPublicABISHA256 = "9a6842b45a218d6caa5da517b0b20f861e13c35a2900e92d34361cdf771781f7" + cliContractPublicABISHA256 = "58890d28e1518df871b2c363a6255fb500822534060c8f3ae169eff9533b66db" maxAggregateFileReadBytesForContractTest = 64 << 20 maxPackageManifestBytesForContractTest = 256 << 10 maxSourceFileBytesForContractTest = 8 << 20 @@ -1525,6 +1525,8 @@ func TestDescriptorFlagConstraintsAreRenderedTruthfully(t *testing.T) { "adopt-plan": "agentic-proofkit adopt plan [--color ] [--format ] --mode --repo-root [--stack ]", "adoption-contract-envelope": "agentic-proofkit adoption-contract-envelope --input [--agent-envelope] [--checked-scope ] [--guidance-mode ] [--materialization-manifest] --mode [--pilot ] [--touched-rule-id ]", "conformance-profile": "agentic-proofkit conformance-profile --input [--format ] [--input-pointer ] (--list | --profile | --verify)", + "integration-check": "agentic-proofkit integration check [--format ] --repo-root --tool ", + "integration-source": "agentic-proofkit integration source [--format ] --tool ", "json-report-cli-adapter-source": "agentic-proofkit json-report-cli-adapter-source [--format ] --language ", "next": "agentic-proofkit next [--color ] [--format ] --repo-root ", "requirement-browser-server": "agentic-proofkit requirement-browser-server --input [--empty-local-environment-policy] [--host <127.0.0.1|::1>] [--input-pointer ] [--local-environment-class ] [--open] [--port ] [--scope ] [--serve] [--session-mode ] [--session-timeout-seconds <1..7200>] --view ", diff --git a/internal/app/command_contract_generated.go b/internal/app/command_contract_generated.go index 0b3edfc..e14c431 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 = "4a409dd87ab13fa3f3951c16438f1d0f1595cc0ca2f6a1e1317c5c0e2fc9801e" +const commandContractSourceSHA256 = "9d8a9843ac240b524eeb5c27f4ca8833b411d410d36c6841e14134e1ce8a57d0" type generatedCommandContractMetadata struct { InputContractSHA256 string @@ -12,15 +12,15 @@ type generatedCommandContractMetadata struct { } var generatedCommandContractMetadataByName = map[string]generatedCommandContractMetadata{ - "adopt-materialize-apply": {InputContractSHA256: "sha256:98539b75bf6d4caebc75d724e9201925a1235290791adcebc865c1b3c74976ee", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.apply-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:8dea77f631337d8b6b4137c27bb3f432ade41f197327743b7b7d8c5296d922d0", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "apply"}}, - "adopt-materialize-plan": {InputContractSHA256: "sha256:65925b56d0332349c5048e9b1450ff0b7a3381224342df1a630128e3f4a2cde2", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.plan-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:b53763c6e129cb62ead5ebf05193ec5316a701bb4277a90fe9fb47f5d8e9d6c3", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "plan"}}, - "adopt-materialize-recover": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:925376cf78507421710b59485da9798d2b79d1bac65da5a3bfc6f4f8aa64fadd", FlagChoices: map[string][]string{"--action": []string{"resume", "rollback"}, "--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "recover"}}, + "adopt-materialize-apply": {InputContractSHA256: "sha256:98539b75bf6d4caebc75d724e9201925a1235290791adcebc865c1b3c74976ee", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.apply-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:1257f86e497d70dd8e16af3cee5852b7b36ff632f4bc6d6ff8852ce4d5e8c075", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "apply"}}, + "adopt-materialize-plan": {InputContractSHA256: "sha256:65925b56d0332349c5048e9b1450ff0b7a3381224342df1a630128e3f4a2cde2", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.plan-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:bca136e026ddd9e8be20c6d25750105f5afb9bdf9dfa45fc07c03b09ae54ac79", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "plan"}}, + "adopt-materialize-recover": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:0837185fa64aec6ec405ca27e6f08af297e773e17a5a1963617cd9a2b04629d8", FlagChoices: map[string][]string{"--action": []string{"resume", "rollback"}, "--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "recover"}}, "adopt-plan": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:13e3392d9005c27fed3003a1d036123fe12a341dbe16a97c0cd9f8272fb83320", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}, "--mode": []string{"audit-from-code", "code-baseline", "fresh"}, "--stack": []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"}}, RouteTokens: []string{"adopt", "plan"}}, "adoption-checklist": {InputContractSHA256: "sha256:4e6c4c9b369279837a5894c0b3f842a411dce529b91c91cb2d4ec63eb5ee4c2c", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.adoption-checklist.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:9d0d0e60f0935407fd31007d8502459663eb4c7228dc5e3c7727ae2c9907bdc9", FlagChoices: map[string][]string{}, RouteTokens: []string{"adoption-checklist"}}, "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{}, RouteTokens: []string{"adoption-contract-envelope"}}, "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{}, RouteTokens: []string{"adoption-doctor"}}, "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{}, RouteTokens: []string{"adoption-workflow-plan"}}, - "agent-route": {InputContractSHA256: "sha256:c00e832b4e9eac6b858eec46e810431c0a5c9f56c5c50f055f39ee024f50014c", InputSchemaSummary: []string{"availableInputs", "browserMode", "goal", "knownChangedPaths", "mode", "nonClaims", "observedReports", "openBrowser", "routeId", "schemaVersion", "root-shape-only definition proofkit.agent-route.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:ad70c213408169defdfc05a2b095b793a9f9e9d9e4a7a3d5ec2e32cd83132f00", FlagChoices: map[string][]string{}, RouteTokens: []string{"agent-route"}}, + "agent-route": {InputContractSHA256: "sha256:c00e832b4e9eac6b858eec46e810431c0a5c9f56c5c50f055f39ee024f50014c", InputSchemaSummary: []string{"availableInputs", "browserMode", "goal", "knownChangedPaths", "mode", "nonClaims", "observedReports", "openBrowser", "routeId", "schemaVersion", "root-shape-only definition proofkit.agent-route.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:746c36d030687077a41e91ea5391e1d051a0fcb9aebe3ed04001624191edfc72", FlagChoices: map[string][]string{}, RouteTokens: []string{"agent-route"}}, "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{}, RouteTokens: []string{"binding-partition"}}, "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{}, RouteTokens: []string{"branch-authority"}}, "capability-map-admission": {InputContractSHA256: "sha256:e49433f295c43c34d5d660ac9d656b117ed87208406b57723d25165ffec5d486", 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:bfa35fe1be210ab98f3620694ab63b52a9724f7b92cd9dcbfd7b01b2c6a3555e", FlagChoices: map[string][]string{}, RouteTokens: []string{"capability-map-admission"}}, @@ -38,6 +38,8 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "gradual-adoption-guidance": {InputContractSHA256: "sha256:4752cbac81c864cb3e18a39facfd666a9707314233d54798c7f71e67d7f2800c", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.gradual-adoption-guidance.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:171fed4bb8d32a47fc5ec49796f5b0b55ed666feaccc2fbbfeb12da31d80ecc9", FlagChoices: map[string][]string{}, RouteTokens: []string{"gradual-adoption-guidance"}}, "help": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "", FlagChoices: map[string][]string{}, RouteTokens: []string{"help"}}, "impact": {InputContractSHA256: "sha256:41d3107414837955ee408d5ce94949a4c1a6b76f6949e6c1dc224bd06f6b09bc", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.impact.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:73066e9a5ca48f21936111ffb7223900fb629875997f4e7b16d7fef9c4177972", FlagChoices: map[string][]string{}, RouteTokens: []string{"impact"}}, + "integration-check": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:4662f396cd9af10c0ccad9bd13e8d69425a312a925629102f9c2d33fa3f8d068", FlagChoices: map[string][]string{"--format": []string{"json", "text"}, "--tool": []string{"claude", "codex"}}, RouteTokens: []string{"integration", "check"}}, + "integration-source": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:a46e36438c4beafd358dfd3b1d5072ae9e0a0b69a4ce674ee7ca5c93cad7a976", FlagChoices: map[string][]string{"--format": []string{"json", "text"}, "--tool": []string{"claude", "codex"}}, RouteTokens: []string{"integration", "source"}}, "json-report-cli-adapter-source": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:6c3dd1c8507a90e055cf2c886089446d8560ff3e0d3ca9cc6360a3377d2d85da", FlagChoices: map[string][]string{}, RouteTokens: []string{"json-report-cli-adapter-source"}}, "migration-parity-admission": {InputContractSHA256: "sha256:0b36c0e68da3b857dac4b13e7b3bd523052459106133aa8c908a4352682e6c05", InputSchemaSummary: []string{"schemaVersion=1", "paritySetId", "sourceProofOwners[]", "targetProofkitRefs[]", "parityRecords[]", "nonClaims[]", "root-shape-only definition proofkit.migration-parity-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:8e0f8af2b205817f018b0fe133fe789661caa29007695e036bfcab63c1830f47", FlagChoices: map[string][]string{}, RouteTokens: []string{"migration-parity-admission"}}, "migration-plan": {InputContractSHA256: "sha256:58a62759a634101ce2ca9218184175134bbe5633328e1b23797b94c19fc9b11a", InputSchemaSummary: []string{"schemaVersion=1", "migrationId", "sourceProofOwners[]", "targetProofkitRefs[]", "parityEvidenceRefs[]", "retainedOwners[]", "retirementCandidates[]", "followUpCommands[]", "nonClaims[]", "root-shape-only definition proofkit.migration-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:f14f0381e9dc241357c346315b95b03ef5b23f1d1bbc3b00f111fbe1515ed3ff", FlagChoices: map[string][]string{}, RouteTokens: []string{"migration-plan"}}, @@ -45,7 +47,7 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "next": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:92f6d3dc427a795ec97112e6c3ce55ebd4ab670e4323b4faa8215310a8492747", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"next"}}, "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{}, RouteTokens: []string{"obligation-decision"}}, "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{}, RouteTokens: []string{"package-runtime-dependency-admission"}}, - "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:33f5142785396fa40de9ea05f8c466e88a116cc5230345569d1bb55ca91401ca", FlagChoices: map[string][]string{}, RouteTokens: []string{"pilot-admission"}}, + "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:892052fb91837cb5778beca131b54515973432e6f6a4ee2e201cb6453c8972c5", FlagChoices: map[string][]string{}, RouteTokens: []string{"pilot-admission"}}, "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{}, RouteTokens: []string{"producer-policy-self-proof"}}, "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{}, RouteTokens: []string{"proof-obligation-algebra"}}, "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{}, RouteTokens: []string{"proof-receipt-admission"}}, @@ -84,7 +86,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{}, RouteTokens: []string{"selective-gate-evidence"}}, "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{}, RouteTokens: []string{"selective-gate-obligation-decision-input"}}, "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{}, RouteTokens: []string{"selective-gate-plan"}}, - "self-check": {InputContractSHA256: "sha256:c8a71b9e6d59bf34cfa1261382a567fe6eed67d7e23256a36661ee434fedefb4", 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:614cf6e5329ee9a97c4d2c8429bf7853acc661abdfc65c496f4b96fd13b5c87b", FlagChoices: map[string][]string{}, RouteTokens: []string{"self-check"}}, + "self-check": {InputContractSHA256: "sha256:915468675e98a165220ad4bf693462526534d53e718556c6f2066b3c1e934ea8", 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:ebe612f409f9a1b9516ce14b0368f812db3f5e8cd759bc15b02936d5d80f8866", FlagChoices: map[string][]string{}, RouteTokens: []string{"self-check"}}, "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{}, RouteTokens: []string{"spec-overview-claims"}}, "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{}, RouteTokens: []string{"spec-proof-bundle-admission"}}, "stack-preset": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:ef5920f363a4a96dcac308ea8412260a06e64ba4876460a369aefb8983130a9d", FlagChoices: map[string][]string{"--preset": []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"}}, RouteTokens: []string{"stack-preset"}}, diff --git a/internal/app/command_coverage_routes.go b/internal/app/command_coverage_routes.go index 5d94687..5574c3c 100644 --- a/internal/app/command_coverage_routes.go +++ b/internal/app/command_coverage_routes.go @@ -56,6 +56,8 @@ var requiredInputAdmissionRoute = commandCoverageRoute{ } var commandCoverageRoutes = map[string][]commandCoverageRoute{ + "integration-source": {directCLIRoute("internal/app/agent_integration_command_test.go", "TestIntegrationSourceCLI", semanticRouteProof("agent_integration.source_whole_cli"), "Integration source must emit exact bounded portable bytes through JSON and text without reading caller input or accepting implicit tool selection.")}, + "integration-check": {directCLIRoute("internal/app/agent_integration_command_test.go", "TestIntegrationCheckCLI", semanticRouteProof("agent_integration.check_whole_cli"), "Integration check must classify actual fixed-path bytes without writing files or treating missing, stale, or invalid content as current.")}, "adoption-checklist": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/adoptionchecklist/adoptionchecklist_test.go", "TestBuildClassifiesRequiredChecklistItemsAndPreservesOptionalNonFailures", semanticRouteProof("adoptionchecklist.build_classifies_required_checklist_items_and_preserves_optional_non_failures"), "Adoption checklist reports must fail missing, blocked, and not-applicable required items while preserving optional non-failures.")}, "adoption-contract-envelope": {requiredInputAdmissionRoute, packageFalsifierRoute("internal/command/adoptioncontract/adoptioncontract_test.go", "TestBuildDelegatesModesWithParity", semanticRouteProof("adoptioncontract.build_delegates_modes_with_parity"), "Adoption contract envelope admission must prove aggregate-root admission while delegating selected modes to existing child command outputs without drift.")}, "adoption-doctor": {requiredInputAdmissionRoute, directCLIRoute("internal/app/cli_abi_test.go", "TestAdoptionDoctorCLIABI", semanticRouteProof("cli_abi.adoption_doctor_cliabi"), "Adoption doctor CLI ABI must emit stable report and agent-envelope JSON for admitted caller records."), packageFalsifierRoute("internal/command/adoptiondoctor/adoptiondoctor_test.go", "TestBuildFailsEnforcementForCandidateBoundaryAndMissingRoutes", semanticRouteProof("adoptiondoctor.build_fails_enforcement_for_candidate_boundary_and_missing_routes"), "Adoption doctor reports must fail closed for enforcement modes when caller-provided owner routes or candidate boundaries are not admitted.")}, diff --git a/internal/app/command_coverage_test.go b/internal/app/command_coverage_test.go index 581bb30..8c3a713 100644 --- a/internal/app/command_coverage_test.go +++ b/internal/app/command_coverage_test.go @@ -528,6 +528,19 @@ func noInputRuntimeSmokeArgs(t *testing.T, descriptor commandDescriptor) ([]stri return append(cloneStrings(descriptor.routeTokens), "--mode", "fresh", "--repo-root", t.TempDir()), true case "help": return []string{"help"}, false + case "integration-source": + return []string{"integration", "source", "--tool", "codex"}, true + case "integration-check": + root := t.TempDir() + document := integrationDocument(t, "codex") + path := filepath.Join(root, ".agents", "skills", "agentic-proofkit", "SKILL.md") + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(document.Content()), 0o600); err != nil { + t.Fatal(err) + } + return []string{"integration", "check", "--tool", "codex", "--repo-root", root}, true case "json-report-cli-adapter-source": return []string{"json-report-cli-adapter-source", "--language", "typescript"}, true case "native-evidence-guidance": diff --git a/internal/app/command_descriptors.go b/internal/app/command_descriptors.go index 8793b39..901ba1f 100644 --- a/internal/app/command_descriptors.go +++ b/internal/app/command_descriptors.go @@ -4,6 +4,7 @@ import ( "slices" "sort" + "github.com/research-engineering/agentic-proofkit/internal/command/agentintegration" "github.com/research-engineering/agentic-proofkit/internal/command/requirementbrowser" "github.com/research-engineering/agentic-proofkit/internal/command/requirementproofview" ) @@ -26,6 +27,7 @@ const ( commandRunnerAdoptionWorkflow commandRunner = "adoption_workflow" commandRunnerAgentWorkflow commandRunner = "agent_workflow" commandRunnerAgentRoute commandRunner = "agent_route" + commandRunnerAgentIntegration commandRunner = "agent_integration" commandRunnerConformanceProfile commandRunner = "conformance_profile" commandRunnerContractEnvelope commandRunner = "contract_envelope" commandRunnerGradualAdoptionBootstrap commandRunner = "gradual_adoption_bootstrap" @@ -121,6 +123,8 @@ var commandDescriptors = []commandDescriptor{ command("gradual-adoption-guidance", commandInputRequired, flags("--agent-envelope", "--checked-scope", "--contract-envelope", "--guidance-mode", "--input", "--input-pointer", "--touched-rule-id"), modes("json"), ownerDirs("gradualadoption"), withRunner(commandRunnerGradualAdoptionGuidance), withAgentEnvelope(), withContractEnvelope()), command("help", commandInputNone, flags("--help", "-h"), modes("text"), ownerDirs("help"), withRunner(commandRunnerHelp), withSemanticAppTests("TestHelpCommandContractForms")), command("impact", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("impact")), + command("integration-check", commandInputNone, flags("--format", "--repo-root", "--tool"), modes("json", "text"), ownerDirs("agentintegration"), withRunner(commandRunnerAgentIntegration), withSemanticAppTests("TestIntegrationCheckCLI"), withScopeClass(commandScopeExplicitFileSystemScan), withRequiredFlags("--repo-root", "--tool"), withFlagChoices("--format", "json", "text"), withFlagChoices("--tool", agentintegration.Tools()...), withSingleOccurrenceFlags("--repo-root", "--tool")), + command("integration-source", commandInputNone, flags("--format", "--tool"), modes("json", "text"), ownerDirs("agentintegration"), withRunner(commandRunnerAgentIntegration), withSemanticAppTests("TestIntegrationSourceCLI"), withRequiredFlags("--tool"), withFlagChoices("--format", "json", "text"), withFlagChoices("--tool", agentintegration.Tools()...), withSingleOccurrenceFlags("--tool")), command("json-report-cli-adapter-source", commandInputNone, flags("--format", "--language"), modes("json"), ownerDirs("jsonreportcliadaptersource"), withRunner(commandRunnerJSONReportCLIAdapterSource), withRequiredFlags("--language")), command("migration-parity-admission", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("migrationparityadmission")), command("migration-plan", commandInputRequired, flags("--input", "--input-pointer"), modes("json"), ownerDirs("migrationplan")), @@ -184,6 +188,7 @@ var commandDescriptors = []commandDescriptor{ } var knownCommandRunners = map[commandRunner]struct{}{ + commandRunnerAgentIntegration: {}, commandRunnerGenericInput: {}, commandRunnerAdoptionFrontDoor: {}, commandRunnerAdoptionMaterialization: {}, diff --git a/internal/app/command_family_catalog_generated.go b/internal/app/command_family_catalog_generated.go index 783a9a3..22affea 100644 --- a/internal/app/command_family_catalog_generated.go +++ b/internal/app/command_family_catalog_generated.go @@ -1,7 +1,7 @@ // Code generated by internal/tools/commandfamilygen; DO NOT EDIT. package app -const commandFamilyCatalogSourceSHA256 = "9f9103412437d96896976af0429bf7ed2bd21fdd878010f13eda22cb05827f53" +const commandFamilyCatalogSourceSHA256 = "f725450743d3d270bce9adae34c23c0468415182ff9a6fc87ba51e845d4d65e0" func generatedCommandFamilyCatalog() commandFamilyCatalog { return commandFamilyCatalog{ @@ -9,6 +9,7 @@ func generatedCommandFamilyCatalog() commandFamilyCatalog { Families: []commandFamily{ {ID: "adoption-lifecycle", Label: "Adoption lifecycle", Purpose: "Select and assess repository adoption stages.", Commands: []string{"adopt-plan", "adoption-checklist", "adoption-doctor", "adoption-workflow-plan", "gradual-adoption", "gradual-adoption-bootstrap", "gradual-adoption-guidance", "pilot-admission"}}, {ID: "adoption-materialization", Label: "Adoption materialization", Purpose: "Plan, apply, and recover confined candidate adoption artifacts.", Commands: []string{"adopt-materialize-apply", "adopt-materialize-plan", "adopt-materialize-recover"}}, + {ID: "agent-integrations", Label: "Agent integrations", Purpose: "Generate portable bootstrap source and check generated-byte freshness without installation or host activation.", Commands: []string{"integration-check", "integration-source"}}, {ID: "agent-workflow-planning", Label: "Agent workflow planning", Purpose: "Plan bounded engineering-change stages and expose repository-neutral native-evidence guidance.", Commands: []string{"change-workflow-plan", "native-evidence-guidance"}}, {ID: "cli-metadata-and-conformance", Label: "CLI metadata and conformance", Purpose: "Expose, route, generate, and self-check CLI contract surfaces.", Commands: []string{"agent-route", "conformance-profile", "help", "json-report-cli-adapter-source", "self-check"}}, {ID: "deployment-and-readiness", Label: "Deployment and readiness", Purpose: "Admit deployment evidence and bounded closeout decisions.", Commands: []string{"branch-authority", "completion-criteria", "deployment-evidence-admission", "readiness-closeout"}}, diff --git a/internal/app/integration_version_edge_test.go b/internal/app/integration_version_edge_test.go new file mode 100644 index 0000000..aeaf1a5 --- /dev/null +++ b/internal/app/integration_version_edge_test.go @@ -0,0 +1,70 @@ +package app + +import ( + "os" + "path/filepath" + "slices" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/commandroute" + "github.com/research-engineering/agentic-proofkit/internal/tools/releasechange" +) + +func readFrozenIntegrationPublicABI(t *testing.T) frozenPublicABI { + t.Helper() + return readFrozenPublicABI(t, "internal/app/testdata/releases/v0.9.0/public-abi-observation.json", + "633f68f322e942902d8ccf10468d298bdf70f6692390ef1bfa24190538b90fc3", "0.9.0", + "sha256:9a6842b45a218d6caa5da517b0b20f861e13c35a2900e92d34361cdf771781f7") +} + +func integrationProcessAppendices() map[string]string { + return map[string]string{ + "stdout": " Integration check also emits exactly one classified JSON value or text report with exit 2 for missing, stale, or invalid; current exits 0, and operational errors exit 1.", + "stderr": " Integration check classified exit-2 reports leave stderr empty; operational or invocation errors use non-disclosing stderr diagnostics.", + } +} + +func TestIntegrationVersionEdgeClosesCompletePublicABIDiff(t *testing.T) { + frozen := readFrozenIntegrationPublicABI(t) + if err := verifyAdditivePublicABIDiff(frozen, readArchivedProjectNavigationContract(t), []string{}, nil); err != nil { + t.Fatalf("frozen predecessor fingerprints differ from archived release bytes: %v", err) + } + if err := verifyAdditivePublicABIDiff(frozen, readCLIContractRaw(t), []string{"integration-check", "integration-source"}, integrationProcessAppendices()); err != nil { + t.Fatal(err) + } + if current := "sha256:" + currentCLIContractPublicABISHA256(t); current == frozen.PublicABISHA256 { + t.Fatal("new public commands retained the previous complete ABI identity") + } + + change, err := releasechange.Read(filepath.Join(repoRoot(t), releasechange.RecordPath)) + if err != nil { + t.Fatal(err) + } + if change.PreviousVersion != frozen.ReleaseVersion || change.Version != "0.10.0" || change.ChangeClass != "compatible" || len(change.BreakingChanges) != 0 || change.Migration.Required || len(change.Migration.Steps) != 0 || !slices.Equal(releaseChangeIDs(change.Additions), []string{"proofkit.agent-integration.freshness", "proofkit.agent-integration.source"}) { + t.Fatal("integration release record does not describe the exact compatible addition") + } + file, err := os.Open(filepath.Join(repoRoot(t), "package.json")) + if err != nil { + t.Fatal(err) + } + defer file.Close() + manifest, err := admission.DecodeTypedJSON[struct { + Version string `json:"version"` + }](file, 64<<10) + if err != nil { + t.Fatal(err) + } + if manifest.Version != change.Version { + t.Fatal("integration package version differs from its release record") + } + contracts, err := currentVersionEdgeCommandContracts(repoRoot(t), []string{"integration-check", "integration-source"}) + if err != nil { + t.Fatal(err) + } + for _, contract := range contracts { + if contract.InputContract != nil || contract.OutputContract.ContractID != "proofkit."+contract.Command+".output.v1" || commandroute.Text(contract.Route) != "integration "+contract.Command[len("integration-"):] { + t.Fatalf("integration command does not expose its exact no-input route and output contract: %s", contract.Command) + } + } +} diff --git a/internal/app/project_navigation_abi_closure_test.go b/internal/app/project_navigation_abi_closure_test.go index ab4d62f..0ff6278 100644 --- a/internal/app/project_navigation_abi_closure_test.go +++ b/internal/app/project_navigation_abi_closure_test.go @@ -2,9 +2,10 @@ package app import ( "bytes" + "compress/gzip" "crypto/sha256" - "encoding/json" "fmt" + "io" "os" "path/filepath" "slices" @@ -12,99 +13,42 @@ import ( "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" - "github.com/research-engineering/agentic-proofkit/internal/kernel/commandroute" "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" ) -const frozenProjectNavigationPublicABIPath = "internal/app/testdata/releases/v0.8.0/public-abi-observation.json" -const frozenProjectNavigationPublicABISHA256 = "ffff23ca84e014176d854104ea07eb92b10b4d6db7822f14f7859f6f6d360997" - -const projectNavigationCommandFingerprintPolicy = "semantic_command_contract_without_native_source_digests" - -type frozenProjectNavigationPublicABI struct { - CommandFingerprintPolicy string `json:"commandFingerprintPolicy"` - Commands map[string]string `json:"commands"` - ContractDefinitions map[string]string `json:"contractDefinitions"` - ContractID string `json:"contractId"` - ContractSchemaVersion int `json:"contractSchemaVersion"` - NonClaims []string `json:"nonClaims"` - ObservationKind string `json:"observationKind"` - OrderingPolicy string `json:"orderingPolicy"` - PackageName string `json:"packageName"` - ProcessContractSHA256 string `json:"processContractSha256"` - PublicABISHA256 string `json:"publicAbiSha256"` - ReleaseVersion string `json:"releaseVersion"` - SchemaVersion int `json:"schemaVersion"` -} +const archivedProjectNavigationContractSHA256 = "4a409dd87ab13fa3f3951c16438f1d0f1595cc0ca2f6a1e1317c5c0e2fc9801e" func TestProjectNavigationVersionEdgeClosesCompletePublicABIDiff(t *testing.T) { - frozen := readFrozenProjectNavigationPublicABI(t) - current := readProjectNavigationCLIContractRaw(t) - if err := verifyCompleteProjectNavigationABIDiff(frozen, current); err != nil { + if err := verifyCompleteProjectNavigationABIDiff(readFrozenProjectNavigationPublicABI(t), readArchivedProjectNavigationContract(t)); err != nil { t.Fatal(err) } } -func readFrozenProjectNavigationPublicABI(t *testing.T) frozenProjectNavigationPublicABI { +func readArchivedProjectNavigationContract(t *testing.T) map[string]any { t.Helper() - content, err := os.ReadFile(filepath.Join(repoRoot(t), frozenProjectNavigationPublicABIPath)) + content, err := os.ReadFile(filepath.Join(repoRoot(t), archivedProjectNavigationReleaseRoot, "cli-contract.v2.json.gz")) if err != nil { t.Fatal(err) } - sum := sha256.Sum256(content) - if got := fmt.Sprintf("%x", sum); got != frozenProjectNavigationPublicABISHA256 { - t.Fatalf("frozen public ABI observation digest=%s, want %s", got, frozenProjectNavigationPublicABISHA256) - } - value, err := admission.DecodeJSON(bytes.NewReader(content), int64(len(content))) + reader, err := gzip.NewReader(bytes.NewReader(content)) if err != nil { t.Fatal(err) } - root, ok := value.(map[string]any) - if !ok { - t.Fatal("frozen public ABI observation must be an object") - } - assertExactObjectKeys(t, root, []string{"commandFingerprintPolicy", "commands", "contractDefinitions", "contractId", "contractSchemaVersion", "nonClaims", "observationKind", "orderingPolicy", "packageName", "processContractSha256", "publicAbiSha256", "releaseVersion", "schemaVersion"}, "frozen public ABI observation") - var observation frozenProjectNavigationPublicABI - if err := json.Unmarshal(content, &observation); err != nil { - t.Fatal(err) + raw, err := io.ReadAll(io.LimitReader(reader, 1<<20)) + closeErr := reader.Close() + if err != nil || closeErr != nil { + t.Fatalf("read archived CLI contract: %v; close: %v", err, closeErr) } - if observation.SchemaVersion != 1 || observation.ObservationKind != "proofkit.frozen-public-abi-observation" || observation.ReleaseVersion != "0.8.0" || observation.ContractID != "proofkit.cli-contract.v2" || observation.ContractSchemaVersion != 2 || observation.PackageName != "@research-engineering/agentic-proofkit" || observation.OrderingPolicy != "lexicographic_by_identity" || observation.CommandFingerprintPolicy != projectNavigationCommandFingerprintPolicy || observation.PublicABISHA256 != "sha256:b5ea707ee5851cea6b75442e4faf20e93879371faf3636e96a98ccd23b527463" || len(observation.Commands) == 0 || len(observation.ContractDefinitions) == 0 || !slices.Equal(observation.NonClaims, []string{"Per-command fingerprints omit only native source canonical digests; the exact raw contract remains bound by publicAbiSha256.", "This frozen source observation does not authenticate registry publication, provider state, consumer migration, or runtime compatibility."}) { - t.Fatalf("frozen public ABI observation is invalid: %#v", observation) + if got := fmt.Sprintf("%x", sha256.Sum256(raw)); got != archivedProjectNavigationContractSHA256 { + t.Fatalf("archived CLI contract changed: %s", got) } - for context, values := range map[string]map[string]string{"command": observation.Commands, "definition": observation.ContractDefinitions} { - for id, value := range values { - if id == "" { - t.Fatalf("frozen %s identity is empty", context) - } - if _, err := admit.SHA256Ref(value, "frozen "+context+" digest"); err != nil { - t.Fatal(err) - } - } - } - if _, err := admit.SHA256Ref(observation.ProcessContractSHA256, "frozen process contract digest"); err != nil { - t.Fatal(err) - } - return observation -} - -func readProjectNavigationCLIContractRaw(t *testing.T) map[string]any { - t.Helper() - content, err := os.ReadFile(filepath.Join(repoRoot(t), "proofkit", "cli-contract.v2.json")) + value, err := admission.DecodeJSON(bytes.NewReader(raw), int64(len(raw))) 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("current CLI contract must be an object") - } - return root + return value.(map[string]any) } - -func verifyCompleteProjectNavigationABIDiff(frozen frozenProjectNavigationPublicABI, current map[string]any) error { +func verifyCompleteProjectNavigationABIDiff(frozen frozenPublicABI, current map[string]any) error { schemaVersion, err := admit.CanonicalInteger(current["schemaVersion"], "current CLI contract schemaVersion") if err != nil || int(schemaVersion) != frozen.ContractSchemaVersion || current["contractId"] != frozen.ContractID || current["packageName"] != frozen.PackageName { return fmt.Errorf("current CLI contract header differs from the frozen predecessor") @@ -181,7 +125,7 @@ func verifyCompleteProjectNavigationABIDiff(frozen frozenProjectNavigationPublic return fmt.Errorf("current CLI command route grammar is invalid") } normalizedGrammar := clonePublicABIRecord(grammar) - if normalizedGrammar["omittedRoutePolicy"] != commandroute.OmittedRoutePolicy { + if normalizedGrammar["omittedRoutePolicy"] != "command_id" { return fmt.Errorf("current CLI omitted route policy is invalid") } delete(normalizedGrammar, "omittedRoutePolicy") @@ -195,145 +139,3 @@ func verifyCompleteProjectNavigationABIDiff(frozen frozenProjectNavigationPublic } return nil } - -func indexPublicABIRecords(raw any, identityField string) (map[string]map[string]any, []string, error) { - values, ok := raw.([]any) - if !ok { - return nil, nil, fmt.Errorf("CLI contract %s inventory must be an array", identityField) - } - indexed := make(map[string]map[string]any, len(values)) - order := make([]string, 0, len(values)) - for _, value := range values { - record, ok := value.(map[string]any) - if !ok { - return nil, nil, fmt.Errorf("CLI contract %s record must be an object", identityField) - } - identity, ok := record[identityField].(string) - if !ok || identity == "" { - return nil, nil, fmt.Errorf("CLI contract %s record has no identity", identityField) - } - if _, exists := indexed[identity]; exists { - return nil, nil, fmt.Errorf("CLI contract repeats %s %s", identityField, identity) - } - indexed[identity] = record - order = append(order, identity) - } - return indexed, order, nil -} - -func addedCommandDefinitionClosure(commands, definitions map[string]map[string]any, frozenDefinitions map[string]string, commandNames []string) ([]string, error) { - queue := []string{} - for _, name := range commandNames { - command, ok := commands[name] - if !ok { - return nil, fmt.Errorf("current CLI contract is missing added command %s", name) - } - for _, field := range []string{"inputContract", "outputContract"} { - contract, ok := command[field].(map[string]any) - if !ok { - continue - } - if root, ok := contract["rootDefinitionRef"].(string); ok && root != "" { - queue = append(queue, root) - } - } - } - visited := map[string]bool{} - result := []string{} - for len(queue) > 0 { - id := queue[0] - queue = queue[1:] - if visited[id] { - continue - } - visited[id] = true - definition, ok := definitions[id] - if !ok { - return nil, fmt.Errorf("added command references missing definition %s", id) - } - if _, existed := frozenDefinitions[id]; !existed { - result = append(result, id) - } - references, ok := definition["definitionRefs"].([]any) - if !ok { - return nil, fmt.Errorf("definition %s has invalid definitionRefs", id) - } - for _, raw := range references { - reference, ok := raw.(string) - if !ok || reference == "" { - return nil, fmt.Errorf("definition %s has invalid referenced identity", id) - } - queue = append(queue, reference) - } - } - slices.Sort(result) - return result, nil -} - -func differenceKeys[V any, W any](current map[string]V, previous map[string]W) []string { - result := []string{} - for key := range current { - if _, exists := previous[key]; !exists { - result = append(result, key) - } - } - slices.Sort(result) - return result -} - -func clonePublicABIRecord(value map[string]any) map[string]any { - clone := make(map[string]any, len(value)) - for key, item := range value { - clone[key] = item - } - return clone -} - -func normalizePublicABICommandFingerprint(value map[string]any) (map[string]any, error) { - normalized := clonePublicABIRecord(value) - for _, contractField := range []string{"inputContract", "outputContract"} { - rawContract, exists := normalized[contractField] - if !exists || rawContract == nil { - continue - } - contract, ok := rawContract.(map[string]any) - if !ok { - return nil, fmt.Errorf("%s must be an object", contractField) - } - contract = clonePublicABIRecord(contract) - if rawSource, exists := contract["nativeSource"]; exists { - source, ok := rawSource.(map[string]any) - if !ok { - return nil, fmt.Errorf("%s nativeSource must be an object", contractField) - } - source = clonePublicABIRecord(source) - if _, exists := source["canonicalDigest"]; !exists { - return nil, fmt.Errorf("%s nativeSource has no canonicalDigest", contractField) - } - delete(source, "canonicalDigest") - contract["nativeSource"] = source - } - if rawSources, exists := contract["nativeSources"]; exists { - sources, ok := rawSources.([]any) - if !ok { - return nil, fmt.Errorf("%s nativeSources must be an array", contractField) - } - normalizedSources := make([]any, len(sources)) - for index, rawSource := range sources { - source, ok := rawSource.(map[string]any) - if !ok { - return nil, fmt.Errorf("%s nativeSources[%d] must be an object", contractField, index) - } - source = clonePublicABIRecord(source) - if _, exists := source["canonicalDigest"]; !exists { - return nil, fmt.Errorf("%s nativeSources[%d] has no canonicalDigest", contractField, index) - } - delete(source, "canonicalDigest") - normalizedSources[index] = source - } - contract["nativeSources"] = normalizedSources - } - normalized[contractField] = contract - } - return normalized, nil -} diff --git a/internal/app/project_navigation_abi_mutation_test.go b/internal/app/project_navigation_abi_mutation_test.go index 84a95f4..f18b9d7 100644 --- a/internal/app/project_navigation_abi_mutation_test.go +++ b/internal/app/project_navigation_abi_mutation_test.go @@ -1,151 +1,7 @@ package app -import ( - "encoding/json" - "slices" - "testing" - - "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" -) +import "testing" func TestProjectNavigationVersionEdgeRejectsUndeclaredPublicABIDrift(t *testing.T) { - frozen := readFrozenProjectNavigationPublicABI(t) - firstDefinition := sortedFirstKey(t, frozen.ContractDefinitions) - tests := []struct { - name string - mutate func(map[string]any) - }{ - {name: "header", mutate: func(current map[string]any) { current["packageName"] = "unexpected" }}, - {name: "predecessor command removed", mutate: func(current map[string]any) { - removePublicABIRecord(t, current, "commands", "command", "impact") - }}, - {name: "predecessor command changed", mutate: func(current map[string]any) { - mutatePublicABIRecord(t, current, "commands", "command", "impact", func(record map[string]any) { record["route"] = []any{"impact-drift"} }) - }}, - {name: "command order", mutate: func(current map[string]any) { swapFirstPublicABIRecords(t, current, "commands") }}, - {name: "unexpected command", mutate: func(current map[string]any) { - appendRenamedPublicABIRecord(t, current, "commands", "command", "impact", "zz-unexpected-command") - }}, - {name: "predecessor definition removed", mutate: func(current map[string]any) { - removePublicABIRecord(t, current, "contractDefinitions", "definitionId", firstDefinition) - }}, - {name: "predecessor definition changed", mutate: func(current map[string]any) { - mutatePublicABIRecord(t, current, "contractDefinitions", "definitionId", firstDefinition, func(record map[string]any) { record["unexpectedField"] = true }) - }}, - {name: "definition order", mutate: func(current map[string]any) { swapFirstPublicABIRecords(t, current, "contractDefinitions") }}, - {name: "unexpected definition", mutate: func(current map[string]any) { - appendRenamedPublicABIRecord(t, current, "contractDefinitions", "definitionId", firstDefinition, "proofkit.zz-unexpected.definition") - }}, - {name: "process contract", mutate: func(current map[string]any) { - process := clonePublicABIRecord(current["processContract"].(map[string]any)) - process["successExitCode"] = json.Number("9") - current["processContract"] = process - }}, - {name: "omitted route policy missing", mutate: func(current map[string]any) { mutateOmittedRoutePolicy(t, current, nil) }}, - {name: "omitted route policy changed", mutate: func(current map[string]any) { mutateOmittedRoutePolicy(t, current, "unexpected") }}, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - current := readProjectNavigationCLIContractRaw(t) - test.mutate(current) - if err := verifyCompleteProjectNavigationABIDiff(frozen, current); err == nil { - t.Fatal("undeclared public ABI drift was admitted") - } - }) - } - - t.Run("native source digest drift is intentionally normalized", func(t *testing.T) { - current := readProjectNavigationCLIContractRaw(t) - mutatePublicABIRecord(t, current, "commands", "command", "impact", func(record map[string]any) { - for _, field := range []string{"inputContract", "outputContract"} { - contract := clonePublicABIRecord(record[field].(map[string]any)) - source := clonePublicABIRecord(contract["nativeSource"].(map[string]any)) - source["canonicalDigest"] = digest.SHA256TextRef("updated native source bytes") - contract["nativeSource"] = source - record[field] = contract - } - }) - if err := verifyCompleteProjectNavigationABIDiff(frozen, current); err != nil { - t.Fatalf("native source digest drift should be normalized: %v", err) - } - }) -} - -func sortedFirstKey(t *testing.T, values map[string]string) string { - t.Helper() - keys := make([]string, 0, len(values)) - for key := range values { - keys = append(keys, key) - } - slices.Sort(keys) - if len(keys) == 0 { - t.Fatal("frozen public ABI inventory is empty") - } - return keys[0] -} - -func mutatePublicABIRecord(t *testing.T, current map[string]any, inventory string, identityField string, identity string, mutate func(map[string]any)) { - t.Helper() - values := current[inventory].([]any) - for index, raw := range values { - record := raw.(map[string]any) - if record[identityField] != identity { - continue - } - mutant := clonePublicABIRecord(record) - mutate(mutant) - values[index] = mutant - return - } - t.Fatalf("%s is missing %s %s", inventory, identityField, identity) -} - -func removePublicABIRecord(t *testing.T, current map[string]any, inventory string, identityField string, identity string) { - t.Helper() - values := current[inventory].([]any) - for index, raw := range values { - if raw.(map[string]any)[identityField] == identity { - current[inventory] = append(append([]any{}, values[:index]...), values[index+1:]...) - return - } - } - t.Fatalf("%s is missing %s %s", inventory, identityField, identity) -} - -func swapFirstPublicABIRecords(t *testing.T, current map[string]any, inventory string) { - t.Helper() - values := current[inventory].([]any) - if len(values) < 2 { - t.Fatalf("%s has fewer than two records", inventory) - } - values[0], values[1] = values[1], values[0] -} - -func appendRenamedPublicABIRecord(t *testing.T, current map[string]any, inventory string, identityField string, sourceIdentity string, newIdentity string) { - t.Helper() - values := current[inventory].([]any) - for _, raw := range values { - record := raw.(map[string]any) - if record[identityField] != sourceIdentity { - continue - } - mutant := clonePublicABIRecord(record) - mutant[identityField] = newIdentity - current[inventory] = append(values, mutant) - return - } - t.Fatalf("%s is missing %s %s", inventory, identityField, sourceIdentity) -} - -func mutateOmittedRoutePolicy(t *testing.T, current map[string]any, replacement any) { - t.Helper() - process := clonePublicABIRecord(current["processContract"].(map[string]any)) - grammar := clonePublicABIRecord(process["commandRouteGrammar"].(map[string]any)) - if replacement == nil { - delete(grammar, "omittedRoutePolicy") - } else { - grammar["omittedRoutePolicy"] = replacement - } - process["commandRouteGrammar"] = grammar - current["processContract"] = process + assertRejectsUndeclaredPublicABIDrift(t, readFrozenProjectNavigationPublicABI(t), readArchivedProjectNavigationContract, verifyCompleteProjectNavigationABIDiff) } diff --git a/internal/app/project_navigation_version_edge_test.go b/internal/app/project_navigation_version_edge_test.go index fd30774..f19310a 100644 --- a/internal/app/project_navigation_version_edge_test.go +++ b/internal/app/project_navigation_version_edge_test.go @@ -17,6 +17,7 @@ import ( ) const projectNavigationVersionEdgePath = "internal/app/testdata/v0.9-wire-observations.json" +const archivedProjectNavigationReleaseRoot = "internal/app/testdata/releases/v0.9.0" const frozenProjectNavigationPredecessorPath = "internal/app/testdata/v0.8-wire-observations.json" const frozenProjectNavigationPredecessorSHA256 = "ed0651c53c015c00d8ed7a0db681a213e9df6248302c5f12fc898e4b6a82c5ab" @@ -58,8 +59,8 @@ type versionEdgeRouteReplacement struct { func TestProjectNavigationVersionEdgeClosesPublicRoutes(t *testing.T) { record := readProjectNavigationVersionEdge(t) - root := repoRoot(t) - if err := validateProjectNavigationVersionEdge(record, root, root, currentCLIContractPublicABISHA256(t)); err != nil { + root := filepath.Join(repoRoot(t), archivedProjectNavigationReleaseRoot) + if err := validateProjectNavigationVersionEdge(record, root); err != nil { t.Fatal(err) } assertProjectNavigationRouteCutover(t) @@ -114,7 +115,7 @@ func TestProjectNavigationVersionEdgeClosesPublicRoutes(t *testing.T) { t.Run(fmt.Sprintf("mutant-%d", index), func(t *testing.T) { value := cloneProjectNavigationVersionEdge(record) mutate(&value) - if err := validateProjectNavigationVersionEdge(value, root, root, currentCLIContractPublicABISHA256(t)); err == nil { + if err := validateProjectNavigationVersionEdge(value, root); err == nil { t.Fatal("project navigation version-edge mutant was admitted") } }) @@ -123,7 +124,7 @@ func TestProjectNavigationVersionEdgeClosesPublicRoutes(t *testing.T) { func TestProjectNavigationVersionEdgeRejectsCoordinatedChangeRecordDrift(t *testing.T) { record := readProjectNavigationVersionEdge(t) - content, err := os.ReadFile(filepath.Join(repoRoot(t), record.ChangeRecordRef)) + content, err := os.ReadFile(filepath.Join(repoRoot(t), archivedProjectNavigationReleaseRoot, record.ChangeRecordRef)) if err != nil { t.Fatal(err) } @@ -149,7 +150,7 @@ func TestProjectNavigationVersionEdgeRejectsCoordinatedChangeRecordDrift(t *test mutant := cloneProjectNavigationVersionEdge(record) digest := sha256.Sum256(mutantContent) mutant.ChangeRecordSHA256 = fmt.Sprintf("sha256:%x", digest) - if err := validateProjectNavigationVersionEdge(mutant, repoRoot(t), mutantRoot, currentCLIContractPublicABISHA256(t)); err == nil || !strings.Contains(err.Error(), "contradicts") { + if err := validateProjectNavigationVersionEdge(mutant, mutantRoot); err == nil || !strings.Contains(err.Error(), "contradicts") { t.Fatalf("coordinated change-record mutant error=%v, want inventory contradiction", err) } } @@ -207,7 +208,7 @@ func readProjectNavigationVersionEdge(t *testing.T) projectNavigationVersionEdge return record } -func validateProjectNavigationVersionEdge(record projectNavigationVersionEdge, contractRoot, changeRecordRoot, currentABI string) error { +func validateProjectNavigationVersionEdge(record projectNavigationVersionEdge, changeRecordRoot string) error { if record.SchemaVersion != 1 || record.EdgeID != "proofkit.public-wire.0.8.0-to-0.9.0" || record.EvidenceClass != "owner_authored_current_version_edge_observation" { return fmt.Errorf("project navigation version-edge identity is invalid") } @@ -217,28 +218,20 @@ func validateProjectNavigationVersionEdge(record projectNavigationVersionEdge, c if record.CommandContractSelection != "added_commands_changed_routes_and_process_contract" { return fmt.Errorf("project navigation command-contract selection policy is invalid") } - if record.PreviousPublicABISHA256 != "sha256:b5ea707ee5851cea6b75442e4faf20e93879371faf3636e96a98ccd23b527463" || record.CurrentPublicABISHA256 != "sha256:"+currentABI || record.PreviousPublicABISHA256 == record.CurrentPublicABISHA256 { + if record.PreviousPublicABISHA256 != "sha256:b5ea707ee5851cea6b75442e4faf20e93879371faf3636e96a98ccd23b527463" || record.CurrentPublicABISHA256 != "sha256:9a6842b45a218d6caa5da517b0b20f861e13c35a2900e92d34361cdf771781f7" || record.PreviousPublicABISHA256 == record.CurrentPublicABISHA256 { return fmt.Errorf("project navigation version-edge ABI identity is invalid") } - currentAdded, err := currentVersionEdgeCommandContracts(contractRoot, []string{"next", "status"}) - if err != nil { - return err - } - if !slices.EqualFunc(record.AddedCommandContracts, currentAdded, equalVersionEdgeCommandContract) { + if !slices.EqualFunc(record.AddedCommandContracts, frozenProjectNavigationCommands(), equalVersionEdgeCommandContract) { return fmt.Errorf("project navigation added command contracts are not exact") } - currentRoute, err := currentVersionEdgeRouteReplacement(contractRoot) - if err != nil { - return err - } - if !slices.EqualFunc(record.ChangedCommandRoutes, []versionEdgeRouteReplacement{currentRoute}, equalVersionEdgeRouteReplacement) { + if !slices.EqualFunc(record.ChangedCommandRoutes, []versionEdgeRouteReplacement{frozenProjectNavigationRoute()}, equalVersionEdgeRouteReplacement) { return fmt.Errorf("project navigation route replacement is not exact") } processChanges := []versionEdgeProcessChange{{ - ChangeID: "proofkit.cli-contract.omitted-route-policy", CurrentValue: commandroute.OmittedRoutePolicy, + ChangeID: "proofkit.cli-contract.omitted-route-policy", CurrentValue: "command_id", JSONPointer: "/processContract/commandRouteGrammar/omittedRoutePolicy", PreviousState: "absent", }} - if !slices.Equal(record.ProcessContractChanges, processChanges) || currentOmittedRoutePolicy(contractRoot) != commandroute.OmittedRoutePolicy { + if !slices.Equal(record.ProcessContractChanges, processChanges) { return fmt.Errorf("project navigation process-contract change is not exact") } if !slices.Equal(record.BreakingChangeIDs, []string{"proofkit.agent-workflow.change-plan-route", "proofkit.cli-contract.omitted-route-policy"}) || !slices.Equal(record.AdditionChangeIDs, []string{"proofkit.project-state.next-action", "proofkit.project-state.status"}) { @@ -386,6 +379,10 @@ func currentOmittedRoutePolicy(root string) string { func assertProjectNavigationRouteCutover(t *testing.T) { t.Helper() + currentRoute, err := currentVersionEdgeRouteReplacement(repoRoot(t)) + if err != nil || currentRoute.Command != "change-workflow-plan" || !slices.Equal(currentRoute.CurrentRoute, []string{"change", "plan"}) || currentOmittedRoutePolicy(repoRoot(t)) != commandroute.OmittedRoutePolicy { + t.Fatal("current route contract does not preserve project navigation cutover") + } status, stdout, stderr := executeAgentWorkflowCLI(t, []string{"change-workflow-plan", "--input", "-"}, panicReader{}, PresentationCapabilities{}) if status != 1 || stdout != "" || !strings.Contains(stderr, "unsupported command: change-workflow-plan") { t.Fatalf("retired route status=%d stdout=%q stderr=%q", status, stdout, stderr) @@ -395,3 +392,18 @@ func assertProjectNavigationRouteCutover(t *testing.T) { t.Fatalf("current route status=%d stdout=%q stderr=%q", status, stdout, stderr) } } + +func frozenProjectNavigationCommands() []versionEdgeCommandContract { + return []versionEdgeCommandContract{ + {Command: "next", Route: []string{"next"}, OutputContract: versionEdgeContractIdentity{ContractID: "proofkit.next.output.v1", ContractSHA256: "sha256:92f6d3dc427a795ec97112e6c3ce55ebd4ab670e4323b4faa8215310a8492747"}}, + {Command: "status", Route: []string{"status"}, OutputContract: versionEdgeContractIdentity{ContractID: "proofkit.status.output.v1", ContractSHA256: "sha256:3ac87fa98c4650fc3b758ab4a43dee1beeb12f9aee3631a35a2ddf5d0fc5f899"}}, + } +} + +func frozenProjectNavigationRoute() versionEdgeRouteReplacement { + return versionEdgeRouteReplacement{ + Command: "change-workflow-plan", PreviousRoute: []string{"change-workflow-plan"}, CurrentRoute: []string{"change", "plan"}, + PreservedInputContract: versionEdgeContractIdentity{ContractID: "proofkit.change-workflow-plan.input.v1", ContractSHA256: "sha256:e3124fc636b7f66b24daf8e1435cea11da15a741abeabe0cc3d3890b13c71625"}, + PreservedOutputContract: versionEdgeContractIdentity{ContractID: "proofkit.change-workflow-plan.output.v1", ContractSHA256: "sha256:cd035e9b71d83c341b1a937a18699fd727cb4b0d694983d715b064292ae4d8bd"}, + } +} diff --git a/internal/app/public_abi_mutation_test.go b/internal/app/public_abi_mutation_test.go new file mode 100644 index 0000000..8546e3a --- /dev/null +++ b/internal/app/public_abi_mutation_test.go @@ -0,0 +1,167 @@ +package app + +import ( + "encoding/json" + "slices" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" +) + +func TestIntegrationVersionEdgeRejectsUndeclaredPublicABIDrift(t *testing.T) { + assertRejectsUndeclaredPublicABIDrift(t, readFrozenIntegrationPublicABI(t), readCLIContractRaw, func(frozen frozenPublicABI, current map[string]any) error { + return verifyAdditivePublicABIDiff(frozen, current, []string{"integration-check", "integration-source"}, integrationProcessAppendices()) + }) +} + +func assertRejectsUndeclaredPublicABIDrift(t *testing.T, frozen frozenPublicABI, read func(*testing.T) map[string]any, verify func(frozenPublicABI, map[string]any) error) { + t.Helper() + firstDefinition := sortedFirstKey(t, frozen.ContractDefinitions) + tests := []struct { + name string + mutate func(map[string]any) + }{ + {name: "header", mutate: func(current map[string]any) { current["packageName"] = "unexpected" }}, + {name: "predecessor command removed", mutate: func(current map[string]any) { + removePublicABIRecord(t, current, "commands", "command", "impact") + }}, + {name: "predecessor command changed", mutate: func(current map[string]any) { + mutatePublicABIRecord(t, current, "commands", "command", "impact", func(record map[string]any) { record["route"] = []any{"impact-drift"} }) + }}, + {name: "command order", mutate: func(current map[string]any) { swapFirstPublicABIRecords(t, current, "commands") }}, + {name: "unexpected command", mutate: func(current map[string]any) { + appendRenamedPublicABIRecord(t, current, "commands", "command", "impact", "zz-unexpected-command") + }}, + {name: "predecessor definition removed", mutate: func(current map[string]any) { + removePublicABIRecord(t, current, "contractDefinitions", "definitionId", firstDefinition) + }}, + {name: "predecessor definition changed", mutate: func(current map[string]any) { + mutatePublicABIRecord(t, current, "contractDefinitions", "definitionId", firstDefinition, func(record map[string]any) { record["unexpectedField"] = true }) + }}, + {name: "definition order", mutate: func(current map[string]any) { swapFirstPublicABIRecords(t, current, "contractDefinitions") }}, + {name: "unexpected definition", mutate: func(current map[string]any) { + appendRenamedPublicABIRecord(t, current, "contractDefinitions", "definitionId", firstDefinition, "proofkit.zz-unexpected.definition") + }}, + {name: "process contract", mutate: func(current map[string]any) { + process := clonePublicABIRecord(current["processContract"].(map[string]any)) + process["successExitCode"] = json.Number("9") + current["processContract"] = process + }}, + {name: "classified exit code", mutate: func(current map[string]any) { + process := clonePublicABIRecord(current["processContract"].(map[string]any)) + process["stdout"] = "unexpected" + current["processContract"] = process + }}, + {name: "classified stderr", mutate: func(current map[string]any) { + process := clonePublicABIRecord(current["processContract"].(map[string]any)) + process["stderr"] = "unexpected" + current["processContract"] = process + }}, + {name: "omitted route policy missing", mutate: func(current map[string]any) { mutateOmittedRoutePolicy(t, current, nil) }}, + {name: "omitted route policy changed", mutate: func(current map[string]any) { mutateOmittedRoutePolicy(t, current, "unexpected") }}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + current := read(t) + test.mutate(current) + if err := verify(frozen, current); err == nil { + t.Fatal("undeclared public ABI drift was admitted") + } + }) + } + + t.Run("native source digest drift is intentionally normalized", func(t *testing.T) { + current := read(t) + mutatePublicABIRecord(t, current, "commands", "command", "impact", func(record map[string]any) { + for _, field := range []string{"inputContract", "outputContract"} { + contract := clonePublicABIRecord(record[field].(map[string]any)) + source := clonePublicABIRecord(contract["nativeSource"].(map[string]any)) + source["canonicalDigest"] = digest.SHA256TextRef("updated native source bytes") + contract["nativeSource"] = source + record[field] = contract + } + }) + if err := verify(frozen, current); err != nil { + t.Fatalf("native source digest drift should be normalized: %v", err) + } + }) +} + +func sortedFirstKey(t *testing.T, values map[string]string) string { + t.Helper() + keys := make([]string, 0, len(values)) + for key := range values { + keys = append(keys, key) + } + slices.Sort(keys) + if len(keys) == 0 { + t.Fatal("frozen public ABI inventory is empty") + } + return keys[0] +} + +func mutatePublicABIRecord(t *testing.T, current map[string]any, inventory string, identityField string, identity string, mutate func(map[string]any)) { + t.Helper() + values := current[inventory].([]any) + for index, raw := range values { + record := raw.(map[string]any) + if record[identityField] != identity { + continue + } + mutant := clonePublicABIRecord(record) + mutate(mutant) + values[index] = mutant + return + } + t.Fatalf("%s is missing %s %s", inventory, identityField, identity) +} + +func removePublicABIRecord(t *testing.T, current map[string]any, inventory string, identityField string, identity string) { + t.Helper() + values := current[inventory].([]any) + for index, raw := range values { + if raw.(map[string]any)[identityField] == identity { + current[inventory] = append(append([]any{}, values[:index]...), values[index+1:]...) + return + } + } + t.Fatalf("%s is missing %s %s", inventory, identityField, identity) +} + +func swapFirstPublicABIRecords(t *testing.T, current map[string]any, inventory string) { + t.Helper() + values := current[inventory].([]any) + if len(values) < 2 { + t.Fatalf("%s has fewer than two records", inventory) + } + values[0], values[1] = values[1], values[0] +} + +func appendRenamedPublicABIRecord(t *testing.T, current map[string]any, inventory string, identityField string, sourceIdentity string, newIdentity string) { + t.Helper() + values := current[inventory].([]any) + for _, raw := range values { + record := raw.(map[string]any) + if record[identityField] != sourceIdentity { + continue + } + mutant := clonePublicABIRecord(record) + mutant[identityField] = newIdentity + current[inventory] = append(values, mutant) + return + } + t.Fatalf("%s is missing %s %s", inventory, identityField, sourceIdentity) +} + +func mutateOmittedRoutePolicy(t *testing.T, current map[string]any, replacement any) { + t.Helper() + process := clonePublicABIRecord(current["processContract"].(map[string]any)) + grammar := clonePublicABIRecord(process["commandRouteGrammar"].(map[string]any)) + if replacement == nil { + delete(grammar, "omittedRoutePolicy") + } else { + grammar["omittedRoutePolicy"] = replacement + } + process["commandRouteGrammar"] = grammar + current["processContract"] = process +} diff --git a/internal/app/public_abi_observation_test.go b/internal/app/public_abi_observation_test.go new file mode 100644 index 0000000..35abe90 --- /dev/null +++ b/internal/app/public_abi_observation_test.go @@ -0,0 +1,330 @@ +package app + +import ( + "bytes" + "crypto/sha256" + "encoding/json" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" +) + +const frozenProjectNavigationPublicABIPath = "internal/app/testdata/releases/v0.8.0/public-abi-observation.json" +const frozenProjectNavigationPublicABISHA256 = "ffff23ca84e014176d854104ea07eb92b10b4d6db7822f14f7859f6f6d360997" + +const projectNavigationCommandFingerprintPolicy = "semantic_command_contract_without_native_source_digests" + +type frozenPublicABI struct { + CommandFingerprintPolicy string `json:"commandFingerprintPolicy"` + Commands map[string]string `json:"commands"` + ContractDefinitions map[string]string `json:"contractDefinitions"` + ContractID string `json:"contractId"` + ContractSchemaVersion int `json:"contractSchemaVersion"` + NonClaims []string `json:"nonClaims"` + ObservationKind string `json:"observationKind"` + OrderingPolicy string `json:"orderingPolicy"` + PackageName string `json:"packageName"` + ProcessContractSHA256 string `json:"processContractSha256"` + PublicABISHA256 string `json:"publicAbiSha256"` + ReleaseVersion string `json:"releaseVersion"` + SchemaVersion int `json:"schemaVersion"` +} + +func readFrozenProjectNavigationPublicABI(t *testing.T) frozenPublicABI { + t.Helper() + return readFrozenPublicABI(t, frozenProjectNavigationPublicABIPath, frozenProjectNavigationPublicABISHA256, + "0.8.0", "sha256:b5ea707ee5851cea6b75442e4faf20e93879371faf3636e96a98ccd23b527463") +} + +func readFrozenPublicABI(t *testing.T, path, contentDigest, version, publicDigest string) frozenPublicABI { + t.Helper() + content, err := os.ReadFile(filepath.Join(repoRoot(t), path)) + if err != nil { + t.Fatal(err) + } + sum := sha256.Sum256(content) + if got := fmt.Sprintf("%x", sum); got != contentDigest { + t.Fatalf("frozen public ABI observation digest=%s, want %s", got, contentDigest) + } + 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("frozen public ABI observation must be an object") + } + assertExactObjectKeys(t, root, []string{"commandFingerprintPolicy", "commands", "contractDefinitions", "contractId", "contractSchemaVersion", "nonClaims", "observationKind", "orderingPolicy", "packageName", "processContractSha256", "publicAbiSha256", "releaseVersion", "schemaVersion"}, "frozen public ABI observation") + var observation frozenPublicABI + if err := json.Unmarshal(content, &observation); err != nil { + t.Fatal(err) + } + if observation.SchemaVersion != 1 || observation.ObservationKind != "proofkit.frozen-public-abi-observation" || observation.ReleaseVersion != version || observation.ContractID != "proofkit.cli-contract.v2" || observation.ContractSchemaVersion != 2 || observation.PackageName != "@research-engineering/agentic-proofkit" || observation.OrderingPolicy != "lexicographic_by_identity" || observation.CommandFingerprintPolicy != projectNavigationCommandFingerprintPolicy || observation.PublicABISHA256 != publicDigest || len(observation.Commands) == 0 || len(observation.ContractDefinitions) == 0 || !slices.Equal(observation.NonClaims, []string{"Per-command fingerprints omit only native source canonical digests; the exact raw contract remains bound by publicAbiSha256.", "This frozen source observation does not authenticate registry publication, provider state, consumer migration, or runtime compatibility."}) { + t.Fatalf("frozen public ABI observation is invalid: %#v", observation) + } + for context, values := range map[string]map[string]string{"command": observation.Commands, "definition": observation.ContractDefinitions} { + for id, value := range values { + if id == "" { + t.Fatalf("frozen %s identity is empty", context) + } + if _, err := admit.SHA256Ref(value, "frozen "+context+" digest"); err != nil { + t.Fatal(err) + } + } + } + if _, err := admit.SHA256Ref(observation.ProcessContractSHA256, "frozen process contract digest"); err != nil { + t.Fatal(err) + } + return observation +} + +func readCLIContractRaw(t *testing.T) map[string]any { + t.Helper() + content, err := os.ReadFile(filepath.Join(repoRoot(t), "proofkit", "cli-contract.v2.json")) + 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("current CLI contract must be an object") + } + return root +} + +func verifyAdditivePublicABIDiff(frozen frozenPublicABI, current map[string]any, expectedAddedCommands []string, processAppendices map[string]string) error { + schemaVersion, err := admit.CanonicalInteger(current["schemaVersion"], "current CLI contract schemaVersion") + if err != nil || int(schemaVersion) != frozen.ContractSchemaVersion || current["contractId"] != frozen.ContractID || current["packageName"] != frozen.PackageName { + return fmt.Errorf("current CLI contract header differs from the frozen predecessor") + } + commands, commandOrder, err := indexPublicABIRecords(current["commands"], "command") + if err != nil { + return err + } + if !slices.IsSorted(commandOrder) { + return fmt.Errorf("current CLI command order is not canonical") + } + addedCommands := differenceKeys(commands, frozen.Commands) + if !slices.Equal(addedCommands, expectedAddedCommands) { + return fmt.Errorf("current CLI contract has undeclared command additions: %v", addedCommands) + } + for name, wantDigest := range frozen.Commands { + record, ok := commands[name] + if !ok { + return fmt.Errorf("current CLI contract removed predecessor command %s", name) + } + normalized, err := normalizePublicABICommandFingerprint(record) + if err != nil { + return fmt.Errorf("normalize current command %s: %w", name, err) + } + gotDigest, err := digest.StableJSONSHA256Ref(normalized) + if err != nil { + return fmt.Errorf("fingerprint current command %s: %w", name, err) + } + if gotDigest != wantDigest { + return fmt.Errorf("current CLI command %s has undeclared ABI drift", name) + } + } + + definitions, definitionOrder, err := indexPublicABIRecords(current["contractDefinitions"], "definitionId") + if err != nil { + return err + } + if !slices.IsSorted(definitionOrder) { + return fmt.Errorf("current CLI definition order is not canonical") + } + for id, wantDigest := range frozen.ContractDefinitions { + record, ok := definitions[id] + if !ok { + return fmt.Errorf("current CLI contract removed predecessor definition %s", id) + } + gotDigest, err := digest.StableJSONSHA256Ref(record) + if err != nil { + return fmt.Errorf("fingerprint current definition %s: %w", id, err) + } + if gotDigest != wantDigest { + return fmt.Errorf("current CLI definition %s has undeclared ABI drift", id) + } + } + addedDefinitions := differenceKeys(definitions, frozen.ContractDefinitions) + expectedDefinitions, err := addedCommandDefinitionClosure(commands, definitions, frozen.ContractDefinitions, expectedAddedCommands) + if err != nil { + return err + } + if !slices.Equal(addedDefinitions, expectedDefinitions) { + return fmt.Errorf("current CLI definition additions are not exactly closed by added commands: got %v want %v", addedDefinitions, expectedDefinitions) + } + + process, ok := current["processContract"].(map[string]any) + if !ok { + return fmt.Errorf("current CLI process contract is invalid") + } + normalizedProcess := clonePublicABIRecord(process) + for field, appendix := range processAppendices { + text, ok := process[field].(string) + if !ok || appendix == "" || !strings.HasSuffix(text, appendix) { + return fmt.Errorf("current CLI process contract is missing its declared %s appendix", field) + } + normalizedProcess[field] = strings.TrimSuffix(text, appendix) + } + processDigest, err := digest.StableJSONSHA256Ref(normalizedProcess) + if err != nil { + return fmt.Errorf("fingerprint normalized process contract: %w", err) + } + if processDigest != frozen.ProcessContractSHA256 { + return fmt.Errorf("current CLI process contract has undeclared ABI drift") + } + return nil +} + +func indexPublicABIRecords(raw any, identityField string) (map[string]map[string]any, []string, error) { + values, ok := raw.([]any) + if !ok { + return nil, nil, fmt.Errorf("CLI contract %s inventory must be an array", identityField) + } + indexed := make(map[string]map[string]any, len(values)) + order := make([]string, 0, len(values)) + for _, value := range values { + record, ok := value.(map[string]any) + if !ok { + return nil, nil, fmt.Errorf("CLI contract %s record must be an object", identityField) + } + identity, ok := record[identityField].(string) + if !ok || identity == "" { + return nil, nil, fmt.Errorf("CLI contract %s record has no identity", identityField) + } + if _, exists := indexed[identity]; exists { + return nil, nil, fmt.Errorf("CLI contract repeats %s %s", identityField, identity) + } + indexed[identity] = record + order = append(order, identity) + } + return indexed, order, nil +} + +func addedCommandDefinitionClosure(commands, definitions map[string]map[string]any, frozenDefinitions map[string]string, commandNames []string) ([]string, error) { + queue := []string{} + for _, name := range commandNames { + command, ok := commands[name] + if !ok { + return nil, fmt.Errorf("current CLI contract is missing added command %s", name) + } + for _, field := range []string{"inputContract", "outputContract"} { + contract, ok := command[field].(map[string]any) + if !ok { + continue + } + if root, ok := contract["rootDefinitionRef"].(string); ok && root != "" { + queue = append(queue, root) + } + } + } + visited := map[string]bool{} + result := []string{} + for len(queue) > 0 { + id := queue[0] + queue = queue[1:] + if visited[id] { + continue + } + visited[id] = true + definition, ok := definitions[id] + if !ok { + return nil, fmt.Errorf("added command references missing definition %s", id) + } + if _, existed := frozenDefinitions[id]; !existed { + result = append(result, id) + } + references, ok := definition["definitionRefs"].([]any) + if !ok { + return nil, fmt.Errorf("definition %s has invalid definitionRefs", id) + } + for _, raw := range references { + reference, ok := raw.(string) + if !ok || reference == "" { + return nil, fmt.Errorf("definition %s has invalid referenced identity", id) + } + queue = append(queue, reference) + } + } + slices.Sort(result) + return result, nil +} + +func differenceKeys[V any, W any](current map[string]V, previous map[string]W) []string { + result := []string{} + for key := range current { + if _, exists := previous[key]; !exists { + result = append(result, key) + } + } + slices.Sort(result) + return result +} + +func clonePublicABIRecord(value map[string]any) map[string]any { + clone := make(map[string]any, len(value)) + for key, item := range value { + clone[key] = item + } + return clone +} + +func normalizePublicABICommandFingerprint(value map[string]any) (map[string]any, error) { + normalized := clonePublicABIRecord(value) + for _, contractField := range []string{"inputContract", "outputContract"} { + rawContract, exists := normalized[contractField] + if !exists || rawContract == nil { + continue + } + contract, ok := rawContract.(map[string]any) + if !ok { + return nil, fmt.Errorf("%s must be an object", contractField) + } + contract = clonePublicABIRecord(contract) + if rawSource, exists := contract["nativeSource"]; exists { + source, ok := rawSource.(map[string]any) + if !ok { + return nil, fmt.Errorf("%s nativeSource must be an object", contractField) + } + source = clonePublicABIRecord(source) + if _, exists := source["canonicalDigest"]; !exists { + return nil, fmt.Errorf("%s nativeSource has no canonicalDigest", contractField) + } + delete(source, "canonicalDigest") + contract["nativeSource"] = source + } + if rawSources, exists := contract["nativeSources"]; exists { + sources, ok := rawSources.([]any) + if !ok { + return nil, fmt.Errorf("%s nativeSources must be an array", contractField) + } + normalizedSources := make([]any, len(sources)) + for index, rawSource := range sources { + source, ok := rawSource.(map[string]any) + if !ok { + return nil, fmt.Errorf("%s nativeSources[%d] must be an object", contractField, index) + } + source = clonePublicABIRecord(source) + if _, exists := source["canonicalDigest"]; !exists { + return nil, fmt.Errorf("%s nativeSources[%d] has no canonicalDigest", contractField, index) + } + delete(source, "canonicalDigest") + normalizedSources[index] = source + } + contract["nativeSources"] = normalizedSources + } + normalized[contractField] = contract + } + return normalized, nil +} diff --git a/internal/app/testdata/compact-current-production-consumers.json b/internal/app/testdata/compact-current-production-consumers.json index 179e23b..ed44249 100644 --- a/internal/app/testdata/compact-current-production-consumers.json +++ b/internal/app/testdata/compact-current-production-consumers.json @@ -57,6 +57,7 @@ "internal/tools/pythonpackage/main.go", "internal/tools/pythonpackage/verify.go", "internal/tools/pythonpackage/workflow_carrier.go", + "internal/tools/workflowsmoke/integration_smoke.go", "internal/tools/workflowsmoke/process.go", "internal/tools/workflowsmoke/project_navigation_smoke.go", "internal/tools/workflowsmoke/workflow_smoke.go" diff --git a/internal/app/testdata/releases/v0.9.0/cli-contract.v2.json.gz b/internal/app/testdata/releases/v0.9.0/cli-contract.v2.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..8301a067e3175cebdb4daf12dff93c2f886175db GIT binary patch literal 53523 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 literal 0 HcmV?d00001 diff --git a/internal/app/testdata/releases/v0.9.0/public-abi-observation.json b/internal/app/testdata/releases/v0.9.0/public-abi-observation.json new file mode 100644 index 0000000..de87a14 --- /dev/null +++ b/internal/app/testdata/releases/v0.9.0/public-abi-observation.json @@ -0,0 +1,268 @@ +{ + "commandFingerprintPolicy": "semantic_command_contract_without_native_source_digests", + "commands": { + "adopt-materialize-apply": "sha256:5f0d52c00cf834dc9ac6c8a40a29cbb441cad8f5c7b8a78999f09a555a3c8e34", + "adopt-materialize-plan": "sha256:421b9f2eeb783b683057c90b46d3184535dfb4e30be799566c3055978852ffcd", + "adopt-materialize-recover": "sha256:b24eddc0b49e73081d6b53439b8c637114f1bbfe8522f8324d1bddec32f120df", + "adopt-plan": "sha256:bf7bc41a2093727deb359c080c42734c4157c93c955ae13f83677f428309846f", + "adoption-checklist": "sha256:4301bde365eea518049173e060d61f1a2d6a12acb7f02685447d3fcd210955b0", + "adoption-contract-envelope": "sha256:24b7fcc6a98f5db44c079707d4e093bff75c5416fb9414ed0074af4cfdb8926a", + "adoption-doctor": "sha256:798b77fbc8e69874cacbf6685d93fa0e92366ad789f280ac1b0f5c85cb7e2cd2", + "adoption-workflow-plan": "sha256:78d6346f3fd49c43e33d12a5dea19bd4d5cb6fa022f7d583d39f94bd92259305", + "agent-route": "sha256:92e5a84a888a0e92d438c9d40d7f8ef3d1a22dcc7abb0367a6310183cc64589c", + "binding-partition": "sha256:7d9812754e8841ddf7cd955dd799815f025a69a392b41893d95ef1a1d06ea446", + "branch-authority": "sha256:42583306c75e9efa20f603c6419d368461f061e4ab83263291aa320e23691d32", + "capability-map-admission": "sha256:bbf7969228241619ef59403284566066e225b365140a01e413657d1e4f9167b2", + "change-workflow-plan": "sha256:202a00112d185d16996dde91df90c6187348ff0dbaf91025f9b9a37945585308", + "changed-path-set": "sha256:ea8d8df0e6136f84d98bc127408f1bfeed1aaee850bb8f4b0b2604896a4f39e3", + "completion-criteria": "sha256:00a9d3cde3173dafba0f682564030c457d0fd0f61de9fbc19bdbb1cc2e3e00ac", + "conformance-profile": "sha256:c551d279669ce56b24b9326394656c9dfd6eb47c1a0c276c1670886db1d19e38", + "custom-rule-boundary": "sha256:f09cbd55617b4502190119b6c60eaa5500ae1edae73bc90cc08f4340d2872c6c", + "deployment-evidence-admission": "sha256:30beda51eef07b7470c02f14289b5a49ee72460ec54d3f8efc1ee6f123339b59", + "document-lifecycle-boundary": "sha256:c9285694816ae8ca7ecd2ae501acf3074ac2518766a22a528dfbf7fe1595c7d8", + "evidence-graph": "sha256:adf2f5a6822d058d6fbf66541159fa2c87cc40a6d7f2c67d0029fdcf264cd483", + "external-consumer": "sha256:498d350faa3e85cf6169e13a11fd2b13b40fdc59a53a95ae00e433dd64c84804", + "gradual-adoption": "sha256:39a43397353855c737e152fc944ca9376e8de5480f202dc65c6ba6b623fb3ce4", + "gradual-adoption-bootstrap": "sha256:ab4359514cea255f75e94fb4669487f4da56eda0728195be9dfe4c8576d98314", + "gradual-adoption-guidance": "sha256:5da4401fe574b6206c8242bd4a559939d25be86fde636df500511eb419301d73", + "help": "sha256:c7c895acccb0bd6e4763f27d93992b9690071cf22927f1e3a5182c894cd695fe", + "impact": "sha256:0351c86bf7b8ad604554230e790af777e70fb7fc6010d0d7c41e0c2539ea4a39", + "json-report-cli-adapter-source": "sha256:ae3e7188f2a297c77053bc620878f95da9a06518d86be329b53b179aeaf5536b", + "migration-parity-admission": "sha256:b9094e7e44e7f71589b98299d2fd9d84f22b9986b90ed63226a6660d7f35e476", + "migration-plan": "sha256:59a1da7205e8d1eaeac6425c6ae601b349a4310a35e4bc83692058c24cb3fed1", + "native-evidence-guidance": "sha256:66ef6fe61beeb2142f8fdf0a4486c855b270781fcc1ca4f07191482fdcbd8a5f", + "next": "sha256:a5dba97011ed9183a0b5cc5eda77d33f1d8a778c4280198ee185c0defbeb329b", + "obligation-decision": "sha256:daba2f642ea83778e0e2fd1138d20ea8f21ae30fd8091098d0cd5088e100b4e9", + "package-runtime-dependency-admission": "sha256:ea4de2c2339eff9768d00b64ed73034ca021fd8fcd0c54571bfbb5c97ff4347d", + "pilot-admission": "sha256:6312cc5ca823d64a4f2907babc6cdbe64d185f561447e9467edccbd51b138dd9", + "producer-policy-self-proof": "sha256:f221d2e12a6dd0fbd0b8dd22365eaf4c170544abf15ea40a11c3fa6a793652db", + "proof-obligation-algebra": "sha256:40a0bdbcd38f6ea3abe35131b74698745bb3ee262bb12c5c1c47ed50d87d455e", + "proof-receipt-admission": "sha256:ad0bdb8d01674a8dcf7d01d47a2766a491554f0f34229bd67547779ee5625328", + "proof-slice": "sha256:7fe4a8d83bc96af0142182e600a93a07c1ac4120c238c89eebcde02626a0e426", + "readiness-closeout": "sha256:dfdc0430c94df86a555e10f220c6791de390a669c6e3d264da333ccf87646762", + "receipt-currentness-scope": "sha256:59f07af61715f93154a64405d18737ede83dad1384d8ee30eaa81ee36f30d931", + "receipt-producer-admission": "sha256:e2a67e3c396ec1a103b4882e2f3d6d217675b3faf649da61b47bdd573d3a1f06", + "receipt-trust-class": "sha256:dcb19494d9f93e9ac7a5131450df8eb301debbc17b3ef97de3a762487a518ad1", + "registry-consumer": "sha256:ba1993e50fbffa1e91b53acdcb80f0018931bc6a8933113daa5c7cec2bc2057e", + "registry-consumer-proof-input-compose": "sha256:afd82cc90313e3a3a0f51cf23224789e13f509458705040e92aa9925688b1264", + "release-authority": "sha256:dfad5dc22d4ee148c9ef812911a37bb92aa9b8714660da19cdfa2d271d7303f0", + "rendered-artifact-freshness": "sha256:e0ec176191308353dce416a8988adc161d44ede3cde0f3f7e7cf0169a12e86f3", + "repo-profile-admission": "sha256:db6632e3f1cada4144a0f174b043f497539ac1a39f068695db09d3ea5b2d6bd8", + "repository-inventory": "sha256:8c637fc38d4151d3fb323bbc17fcf18c707526059d17b19cd3e3216f033ac0f7", + "requirement-authoring-plan": "sha256:e5f6520e8db048f55f8e46d33dd6e1e8c32c0986a839a84ea2dc5b692215ab54", + "requirement-bindings": "sha256:8747f9f25446ca8bf543c8d4eae42c45aacf91f919f9516aa0da696c99b80e4d", + "requirement-browser-server": "sha256:3eb6e553849772503844ac0a808f504044c705e920587225ee7e0c77018e8d01", + "requirement-context-compose": "sha256:f510132944ab918aabfe79e837c502a9b36ef1ba9809b2eeebba5a66560ba840", + "requirement-context-slice": "sha256:ccf1f912d0e9cfd4f330c3b4b0a3fa8ab8811192d4d6fdf6c6c8175d5415062d", + "requirement-coverage-input-compose": "sha256:a2a38a9e95780af68adb76a8afa16ccc104787e205764616f429b76947eda789", + "requirement-coverage-view": "sha256:08af3b6ccb675c9533329c27cc44f9486d797a53770fb2f5df77740cec9484e5", + "requirement-impact-input-compose": "sha256:a80c3f5a7e4c4879c24aff806c7a6675696533d14fc938211160663f3cabff81", + "requirement-proof-resolver": "sha256:5f6b34e11fdccf381fd77fb49fd0e0b99784ea875a1184f88621e2d1e6a19345", + "requirement-proof-source-set": "sha256:bfe2aa94d1713c70ce939f794d7adc1f866ad6da71c46d347559e74faa55cbad", + "requirement-proof-view": "sha256:0f316e3da056f7e4a5a0a3ccdca72304e92b6b2abf9deec9c957672b2ef8505c", + "requirement-semantic-diff": "sha256:73d590602d2ee45533b610e8c31f9776dd5c504477a1c3c9e6325bfd4b9ee88d", + "requirement-source-admission": "sha256:d63a1b1a3b3f0b057602c6eca4dc8f9f9d2d8697eb314316c5a624044831099d", + "requirement-source-transition": "sha256:036faa999d55f92d194e4648cfeb03c9de0fc08fa5ad90669a82287acc58a1df", + "requirement-source-view": "sha256:380b829ebfcdfcf35bd024f2a4d474bde56c013068353f0f897c0fa02566ad21", + "requirement-spec-tree": "sha256:b865f5988555a56c6ba158f73da85120277ba2d6f6d764a9e475da0e6601a088", + "requirement-spec-tree-view": "sha256:a8fdbb358902c36912bc15097315630cd02b926672d1ebe94eb8aa15ebcd3125", + "requirement-traceability-graph": "sha256:ebe9aee328218caec9b94ce50ef23a13aafcdb775440d012ee1b7cbc10a9075f", + "scaffold-profile-plan": "sha256:bd1a2f627d79b38fec7b4857659f9870588f7c52e4da003abd40b01d66907d8a", + "scaffold-project-structure": "sha256:61528eb82049b3b595564079292aae805671351dcdb5c99813ba172f25bab95c", + "secret-scan": "sha256:e4fba0f0c9bb2ab59d3ae839290804cd867ae2fda51e4bb2cf7da4cb90ff34d9", + "selective-gate-evidence": "sha256:8d8a5f8b6a53dc80ea73ca3d183f7f41147ea7a895696fe1dcf56428696074cb", + "selective-gate-obligation-decision-input": "sha256:05a59d50765ebea08c209cbed21b9fd4de74b5bb9f9ff748683042fad986da32", + "selective-gate-plan": "sha256:cdcd34fb480b64170d8406acc74832fff33ee499430c2e22ad42246ea9f4045b", + "self-check": "sha256:93464e3868286e76ec193438e70c2157e448f29eb7932b0b0a2d7afef12e72f6", + "spec-overview-claims": "sha256:4c98991b271e8f9b441811a1d408fc9473642aa87effa4614e497d65cba9ec3b", + "spec-proof-bundle-admission": "sha256:eeff98b256d94200d0326e143b633deedc65531b87ce9af03f63d65f3e6968c8", + "stack-preset": "sha256:77714de54868bc398ab7c6911237957f5d9bc1cc98d218b140f0b47ca437af17", + "status": "sha256:fd4c19cf0becab9f8104a967ac677b6f22de3498b05c08b003887f047db1999c", + "test-evidence-inventory": "sha256:0424529a9c6e258fb746d7a71b45673765db64241cfecf9b4d5ae40e268ed94e", + "text-policy": "sha256:8b616f59d4c1bbcc179f6ae4797d97bffae0dc9e30923be80ab68186e8d0a2c2", + "typescript-public-api-surfaces": "sha256:dc1f2dee440c0f8130f0a439e17f275b2f13b76ca22eed8ed46433c853cb5eba", + "witness-plan": "sha256:7ea2cc4ecc54743c3ec60b9ccd744272e204bbd854932ee8262e27d5eb46648f", + "witness-scheduler-plan": "sha256:3a15a957d55c7192a64789031570dd3aa2a56d5621c7a6e5d2fc309ecee5319d", + "workspace-changed-package-plan": "sha256:13ca36deb447eb2c4561657e9abdfd348c932848fe28dd92ce184b2eadc07e26", + "workspace-manifest-facts": "sha256:732e8d9cc582cbcd1b9e0d3be142f25bc1305f295378f3962368a93d1402c4b4", + "workspace-registry": "sha256:5f339565f1e0228cad9f1f59bc42a6baf3cfe8240ba412ab1a4f25c412ff7bae", + "workspace-shard-partition": "sha256:11e500cc8b3c6b18b27bdc0e838f92cdb27725cde1497e0beb38f07138cdd391" + }, + "contractDefinitions": { + "proofkit.adopt-plan.output.v1.root-shape": "sha256:b1055d057771573252ed1ebb982f2b99d5bd1059caf2c1d57373af3249ec4444", + "proofkit.adoption-checklist.input.v1.root-shape": "sha256:183d3924bcd381a0718167ed1e5c16019edab1c8ed98edb329ec0c706c179b8b", + "proofkit.adoption-checklist.output.v1.root-shape": "sha256:d4fbec44fbbc3fa81d0a1f0383524be80952f7252d9ce02fcb041d3ca4dc449d", + "proofkit.adoption-contract-envelope.input.v2.root-shape": "sha256:648d0b5cbb894e06063bdd160a7b8d3a5cbbd41aa0825b2b6593186e3ae9790b", + "proofkit.adoption-contract-envelope.output.v1.root-shape": "sha256:3c28c67b8a40e150561a97d68691298cdffc4e381dfe8d8880d9aa4edf168664", + "proofkit.adoption-doctor.input.v1.root-shape": "sha256:67b5d7169f47e8ae4e696c651f87f433f4026112f9ccd9ff8c14e1709c7972dc", + "proofkit.adoption-doctor.output.v1.root-shape": "sha256:bbcf32e471e51d65ab0cac13f76d3c3a08c58374eb672c730bcda079b42e70cb", + "proofkit.adoption-materialization.apply-input.v1.root-shape": "sha256:d9b8a2d1725e0d64edb0e4ba037ce123909b0663bbcfde8d7ca4ba2025f98df2", + "proofkit.adoption-materialization.apply-output.v1.root-shape": "sha256:1ef903ee78ca688268a0f6ab51abce1ee389f4583724a4c55b5fe94482592307", + "proofkit.adoption-materialization.plan-input.v1.root-shape": "sha256:3e44ecca4b4b5012155bc2b461d293bb624663e93650ec5fef9d16c6bc3a0f2f", + "proofkit.adoption-materialization.plan-output.v1.root-shape": "sha256:a5d7762b12030d42c107134083b038a162ec0c02151c1f7dbdef32d88433111b", + "proofkit.adoption-materialization.recover-output.v1.root-shape": "sha256:1969957f358d4abacb179b741c2718fd89044b251eefac09901ebc31f7b2d999", + "proofkit.adoption-workflow-plan.input.v1.root-shape": "sha256:4b1ea641b5b3d5541a09b42f7e70f760da272ca62c77c1769116fdffb9dd06e5", + "proofkit.adoption-workflow-plan.output.v1.root-shape": "sha256:b66f9127f43ac3c5d4e97ef1920664df96bd8da655a53c333ac6b5fc3739d63d", + "proofkit.agent-route.input.v2.root-shape": "sha256:0a37b2ce49fc2ca2dca9d95653f1efa5363b1177d79364ca82b1281d06ae5e23", + "proofkit.agent-route.output.v3.root-shape": "sha256:09d2ffb20df8eeef73e4f0e67bdcc70cc7602525bf12d9b982a92d3c2d98ddee", + "proofkit.binding-partition.input.v1.root-shape": "sha256:db3aa86a745fd5650c0558668deb0dee2c1a04042527193efb3295774e579c97", + "proofkit.binding-partition.output.v1.root-shape": "sha256:a044f95c13e445edb611c53b61dcfbfeeef3c172596b3bb7f574529039f95093", + "proofkit.branch-authority.input.v1.root-shape": "sha256:b2dd6ab268f6430dfa27cb351d7f99b6d08d09eb2d903a957030cf384883be58", + "proofkit.branch-authority.output.v1.root-shape": "sha256:09ffe941c0c08ab9399e4e17dc0e85146abaf869a47a2c770c71ceb46ab895bc", + "proofkit.capability-map-admission.input.v1.root-shape": "sha256:5d02b008d6069c2838f38e0c1af0d68b820b7a37b14092c0336c02f0a4e01f03", + "proofkit.capability-map-admission.output.v1.root-shape": "sha256:f77609e0606035c0046ecb5baefd02a8deb9567aa03c88db568faebe1d665fbb", + "proofkit.change-workflow-plan.input.v1.root-shape": "sha256:c741c11ee19f6aa74df7c9cb3865a6b174f443a8ce6fcd5ec13c6a1105d87901", + "proofkit.change-workflow-plan.output.v1.root-shape": "sha256:fb4c512272e7d6f11f81566023ca5a83d54b31e9ef22d7324ba2c266625aa53d", + "proofkit.changed-path-set.input.v1.root-shape": "sha256:c2eb73384ab1ae5345156eec9a304835acf6ffa74c8709bac2508d427d489c29", + "proofkit.changed-path-set.output.v1.root-shape": "sha256:110a5c0ee9ec150916d832ca5d5ca99e50f1c90efee31249d8dcf547b0a6bffc", + "proofkit.completion-criteria.input.v1.root-shape": "sha256:85695f5582e2c2741ea136b32d86cdb213ee96c729b558e257e0d9222e65856f", + "proofkit.completion-criteria.output.v1.root-shape": "sha256:5f3bd4bc6bfc668135ed90a373e3c33d6d63da76637255e0722868974c3b98d2", + "proofkit.conformance-profile.input.v2.root-shape": "sha256:e1596622238c513eb0f04f9472047d70530bb98467d9d7e3e99ab944106d6777", + "proofkit.conformance-profile.output.v2.root-shape": "sha256:c48f4f4813087c55e02b53a5b6bc4cc581ca76f43f97b9d9c5da62044924ab23", + "proofkit.custom-rule-boundary.input.v1.root-shape": "sha256:072b25c19600d00d636df40b1bb9f21bd1f138ce63ca595b44b50f0648f79925", + "proofkit.custom-rule-boundary.output.v1.root-shape": "sha256:9bb1c66dded3a22febeab730b198258b731051770ea3519704eb45cf5ca5e6f3", + "proofkit.deployment-evidence-admission.input.v1.root-shape": "sha256:bd9e7dc71a4651da2ae10bf34110adb10285e4ebd30210ab6bb93b956b26a12c", + "proofkit.deployment-evidence-admission.output.v1.root-shape": "sha256:c4e5f4ca22deb9f7b79b4abfdeb2fdda2b4d7413e915e703c8038abd619b212d", + "proofkit.document-lifecycle-boundary.input.v1.root-shape": "sha256:1d4d94e4b7e4a12b39432af4d51cf483d02bd7de4f78cadd9b7262b908ebe1f4", + "proofkit.document-lifecycle-boundary.output.v1.root-shape": "sha256:e1cafa2ec4595fac0365bc3910ef9018b79e078bb49a7b26e32a94e745589da1", + "proofkit.evidence-graph.input.v1.root-shape": "sha256:e3d7704e1139ec9ecae885285cc71ec6ba59f020d2bc827162ebc47d8880332b", + "proofkit.evidence-graph.output.v1.root-shape": "sha256:482b548c92db4e3a43c29414e47ffaff788b4dec9500a863e10e6506803369a8", + "proofkit.external-consumer.input.v1.root-shape": "sha256:bf99fd56342e243a63148f828b82d55cbfca09976e348bd30c100191066325a1", + "proofkit.external-consumer.output.v1.root-shape": "sha256:906c1a16937fe87113700dd6f4e4b9161ce2a2344b6722f163ad9cbb1f5c72ba", + "proofkit.gradual-adoption-bootstrap.input.v1.root-shape": "sha256:9197bf926b6c560146de8c37c07f99c0ab0700ca02d0c08369a0ab88ff7d5233", + "proofkit.gradual-adoption-bootstrap.output.v1.root-shape": "sha256:ad134eeec1ca291e73da8d945209f98b7dfcdbf3a8ce41fcc4e9ea9e05de67cc", + "proofkit.gradual-adoption-guidance.input.v1.root-shape": "sha256:d3ba1789108f9b097a76ce388b17ffda975915ba0728a1dd465fe1b5b0d25c56", + "proofkit.gradual-adoption-guidance.output.v1.root-shape": "sha256:5dc2baadf195bcdef7f441ed1f64cc4ce91df762209c0c98a9433ab3fa8bfec2", + "proofkit.gradual-adoption.input.v1.root-shape": "sha256:6c51b22526e345d56a2568aab21c7696916c4c9bafa8f146d2ddd8ca84fc2f1a", + "proofkit.gradual-adoption.output.v1.root-shape": "sha256:27f63f422c35eabe29d1fdbe8ba663d05e9bbd7aab695471f101fe6fa881c152", + "proofkit.impact.input.v2.root-shape": "sha256:2de844000fd3b54bfed60df8d6992609477a38742c56fd9a4744a07ad19a0bc6", + "proofkit.impact.output.v2.root-shape": "sha256:9ab04fde5afaba3f6bc925e8274a3fadf21f0fec020871e2575d38f2616ffb1d", + "proofkit.json-report-cli-adapter-source.output.v1.root-shape": "sha256:6c506e9a805ec1e2b998a3f91cd80dcfe47b49b174d5fcb390fe9a3b495d364c", + "proofkit.migration-parity-admission.input.v1.root-shape": "sha256:4466c6a3a7d33620c1605d4e8eea932bd90432f0efe72783cafb9d269cdb7877", + "proofkit.migration-parity-admission.output.v1.root-shape": "sha256:e6fe3414939f0c297827fd94c50ff0bfadfba18969530aac40788bb9f66193d0", + "proofkit.migration-plan.input.v1.root-shape": "sha256:a0fb2f7e9d2469b648a6933f693ba6948a9f06c06e79393c236e2ad62a721690", + "proofkit.migration-plan.output.v1.root-shape": "sha256:d2bf5285bafc193f832836b2a8ff36d29d2c2d6a8c6efeb812edc58b2b25d31a", + "proofkit.native-evidence-guidance.output.v1.root-shape": "sha256:ea40df7bad16d5893871398f31d00af8e7d0a154dd830d03441dee1f3b3f457e", + "proofkit.next.output.v1.root-shape": "sha256:1374b8a1a9ee4100ed9323da8e2260dcf6cacf16026a16090c182643d112c9ee", + "proofkit.obligation-decision.input.v1.root-shape": "sha256:b46ca0727b8bd37c54af94d40ddb8a393c7b34352e10d98d13aa5295b5c5e99e", + "proofkit.obligation-decision.output.v1.root-shape": "sha256:d00ef83f944f8617dd20c8a02e305e4d5a30321a1e78bb0f413d68aeea15d277", + "proofkit.package-runtime-dependency-admission.input.v1.root-shape": "sha256:79457e79a5057dea7da0f933ede4b3f2d2eff15abb327d7d231d88e138fa4d15", + "proofkit.package-runtime-dependency-admission.output.v1.root-shape": "sha256:edd2d97079917745afc4c35bfb41e05594bb0e1b76ed174e15dba4d06639932b", + "proofkit.pilot-admission.input.v2.root-shape": "sha256:b3449d30c5995ec43c70a0e0b375741e91322c391865c5b9809e4beda29beb18", + "proofkit.pilot-admission.output.v1.root-shape": "sha256:ae8162846cdb4285a6c9fbf516e3500316ab7111055d83743f2c2706d8eba764", + "proofkit.producer-policy-self-proof.input.v1.root-shape": "sha256:712d3a8ac69c02c7fb348020ac44e2929325c2ac82c20fe9abf013d74f0b3fad", + "proofkit.producer-policy-self-proof.output.v1.root-shape": "sha256:76a8b86df2051f8dcd5ba67863d04afb66bf037726e7616cfc199ac2a2f85bbf", + "proofkit.proof-obligation-algebra.input.v1.root-shape": "sha256:ef13ec8945a62655b1ff804d30dc9ba3b5f712377142bf96ff5b4c3b6b6fb171", + "proofkit.proof-obligation-algebra.output.v1.root-shape": "sha256:1309b0bdd52bdb413a64b8084ce6af876ff3973e9edc82c5dd3ebe7a3b11692c", + "proofkit.proof-receipt-admission.input.v1.root-shape": "sha256:90880159e5bac4c583021791fac7acd6cbd5bd00d0f8d9e2b7761e066572b938", + "proofkit.proof-receipt-admission.output.v1.root-shape": "sha256:6ac0f95689749db3e78f56f789222475722464c3028e4ae52c45b5eb1a38b879", + "proofkit.proof-slice.input.v1.root-shape": "sha256:00bc6195cc7545c50bc8a9b3aa514423177537d02410f56ab97637fd7e5d6141", + "proofkit.proof-slice.output.v1.root-shape": "sha256:ebe71fa04a2bcea25b718a3270de37a695cf8b1cbd3338074ce5d2097e14ea8d", + "proofkit.readiness-closeout.input.v1.root-shape": "sha256:01aa2f0eb36ebe4d626d067a74bda0a98da4d6d9aab0fc29739a1f73fa23013e", + "proofkit.readiness-closeout.output.v1.root-shape": "sha256:af0c0fc7feee99b450dc665d14a9fe85a7e5f6e07a8d6fd3246c64018a7397bf", + "proofkit.receipt-currentness-scope.input.v1.root-shape": "sha256:fe1b7088d55547ce55caba88434df6e1728e522f9b68f22a006943e5c86f7073", + "proofkit.receipt-currentness-scope.output.v1.root-shape": "sha256:44a90b8eea868c9be44cbde472438dab29d2d02a0745741c94deedca4a954c4c", + "proofkit.receipt-producer-admission.input.v1.root-shape": "sha256:f13cd253ee1de8f0514d76f1e52e4a8c484f3dbd0151e961a20659cef57a75bd", + "proofkit.receipt-producer-admission.output.v1.root-shape": "sha256:f7da547e6ed0bf89a29aeb5f5426fbce04bb0f6369246ff0c74fe6ee5c4d80bf", + "proofkit.receipt-trust-class.input.v1.root-shape": "sha256:46d9d121275c7763632e51edc9fbe04829b7843b6a587a65e08954c2dbad0fe7", + "proofkit.receipt-trust-class.output.v1.root-shape": "sha256:e1a9d098ea2037cf466481824a4a3ca017ba74df0d92aa6613b7603eea794d32", + "proofkit.registry-consumer-proof-input-compose.input.v1.root-shape": "sha256:529947c77c35bbf762c410f7446655132caa829c70cbfbb6e6c5d93672cc998a", + "proofkit.registry-consumer-proof-input-compose.output.v1.root-shape": "sha256:094335d032894ba6d0681a1aa91b62e76f056d06fdffc24b1748b78f9ed954d0", + "proofkit.registry-consumer.input.v1.root-shape": "sha256:a89496595f72e9376a2cfb5a1b2639157952817add2202428356ac448c3364f1", + "proofkit.registry-consumer.output.v1.root-shape": "sha256:1610d3231112d3e2dbbcc241debe0ca1077f6a603086e10fde1d94b8262cac17", + "proofkit.release-authority.input.v1.root-shape": "sha256:31c75401d61456af4047e5611f66dc91124f4388de59278c7ad9ccf919069daf", + "proofkit.release-authority.output.v1.root-shape": "sha256:f1b0a9ef15a0ec0f51c6e86860fe108df3098bef59d8af92f1d833d688efc45c", + "proofkit.rendered-artifact-freshness.input.v1.root-shape": "sha256:8db852267c4e57b95b7498fb039f01ccb6ee1595bd7ec2837d9c35dd126adccb", + "proofkit.rendered-artifact-freshness.output.v1.root-shape": "sha256:f62fb968ec2cae41525ecac5665e4ac49add852c3d82d820073d60d9fa6ea3ef", + "proofkit.repo-profile-admission.input.v1.root-shape": "sha256:c2340140fc4e1093500fc9500ca619a05fac79767a19a63d8c6e36cb33937170", + "proofkit.repo-profile-admission.output.v1.root-shape": "sha256:063bf420f694f15383352da18ec0180cd5727337a2e4813d8de082b2a6450b2f", + "proofkit.repository-inventory.output.v1.root-shape": "sha256:02f0bd8e33d192437bb8f7c3fc1c0d01e39d428b90a8135470521a69d3d08d25", + "proofkit.requirement-authoring-plan.input.v1.root-shape": "sha256:aa8bc39d8dfd7746a4b23176a8e834b0e84a0937136bc506435fefb39d3a5d99", + "proofkit.requirement-authoring-plan.output.v2.root-shape": "sha256:47a28cb30a58048faf0d824523921fb4204120ff63b19f4833a4f2dfaaa40a13", + "proofkit.requirement-bindings.input.v1.root-shape": "sha256:b57bcec7fcdfa6a98c13051d06ff69ff05e15f2b4a19860eb71c7aeabda23018", + "proofkit.requirement-bindings.output.v1.root-shape": "sha256:ff1fbd4ad20e6c5c6b13a2a3ff443c2082c1514d7c3b34013817ee10ff0077fc", + "proofkit.requirement-browser-server.input.v3.root-shape": "sha256:7a15e92365672d1ca260fcbee8be2bd6fbb4b1e194e9996fcbfd59e73e1f4f34", + "proofkit.requirement-browser-server.output.v1.root-shape": "sha256:bfb0da899a9c77d969d2687e04e866912031a7f9cb1086e40f5038144c8924b3", + "proofkit.requirement-context-compose.input.v1.root-shape": "sha256:99984834220692385d55e421fca1a03cf596d303d3be91f88827d5860659b9f0", + "proofkit.requirement-context-compose.output.v2.root-shape": "sha256:66ed99649703edfe696684ec5a38264b03f456c3349c3b6010742187c5235f1a", + "proofkit.requirement-context-slice.input.v1.root-shape": "sha256:994d367551a46aeea385257bd27798a3bd32d8eff7f494bbf332bed71ee456f9", + "proofkit.requirement-context-slice.output.v1.root-shape": "sha256:990ecf59350ba938d31fc32c63b9b88478911782d608245a2a4457c132f5a7d2", + "proofkit.requirement-coverage-input-compose.input.v2.root-shape": "sha256:cfaf53655f04130f02ee986bdc57379593d6b63ec0ae6c7415370866000baed3", + "proofkit.requirement-coverage-input-compose.output.v2.root-shape": "sha256:e63a626870e41ce2b9ff3ecbdb82029fcea6c26d1c2893f6fbe325045dd34087", + "proofkit.requirement-coverage-view.input.v2.root-shape": "sha256:c890cddbc60c77bd9a66116caedb46d71eb45da9b657be95e6adf54f70aa07f7", + "proofkit.requirement-coverage-view.output.v3.root-shape": "sha256:9d4404f95e4a54fbe21a8817c5577d30c9cf76903c898888b9d688d88d557161", + "proofkit.requirement-impact-input-compose.input.v2.root-shape": "sha256:17489925d632b6e8543f86be0f6824818803b47cdb882278d212032b944a384f", + "proofkit.requirement-impact-input-compose.output.v2.root-shape": "sha256:809cbddcc6ed83a781d01a0ea6ed59bb7730a5916ec1f95aeafd6ae2f19b3ec5", + "proofkit.requirement-proof-resolver.input.v2.root-shape": "sha256:3b8a89fc3d32e20d40db72ccbb2a90fe8329072108fbef3a439c7df4ad89b711", + "proofkit.requirement-proof-resolver.output.v2.root-shape": "sha256:7cf374f46d230331241e723c423f3695b2e06a50fc385aae18d7341d95059307", + "proofkit.requirement-proof-source-set.input.v2.root-shape": "sha256:57c43710648281e5c4264bfadaa407d1268aecd020df22ba6641212749146a7f", + "proofkit.requirement-proof-source-set.output.v2.root-shape": "sha256:f8a03a5f5b4964cc8f7e5d00d6b962052e95ff0b91d27b0d41cc43e771310055", + "proofkit.requirement-proof-view.input.v2.root-shape": "sha256:fd96cde2473fe37924a588a5e257017e025efb73d1ec1fc063f8b715b4b7e0cb", + "proofkit.requirement-proof-view.output.v2.root-shape": "sha256:a11aef4bb9ac6eced780b75287412b3f6bd43b1302d392773e1b6db81f42ff67", + "proofkit.requirement-semantic-diff.input.v2.root-shape": "sha256:9df463380742b2424153c275365401765c5b8da8c19a116e42e108d5359ab0b3", + "proofkit.requirement-semantic-diff.output.v2.root-shape": "sha256:25a530d869c69ff644ad53f84abbcbdaa03559577c99e244e5e944ec3a811ff9", + "proofkit.requirement-source-admission.input.v1.root-shape": "sha256:1ef5b4ff14c7ee7e5f0b76d0e8cdedb5f9fad32cbcfdd7b3930493eae14670e6", + "proofkit.requirement-source-admission.output.v1.root-shape": "sha256:cd9db4e8db0336364a4815d1c7aac3ad317e42d89279c2f0924bcf1f439b0a7a", + "proofkit.requirement-source-transition.input.v1.root-shape": "sha256:4cb46831aa6d7350eaf22fbd5b9b7e154c9a660e21a901815f70378369cf38f6", + "proofkit.requirement-source-transition.output.v1.root-shape": "sha256:955fff4cf23f583cb860d4490b369f4e253c54ec6516221660c803df592cb5b7", + "proofkit.requirement-source-view.input.v1.root-shape": "sha256:ae046f9795c4e524f95fcf47e8cdbd008c1d9424d66ecfb17683ea3c0abf3df8", + "proofkit.requirement-source-view.output.v1.root-shape": "sha256:26591b6039c8a3550854dc161a1a60b9259f3fd5be80790950d9ec8e36346620", + "proofkit.requirement-spec-tree-view.input.v1.root-shape": "sha256:bfecbc18fe9e747ce79bbe03b83db5a7f7178d58f514b0b9d23ed26cb2190f32", + "proofkit.requirement-spec-tree-view.output.v2.root-shape": "sha256:9a03221240f61eeb94157bfb556edee325840b6e54f5d8a79169c2a9e93d3b4c", + "proofkit.requirement-spec-tree.input.v1.root-shape": "sha256:394f54e615f552af8012c9ff762b83095ec9dabcc163aa19d52404f02589ea73", + "proofkit.requirement-spec-tree.output.v1.root-shape": "sha256:929838c2402c4512c97721a2d8a1d9389a34fc09d992fa801e22b4527edbdcd9", + "proofkit.requirement-traceability-graph.input.v2.root-shape": "sha256:5a3b03a8103996a61ea80cfdfeef3bf18a57886cd37d58e7c8df4b0efb8a947f", + "proofkit.requirement-traceability-graph.output.v1.root-shape": "sha256:16e8aec9edb257caff0e834816dfc44e6929bbdf399055477c779383dbcf4a5c", + "proofkit.scaffold-profile-plan.input.v1.root-shape": "sha256:aec8ad7d35f5c77dbae52197834c3abc797251371b20f3ce7036353154d28f0b", + "proofkit.scaffold-profile-plan.output.v1.root-shape": "sha256:9e6cccdfd25d48ae93d2a6c247873c1db4204cdebed3ec7d83fe332c8023ecc9", + "proofkit.scaffold-project-structure.input.v1.root-shape": "sha256:4eaa681eb5a480b123693a8e87815958e7ae7e0a409a71189c671fca4e345ede", + "proofkit.scaffold-project-structure.output.v1.root-shape": "sha256:4ffbee5bc142abb84d0fb8fbdcc25dac1b043310e3b728b7b8de190d30bb03fe", + "proofkit.secret-scan.input.v1.root-shape": "sha256:3e8d086056134b306a27f817012f1fa6728bf7b1eb7636b37d5f180ee0f8b46a", + "proofkit.secret-scan.output.v1.root-shape": "sha256:b1e5663db3a831f670b7d76015697719c87683766088a4a62674d9fd6e34c186", + "proofkit.selective-gate-evidence.input.v1.root-shape": "sha256:a0b9c78088bb7429086cf9244ce54ba5636281899ae506e410baab9ffa320146", + "proofkit.selective-gate-evidence.output.v1.root-shape": "sha256:a5f97a52b9f74d20d757495d22e7ba736aa8e93fef4521aa2d130e65eead8e5f", + "proofkit.selective-gate-obligation-decision-input.input.v1.root-shape": "sha256:e9ff696a70ecb2b08b74c84ea3fb34bd56bdc615f6d75f151694137e15677905", + "proofkit.selective-gate-obligation-decision-input.output.v1.root-shape": "sha256:ffd3ee7cafe59efa0fb20ff99bdc33962d090f012dec81060e2b9f78b36ed08d", + "proofkit.selective-gate-plan.input.v1.root-shape": "sha256:b7bc0bc0f5860b6562039caf8a74a4b3cf10c2bec0776db642d8a6c833dfed02", + "proofkit.selective-gate-plan.output.v1.root-shape": "sha256:fc1736408f2bc4e51fdd353e89ee6912295c7b4401b237c972d4ceddd45b729b", + "proofkit.self-check.input.v1.root-shape": "sha256:f3d13182d45baade9b5870bc33472ddc177721a31b0b82fe6c3ecd2fb08ca6de", + "proofkit.self-check.output.v1.root-shape": "sha256:8635d51a9ae997df05d7b64f86f6d6c29bb7850cf85835293dddbe3a204864a5", + "proofkit.spec-overview-claims.input.v1.root-shape": "sha256:1aea724c3878b0d79354cd6320fdc67de6e0efe189882c311397d702d385e752", + "proofkit.spec-overview-claims.output.v1.root-shape": "sha256:0e9cabc13356be0946beeb84f18f48a2be287a34f73a2c0d03c548eec49d0a5e", + "proofkit.spec-proof-bundle-admission.input.v1.root-shape": "sha256:85e99552f0e1e6bc24aa23761756c7989d8d297280ae8da6f09e3e7a29cfdee3", + "proofkit.spec-proof-bundle-admission.output.v1.root-shape": "sha256:a8d651222dda8c6c3652f6dc8858aaa4b69b01eb7010727222bfcfebbecd8371", + "proofkit.stack-preset.output.v1.root-shape": "sha256:0cabd5952f7716cd6db163a2037bdb99ff12f8879712420987a4c93dbe3e0528", + "proofkit.status.output.v1.root-shape": "sha256:c88e90087b87c789cbbdf4082fb3569fba17f4665ea325a8b222577a72a4b5e3", + "proofkit.test-evidence-inventory.input.v2.root-shape": "sha256:b89dc7cb02e288f4734da6febc02eedfe8dd113018e90b5ab4ae501cc60aff3c", + "proofkit.test-evidence-inventory.output.v2.root-shape": "sha256:12259060e4b5dde06ef1ea4440b249633af88be5555496e9daa95838b0593d22", + "proofkit.text-policy.input.v1.root-shape": "sha256:8343f50b7d23e0104beff0c2fc4272b205b40dfc122dfff4acb0a7dab49ac1ad", + "proofkit.text-policy.output.v1.root-shape": "sha256:fdd89dc7df35b4d3e13d668aba3835bcfe1dd8f45e5e417a15e04d356cbc7b90", + "proofkit.typescript-public-api-surfaces.input.v1.root-shape": "sha256:3595f9120e43ab94658e320057650ed68b9fa1b81b8c450423949453e4c4b80d", + "proofkit.typescript-public-api-surfaces.output.v1.root-shape": "sha256:4f3c85ffe365b67bb763099c51c712ca5ce2c3e6a18c8f807523d26cf1b146b8", + "proofkit.witness-plan.input.v1.root-shape": "sha256:4cd56bcc87a5a38ab7767797e2c3c3aa58de8d8785375a740aae2abfdcd895e1", + "proofkit.witness-plan.output.v1.root-shape": "sha256:54bbc6306250bb8d5e5e92355380427c36f7f607b60d72dd1408600a25a7f588", + "proofkit.witness-scheduler-plan.input.v1.root-shape": "sha256:13be42b86615146db538ca509127b8685503bfcc38053954db049948dc975224", + "proofkit.witness-scheduler-plan.output.v1.root-shape": "sha256:fa148200a4050d07122235767624170bd477d2d7e307cb62e6877c0ac1469e09", + "proofkit.workspace-changed-package-plan.input.v1.root-shape": "sha256:65f68f90d6b8d5227c43f6f7a86676d80c23874fa3f678ea154daf1923ba8743", + "proofkit.workspace-changed-package-plan.output.v1.root-shape": "sha256:6431ffd5078f661d919ea9d74dc670d3ddf964b815639c31dd2ad4d9b7265b99", + "proofkit.workspace-manifest-facts.input.v1.root-shape": "sha256:27561dac52db8ce803e5ae75ec1f8113822896fa7ae283c1786448ef6bbcc95c", + "proofkit.workspace-manifest-facts.output.v1.root-shape": "sha256:20b1c2c0caeab5c64c22e1ac8445bba65712b8907d73d960c6162fe6d4786ea8", + "proofkit.workspace-registry.input.v1.root-shape": "sha256:eff179bdd299387a66ac3c9c41b404687cd7e3ea8770d411f8ac443bd8d48fed", + "proofkit.workspace-registry.output.v1.root-shape": "sha256:87881114c7ae0a0ea62b08644df9cba0d919f53dfcf402ecc772edcd81b5965b", + "proofkit.workspace-shard-partition.input.v1.root-shape": "sha256:21148091a01be99a42b3b44694565afd69c63fe63326ca2589a20836573c3807", + "proofkit.workspace-shard-partition.output.v1.root-shape": "sha256:4b54ef65d891ea3ac80459bc3d2f2f2bb5b7306e06450c5d8dde6bca5da0b2f4" + }, + "contractId": "proofkit.cli-contract.v2", + "contractSchemaVersion": 2, + "nonClaims": [ + "Per-command fingerprints omit only native source canonical digests; the exact raw contract remains bound by publicAbiSha256.", + "This frozen source observation does not authenticate registry publication, provider state, consumer migration, or runtime compatibility." + ], + "observationKind": "proofkit.frozen-public-abi-observation", + "orderingPolicy": "lexicographic_by_identity", + "packageName": "@research-engineering/agentic-proofkit", + "processContractSha256": "sha256:ce7724268c700f14fff3d680b3d959085a7dea8b3420f8b32fd243365192df5c", + "publicAbiSha256": "sha256:9a6842b45a218d6caa5da517b0b20f861e13c35a2900e92d34361cdf771781f7", + "releaseVersion": "0.9.0", + "schemaVersion": 1 +} diff --git a/internal/app/testdata/releases/v0.9.0/release/change-record.v2.json b/internal/app/testdata/releases/v0.9.0/release/change-record.v2.json new file mode 100644 index 0000000..41c5ffa --- /dev/null +++ b/internal/app/testdata/releases/v0.9.0/release/change-record.v2.json @@ -0,0 +1,49 @@ +{ + "schemaVersion": 2, + "previousVersion": "0.8.0", + "version": "0.9.0", + "changeClass": "breaking", + "breakingChanges": [ + { + "changeId": "proofkit.agent-workflow.change-plan-route", + "summary": "Replace the flat change-workflow-plan CLI route with the hierarchical change plan route while preserving one internal command implementation and its input and output contracts." + }, + { + "changeId": "proofkit.cli-contract.omitted-route-policy", + "summary": "Make the command-id fallback for an omitted command route an explicit required CLI-contract grammar field; Proofkit source and installed-carrier validators reject contracts that omit or alter this policy." + } + ], + "additions": [ + { + "changeId": "proofkit.project-state.next-action", + "summary": "Add a bounded next command that maps each admitted structural project state to exactly one non-authoritative repository action." + }, + { + "changeId": "proofkit.project-state.status", + "summary": "Add a read-only status command that classifies a bounded normalized materialized-project and transaction observation; admitted in-bound records bind exact content digests, while unread out-of-bound records identify only their invalid class, without claiming native verification or workflow completion." + } + ], + "migration": { + "required": true, + "steps": [ + "Replace agentic-proofkit change-workflow-plan invocations with agentic-proofkit change plan; input and output JSON contracts are unchanged.", + "Update CLI-contract consumers to require commandRouteGrammar.omittedRoutePolicy=command_id; commands without an explicit route continue to resolve to their stable command ID." + ] + }, + "platformRequirements": [ + "Published Darwin package binaries require macOS 13.0 or later on arm64 and x86_64." + ], + "knownLimitations": [ + "Adopt plan inventories only a fixed root-file catalog; it does not infer stack identity, inspect arbitrary source semantics, generate requirements, write files, or execute native evidence.", + "Transactional materialization writes only owner-admitted candidate artifacts under one explicit repository root; it does not infer requirement meaning, execute native evidence, approve merge or release, provide filesystem-wide atomic visibility to concurrent readers, or protect its private namespace from a hostile same-user process.", + "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.", + "Project status and next classify materialized repository structure only; they do not execute native verification, validate receipt currentness or trust, or declare workflow completion.", + "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." + ], + "rollback": { + "strategy": "previous_admitted_version" + } +} diff --git a/internal/command/agentintegration/check.go b/internal/command/agentintegration/check.go new file mode 100644 index 0000000..ae3bb57 --- /dev/null +++ b/internal/command/agentintegration/check.go @@ -0,0 +1,203 @@ +package agentintegration + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "io/fs" + "os" + "unicode/utf8" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/repositorytransaction" + "github.com/research-engineering/agentic-proofkit/internal/kernel/rootpath" +) + +const maximumCheckBytes = 8 << 10 + +// CheckResult reports only generated-byte freshness, not host activation. +type CheckResult struct { + document Document + state string +} + +func (result CheckResult) State() string { return result.state } + +func (result CheckResult) JSONValue() map[string]any { + return map[string]any{ + "schemaVersion": 1, + "kind": "proofkit.integration-check.v1", + "tool": result.document.tool, + "targetPath": result.document.path, + "integrationId": result.document.identity, + "expectedContentDigest": result.document.contentDigest, + "state": result.state, + "nonClaims": checkNonClaims(), + } +} + +func (result CheckResult) Text() string { + return fmt.Sprintf("Integration check: %s\nTool: %s\nTarget: %s\nCurrent means generated-byte freshness only; no installation, host activation, or post-return stability is proven.\n", + result.state, result.document.tool, result.document.path) +} + +func checkNonClaims() []any { + return []any{ + "Current means exact generated-byte freshness only.", + "No installation, update, removal, or host activation is performed or proven.", + "No native verification or approved-launcher invocation is proven.", + "Observed stability does not guarantee absence of mutations after return.", + } +} + +type checkDependencies struct { + openFile func(*repositorytransaction.InspectionLease, string) (repositorytransaction.InspectionFile, error) + closeLease func(*repositorytransaction.InspectionLease) error +} + +func nativeCheckDependencies() checkDependencies { + return checkDependencies{ + openFile: (*repositorytransaction.InspectionLease).OpenExactRegularFile, + closeLease: (*repositorytransaction.InspectionLease).Close, + } +} + +// Check uses one read-only lease and two bounded observations of the selected +// descriptor-owned path. Neither operation authorizes filesystem mutation. +func Check(ctx context.Context, repositoryRoot string, document Document) (CheckResult, error) { + return checkWithDependencies(ctx, repositoryRoot, document, nativeCheckDependencies()) +} + +func checkWithDependencies(ctx context.Context, repositoryRoot string, document Document, dependencies checkDependencies) (result CheckResult, returnErr error) { + if ctx == nil { + return CheckResult{}, checkOperationError("context is required") + } + // Cancellation and cleanup participate in every outcome, including errors. + defer func() { + if err := ctx.Err(); err != nil && !errors.Is(returnErr, err) { + returnErr = errors.Join(returnErr, fmt.Errorf("integration check operation cancelled: %w", err)) + } + if returnErr != nil { + result = CheckResult{} + } + }() + if ctx.Err() != nil { + return CheckResult{}, nil + } + if document.tool == "" || document.path == "" || document.content == "" || document.identity == "" || document.contentDigest == "" || document.capabilityDigest == "" { + return CheckResult{}, checkOperationError("source document is required") + } + if dependencies.openFile == nil || dependencies.closeLease == nil { + return CheckResult{}, checkOperationError("inspection dependencies are incomplete") + } + lease, err := repositorytransaction.OpenInspectionLease(ctx, repositoryRoot) + if err != nil { + if errors.Is(err, repositorytransaction.ErrReadCleanup) || errors.Is(err, rootpath.ErrTraversalCleanup) { + return CheckResult{}, checkCleanupError("open repository inspection lease") + } + return CheckResult{}, checkOperationError("open repository inspection lease") + } + defer func() { + if err := dependencies.closeLease(lease); err != nil { + returnErr = errors.Join(returnErr, checkCleanupError("close inspection lease")) + } + }() + before, err := observeCheckFile(ctx, lease, document, dependencies.openFile) + if err != nil { + return CheckResult{}, err + } + after, err := observeCheckFile(ctx, lease, document, dependencies.openFile) + if err != nil { + return CheckResult{}, err + } + if !sameCheckObservation(before, after) { + return CheckResult{}, checkOperationError("selected file changed during inspection") + } + if err := lease.VerifyRootIdentity(); err != nil { + return CheckResult{}, checkOperationError("repository root changed during inspection") + } + return CheckResult{document: document, state: after.state}, nil +} + +func checkOperationError(operation string) error { + return fmt.Errorf("integration check operation failed: %s", operation) +} + +func checkCleanupError(operation string) error { + return fmt.Errorf("integration check operation failed: %s: %w", operation, repositorytransaction.ErrReadCleanup) +} + +type checkObservation struct { + state string + content []byte + info fs.FileInfo +} + +func observeCheckFile(ctx context.Context, lease *repositorytransaction.InspectionLease, document Document, openFile func(*repositorytransaction.InspectionLease, string) (repositorytransaction.InspectionFile, error)) (observation checkObservation, returnErr error) { + if err := ctx.Err(); err != nil { + return checkObservation{}, fmt.Errorf("integration check operation cancelled: %w", err) + } + file, err := openFile(lease, document.path) + switch { + case errors.Is(err, repositorytransaction.ErrReadCleanup), errors.Is(err, rootpath.ErrTraversalCleanup): + return checkObservation{}, checkCleanupError("open selected file") + case errors.Is(err, repositorytransaction.ErrInspectionRouteChanged), errors.Is(err, rootpath.ErrAmbiguousRoute), + errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded), errors.Is(err, fs.ErrPermission): + return checkObservation{}, checkOperationError("open selected file") + case errors.Is(err, fs.ErrNotExist): + return checkObservation{state: "missing"}, nil + case errors.Is(err, repositorytransaction.ErrUnsafeInspectionRoute): + return checkObservation{state: "invalid"}, nil + case err != nil: + return checkObservation{}, checkOperationError("open selected file") + } + defer func() { + if err := file.Close(); err != nil { + observation = checkObservation{} + returnErr = errors.Join(returnErr, checkCleanupError("close selected file")) + } + }() + opened, err := file.Stat() + if err != nil { + return checkObservation{}, checkOperationError("inspect selected file") + } + if !opened.Mode().IsRegular() { + return checkObservation{state: "invalid", info: opened}, nil + } + // Oversized files remain invalid, but their bounded prefix participates in + // reobservation too. Never read more than the admitted 8 KiB per observation. + content, err := io.ReadAll(io.LimitReader(file, maximumCheckBytes)) + if err != nil { + return checkObservation{}, checkOperationError("read selected file") + } + after, err := file.Stat() + if err != nil { + return checkObservation{}, checkOperationError("reinspect selected file") + } + if !sameCheckFile(opened, after) || int64(len(content)) != min(opened.Size(), maximumCheckBytes) { + return checkObservation{}, checkOperationError("selected file changed while reading") + } + state := "stale" + switch { + case opened.Size() > maximumCheckBytes || !utf8.Valid(content) || bytes.IndexByte(content, 0) >= 0: + state = "invalid" + case string(content) == document.content: + state = "current" + } + return checkObservation{state: state, content: content, info: opened}, nil +} + +func sameCheckObservation(before, after checkObservation) bool { + if before.state != after.state || !bytes.Equal(before.content, after.content) { + return false + } + if before.info == nil || after.info == nil { + return before.info == nil && after.info == nil + } + return sameCheckFile(before.info, after.info) +} + +func sameCheckFile(before, after fs.FileInfo) bool { + return os.SameFile(before, after) && before.Mode() == after.Mode() && before.Size() == after.Size() && before.ModTime().Equal(after.ModTime()) +} diff --git a/internal/command/agentintegration/check_test.go b/internal/command/agentintegration/check_test.go new file mode 100644 index 0000000..9ae7779 --- /dev/null +++ b/internal/command/agentintegration/check_test.go @@ -0,0 +1,720 @@ +package agentintegration + +import ( + "context" + "encoding/json" + "errors" + "io" + "io/fs" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + "time" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" + "github.com/research-engineering/agentic-proofkit/internal/kernel/repositorytransaction" + "github.com/research-engineering/agentic-proofkit/internal/kernel/rootpath" + "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" +) + +const checkPrivateSentinel = "private-check-observation-92a61" + +func checkTestDocument(t *testing.T, tool string) Document { + t.Helper() + var capabilities []Capability + for _, command := range ConsumedCommands() { + capabilities = append(capabilities, Capability{ + Command: command, Route: []string{command}, ContractDigest: digest.SHA256TextRef("check-fixture:" + command), + }) + } + document, err := Source(tool, capabilities) + if err != nil { + t.Fatal("source fixture failed") + } + return document +} + +func checkWrite(t *testing.T, path, content string) { + t.Helper() + checkMkdir(t, filepath.Dir(path)) + if err := os.WriteFile(path, []byte(content), 0o600); err != nil { + t.Fatal("fixture write failed") + } +} + +func checkMkdir(t *testing.T, path string) { + t.Helper() + if err := os.MkdirAll(path, 0o700); err != nil { + t.Fatal("fixture directory creation failed") + } +} + +func checkSymlink(t *testing.T, target, path string) { + t.Helper() + checkMkdir(t, filepath.Dir(path)) + if err := os.Symlink(target, path); err != nil { + t.Fatal("fixture symlink creation failed") + } +} + +type checkTestEntry struct { + mode fs.FileMode + modTime time.Time + content string +} + +func checkTree(t *testing.T, root string) map[string]checkTestEntry { + t.Helper() + entries := map[string]checkTestEntry{} + err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + info, err := entry.Info() + if err != nil { + return err + } + value := checkTestEntry{mode: info.Mode(), modTime: info.ModTime()} + switch { + case info.Mode().IsRegular(): + content, err := os.ReadFile(path) + if err != nil { + return err + } + value.content = string(content) + case info.Mode()&fs.ModeSymlink != 0: + value.content, err = os.Readlink(path) + if err != nil { + return err + } + } + entries[path] = value + return nil + }) + if err != nil { + t.Fatal("fixture snapshot failed") + } + return entries +} + +func checkUnchanged(t *testing.T, root string, before map[string]checkTestEntry) { + t.Helper() + if !reflect.DeepEqual(before, checkTree(t, root)) { + t.Fatal("check changed the protected filesystem snapshot") + } +} + +func checkNoDisclosure(t *testing.T, result CheckResult, err error, private ...string) { + t.Helper() + encoded, marshalErr := json.Marshal(result.JSONValue()) + if marshalErr != nil { + t.Fatal("check report is not JSON serializable") + } + output := string(encoded) + result.Text() + if err != nil { + output += err.Error() + } + for _, secret := range append(private, checkPrivateSentinel, digest.SHA256TextRef(checkPrivateSentinel)) { + if secret != "" && strings.Contains(output, secret) { + t.Fatal("check disclosed protected data") + } + } + if strings.Contains(output, "passed") { + t.Fatal("freshness was promoted to passed") + } +} + +func checkWantError(t *testing.T, result CheckResult, err error, private ...string) { + t.Helper() + checkNoDisclosure(t, result, err, private...) + if err == nil || result != (CheckResult{}) || !strings.Contains(err.Error(), "integration check operation") { + t.Fatal("expected normalized operation error and zero result") + } +} + +func TestCheckStatesAreReadOnlyAndPrivate(t *testing.T) { + for _, tool := range []string{"codex", "claude"} { + document := checkTestDocument(t, tool) + cases := []struct { + name string + state string + content string + }{ + {name: "missing", state: "missing"}, + {name: "missing leaf", state: "missing"}, + {name: "current", state: "current", content: document.Content()}, + {name: "unknown", state: "stale", content: checkPrivateSentinel}, + {name: "empty", state: "stale"}, + {name: "exact bound", state: "stale", content: strings.Repeat("a", maximumCheckBytes)}, + {name: "oversized", state: "invalid", content: strings.Repeat("a", maximumCheckBytes+1)}, + {name: "invalid UTF8", state: "invalid", content: checkPrivateSentinel + "\xff"}, + {name: "NUL", state: "invalid", content: checkPrivateSentinel + "\x00"}, + {name: "directory", state: "invalid"}, + {name: "non directory parent", state: "invalid"}, + } + for _, test := range cases { + t.Run(tool+"/"+test.name, func(t *testing.T) { + sandbox := t.TempDir() + root := filepath.Join(sandbox, "repository") + home := filepath.Join(sandbox, "home") + checkMkdir(t, root) + t.Setenv("HOME", home) + t.Setenv("CODEX_HOME", filepath.Join(home, ".codex")) + t.Setenv("CLAUDE_CONFIG_DIR", filepath.Join(home, ".claude")) + otherTool := "claude" + if tool == "claude" { + otherTool = "codex" + } + other := checkTestDocument(t, otherTool) + checkWrite(t, filepath.Join(root, other.path), checkPrivateSentinel) + checkWrite(t, filepath.Join(root, "AGENTS.md"), checkPrivateSentinel) + for _, directory := range []string{".agents", ".codex", ".claude"} { + checkWrite(t, filepath.Join(home, directory, "skills/agentic-proofkit/SKILL.md"), checkPrivateSentinel) + } + selected := filepath.Join(root, document.path) + switch test.name { + case "missing": + case "missing leaf": + checkMkdir(t, filepath.Dir(selected)) + case "directory": + checkMkdir(t, selected) + case "non directory parent": + checkWrite(t, filepath.Join(root, strings.Split(document.path, "/")[0]), checkPrivateSentinel) + default: + checkWrite(t, selected, test.content) + } + before := checkTree(t, sandbox) + result, err := Check(context.Background(), root, document) + checkNoDisclosure(t, result, err, root) + if err != nil || result.State() != test.state { + t.Fatal("unexpected freshness classification") + } + if test.state != "current" && test.content != "" { + checkNoDisclosure(t, result, err, digest.SHA256TextRef(test.content)) + } + checkUnchanged(t, sandbox, before) + }) + } + } +} + +func TestCheckProjectionHasOnlyAdmittedFields(t *testing.T) { + document := checkTestDocument(t, "codex") + result, err := Check(context.Background(), t.TempDir(), document) + if err != nil { + t.Fatal("check failed") + } + value := result.JSONValue() + if _, err := stablejson.Marshal(value); err != nil { + t.Fatalf("check output is not a canonical JSON value: %v", err) + } + expected := map[string]any{ + "schemaVersion": 1, "kind": "proofkit.integration-check.v1", "tool": "codex", + "targetPath": document.path, "integrationId": document.identity, + "expectedContentDigest": document.contentDigest, "state": "missing", "nonClaims": checkNonClaims(), + } + if !reflect.DeepEqual(value, expected) || !strings.Contains(result.Text(), "missing") { + t.Fatal("check projection differs from its exact contract") + } + value["state"] = "passed" + value["nonClaims"].([]any)[0] = "changed" + if !reflect.DeepEqual(result.JSONValue(), expected) || result.State() != "missing" { + t.Fatal("projection mutation changed the private result") + } +} + +func TestCheckRejectsZeroDocumentBeforeInspection(t *testing.T) { + dependencies := nativeCheckDependencies() + dependencies.openFile = func(*repositorytransaction.InspectionLease, string) (repositorytransaction.InspectionFile, error) { + t.Fatal("zero document reached file inspection") + return nil, nil + } + result, err := checkWithDependencies(context.Background(), checkPrivateSentinel, Document{}, dependencies) + checkWantError(t, result, err) + if !strings.Contains(err.Error(), "source document") { + t.Fatal("zero document was not rejected before root admission") + } +} + +func TestCheckInvalidRootsAreOperationErrors(t *testing.T) { + document := checkTestDocument(t, "codex") + sandbox := t.TempDir() + file := filepath.Join(sandbox, "file") + link := filepath.Join(sandbox, "link") + checkWrite(t, file, checkPrivateSentinel) + checkSymlink(t, sandbox, link) + for _, root := range []string{"", filepath.Join(sandbox, "absent"), file, link} { + result, err := Check(context.Background(), root, document) + checkWantError(t, result, err, root) + } +} + +func TestCheckRejectsEverySymlinkComponentWithoutFollowing(t *testing.T) { + document := checkTestDocument(t, "codex") + components := strings.Split(document.path, "/") + for index := range components { + t.Run(components[index], func(t *testing.T) { + sandbox := t.TempDir() + root := filepath.Join(sandbox, "repository") + outside := filepath.Join(sandbox, "outside") + checkMkdir(t, root) + target := outside + if index < len(components)-1 { + checkWrite(t, filepath.Join(outside, filepath.Join(components[index+1:]...)), checkPrivateSentinel) + } else { + checkWrite(t, target, checkPrivateSentinel) + } + checkSymlink(t, target, filepath.Join(root, filepath.Join(components[:index+1]...))) + before := checkTree(t, sandbox) + result, err := Check(context.Background(), root, document) + checkNoDisclosure(t, result, err, root, outside) + if err != nil || result.State() != "invalid" { + t.Fatal("symlink component was not invalid") + } + checkUnchanged(t, sandbox, before) + }) + } +} + +func TestCheckPortableAliasesAreOperationErrors(t *testing.T) { + document := checkTestDocument(t, "codex") + for index := range strings.Split(document.path, "/") { + components := strings.Split(document.path, "/") + components[index] = strings.ToUpper(components[index]) + t.Run(components[index], func(t *testing.T) { + root := t.TempDir() + checkWrite(t, filepath.Join(root, filepath.Join(components...)), checkPrivateSentinel) + before := checkTree(t, root) + result, err := Check(context.Background(), root, document) + checkWantError(t, result, err, root) + checkUnchanged(t, root, before) + }) + } +} + +type checkTestFile struct { + repositorytransaction.InspectionFile + read func([]byte) (int, error) + stat func() (fs.FileInfo, error) + close func() error +} + +func (file checkTestFile) Read(buffer []byte) (int, error) { + if file.read != nil { + return file.read(buffer) + } + return file.InspectionFile.Read(buffer) +} + +func (file checkTestFile) Stat() (fs.FileInfo, error) { + if file.stat != nil { + return file.stat() + } + return file.InspectionFile.Stat() +} + +func (file checkTestFile) Close() error { + if file.close != nil { + return file.close() + } + return file.InspectionFile.Close() +} + +func TestCheckOpenErrorsAreNotMissingOrInvalid(t *testing.T) { + document := checkTestDocument(t, "codex") + cases := []struct { + name string + err error + }{ + {"permission", fs.ErrPermission}, + {"IO", errors.New(checkPrivateSentinel)}, + {"route changed", repositorytransaction.ErrInspectionRouteChanged}, + {"alias", rootpath.ErrAmbiguousRoute}, + {"missing with cleanup failure", errors.Join(fs.ErrNotExist, rootpath.ErrTraversalCleanup)}, + {"unsafe with cleanup failure", errors.Join(repositorytransaction.ErrUnsafeInspectionRoute, repositorytransaction.ErrReadCleanup)}, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + root := t.TempDir() + dependencies := nativeCheckDependencies() + dependencies.openFile = func(*repositorytransaction.InspectionLease, string) (repositorytransaction.InspectionFile, error) { + return nil, &fs.PathError{Op: checkPrivateSentinel, Path: root, Err: test.err} + } + before := checkTree(t, root) + result, err := checkWithDependencies(context.Background(), root, document, dependencies) + checkWantError(t, result, err, root) + if strings.Contains(test.name, "cleanup failure") && !errors.Is(err, repositorytransaction.ErrReadCleanup) { + t.Fatal("route cleanup failure was not retained") + } + checkUnchanged(t, root, before) + }) + } + t.Run("wrapped absence", func(t *testing.T) { + root := t.TempDir() + dependencies := nativeCheckDependencies() + dependencies.openFile = func(*repositorytransaction.InspectionLease, string) (repositorytransaction.InspectionFile, error) { + return nil, &fs.PathError{Op: checkPrivateSentinel, Path: root, Err: fs.ErrNotExist} + } + result, err := checkWithDependencies(context.Background(), root, document, dependencies) + checkNoDisclosure(t, result, err, root) + if err != nil || result.State() != "missing" { + t.Fatal("ordinary absence was not missing") + } + }) +} + +func TestCheckPermissionDeniedIsNotMissing(t *testing.T) { + root := t.TempDir() + document := checkTestDocument(t, "codex") + selected := filepath.Join(root, document.path) + checkWrite(t, selected, checkPrivateSentinel) + before := checkTree(t, root) + if err := os.Chmod(selected, 0); err != nil { + t.Fatal("fixture permission change failed") + } + t.Cleanup(func() { _ = os.Chmod(selected, 0o600) }) + dependencies := nativeCheckDependencies() + probe, probeErr := os.Open(selected) + if probeErr == nil { + if err := probe.Close(); err != nil { + t.Fatal("permission probe cleanup failed") + } + // Privileged filesystems cannot supply a real denied-read operand. + dependencies.openFile = func(*repositorytransaction.InspectionLease, string) (repositorytransaction.InspectionFile, error) { + return nil, &fs.PathError{Op: "open", Path: selected, Err: fs.ErrPermission} + } + } else if !errors.Is(probeErr, fs.ErrPermission) { + t.Fatal("permission fixture failed for an unrelated reason") + } + result, err := checkWithDependencies(context.Background(), root, document, dependencies) + checkWantError(t, result, err, root, selected) + if err := os.Chmod(selected, 0o600); err != nil { + t.Fatal("fixture permission restoration failed") + } + checkUnchanged(t, root, before) +} + +func TestCheckDetectsChangesWithinAnObservation(t *testing.T) { + document := checkTestDocument(t, "codex") + for _, scenario := range []string{"size drift", "short read", "mode drift"} { + t.Run(scenario, func(t *testing.T) { + root := t.TempDir() + selected := filepath.Join(root, document.path) + checkWrite(t, selected, document.Content()) + dependencies := nativeCheckDependencies() + dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, error) { + file, err := lease.OpenExactRegularFile(path) + if err != nil { + return nil, err + } + changed := false + return checkTestFile{InspectionFile: file, read: func(buffer []byte) (int, error) { + if !changed { + changed = true + switch scenario { + case "size drift": + checkWrite(t, selected, document.Content()+"changed") + case "short read": + return 0, io.EOF + case "mode drift": + if err := os.Chmod(selected, 0o400); err != nil { + t.Fatal("fixture mode change failed") + } + } + } + return file.Read(buffer) + }}, nil + } + result, err := checkWithDependencies(context.Background(), root, document, dependencies) + checkWantError(t, result, err, root) + if !strings.Contains(err.Error(), "changed while reading") { + t.Fatal("intra-observation drift was not detected") + } + }) + } +} + +func TestCheckReobservesBytesStateAndOpenedIdentityIndependently(t *testing.T) { + document := checkTestDocument(t, "codex") + for _, scenario := range []string{"bytes only", "invalid bytes only", "oversized prefix only", "state only", "reverse state only", "file identity only"} { + t.Run(scenario, func(t *testing.T) { + root := t.TempDir() + selected := filepath.Join(root, document.path) + content := checkPrivateSentinel + "A" + if scenario == "invalid bytes only" { + content += "\x00" + } + if scenario == "oversized prefix only" { + content += strings.Repeat("x", maximumCheckBytes) + } + switch scenario { + case "state only": + checkMkdir(t, filepath.Dir(selected)) + case "reverse state only": + checkMkdir(t, selected) + default: + checkWrite(t, selected, content) + } + dependencies := nativeCheckDependencies() + opens := 0 + dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, error) { + opens++ + if opens == 2 { + switch scenario { + case "state only": + checkMkdir(t, selected) + case "reverse state only": + if err := os.Remove(selected); err != nil { + t.Fatal("fixture removal failed") + } + default: + before, err := os.Stat(selected) + if err != nil { + t.Fatal("fixture stat failed") + } + replacement := strings.Replace(content, "A", "B", 1) + if scenario == "file identity only" { + if err := os.Rename(selected, selected+".saved"); err != nil { + t.Fatal("fixture rename failed") + } + replacement = content + } + checkWrite(t, selected, replacement) + if err := os.Chtimes(selected, before.ModTime(), before.ModTime()); err != nil { + t.Fatal("fixture timestamp restoration failed") + } + after, err := os.Stat(selected) + if err != nil || before.Size() != after.Size() || before.Mode() != after.Mode() || !before.ModTime().Equal(after.ModTime()) { + t.Fatal("fixture did not preserve metadata") + } + if os.SameFile(before, after) == (scenario == "file identity only") { + t.Fatal("fixture did not isolate opened-file identity") + } + } + } + return lease.OpenExactRegularFile(path) + } + result, err := checkWithDependencies(context.Background(), root, document, dependencies) + checkWantError(t, result, err, root) + if opens != 2 { + t.Fatal("drift test did not reach exactly two observations") + } + }) + } +} + +func TestCheckVerifiesRootAfterBothObservations(t *testing.T) { + sandbox := t.TempDir() + root := filepath.Join(sandbox, "repository") + document := checkTestDocument(t, "codex") + checkWrite(t, filepath.Join(root, document.path), document.Content()) + dependencies := nativeCheckDependencies() + closes := 0 + dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, error) { + file, err := lease.OpenExactRegularFile(path) + if err != nil { + return nil, err + } + return checkTestFile{InspectionFile: file, close: func() error { + closeErr := file.Close() + closes++ + if closes == 2 { + if err := os.Rename(root, root+".saved"); err != nil { + t.Fatal("fixture root replacement failed") + } + checkMkdir(t, root) + } + return closeErr + }}, nil + } + result, err := checkWithDependencies(context.Background(), root, document, dependencies) + checkWantError(t, result, err, root) + if closes != 2 || !strings.Contains(err.Error(), "repository root changed") { + t.Fatal("root identity was not checked after both file observations") + } +} + +func TestCheckUsesOneLeaseAndStrictReadBounds(t *testing.T) { + root := t.TempDir() + document := checkTestDocument(t, "codex") + checkWrite(t, filepath.Join(root, document.path), strings.Repeat("a", maximumCheckBytes*2)) + dependencies := nativeCheckDependencies() + var pinned *repositorytransaction.InspectionLease + opens, closes, totalRead, leaseCloses := 0, 0, 0, 0 + dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, error) { + if pinned != nil && pinned != lease || path != document.path { + t.Fatal("check changed its root lease or selected path") + } + pinned = lease + opens++ + file, err := lease.OpenExactRegularFile(path) + if err != nil { + return nil, err + } + read := 0 + return checkTestFile{InspectionFile: file, read: func(buffer []byte) (int, error) { + if read+len(buffer) > maximumCheckBytes { + t.Fatal("read request exceeded the observation budget") + } + n, err := file.Read(buffer) + read += n + totalRead += n + return n, err + }, close: func() error { + closes++ + return file.Close() + }}, nil + } + dependencies.closeLease = func(lease *repositorytransaction.InspectionLease) error { + leaseCloses++ + if closes != 2 || lease != pinned { + t.Fatal("lease closed before its files") + } + return lease.Close() + } + result, err := checkWithDependencies(context.Background(), root, document, dependencies) + if err != nil || result.State() != "invalid" || opens != 2 || closes != 2 || leaseCloses != 1 || totalRead != maximumCheckBytes*2 { + t.Fatal("check violated bounded single-lease observation") + } +} + +func TestCheckCancellationBeforeAndAfterObservation(t *testing.T) { + document := checkTestDocument(t, "codex") + t.Run("before", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + result, err := Check(ctx, checkPrivateSentinel, document) + checkWantError(t, result, err) + if !errors.Is(err, context.Canceled) { + t.Fatal("pre-cancellation was not preserved before root admission") + } + }) + for _, state := range []string{"missing", "current", "stale", "invalid"} { + t.Run("late/"+state, func(t *testing.T) { + root := t.TempDir() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + checkStateFixture(t, root, document, state) + before := checkTree(t, root) + dependencies := nativeCheckDependencies() + dependencies.closeLease = func(lease *repositorytransaction.InspectionLease) error { + err := lease.Close() + cancel() + return err + } + result, err := checkWithDependencies(ctx, root, document, dependencies) + checkWantError(t, result, err, root) + if !errors.Is(err, context.Canceled) { + t.Fatal("late cancellation was not preserved") + } + checkUnchanged(t, root, before) + }) + } +} + +func checkStateFixture(t *testing.T, root string, document Document, state string) { + t.Helper() + switch state { + case "missing": + case "current": + checkWrite(t, filepath.Join(root, document.path), document.Content()) + case "stale": + checkWrite(t, filepath.Join(root, document.path), checkPrivateSentinel) + case "invalid": + checkMkdir(t, filepath.Join(root, document.path)) + default: + t.Fatal("unknown fixture state") + } +} + +func TestCheckCleanupFailureInvalidatesEveryState(t *testing.T) { + document := checkTestDocument(t, "codex") + for _, state := range []string{"missing", "current", "stale", "invalid"} { + t.Run(state, func(t *testing.T) { + root := t.TempDir() + checkStateFixture(t, root, document, state) + before := checkTree(t, root) + dependencies := nativeCheckDependencies() + dependencies.closeLease = func(lease *repositorytransaction.InspectionLease) error { + if err := lease.Close(); err != nil { + t.Fatal("real lease cleanup failed") + } + return errors.New(checkPrivateSentinel) + } + result, err := checkWithDependencies(context.Background(), root, document, dependencies) + checkWantError(t, result, err, root) + if !errors.Is(err, repositorytransaction.ErrReadCleanup) { + t.Fatal("lease cleanup failure was not retained") + } + checkUnchanged(t, root, before) + }) + } +} + +func TestCheckFileIOAndCleanupFailures(t *testing.T) { + document := checkTestDocument(t, "codex") + for _, scenario := range []string{"read", "first stat", "second stat", "first close", "second close", "read and close", "cancel and close"} { + t.Run(scenario, func(t *testing.T) { + root := t.TempDir() + checkWrite(t, filepath.Join(root, document.path), document.Content()) + before := checkTree(t, root) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + dependencies := nativeCheckDependencies() + opens, closes, leaseCloses := 0, 0, 0 + dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, error) { + opens++ + file, err := lease.OpenExactRegularFile(path) + if err != nil { + return nil, err + } + stats := 0 + return checkTestFile{InspectionFile: file, read: func(buffer []byte) (int, error) { + if scenario == "read" || scenario == "read and close" { + return 0, errors.New(checkPrivateSentinel) + } + return file.Read(buffer) + }, stat: func() (fs.FileInfo, error) { + stats++ + if scenario == "first stat" && stats == 1 || scenario == "second stat" && stats == 2 { + return nil, errors.New(checkPrivateSentinel) + } + return file.Stat() + }, close: func() error { + closes++ + if err := file.Close(); err != nil { + t.Fatal("real file cleanup failed") + } + if scenario == "cancel and close" { + cancel() + } + if scenario == "first close" || scenario == "second close" && opens == 2 || scenario == "read and close" || scenario == "cancel and close" { + return errors.New(checkPrivateSentinel) + } + return nil + }}, nil + } + dependencies.closeLease = func(lease *repositorytransaction.InspectionLease) error { + leaseCloses++ + return lease.Close() + } + result, err := checkWithDependencies(ctx, root, document, dependencies) + checkWantError(t, result, err, root) + if closes != opens || leaseCloses != 1 { + t.Fatal("operation error skipped resource cleanup") + } + if strings.Contains(scenario, "close") && !errors.Is(err, repositorytransaction.ErrReadCleanup) { + t.Fatal("file cleanup failure was not retained") + } + if scenario == "read and close" && !strings.Contains(err.Error(), "read selected file") { + t.Fatal("cleanup failure masked the read failure") + } + if scenario == "cancel and close" && !errors.Is(err, context.Canceled) { + t.Fatal("cleanup failure masked cancellation") + } + checkUnchanged(t, root, before) + }) + } +} diff --git a/internal/command/agentintegration/check_unix_test.go b/internal/command/agentintegration/check_unix_test.go new file mode 100644 index 0000000..21bed57 --- /dev/null +++ b/internal/command/agentintegration/check_unix_test.go @@ -0,0 +1,53 @@ +//go:build darwin || linux + +package agentintegration + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/repositorytransaction" + "golang.org/x/sys/unix" +) + +func TestCheckFIFONeverRead(t *testing.T) { + const helperEnvironment = "PROOFKIT_INTEGRATION_CHECK_FIFO_HELPER" + if os.Getenv(helperEnvironment) != "1" { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + command := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestCheckFIFONeverRead$") + command.Env = append(os.Environ(), helperEnvironment+"=1") + if err := command.Run(); err != nil || ctx.Err() != nil { + t.Fatal("FIFO check failed or blocked beyond its bounded subprocess lifetime") + } + return + } + root := t.TempDir() + document := checkTestDocument(t, "codex") + selected := filepath.Join(root, document.path) + checkMkdir(t, filepath.Dir(selected)) + if err := unix.Mkfifo(selected, 0o600); err != nil { + t.Fatal("FIFO fixture creation failed") + } + before := checkTree(t, root) + dependencies := nativeCheckDependencies() + opens := 0 + dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, error) { + opens++ + file, err := lease.OpenExactRegularFile(path) + if file != nil { + _ = file.Close() + t.Fatal("FIFO unexpectedly returned an opened regular file") + } + return file, err + } + result, err := checkWithDependencies(context.Background(), root, document, dependencies) + if err != nil || result.State() != "invalid" || opens != 2 { + t.Fatal("FIFO was not classified invalid by both non-reading observations") + } + checkUnchanged(t, root, before) +} diff --git a/internal/command/agentintegration/source.go b/internal/command/agentintegration/source.go new file mode 100644 index 0000000..ec6ad84 --- /dev/null +++ b/internal/command/agentintegration/source.go @@ -0,0 +1,189 @@ +// Package agentintegration owns portable CLI bootstrap materialization and +// read-only freshness. It does not own host activation or installation. +package agentintegration + +import ( + "bytes" + "fmt" + "slices" + "text/template" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/admit" + "github.com/research-engineering/agentic-proofkit/internal/kernel/commandroute" + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" +) + +const ( + MaximumMetadataBytes = 512 + MaximumBodyBytes = 4096 +) + +type toolDescriptor struct { + name string + path string +} + +var tools = []toolDescriptor{ + {name: "claude", path: ".claude/skills/agentic-proofkit/SKILL.md"}, + {name: "codex", path: ".agents/skills/agentic-proofkit/SKILL.md"}, +} + +var consumedCommands = []string{ + "adopt-plan", "agent-route", "change-workflow-plan", "help", + "native-evidence-guidance", "next", "status", +} + +// Capability binds an existing public invocation contract, not a new schema or +// a claim of complete transitive implementation equivalence. +type Capability struct { + Command string + Route []string + ContractDigest string +} + +type Document struct { + tool string + path string + content string + identity string + contentDigest string + capabilityDigest string + metadataBytes int + bodyBytes int +} + +func Tools() []string { + result := make([]string, len(tools)) + for index, descriptor := range tools { + result[index] = descriptor.name + } + return result +} + +func ConsumedCommands() []string { return slices.Clone(consumedCommands) } + +func Source(tool string, capabilities []Capability) (Document, error) { + var selected toolDescriptor + for _, descriptor := range tools { + if descriptor.name == tool { + selected = descriptor + } + } + if selected.name == "" { + return Document{}, fmt.Errorf("integration tool must be claude or codex") + } + if len(capabilities) != len(consumedCommands) { + return Document{}, fmt.Errorf("integration requires its exact consumed command set") + } + routes := map[string]string{} + records := make([]any, len(consumedCommands)) + for index, command := range consumedCommands { + capability := capabilities[index] + _, digestErr := admit.SHA256Ref(capability.ContractDigest, "integration consumed contract digest") + if capability.Command != command || !commandroute.Valid(capability.Route) || digestErr != nil { + return Document{}, fmt.Errorf("integration consumed command identity is invalid") + } + routes[command] = commandroute.Text(capability.Route) + route := make([]any, len(capability.Route)) + for index, token := range capability.Route { + route[index] = token + } + records[index] = map[string]any{"command": command, "route": route, "contractDigest": capability.ContractDigest} + } + capabilityDigest, err := digest.StableJSONSHA256Ref(records) + if err != nil { + return Document{}, err + } + identity, err := digest.StableJSONSHA256Ref(map[string]any{ + "schemaVersion": 1, "tool": selected.name, "targetPath": selected.path, + "templateDigest": digest.SHA256TextRef(frontmatter + bootstrap), "capabilityDigest": capabilityDigest, + }) + if err != nil { + return Document{}, err + } + routes["identity"] = identity + parsed, err := template.New("bootstrap").Option("missingkey=error").Parse(bootstrap) + if err != nil { + return Document{}, fmt.Errorf("integration bootstrap template is invalid") + } + var body bytes.Buffer + if err := parsed.Execute(&body, routes); err != nil { + return Document{}, fmt.Errorf("integration bootstrap rendering failed") + } + if len(frontmatter) > MaximumMetadataBytes || body.Len() > MaximumBodyBytes { + return Document{}, fmt.Errorf("integration bootstrap exceeds its byte budget") + } + content := frontmatter + body.String() + return Document{ + tool: selected.name, path: selected.path, content: content, identity: identity, + contentDigest: digest.SHA256TextRef(content), capabilityDigest: capabilityDigest, + metadataBytes: len(frontmatter), bodyBytes: body.Len(), + }, nil +} + +func (document Document) Content() string { return document.content } + +func (document Document) JSONValue() map[string]any { + return map[string]any{ + "schemaVersion": 1, "kind": "proofkit.integration-source.v1", + "tool": document.tool, "targetPath": document.path, "integrationId": document.identity, + "content": document.content, "contentDigest": document.contentDigest, + "capabilityDigest": document.capabilityDigest, + "metadataBytes": document.metadataBytes, "bodyBytes": document.bodyBytes, + "nonClaims": []any{ + "Generation does not install instructions, activate a host skill, or authorize execution.", + "The identity binds materialization and consumed registered contracts, not every transitive runtime behavior.", + }, + } +} + +const frontmatter = `--- +name: agentic-proofkit +description: "Use when repository authority requests Proofkit-governed specification, change planning, evidence, or closeout work. Route to the installed CLI; do not infer governance from file presence alone." +--- + +` + +const bootstrap = `# Proofkit Workflow + + + +## Authority And Launcher + +Read the nearest repository instructions and the selected task owner. This +bootstrap routes work; it does not replace repository policy or approve actions. +Use the repository-approved, already-installed Proofkit launcher. Resolve its +concrete executable for this session only. If absent or ambiguous, ask the owner; +do not install, download, select a package manager, or guess a launcher. +Below, command routes are arguments to that approved launcher, not shell scripts. + +## Start With One Next Action + +1. Run {{index . "status"}} --repo-root for the bounded current state. +2. Run {{index . "next"}} --repo-root for the next owner-defined action. +3. Read only the references needed for that action. Use {{index . "help"}} + for exact invocation flags. Do not reconstruct a state table from this file. +4. Obtain missing inputs or authorization from the repository owner. A plan or + a recovery suggestion is not permission to write files or execute witnesses. + +## Plan And Request Detail + +Use {{index . "adopt-plan"}} with an explicit owner-selected mode for initial +adoption; do not silently treat existing code as intended behavior. For changes, +use {{index . "change-workflow-plan"}} with admitted caller-owned input. +Use {{index . "agent-route"}} --agent-envelope --agent-envelope-mode brief for +bounded routing after supplying its required input. Follow returned detailAccess +only when a selected action needs detail. Do not cache schemas or fabricate file +references, selectors, proof results, or a missing prerequisite. + +## Repository-Specific Evidence + +Request {{index . "native-evidence-guidance"}} when creating or assessing local +checks. Bind each promised invariant to its repository owner, an independently +falsifying oracle, an executable native witness, and its environment. Generate +repo-specific scripts only under that owner's rules; this bootstrap contains no +consumer policy or test implementation. Run authorized native checks and retain +their actual outcomes. Admitted metadata and generated reports do not prove +execution, complete coverage, merge approval, publication, or production readiness. +Stop on failed, unknown, or stale evidence instead of promoting it to success. +` diff --git a/internal/command/agentintegration/source_test.go b/internal/command/agentintegration/source_test.go new file mode 100644 index 0000000..98a9e42 --- /dev/null +++ b/internal/command/agentintegration/source_test.go @@ -0,0 +1,98 @@ +package agentintegration + +import ( + "slices" + "strings" + "testing" + + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" +) + +func sourceCapabilities() []Capability { + result := make([]Capability, len(consumedCommands)) + for index, command := range consumedCommands { + route := []string{command} + if command == "change-workflow-plan" { + route = []string{"change", "plan"} + } + if command == "adopt-plan" { + route = []string{"adopt", "plan"} + } + result[index] = Capability{Command: command, Route: route, ContractDigest: digest.SHA256TextRef(command)} + } + return result +} + +func TestSourceBindsPortableConsumedContracts(t *testing.T) { + for _, tool := range Tools() { + t.Run(tool, func(t *testing.T) { + capabilities := sourceCapabilities() + document, err := Source(tool, capabilities) + if err != nil { + t.Fatal(err) + } + again, err := Source(tool, capabilities) + if err != nil || document != again { + t.Fatal("identical generation inputs changed the document") + } + wantPath := ".agents/skills/agentic-proofkit/SKILL.md" + if tool == "claude" { + wantPath = ".claude/skills/agentic-proofkit/SKILL.md" + } + if document.path != wantPath || document.metadataBytes != len(frontmatter) || document.bodyBytes != len(document.content)-len(frontmatter) { + t.Fatal("descriptor path or measured budgets differ") + } + if document.metadataBytes > MaximumMetadataBytes || document.bodyBytes > MaximumBodyBytes || document.contentDigest != digest.SHA256TextRef(document.content) { + t.Fatal("generated bytes violate budget or identity") + } + for _, fragment := range []string{"name: agentic-proofkit", "change plan", "adopt plan", "--agent-envelope-mode brief", "detailAccess", "native-evidence-guidance", document.identity} { + if !strings.Contains(document.content, fragment) { + t.Fatalf("bootstrap is missing owner fragment %q", fragment) + } + } + for index := range capabilities { + for _, operand := range []string{"route", "contract"} { + t.Run(capabilities[index].Command+"/"+operand, func(t *testing.T) { + changed := sourceCapabilities() + if operand == "route" { + changed[index].Route = []string{"replacement", "route"} + } else { + changed[index].ContractDigest = digest.SHA256TextRef("changed contract") + } + updated, err := Source(tool, changed) + if err != nil || updated.identity == document.identity || updated.content == document.content { + t.Fatalf("consumed %s change did not invalidate generated bytes: %v", operand, err) + } + }) + } + } + capabilities[0].Route[0] = "mutated" + if document != again { + t.Fatal("caller mutation changed an already rendered document") + } + }) + } +} + +func TestSourceRejectsUnboundCapabilities(t *testing.T) { + for _, mutate := range []func([]Capability) []Capability{ + func(values []Capability) []Capability { return values[1:] }, + func(values []Capability) []Capability { return append(values, values[0]) }, + func(values []Capability) []Capability { values[1] = values[0]; return values }, + func(values []Capability) []Capability { slices.Reverse(values); return values }, + func(values []Capability) []Capability { values[0].Route = []string{"not a route"}; return values }, + func(values []Capability) []Capability { values[0].ContractDigest = "invalid"; return values }, + } { + if _, err := Source("codex", mutate(sourceCapabilities())); err == nil { + t.Fatal("invalid consumed capability set was accepted") + } + } + if _, err := Source("", sourceCapabilities()); err == nil { + t.Fatal("implicit tool selection was accepted") + } + toolNames, commands := Tools(), ConsumedCommands() + toolNames[0], commands[0] = "modified", "modified" + if Tools()[0] == "modified" || ConsumedCommands()[0] == "modified" { + t.Fatal("descriptor projection exposes mutable owner state") + } +} diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go index 7f81698..e10591b 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 = "4a409dd87ab13fa3f3951c16438f1d0f1595cc0ca2f6a1e1317c5c0e2fc9801e" +const presetContractSourceSHA256 = "9d8a9843ac240b524eeb5c27f4ca8833b411d410d36c6841e14134e1ce8a57d0" 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/required_inventory.go b/internal/tools/coveragemetrics/required_inventory.go index dfeed05..cf7e3af 100644 --- a/internal/tools/coveragemetrics/required_inventory.go +++ b/internal/tools/coveragemetrics/required_inventory.go @@ -191,6 +191,69 @@ func requiredBindingWitnessInventory() map[inventoryKey]requiredInventoryEntry { witnessPath: "internal/app/project_navigation_version_edge_test.go", selectors: []string{"TestProjectNavigationVersionEdgeClosesPublicRoutes"}, }, + {"REQ-PROOFKIT-WORKFLOW-018", "proofkit.agent-workflow.integration-installed-carriers"}: { + commandIDs: []string{"proofkit.go-test", "proofkit.package-artifact"}, + environmentClasses: []string{"local-go", "local-go-python"}, + witnessPath: "internal/tools/workflowsmoke/workflow_smoke_test.go", + selectors: []string{"TestVerifyAcceptsApplicationCLI", "TestVerifyRejectsCarrierContractMutations"}, + }, + {"REQ-PROOFKIT-WORKFLOW-016", "proofkit.agent-workflow.integration-capability-and-carrier-identity"}: { + witnessPath: "internal/app/agent_integration_command_test.go", + selectors: []string{"TestIntegrationCapabilityIdentityScope", "TestIntegrationSourcesAreCarrierIndependent"}, + }, + {"REQ-PROOFKIT-WORKFLOW-016", "proofkit.agent-workflow.integration-portable-source"}: { + witnessPath: "internal/command/agentintegration/source_test.go", + selectors: []string{"TestSourceBindsPortableConsumedContracts", "TestSourceRejectsUnboundCapabilities"}, + }, + {"REQ-PROOFKIT-WORKFLOW-017", "proofkit.agent-workflow.integration-bounded-read-only-classification"}: { + witnessPath: "internal/command/agentintegration/check_test.go", + selectors: []string{ + "TestCheckInvalidRootsAreOperationErrors", "TestCheckPortableAliasesAreOperationErrors", "TestCheckProjectionHasOnlyAdmittedFields", + "TestCheckRejectsEverySymlinkComponentWithoutFollowing", "TestCheckRejectsZeroDocumentBeforeInspection", + "TestCheckStatesAreReadOnlyAndPrivate", "TestCheckUsesOneLeaseAndStrictReadBounds", + }, + }, + {"REQ-PROOFKIT-WORKFLOW-017", "proofkit.agent-workflow.integration-fifo-denial"}: { + witnessPath: "internal/command/agentintegration/check_unix_test.go", + selectors: []string{"TestCheckFIFONeverRead"}, + }, + {"REQ-PROOFKIT-WORKFLOW-017", "proofkit.agent-workflow.integration-operational-failure-and-cohort"}: { + witnessPath: "internal/command/agentintegration/check_test.go", + selectors: []string{ + "TestCheckCancellationBeforeAndAfterObservation", "TestCheckCleanupFailureInvalidatesEveryState", "TestCheckDetectsChangesWithinAnObservation", + "TestCheckFileIOAndCleanupFailures", "TestCheckOpenErrorsAreNotMissingOrInvalid", "TestCheckPermissionDeniedIsNotMissing", + "TestCheckReobservesBytesStateAndOpenedIdentityIndependently", "TestCheckVerifiesRootAfterBothObservations", + }, + }, + {"REQ-PROOFKIT-WORKFLOW-017", "proofkit.agent-workflow.integration-pre-io-cli-admission"}: { + witnessPath: "internal/app/agent_integration_command_test.go", + selectors: []string{"TestIntegrationCheckCLI"}, + }, + {"REQ-PROOFKIT-WORKFLOW-018", "proofkit.agent-workflow.integration-contract-and-route-closure"}: { + commandIDs: []string{"proofkit.command-contract-check", "proofkit.go-test"}, + witnessPath: "internal/app/cli_contract_test.go", + selectors: []string{"TestCLIContractMatchesDispatcherAndHelp", "TestCLIContractsAreCompleteGeneratedAndWitnessBound", "TestContractMapDecisionTreeHasThreeCells"}, + }, + {"REQ-PROOFKIT-WORKFLOW-018", "proofkit.agent-workflow.integration-public-cli"}: { + witnessPath: "internal/app/agent_integration_command_test.go", + selectors: []string{"TestIntegrationCancellationBeforeOutput", "TestIntegrationCheckCLI", "TestIntegrationCommandsExactRootShapes", "TestIntegrationSourceCLI"}, + }, + {"REQ-PROOFKIT-WORKFLOW-018", "proofkit.agent-workflow.integration-installed-npm-carrier-closure"}: { + witnessPath: "internal/tools/packageverify/workflow_carrier_test.go", + selectors: []string{"TestInstalledNPMWorkflowCarrierClosure"}, + }, + {"REQ-PROOFKIT-WORKFLOW-018", "proofkit.agent-workflow.integration-installed-wheel-carrier-closure"}: { + witnessPath: "internal/tools/pythonpackage/workflow_carrier_test.go", + selectors: []string{"TestInstalledPythonWorkflowCarrierClosure"}, + }, + {"REQ-PROOFKIT-WORKFLOW-018", "proofkit.agent-workflow.integration-version-edge"}: { + witnessPath: "internal/app/integration_version_edge_test.go", + selectors: []string{"TestIntegrationVersionEdgeClosesCompletePublicABIDiff"}, + }, + {"REQ-PROOFKIT-WORKFLOW-018", "proofkit.agent-workflow.integration-public-abi-mutations"}: { + witnessPath: "internal/app/public_abi_mutation_test.go", + selectors: []string{"TestIntegrationVersionEdgeRejectsUndeclaredPublicABIDrift"}, + }, {"REQ-PROOFKIT-PACKAGE-001", "proofkit.package-boundary.root-export-and-deep-import-denial"}: { witnessPath: "internal/tools/packageverify/main_test.go", selectors: []string{"TestVerifyRootPackageRejectsEachForbiddenRootEntry"}, diff --git a/internal/tools/releasechange/record_test.go b/internal/tools/releasechange/record_test.go index 7c1a40c..79a6c25 100644 --- a/internal/tools/releasechange/record_test.go +++ b/internal/tools/releasechange/record_test.go @@ -194,20 +194,14 @@ 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{ - {ChangeID: "proofkit.agent-workflow.change-plan-route", Summary: "Replace the flat change-workflow-plan CLI route with the hierarchical change plan route while preserving one internal command implementation and its input and output contracts."}, - {ChangeID: "proofkit.cli-contract.omitted-route-policy", Summary: "Make the command-id fallback for an omitted command route an explicit required CLI-contract grammar field; Proofkit source and installed-carrier validators reject contracts that omit or alter this policy."}, -} +var currentBreakingChanges = []Change{} var currentAdditions = []Change{ - {ChangeID: "proofkit.project-state.next-action", Summary: "Add a bounded next command that maps each admitted structural project state to exactly one non-authoritative repository action."}, - {ChangeID: "proofkit.project-state.status", Summary: "Add a read-only status command that classifies a bounded normalized materialized-project and transaction observation; admitted in-bound records bind exact content digests, while unread out-of-bound records identify only their invalid class, without claiming native verification or workflow completion."}, + {ChangeID: "proofkit.agent-integration.freshness", Summary: "Add integration check for explicit Codex or Claude repository paths; compare bounded current template bytes through confined read-only inspection without granting installation or host-activation authority."}, + {ChangeID: "proofkit.agent-integration.source", Summary: "Add integration source to generate one bounded portable skill body and descriptor-owned path, with an identity bound to consumed registered CLI invocation contracts rather than package version or a host-specific executable."}, } -var currentMigrationSteps = []string{ - "Replace agentic-proofkit change-workflow-plan invocations with agentic-proofkit change plan; input and output JSON contracts are unchanged.", - "Update CLI-contract consumers to require commandRouteGrammar.omittedRoutePolicy=command_id; commands without an explicit route continue to resolve to their stable command ID.", -} +var currentMigrationSteps = []string{} func validateCurrentChangeRecord(record Record, notes string) error { if !slices.Equal(record.BreakingChanges, currentBreakingChanges) { @@ -227,7 +221,7 @@ func validateCurrentChangeRecord(record Record, notes string) error { func currentExpectedReleaseNotes() string { lines := []string{ - "# @research-engineering/agentic-proofkit 0.9.0", + "# @research-engineering/agentic-proofkit 0.10.0", "", "## Breaking Contract Changes", "", @@ -249,8 +243,7 @@ func currentExpectedReleaseNotes() string { "", "## Migration", "", - "Migration is required:", - "", + "No consumer migration is required.", ) for _, step := range currentMigrationSteps { lines = append(lines, "- "+step) @@ -271,13 +264,14 @@ func currentExpectedReleaseNotes() string { "- Project status and next classify materialized repository structure only; they do not execute native verification, validate receipt currentness or trust, or declare workflow completion.", "- 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.", + "- Integration source and check do not install, update, remove, activate, or execute a host skill. Managed lifecycle and observed host activation remain separate open work.", "", "## Install", "", "Primary npm channel:", "", "```bash", - "npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.9.0", + "npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.10.0", "```", "", "Pre-1.0 npm consumers must keep this dependency exact-pinned.", @@ -288,7 +282,7 @@ func currentExpectedReleaseNotes() string { "", "## Rollback", "", - "- Pin npm consumers to the previous admitted version 0.8.0 with `npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.8.0`.", + "- Pin npm consumers to the previous admitted version 0.9.0 with `npm install --save-dev --save-exact @research-engineering/agentic-proofkit@0.9.0`.", "- Treat local package artifacts as candidates until registry identity is proven.", ) return strings.Join(lines, "\n") + "\n" diff --git a/internal/tools/workflowsmoke/integration_smoke.go b/internal/tools/workflowsmoke/integration_smoke.go new file mode 100644 index 0000000..8f49691 --- /dev/null +++ b/internal/tools/workflowsmoke/integration_smoke.go @@ -0,0 +1,145 @@ +package workflowsmoke + +import ( + "bytes" + "context" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "reflect" + + "github.com/research-engineering/agentic-proofkit/internal/app" + "github.com/research-engineering/agentic-proofkit/internal/command/agentintegration" + "github.com/research-engineering/agentic-proofkit/internal/kernel/admission" + "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" +) + +// The in-process reference binds carrier equivalence to the current app. The +// separate native owner tests prove content and filesystem semantics; this smoke +// does not promote equivalence to independent proof of those semantics. +func integrationReference(ctx context.Context, args ...string) Result { + var stdout, stderr bytes.Buffer + code := app.Run(ctx, args, integrationUnreadReader{}, &stdout, &stderr) + return Result{ExitCode: code, Stdout: stdout.Bytes(), Stderr: stderr.Bytes()} +} + +type integrationUnreadReader struct{} + +func (integrationUnreadReader) Read([]byte) (int, error) { + return 0, fmt.Errorf("integration must not read stdin") +} + +func verifyIntegrations(ctx context.Context, run Runner) (returnErr error) { + root, err := os.MkdirTemp("", "proofkit-integration-smoke-") + if err != nil { + return fmt.Errorf("create isolated integration repository") + } + defer func() { returnErr = errors.Join(returnErr, os.RemoveAll(root)) }() + for _, tool := range agentintegration.Tools() { + var source map[string]any + for _, format := range []string{"json", "text"} { + args := []string{"integration", "source", "--tool", tool, "--format", format} + expected := integrationReference(ctx, args...) + actual, err := invoke(ctx, run, "integration source "+format, unreadInvocation(args...)) + if err != nil { + return err + } + if expected.ExitCode != 0 || len(expected.Stderr) != 0 || !bytes.Equal(actual.Stdout, expected.Stdout) { + return fmt.Errorf("installed integration source differs from the current app") + } + if format == "json" { + value, err := admission.DecodeJSON(bytes.NewReader(actual.Stdout), defaultMaximumStdoutBytes) + if err != nil { + return err + } + var ok bool + source, ok = value.(map[string]any) + if !ok { + return fmt.Errorf("integration source must be an object") + } + } + } + if err := verifyIntegrationStates(ctx, run, root, tool, source); err != nil { + return err + } + } + return verifyFailure(ctx, run, "integration explicit tool", unreadInvocation("integration", "source"), "requires --tool") +} + +func verifyIntegrationStates(ctx context.Context, run Runner, root, tool string, source map[string]any) error { + content, contentOK := source["content"].(string) + relative, pathOK := source["targetPath"].(string) + if !contentOK || !pathOK || !filepath.IsLocal(relative) { + return fmt.Errorf("integration source lacks its exact file projection") + } + path := filepath.Join(root, filepath.FromSlash(relative)) + for _, state := range []string{"missing", "current", "stale", "invalid"} { + if state != "missing" { + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + return err + } + value := content + if state == "stale" { + value += "local edit\n" + } + if state == "invalid" { + value = "\x00" + } + if err := os.WriteFile(path, []byte(value), 0o600); err != nil { + return err + } + } + before, err := integrationTree(root) + if err != nil { + return err + } + for _, format := range []string{"json", "text"} { + args := []string{"integration", "check", "--tool", tool, "--repo-root", root, "--format", format} + expected := integrationReference(ctx, args...) + invocationContext, cancel := context.WithTimeout(ctx, invocationTimeout) + actual, err := run(invocationContext, unreadInvocation(args...)) + cancel() + if err != nil { + return err + } + wantCode := 2 + if state == "current" { + wantCode = 0 + } + if expected.ExitCode != wantCode || actual.ExitCode != wantCode || len(expected.Stderr) != 0 || len(actual.Stderr) != 0 || !bytes.Equal(expected.Stdout, actual.Stdout) { + return fmt.Errorf("installed integration check differs for %s/%s", state, format) + } + } + after, err := integrationTree(root) + if err != nil || !reflect.DeepEqual(before, after) { + return fmt.Errorf("installed integration check changed repository entries") + } + } + return nil +} + +func integrationTree(root string) (map[string]string, error) { + entries := map[string]string{} + err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + info, err := entry.Info() + if err != nil { + return err + } + value := info.Mode().String() + if info.Mode().IsRegular() { + content, err := os.ReadFile(path) + if err != nil { + return err + } + value += ":" + digest.SHA256BytesRef(content) + } + entries[path] = value + return nil + }) + return entries, err +} diff --git a/internal/tools/workflowsmoke/workflow_smoke.go b/internal/tools/workflowsmoke/workflow_smoke.go index 5d5d837..41fd488 100644 --- a/internal/tools/workflowsmoke/workflow_smoke.go +++ b/internal/tools/workflowsmoke/workflow_smoke.go @@ -154,7 +154,10 @@ func Verify(ctx context.Context, run Runner) error { return fmt.Errorf("no-input guidance text does not equal the command-owned plain-text projection") } - return verifyProjectNavigation(ctx, run) + if err := verifyProjectNavigation(ctx, run); err != nil { + return err + } + return verifyIntegrations(ctx, run) } func bytesInvocation(input []byte, args ...string) Invocation { diff --git a/internal/tools/workflowsmoke/workflow_smoke_test.go b/internal/tools/workflowsmoke/workflow_smoke_test.go index 0e158bf..0b16a5a 100644 --- a/internal/tools/workflowsmoke/workflow_smoke_test.go +++ b/internal/tools/workflowsmoke/workflow_smoke_test.go @@ -34,6 +34,12 @@ func TestVerifyRejectsCarrierContractMutations(t *testing.T) { materializedOnly bool apply func(workflowsmoke.Result) workflowsmoke.Result }{ + {name: "integration source identity", match: "integration source --tool codex --format json", apply: replaceStdout(`{"kind":"wrong"}`)}, + {name: "integration source text suffix", match: "integration source --tool claude --format text", apply: appendStdout("surplus\n")}, + {name: "integration missing promoted", match: "integration check --tool codex --repo-root ", matchPrefix: true, apply: func(result workflowsmoke.Result) workflowsmoke.Result { + result.ExitCode = 0 + return result + }}, {name: "retired planner route", match: "change-workflow-plan --input -", apply: func(result workflowsmoke.Result) workflowsmoke.Result { return workflowsmoke.Result{ExitCode: 0, Stdout: []byte("{}\n")} }}, diff --git a/package-lock.json b/package-lock.json index 50e81dc..84a980f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@research-engineering/agentic-proofkit", - "version": "0.9.0", + "version": "0.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@research-engineering/agentic-proofkit", - "version": "0.9.0", + "version": "0.10.0", "cpu": [ "arm64", "x64" diff --git a/package.json b/package.json index bad6182..e052639 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@research-engineering/agentic-proofkit", "description": "Reusable proof profile, report, graph, and witness-planning primitives.", - "version": "0.9.0", + "version": "0.10.0", "type": "module", "license": "MIT", "sideEffects": false, diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index df37fab..0324686 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -5,8 +5,8 @@ "processContract": { "successExitCode": 0, "failureExitCode": 1, - "stdout": "Successful JSON commands write exactly one JSON value to stdout unless an admitted explicit output-file flag writes that JSON value to the selected path and leaves stdout empty. Successful text or help commands write text to stdout unless an admitted explicit output-file flag writes the selected representation to the selected path and leaves stdout empty. Commands do not write stderr on success.", - "stderr": "Argument parsing, unsupported command, input admission, and output serialization failures write human diagnostics to stderr. Failed report commands may still write a machine-readable failed report to stdout when admission succeeds. Opt-in agent-envelope repair packets may write deterministic invalid-input JSON to stdout with exit code 1 and empty stderr.", + "stdout": "Successful JSON commands write exactly one JSON value to stdout unless an admitted explicit output-file flag writes that JSON value to the selected path and leaves stdout empty. Successful text or help commands write text to stdout unless an admitted explicit output-file flag writes the selected representation to the selected path and leaves stdout empty. Commands do not write stderr on success. Integration check also emits exactly one classified JSON value or text report with exit 2 for missing, stale, or invalid; current exits 0, and operational errors exit 1.", + "stderr": "Argument parsing, unsupported command, input admission, and output serialization failures write human diagnostics to stderr. Failed report commands may still write a machine-readable failed report to stdout when admission succeeds. Opt-in agent-envelope repair packets may write deterministic invalid-input JSON to stdout with exit code 1 and empty stderr. Integration check classified exit-2 reports leave stderr empty; operational or invocation errors use non-disclosing stderr diagnostics.", "commandRouteGrammar": { "minimumTokens": 1, "maximumTokens": 4, @@ -143,7 +143,7 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:7fa037cfe6147827468954667d103b27796ff43c1d1f74d81be1f545d7534a8e", + "canonicalDigest": "sha256:8cf495d309f3447cc7743e8c2e9ff303e5f4bb78d69eeff27fd48d37fa47d605", "evidenceClass": "source_checkout" }, { @@ -281,7 +281,7 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:7fa037cfe6147827468954667d103b27796ff43c1d1f74d81be1f545d7534a8e", + "canonicalDigest": "sha256:8cf495d309f3447cc7743e8c2e9ff303e5f4bb78d69eeff27fd48d37fa47d605", "evidenceClass": "source_checkout" }, { @@ -395,7 +395,7 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:7fa037cfe6147827468954667d103b27796ff43c1d1f74d81be1f545d7534a8e", + "canonicalDigest": "sha256:8cf495d309f3447cc7743e8c2e9ff303e5f4bb78d69eeff27fd48d37fa47d605", "evidenceClass": "source_checkout" }, { @@ -1186,7 +1186,7 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:7fa037cfe6147827468954667d103b27796ff43c1d1f74d81be1f545d7534a8e", + "canonicalDigest": "sha256:8cf495d309f3447cc7743e8c2e9ff303e5f4bb78d69eeff27fd48d37fa47d605", "evidenceClass": "source_checkout" }, { @@ -2425,6 +2425,196 @@ ] } }, + { + "command": "integration-check", + "route": [ + "integration", + "check" + ], + "input": "none", + "stdin": false, + "inputPointer": false, + "scopeClass": "explicit_filesystem_scan", + "outputModes": [ + "json", + "text" + ], + "allowedFlags": [ + "--format", + "--repo-root", + "--tool" + ], + "requiredFlags": [ + "--repo-root", + "--tool" + ], + "singleOccurrenceFlags": [ + "--format", + "--repo-root", + "--tool" + ], + "flagChoices": { + "--format": [ + "json", + "text" + ], + "--tool": [ + "claude", + "codex" + ] + }, + "outputContract": { + "contractId": "proofkit.integration-check.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.integration-check.output.v1.root-shape", + "rootDefinitionDigest": "sha256:309c5f43b8d5596d88859819ed59e4486926d75d5f41d2b2b6e252389f5913ca", + "nativeSources": [ + { + "path": "internal/app", + "canonicalDigest": "sha256:8cf495d309f3447cc7743e8c2e9ff303e5f4bb78d69eeff27fd48d37fa47d605", + "evidenceClass": "source_checkout" + }, + { + "path": "internal/command/agentintegration", + "canonicalDigest": "sha256:0e6f808d4fa7515383915426cf9678ee88f916c6baf77006cc9a500fa61fdbec", + "evidenceClass": "source_checkout" + }, + { + "path": "internal/kernel/repositorytransaction", + "canonicalDigest": "sha256:fd549a1f5795524d4f7feed3a4ebcef03e3d6fe53d6ce7e8e24ff5014de9a9cd", + "evidenceClass": "source_checkout" + }, + { + "path": "internal/kernel/rootpath", + "canonicalDigest": "sha256:160244fcf85783eb77d19ccbb9c7bf80de38bbb190fcc5b9358f3d5701844607", + "evidenceClass": "source_checkout" + } + ], + "nativeOutputWitnessSelector": { + "path": "internal/app/agent_integration_command_test.go", + "test": "TestIntegrationCommandsExactRootShapes", + "command": "go test ./internal/app -run '^TestIntegrationCommandsExactRootShapes$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "kind=proofkit.integration-check.v1; tool=claude|codex; targetPath is the fixed selected descriptor-owned repository-relative path", + "state=missing|current|stale|invalid; expectedContentDigest identifies generated bytes, never observed file bytes", + "JSON is the default; text is an ANSI-free classification projection; no input, output-file, color, carrier, or install option", + "exit 0 only for current; exit 2 for missing/stale/invalid with a classified report and empty stderr; exit 1 for invocation, operational, cancellation, cleanup, serialization, or output transport error", + "two read-only confined observations of at most 8192 bytes each; no observed content, observed digest, or caller root paths in output or diagnostics", + "current means exact generated-byte freshness only, not installation, host activation, permission grant, or semantic full proof", + "root-shape-only definition proofkit.integration-check.output.v1.root-shape; nested fields, leaf types, cardinalities, and semantic validity remain native-owner claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-WORKFLOW-016", + "REQ-PROOFKIT-WORKFLOW-017", + "REQ-PROOFKIT-WORKFLOW-018" + ], + "flagChoices": { + "--format": [ + "json", + "text" + ], + "--tool": [ + "claude", + "codex" + ] + } + } + }, + { + "command": "integration-source", + "route": [ + "integration", + "source" + ], + "input": "none", + "stdin": false, + "inputPointer": false, + "scopeClass": "built_in_package_catalog", + "outputModes": [ + "json", + "text" + ], + "allowedFlags": [ + "--format", + "--tool" + ], + "requiredFlags": [ + "--tool" + ], + "singleOccurrenceFlags": [ + "--format", + "--tool" + ], + "flagChoices": { + "--format": [ + "json", + "text" + ], + "--tool": [ + "claude", + "codex" + ] + }, + "outputContract": { + "contractId": "proofkit.integration-source.output.v1", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "rootDefinitionRef": "proofkit.integration-source.output.v1.root-shape", + "rootDefinitionDigest": "sha256:f7437f3154a39e0bb0b2b1329c626401140ee93b7f58ae23de63905cdcf30ec5", + "nativeSources": [ + { + "path": "internal/app", + "canonicalDigest": "sha256:8cf495d309f3447cc7743e8c2e9ff303e5f4bb78d69eeff27fd48d37fa47d605", + "evidenceClass": "source_checkout" + }, + { + "path": "internal/command/agentintegration", + "canonicalDigest": "sha256:0e6f808d4fa7515383915426cf9678ee88f916c6baf77006cc9a500fa61fdbec", + "evidenceClass": "source_checkout" + } + ], + "nativeOutputWitnessSelector": { + "path": "internal/app/agent_integration_command_test.go", + "test": "TestIntegrationCommandsExactRootShapes", + "command": "go test ./internal/app -run '^TestIntegrationCommandsExactRootShapes$'", + "evidenceClass": "source_checkout" + }, + "compatibilitySummary": [ + "schemaVersion=1", + "kind=proofkit.integration-source.v1; tool=claude|codex; targetPath is the fixed selected descriptor-owned repository-relative path", + "JSON is the default; --format text is byte-identical to content and adds no wrapper, newline, or styling", + "metadataBytes<=512 and bodyBytes<=4096 count UTF-8 bytes separately; contentDigest binds exact content", + "integrationId binds the template, tool/path descriptor, and capabilityDigest over exactly consumed registered invocation contracts; package version and process-local launcher are not persisted", + "pure generation performs no repository read or application write; no input, repo-root, output-file, color, carrier, or install option", + "exit 0 on generation success; exit 1 on invocation, cancellation, serialization, or output transport error", + "root-shape-only definition proofkit.integration-source.output.v1.root-shape; nested fields, leaf types, cardinalities, and semantic validity remain native-owner claims" + ], + "ownerRequirementRefs": [ + "REQ-PROOFKIT-PACKAGE-002", + "REQ-PROOFKIT-QUALITY-004", + "REQ-PROOFKIT-WORKFLOW-016", + "REQ-PROOFKIT-WORKFLOW-018" + ], + "flagChoices": { + "--format": [ + "json", + "text" + ], + "--tool": [ + "claude", + "codex" + ] + } + } + }, { "command": "json-report-cli-adapter-source", "input": "none", @@ -3057,7 +3247,7 @@ "nativeSources": [ { "path": "internal/app", - "canonicalDigest": "sha256:7fa037cfe6147827468954667d103b27796ff43c1d1f74d81be1f545d7534a8e", + "canonicalDigest": "sha256:8cf495d309f3447cc7743e8c2e9ff303e5f4bb78d69eeff27fd48d37fa47d605", "evidenceClass": "source_checkout" }, { @@ -6347,7 +6537,7 @@ "rootDefinitionDigest": "sha256:3c842174dff5361e7f83166469b832805e05aa314b073c16234b5b64e346281e", "nativeSource": { "path": "internal/app", - "canonicalDigest": "sha256:7fa037cfe6147827468954667d103b27796ff43c1d1f74d81be1f545d7534a8e", + "canonicalDigest": "sha256:8cf495d309f3447cc7743e8c2e9ff303e5f4bb78d69eeff27fd48d37fa47d605", "evidenceClass": "source_checkout" }, "nativeAdmissionWitnessSelector": { @@ -6376,7 +6566,7 @@ "rootDefinitionDigest": "sha256:0ea95e277ebe44cd2de42c29b47c38686ac0b6b390d8965367437b3fe138e209", "nativeSource": { "path": "internal/app", - "canonicalDigest": "sha256:7fa037cfe6147827468954667d103b27796ff43c1d1f74d81be1f545d7534a8e", + "canonicalDigest": "sha256:8cf495d309f3447cc7743e8c2e9ff303e5f4bb78d69eeff27fd48d37fa47d605", "evidenceClass": "source_checkout" }, "nativeOutputWitnessSelector": { @@ -8363,11 +8553,33 @@ ], "variants": [ { - "allowedFields": ["nonClaims", "projectId", "requestId", "requestKind", "requirementProofBinding", "requirementSources", "schemaVersion", "sourcePlan", "testEvidenceInventory"], - "requiredFields": ["nonClaims", "projectId", "requestId", "requestKind", "requirementProofBinding", "requirementSources", "schemaVersion", "sourcePlan", "testEvidenceInventory"], + "allowedFields": [ + "nonClaims", + "projectId", + "requestId", + "requestKind", + "requirementProofBinding", + "requirementSources", + "schemaVersion", + "sourcePlan", + "testEvidenceInventory" + ], + "requiredFields": [ + "nonClaims", + "projectId", + "requestId", + "requestKind", + "requirementProofBinding", + "requirementSources", + "schemaVersion", + "sourcePlan", + "testEvidenceInventory" + ], "rootKind": "object", "variantId": "01-root", - "when": ["default JSON input"] + "when": [ + "default JSON input" + ] } ] }, @@ -8387,11 +8599,35 @@ ], "variants": [ { - "allowedFields": ["expectedDesiredStateId", "expectedTransactionId", "failureClass", "nonClaims", "operation", "receiptId", "receiptKind", "schemaVersion", "state", "transactionResult"], - "requiredFields": ["expectedDesiredStateId", "expectedTransactionId", "failureClass", "nonClaims", "operation", "receiptId", "receiptKind", "schemaVersion", "state", "transactionResult"], + "allowedFields": [ + "expectedDesiredStateId", + "expectedTransactionId", + "failureClass", + "nonClaims", + "operation", + "receiptId", + "receiptKind", + "schemaVersion", + "state", + "transactionResult" + ], + "requiredFields": [ + "expectedDesiredStateId", + "expectedTransactionId", + "failureClass", + "nonClaims", + "operation", + "receiptId", + "receiptKind", + "schemaVersion", + "state", + "transactionResult" + ], "rootKind": "object", "variantId": "01-root", - "when": ["default JSON mode"] + "when": [ + "default JSON mode" + ] } ] }, @@ -8411,11 +8647,33 @@ ], "variants": [ { - "allowedFields": ["nonClaims", "projectId", "requestId", "requestKind", "requirementProofBinding", "requirementSources", "schemaVersion", "sourcePlan", "testEvidenceInventory"], - "requiredFields": ["nonClaims", "projectId", "requestId", "requestKind", "requirementProofBinding", "requirementSources", "schemaVersion", "sourcePlan", "testEvidenceInventory"], + "allowedFields": [ + "nonClaims", + "projectId", + "requestId", + "requestKind", + "requirementProofBinding", + "requirementSources", + "schemaVersion", + "sourcePlan", + "testEvidenceInventory" + ], + "requiredFields": [ + "nonClaims", + "projectId", + "requestId", + "requestKind", + "requirementProofBinding", + "requirementSources", + "schemaVersion", + "sourcePlan", + "testEvidenceInventory" + ], "rootKind": "object", "variantId": "01-root", - "when": ["default JSON input"] + "when": [ + "default JSON input" + ] } ] }, @@ -8435,11 +8693,35 @@ ], "variants": [ { - "allowedFields": ["manifest", "nonClaims", "planKind", "projectId", "requestId", "schemaVersion", "sourceIntent", "sourcePlanId", "state", "transaction"], - "requiredFields": ["manifest", "nonClaims", "planKind", "projectId", "requestId", "schemaVersion", "sourceIntent", "sourcePlanId", "state", "transaction"], + "allowedFields": [ + "manifest", + "nonClaims", + "planKind", + "projectId", + "requestId", + "schemaVersion", + "sourceIntent", + "sourcePlanId", + "state", + "transaction" + ], + "requiredFields": [ + "manifest", + "nonClaims", + "planKind", + "projectId", + "requestId", + "schemaVersion", + "sourceIntent", + "sourcePlanId", + "state", + "transaction" + ], "rootKind": "object", "variantId": "01-root", - "when": ["default JSON mode"] + "when": [ + "default JSON mode" + ] } ] }, @@ -8459,11 +8741,35 @@ ], "variants": [ { - "allowedFields": ["expectedDesiredStateId", "expectedTransactionId", "failureClass", "nonClaims", "operation", "receiptId", "receiptKind", "schemaVersion", "state", "transactionResult"], - "requiredFields": ["expectedDesiredStateId", "expectedTransactionId", "failureClass", "nonClaims", "operation", "receiptId", "receiptKind", "schemaVersion", "state", "transactionResult"], + "allowedFields": [ + "expectedDesiredStateId", + "expectedTransactionId", + "failureClass", + "nonClaims", + "operation", + "receiptId", + "receiptKind", + "schemaVersion", + "state", + "transactionResult" + ], + "requiredFields": [ + "expectedDesiredStateId", + "expectedTransactionId", + "failureClass", + "nonClaims", + "operation", + "receiptId", + "receiptKind", + "schemaVersion", + "state", + "transactionResult" + ], "rootKind": "object", "variantId": "01-root", - "when": ["default JSON mode"] + "when": [ + "default JSON mode" + ] } ] }, @@ -10674,6 +10980,100 @@ }, "canonicalDigest": "sha256:e6dacd55cace039ca8e24bdec628b1498d0c12bbfcdb60c46854ab2047c3f2d7" }, + { + "definitionId": "proofkit.integration-check.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "expectedContentDigest", + "integrationId", + "kind", + "nonClaims", + "schemaVersion", + "state", + "targetPath", + "tool" + ], + "requiredFields": [ + "expectedContentDigest", + "integrationId", + "kind", + "nonClaims", + "schemaVersion", + "state", + "targetPath", + "tool" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:309c5f43b8d5596d88859819ed59e4486926d75d5f41d2b2b6e252389f5913ca" + }, + { + "definitionId": "proofkit.integration-source.output.v1.root-shape", + "schemaVersion": 1, + "rootType": "object", + "closed": true, + "definitionRefs": [], + "fieldTree": { + "kind": "root_shape_only", + "nonClaims": [ + "Root-shape definitions do not claim nested field shapes, leaf types, cardinalities, or semantic validity.", + "Root-shape definitions do not replace direct public-CLI runtime witnesses for variant selection." + ], + "variants": [ + { + "allowedFields": [ + "bodyBytes", + "capabilityDigest", + "content", + "contentDigest", + "integrationId", + "kind", + "metadataBytes", + "nonClaims", + "schemaVersion", + "targetPath", + "tool" + ], + "requiredFields": [ + "bodyBytes", + "capabilityDigest", + "content", + "contentDigest", + "integrationId", + "kind", + "metadataBytes", + "nonClaims", + "schemaVersion", + "targetPath", + "tool" + ], + "rootKind": "object", + "variantId": "01-root", + "when": [ + "default JSON mode" + ] + } + ] + }, + "canonicalDigest": "sha256:f7437f3154a39e0bb0b2b1329c626401140ee93b7f58ae23de63905cdcf30ec5" + }, { "definitionId": "proofkit.json-report-cli-adapter-source.output.v1.root-shape", "schemaVersion": 1, diff --git a/proofkit/command-families.v1.json b/proofkit/command-families.v1.json index 5ea32b3..d0582be 100644 --- a/proofkit/command-families.v1.json +++ b/proofkit/command-families.v1.json @@ -27,6 +27,15 @@ "adopt-materialize-recover" ] }, + { + "familyId": "agent-integrations", + "label": "Agent integrations", + "purpose": "Generate portable bootstrap source and check generated-byte freshness without installation or host activation.", + "commands": [ + "integration-check", + "integration-source" + ] + }, { "familyId": "agent-workflow-planning", "label": "Agent workflow planning", diff --git a/proofkit/requirement-bindings.json b/proofkit/requirement-bindings.json index ebf84eb..d07f020 100644 --- a/proofkit/requirement-bindings.json +++ b/proofkit/requirement-bindings.json @@ -907,6 +907,42 @@ "CLI surface closure does not prove registry publication, provider ingestion, consumer migration, native witness truth, rollout, or production readiness.", "Cancellation racing after the final pre-emission checkpoint cannot retract bytes subsequently accepted by an external writer." ] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-016", + "ownerId": "proofkit.agent-workflow", + "specPath": "docs/specs/proofkit-agent-workflow/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": [ + "Byte budgets do not prove tokenizer-specific token cost or model behavior.", + "Materialization identity binds consumed registered invocation contracts, not complete schemas or every transitive native semantic behavior.", + "Generation does not install instructions, activate a host skill, grant permissions, authorize execution, or prove native verification." + ] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-017", + "ownerId": "proofkit.agent-workflow", + "specPath": "docs/specs/proofkit-agent-workflow/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": [ + "Current means exact generated-byte freshness only, not installation, host activation, approved-launcher invocation, native execution, semantic proof completion, or stability after return.", + "Application-write-free inspection does not prevent filesystem-owned read metadata updates, exclude an uncooperative same-user writer, or identify unread oversized-file bytes beyond the bounded observation." + ] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-018", + "ownerId": "proofkit.agent-workflow", + "specPath": "docs/specs/proofkit-agent-workflow/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": [ + "Witness selectors declare exact verification routes, not executed or passed proof; installed npm/Python integration witnesses and final frozen-tree closure require actual execution against the named source and artifacts.", + "Installed CLI behavior does not prove native-host file discovery, body loading, approved-launcher calls, registry publication, consumer migration, or host instruction revocation after file removal.", + "Cancellation after the final pre-emission checkpoint cannot retract accepted output, and a caller-provided writer that accepts a prefix before failure is not an atomic sink.", + "Phase5A is generation/check only, not managed installation, update, removal, host activation, permission grant, semantic full proof, merge approval, or production readiness." + ] } ], "bindings": [ @@ -6631,6 +6667,340 @@ ], "commandIds": ["proofkit.go-test"], "environmentClasses": ["local-go"] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-016", + "scenarioId": "proofkit.agent-workflow.integration-capability-and-carrier-identity", + "witnessId": "proofkit.agent-integration.capability-and-carrier-identity-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/agent_integration_command_test.go", + "witnessSelectors": [ + { + "selector": "TestIntegrationCapabilityIdentityScope", + "command": "go test ./internal/app -run '^TestIntegrationCapabilityIdentityScope$'" + }, + { + "selector": "TestIntegrationSourcesAreCarrierIndependent", + "command": "go test ./internal/app -run '^TestIntegrationSourcesAreCarrierIndependent$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-016", + "scenarioId": "proofkit.agent-workflow.integration-portable-source", + "witnessId": "proofkit.agent-integration.portable-source-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/agentintegration/source_test.go", + "witnessSelectors": [ + { + "selector": "TestSourceBindsPortableConsumedContracts", + "command": "go test ./internal/command/agentintegration -run '^TestSourceBindsPortableConsumedContracts$'" + }, + { + "selector": "TestSourceRejectsUnboundCapabilities", + "command": "go test ./internal/command/agentintegration -run '^TestSourceRejectsUnboundCapabilities$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-017", + "scenarioId": "proofkit.agent-workflow.integration-bounded-read-only-classification", + "witnessId": "proofkit.agent-integration.bounded-read-only-classification-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/agentintegration/check_test.go", + "witnessSelectors": [ + { + "selector": "TestCheckInvalidRootsAreOperationErrors", + "command": "go test ./internal/command/agentintegration -run '^TestCheckInvalidRootsAreOperationErrors$'" + }, + { + "selector": "TestCheckPortableAliasesAreOperationErrors", + "command": "go test ./internal/command/agentintegration -run '^TestCheckPortableAliasesAreOperationErrors$'" + }, + { + "selector": "TestCheckProjectionHasOnlyAdmittedFields", + "command": "go test ./internal/command/agentintegration -run '^TestCheckProjectionHasOnlyAdmittedFields$'" + }, + { + "selector": "TestCheckRejectsEverySymlinkComponentWithoutFollowing", + "command": "go test ./internal/command/agentintegration -run '^TestCheckRejectsEverySymlinkComponentWithoutFollowing$'" + }, + { + "selector": "TestCheckRejectsZeroDocumentBeforeInspection", + "command": "go test ./internal/command/agentintegration -run '^TestCheckRejectsZeroDocumentBeforeInspection$'" + }, + { + "selector": "TestCheckStatesAreReadOnlyAndPrivate", + "command": "go test ./internal/command/agentintegration -run '^TestCheckStatesAreReadOnlyAndPrivate$'" + }, + { + "selector": "TestCheckUsesOneLeaseAndStrictReadBounds", + "command": "go test ./internal/command/agentintegration -run '^TestCheckUsesOneLeaseAndStrictReadBounds$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-017", + "scenarioId": "proofkit.agent-workflow.integration-fifo-denial", + "witnessId": "proofkit.agent-integration.fifo-denial-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/agentintegration/check_unix_test.go", + "witnessSelectors": [ + { + "selector": "TestCheckFIFONeverRead", + "command": "go test ./internal/command/agentintegration -run '^TestCheckFIFONeverRead$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-017", + "scenarioId": "proofkit.agent-workflow.integration-operational-failure-and-cohort", + "witnessId": "proofkit.agent-integration.operational-failure-and-cohort-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/agentintegration/check_test.go", + "witnessSelectors": [ + { + "selector": "TestCheckCancellationBeforeAndAfterObservation", + "command": "go test ./internal/command/agentintegration -run '^TestCheckCancellationBeforeAndAfterObservation$'" + }, + { + "selector": "TestCheckCleanupFailureInvalidatesEveryState", + "command": "go test ./internal/command/agentintegration -run '^TestCheckCleanupFailureInvalidatesEveryState$'" + }, + { + "selector": "TestCheckDetectsChangesWithinAnObservation", + "command": "go test ./internal/command/agentintegration -run '^TestCheckDetectsChangesWithinAnObservation$'" + }, + { + "selector": "TestCheckFileIOAndCleanupFailures", + "command": "go test ./internal/command/agentintegration -run '^TestCheckFileIOAndCleanupFailures$'" + }, + { + "selector": "TestCheckOpenErrorsAreNotMissingOrInvalid", + "command": "go test ./internal/command/agentintegration -run '^TestCheckOpenErrorsAreNotMissingOrInvalid$'" + }, + { + "selector": "TestCheckPermissionDeniedIsNotMissing", + "command": "go test ./internal/command/agentintegration -run '^TestCheckPermissionDeniedIsNotMissing$'" + }, + { + "selector": "TestCheckReobservesBytesStateAndOpenedIdentityIndependently", + "command": "go test ./internal/command/agentintegration -run '^TestCheckReobservesBytesStateAndOpenedIdentityIndependently$'" + }, + { + "selector": "TestCheckVerifiesRootAfterBothObservations", + "command": "go test ./internal/command/agentintegration -run '^TestCheckVerifiesRootAfterBothObservations$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-017", + "scenarioId": "proofkit.agent-workflow.integration-pre-io-cli-admission", + "witnessId": "proofkit.agent-integration.pre-io-cli-admission-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/agent_integration_command_test.go", + "witnessSelectors": [ + { + "selector": "TestIntegrationCheckCLI", + "command": "go test ./internal/app -run '^TestIntegrationCheckCLI$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-018", + "scenarioId": "proofkit.agent-workflow.integration-contract-and-route-closure", + "witnessId": "proofkit.agent-integration.contract-and-route-closure-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/cli_contract_test.go", + "witnessSelectors": [ + { + "selector": "TestCLIContractMatchesDispatcherAndHelp", + "command": "go test ./internal/app -run '^TestCLIContractMatchesDispatcherAndHelp$'" + }, + { + "selector": "TestCLIContractsAreCompleteGeneratedAndWitnessBound", + "command": "go test ./internal/app -run '^TestCLIContractsAreCompleteGeneratedAndWitnessBound$'" + }, + { + "selector": "TestContractMapDecisionTreeHasThreeCells", + "command": "go test ./internal/app -run '^TestContractMapDecisionTreeHasThreeCells$'" + } + ], + "commandIds": [ + "proofkit.command-contract-check", + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-018", + "scenarioId": "proofkit.agent-workflow.integration-public-cli", + "witnessId": "proofkit.agent-integration.public-cli-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/agent_integration_command_test.go", + "witnessSelectors": [ + { + "selector": "TestIntegrationCancellationBeforeOutput", + "command": "go test ./internal/app -run '^TestIntegrationCancellationBeforeOutput$'" + }, + { + "selector": "TestIntegrationCheckCLI", + "command": "go test ./internal/app -run '^TestIntegrationCheckCLI$'" + }, + { + "selector": "TestIntegrationCommandsExactRootShapes", + "command": "go test ./internal/app -run '^TestIntegrationCommandsExactRootShapes$'" + }, + { + "selector": "TestIntegrationSourceCLI", + "command": "go test ./internal/app -run '^TestIntegrationSourceCLI$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-018", + "scenarioId": "proofkit.agent-workflow.integration-installed-carriers", + "witnessId": "proofkit.agent-integration.installed-carriers-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/tools/workflowsmoke/workflow_smoke_test.go", + "witnessSelectors": [ + { + "selector": "TestVerifyAcceptsApplicationCLI", + "command": "go test ./internal/tools/workflowsmoke -run '^TestVerifyAcceptsApplicationCLI$'" + }, + { + "selector": "TestVerifyRejectsCarrierContractMutations", + "command": "go test ./internal/tools/workflowsmoke -run '^TestVerifyRejectsCarrierContractMutations$'" + } + ], + "commandIds": [ + "proofkit.go-test", + "proofkit.package-artifact" + ], + "environmentClasses": [ + "local-go", + "local-go-python" + ] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-018", + "scenarioId": "proofkit.agent-workflow.integration-installed-npm-carrier-closure", + "witnessId": "proofkit.agent-integration.installed-npm-carrier-closure-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/tools/packageverify/workflow_carrier_test.go", + "witnessSelectors": [ + { + "selector": "TestInstalledNPMWorkflowCarrierClosure", + "command": "go test ./internal/tools/packageverify -run '^TestInstalledNPMWorkflowCarrierClosure$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-018", + "scenarioId": "proofkit.agent-workflow.integration-installed-wheel-carrier-closure", + "witnessId": "proofkit.agent-integration.installed-wheel-carrier-closure-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/tools/pythonpackage/workflow_carrier_test.go", + "witnessSelectors": [ + { + "selector": "TestInstalledPythonWorkflowCarrierClosure", + "command": "go test ./internal/tools/pythonpackage -run '^TestInstalledPythonWorkflowCarrierClosure$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-018", + "scenarioId": "proofkit.agent-workflow.integration-version-edge", + "witnessId": "proofkit.agent-integration.version-edge-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/integration_version_edge_test.go", + "witnessSelectors": [ + { + "selector": "TestIntegrationVersionEdgeClosesCompletePublicABIDiff", + "command": "go test ./internal/app -run '^TestIntegrationVersionEdgeClosesCompletePublicABIDiff$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-018", + "scenarioId": "proofkit.agent-workflow.integration-public-abi-mutations", + "witnessId": "proofkit.agent-integration.public-abi-mutations-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/public_abi_mutation_test.go", + "witnessSelectors": [ + { + "selector": "TestIntegrationVersionEdgeRejectsUndeclaredPublicABIDrift", + "command": "go test ./internal/app -run '^TestIntegrationVersionEdgeRejectsUndeclaredPublicABIDrift$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] } ], "witnessCommands": [ diff --git a/release/change-record.v2.json b/release/change-record.v2.json index 41c5ffa..850899a 100644 --- a/release/change-record.v2.json +++ b/release/change-record.v2.json @@ -1,34 +1,22 @@ { "schemaVersion": 2, - "previousVersion": "0.8.0", - "version": "0.9.0", - "changeClass": "breaking", - "breakingChanges": [ - { - "changeId": "proofkit.agent-workflow.change-plan-route", - "summary": "Replace the flat change-workflow-plan CLI route with the hierarchical change plan route while preserving one internal command implementation and its input and output contracts." - }, - { - "changeId": "proofkit.cli-contract.omitted-route-policy", - "summary": "Make the command-id fallback for an omitted command route an explicit required CLI-contract grammar field; Proofkit source and installed-carrier validators reject contracts that omit or alter this policy." - } - ], + "previousVersion": "0.9.0", + "version": "0.10.0", + "changeClass": "compatible", + "breakingChanges": [], "additions": [ { - "changeId": "proofkit.project-state.next-action", - "summary": "Add a bounded next command that maps each admitted structural project state to exactly one non-authoritative repository action." + "changeId": "proofkit.agent-integration.freshness", + "summary": "Add integration check for explicit Codex or Claude repository paths; compare bounded current template bytes through confined read-only inspection without granting installation or host-activation authority." }, { - "changeId": "proofkit.project-state.status", - "summary": "Add a read-only status command that classifies a bounded normalized materialized-project and transaction observation; admitted in-bound records bind exact content digests, while unread out-of-bound records identify only their invalid class, without claiming native verification or workflow completion." + "changeId": "proofkit.agent-integration.source", + "summary": "Add integration source to generate one bounded portable skill body and descriptor-owned path, with an identity bound to consumed registered CLI invocation contracts rather than package version or a host-specific executable." } ], "migration": { - "required": true, - "steps": [ - "Replace agentic-proofkit change-workflow-plan invocations with agentic-proofkit change plan; input and output JSON contracts are unchanged.", - "Update CLI-contract consumers to require commandRouteGrammar.omittedRoutePolicy=command_id; commands without an explicit route continue to resolve to their stable command ID." - ] + "required": false, + "steps": [] }, "platformRequirements": [ "Published Darwin package binaries require macOS 13.0 or later on arm64 and x86_64." @@ -41,7 +29,8 @@ "Complete nested public structural contracts remain blocked under SCHEMA-01; current CLI contracts own exact root variants only.", "Project status and next classify materialized repository structure only; they do not execute native verification, validate receipt currentness or trust, or declare workflow completion.", "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." + "TSX source parsing remains unsupported.", + "Integration source and check do not install, update, remove, activate, or execute a host skill. Managed lifecycle and observed host activation remain separate open work." ], "rollback": { "strategy": "previous_admitted_version" From 69c27d530a3eefca6abd7ea73e114e0e66933197 Mon Sep 17 00:00:00 2001 From: iperev Date: Sat, 5 Sep 2026 11:34:49 +0200 Subject: [PATCH 2/3] fix: preserve complete integration route observations --- BACKLOG.md | 1 + internal/app/cli_contract_test.go | 2 +- internal/app/command_contract_generated.go | 12 +- .../project_navigation_abi_closure_test.go | 13 +- .../releases/v0.9.0/cli-contract.v2.json.gz | Bin 53523 -> 0 bytes .../releases/v0.9.0/cli-contract.v2.json.zip | Bin 0 -> 55307 bytes internal/command/agentintegration/check.go | 20 +- .../command/agentintegration/check_test.go | 119 +++++-- .../agentintegration/check_unix_test.go | 90 ++++- .../command/agentintegration/source_test.go | 8 + .../stackpreset/preset_ids_generated.go | 2 +- .../control_inspection_test.go | 96 ++++++ .../repositorytransaction/inspection_lease.go | 21 +- internal/kernel/rootpath/exact.go | 55 +++ .../kernel/rootpath/observation_unix_test.go | 315 ++++++++++++++++++ internal/kernel/rootpath/open_other.go | 10 +- internal/kernel/rootpath/open_unix.go | 72 ++-- .../coveragemetrics/required_inventory.go | 19 +- proofkit/cli-contract.v2.json | 14 +- proofkit/requirement-bindings.json | 81 +++++ 20 files changed, 863 insertions(+), 87 deletions(-) delete mode 100644 internal/app/testdata/releases/v0.9.0/cli-contract.v2.json.gz create mode 100644 internal/app/testdata/releases/v0.9.0/cli-contract.v2.json.zip create mode 100644 internal/kernel/rootpath/observation_unix_test.go diff --git a/BACKLOG.md b/BACKLOG.md index a6dacec..c451043 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -48,6 +48,7 @@ records, generated release manifests, or the owning docs named above. | Status | ID | Scope | Completion condition | |---|---|---|---| +| NEXT | INTEGRATION-01 | Complete the explicitly selected tool's managed bootstrap lifecycle without treating generation/check as installation or host activation. | One existing repository transaction owner performs reviewed install/update/remove with exact prior-byte and desired-state preconditions, conflict preservation for local edits, real final absence on removal, and rollback/recovery/cancellation/cleanup witnesses; adjacent and global instructions remain unchanged. Isolated native-host tests separately establish file discovery, body loading, and approved-launcher invocation, or retain those precise non-claims. | | BLOCKED | SOURCE-CUTOVER-01 | Migrate self-hosted requirement sources only after one codec, the typed v2 model, nested structural contracts, and the complete evidence counterfeit corpus pass their gates. | The `REQ-PROOFKIT-QUALITY-010` execution-backed command-oracle closure and `SCHEMA-01` are complete; a digest-bound clause ledger proves representation-only equality or owner-reviewed semantic decomposition for every legacy requirement; all bindings/scenarios/contracts/context/diff/graph/browser owners cut over atomically; v1 admission and the losing codec are removed; and active-v1 inventory is zero. | | BLOCKED | SCHEMA-01 | Replace root-shape-only public contracts with one independent complete nested structural-contract owner. | A versioned schema owner covers nested fields, variants, cardinalities, bounds, enums, defaults, duplicate and unknown-field policy, and cross-field constraints; generated artifacts pass parity against an independently authored completeness manifest and mutant corpus without becoming semantic or policy authority. | | BLOCKED | SOURCE-PILOT-01 | Validate the selected source-v2 model and agent routing against heterogeneous external repositories without mutating them. | At least two independent repository classes complete no-push dual runs whose frozen inputs compare incumbent and candidate mapping, diagnostics, token cost, authoring accuracy, proof-route gaps, and rollback; unresolved parity or authority gaps keep incumbent owners active. | diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index 924cebf..b333ea1 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -24,7 +24,7 @@ import ( ) const ( - cliContractPublicABISHA256 = "58890d28e1518df871b2c363a6255fb500822534060c8f3ae169eff9533b66db" + cliContractPublicABISHA256 = "0e246c0d26bebd39ca6d6747ee7e325cb73aaca4dc01d80fa63cb6dc4899155f" 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 e14c431..16bd69b 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 = "9d8a9843ac240b524eeb5c27f4ca8833b411d410d36c6841e14134e1ce8a57d0" +const commandContractSourceSHA256 = "c9322a64711d906f2330ae225c08c69042ad244d9f4fe00d9e8ae0e4815ab7f5" type generatedCommandContractMetadata struct { InputContractSHA256 string @@ -12,9 +12,9 @@ type generatedCommandContractMetadata struct { } var generatedCommandContractMetadataByName = map[string]generatedCommandContractMetadata{ - "adopt-materialize-apply": {InputContractSHA256: "sha256:98539b75bf6d4caebc75d724e9201925a1235290791adcebc865c1b3c74976ee", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.apply-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:1257f86e497d70dd8e16af3cee5852b7b36ff632f4bc6d6ff8852ce4d5e8c075", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "apply"}}, - "adopt-materialize-plan": {InputContractSHA256: "sha256:65925b56d0332349c5048e9b1450ff0b7a3381224342df1a630128e3f4a2cde2", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.plan-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:bca136e026ddd9e8be20c6d25750105f5afb9bdf9dfa45fc07c03b09ae54ac79", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "plan"}}, - "adopt-materialize-recover": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:0837185fa64aec6ec405ca27e6f08af297e773e17a5a1963617cd9a2b04629d8", FlagChoices: map[string][]string{"--action": []string{"resume", "rollback"}, "--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "recover"}}, + "adopt-materialize-apply": {InputContractSHA256: "sha256:98539b75bf6d4caebc75d724e9201925a1235290791adcebc865c1b3c74976ee", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.apply-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:cd6f4a60c4539b453b5e3e48cb67507ed3402c425ecc7bb5007469e48abad96e", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "apply"}}, + "adopt-materialize-plan": {InputContractSHA256: "sha256:65925b56d0332349c5048e9b1450ff0b7a3381224342df1a630128e3f4a2cde2", InputSchemaSummary: []string{"schemaVersion=1", "owner-admitted adoption plan, requirement sources, proof bindings, and direct test inventory", "root-shape-only definition proofkit.adoption-materialization.plan-input.v1.root-shape; nested fields, types, cardinalities, and cross-record closure remain native-owner claims"}, OutputContractSHA256: "sha256:67c56553cdbb4000b1df58871c148683e322b5fb2076381c30c531fabb23d645", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "plan"}}, + "adopt-materialize-recover": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:e1d51c61e787021cd5dad1a92e66d2d576c0f0c88d3a2855d9034cf96da7278c", FlagChoices: map[string][]string{"--action": []string{"resume", "rollback"}, "--color": []string{"auto", "never"}, "--format": []string{"json", "text"}}, RouteTokens: []string{"adopt", "materialize", "recover"}}, "adopt-plan": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:13e3392d9005c27fed3003a1d036123fe12a341dbe16a97c0cd9f8272fb83320", FlagChoices: map[string][]string{"--color": []string{"auto", "never"}, "--format": []string{"json", "text"}, "--mode": []string{"audit-from-code", "code-baseline", "fresh"}, "--stack": []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"}}, RouteTokens: []string{"adopt", "plan"}}, "adoption-checklist": {InputContractSHA256: "sha256:4e6c4c9b369279837a5894c0b3f842a411dce529b91c91cb2d4ec63eb5ee4c2c", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.adoption-checklist.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:9d0d0e60f0935407fd31007d8502459663eb4c7228dc5e3c7727ae2c9907bdc9", FlagChoices: map[string][]string{}, RouteTokens: []string{"adoption-checklist"}}, "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{}, RouteTokens: []string{"adoption-contract-envelope"}}, @@ -38,8 +38,8 @@ var generatedCommandContractMetadataByName = map[string]generatedCommandContract "gradual-adoption-guidance": {InputContractSHA256: "sha256:4752cbac81c864cb3e18a39facfd666a9707314233d54798c7f71e67d7f2800c", InputSchemaSummary: []string{"schemaVersion=1", "root-shape-only definition proofkit.gradual-adoption-guidance.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:171fed4bb8d32a47fc5ec49796f5b0b55ed666feaccc2fbbfeb12da31d80ecc9", FlagChoices: map[string][]string{}, RouteTokens: []string{"gradual-adoption-guidance"}}, "help": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "", FlagChoices: map[string][]string{}, RouteTokens: []string{"help"}}, "impact": {InputContractSHA256: "sha256:41d3107414837955ee408d5ce94949a4c1a6b76f6949e6c1dc224bd06f6b09bc", InputSchemaSummary: []string{"schemaVersion=2", "root-shape-only definition proofkit.impact.input.v2.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:73066e9a5ca48f21936111ffb7223900fb629875997f4e7b16d7fef9c4177972", FlagChoices: map[string][]string{}, RouteTokens: []string{"impact"}}, - "integration-check": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:4662f396cd9af10c0ccad9bd13e8d69425a312a925629102f9c2d33fa3f8d068", FlagChoices: map[string][]string{"--format": []string{"json", "text"}, "--tool": []string{"claude", "codex"}}, RouteTokens: []string{"integration", "check"}}, - "integration-source": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:a46e36438c4beafd358dfd3b1d5072ae9e0a0b69a4ce674ee7ca5c93cad7a976", FlagChoices: map[string][]string{"--format": []string{"json", "text"}, "--tool": []string{"claude", "codex"}}, RouteTokens: []string{"integration", "source"}}, + "integration-check": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:a43909ee05b82a33fa87b700714279991972b1bc158339dd4ed459d766921477", FlagChoices: map[string][]string{"--format": []string{"json", "text"}, "--tool": []string{"claude", "codex"}}, RouteTokens: []string{"integration", "check"}}, + "integration-source": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:7c0ff8d63ac58eeb5dbc40fa4b6be341d2e9814e6ad39805926de26414761303", FlagChoices: map[string][]string{"--format": []string{"json", "text"}, "--tool": []string{"claude", "codex"}}, RouteTokens: []string{"integration", "source"}}, "json-report-cli-adapter-source": {InputContractSHA256: "", InputSchemaSummary: []string(nil), OutputContractSHA256: "sha256:6c3dd1c8507a90e055cf2c886089446d8560ff3e0d3ca9cc6360a3377d2d85da", FlagChoices: map[string][]string{}, RouteTokens: []string{"json-report-cli-adapter-source"}}, "migration-parity-admission": {InputContractSHA256: "sha256:0b36c0e68da3b857dac4b13e7b3bd523052459106133aa8c908a4352682e6c05", InputSchemaSummary: []string{"schemaVersion=1", "paritySetId", "sourceProofOwners[]", "targetProofkitRefs[]", "parityRecords[]", "nonClaims[]", "root-shape-only definition proofkit.migration-parity-admission.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:8e0f8af2b205817f018b0fe133fe789661caa29007695e036bfcab63c1830f47", FlagChoices: map[string][]string{}, RouteTokens: []string{"migration-parity-admission"}}, "migration-plan": {InputContractSHA256: "sha256:58a62759a634101ce2ca9218184175134bbe5633328e1b23797b94c19fc9b11a", InputSchemaSummary: []string{"schemaVersion=1", "migrationId", "sourceProofOwners[]", "targetProofkitRefs[]", "parityEvidenceRefs[]", "retainedOwners[]", "retirementCandidates[]", "followUpCommands[]", "nonClaims[]", "root-shape-only definition proofkit.migration-plan.input.v1.root-shape; nested fields, types, and cardinalities are non-claims"}, OutputContractSHA256: "sha256:f14f0381e9dc241357c346315b95b03ef5b23f1d1bbc3b00f111fbe1515ed3ff", FlagChoices: map[string][]string{}, RouteTokens: []string{"migration-plan"}}, diff --git a/internal/app/project_navigation_abi_closure_test.go b/internal/app/project_navigation_abi_closure_test.go index 0ff6278..78127e6 100644 --- a/internal/app/project_navigation_abi_closure_test.go +++ b/internal/app/project_navigation_abi_closure_test.go @@ -1,8 +1,8 @@ package app import ( + "archive/zip" "bytes" - "compress/gzip" "crypto/sha256" "fmt" "io" @@ -26,11 +26,18 @@ func TestProjectNavigationVersionEdgeClosesCompletePublicABIDiff(t *testing.T) { func readArchivedProjectNavigationContract(t *testing.T) map[string]any { t.Helper() - content, err := os.ReadFile(filepath.Join(repoRoot(t), archivedProjectNavigationReleaseRoot, "cli-contract.v2.json.gz")) + content, err := os.ReadFile(filepath.Join(repoRoot(t), archivedProjectNavigationReleaseRoot, "cli-contract.v2.json.zip")) if err != nil { t.Fatal(err) } - reader, err := gzip.NewReader(bytes.NewReader(content)) + archive, err := zip.NewReader(bytes.NewReader(content), int64(len(content))) + if err != nil { + t.Fatal(err) + } + if len(archive.File) != 1 || archive.File[0].Name != "cli-contract.v2.json" || archive.File[0].UncompressedSize64 > 1<<20 { + t.Fatal("archived CLI contract must contain exactly its bounded contract entry") + } + reader, err := archive.File[0].Open() if err != nil { t.Fatal(err) } diff --git a/internal/app/testdata/releases/v0.9.0/cli-contract.v2.json.gz b/internal/app/testdata/releases/v0.9.0/cli-contract.v2.json.gz deleted file mode 100644 index 8301a067e3175cebdb4daf12dff93c2f886175db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53523 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 diff --git a/internal/app/testdata/releases/v0.9.0/cli-contract.v2.json.zip b/internal/app/testdata/releases/v0.9.0/cli-contract.v2.json.zip new file mode 100644 index 0000000000000000000000000000000000000000..58b42ed1e4b0e7aed0712d59a49cbbfac53edbb6 GIT binary patch literal 55307 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 U000010096(00029*8l(j0LdP@VE_OC literal 0 HcmV?d00001 diff --git a/internal/command/agentintegration/check.go b/internal/command/agentintegration/check.go index ae3bb57..774dbdc 100644 --- a/internal/command/agentintegration/check.go +++ b/internal/command/agentintegration/check.go @@ -52,13 +52,13 @@ func checkNonClaims() []any { } type checkDependencies struct { - openFile func(*repositorytransaction.InspectionLease, string) (repositorytransaction.InspectionFile, error) + openFile func(*repositorytransaction.InspectionLease, string) (repositorytransaction.InspectionFile, rootpath.RouteObservation, error) closeLease func(*repositorytransaction.InspectionLease) error } func nativeCheckDependencies() checkDependencies { return checkDependencies{ - openFile: (*repositorytransaction.InspectionLease).OpenExactRegularFile, + openFile: (*repositorytransaction.InspectionLease).OpenObservedExactRegularFile, closeLease: (*repositorytransaction.InspectionLease).Close, } } @@ -132,13 +132,14 @@ type checkObservation struct { state string content []byte info fs.FileInfo + route rootpath.RouteObservation } -func observeCheckFile(ctx context.Context, lease *repositorytransaction.InspectionLease, document Document, openFile func(*repositorytransaction.InspectionLease, string) (repositorytransaction.InspectionFile, error)) (observation checkObservation, returnErr error) { +func observeCheckFile(ctx context.Context, lease *repositorytransaction.InspectionLease, document Document, openFile func(*repositorytransaction.InspectionLease, string) (repositorytransaction.InspectionFile, rootpath.RouteObservation, error)) (observation checkObservation, returnErr error) { if err := ctx.Err(); err != nil { return checkObservation{}, fmt.Errorf("integration check operation cancelled: %w", err) } - file, err := openFile(lease, document.path) + file, route, err := openFile(lease, document.path) switch { case errors.Is(err, repositorytransaction.ErrReadCleanup), errors.Is(err, rootpath.ErrTraversalCleanup): return checkObservation{}, checkCleanupError("open selected file") @@ -146,9 +147,9 @@ func observeCheckFile(ctx context.Context, lease *repositorytransaction.Inspecti errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded), errors.Is(err, fs.ErrPermission): return checkObservation{}, checkOperationError("open selected file") case errors.Is(err, fs.ErrNotExist): - return checkObservation{state: "missing"}, nil + return checkObservation{state: "missing", route: route}, nil case errors.Is(err, repositorytransaction.ErrUnsafeInspectionRoute): - return checkObservation{state: "invalid"}, nil + return checkObservation{state: "invalid", route: route}, nil case err != nil: return checkObservation{}, checkOperationError("open selected file") } @@ -163,7 +164,7 @@ func observeCheckFile(ctx context.Context, lease *repositorytransaction.Inspecti return checkObservation{}, checkOperationError("inspect selected file") } if !opened.Mode().IsRegular() { - return checkObservation{state: "invalid", info: opened}, nil + return checkObservation{state: "invalid", info: opened, route: route}, nil } // Oversized files remain invalid, but their bounded prefix participates in // reobservation too. Never read more than the admitted 8 KiB per observation. @@ -185,10 +186,13 @@ func observeCheckFile(ctx context.Context, lease *repositorytransaction.Inspecti case string(content) == document.content: state = "current" } - return checkObservation{state: state, content: content, info: opened}, nil + return checkObservation{state: state, content: content, info: opened, route: route}, nil } func sameCheckObservation(before, after checkObservation) bool { + if !before.route.Equal(after.route) { + return false + } if before.state != after.state || !bytes.Equal(before.content, after.content) { return false } diff --git a/internal/command/agentintegration/check_test.go b/internal/command/agentintegration/check_test.go index 9ae7779..01edb48 100644 --- a/internal/command/agentintegration/check_test.go +++ b/internal/command/agentintegration/check_test.go @@ -227,9 +227,9 @@ func TestCheckProjectionHasOnlyAdmittedFields(t *testing.T) { func TestCheckRejectsZeroDocumentBeforeInspection(t *testing.T) { dependencies := nativeCheckDependencies() - dependencies.openFile = func(*repositorytransaction.InspectionLease, string) (repositorytransaction.InspectionFile, error) { + dependencies.openFile = func(*repositorytransaction.InspectionLease, string) (repositorytransaction.InspectionFile, rootpath.RouteObservation, error) { t.Fatal("zero document reached file inspection") - return nil, nil + return nil, rootpath.RouteObservation{}, nil } result, err := checkWithDependencies(context.Background(), checkPrivateSentinel, Document{}, dependencies) checkWantError(t, result, err) @@ -339,8 +339,8 @@ func TestCheckOpenErrorsAreNotMissingOrInvalid(t *testing.T) { t.Run(test.name, func(t *testing.T) { root := t.TempDir() dependencies := nativeCheckDependencies() - dependencies.openFile = func(*repositorytransaction.InspectionLease, string) (repositorytransaction.InspectionFile, error) { - return nil, &fs.PathError{Op: checkPrivateSentinel, Path: root, Err: test.err} + dependencies.openFile = func(*repositorytransaction.InspectionLease, string) (repositorytransaction.InspectionFile, rootpath.RouteObservation, error) { + return nil, rootpath.RouteObservation{}, &fs.PathError{Op: checkPrivateSentinel, Path: root, Err: test.err} } before := checkTree(t, root) result, err := checkWithDependencies(context.Background(), root, document, dependencies) @@ -354,8 +354,12 @@ func TestCheckOpenErrorsAreNotMissingOrInvalid(t *testing.T) { t.Run("wrapped absence", func(t *testing.T) { root := t.TempDir() dependencies := nativeCheckDependencies() - dependencies.openFile = func(*repositorytransaction.InspectionLease, string) (repositorytransaction.InspectionFile, error) { - return nil, &fs.PathError{Op: checkPrivateSentinel, Path: root, Err: fs.ErrNotExist} + dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, rootpath.RouteObservation, error) { + file, route, err := lease.OpenObservedExactRegularFile(path) + if file != nil || !errors.Is(err, fs.ErrNotExist) { + t.Fatal("fixture did not observe ordinary absence") + } + return nil, route, &fs.PathError{Op: checkPrivateSentinel, Path: root, Err: err} } result, err := checkWithDependencies(context.Background(), root, document, dependencies) checkNoDisclosure(t, result, err, root) @@ -382,8 +386,8 @@ func TestCheckPermissionDeniedIsNotMissing(t *testing.T) { t.Fatal("permission probe cleanup failed") } // Privileged filesystems cannot supply a real denied-read operand. - dependencies.openFile = func(*repositorytransaction.InspectionLease, string) (repositorytransaction.InspectionFile, error) { - return nil, &fs.PathError{Op: "open", Path: selected, Err: fs.ErrPermission} + dependencies.openFile = func(*repositorytransaction.InspectionLease, string) (repositorytransaction.InspectionFile, rootpath.RouteObservation, error) { + return nil, rootpath.RouteObservation{}, &fs.PathError{Op: "open", Path: selected, Err: fs.ErrPermission} } } else if !errors.Is(probeErr, fs.ErrPermission) { t.Fatal("permission fixture failed for an unrelated reason") @@ -404,10 +408,10 @@ func TestCheckDetectsChangesWithinAnObservation(t *testing.T) { selected := filepath.Join(root, document.path) checkWrite(t, selected, document.Content()) dependencies := nativeCheckDependencies() - dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, error) { - file, err := lease.OpenExactRegularFile(path) + dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, rootpath.RouteObservation, error) { + file, route, err := lease.OpenObservedExactRegularFile(path) if err != nil { - return nil, err + return nil, route, err } changed := false return checkTestFile{InspectionFile: file, read: func(buffer []byte) (int, error) { @@ -425,7 +429,7 @@ func TestCheckDetectsChangesWithinAnObservation(t *testing.T) { } } return file.Read(buffer) - }}, nil + }}, route, nil } result, err := checkWithDependencies(context.Background(), root, document, dependencies) checkWantError(t, result, err, root) @@ -459,7 +463,7 @@ func TestCheckReobservesBytesStateAndOpenedIdentityIndependently(t *testing.T) { } dependencies := nativeCheckDependencies() opens := 0 - dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, error) { + dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, rootpath.RouteObservation, error) { opens++ if opens == 2 { switch scenario { @@ -494,7 +498,7 @@ func TestCheckReobservesBytesStateAndOpenedIdentityIndependently(t *testing.T) { } } } - return lease.OpenExactRegularFile(path) + return lease.OpenObservedExactRegularFile(path) } result, err := checkWithDependencies(context.Background(), root, document, dependencies) checkWantError(t, result, err, root) @@ -512,10 +516,10 @@ func TestCheckVerifiesRootAfterBothObservations(t *testing.T) { checkWrite(t, filepath.Join(root, document.path), document.Content()) dependencies := nativeCheckDependencies() closes := 0 - dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, error) { - file, err := lease.OpenExactRegularFile(path) + dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, rootpath.RouteObservation, error) { + file, route, err := lease.OpenObservedExactRegularFile(path) if err != nil { - return nil, err + return nil, route, err } return checkTestFile{InspectionFile: file, close: func() error { closeErr := file.Close() @@ -527,7 +531,7 @@ func TestCheckVerifiesRootAfterBothObservations(t *testing.T) { checkMkdir(t, root) } return closeErr - }}, nil + }}, route, nil } result, err := checkWithDependencies(context.Background(), root, document, dependencies) checkWantError(t, result, err, root) @@ -543,15 +547,15 @@ func TestCheckUsesOneLeaseAndStrictReadBounds(t *testing.T) { dependencies := nativeCheckDependencies() var pinned *repositorytransaction.InspectionLease opens, closes, totalRead, leaseCloses := 0, 0, 0, 0 - dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, error) { + dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, rootpath.RouteObservation, error) { if pinned != nil && pinned != lease || path != document.path { t.Fatal("check changed its root lease or selected path") } pinned = lease opens++ - file, err := lease.OpenExactRegularFile(path) + file, route, err := lease.OpenObservedExactRegularFile(path) if err != nil { - return nil, err + return nil, route, err } read := 0 return checkTestFile{InspectionFile: file, read: func(buffer []byte) (int, error) { @@ -565,7 +569,7 @@ func TestCheckUsesOneLeaseAndStrictReadBounds(t *testing.T) { }, close: func() error { closes++ return file.Close() - }}, nil + }}, route, nil } dependencies.closeLease = func(lease *repositorytransaction.InspectionLease) error { leaseCloses++ @@ -664,11 +668,11 @@ func TestCheckFileIOAndCleanupFailures(t *testing.T) { defer cancel() dependencies := nativeCheckDependencies() opens, closes, leaseCloses := 0, 0, 0 - dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, error) { + dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, rootpath.RouteObservation, error) { opens++ - file, err := lease.OpenExactRegularFile(path) + file, route, err := lease.OpenObservedExactRegularFile(path) if err != nil { - return nil, err + return nil, route, err } stats := 0 return checkTestFile{InspectionFile: file, read: func(buffer []byte) (int, error) { @@ -694,7 +698,7 @@ func TestCheckFileIOAndCleanupFailures(t *testing.T) { return errors.New(checkPrivateSentinel) } return nil - }}, nil + }}, route, nil } dependencies.closeLease = func(lease *repositorytransaction.InspectionLease) error { leaseCloses++ @@ -718,3 +722,66 @@ func TestCheckFileIOAndCleanupFailures(t *testing.T) { }) } } + +func TestCheckRouteWitnessRejectsZeroInEveryState(t *testing.T) { + for _, state := range []string{"missing", "current", "stale", "invalid"} { + t.Run(state, func(t *testing.T) { + root := t.TempDir() + document := checkTestDocument(t, "codex") + checkStateFixture(t, root, document, state) + dependencies := nativeCheckDependencies() + dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, rootpath.RouteObservation, error) { + file, _, err := lease.OpenObservedExactRegularFile(path) + return file, rootpath.RouteObservation{}, err + } + before := checkTree(t, root) + result, err := checkWithDependencies(context.Background(), root, document, dependencies) + checkWantError(t, result, err, root) + checkUnchanged(t, root, before) + }) + } +} + +func TestCheckRouteWitnessIgnoresUnrelatedSiblingWrites(t *testing.T) { + for _, tool := range []string{"codex", "claude"} { + for _, state := range []string{"missing", "current", "stale", "invalid"} { + t.Run(tool+"/"+state, func(t *testing.T) { + root := t.TempDir() + document := checkTestDocument(t, tool) + parent := filepath.Dir(filepath.Join(root, document.path)) + checkMkdir(t, parent) + checkStateFixture(t, root, document, state) + dependencies := nativeCheckDependencies() + opens := 0 + var expectedTree map[string]checkTestEntry + dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, rootpath.RouteObservation, error) { + opens++ + if opens == 2 { + for directory := parent; ; directory = filepath.Dir(directory) { + before, err := os.Stat(directory) + if err != nil { + t.Fatal(err) + } + checkWrite(t, filepath.Join(directory, "unrelated-sibling"), checkPrivateSentinel) + changedTime := before.ModTime().Add(time.Hour) + if err := os.Chtimes(directory, changedTime, changedTime); err != nil { + t.Fatal(err) + } + if directory == root { + break + } + } + expectedTree = checkTree(t, root) + } + return lease.OpenObservedExactRegularFile(path) + } + result, err := checkWithDependencies(context.Background(), root, document, dependencies) + checkNoDisclosure(t, result, err, root) + if err != nil || result.State() != state || opens != 2 { + t.Fatal("unrelated directory changes altered classification") + } + checkUnchanged(t, root, expectedTree) + }) + } + } +} diff --git a/internal/command/agentintegration/check_unix_test.go b/internal/command/agentintegration/check_unix_test.go index 21bed57..7de9f16 100644 --- a/internal/command/agentintegration/check_unix_test.go +++ b/internal/command/agentintegration/check_unix_test.go @@ -4,6 +4,7 @@ package agentintegration import ( "context" + "io/fs" "os" "os/exec" "path/filepath" @@ -11,6 +12,7 @@ import ( "time" "github.com/research-engineering/agentic-proofkit/internal/kernel/repositorytransaction" + "github.com/research-engineering/agentic-proofkit/internal/kernel/rootpath" "golang.org/x/sys/unix" ) @@ -36,14 +38,14 @@ func TestCheckFIFONeverRead(t *testing.T) { before := checkTree(t, root) dependencies := nativeCheckDependencies() opens := 0 - dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, error) { + dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, rootpath.RouteObservation, error) { opens++ - file, err := lease.OpenExactRegularFile(path) + file, route, err := lease.OpenObservedExactRegularFile(path) if file != nil { _ = file.Close() t.Fatal("FIFO unexpectedly returned an opened regular file") } - return file, err + return file, route, err } result, err := checkWithDependencies(context.Background(), root, document, dependencies) if err != nil || result.State() != "invalid" || opens != 2 { @@ -51,3 +53,85 @@ func TestCheckFIFONeverRead(t *testing.T) { } checkUnchanged(t, root, before) } + +func TestCheckReobservesCompleteRoute(t *testing.T) { + for _, tool := range []string{"codex", "claude"} { + for _, scenario := range []string{"missing position", "parent identity", "parent mode", "unsafe kind", "unsafe identity"} { + t.Run(tool+"/"+scenario, func(t *testing.T) { + root := t.TempDir() + document := checkTestDocument(t, tool) + selected := filepath.Join(root, document.path) + parent := filepath.Dir(selected) + var originalLeaf fs.FileInfo + switch scenario { + case "missing position": + case "unsafe kind", "unsafe identity": + checkMkdir(t, selected) + default: + checkWrite(t, selected, document.Content()) + var err error + originalLeaf, err = os.Stat(selected) + if err != nil { + t.Fatal(err) + } + } + dependencies := nativeCheckDependencies() + opens := 0 + dependencies.openFile = func(lease *repositorytransaction.InspectionLease, path string) (repositorytransaction.InspectionFile, rootpath.RouteObservation, error) { + opens++ + if opens == 2 { + switch scenario { + case "missing position": + checkMkdir(t, parent) + case "parent identity": + before, err := os.Stat(parent) + if err != nil { + t.Fatal(err) + } + if err := os.Rename(parent, parent+".saved"); err != nil { + t.Fatal(err) + } + checkMkdir(t, parent) + if err := os.Link(filepath.Join(parent+".saved", "SKILL.md"), selected); err != nil { + t.Fatal(err) + } + after, err := os.Stat(parent) + if err != nil || os.SameFile(before, after) { + t.Fatal("parent identity did not change") + } + case "parent mode": + if err := os.Chmod(parent, 0o500); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.Chmod(parent, 0o700) }) + case "unsafe kind": + if err := os.Remove(selected); err != nil { + t.Fatal(err) + } + if err := unix.Mkfifo(selected, 0o600); err != nil { + t.Fatal(err) + } + case "unsafe identity": + if err := os.Rename(selected, selected+".saved"); err != nil { + t.Fatal(err) + } + checkMkdir(t, selected) + } + if originalLeaf != nil { + after, err := os.Stat(selected) + if err != nil || !sameCheckFile(originalLeaf, after) { + t.Fatal("route fixture changed leaf observation") + } + } + } + return lease.OpenObservedExactRegularFile(path) + } + result, err := checkWithDependencies(context.Background(), root, document, dependencies) + checkWantError(t, result, err, root) + if opens != 2 { + t.Fatal("route comparison did not reach two complete observations") + } + }) + } + } +} diff --git a/internal/command/agentintegration/source_test.go b/internal/command/agentintegration/source_test.go index 98a9e42..d197e5f 100644 --- a/internal/command/agentintegration/source_test.go +++ b/internal/command/agentintegration/source_test.go @@ -75,6 +75,14 @@ func TestSourceBindsPortableConsumedContracts(t *testing.T) { } func TestSourceRejectsUnboundCapabilities(t *testing.T) { + t.Run("rendered body budget", func(t *testing.T) { + capabilities := sourceCapabilities() + capabilities[0].Route = []string{strings.Repeat("route", MaximumBodyBytes)} + document, err := Source("codex", capabilities) + if err == nil || document != (Document{}) || !strings.Contains(err.Error(), "byte budget") { + t.Fatal("an admitted route beyond the rendered body budget was not rejected") + } + }) for _, mutate := range []func([]Capability) []Capability{ func(values []Capability) []Capability { return values[1:] }, func(values []Capability) []Capability { return append(values, values[0]) }, diff --git a/internal/command/stackpreset/preset_ids_generated.go b/internal/command/stackpreset/preset_ids_generated.go index e10591b..4503e9e 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 = "9d8a9843ac240b524eeb5c27f4ca8833b411d410d36c6841e14134e1ce8a57d0" +const presetContractSourceSHA256 = "c9322a64711d906f2330ae225c08c69042ad244d9f4fe00d9e8ae0e4815ab7f5" var presetIDs = []string{"agentic_runtime_repo", "generated_docs_contract_repo", "python_service", "python_typescript_service", "typescript_monorepo", "typescript_workspace"} diff --git a/internal/kernel/repositorytransaction/control_inspection_test.go b/internal/kernel/repositorytransaction/control_inspection_test.go index c44a962..4d963e3 100644 --- a/internal/kernel/repositorytransaction/control_inspection_test.go +++ b/internal/kernel/repositorytransaction/control_inspection_test.go @@ -15,6 +15,7 @@ import ( "testing" "github.com/research-engineering/agentic-proofkit/internal/kernel/digest" + "github.com/research-engineering/agentic-proofkit/internal/kernel/rootpath" "github.com/research-engineering/agentic-proofkit/internal/kernel/stablejson" ) @@ -456,6 +457,101 @@ func TestInspectionLeaseFileCannotBeReassertedAsMutable(t *testing.T) { } } +func TestInspectionLeaseObservedOpenPreservesWitnessAndReadCapability(t *testing.T) { + for _, state := range []string{"missing", "regular", "unsafe"} { + t.Run(state, func(t *testing.T) { + rootPath := t.TempDir() + selected := filepath.Join(rootPath, "record") + var kernelErr, leaseErr error + switch state { + case "missing": + kernelErr, leaseErr = fs.ErrNotExist, fs.ErrNotExist + case "regular": + if err := os.WriteFile(selected, []byte("private fixture"), 0o600); err != nil { + t.Fatal(err) + } + case "unsafe": + kernelErr, leaseErr = rootpath.ErrUnsafeRoute, ErrUnsafeInspectionRoute + if err := os.Symlink("private target", selected); err != nil { + t.Fatal(err) + } + } + lease, err := OpenInspectionLease(context.Background(), rootPath) + if err != nil { + t.Fatal(err) + } + defer lease.Close() + kernelFile, kernelWitness, err := rootpath.OpenObservedExactRegularFile(lease.root, "record") + if !errors.Is(err, kernelErr) { + t.Fatal("unexpected kernel outcome") + } + if kernelFile != nil { + if err := kernelFile.Close(); err != nil { + t.Fatal(err) + } + } + file, witness, err := lease.OpenObservedExactRegularFile("record") + if !errors.Is(err, leaseErr) || !kernelWitness.Equal(witness) { + t.Fatal("lease lost kernel witness or changed classification") + } + if file != nil { + if _, mutable := file.(interface{ Write([]byte) (int, error) }); mutable { + t.Fatal("observed file exposes mutation") + } + if _, raw := file.(*os.File); raw { + t.Fatal("observed file exposes raw descriptor") + } + if err := file.Close(); err != nil { + t.Fatal(err) + } + } else if state == "regular" { + t.Fatal("regular observation lost file capability") + } + if err := lease.Close(); err != nil { + t.Fatal(err) + } + file, witness, err = lease.OpenObservedExactRegularFile("record") + if file != nil || err == nil || witness.Equal(witness) { + t.Fatal("closed lease admitted observation") + } + }) + } + var lease *InspectionLease + file, witness, err := lease.OpenObservedExactRegularFile("record") + if file != nil || err == nil || witness.Equal(witness) { + t.Fatal("nil lease admitted observation") + } +} + +func TestInspectionLeaseObservedRouteBindsRootAndMissingPosition(t *testing.T) { + firstRoot, secondRoot := t.TempDir(), t.TempDir() + first, err := OpenInspectionLease(context.Background(), firstRoot) + if err != nil { + t.Fatal(err) + } + defer first.Close() + second, err := OpenInspectionLease(context.Background(), secondRoot) + if err != nil { + t.Fatal(err) + } + defer second.Close() + _, before, err := first.OpenObservedExactRegularFile("docs/record") + if !errors.Is(err, fs.ErrNotExist) { + t.Fatal(err) + } + _, otherRoot, err := second.OpenObservedExactRegularFile("docs/record") + if !errors.Is(err, fs.ErrNotExist) || before.Equal(otherRoot) { + t.Fatal("distinct base roots compared equal") + } + if err := os.Mkdir(filepath.Join(firstRoot, "docs"), 0o700); err != nil { + t.Fatal(err) + } + _, after, err := first.OpenObservedExactRegularFile("docs/record") + if !errors.Is(err, fs.ErrNotExist) || before.Equal(after) { + t.Fatal("lease discarded changed missing terminal") + } +} + func TestInspectControlStateEpochIsDeterministicAndContentBound(t *testing.T) { rootPath := t.TempDir() plan, err := BuildPlan(context.Background(), rootPath, []Target{{Path: "proofkit/a.json", Content: []byte("a\n"), Mode: 0o644}}) diff --git a/internal/kernel/repositorytransaction/inspection_lease.go b/internal/kernel/repositorytransaction/inspection_lease.go index 7634b7a..59cba84 100644 --- a/internal/kernel/repositorytransaction/inspection_lease.go +++ b/internal/kernel/repositorytransaction/inspection_lease.go @@ -104,19 +104,28 @@ func OpenInspectionLease(ctx context.Context, rootPath string) (*InspectionLease // OpenExactRegularFile opens one exact repository-relative regular file // without exposing the mutation-capable confined root. func (lease *InspectionLease) OpenExactRegularFile(relativePath string) (InspectionFile, error) { + file, _, err := lease.OpenObservedExactRegularFile(relativePath) + return file, err +} + +// OpenObservedExactRegularFile projects a read-only file capability and the +// traversal owner's opaque witness, including missing and unsafe terminals. +func (lease *InspectionLease) OpenObservedExactRegularFile(relativePath string) (InspectionFile, rootpath.RouteObservation, error) { if lease == nil || lease.root == nil { - return nil, fmt.Errorf("repository inspection lease is closed") + return nil, rootpath.RouteObservation{}, fmt.Errorf("repository inspection lease is closed") } - file, err := rootpath.OpenExactRegularFile(lease.root, relativePath) + file, observation, err := rootpath.OpenObservedExactRegularFile(lease.root, relativePath) switch { + case errors.Is(err, rootpath.ErrTraversalCleanup): + return nil, rootpath.RouteObservation{}, err case errors.Is(err, rootpath.ErrRouteChanged): - return nil, ErrInspectionRouteChanged + return nil, rootpath.RouteObservation{}, ErrInspectionRouteChanged case errors.Is(err, rootpath.ErrUnsafeRoute): - return nil, ErrUnsafeInspectionRoute + return nil, observation, ErrUnsafeInspectionRoute case err == nil: - return &inspectionFile{file: file}, nil + return &inspectionFile{file: file}, observation, nil default: - return nil, err + return nil, observation, err } } diff --git a/internal/kernel/rootpath/exact.go b/internal/kernel/rootpath/exact.go index df0012f..0640fc1 100644 --- a/internal/kernel/rootpath/exact.go +++ b/internal/kernel/rootpath/exact.go @@ -9,6 +9,8 @@ import ( "io/fs" "os" "path/filepath" + "slices" + "strings" "github.com/research-engineering/agentic-proofkit/internal/kernel/pathidentity" ) @@ -22,6 +24,59 @@ var ( ErrUnsafeRoute = errors.New("exact root path traverses a symlink or non-regular entry") ) +type routeTerminal uint8 + +const ( + routeIncomplete routeTerminal = iota + routeRegular + routeMissing + routeUnsafe +) + +type routeComponent struct { + device uint64 + inode uint64 + mode uint32 +} + +// RouteObservation is an immutable, process-local traversal witness. Its +// private components include the base directory but exclude directory size +// and timestamps, which can change through unrelated sibling writes. +type RouteObservation struct { + route string + components []routeComponent + terminal routeTerminal + position int +} + +// Equal compares complete observations only; a zero or incomplete witness +// never supplies evidence of stability, even when compared with itself. +func (observation RouteObservation) Equal(other RouteObservation) bool { + return observation.complete() && other.complete() && + observation.route == other.route && observation.terminal == other.terminal && + observation.position == other.position && slices.Equal(observation.components, other.components) +} + +func (observation RouteObservation) complete() bool { + if observation.route == "" || observation.position < 0 { + return false + } + last := strings.Count(observation.route, "/") + if observation.position > last { + return false + } + switch observation.terminal { + case routeMissing: + return len(observation.components) == observation.position+1 + case routeUnsafe: + return len(observation.components) == observation.position+2 + case routeRegular: + return observation.position == last && len(observation.components) == last+2 + default: + return false + } +} + // ExactEntryExists reports whether component exists with the exact spelling // supplied by the caller. A portable-equivalent alias is rejected instead of // being treated as the canonical route on a case-insensitive filesystem. diff --git a/internal/kernel/rootpath/observation_unix_test.go b/internal/kernel/rootpath/observation_unix_test.go new file mode 100644 index 0000000..336db82 --- /dev/null +++ b/internal/kernel/rootpath/observation_unix_test.go @@ -0,0 +1,315 @@ +//go:build darwin || linux + +package rootpath + +import ( + "encoding/json" + "errors" + "io" + "io/fs" + "os" + "path/filepath" + "slices" + "testing" + "time" + + "golang.org/x/sys/unix" +) + +func observedRoute(t *testing.T, root *os.Root, path string, wantErr error) RouteObservation { + t.Helper() + file, observation, err := OpenObservedExactRegularFile(root, path) + if !errors.Is(err, wantErr) { + t.Fatalf("observed open error=%v, want %v", err, wantErr) + } + if file != nil { + if wantErr != nil { + t.Fatal("non-regular outcome returned a file") + } + if err := file.Close(); err != nil { + t.Fatal(err) + } + } + return observation +} + +func TestObservedOpenPreservesLegacyResultsAndPrivateWitness(t *testing.T) { + for _, state := range []string{"regular", "missing parent", "missing leaf", "unsafe parent", "directory", "symlink", "FIFO"} { + t.Run(state, func(t *testing.T) { + rootPath := t.TempDir() + parent := filepath.Join(rootPath, "docs") + selected := filepath.Join(parent, "record") + wantErr := error(nil) + if state != "missing parent" && state != "unsafe parent" { + if err := os.Mkdir(parent, 0o700); err != nil { + t.Fatal(err) + } + } + switch state { + case "regular": + if err := os.WriteFile(selected, []byte("private witness fixture"), 0o600); err != nil { + t.Fatal(err) + } + case "missing parent", "missing leaf": + wantErr = fs.ErrNotExist + case "unsafe parent": + wantErr = ErrUnsafeRoute + if err := os.WriteFile(parent, nil, 0o600); err != nil { + t.Fatal(err) + } + case "directory": + wantErr = ErrUnsafeRoute + if err := os.Mkdir(selected, 0o700); err != nil { + t.Fatal(err) + } + case "symlink": + wantErr = ErrUnsafeRoute + if err := os.Symlink("private-link-target", selected); err != nil { + t.Fatal(err) + } + case "FIFO": + wantErr = ErrUnsafeRoute + if err := unix.Mkfifo(selected, 0o600); err != nil { + t.Fatal(err) + } + } + root, err := os.OpenRoot(rootPath) + if err != nil { + t.Fatal(err) + } + defer root.Close() + before := observedRoute(t, root, "docs/record", wantErr) + after := observedRoute(t, root, "docs/record", wantErr) + if !before.Equal(after) || !after.Equal(before) { + t.Fatal("stable route observations differ") + } + position, terminal, count := 1, routeRegular, 3 + switch state { + case "missing parent": + position, terminal, count = 0, routeMissing, 1 + case "missing leaf": + terminal, count = routeMissing, 2 + case "unsafe parent": + position, terminal, count = 0, routeUnsafe, 2 + case "directory", "symlink", "FIFO": + terminal = routeUnsafe + } + if before.position != position || before.terminal != terminal || len(before.components) != count { + t.Fatal("witness did not retain the observed terminal and component count") + } + for index, path := range []string{rootPath, parent, selected}[:count] { + var stat unix.Stat_t + if err := unix.Lstat(path, &stat); err != nil { + t.Fatal(err) + } + component := before.components[index] + if component.device != uint64(stat.Dev) || component.inode != uint64(stat.Ino) || component.mode != uint32(stat.Mode) { + t.Fatal("witness disagrees with independent native component metadata") + } + } + file, err := OpenExactRegularFile(root, "docs/record") + if !errors.Is(err, wantErr) { + t.Fatalf("legacy open error=%v, want %v", err, wantErr) + } + if file != nil { + content, err := io.ReadAll(file) + closeErr := file.Close() + if err != nil || closeErr != nil || string(content) != "private witness fixture" { + t.Fatal("legacy content or cleanup changed") + } + } + encoded, err := json.Marshal(before) + if err != nil || string(encoded) != "{}" { + t.Fatal("route witness exposed wire data") + } + var decoded RouteObservation + if err := json.Unmarshal(encoded, &decoded); err != nil { + t.Fatal(err) + } + if decoded.Equal(before) || decoded.Equal(decoded) { + t.Fatal("wire roundtrip admitted a witness") + } + }) + } +} + +func TestRouteObservationEqualityBindsEveryOperand(t *testing.T) { + rootPath := t.TempDir() + if err := os.MkdirAll(filepath.Join(rootPath, "docs"), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(rootPath, "docs/record"), nil, 0o600); err != nil { + t.Fatal(err) + } + root, err := os.OpenRoot(rootPath) + if err != nil { + t.Fatal(err) + } + defer root.Close() + before := observedRoute(t, root, "docs/record", nil) + mutations := map[string]func(*RouteObservation){ + "route": func(value *RouteObservation) { value.route = "other/record" }, + "terminal": func(value *RouteObservation) { value.terminal = routeUnsafe }, + "position": func(value *RouteObservation) { value.position-- }, + "incomplete": func(value *RouteObservation) { value.terminal = routeIncomplete }, + "truncated": func(value *RouteObservation) { value.components = value.components[:1] }, + "empty route": func(value *RouteObservation) { value.route = "" }, + } + for name, mutate := range mutations { + t.Run(name, func(t *testing.T) { + after := before + mutate(&after) + if before.Equal(after) || after.Equal(before) { + t.Fatal("changed witness operand compared equal") + } + }) + } + // Isolate each equality operand from the other fields of real observations; + // device-only differences need no host-specific second mounted filesystem. + for index := range before.components { + for _, operand := range []string{"device", "inode", "type", "mode"} { + after := before + after.components = slices.Clone(before.components) + switch operand { + case "device": + after.components[index].device++ + case "inode": + after.components[index].inode++ + case "type": + after.components[index].mode ^= unix.S_IFDIR + case "mode": + after.components[index].mode ^= 0o100 + } + if before.Equal(after) || after.Equal(before) { + t.Fatalf("component %d operand %s was ignored", index, operand) + } + } + } + for _, invalid := range []RouteObservation{{}, {route: "record", terminal: routeRegular}, {route: "record", terminal: routeMissing, position: 2}} { + if invalid.Equal(invalid) || before.Equal(invalid) || invalid.Equal(before) { + t.Fatal("incomplete witness was admitted") + } + } + if !before.Equal(observedRoute(t, root, "docs/record", nil)) { + t.Fatal("comparison changed immutable observation") + } +} + +func TestObservedOpenBindsRealRouteChangesWithoutSiblingMetadata(t *testing.T) { + rootPath := t.TempDir() + root, err := os.OpenRoot(rootPath) + if err != nil { + t.Fatal(err) + } + defer root.Close() + before := observedRoute(t, root, "docs/record", fs.ErrNotExist) + parent := filepath.Join(rootPath, "docs") + if err := os.Mkdir(parent, 0o700); err != nil { + t.Fatal(err) + } + after := observedRoute(t, root, "docs/record", fs.ErrNotExist) + if before.Equal(after) { + t.Fatal("missing terminal position was ignored") + } + if err := os.WriteFile(filepath.Join(parent, "record"), []byte("content"), 0o600); err != nil { + t.Fatal(err) + } + before = observedRoute(t, root, "docs/record", nil) + if err := os.Link(filepath.Join(parent, "record"), filepath.Join(parent, "alias")); err != nil { + t.Fatal(err) + } + if before.Equal(observedRoute(t, root, "docs/alias", nil)) { + t.Fatal("hardlink path key was ignored") + } + info, err := os.Stat(parent) + if err != nil { + t.Fatal(err) + } + changedTime := info.ModTime().Add(time.Hour) + if err := os.Chtimes(parent, changedTime, changedTime); err != nil { + t.Fatal(err) + } + if !before.Equal(observedRoute(t, root, "docs/record", nil)) { + t.Fatal("sibling write or parent timestamp changed witness") + } + if err := os.Rename(parent, parent+".saved"); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(parent, 0o700); err != nil { + t.Fatal(err) + } + if err := os.Link(filepath.Join(parent+".saved", "record"), filepath.Join(parent, "record")); err != nil { + t.Fatal(err) + } + if before.Equal(observedRoute(t, root, "docs/record", nil)) { + t.Fatal("parent replacement preserved witness") + } +} + +func TestObservedOpenRejectsAdmissionModeDriftAndCleanup(t *testing.T) { + for _, scenario := range []string{"parent mode", "leaf mode", "missing cleanup", "unsafe cleanup", "regular cleanup", "alias"} { + t.Run(scenario, func(t *testing.T) { + rootPath := t.TempDir() + parent := filepath.Join(rootPath, "docs") + if err := os.Mkdir(parent, 0o700); err != nil { + t.Fatal(err) + } + selected := filepath.Join(parent, "record") + switch scenario { + case "missing cleanup": + case "unsafe cleanup": + if err := os.Mkdir(selected, 0o700); err != nil { + t.Fatal(err) + } + case "alias": + if err := os.WriteFile(filepath.Join(parent, "RECORD"), nil, 0o600); err != nil { + t.Fatal(err) + } + default: + if err := os.WriteFile(selected, nil, 0o600); err != nil { + t.Fatal(err) + } + } + root, err := os.OpenRoot(rootPath) + if err != nil { + t.Fatal(err) + } + defer root.Close() + operations := nativeTraversalOperations() + wantErr := ErrRouteChanged + if scenario == "missing cleanup" || scenario == "unsafe cleanup" || scenario == "regular cleanup" { + wantErr = ErrTraversalCleanup + operations.closeFile = func(file *os.File) error { + err := file.Close() + if file.Name() == "exact-root-entry" { + return errors.Join(err, errors.New("injected cleanup failure")) + } + return err + } + } + if scenario == "alias" { + wantErr = ErrAmbiguousRoute + } + file, observation, err := openObservedExactRegularFileWithOperations(root, "docs/record", func(index int) { + if scenario == "parent mode" && index == 0 { + if err := os.Chmod(parent, 0o500); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.Chmod(parent, 0o700) }) + } + if scenario == "leaf mode" && index == 1 { + if err := os.Chmod(selected, 0o400); err != nil { + t.Fatal(err) + } + } + }, operations) + if file != nil { + _ = file.Close() + t.Fatal("failed admission returned a file") + } + if !errors.Is(err, wantErr) || observation.Equal(observation) { + t.Fatalf("error=%v, want %v and no complete witness", err, wantErr) + } + }) + } +} diff --git a/internal/kernel/rootpath/open_other.go b/internal/kernel/rootpath/open_other.go index e00e03c..b16e4d7 100644 --- a/internal/kernel/rootpath/open_other.go +++ b/internal/kernel/rootpath/open_other.go @@ -9,6 +9,12 @@ import ( // OpenExactRegularFile reports that descriptor-relative traversal is not // available outside the package's supported runtime platforms. -func OpenExactRegularFile(*os.Root, string) (*os.File, error) { - return nil, fmt.Errorf("exact root file traversal requires darwin or linux") +func OpenExactRegularFile(root *os.Root, relativePath string) (*os.File, error) { + file, _, err := OpenObservedExactRegularFile(root, relativePath) + return file, err +} + +// OpenObservedExactRegularFile cannot admit a witness on unsupported platforms. +func OpenObservedExactRegularFile(*os.Root, string) (*os.File, RouteObservation, error) { + return nil, RouteObservation{}, fmt.Errorf("exact root file traversal requires darwin or linux") } diff --git a/internal/kernel/rootpath/open_unix.go b/internal/kernel/rootpath/open_unix.go index 7cdb774..bebd324 100644 --- a/internal/kernel/rootpath/open_unix.go +++ b/internal/kernel/rootpath/open_unix.go @@ -33,59 +33,80 @@ func OpenExactRegularFile(root *os.Root, relativePath string) (*os.File, error) return openExactRegularFile(root, relativePath, nil) } +// OpenObservedExactRegularFile also returns a private route witness for a +// regular file, ordinary absence, or unsafe entry, without a second traversal. +// Operational failures return no admitted witness. +func OpenObservedExactRegularFile(root *os.Root, relativePath string) (*os.File, RouteObservation, error) { + return openObservedExactRegularFileWithOperations(root, relativePath, nil, nativeTraversalOperations()) +} + func openExactRegularFile(root *os.Root, relativePath string, hook traversalHook) (*os.File, error) { return openExactRegularFileWithOperations(root, relativePath, hook, nativeTraversalOperations()) } func openExactRegularFileWithOperations(root *os.Root, relativePath string, hook traversalHook, operations traversalOperations) (*os.File, error) { + file, _, err := openObservedExactRegularFileWithOperations(root, relativePath, hook, operations) + return file, err +} + +func openObservedExactRegularFileWithOperations(root *os.Root, relativePath string, hook traversalHook, operations traversalOperations) (*os.File, RouteObservation, error) { if root == nil || relativePath == "" || path.IsAbs(relativePath) || path.Clean(relativePath) != relativePath { - return nil, fmt.Errorf("exact root file route is invalid") + return nil, RouteObservation{}, fmt.Errorf("exact root file route is invalid") } if operations.closeFD == nil || operations.closeFile == nil { - return nil, fmt.Errorf("exact root file traversal operations are incomplete") + return nil, RouteObservation{}, fmt.Errorf("exact root file traversal operations are incomplete") } components := strings.Split(relativePath, "/") for _, component := range components { if component == "" || component == "." || component == ".." { - return nil, fmt.Errorf("exact root file route is invalid") + return nil, RouteObservation{}, fmt.Errorf("exact root file route is invalid") } } current, err := root.Open(".") if err != nil { - return nil, fmt.Errorf("open exact root file base") + return nil, RouteObservation{}, fmt.Errorf("open exact root file base") } + var base unix.Stat_t + if err := unix.Fstat(int(current.Fd()), &base); err != nil { + return nil, RouteObservation{}, errors.Join(fmt.Errorf("inspect exact root file base"), closeTraversalFile(operations, current)) + } + observation := RouteObservation{route: relativePath, components: make([]routeComponent, 0, len(components)+1)} + observation.components = append(observation.components, observeRouteComponent(base)) for index, component := range components { _, exists, err := exactDirectoryEntry(current, component) if err != nil { if closeErr := closeTraversalFile(operations, current); closeErr != nil { - return nil, closeErr + return nil, RouteObservation{}, closeErr } - return nil, err + return nil, RouteObservation{}, err } if !exists { if closeErr := closeTraversalFile(operations, current); closeErr != nil { - return nil, closeErr + return nil, RouteObservation{}, closeErr } - return nil, fs.ErrNotExist + observation.terminal, observation.position = routeMissing, index + return nil, observation, fs.ErrNotExist } var expected unix.Stat_t if err := unix.Fstatat(int(current.Fd()), component, &expected, unix.AT_SYMLINK_NOFOLLOW); err != nil { if closeErr := closeTraversalFile(operations, current); closeErr != nil { - return nil, closeErr + return nil, RouteObservation{}, closeErr } if errors.Is(err, unix.ENOENT) { - return nil, ErrRouteChanged + return nil, RouteObservation{}, ErrRouteChanged } - return nil, fmt.Errorf("inspect exact root file route") + return nil, RouteObservation{}, fmt.Errorf("inspect exact root file route") } + observation.components = append(observation.components, observeRouteComponent(expected)) last := index == len(components)-1 kind := expected.Mode & unix.S_IFMT if kind == unix.S_IFLNK || (!last && kind != unix.S_IFDIR) || (last && kind != unix.S_IFREG) { if closeErr := closeTraversalFile(operations, current); closeErr != nil { - return nil, closeErr + return nil, RouteObservation{}, closeErr } - return nil, ErrUnsafeRoute + observation.terminal, observation.position = routeUnsafe, index + return nil, observation, ErrUnsafeRoute } if hook != nil { hook(index) @@ -97,33 +118,38 @@ func openExactRegularFileWithOperations(root *os.Root, relativePath string, hook fd, openErr := unix.Openat(int(current.Fd()), component, flags, 0) if openErr != nil { if closeErr := closeTraversalFile(operations, current); closeErr != nil { - return nil, closeErr + return nil, RouteObservation{}, closeErr } if errors.Is(openErr, unix.ENOENT) || errors.Is(openErr, unix.ELOOP) || errors.Is(openErr, unix.ENOTDIR) { - return nil, ErrRouteChanged + return nil, RouteObservation{}, ErrRouteChanged } - return nil, fmt.Errorf("open exact root file route") + return nil, RouteObservation{}, fmt.Errorf("open exact root file route") } var observed unix.Stat_t - if statErr := unix.Fstat(fd, &observed); statErr != nil || expected.Dev != observed.Dev || expected.Ino != observed.Ino { + if statErr := unix.Fstat(fd, &observed); statErr != nil || observeRouteComponent(expected) != observeRouteComponent(observed) { if closeErr := errors.Join(closeTraversalFD(operations, fd), closeTraversalFile(operations, current)); closeErr != nil { - return nil, closeErr + return nil, RouteObservation{}, closeErr } - return nil, ErrRouteChanged + return nil, RouteObservation{}, ErrRouteChanged } next := os.NewFile(uintptr(fd), "exact-root-entry") if next == nil { if closeErr := errors.Join(closeTraversalFD(operations, fd), closeTraversalFile(operations, current)); closeErr != nil { - return nil, closeErr + return nil, RouteObservation{}, closeErr } - return nil, fmt.Errorf("adopt exact root file descriptor") + return nil, RouteObservation{}, fmt.Errorf("adopt exact root file descriptor") } if closeErr := closeTraversalFile(operations, current); closeErr != nil { - return nil, errors.Join(closeErr, closeTraversalFile(operations, next)) + return nil, RouteObservation{}, errors.Join(closeErr, closeTraversalFile(operations, next)) } current = next } - return current, nil + observation.terminal, observation.position = routeRegular, len(components)-1 + return current, observation, nil +} + +func observeRouteComponent(stat unix.Stat_t) routeComponent { + return routeComponent{device: uint64(stat.Dev), inode: uint64(stat.Ino), mode: uint32(stat.Mode)} } func closeTraversalFile(operations traversalOperations, file *os.File) error { diff --git a/internal/tools/coveragemetrics/required_inventory.go b/internal/tools/coveragemetrics/required_inventory.go index cf7e3af..00501b3 100644 --- a/internal/tools/coveragemetrics/required_inventory.go +++ b/internal/tools/coveragemetrics/required_inventory.go @@ -222,7 +222,24 @@ func requiredBindingWitnessInventory() map[inventoryKey]requiredInventoryEntry { selectors: []string{ "TestCheckCancellationBeforeAndAfterObservation", "TestCheckCleanupFailureInvalidatesEveryState", "TestCheckDetectsChangesWithinAnObservation", "TestCheckFileIOAndCleanupFailures", "TestCheckOpenErrorsAreNotMissingOrInvalid", "TestCheckPermissionDeniedIsNotMissing", - "TestCheckReobservesBytesStateAndOpenedIdentityIndependently", "TestCheckVerifiesRootAfterBothObservations", + "TestCheckReobservesBytesStateAndOpenedIdentityIndependently", + "TestCheckRouteWitnessIgnoresUnrelatedSiblingWrites", "TestCheckRouteWitnessRejectsZeroInEveryState", + "TestCheckVerifiesRootAfterBothObservations", + }, + }, + {"REQ-PROOFKIT-WORKFLOW-017", "proofkit.agent-workflow.integration-complete-route-reobservation"}: { + witnessPath: "internal/command/agentintegration/check_unix_test.go", + selectors: []string{"TestCheckReobservesCompleteRoute"}, + }, + {"REQ-PROOFKIT-WORKFLOW-017", "proofkit.agent-workflow.integration-inspection-route-capability"}: { + witnessPath: "internal/kernel/repositorytransaction/control_inspection_test.go", + selectors: []string{"TestInspectionLeaseObservedOpenPreservesWitnessAndReadCapability", "TestInspectionLeaseObservedRouteBindsRootAndMissingPosition"}, + }, + {"REQ-PROOFKIT-WORKFLOW-017", "proofkit.agent-workflow.integration-opaque-route-observation"}: { + witnessPath: "internal/kernel/rootpath/observation_unix_test.go", + selectors: []string{ + "TestObservedOpenBindsRealRouteChangesWithoutSiblingMetadata", "TestObservedOpenPreservesLegacyResultsAndPrivateWitness", + "TestObservedOpenRejectsAdmissionModeDriftAndCleanup", "TestRouteObservationEqualityBindsEveryOperand", }, }, {"REQ-PROOFKIT-WORKFLOW-017", "proofkit.agent-workflow.integration-pre-io-cli-admission"}: { diff --git a/proofkit/cli-contract.v2.json b/proofkit/cli-contract.v2.json index 0324686..3dad2fa 100644 --- a/proofkit/cli-contract.v2.json +++ b/proofkit/cli-contract.v2.json @@ -153,7 +153,7 @@ }, { "path": "internal/kernel/repositorytransaction", - "canonicalDigest": "sha256:fd549a1f5795524d4f7feed3a4ebcef03e3d6fe53d6ce7e8e24ff5014de9a9cd", + "canonicalDigest": "sha256:4279c057766efe379f92e1fbfdbafa1f961a1a15c0b113230ea6cb8cfd0d7ae1", "evidenceClass": "source_checkout" } ], @@ -291,7 +291,7 @@ }, { "path": "internal/kernel/repositorytransaction", - "canonicalDigest": "sha256:fd549a1f5795524d4f7feed3a4ebcef03e3d6fe53d6ce7e8e24ff5014de9a9cd", + "canonicalDigest": "sha256:4279c057766efe379f92e1fbfdbafa1f961a1a15c0b113230ea6cb8cfd0d7ae1", "evidenceClass": "source_checkout" } ], @@ -405,7 +405,7 @@ }, { "path": "internal/kernel/repositorytransaction", - "canonicalDigest": "sha256:fd549a1f5795524d4f7feed3a4ebcef03e3d6fe53d6ce7e8e24ff5014de9a9cd", + "canonicalDigest": "sha256:4279c057766efe379f92e1fbfdbafa1f961a1a15c0b113230ea6cb8cfd0d7ae1", "evidenceClass": "source_checkout" } ], @@ -2478,17 +2478,17 @@ }, { "path": "internal/command/agentintegration", - "canonicalDigest": "sha256:0e6f808d4fa7515383915426cf9678ee88f916c6baf77006cc9a500fa61fdbec", + "canonicalDigest": "sha256:d85cf0fc38bbd28f86ececb3e41f9e04fca7f2741507bf006cd1a55500484505", "evidenceClass": "source_checkout" }, { "path": "internal/kernel/repositorytransaction", - "canonicalDigest": "sha256:fd549a1f5795524d4f7feed3a4ebcef03e3d6fe53d6ce7e8e24ff5014de9a9cd", + "canonicalDigest": "sha256:4279c057766efe379f92e1fbfdbafa1f961a1a15c0b113230ea6cb8cfd0d7ae1", "evidenceClass": "source_checkout" }, { "path": "internal/kernel/rootpath", - "canonicalDigest": "sha256:160244fcf85783eb77d19ccbb9c7bf80de38bbb190fcc5b9358f3d5701844607", + "canonicalDigest": "sha256:948e23e5063da448335fbcd9a1bee0a0d92e9b07092fafeec7a1ca935d425055", "evidenceClass": "source_checkout" } ], @@ -2577,7 +2577,7 @@ }, { "path": "internal/command/agentintegration", - "canonicalDigest": "sha256:0e6f808d4fa7515383915426cf9678ee88f916c6baf77006cc9a500fa61fdbec", + "canonicalDigest": "sha256:d85cf0fc38bbd28f86ececb3e41f9e04fca7f2741507bf006cd1a55500484505", "evidenceClass": "source_checkout" } ], diff --git a/proofkit/requirement-bindings.json b/proofkit/requirement-bindings.json index d07f020..d2dad8a 100644 --- a/proofkit/requirement-bindings.json +++ b/proofkit/requirement-bindings.json @@ -6811,6 +6811,14 @@ "selector": "TestCheckReobservesBytesStateAndOpenedIdentityIndependently", "command": "go test ./internal/command/agentintegration -run '^TestCheckReobservesBytesStateAndOpenedIdentityIndependently$'" }, + { + "selector": "TestCheckRouteWitnessIgnoresUnrelatedSiblingWrites", + "command": "go test ./internal/command/agentintegration -run '^TestCheckRouteWitnessIgnoresUnrelatedSiblingWrites$'" + }, + { + "selector": "TestCheckRouteWitnessRejectsZeroInEveryState", + "command": "go test ./internal/command/agentintegration -run '^TestCheckRouteWitnessRejectsZeroInEveryState$'" + }, { "selector": "TestCheckVerifiesRootAfterBothObservations", "command": "go test ./internal/command/agentintegration -run '^TestCheckVerifiesRootAfterBothObservations$'" @@ -7001,6 +7009,79 @@ "environmentClasses": [ "local-go" ] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-017", + "scenarioId": "proofkit.agent-workflow.integration-complete-route-reobservation", + "witnessId": "proofkit.agent-integration.complete-route-reobservation-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/command/agentintegration/check_unix_test.go", + "witnessSelectors": [ + { + "selector": "TestCheckReobservesCompleteRoute", + "command": "go test ./internal/command/agentintegration -run '^TestCheckReobservesCompleteRoute$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-017", + "scenarioId": "proofkit.agent-workflow.integration-inspection-route-capability", + "witnessId": "proofkit.agent-integration.inspection-route-capability-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/kernel/repositorytransaction/control_inspection_test.go", + "witnessSelectors": [ + { + "selector": "TestInspectionLeaseObservedOpenPreservesWitnessAndReadCapability", + "command": "go test ./internal/kernel/repositorytransaction -run '^TestInspectionLeaseObservedOpenPreservesWitnessAndReadCapability$'" + }, + { + "selector": "TestInspectionLeaseObservedRouteBindsRootAndMissingPosition", + "command": "go test ./internal/kernel/repositorytransaction -run '^TestInspectionLeaseObservedRouteBindsRootAndMissingPosition$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, + { + "requirementId": "REQ-PROOFKIT-WORKFLOW-017", + "scenarioId": "proofkit.agent-workflow.integration-opaque-route-observation", + "witnessId": "proofkit.agent-integration.opaque-route-observation-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/kernel/rootpath/observation_unix_test.go", + "witnessSelectors": [ + { + "selector": "TestObservedOpenBindsRealRouteChangesWithoutSiblingMetadata", + "command": "go test ./internal/kernel/rootpath -run '^TestObservedOpenBindsRealRouteChangesWithoutSiblingMetadata$'" + }, + { + "selector": "TestObservedOpenPreservesLegacyResultsAndPrivateWitness", + "command": "go test ./internal/kernel/rootpath -run '^TestObservedOpenPreservesLegacyResultsAndPrivateWitness$'" + }, + { + "selector": "TestObservedOpenRejectsAdmissionModeDriftAndCleanup", + "command": "go test ./internal/kernel/rootpath -run '^TestObservedOpenRejectsAdmissionModeDriftAndCleanup$'" + }, + { + "selector": "TestRouteObservationEqualityBindsEveryOperand", + "command": "go test ./internal/kernel/rootpath -run '^TestRouteObservationEqualityBindsEveryOperand$'" + } + ], + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] } ], "witnessCommands": [ From 50bbd4663b8aed02dec3c0aebf344164adcff42f Mon Sep 17 00:00:00 2001 From: iperev Date: Sat, 5 Sep 2026 11:50:52 +0200 Subject: [PATCH 3/3] test: verify opaque route witnesses inside JSON envelopes --- internal/kernel/rootpath/observation_unix_test.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/internal/kernel/rootpath/observation_unix_test.go b/internal/kernel/rootpath/observation_unix_test.go index 336db82..acf0b6b 100644 --- a/internal/kernel/rootpath/observation_unix_test.go +++ b/internal/kernel/rootpath/observation_unix_test.go @@ -118,15 +118,19 @@ func TestObservedOpenPreservesLegacyResultsAndPrivateWitness(t *testing.T) { t.Fatal("legacy content or cleanup changed") } } - encoded, err := json.Marshal(before) - if err != nil || string(encoded) != "{}" { + // A public envelope must neither disclose nor re-admit its opaque child. + type observationEnvelope struct { + Route RouteObservation `json:"route"` + } + encoded, err := json.Marshal(observationEnvelope{Route: before}) + if err != nil || string(encoded) != `{"route":{}}` { t.Fatal("route witness exposed wire data") } - var decoded RouteObservation + var decoded observationEnvelope if err := json.Unmarshal(encoded, &decoded); err != nil { t.Fatal(err) } - if decoded.Equal(before) || decoded.Equal(decoded) { + if decoded.Route.Equal(before) || decoded.Route.Equal(decoded.Route) { t.Fatal("wire roundtrip admitted a witness") } })