From 2c3ea802b411020cf8ea0128dab8ec808f85eb6c Mon Sep 17 00:00:00 2001 From: Pengfei Hu Date: Mon, 13 Jul 2026 23:36:17 -0700 Subject: [PATCH 1/3] P0: make verification identity reproducible --- .agents/skills/agents-shipgate/SKILL.md | 2 +- .../references/report-reading.md | 3 +- .claude-plugin/marketplace.json | 2 +- .claude/commands/shipgate.md | 6 +- .cursor/rules/agents-shipgate.mdc | 3 +- .well-known/agents-shipgate.json | 71 +- AGENTS.md | 25 +- CHANGELOG.md | 27 + README.md | 36 +- STABILITY.md | 90 +- action.yml | 19 + .../.agents-shipgate-kit-metadata.json | 6 +- adoption-kits/claude-code-skill/SKILL.md | 13 +- .../.agents-shipgate-kit-metadata.json | 6 +- adoption-kits/codex-skill/SKILL.md | 2 +- .../codex-skill/references/report-reading.md | 3 +- benchmark/safety-qualification/README.md | 13 +- docs/INDEX.md | 27 +- docs/agent-boundary-result-schema.v1.json | 2 +- docs/agent-contract-current.md | 73 +- docs/agent-handoff-schema.v5.json | 1101 +++ docs/agent-native-merge-contract.md | 2 +- docs/agent-result-schema.v2.json | 2 +- docs/agents/protocol.md | 2 +- docs/ai-search-summary.md | 6 +- docs/architecture.md | 10 +- docs/attestation-schema.v0.5.json | 772 ++ docs/autofix-policy.md | 2 +- docs/baseline.md | 2 +- docs/distribution.md | 2 +- docs/examples.md | 11 +- .../capability-lock.v0.6.example.json | 2 +- docs/faq.md | 12 +- docs/integrations.md | 2 +- docs/mcp-server.md | 4 +- docs/org-evidence-bundle-schema.v2.json | 281 + docs/overview.md | 2 +- docs/packet-schema.v0.12.json | 2239 +++++ docs/passed-verdict-contract.md | 7 +- docs/registry-schema.v0.4.json | 618 ++ docs/report-reading-for-agents.md | 6 +- docs/report-schema.v0.34.json | 7417 +++++++++++++++++ docs/report-sensitive-fields.json | 5 + docs/report-v1-consolidation-rc.md | 2 +- docs/target-repo-agent-snippets.md | 9 +- ...ification-artifact-manifest-schema.v1.json | 78 + docs/verification-plan-schema.v1.json | 445 + docs/verification-receipt-schema.v1.json | 201 + docs/verification-reproducibility.md | 141 + docs/verification-unit-result-schema.v1.json | 90 + docs/verifier-schema.v0.5.json | 2311 +++++ docs/verify-run-schema.v3.json | 1137 +++ .../agent-protocol/expected/block-stop.json | 2 +- .../expected/missing-install.json | 2 +- .../expected/policy-bypass.json | 2 +- .../agent-protocol/expected/repair-after.json | 2 +- .../expected/repair-before.json | 2 +- .../expected/stale-install.json | 2 +- llms-full.txt | 100 +- llms.txt | 27 +- .../agents-shipgate/.codex-plugin/plugin.json | 2 +- .../skills/agents-shipgate/SKILL.md | 2 +- .../references/report-reading.md | 3 +- .../claude-code/.claude-plugin/plugin.json | 2 +- plugins/claude-code/commands/shipgate.md | 6 +- .../skills/agents-shipgate/SKILL.md | 13 +- pyproject.toml | 2 +- samples/conductor_agent/expected/report.json | 9 +- .../simple_crewai_agent/expected/report.json | 9 +- .../expected/report.json | 9 +- .../expected/report.json | 161 +- .../support_refund_agent/expected/packet.html | 2 +- .../support_refund_agent/expected/packet.json | 7 +- .../support_refund_agent/expected/packet.md | 2 +- .../support_refund_agent/expected/report.json | 345 +- scripts/generate_schemas.py | 88 +- scripts/github_action_outputs.py | 27 + scripts/run_safety_qualification.py | 80 +- skills/agents-shipgate/SKILL.md | 13 +- src/agents_shipgate/__init__.py | 2 +- src/agents_shipgate/cli/attest.py | 24 +- .../agent_instructions/renderers/agents_md.py | 3 +- .../agent_instructions/renderers/claude_md.py | 3 +- .../agent_instructions/renderers/cursor.py | 3 +- .../cli/discovery/local_contract.py | 16 +- src/agents_shipgate/cli/main.py | 14 +- src/agents_shipgate/cli/org.py | 61 +- src/agents_shipgate/cli/registry.py | 39 +- src/agents_shipgate/cli/verification.py | 522 ++ src/agents_shipgate/cli/verify/git.py | 109 +- .../cli/verify/orchestrator.py | 456 +- src/agents_shipgate/core/agent_handoff.py | 26 +- src/agents_shipgate/core/baseline.py | 47 +- src/agents_shipgate/core/evaluation_clock.py | 28 + .../core/severity_overrides.py | 15 +- .../core/verification_identity.py | 860 ++ src/agents_shipgate/packet/json_packet.py | 29 +- src/agents_shipgate/schemas/agent_handoff.py | 13 +- src/agents_shipgate/schemas/attestation.py | 28 +- src/agents_shipgate/schemas/contract.py | 65 +- .../schemas/org_evidence_bundle.py | 16 +- src/agents_shipgate/schemas/packet.py | 8 +- src/agents_shipgate/schemas/registry.py | 17 +- src/agents_shipgate/schemas/report.py | 9 +- .../schemas/safety_qualification.py | 26 +- .../schemas/verification_identity.py | 407 + src/agents_shipgate/schemas/verifier.py | 19 +- src/agents_shipgate/schemas/verify_run.py | 91 +- .../claude-code-block-stop.json | 2 +- .../agent_protocol/codex-block-stop.json | 2 +- .../agent_protocol/codex-repair-after.json | 2 +- .../agent_protocol/codex-repair-before.json | 2 +- .../agent_protocol/cursor-block-stop.json | 2 +- .../agent_protocol/missing-install.json | 2 +- .../golden/agent_protocol/stale-install.json | 2 +- .../agents_requirement_removed.json | 2 +- .../codex_boundary_result/docs_only.json | 2 +- .../github_action_removed.json | 2 +- .../codex_boundary_result/malformed_toml.json | 2 +- .../mcp_auto_approve_write.json | 2 +- .../network_wildcard.json | 2 +- .../python_refactor.json | 2 +- .../unknown_permission_key.json | 2 +- tests/test_adapter_static_only.py | 84 +- tests/test_agent_handoff.py | 8 +- tests/test_agent_instructions_apply.py | 23 +- tests/test_agent_instructions_renderers.py | 33 +- tests/test_agent_mode.py | 10 +- tests/test_attest.py | 44 +- tests/test_capability_domain.py | 6 +- tests/test_cli.py | 17 +- tests/test_conductor.py | 25 +- tests/test_evidence_packet.py | 20 +- tests/test_governance_benchmark.py | 6 +- tests/test_local_contract.py | 32 +- tests/test_mcp_server.py | 2 +- tests/test_org_governance.py | 35 +- .../test_p0_verification_identity_canaries.py | 221 + tests/test_public_surface_contract.py | 61 +- tests/test_registry_ledger.py | 14 +- tests/test_safety_qualification.py | 185 +- tests/test_safety_qualification_release.py | 8 +- tests/test_schema_boundaries.py | 33 +- tests/test_v07_metadata_roundtrip.py | 4 +- tests/test_verification_git_snapshot.py | 57 + tests/test_verifier_control_contract.py | 95 +- tests/test_verify.py | 9 +- tests/test_verify_orchestrator.py | 38 + tests/test_verify_run.py | 255 +- 149 files changed, 21481 insertions(+), 1101 deletions(-) create mode 100644 docs/agent-handoff-schema.v5.json create mode 100644 docs/attestation-schema.v0.5.json create mode 100644 docs/org-evidence-bundle-schema.v2.json create mode 100644 docs/packet-schema.v0.12.json create mode 100644 docs/registry-schema.v0.4.json create mode 100644 docs/report-schema.v0.34.json create mode 100644 docs/verification-artifact-manifest-schema.v1.json create mode 100644 docs/verification-plan-schema.v1.json create mode 100644 docs/verification-receipt-schema.v1.json create mode 100644 docs/verification-reproducibility.md create mode 100644 docs/verification-unit-result-schema.v1.json create mode 100644 docs/verifier-schema.v0.5.json create mode 100644 docs/verify-run-schema.v3.json create mode 100644 src/agents_shipgate/cli/verification.py create mode 100644 src/agents_shipgate/core/evaluation_clock.py create mode 100644 src/agents_shipgate/core/verification_identity.py create mode 100644 src/agents_shipgate/schemas/verification_identity.py create mode 100644 tests/test_p0_verification_identity_canaries.py create mode 100644 tests/test_verification_git_snapshot.py diff --git a/.agents/skills/agents-shipgate/SKILL.md b/.agents/skills/agents-shipgate/SKILL.md index 11fbbeaf..8a5b6d38 100644 --- a/.agents/skills/agents-shipgate/SKILL.md +++ b/.agents/skills/agents-shipgate/SKILL.md @@ -20,7 +20,7 @@ Do not use it for general linting, runtime monitoring, evals, model-output quali 5. Default first-time CI to advisory mode. Do not enable release-blocking CI or save a baseline until a human has reviewed current findings. 6. For local agent control, run `shipgate check --agent codex --workspace . --format agent-boundary-json` and read the stdout `shipgate.agent_boundary_result/v1` object. Switch on `control.state`; follow only `control.next_action`, `control.allowed_next_commands`, and `control.human_review`. Treat `decision` as diagnostic context only. 7. Before editing `shipgate.yaml`, Shipgate CI, AGENTS/CLAUDE/Cursor rules, policy packs, baselines, waivers, suppressions, Codex hooks/config, Codex plugin manifests, `.mcp.json`, `.app.json`, or `SKILL.md`, plan to run `agents-shipgate verify` before completion and route trust-root review to a human when the verifier requires it. -8. For full PR verification, read `agents-shipgate-reports/agent-handoff.json` first and switch on `control.state`, then read `verifier.json` for detailed control state, `verify-run.json` for reproducibility metadata, and `report.json` for reviewer detail; `report.json.release_decision.decision` remains the release gate. +8. For full PR verification, validate `agents-shipgate-reports/verification-receipt.json` first, then read `agent-handoff.json` and switch on `control.state`; read `verifier.json` for detailed control state, `verify-run.json` for the request graph, and `report.json` for reviewer detail. `report.json.release_decision.decision` remains the release gate. 9. Auto-apply only high-confidence safe patches. Do not auto-assert action effect, action authority, agent bindings, approval, confirmation, idempotency, broad-scope, prohibited-action, or runtime-trace evidence. 10. Ensure `.gitignore` covers `agents-shipgate-reports/` before committing. diff --git a/.agents/skills/agents-shipgate/references/report-reading.md b/.agents/skills/agents-shipgate/references/report-reading.md index db3e7974..87c649eb 100644 --- a/.agents/skills/agents-shipgate/references/report-reading.md +++ b/.agents/skills/agents-shipgate/references/report-reading.md @@ -1,6 +1,7 @@ # Reading Agents Shipgate Reports -For verify runs, read `agents-shipgate-reports/agent-handoff.json` first. Then +For verify runs, validate `agents-shipgate-reports/verification-receipt.json` +first. Then read `agents-shipgate-reports/agent-handoff.json`. After that, read `agents-shipgate-reports/verifier.json` for detailed control context and `agents-shipgate-reports/report.json` for findings. Do not scrape Markdown. diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 2327d81b..d4b1114b 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "name": "agents-shipgate", "source": "./plugins/claude-code", "description": "Run Agents Shipgate Tool-Use Readiness workflows from Claude Code: detect whether Shipgate applies, bootstrap the advisory gate, verify agent-capability PRs, and triage findings without fabricating human approval. Requires the agents-shipgate CLI (pipx install agents-shipgate, runtime contract 15).", - "version": "0.16.0b5", + "version": "0.16.0b6", "author": { "name": "Three Moons Lab", "url": "https://threemoonslab.com/" diff --git a/.claude/commands/shipgate.md b/.claude/commands/shipgate.md index ee65d5d8..55f75f8c 100644 --- a/.claude/commands/shipgate.md +++ b/.claude/commands/shipgate.md @@ -48,7 +48,8 @@ Required behavior (do not skip): 1. Set `AGENTS_SHIPGATE_AGENT_MODE=1` for every CLI call so errors emit a `next_action` JSON line on stderr. 2. Run `agents-shipgate contract --json` when available and use it to verify the installed CLI's schema versions and gating signal. -3. For verifier runs, parse `agents-shipgate-reports/agent-handoff.json` first, +3. For verifier runs, validate `agents-shipgate-reports/verification-receipt.json` first, + then parse `agents-shipgate-reports/agent-handoff.json`, then `verifier.json`, `verify-run.json`, and `report.json.release_decision.decision` as the release gate. 4. For check runs, parse stdout as `shipgate.agent_boundary_result/v1` and @@ -77,7 +78,8 @@ permissions, policies, CI gates, or `shipgate.yaml`, run the verifier: agents-shipgate verify --base origin/main --head HEAD --json ``` -Read `agents-shipgate-reports/agent-handoff.json` first and lead with +Validate `agents-shipgate-reports/verification-receipt.json` first, then read +`agents-shipgate-reports/agent-handoff.json` and lead with `gate.merge_verdict` (a deterministic projection of `release_decision.decision`, which remains the gate in `report.json`), then the authoritative substrate `agents-shipgate-reports/verifier.json` and supporting/provisional diff --git a/.cursor/rules/agents-shipgate.mdc b/.cursor/rules/agents-shipgate.mdc index fe7ce775..583e89c7 100644 --- a/.cursor/rules/agents-shipgate.mdc +++ b/.cursor/rules/agents-shipgate.mdc @@ -58,7 +58,8 @@ For local verification, run: For committed PR/CI verification, run `agents-shipgate verify --base origin/main --head HEAD --json` after making the base ref available; it never -fetches. Read `agents-shipgate-reports/agent-handoff.json` first for +fetches. Validate `agents-shipgate-reports/verification-receipt.json` first, +then read `agents-shipgate-reports/agent-handoff.json` for `gate.merge_verdict`, `gate.can_merge_without_human`, and `control`; then read `agents-shipgate-reports/verifier.json` for detailed control context, `agents-shipgate-reports/verify-run.json` for reproducibility metadata, and diff --git a/.well-known/agents-shipgate.json b/.well-known/agents-shipgate.json index 51832292..417c9047 100644 --- a/.well-known/agents-shipgate.json +++ b/.well-known/agents-shipgate.json @@ -3,7 +3,7 @@ "name": "agents-shipgate", "display_name": "Agents Shipgate", "tagline": "The deterministic merge gate for AI-generated agent capability changes", - "version": "0.16.0b5", + "version": "0.16.0b6", "license": "Apache-2.0", "publisher": { "name": "Three Moons Lab", @@ -102,6 +102,10 @@ "install_ai_coding_workflow": "agents-shipgate init --workspace . --write --json", "verify_local": "agents-shipgate verify --workspace . --config shipgate.yaml --ci-mode advisory --json", "verify_pr": "agents-shipgate verify --workspace . --config shipgate.yaml --base origin/main --head HEAD --ci-mode advisory --format json", + "verification_prepare": "agents-shipgate verification prepare --workspace . --config shipgate.yaml --base origin/main --head HEAD --out agents-shipgate-reports/verification-plan.json", + "verification_worker": "agents-shipgate verification worker --plan agents-shipgate-reports/verification-plan.json --workspace . --out agents-shipgate-reports/verification-unit-result.json", + "verification_assemble": "agents-shipgate verification assemble --plan agents-shipgate-reports/verification-plan.json --unit-result agents-shipgate-reports/verification-unit-result.json --verifier agents-shipgate-reports/verifier.json --artifacts-root agents-shipgate-reports --out agents-shipgate-reports/verification-receipt.json", + "verification_reproduce": "agents-shipgate verification reproduce --receipt agents-shipgate-reports/verification-receipt.json --artifacts-root agents-shipgate-reports", "agent_handoff": "agents-shipgate agent handoff --from agents-shipgate-reports/verifier.json --json", "trigger": "agents-shipgate trigger --base origin/main --head HEAD --json", "capability_export": "agents-shipgate capability export -c shipgate.yaml", @@ -172,16 +176,20 @@ "codex_boundary_result_schema_path": "docs/codex-boundary-result-schema.v2.json", "agent_boundary_result_schema_version": "shipgate.agent_boundary_result/v1", "agent_boundary_result_schema_path": "docs/agent-boundary-result-schema.v1.json", - "report_schema_version": "0.33", - "packet_schema_version": "0.11", - "verifier_schema_version": "0.4", - "verify_run_schema_version": "shipgate.verify_run/v2", - "agent_handoff_schema_version": "shipgate.agent_handoff/v4", - "agent_handoff_schema_path": "docs/agent-handoff-schema.v4.json", + "report_schema_version": "0.34", + "packet_schema_version": "0.12", + "verifier_schema_version": "0.5", + "verify_run_schema_version": "shipgate.verify_run/v3", + "verification_plan_schema_version": "shipgate.verification_plan/v1", + "verification_unit_result_schema_version": "shipgate.verification_unit_result/v1", + "verification_artifact_manifest_schema_version": "shipgate.verification_artifact_manifest/v1", + "verification_receipt_schema_version": "shipgate.verification_receipt/v1", + "agent_handoff_schema_version": "shipgate.agent_handoff/v5", + "agent_handoff_schema_path": "docs/agent-handoff-schema.v5.json", "agent_handoff_artifact": "agents-shipgate-reports/agent-handoff.json", - "contract_version": "16", + "contract_version": "17", "minimum_control_contract_version": "14", - "local_agent_contract_schema_version": "5", + "local_agent_contract_schema_version": "6", "inputs": [ "mcp", "openapi", @@ -206,6 +214,10 @@ "verifier_json", "verify_run_json", "agent_handoff_json", + "verification_plan_json", + "verification_unit_result_json", + "verification_artifact_manifest_json", + "verification_receipt_json", "pr_comment_md", "check_annotations_json", "capability_lock_json", @@ -224,6 +236,10 @@ "verifier": "agents-shipgate-reports/verifier.json", "verify_run": "agents-shipgate-reports/verify-run.json", "agent_handoff": "agents-shipgate-reports/agent-handoff.json", + "verification_plan": "agents-shipgate-reports/verification-plan.json", + "verification_unit_result": "agents-shipgate-reports/verification-unit-result.json", + "verification_artifact_manifest": "agents-shipgate-reports/verification-artifacts.json", + "verification_receipt": "agents-shipgate-reports/verification-receipt.json", "report": "agents-shipgate-reports/report.json", "pr_comment": "agents-shipgate-reports/pr-comment.md", "check_annotations": "agents-shipgate-reports/check-annotations.json", @@ -245,9 +261,9 @@ "capability_lock_schema_version": "0.6", "capability_lock_diff_schema_version": "0.7", "preflight_schema_version": "0.3", - "attestation_schema_version": "0.4", - "registry_schema_version": "0.3", - "org_evidence_bundle_schema_version": "shipgate.org_evidence_bundle/v1", + "attestation_schema_version": "0.5", + "registry_schema_version": "0.4", + "org_evidence_bundle_schema_version": "shipgate.org_evidence_bundle/v2", "host_grants_inventory_schema_version": "0.2", "host_grants_baseline_schema_version": "0.2", "host_grants_drift_schema_version": "0.2", @@ -276,6 +292,10 @@ "registry", "org_governance", "org_evidence_bundle", + "verification_plan", + "verification_unit_result", + "verification_artifact_manifest", + "verification_receipt", "host_grants_inventory", "host_grants_baseline", "host_grants_drift", @@ -365,9 +385,14 @@ "control.next_action", "fix_task", "capability_review.top_changes", - "release_decision.decision" + "release_decision.decision", + "request_id", + "decision_id" ], "agent_read_order": [ + "verification-receipt.json", + "verification-receipt.json.request_id", + "verification-receipt.json.receipt_id", "agent-handoff.json", "agent-handoff.json.control.state", "verifier.json.control.state", @@ -396,24 +421,28 @@ "static_verdict_disclaimer": "This verdict covers deterministic static evidence only. Agents Shipgate did not execute the agent or prove runtime behavior, tool routing, credential enforcement, or safety.", "schemas": { "manifest": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/manifest-v0.1.json", - "report": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/report-schema.v0.33.json", + "report": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/report-schema.v0.34.json", "agent_result": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/agent-result-schema.v2.json", "agent_boundary_result": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/agent-boundary-result-schema.v1.json", "codex_boundary_result": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/codex-boundary-result-schema.v2.json", - "verifier": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/verifier-schema.v0.4.json", - "verify_run": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/verify-run-schema.v2.json", - "agent_handoff": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/agent-handoff-schema.v4.json", - "packet": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/packet-schema.v0.11.json", + "verifier": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/verifier-schema.v0.5.json", + "verify_run": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/verify-run-schema.v3.json", + "verification_plan": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/verification-plan-schema.v1.json", + "verification_unit_result": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/verification-unit-result-schema.v1.json", + "verification_artifact_manifest": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/verification-artifact-manifest-schema.v1.json", + "verification_receipt": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/verification-receipt-schema.v1.json", + "agent_handoff": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/agent-handoff-schema.v5.json", + "packet": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/packet-schema.v0.12.json", "preflight": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/preflight-schema.v0.3.json", "capability_lock": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/capability-lock-schema.v0.6.json", "capability_lock_diff": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/capability-lock-diff-schema.v0.7.json", "governance_benchmark_catalog": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/governance-benchmark-catalog-schema.v0.2.json", "governance_benchmark_result": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/governance-benchmark-result-schema.v0.2.json", "feedback": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/feedback-schema.v0.1.json", - "attestation": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/attestation-schema.v0.4.json", + "attestation": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/attestation-schema.v0.5.json", "org_governance": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/org-governance-schema.v0.1.json", - "org_evidence_bundle": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/org-evidence-bundle-schema.v1.json", - "registry": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/registry-schema.v0.3.json", + "org_evidence_bundle": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/org-evidence-bundle-schema.v2.json", + "registry": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/registry-schema.v0.4.json", "host_grants_inventory": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/host-grants-inventory-schema.v0.2.json", "host_grants_baseline": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/host-grants-baseline-schema.v0.2.json", "host_grants_drift": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/host-grants-drift-schema.v0.2.json", diff --git a/AGENTS.md b/AGENTS.md index 099d513e..bd326965 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,7 +130,8 @@ agents-shipgate verify --workspace . --config shipgate.yaml \ ``` For local uncommitted verifier work, omit `--base`/`--head` so the working tree -is scanned. Read `agents-shipgate-reports/agent-handoff.json` first and lead +is scanned. Validate `agents-shipgate-reports/verification-receipt.json` +first, then read `agents-shipgate-reports/agent-handoff.json` and lead with `control.state`, then `gate.merge_verdict` (`mergeable | human_review_required | insufficient_evidence | blocked | unknown`), `gate.can_merge_without_human`, `next_action`, @@ -363,7 +364,7 @@ restate version archaeology here): - Audit envelopes: `release_decision.contribution_rules[]`, `policy_audit`, `privacy_audit`, `heuristics_filter` — explanatory, never a second gate -The current schema is [`docs/report-schema.v0.33.json`](docs/report-schema.v0.33.json). Emitted reports carry `report_schema_version: "0.33"`; typed predicate support prevents heuristic evidence from being upgraded by policy severity or block metadata. A `passed` result requires a complete root-reachable static binding graph plus complete, conflict-free identity, effect, authority, and applicable-policy evidence for every reachable action. Every release decision explicitly carries `static_analysis_only: true`, `runtime_behavior_verified: false`, and `static_verdict_disclaimer`; packet §1 mirrors them. Binding, semantic, and policy-applicability gaps are not Findings and cannot be suppressed or baselined. See [`docs/passed-verdict-contract.md`](docs/passed-verdict-contract.md) for the exact claim and [`docs/agent-contract-current.md`](docs/agent-contract-current.md) for version history. v0.32 remains frozen at [`docs/report-schema.v0.32.json`](docs/report-schema.v0.32.json). +The current schema is [`docs/report-schema.v0.34.json`](docs/report-schema.v0.34.json). Emitted reports carry `report_schema_version: "0.34"`; typed predicate support prevents heuristic evidence from being upgraded by policy severity or block metadata, and verify-native reports bind the content-addressed request and decision. A `passed` result requires a complete root-reachable static binding graph plus complete, conflict-free identity, effect, authority, and applicable-policy evidence for every reachable action. Every release decision explicitly carries `static_analysis_only: true`, `runtime_behavior_verified: false`, and `static_verdict_disclaimer`; packet §1 mirrors them. Binding, semantic, and policy-applicability gaps are not Findings and cannot be suppressed or baselined. See [`docs/passed-verdict-contract.md`](docs/passed-verdict-contract.md), [`docs/verification-reproducibility.md`](docs/verification-reproducibility.md), and [`docs/agent-contract-current.md`](docs/agent-contract-current.md). v0.33 remains frozen at [`docs/report-schema.v0.33.json`](docs/report-schema.v0.33.json). **Release gating signal**: prefer `release_decision.decision` (`"blocked" | "review_required" | "insufficient_evidence" | "passed"`) over `summary.status`. The new field is **baseline-aware** — a baseline-matched critical surfaces in `release_decision.review_items` (accepted debt), not `release_decision.blockers`. `summary.status` stays baseline-blind for v0.7 compatibility, so a baseline-matched-only critical produces both `summary.status = "release_blockers_detected"` AND `release_decision.decision = "review_required"` (intentional divergence — see [STABILITY.md](STABILITY.md#release_decisiondecision-vs-summarystatus)). `insufficient_evidence` (added v0.14) signals that the scan saw too many low-confidence tools or source-loader warnings to be trustworthy; consumers that switch on the enum must fall back to `review_required` for unknown future values. @@ -443,7 +444,7 @@ validation and [`docs/manifest-v0.1.md`](docs/manifest-v0.1.md) for prose. ### Where is the report schema? Parse `agents-shipgate-reports/report.json` and validate against -[`docs/report-schema.v0.33.json`](docs/report-schema.v0.33.json) (current). +[`docs/report-schema.v0.34.json`](docs/report-schema.v0.34.json) (current). Older reports (`report_schema_version: "0.10"`) validate against the frozen [`docs/report-schema.v0.10.json`](docs/report-schema.v0.10.json). Do not scrape Markdown when JSON is available. @@ -481,7 +482,7 @@ For the short, current statement of "which fields to read", see [`docs/agent-con | What | Path | Stable | |---|---|---| | Manifest schema | [`docs/manifest-v0.1.json`](docs/manifest-v0.1.json) | `0.1` | -| Report schema (current) | [`docs/report-schema.v0.33.json`](docs/report-schema.v0.33.json) | `0.33` | +| Report schema (current) | [`docs/report-schema.v0.34.json`](docs/report-schema.v0.34.json) | `0.34` | | Report schema (v0.32 frozen reference) | [`docs/report-schema.v0.32.json`](docs/report-schema.v0.32.json) | `0.32` | | Report schema (v0.31 frozen reference) | [`docs/report-schema.v0.31.json`](docs/report-schema.v0.31.json) | `0.31` | | Report schema (v0.30 frozen reference) | [`docs/report-schema.v0.30.json`](docs/report-schema.v0.30.json) | `0.30` | @@ -490,8 +491,12 @@ For the short, current statement of "which fields to read", see [`docs/agent-con | Report schema (v0.27 frozen reference) | [`docs/report-schema.v0.27.json`](docs/report-schema.v0.27.json) | `0.27` | | Report schema (v0.26 frozen reference) | [`docs/report-schema.v0.26.json`](docs/report-schema.v0.26.json) | `0.26` | | Report schema (v0.25 frozen reference) | [`docs/report-schema.v0.25.json`](docs/report-schema.v0.25.json) | `0.25` | -| Verify-run schema | [`docs/verify-run-schema.v2.json`](docs/verify-run-schema.v2.json) | `shipgate.verify_run/v2` | -| Agent handoff schema | [`docs/agent-handoff-schema.v4.json`](docs/agent-handoff-schema.v4.json) | `shipgate.agent_handoff/v4` | +| Verify-run schema | [`docs/verify-run-schema.v3.json`](docs/verify-run-schema.v3.json) | `shipgate.verify_run/v3` | +| Verification plan schema | [`docs/verification-plan-schema.v1.json`](docs/verification-plan-schema.v1.json) | `shipgate.verification_plan/v1` | +| Verification unit result schema | [`docs/verification-unit-result-schema.v1.json`](docs/verification-unit-result-schema.v1.json) | `shipgate.verification_unit_result/v1` | +| Verification artifact manifest schema | [`docs/verification-artifact-manifest-schema.v1.json`](docs/verification-artifact-manifest-schema.v1.json) | `shipgate.verification_artifact_manifest/v1` | +| Verification receipt schema | [`docs/verification-receipt-schema.v1.json`](docs/verification-receipt-schema.v1.json) | `shipgate.verification_receipt/v1` | +| Agent handoff schema | [`docs/agent-handoff-schema.v5.json`](docs/agent-handoff-schema.v5.json) | `shipgate.agent_handoff/v5` | | Agent boundary result schema | [`docs/agent-boundary-result-schema.v1.json`](docs/agent-boundary-result-schema.v1.json) | `shipgate.agent_boundary_result/v1` | | Codex boundary result schema (deprecated frozen projection) | [`docs/codex-boundary-result-schema.v2.json`](docs/codex-boundary-result-schema.v2.json) | `shipgate.codex_boundary_result/v2` | | Report schema (v0.24 frozen reference) | [`docs/report-schema.v0.24.json`](docs/report-schema.v0.24.json) | `0.24` | @@ -513,10 +518,10 @@ For the short, current statement of "which fields to read", see [`docs/agent-con | Report schema (v0.8 frozen reference) | [`docs/report-schema.v0.8.json`](docs/report-schema.v0.8.json) | `0.8` | | Report schema (v0.7 frozen reference) | [`docs/report-schema.v0.7.json`](docs/report-schema.v0.7.json) | `0.7` | | Report schema (v0.6 frozen reference) | [`docs/report-schema.v0.6.json`](docs/report-schema.v0.6.json) | `0.6` | -| Packet schema (Release Evidence Packet, latest) | [`docs/packet-schema.v0.11.json`](docs/packet-schema.v0.11.json) | `0.11` | +| Packet schema (Release Evidence Packet, latest) | [`docs/packet-schema.v0.12.json`](docs/packet-schema.v0.12.json) | `0.12` | | Agent result schema (current) | [`docs/agent-result-schema.v2.json`](docs/agent-result-schema.v2.json) | `agent_result_v2` | -| Verifier schema (current) | [`docs/verifier-schema.v0.4.json`](docs/verifier-schema.v0.4.json) | `0.4` | -| Agent handoff schema (current) | [`docs/agent-handoff-schema.v4.json`](docs/agent-handoff-schema.v4.json) | `shipgate.agent_handoff/v4` | +| Verifier schema (current) | [`docs/verifier-schema.v0.5.json`](docs/verifier-schema.v0.5.json) | `0.5` | +| Agent handoff schema (current) | [`docs/agent-handoff-schema.v5.json`](docs/agent-handoff-schema.v5.json) | `shipgate.agent_handoff/v5` | | Preflight schema (current) | [`docs/preflight-schema.v0.3.json`](docs/preflight-schema.v0.3.json) | `0.3` | | Host-grants inventory schema | [`docs/host-grants-inventory-schema.v0.2.json`](docs/host-grants-inventory-schema.v0.2.json) | `0.2` | | Host-grants baseline schema | [`docs/host-grants-baseline-schema.v0.2.json`](docs/host-grants-baseline-schema.v0.2.json) | `0.2` | @@ -583,7 +588,7 @@ NOT prove. Use `--no-packet` / `--packet-format` on `scan`, and `agents-shipgate evidence-packet --from ` to re-render. The full packet contract (fixed sections, disclaimers, `evidence_matrix` rules) lives in -[STABILITY.md §Release Evidence Packet](STABILITY.md#release-evidence-packet-v011) +[STABILITY.md §Release Evidence Packet](STABILITY.md#release-evidence-packet-v012) and [`docs/agent-contract-current.md`](docs/agent-contract-current.md#read-these-for-release-review). Exit codes (stable): diff --git a/CHANGELOG.md b/CHANGELOG.md index a3fd32ff..cdad25a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ ## Unreleased +- **Reproducible verification identity (P0, `0.16.0b6`).** Verify now binds + the resolved Git subject, exact input blobs, evaluation date, behavior + options, installed engine-content and dependency/adapter/plugin/policy set, normalized task, + executor, assembled decision, and complete artifact set through + content-addressed IDs. Exact Git objects are materialized with `ls-tree` and + `cat-file`; base-cache reuse is invisible to public artifacts and guarded by + a content hash. +- **Terminal receipts and portable execution boundary.** Successful verify runs emit + `verification-plan.json`, a decision-free `verification-unit-result.json`, + `verification-artifacts.json`, and, last, `verification-receipt.json`. + Workers validate their installed engine and transported inputs but cannot + assert a verdict; the verifier remains the sole policy engine and the + assembler re-closes its decision. `verification + prepare|worker|assemble|reproduce` exposes the portable v1 protocol without + claiming distributed policy evaluation, arbitrary sharding, or parallel + speedup. +- **Identity contract versions.** Runtime contract advances to v17; report to + v0.34; packet to v0.12; verifier to v0.5; verify-run to v3; handoff to v5; + attestation to v0.5; registry to v0.4; organization evidence bundle to v2; + downstream local contract to v6; and safety qualification formats to v4. + Verification plan, unit-result, artifact-manifest, and receipt schemas begin + at v1. Prior schemas remain frozen readers. +- **Immutable CI subject.** The GitHub Action evaluates `github.sha` by default + and records a pull request's source head separately. It exports receipt, + request, decision, and artifact-set identities only after validating every + terminal artifact hash. + - **Evidence-basis policy gate (P0, `0.16.0b5`).** Semantic claims and risk hints now carry a typed evidence basis, stable claim IDs, and derived policy eligibility. Policy-pack and action-policy predicates evaluate to diff --git a/README.md b/README.md index e5fd5df8..60249be0 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Local-first and static by default — no agent execution, tool calls, LLM calls, > [!IMPORTANT] > **Status: pre-1.0 (beta).** The decision engine is deterministic and stable. -> This source tree is `0.16.0b5`; install and GitHub Action examples remain +> This source tree is `0.16.0b6`; install and GitHub Action examples remain > pinned to the latest published tag, `v0.15.0`, until the beta is released. > Real-history accuracy numbers (small n, published in full in > [`benchmark/miner/README.md`](benchmark/miner/README.md)): across 361 mined @@ -210,8 +210,9 @@ The release gate is `agents-shipgate-reports/report.json` → `release_decision.decision` (`blocked | review_required | insufficient_evidence | passed`). The PR/control surface is `agents-shipgate-reports/verifier.json` → `merge_verdict` (`mergeable | human_review_required | insufficient_evidence | -blocked | unknown`), a deterministic projection of the release decision. Read -`agent-handoff.json` first (`control.state`, then `gate.merge_verdict`), then the +blocked | unknown`), a deterministic projection of the release decision. +Validate `verification-receipt.json` first; then read `agent-handoff.json` +(`control.state`, then `gate.merge_verdict`), followed by the authoritative control substrate `verifier.json` for `control`, `merge_verdict`, `applicability`, `can_merge_without_human`, `control.next_action`, and `fix_task`. `capability_review.top_changes` is @@ -230,7 +231,8 @@ below. ## How to read your first result -For PR verification, read `agent-handoff.json.gate.merge_verdict` first: +For PR verification, validate `verification-receipt.json`, then read +`agent-handoff.json.gate.merge_verdict`: | Merge verdict | Meaning | Next step | |---|---|---| @@ -300,8 +302,9 @@ For local control, parse the `shipgate check` stdout JSON `control.next_action`, `control.allowed_next_commands`, and `control.human_review`; `decision` is diagnostic context only. For local uncommitted verify work, omit `--base`/`--head`. For committed PR/CI refs, -make the base ref available first because `verify` never fetches. Read -`agents-shipgate-reports/agent-handoff.json` first and lead with +make the base ref available first because `verify` never fetches. Validate +`agents-shipgate-reports/verification-receipt.json`, then read +`agents-shipgate-reports/agent-handoff.json` and lead with `control.state` and `gate.merge_verdict`, then read the authoritative substrate `agents-shipgate-reports/verifier.json` (`merge_verdict`, `applicability`, `control`, `can_merge_without_human`, `control.next_action`, @@ -395,7 +398,9 @@ agents-shipgate verify --workspace . --config shipgate.yaml \ For local uncommitted work, omit `--base`/`--head`. For committed PR/CI refs, make the base ref available first because `verify` never fetches. Verify writes -`agents-shipgate-reports/agent-handoff.json`, `verifier.json`, +`agents-shipgate-reports/verification-plan.json`, +`verification-unit-result.json`, `verification-artifacts.json`, +`verification-receipt.json`, `agent-handoff.json`, `verifier.json`, `verify-run.json`, `pr-comment.md`, the head capability lock, and the normal `report.{md,json,sarif}` / packet artifacts when a scan is required. If the base scan can be materialized, verify also writes @@ -528,16 +533,17 @@ and pre-commit equivalents. When a PR changes what your agent can do, the verify loop writes these artifacts — in read order: -- **`agents-shipgate-reports/agent-handoff.json`** — the **first artifact a coding agent reads**: the compact `shipgate.agent_handoff/v4` object. Lead with `control.state`, then `gate.merge_verdict`; it also projects `blocked_by[]`, `remediation_plan[]`, and verify-run reproducibility from existing artifacts, and it does not introduce a second verdict. -- **`agents-shipgate-reports/verifier.json`** — the **authoritative PR/control evidence substrate** (`verifier_schema_version: "0.4"`). A coding agent switches on `control.state`, then reads `merge_verdict` (`mergeable | human_review_required | insufficient_evidence | blocked | unknown`), `can_merge_without_human`, `control.next_action`, and `fix_task` when producing reviewer evidence for an agent-capability PR. Local control comes from `shipgate check --format agent-boundary-json` and `shipgate.agent_boundary_result/v1`. See [`docs/agent-contract-current.md`](docs/agent-contract-current.md) for the field contract. -- **`agents-shipgate-reports/verify-run.json`** — the deterministic verify-run reproducibility artifact. It records stable subject/input hashes, policy-pack hashes, outcome, artifact paths, and `run_id` without wall-clock timestamps. +- **`agents-shipgate-reports/verification-receipt.json`** — the **first artifact a coding agent validates**: a terminal content-addressed closure over the exact request (including `verification-input.diff`), worker result, decision, and artifact set. It is written last; use `agents-shipgate verification reproduce` to validate every referenced hash. +- **`agents-shipgate-reports/agent-handoff.json`** — the compact `shipgate.agent_handoff/v5` object. Lead with `control.state`, then `gate.merge_verdict`; it projects the same request and decision IDs and does not introduce a second verdict. +- **`agents-shipgate-reports/verifier.json`** — the **authoritative PR/control evidence substrate** (`verifier_schema_version: "0.5"`). A coding agent switches on `control.state`, then reads `merge_verdict` (`mergeable | human_review_required | insufficient_evidence | blocked | unknown`), `can_merge_without_human`, `control.next_action`, and `fix_task` when producing reviewer evidence for an agent-capability PR. Local control comes from `shipgate check --format agent-boundary-json` and `shipgate.agent_boundary_result/v1`. See [`docs/agent-contract-current.md`](docs/agent-contract-current.md) for the field contract. +- **`agents-shipgate-reports/verify-run.json`** — the `shipgate.verify_run/v3` projection embedding the exact verification plan, executor, unit-result IDs, decision ID, outcome, and artifact paths. Its deprecated `run_id` is an exact alias of `request_id`. - **`agents-shipgate-reports/attestation.json`** + **`agents-shipgate-reports/org-evidence-bundle.json`** — optional organization-governance projections over the same verifier/report artifacts. They are ledger inputs for platform teams, not release gates; `report.json.release_decision.decision` remains the decision engine. - **`agents-shipgate-reports/host-grants.json`** + **`agents-shipgate-reports/org-status.json`** — optional fleet-governance artifacts from `audit --host --out` and `org status --json`, useful for host-grant drift, policy-pack pin state, and exception hygiene. - **`agents-shipgate-reports/pr-comment.md`** — the **human PR surface**: the same verdict and semantic capability diff when available, shaped for a reviewer. - **`agents-shipgate-reports/capabilities.lock.json`** + **`agents-shipgate-reports/base.capabilities.lock.json`** + **`agents-shipgate-reports/capability-lock-diff.{json,md}`** — the **capability review primitive**. Verify always emits the head lock after a successful scan; it emits the base lock and diff when the base scan can be materialized, falling back to the reviewed committed lock at `.agents-shipgate/capabilities.lock.json` if needed. - **Gate source of truth** — `report.json.release_decision.decision` (`passed | review_required | insufficient_evidence | blocked`). `merge_verdict` is a deterministic projection of it; the report stays the one decision engine. - **Tool-Use Readiness Report** (supporting) — `agents-shipgate-reports/report.{md,json,sarif}`. Markdown for human release review, JSON for tools and coding agents, SARIF for GitHub code-scanning workflows. This is the underlying check domain the verdict summarizes. -- **Release Evidence Packet** (supporting) — `agents-shipgate-reports/packet.{md,json,html}` (and `packet.pdf` with the `[pdf]` extras). Reviewer-shaped synthesis with fixed sections, including binding and semantic evidence coverage, the compact evidence matrix, and tool/action diffs when available. Packet outputs are locally redacted by default; see [STABILITY.md §Release Evidence Packet](STABILITY.md#release-evidence-packet-v011). +- **Release Evidence Packet** (supporting) — `agents-shipgate-reports/packet.{md,json,html}` (and `packet.pdf` with the `[pdf]` extras). Reviewer-shaped synthesis with fixed sections, including binding and semantic evidence coverage, the compact evidence matrix, and tool/action diffs when available. Packet outputs are locally redacted by default; see [STABILITY.md §Release Evidence Packet](STABILITY.md#release-evidence-packet-v012). ## Exit codes @@ -578,9 +584,9 @@ Agents Shipgate is designed to be agent-friendly. If you're a coding agent (Clau - **`agents-shipgate install-hooks --target claude-code --write`** — deterministic Claude Code hooks: a PreToolUse trust-root guard, a cheap trigger check after `Edit|Write|MultiEdit`, and a full `verify` at `Stop`, so the gate runs even when instruction files lose attention on long sessions. See [`docs/agents/use-with-claude-code.md`](docs/agents/use-with-claude-code.md#hooks-the-deterministic-path-recommended). - **`agents-shipgate mcp-serve`** (`[mcp]` extra) — read-only stdio MCP server exposing `shipgate.check`, `shipgate.preflight`, `shipgate.explain`, `shipgate.capabilities`, and `shipgate.handoff` for agents without comfortable shell access. It is static-only and not a general MCP permission broker. See [`docs/mcp-server.md`](docs/mcp-server.md). - **[`docs/ai-search-summary.md`](docs/ai-search-summary.md)** — human-readable summary for AI search, answer engines, and coding agents -- **[`docs/manifest-v0.1.json`](docs/manifest-v0.1.json)** + **[`docs/report-schema.v0.33.json`](docs/report-schema.v0.33.json)** + **[`docs/agent-handoff-schema.v4.json`](docs/agent-handoff-schema.v4.json)** + **[`docs/preflight-schema.v0.3.json`](docs/preflight-schema.v0.3.json)** — JSON Schemas for live editor validation and agent routing. Reports carry `report_schema_version: "0.33"`; every policy finding now records typed predicate support, while heuristic-only, mixed, unknown, or conflicting applicability becomes a non-waivable evidence gap instead of a high-confidence finding. `passed` still requires a complete root-reachable binding graph and complete identity, effect, authority, and policy evidence for each reachable action. `tool_catalog[]` remains visible while `tool_inventory[]` contains only proven reachable tools. Every release decision carries `static_analysis_only: true`, `runtime_behavior_verified: false`, and a canonical static-verdict disclaimer; packet §1 mirrors them. Read `release_decision.decision` for gating and [`docs/passed-verdict-contract.md`](docs/passed-verdict-contract.md) for the exact non-runtime claim. v0.32 is frozen. +- **[`docs/manifest-v0.1.json`](docs/manifest-v0.1.json)** + **[`docs/report-schema.v0.34.json`](docs/report-schema.v0.34.json)** + **[`docs/agent-handoff-schema.v5.json`](docs/agent-handoff-schema.v5.json)** + **[`docs/verification-receipt-schema.v1.json`](docs/verification-receipt-schema.v1.json)** — JSON Schemas for live editor validation, agent routing, and reproducible verification identity. Reports carry `report_schema_version: "0.34"`; every policy finding records typed predicate support, while heuristic-only, mixed, unknown, or conflicting applicability becomes a non-waivable evidence gap. `passed` still requires a complete root-reachable binding graph and complete identity, effect, authority, and policy evidence for each reachable action. A successful verifier run writes a terminal content-addressed receipt last; validate it before trusting the handoff or report. Every release decision remains static-only. See [`docs/verification-reproducibility.md`](docs/verification-reproducibility.md) and [`docs/passed-verdict-contract.md`](docs/passed-verdict-contract.md). v0.33 is frozen. - **[`docs/capability-lock-schema.v0.5.json`](docs/capability-lock-schema.v0.5.json)** + **[`docs/capability-lock-diff-schema.v0.6.json`](docs/capability-lock-diff-schema.v0.6.json)** — current capability standard v0.4 schemas, including binding hashes, for the static capability envelope and semantic diff; non-gating and separate from `report.json`. -- **[`docs/attestation-schema.v0.4.json`](docs/attestation-schema.v0.4.json)** + **[`docs/org-governance-schema.v0.1.json`](docs/org-governance-schema.v0.1.json)** + **[`docs/org-evidence-bundle-schema.v1.json`](docs/org-evidence-bundle-schema.v1.json)** + **[`docs/registry-schema.v0.3.json`](docs/registry-schema.v0.3.json)** + **[`docs/host-grants-inventory-schema.v0.2.json`](docs/host-grants-inventory-schema.v0.2.json)** — deterministic local attestation, organization governance, org evidence bundle, append-only registry, and scope-aware host-grant inventory schemas for multi-repo governance. +- **[`docs/attestation-schema.v0.5.json`](docs/attestation-schema.v0.5.json)** + **[`docs/org-governance-schema.v0.1.json`](docs/org-governance-schema.v0.1.json)** + **[`docs/org-evidence-bundle-schema.v2.json`](docs/org-evidence-bundle-schema.v2.json)** + **[`docs/registry-schema.v0.4.json`](docs/registry-schema.v0.4.json)** + **[`docs/host-grants-inventory-schema.v0.2.json`](docs/host-grants-inventory-schema.v0.2.json)** — deterministic local attestation, organization governance, org evidence bundle, append-only registry, and scope-aware host-grant inventory schemas for multi-repo governance. - **[`docs/governance-benchmark-catalog-schema.v0.2.json`](docs/governance-benchmark-catalog-schema.v0.2.json)** + **[`docs/governance-benchmark-result-schema.v0.2.json`](docs/governance-benchmark-result-schema.v0.2.json)** — stable schemas for the research benchmark catalog and deterministic result artifact. - **[`docs/checks.json`](docs/checks.json)** — machine-readable check catalog @@ -670,7 +676,7 @@ Agents Shipgate is a static, manifest-first scanner. It is intentionally narrow: - It does not verify runtime behavior, latency, prompt quality, or routing decisions. - It does not replace dynamic security testing or human security review of the underlying systems. - It only inspects what is declared in `shipgate.yaml`, local OpenAPI specs, MCP exports, Anthropic/OpenAI API artifacts, optional SDK AST metadata, static Google ADK/LangChain/CrewAI/n8n/Conductor OSS inputs, Codex repo config, and static Codex plugin package metadata; tools that are not declared or statically discoverable are not scanned. -- The manifest remains `version: "0.1"` so existing configs keep working. Current reports carry `report_schema_version: "0.33"`; binding, semantic, and policy-applicability gaps are non-waivable release evidence rather than Findings, while v0.32 remains frozen for archived reports. +- The manifest remains `version: "0.1"` so existing configs keep working. Current reports carry `report_schema_version: "0.34"`; binding, semantic, and policy-applicability gaps are non-waivable release evidence rather than Findings, while v0.33 remains frozen for archived reports. See [ROADMAP.md](ROADMAP.md) for what is planned next. @@ -720,7 +726,7 @@ readers and AI search ingest. - [Check catalog](docs/checks.md) - [Policy packs](docs/policy-packs.md) - [Baseline workflow](docs/baseline.md) -- [JSON report schema v0.33](docs/report-schema.v0.33.json) +- [JSON report schema v0.34](docs/report-schema.v0.34.json) - [JSON report schema v0.32 (frozen)](docs/report-schema.v0.32.json) - [JSON report schema v0.31 (frozen)](docs/report-schema.v0.31.json) - [Capability standard](docs/capability-standard.md) diff --git a/STABILITY.md b/STABILITY.md index 29d1f3f0..27536bb7 100644 --- a/STABILITY.md +++ b/STABILITY.md @@ -1,4 +1,4 @@ -# Stability Contract · 0.16.0b5 +# Stability Contract · 0.16.0b6 What agents and CI integrations can rely on across versions of Agents Shipgate. @@ -6,13 +6,50 @@ This document is the contract. If the runtime ever diverges from what's document Shipgate is pre-1.0. The CLI surface, exit codes, and `contract_version` described here are stable within the `0.x` line, but the `report.json` schema -(`report_schema_version`, currently `0.33`) is still additive-versioned and +(`report_schema_version`, currently `0.34`) is still additive-versioned and not yet frozen. A `1.0` line will not begin until the report schema reaches `1.0` and holds without a breaking change. Pin a version (or the Action tag) for reproducible CI. --- + + +## Migration Note: 0.16.0b6 + +Runtime contract `16 → 17` makes verification identity and artifact closure +content-addressed. A successful `verify` now emits a verification plan, +decision-free unit result, artifact manifest, and terminal receipt. The +receipt is written last and binds the resolved Git subject, hashed inputs, +engine requirement, executor, assembled decision, and every referenced +artifact. Git snapshots use exact blobs rather than `git archive`; cache reuse +cannot change public artifacts; and expiry-sensitive policy evaluation uses a +declared evaluation date rather than a worker wall clock. + +Report advances `0.33 → 0.34`, packet `0.11 → 0.12`, verifier `0.4 → 0.5`, +verify-run v2 → v3, handoff v4 → v5, attestation `0.4 → 0.5`, registry +`0.3 → 0.4`, organization evidence bundle v1 → v2, generated downstream +contract `5 → 6`, and safety qualification envelopes v3 → v4. New +verification-plan, unit-result, artifact-manifest, and receipt schemas begin +at v1. Previous schemas remain frozen readers; they are not emitted by +default. + +`verify-run.run_id` remains for one compatibility cycle as an exact alias of +`request_id`; it is no longer independently derived. GitHub Actions evaluate +the immutable `${{ github.sha }}` by default and separately record a PR's +source head. Current Action outputs include the validated receipt path and +request, receipt, decision, and artifact-set IDs. + +The v1 portable protocol has one deterministic evaluate task. Workers validate +their installed engine and immutable transported inputs and emit normalized IR, +but cannot emit a release decision. The verifier remains the sole policy +engine; the assembler re-closes that decision over the supplied unit. This is +execution validation, not distributed scan/policy evaluation, arbitrary +sharding, or parallel speedup. See +[`docs/verification-reproducibility.md`](docs/verification-reproducibility.md). + +--- + ## Migration Note: 0.16.0b5 @@ -253,7 +290,8 @@ Breaking changes from the `0.13.0` line: - `agents-shipgate verify` no longer writes `agents-shipgate-reports/agent-result.json`. Agents should read - `agent-handoff.json` first, then `verifier.json`, then `verify-run.json`, then + `verification-receipt.json` first, then `agent-handoff.json`, + `verifier.json`, `verify-run.json`, and finally `report.json.release_decision.decision`. - `agents-shipgate verify --format agent` was removed. Use `--format json` to print the full `VerifierArtifact`. @@ -876,8 +914,8 @@ tests on every CI run, not by convention: only. **Plus** one `importlib.resources.files('agents_shipgate')` call to resolve the bundled trigger catalog. - **`cli/verify/git.py`** — one shared `subprocess.run` helper invokes - local `git rev-parse`, `git diff`, `git ls-files`, `git show`, and - `git archive` for verify base/head and working-tree orchestration. It + local `git rev-parse`, `git diff`, `git ls-files`, `git ls-tree`, and + `git cat-file` for exact base/head and working-tree orchestration. It never fetches, uses fixed argv, captures output, and never executes user code. - **`cli/fixture.py`** — one `subprocess.run` helper invokes local @@ -1090,7 +1128,9 @@ fix the config path or run `agents-shipgate verify --preview --json` before initializing. The head scan writes `report.md`, `report.json`, `report.sarif`, `packet.json`, -`verifier.json`, `verify-run.json`, `agent-handoff.json`, and `pr-comment.md`. +`verifier.json`, `verify-run.json`, `agent-handoff.json`, `pr-comment.md`, +`verification-plan.json`, `verification-unit-result.json`, +`verification-artifacts.json`, and, last, `verification-receipt.json`. `verify` intentionally requests packet JSON only, regardless of manifest `output.packet.formats`; `pr-comment.md` is the human PR surface. Use `agents-shipgate scan` when you want the manifest's @@ -1106,8 +1146,8 @@ release decision. That action may be `detect`/`initialize` for relevant unconfigured repos, or `verify` for configured repos. Use it as the first touch on a repo or PR before committing to a full scan. -`verifier.json` is governed by [`docs/verifier-schema.v0.3.json`](docs/verifier-schema.v0.3.json). -Verifier v0.1 and v0.2 remain frozen references. It remains an orchestration artifact: `release_decision.decision` in +`verifier.json` is governed by [`docs/verifier-schema.v0.5.json`](docs/verifier-schema.v0.5.json). +Verifier v0.1 through v0.4 remain frozen references. It remains an orchestration artifact: `release_decision.decision` in `report.json` is still the only release gate, and every verifier field is either a mirror or a deterministic projection of report data. Stable additive fields a consumer may read: @@ -1221,15 +1261,15 @@ infer runtime routing, or execute tools. Action Surface Diff policy findings can affect release gating through `findings[].blocks_release`; Tool Surface Diff remains explanatory only. -### Release Evidence Packet (v0.11) +### Release Evidence Packet (v0.12) `agents-shipgate-reports/packet.json` is a supporting/provisional reviewer -artifact governed by [`docs/packet-schema.v0.9.json`](docs/packet-schema.v0.9.json). -v0.9 adds provider-scoped tool identities to capability, approval, idempotency, -and scope evidence while preserving the report release decision as the only -gate. v0.8 stays as the frozen reference at -[`docs/packet-schema.v0.8.json`](docs/packet-schema.v0.8.json); pre-v0.9 packets -validate against the matching frozen schema. v0.8 added report v0.29 semantic +artifact governed by [`docs/packet-schema.v0.12.json`](docs/packet-schema.v0.12.json). +v0.12 adds request, subject, input-set, engine-requirement, and decision IDs +while preserving the report release decision as the only gate. v0.11 and +earlier packets validate against their matching frozen schemas. v0.11 added +typed policy support; v0.9 added provider-scoped tool identities; v0.8 added +report v0.29 semantic coverage and evidence-gap remediation; v0.7 added capability-linked local trace evidence under `human_in_the_loop`; v0.6 added the top-level `evidence_matrix` section and the @@ -1254,7 +1294,8 @@ Fixture names listed by `agents-shipgate fixture list` are stable. Names will no `ai_generated_refund_pr` is the verify-native demo fixture. It creates a temporary base/head git history and writes `verifier.json`, `verify-run.json`, -`agent-handoff.json`, `report.json`, and `pr-comment.md` for a blocked +`agent-handoff.json`, `verification-receipt.json`, `report.json`, and +`pr-comment.md` for a blocked refund-capability PR. ### Agent handoff artifact @@ -1262,8 +1303,8 @@ refund-capability PR. `agents-shipgate-reports/agent-handoff.json` is the preferred compact machine-readable handoff object for coding agents and CI agents. The current schema is -[`docs/agent-handoff-schema.v3.json`](docs/agent-handoff-schema.v3.json) with -`schema_version: "shipgate.agent_handoff/v3"`. v1 and v2 remain frozen +[`docs/agent-handoff-schema.v5.json`](docs/agent-handoff-schema.v5.json) with +`schema_version: "shipgate.agent_handoff/v5"`. v1 through v4 remain frozen references. The handoff artifact is derived only from `verifier.json`, `verify-run.json`, @@ -1315,18 +1356,19 @@ artifact does not leak usernames or confidential workspace directory names. `agents-shipgate attest` derives a deterministic, local attestation from `agents-shipgate-reports/verifier.json` (enriched from the sibling `report.json` when present). The current schema is -[`docs/attestation-schema.v0.4.json`](docs/attestation-schema.v0.4.json). It +[`docs/attestation-schema.v0.5.json`](docs/attestation-schema.v0.5.json). It records the verdict, the report-derived capability delta, optional local organization/CI context, detailed declared `human_ack` entries, a policy-snapshot hash, content hashes of the verify artifacts, and capability -lock/diff hash bindings when verify emitted those artifacts. It carries no +lock/diff hash bindings and the terminal receipt graph when verify emitted +those artifacts. It carries no wall-clock timestamp — it is content-addressed by git SHAs and artifact hashes, so re-deriving from the same inputs is byte-identical. It does not gate; -`release_decision.decision` remains the only gate. Current v0.4 fields: +`release_decision.decision` remains the only gate. Current v0.5 fields: -`org bundle` accepts a previously generated v0.3 attestation file by -normalizing the new optional v0.4 fields to `null` / `[]` before validation. -The emitted bundle still projects the normalized attestation as v0.4. +`org bundle` accepts previously generated attestation files through the frozen +reader path. The emitted bundle projects a current v0.5 attestation and binds +the same request, receipt, decision, and artifact-set IDs. With `--redact` (the default), `source_verifier`, capability lock/diff paths, and artifact paths are reduced to filenames. Redaction does not remove explicit diff --git a/action.yml b/action.yml index 6aa4c0d9..bc518343 100644 --- a/action.yml +++ b/action.yml @@ -190,6 +190,21 @@ outputs: agent_handoff_json: description: Path to agent-handoff.json. value: ${{ steps.report_outputs.outputs.agent_handoff_json }} + verification_receipt_json: + description: Path to the validated terminal verification receipt. + value: ${{ steps.report_outputs.outputs.verification_receipt_json }} + request_id: + description: Content-addressed verification request identity. + value: ${{ steps.report_outputs.outputs.request_id }} + receipt_id: + description: Content-addressed terminal receipt identity. + value: ${{ steps.report_outputs.outputs.receipt_id }} + decision_id: + description: Content-addressed assembled decision identity. + value: ${{ steps.report_outputs.outputs.decision_id }} + artifact_set_id: + description: Content-addressed artifact-set identity. + value: ${{ steps.report_outputs.outputs.artifact_set_id }} run_id: description: Deterministic verify-run input identity from verify-run.json. value: ${{ steps.report_outputs.outputs.run_id }} @@ -334,6 +349,8 @@ runs: INPUT_HEAD_REF: ${{ inputs.head_ref }} BASE_REF: ${{ github.base_ref }} EVENT_NAME: ${{ github.event_name }} + EVALUATED_HEAD_SHA: ${{ github.sha }} + SOURCE_HEAD_SHA: ${{ github.event.pull_request.head.sha }} POLICY_PACKS: ${{ inputs.policy_packs }} NO_PLUGINS: ${{ inputs.no_plugins }} OUTPUT_DIR: ${{ inputs.output_dir }} @@ -382,6 +399,8 @@ runs: fi if [ -n "${INPUT_HEAD_REF}" ]; then args+=(--head "${INPUT_HEAD_REF}") + elif [ -n "${EVALUATED_HEAD_SHA}" ]; then + args+=(--head "${EVALUATED_HEAD_SHA}") fi fi if [ -n "${POLICY_PACKS}" ]; then diff --git a/adoption-kits/claude-code-skill/.agents-shipgate-kit-metadata.json b/adoption-kits/claude-code-skill/.agents-shipgate-kit-metadata.json index b4d732b3..74ca59bb 100644 --- a/adoption-kits/claude-code-skill/.agents-shipgate-kit-metadata.json +++ b/adoption-kits/claude-code-skill/.agents-shipgate-kit-metadata.json @@ -34,7 +34,8 @@ "370e7f8890aef88540c847719f62d2e72529f172c8c4a213d7e8b20c6fde1346", "6f7c06b290bd2ae7b0a9ab6e9fd6abe567a9e1051e8ca4a1d5c098a1359825c7", "e45f9d385f0e7744a5731694f337952682e1849e97be2d0a488ca3cff9db5792", - "98ba22d7518ae4635ed109fd187323da0541281061dd4f259ac7fdb950c7b185" + "98ba22d7518ae4635ed109fd187323da0541281061dd4f259ac7fdb950c7b185", + "02e780f5a1506d948e4c1d77f6ee4c6b4193227a4fd2ced081847d1fb2e5fbd0" ], "prompts/add-shipgate-to-repo.md": [ "ea3c37cfbbd42c40d164abfe21d468a3a5550d5384125f94a53c947dea6b4b2a", @@ -69,7 +70,8 @@ "b6f87f58f70b5920442f342b5118419ef685ad9f4ff8b0ff87c2729a92929786", "b2e42bce1eb6892c5207890bd53ab987d0beaac7c9fa2d9de8ba07033a3f2f60", "2b1d989ee32b8cfee59fb1cc0f51d70277cc16c9f2ad72932a348ecd285b4c38", - "e35be58338a6343c712a6a63aff826f5785040f201aa0d7f5f6f257c27d12532" + "e35be58338a6343c712a6a63aff826f5785040f201aa0d7f5f6f257c27d12532", + "44648891d77a12f562913e4402c08bca4be6cff4aa8c69b7950ea97e7d5cc17b" ], "prompts/decide-shipgate-relevance.md": [ "1bf8b9d91f081a246dcff14a84810ca5384f8e0987e4e7a8c0c5df56b151564c", diff --git a/adoption-kits/claude-code-skill/SKILL.md b/adoption-kits/claude-code-skill/SKILL.md index 12caf111..07b593f5 100644 --- a/adoption-kits/claude-code-skill/SKILL.md +++ b/adoption-kits/claude-code-skill/SKILL.md @@ -48,7 +48,7 @@ Always: 1. Set `AGENTS_SHIPGATE_AGENT_MODE=1` so errors emit a `next_action` JSON line on stderr (auto-enabled inside Claude Code via the harness's `CLAUDECODE=1` env var, and Cursor via `CURSOR_TRACE_ID`). 2. For local agent control, run `shipgate check --agent claude-code --workspace . --format agent-boundary-json` and read the stdout `shipgate.agent_boundary_result/v1` object. Switch on `control.state`; follow only `control.next_action`, `control.allowed_next_commands`, and `control.human_review`. Treat `decision` as diagnostic context only. -3. For verifier runs, parse `agents-shipgate-reports/agent-handoff.json` first, then `verifier.json` and `verify-run.json`: +3. For verifier runs, validate `agents-shipgate-reports/verification-receipt.json` first, then parse `agent-handoff.json`, `verifier.json`, and `verify-run.json`: `control.state`, `merge_verdict`, `can_merge_without_human`, `control.next_action`, `fix_task`, and `capability_review.top_changes`. Then parse `agents-shipgate-reports/report.json.release_decision.decision`; it is the @@ -74,12 +74,13 @@ For non-GitHub CI (GitLab, CircleCI, Jenkins, Azure Pipelines, Buildkite, Bitbuc - **CLI surface** follows the current 0.x contract line — see https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/STABILITY.md. - **Installed CLI contract**: when available, run `agents-shipgate contract --json` to verify local schema versions, capability/research surfaces, `release_decision.decision`, and manual-review signal fields. Older installs should use [`docs/agent-contract-current.md`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/agent-contract-current.md) or upgrade before automating against the local contract command. -- **Verifier JSON**: `verifier_schema_version: "0.3"`. Switch on `control.state`, then read `merge_verdict`, `can_merge_without_human`, `control.next_action`, `fix_task`, `capability_review.top_changes`, `trust_root_touched`, and `policy_weakened` before summarizing an AI-generated PR. `merge_verdict` is a deterministic projection; the gate remains `report.json.release_decision.decision`. -- **Verify run JSON**: `verify-run.json` uses `schema_version: "shipgate.verify_run/v2"` and records stable run identity, subject refs, input hashes, execution, applicability, release outcome, and artifact hashes. It is the reproducibility artifact for `verify`; do not treat it as a second gate. -- **Report JSON**: `report_schema_version: "0.33"`. Read `release_decision.decision` first. A `passed` decision requires a complete root-reachable static binding graph plus complete, conflict-free identity, effect, and authority evidence for every reachable action; it does not prove runtime behavior. Preserve `release_decision.static_analysis_only=true`, `runtime_behavior_verified=false`, and `static_verdict_disclaimer` in summaries. Read `release_decision.evidence_coverage.binding_coverage`, `semantic_coverage`, `identity_coverage`, and `policy_gap_count`, then work every `evidence_gaps[].next_action` in order. Binding, semantic, and policy-applicability gaps are not Findings and cannot be suppressed, baselined, severity-overridden, cleared by `--no-heuristics`, or satisfied by `human_ack`; binding, effect, and authority declarations are human assertions and must never be auto-written. Use `tool_catalog[]` for diagnostics and `tool_inventory[]` for the proven reachable surface. The current schema is [`docs/report-schema.v0.33.json`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.33.json); v0.32 is a frozen compatibility reference. See the [current agent contract](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/agent-contract-current.md#read-these-first-for-release-gating) and [evidence-backed passed contract](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/passed-verdict-contract.md). +- **Verifier JSON**: `verifier_schema_version: "0.5"`. Switch on `control.state`, then read `merge_verdict`, `can_merge_without_human`, `control.next_action`, `fix_task`, `capability_review.top_changes`, `trust_root_touched`, and `policy_weakened` before summarizing an AI-generated PR. `merge_verdict` is a deterministic projection; the gate remains `report.json.release_decision.decision`. +- **Verification receipt**: `verification-receipt.json` uses `schema_version: "shipgate.verification_receipt/v1"` and is written last. Validate it before trusting any projected verdict; it content-addresses the request, executor, unit result, decision, and complete artifact set. +- **Verify run JSON**: `verify-run.json` uses `schema_version: "shipgate.verify_run/v3"`, embeds the content-addressed plan and executor, and binds unit-result and decision IDs. `run_id` is an exact compatibility alias of `request_id`; do not treat the run projection as a second gate. +- **Report JSON**: `report_schema_version: "0.34"`. Read `release_decision.decision` first. A `passed` decision requires a complete root-reachable static binding graph plus complete, conflict-free identity, effect, and authority evidence for every reachable action; it does not prove runtime behavior. Preserve `release_decision.static_analysis_only=true`, `runtime_behavior_verified=false`, and `static_verdict_disclaimer` in summaries. Read `release_decision.evidence_coverage.binding_coverage`, `semantic_coverage`, `identity_coverage`, and `policy_gap_count`, then work every `evidence_gaps[].next_action` in order. Binding, semantic, and policy-applicability gaps are not Findings and cannot be suppressed, baselined, severity-overridden, cleared by `--no-heuristics`, or satisfied by `human_ack`; binding, effect, and authority declarations are human assertions and must never be auto-written. Use `tool_catalog[]` for diagnostics and `tool_inventory[]` for the proven reachable surface. The current schema is [`docs/report-schema.v0.34.json`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.34.json); v0.33 is a frozen compatibility reference. See the [current agent contract](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/agent-contract-current.md), [verification identity contract](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/verification-reproducibility.md), and [evidence-backed passed contract](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/passed-verdict-contract.md). These binding-backed fields require runtime contract v13; the unambiguous `AgentControl` projection requires runtime contract v14. A contract-v12 CLI emits the frozen v0.30 report and must not be described using the v0.31 binding claim. -- **Release Evidence Packet**: `agents-shipgate-reports/packet.{md,json,html}` (and `packet.pdf` with the `[pdf]` extras) is a supporting/provisional reviewer artifact. Packet v0.11 projects binding and semantic coverage under the release decision; it never creates a second gate. See [`docs/packet-schema.v0.11.json`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/packet-schema.v0.11.json) and [STABILITY.md §Release Evidence Packet](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/STABILITY.md#release-evidence-packet-v011). -- **Capability standard**: `agents-shipgate capability export` emits a stable static capability lock (`capability_lock_schema_version: "0.5"`) and `agents-shipgate capability diff` emits a stable semantic diff (`capability_lock_diff_schema_version: "0.6"`). These artifacts implement capability standard v0.4, are supporting/provisional and non-gating, exclude runtime trace evidence, and are documented in [`docs/capability-standard.md`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/capability-standard.md). +- **Release Evidence Packet**: `agents-shipgate-reports/packet.{md,json,html}` (and `packet.pdf` with the `[pdf]` extras) is a supporting/provisional reviewer artifact. Packet v0.12 projects the verification request and decision IDs plus binding and semantic coverage; it never creates a second gate. See [`docs/packet-schema.v0.12.json`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/packet-schema.v0.12.json) and [STABILITY.md §Release Evidence Packet](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/STABILITY.md#release-evidence-packet-v012). +- **Capability standard**: `agents-shipgate capability export` emits a stable static capability lock (`capability_lock_schema_version: "0.6"`) and `agents-shipgate capability diff` emits a stable semantic diff (`capability_lock_diff_schema_version: "0.7"`). These artifacts implement capability standard v0.5, are supporting/provisional and non-gating, exclude runtime trace evidence, and are documented in [`docs/capability-standard.md`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/capability-standard.md). - **Governance benchmark**: `benchmark/agent-pr-governance/cases.yaml` and `scripts/run_governance_benchmark.py` are the stable research benchmark substrate (`governance_benchmark_result_schema_version: "0.2"`), not a release gate. See [`docs/governance-benchmark.md`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/governance-benchmark.md). - **Single source of truth for the contract**: [`docs/agent-contract-current.md`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/agent-contract-current.md). When the schema bumps, that file updates first. - **Exit codes**: `0` pass, `2` config error, `3` parse error, `4` other error, `20` strict-mode gate failure. diff --git a/adoption-kits/codex-skill/.agents-shipgate-kit-metadata.json b/adoption-kits/codex-skill/.agents-shipgate-kit-metadata.json index 00c69bb8..e710c4ee 100644 --- a/adoption-kits/codex-skill/.agents-shipgate-kit-metadata.json +++ b/adoption-kits/codex-skill/.agents-shipgate-kit-metadata.json @@ -17,7 +17,8 @@ "8d5080cf5c4c429a6920b466fe5c3415d89fec3434760b549883ee9a458a48ac", "d2d6549ca3bde2ec2581e8712419ace340afc5be3078f2255884670f4a8db870", "37795c6ffc3dcdda624b609dd17da8656c245d00ea9cebb9fae25c50842a4a9b", - "a8dd8e22d9a9dd3358f9d7d328f6f5da5d80ac142681dfdb28b96b44bc68004b" + "a8dd8e22d9a9dd3358f9d7d328f6f5da5d80ac142681dfdb28b96b44bc68004b", + "ad6ca3c53872f1d7e2d8a42794a766f51f0c50a8b4599bb3b76ddd2e31260af1" ], "references/recipes.md": [ "df5110bfa05eeabd9b918d8902b5c054fa547d1155be61ef6e7d7d63378bf210", @@ -40,7 +41,8 @@ "75a655059f3d45db365c744b0ff82d1c9d69c3638acacf640fd667ae87260d05", "3e7bd6a3a882f5e52c0fc4f215c5589149f8eb24eeef0ea054854f03f0f050de", "6d2848f3436f6e246bf553e6cf061c990888d6ff39eb82fec9a41f291b2e94fe", - "7baeb0715e59daf92cf78fb9fee6e4f659174f5fcb6c9a1e3eca9f73fc697503" + "7baeb0715e59daf92cf78fb9fee6e4f659174f5fcb6c9a1e3eca9f73fc697503", + "b652d59a846ccf131df71cf284e10f2e5a72c6c1da6d5454067f1a6a457452d5" ], "agents/openai.yaml": [ "4d94a724336e5d36a2769630495f341007580e4dee306bc42a1aeca1af9e867b" diff --git a/adoption-kits/codex-skill/SKILL.md b/adoption-kits/codex-skill/SKILL.md index 11fbbeaf..8a5b6d38 100644 --- a/adoption-kits/codex-skill/SKILL.md +++ b/adoption-kits/codex-skill/SKILL.md @@ -20,7 +20,7 @@ Do not use it for general linting, runtime monitoring, evals, model-output quali 5. Default first-time CI to advisory mode. Do not enable release-blocking CI or save a baseline until a human has reviewed current findings. 6. For local agent control, run `shipgate check --agent codex --workspace . --format agent-boundary-json` and read the stdout `shipgate.agent_boundary_result/v1` object. Switch on `control.state`; follow only `control.next_action`, `control.allowed_next_commands`, and `control.human_review`. Treat `decision` as diagnostic context only. 7. Before editing `shipgate.yaml`, Shipgate CI, AGENTS/CLAUDE/Cursor rules, policy packs, baselines, waivers, suppressions, Codex hooks/config, Codex plugin manifests, `.mcp.json`, `.app.json`, or `SKILL.md`, plan to run `agents-shipgate verify` before completion and route trust-root review to a human when the verifier requires it. -8. For full PR verification, read `agents-shipgate-reports/agent-handoff.json` first and switch on `control.state`, then read `verifier.json` for detailed control state, `verify-run.json` for reproducibility metadata, and `report.json` for reviewer detail; `report.json.release_decision.decision` remains the release gate. +8. For full PR verification, validate `agents-shipgate-reports/verification-receipt.json` first, then read `agent-handoff.json` and switch on `control.state`; read `verifier.json` for detailed control state, `verify-run.json` for the request graph, and `report.json` for reviewer detail. `report.json.release_decision.decision` remains the release gate. 9. Auto-apply only high-confidence safe patches. Do not auto-assert action effect, action authority, agent bindings, approval, confirmation, idempotency, broad-scope, prohibited-action, or runtime-trace evidence. 10. Ensure `.gitignore` covers `agents-shipgate-reports/` before committing. diff --git a/adoption-kits/codex-skill/references/report-reading.md b/adoption-kits/codex-skill/references/report-reading.md index db3e7974..87c649eb 100644 --- a/adoption-kits/codex-skill/references/report-reading.md +++ b/adoption-kits/codex-skill/references/report-reading.md @@ -1,6 +1,7 @@ # Reading Agents Shipgate Reports -For verify runs, read `agents-shipgate-reports/agent-handoff.json` first. Then +For verify runs, validate `agents-shipgate-reports/verification-receipt.json` +first. Then read `agents-shipgate-reports/agent-handoff.json`. After that, read `agents-shipgate-reports/verifier.json` for detailed control context and `agents-shipgate-reports/report.json` for findings. Do not scrape Markdown. diff --git a/benchmark/safety-qualification/README.md b/benchmark/safety-qualification/README.md index 398603ec..6935339d 100644 --- a/benchmark/safety-qualification/README.md +++ b/benchmark/safety-qualification/README.md @@ -1,6 +1,6 @@ # Evidence-Backed Pass Safety Qualification -This directory is the runbook for the `0.16.0b5` beta safety qualification. +This directory is the runbook for the `0.16.0b6` beta safety qualification. The repository does **not** ship fabricated human labels or a passing result. Until a real frozen corpus and its verifier receipts exist, `safety-qualification.json` must not be published as qualified. @@ -11,20 +11,21 @@ The runner consumes four independently content-addressed inputs: 1. A built `agents-shipgate` wheel. The runner reads wheel metadata without importing or executing it and records the wheel SHA-256. -2. A `shipgate.safety_corpus/v3` JSON/YAML corpus. Every case has two blind, +2. A `shipgate.safety_corpus/v4` JSON/YAML corpus. Every case has two blind, attributable labels (`security_governance` and `framework_tooling`), an evidence-backed final decision, a remediation condition, and a third-human adjudication for every disagreement. `frozen_labels_sha256` must match the canonical label payload. -3. A `shipgate.safety_receipt_index/v3` JSON/YAML index created only after +3. A `shipgate.safety_receipt_index/v4` JSON/YAML index created only after labels are frozen. It binds the exact wheel, corpus, labels, policy bundle, - and one `shipgate.verify_run/v2` receipt per case. + and one terminal `shipgate.verification_receipt/v1` plus its + `shipgate.verify_run/v3` projection per case. 4. The qualification policy file(s) or directory. Directory hashing includes every file by stable relative path. Receipt entries must point to real `verify` artifacts. Each receipt needs successful base and head tree-bound runs plus content-addressed -`verifier_json` and `report_json` artifacts. The report must use schema `0.31`, +`verifier_json` and `report_json` artifacts. The report must use schema `0.34`, contain binding and semantic coverage, and agree with the verifier receipt. Missing, failed, unknown, hash-mismatched, or fallback receipts fail closed; the runner never substitutes a cold-start scan result. @@ -52,7 +53,7 @@ are marked `qualification_tier: test` and can never set ```bash PYTHONPATH=src python scripts/run_safety_qualification.py \ - --wheel dist/agents_shipgate-0.16.0b5-py3-none-any.whl \ + --wheel dist/agents_shipgate-0.16.0b6-py3-none-any.whl \ --corpus /secure/frozen-corpus.json \ --receipts /secure/receipt-index.json \ --policy /secure/qualification-policy/ \ diff --git a/docs/INDEX.md b/docs/INDEX.md index 49f672a3..9f82a255 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -36,7 +36,8 @@ A single entry point for human readers and AI agents walking the `docs/` tree. - [`checks.md`](checks.md) — full check catalog (human-readable) - [`checks.json`](checks.json) — machine-readable check catalog (regenerated each release) - [`manifest-v0.1.json`](manifest-v0.1.json) — JSON Schema for `shipgate.yaml` -- [`report-schema.v0.33.json`](report-schema.v0.33.json) — JSON Schema for `report.json` (current; emitted reports carry `report_schema_version: "0.33"`, typed policy-evidence support and non-waivable applicability gaps, the Conductor OSS summary, root-reachable binding facts, and the static-only verdict boundary) +- [`report-schema.v0.34.json`](report-schema.v0.34.json) — JSON Schema for `report.json` (current; emitted reports carry `report_schema_version: "0.34"`, typed policy-evidence support and non-waivable applicability gaps, the Conductor OSS summary, root-reachable binding facts, and the static-only verdict boundary) +- [`report-schema.v0.33.json`](report-schema.v0.33.json) — frozen v0.33 typed policy-evidence reference; pre-v0.34 reports validate against this - [`report-schema.v0.32.json`](report-schema.v0.32.json) — frozen v0.32 Conductor OSS summary reference; pre-v0.33 reports validate against this - [`report-schema.v0.31.json`](report-schema.v0.31.json) — frozen v0.31 root-reachable binding reference; pre-v0.32 reports validate against this - [`report-schema.v0.30.json`](report-schema.v0.30.json) — frozen v0.30 provider-scoped identity reference; pre-v0.31 reports validate against this @@ -45,13 +46,20 @@ A single entry point for human readers and AI agents walking the `docs/` tree. - [`report-schema.v0.27.json`](report-schema.v0.27.json) — frozen v0.27 reference schema; pre-v0.28 reports validate against this - [`report-schema.v0.26.json`](report-schema.v0.26.json) — frozen v0.26 reference schema; pre-v0.27 reports validate against this - [`report-schema.v0.25.json`](report-schema.v0.25.json) — frozen v0.25 reference schema; pre-v0.26 reports validate against this -- [`verifier-schema.v0.4.json`](verifier-schema.v0.4.json) — JSON Schema for `verifier.json`, including typed policy-evidence gaps and finding support +- [`verifier-schema.v0.5.json`](verifier-schema.v0.5.json) — JSON Schema for `verifier.json`, including typed policy-evidence gaps and finding support +- [`verifier-schema.v0.4.json`](verifier-schema.v0.4.json) — frozen v0.4 verifier reference - [`verifier-schema.v0.3.json`](verifier-schema.v0.3.json) — frozen v0.3 verifier reference - [`verifier-schema.v0.2.json`](verifier-schema.v0.2.json) — frozen v0.2 verifier reference - [`verifier-schema.v0.1.json`](verifier-schema.v0.1.json) — frozen v0.1 verifier reference -- [`verify-run-schema.v2.json`](verify-run-schema.v2.json) — JSON Schema for `verify-run.json`, the deterministic verify-run reproducibility artifact +- [`verify-run-schema.v3.json`](verify-run-schema.v3.json) — JSON Schema for `verify-run.json`, the deterministic verify-run reproducibility artifact +- [`verify-run-schema.v2.json`](verify-run-schema.v2.json) — frozen verify-run v2 reference - [`verify-run-schema.v1.json`](verify-run-schema.v1.json) — frozen verify-run v1 reference -- [`agent-handoff-schema.v4.json`](agent-handoff-schema.v4.json) — current compact verifier handoff schema with support hashes on blocking evidence +- [`agent-handoff-schema.v5.json`](agent-handoff-schema.v5.json) — current compact verifier handoff schema with support hashes on blocking evidence +- [`verification-plan-schema.v1.json`](verification-plan-schema.v1.json) — content-addressed verification subject, inputs, engine requirement, and task plan +- [`verification-unit-result-schema.v1.json`](verification-unit-result-schema.v1.json) — decision-free worker result contract +- [`verification-artifact-manifest-schema.v1.json`](verification-artifact-manifest-schema.v1.json) — content-addressed terminal artifact set +- [`verification-receipt-schema.v1.json`](verification-receipt-schema.v1.json) — terminal request, decision, executor, and artifact closure +- [`agent-handoff-schema.v4.json`](agent-handoff-schema.v4.json) — frozen handoff v4 reference - [`agent-handoff-schema.v3.json`](agent-handoff-schema.v3.json) — frozen handoff v3 reference - [`agent-handoff-schema.v2.json`](agent-handoff-schema.v2.json) — frozen handoff v2 reference - [`agent-result-schema.v2.json`](agent-result-schema.v2.json) — current shared local-check and MCP result schema @@ -64,10 +72,12 @@ A single entry point for human readers and AI agents walking the `docs/` tree. - [`policy-pack-schema.v0.3.json`](policy-pack-schema.v0.3.json) — frozen v0.3 policy-pack reference - [`policy-pack-schema.v0.2.json`](policy-pack-schema.v0.2.json) — frozen v0.2 policy-pack reference - [`policy-pack-schema.v0.1.json`](policy-pack-schema.v0.1.json) — frozen v0.1 reference schema for the flat match syntax -- [`attestation-schema.v0.4.json`](attestation-schema.v0.4.json) — JSON Schema for `attestation.json` emitted by `agents-shipgate attest`; adds verify-run binding, explicit CI event facts, capability-lock/diff summaries, and policy-pack pin records for cross-repo ledgers +- [`attestation-schema.v0.5.json`](attestation-schema.v0.5.json) — JSON Schema for `attestation.json` emitted by `agents-shipgate attest`; adds verify-run binding, explicit CI event facts, capability-lock/diff summaries, and policy-pack pin records for cross-repo ledgers +- [`attestation-schema.v0.4.json`](attestation-schema.v0.4.json) — frozen v0.4 attestation reference - [`org-governance-schema.v0.1.json`](org-governance-schema.v0.1.json) — JSON Schema for `agents-shipgate org status --json`; local governance projection, not a release verdict -- [`org-evidence-bundle-schema.v1.json`](org-evidence-bundle-schema.v1.json) — JSON Schema for `agents-shipgate org bundle`; compact CI/ledger ingestion artifact over verifier/report/attestation/org/host-grant evidence, not a release verdict -- [`registry-schema.v0.3.json`](registry-schema.v0.3.json) — JSON Schema for `agents-shipgate registry query --json`, `registry summary --json`, `registry verify --json`, and `registry report --bypass --json` +- [`org-evidence-bundle-schema.v2.json`](org-evidence-bundle-schema.v2.json) — JSON Schema for `agents-shipgate org bundle`; compact CI/ledger ingestion artifact over verifier/report/attestation/org/host-grant evidence, not a release verdict +- [`registry-schema.v0.4.json`](registry-schema.v0.4.json) — JSON Schema for `agents-shipgate registry query --json`, `registry summary --json`, `registry verify --json`, and `registry report --bypass --json` +- [`registry-schema.v0.3.json`](registry-schema.v0.3.json) — frozen v0.3 registry reference - [`host-grants-inventory-schema.v0.2.json`](host-grants-inventory-schema.v0.2.json) — current typed, redacted, scope-aware host inventory - [`host-grants-baseline-schema.v0.2.json`](host-grants-baseline-schema.v0.2.json) — current acknowledged host-grant baseline - [`host-grants-drift-schema.v0.2.json`](host-grants-drift-schema.v0.2.json) — current comparable/incomparable host-grant drift result @@ -111,7 +121,8 @@ A single entry point for human readers and AI agents walking the `docs/` tree. - [`report-schema.v0.8.json`](report-schema.v0.8.json) — frozen v0.8 reference schema; pre-v0.9 reports validate against this - [`report-schema.v0.7.json`](report-schema.v0.7.json) — frozen v0.7 reference schema; pre-v0.8 reports validate against this - [`report-schema.v0.6.json`](report-schema.v0.6.json) — frozen v0.6 reference schema; pre-v0.7 reports validate against this -- [`packet-schema.v0.11.json`](packet-schema.v0.11.json) — JSON Schema for the Release Evidence Packet (current; emitted packets project typed policy support, evidence gaps, semantic coverage, and the static-only verdict boundary in §1) +- [`packet-schema.v0.12.json`](packet-schema.v0.12.json) — JSON Schema for the Release Evidence Packet (current; emitted packets project typed policy support, evidence gaps, semantic coverage, and the static-only verdict boundary in §1) +- [`packet-schema.v0.11.json`](packet-schema.v0.11.json) — frozen v0.11 typed policy-evidence packet reference - [`packet-schema.v0.10.json`](packet-schema.v0.10.json) — frozen v0.10 binding-aware packet reference - [`packet-schema.v0.9.json`](packet-schema.v0.9.json) — frozen v0.9 reference packet schema; pre-v0.10 packets validate against this - [`packet-schema.v0.8.json`](packet-schema.v0.8.json) — frozen v0.8 reference packet schema; pre-v0.9 packets validate against this diff --git a/docs/agent-boundary-result-schema.v1.json b/docs/agent-boundary-result-schema.v1.json index fc595b39..b551d96b 100644 --- a/docs/agent-boundary-result-schema.v1.json +++ b/docs/agent-boundary-result-schema.v1.json @@ -401,7 +401,7 @@ "type": "string" }, "version": { - "default": "0.16.0b5", + "default": "0.16.0b6", "title": "Version", "type": "string" } diff --git a/docs/agent-contract-current.md b/docs/agent-contract-current.md index 8ac0f700..960e9bbc 100644 --- a/docs/agent-contract-current.md +++ b/docs/agent-contract-current.md @@ -10,22 +10,26 @@ Verify the installed CLI contract locally before relying on hard-coded docs: agents-shipgate contract --json ``` -Runtime contract v16 retains the v15 host-neutral boundary, v14 unambiguous -`AgentControl`, and v13 root-reachable binding contracts. It adds typed -evidence bases and tri-state policy applicability across scan, verify, packet, -and handoff projections. Agents +Runtime contract v17 retains the v16 typed policy-evidence, v15 host-neutral +boundary, v14 unambiguous `AgentControl`, and v13 root-reachable binding +contracts. It adds a content-addressed verification request, decision-free +worker result, artifact manifest, and terminal receipt shared by local and +distributed verification. Agents switch on `control.state`; `decision` remains diagnostic and `release_decision.decision` remains the release gate. Contract v14 requires `completion_allowed == (state == "complete")` and -`must_stop == (state == "human_review_required")`. Report v0.33 prevents rule -metadata from upgrading heuristic evidence; packet v0.11, capability standard -v0.5, capability lock v0.6, and capability-lock diff v0.7 project the same -typed support. +`must_stop == (state == "human_review_required")`. Report v0.34, packet v0.12, +verifier v0.5, verify-run v3, and handoff v5 bind their projections to the +same request and decision IDs. The terminal receipt hashes the complete +artifact set; see [Verification Identity and Reproduction](verification-reproducibility.md). The runtime contract also exposes the local agent command spec: `primary_commands{}`, `commands{}`, `default_paths{}`, `artifacts{}`, `agent_read_order[]`, `verifier_read_order[]`, `merge_verdicts[]`, `release_decisions[]`, `do_not_auto_assert[]`, `verifier_schema_version`, -`verify_run_schema_version`, `agent_handoff_schema_version`, +`verify_run_schema_version`, `verification_plan_schema_version`, +`verification_unit_result_schema_version`, +`verification_artifact_manifest_schema_version`, +`verification_receipt_schema_version`, `agent_handoff_schema_version`, `agent_handoff_schema_path`, `agent_handoff_artifact`, `agent_boundary_result_schema_version`, the deprecated `codex_boundary_result_schema_version`, `attestation_schema_version`, @@ -51,31 +55,32 @@ Downstream repos generated with `.shipgate/agent-contract.json`. - Latest release: `v0.15.0` -- In-tree runtime: `0.16.0b5` — see [pyproject.toml](../pyproject.toml) -- Runtime contract: `16` (minimum control contract: `14`) -- Current report schema: `0.33` — [`docs/report-schema.v0.33.json`](report-schema.v0.33.json) -- Current packet schema: `0.11` — [`docs/packet-schema.v0.11.json`](packet-schema.v0.11.json) +- In-tree runtime: `0.16.0b6` — see [pyproject.toml](../pyproject.toml) +- Runtime contract: `17` (minimum control contract: `14`) +- Current report schema: `0.34` — [`docs/report-schema.v0.34.json`](report-schema.v0.34.json) +- Current packet schema: `0.12` — [`docs/packet-schema.v0.12.json`](packet-schema.v0.12.json) - Current shared agent result schema: `agent_result_v2` — [`docs/agent-result-schema.v2.json`](agent-result-schema.v2.json) -- Current verifier schema: `0.4` — [`docs/verifier-schema.v0.4.json`](verifier-schema.v0.4.json) -- Current verify-run schema: `shipgate.verify_run/v2` — [`docs/verify-run-schema.v2.json`](verify-run-schema.v2.json) -- Current agent handoff schema: `shipgate.agent_handoff/v4` — [`docs/agent-handoff-schema.v4.json`](agent-handoff-schema.v4.json) +- Current verifier schema: `0.5` — [`docs/verifier-schema.v0.5.json`](verifier-schema.v0.5.json) +- Current verify-run schema: `shipgate.verify_run/v3` — [`docs/verify-run-schema.v3.json`](verify-run-schema.v3.json) +- Current verification identity schemas: [`plan v1`](verification-plan-schema.v1.json), [`unit result v1`](verification-unit-result-schema.v1.json), [`artifact manifest v1`](verification-artifact-manifest-schema.v1.json), and [`terminal receipt v1`](verification-receipt-schema.v1.json) +- Current agent handoff schema: `shipgate.agent_handoff/v5` — [`docs/agent-handoff-schema.v5.json`](agent-handoff-schema.v5.json) - Current agent boundary result schema: `shipgate.agent_boundary_result/v1` — [`docs/agent-boundary-result-schema.v1.json`](agent-boundary-result-schema.v1.json) - Frozen deprecated Codex projection: `shipgate.codex_boundary_result/v2` — [`docs/codex-boundary-result-schema.v2.json`](codex-boundary-result-schema.v2.json) - Current preflight schema: `0.3` — [`docs/preflight-schema.v0.3.json`](preflight-schema.v0.3.json) -- Current downstream local agent contract schema: `5` +- Current downstream local agent contract schema: `6` - Current capability standard: `0.5` — [`docs/capability-standard.md`](capability-standard.md) - Current capability lock schema: `0.6` — [`docs/capability-lock-schema.v0.6.json`](capability-lock-schema.v0.6.json) - Current capability lock diff schema: `0.7` — [`docs/capability-lock-diff-schema.v0.7.json`](capability-lock-diff-schema.v0.7.json) -- Current attestation schema: `0.4` — [`docs/attestation-schema.v0.4.json`](attestation-schema.v0.4.json) -- Current registry schema: `0.3` — [`docs/registry-schema.v0.3.json`](registry-schema.v0.3.json) -- Current org evidence bundle schema: `shipgate.org_evidence_bundle/v1` — [`docs/org-evidence-bundle-schema.v1.json`](org-evidence-bundle-schema.v1.json) +- Current attestation schema: `0.5` — [`docs/attestation-schema.v0.5.json`](attestation-schema.v0.5.json) +- Current registry schema: `0.4` — [`docs/registry-schema.v0.4.json`](registry-schema.v0.4.json) +- Current org evidence bundle schema: `shipgate.org_evidence_bundle/v2` — [`docs/org-evidence-bundle-schema.v2.json`](org-evidence-bundle-schema.v2.json) - Current host-grants inventory, baseline, and drift schemas: `0.2` — [`inventory`](host-grants-inventory-schema.v0.2.json), [`baseline`](host-grants-baseline-schema.v0.2.json), [`drift`](host-grants-drift-schema.v0.2.json) - Current trigger catalog schema: `0.2` — [`docs/triggers.json`](triggers.json) - Current governance benchmark catalog schema: `0.2` — [`docs/governance-benchmark-catalog-schema.v0.2.json`](governance-benchmark-catalog-schema.v0.2.json) - Current governance benchmark result schema: `0.2` — [`docs/governance-benchmark-result-schema.v0.2.json`](governance-benchmark-result-schema.v0.2.json) -- Frozen-reference report schemas: frozen [`v0.32`](report-schema.v0.32.json), frozen [`v0.31`](report-schema.v0.31.json), frozen [`v0.30`](report-schema.v0.30.json), frozen [`v0.29`](report-schema.v0.29.json), frozen [`v0.28`](report-schema.v0.28.json), frozen [`v0.27`](report-schema.v0.27.json), frozen [`v0.26`](report-schema.v0.26.json), frozen [`v0.25`](report-schema.v0.25.json), frozen [`v0.24`](report-schema.v0.24.json), frozen [`v0.23`](report-schema.v0.23.json), frozen [`v0.22`](report-schema.v0.22.json), frozen [`v0.21`](report-schema.v0.21.json), frozen [`v0.20`](report-schema.v0.20.json), frozen [`v0.19`](report-schema.v0.19.json), frozen [`v0.18`](report-schema.v0.18.json), frozen [`v0.17`](report-schema.v0.17.json), frozen [`v0.16`](report-schema.v0.16.json), frozen [`v0.15`](report-schema.v0.15.json), frozen [`v0.14`](report-schema.v0.14.json), frozen [`v0.13`](report-schema.v0.13.json), frozen [`v0.12`](report-schema.v0.12.json), frozen [`v0.11`](report-schema.v0.11.json), frozen [`v0.10`](report-schema.v0.10.json), frozen [`v0.9`](report-schema.v0.9.json), frozen [`v0.8`](report-schema.v0.8.json), frozen [`v0.7`](report-schema.v0.7.json), frozen [`v0.6`](report-schema.v0.6.json), older +- Frozen-reference report schemas: frozen [`v0.33`](report-schema.v0.33.json), frozen [`v0.32`](report-schema.v0.32.json), frozen [`v0.31`](report-schema.v0.31.json), frozen [`v0.30`](report-schema.v0.30.json), and older versions listed in [`docs/INDEX.md`](INDEX.md#reference) - Frozen-reference packet schemas live in [`docs/INDEX.md`](INDEX.md#reference). -- Boundary v1, verifier v0.1/v0.2/v0.3, verify-run v1, handoff v1/v2/v3, and preflight +- Boundary v1, verifier v0.1–v0.4, verify-run v1/v2, handoff v1–v4, and preflight v0.1/v0.2 remain frozen references for legacy readers. - Frozen experimental capability lock and governance benchmark result schemas live in [`docs/INDEX.md`](INDEX.md#reference). @@ -87,11 +92,13 @@ one decision engine. - **PR / controller flow** — an autonomous coding agent deciding *continue, repair, or stop*. Prefer + validate `agents-shipgate-reports/verification-receipt.json`, then read `agents-shipgate-reports/agent-handoff.json` for the compact - `shipgate.agent_handoff/v4` view: lead with `control.state`, then read + `shipgate.agent_handoff/v5` view: lead with `control.state`, then read `control.next_action`, `gate.merge_verdict`, and `reproducibility.run_id` for the - stable verify identity. `verifier.json` remains the authoritative controller - substrate and `verify-run.json` remains the reproducibility record; finally + content-addressed verify identity. `verifier.json` remains the authoritative + controller substrate and `verify-run.json` remains the detailed run + projection; finally confirm `report.json.release_decision.decision` for the release gate. `.well-known/agents-shipgate.json` → `agent_read_order` is the machine-readable cross-artifact order. `verifier_read_order` remains the @@ -281,7 +288,7 @@ separately), local input hashes (`config_sha256`, `baseline_sha256`, emitted files. It has no wall-clock timestamp and is not a second gate. `agents-shipgate-reports/agent-handoff.json` carries -`schema_version: "shipgate.agent_handoff/v4"` and top-level sections +`schema_version: "shipgate.agent_handoff/v5"` and top-level sections `gate`, `control`, `fix_task`, `blocked_by[]`, `remediation_plan[]`, `capability_review`, `reproducibility`, and `artifacts`. `gate.decision` mirrors `release_decision.decision`; `gate.merge_verdict` @@ -298,8 +305,8 @@ from existing artifacts with: agents-shipgate agent handoff --from agents-shipgate-reports/verifier.json --json ``` -In `agents-shipgate-reports/verifier.json`, read the v0.4 fields below (full -schema [`docs/verifier-schema.v0.4.json`](verifier-schema.v0.4.json)). **Lead +In `agents-shipgate-reports/verifier.json`, read the v0.5 fields below (full +schema [`docs/verifier-schema.v0.5.json`](verifier-schema.v0.5.json)). **Lead with `control.state`.** Every field below is a mirror or deterministic projection of `report.json`; `release_decision.decision` remains the gate. @@ -331,7 +338,7 @@ with `control.state`.** Every field below is a mirror or deterministic projectio - `decision` — mirror of `release_decision.decision` (or `null` when no scan ran). - `headline` — single-sentence, PR-comment-friendly summary (or `null`). - `control.human_review` and `control.next_action` are the only serialized - human-review and next-action authority in verifier v0.4. + human-review and next-action authority in verifier v0.5. - `AgentController`, `VerifierNextAction`, and `VerifierHumanReview` remain importable only as deprecated v0.1/v0.2 reader models. Verifier v0.4 does not emit or invoke the retired `build_agent_controller` projector. @@ -525,8 +532,8 @@ For reviewer-shaped output, also read the **Release Evidence Packet** at `[pdf]` extras are installed). The packet is a supporting/provisional reviewer projection, not a second gate. Packet outputs are redacted by the same default privacy layer as the report. The packet has fixed reviewer sections governed by -[`docs/packet-schema.v0.11.json`](packet-schema.v0.11.json) — see -[STABILITY.md §Release Evidence Packet](../STABILITY.md#release-evidence-packet-v011). +[`docs/packet-schema.v0.12.json`](packet-schema.v0.12.json) — see +[STABILITY.md §Release Evidence Packet](../STABILITY.md#release-evidence-packet-v012). Packet schema `0.9` carries the report's evidence-backed semantic coverage and gap remediation contract. Packet §1 also mirrors `static_analysis_only=true`, `runtime_behavior_verified=false`, and @@ -579,9 +586,9 @@ Companion prompt: [`prompts/explain-finding-to-user.md`](../prompts/explain-find - [STABILITY.md](../STABILITY.md) — full alpha stability contract. Source of truth for everything above. - [AGENTS.md](../AGENTS.md) — agent-facing instructions: install, run, single-turn flow, error semantics. -- [`docs/report-schema.v0.33.json`](report-schema.v0.33.json) — machine-validatable JSON Schema for the current report. +- [`docs/report-schema.v0.34.json`](report-schema.v0.34.json) — machine-validatable JSON Schema for the current report. - [`docs/privacy.md`](privacy.md) and [`docs/report-sensitive-fields.json`](report-sensitive-fields.json) — default redaction behavior and sensitive-field inventory. -- [`docs/packet-schema.v0.11.json`](packet-schema.v0.11.json) — machine-validatable JSON Schema for the current packet. +- [`docs/packet-schema.v0.12.json`](packet-schema.v0.12.json) — machine-validatable JSON Schema for the current packet. - [`docs/checks.json`](checks.json) — check catalog, including `mvp_tier` for MVP/readiness triage. ## See also diff --git a/docs/agent-handoff-schema.v5.json b/docs/agent-handoff-schema.v5.json new file mode 100644 index 00000000..65c93088 --- /dev/null +++ b/docs/agent-handoff-schema.v5.json @@ -0,0 +1,1101 @@ +{ + "$defs": { + "AgentActionRequiredControl": { + "additionalProperties": false, + "description": "Non-terminal state with one exact coding-agent-owned next step.", + "properties": { + "allowed_next_commands": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Allowed Next Commands", + "type": "array" + }, + "completion_allowed": { + "const": false, + "default": false, + "title": "Completion Allowed", + "type": "boolean" + }, + "human_review": { + "$ref": "#/$defs/NoHumanReview" + }, + "must_stop": { + "const": false, + "default": false, + "title": "Must Stop", + "type": "boolean" + }, + "next_action": { + "$ref": "#/$defs/CodingAgentAction" + }, + "reason": { + "minLength": 1, + "title": "Reason", + "type": "string" + }, + "state": { + "const": "agent_action_required", + "title": "State", + "type": "string" + }, + "stop_reason": { + "default": null, + "title": "Stop Reason", + "type": "null" + }, + "verify_required": { + "default": false, + "title": "Verify Required", + "type": "boolean" + } + }, + "required": [ + "state", + "reason", + "completion_allowed", + "must_stop", + "verify_required", + "next_action", + "allowed_next_commands", + "human_review", + "stop_reason" + ], + "title": "AgentActionRequiredControl", + "type": "object" + }, + "AgentControl": { + "discriminator": { + "mapping": { + "agent_action_required": "#/$defs/AgentActionRequiredControl", + "complete": "#/$defs/CompleteAgentControl", + "human_review_required": "#/$defs/HumanReviewRequiredControl" + }, + "propertyName": "state" + }, + "oneOf": [ + { + "$ref": "#/$defs/CompleteAgentControl" + }, + { + "$ref": "#/$defs/AgentActionRequiredControl" + }, + { + "$ref": "#/$defs/HumanReviewRequiredControl" + } + ] + }, + "AgentHandoffBlockedBy": { + "additionalProperties": false, + "properties": { + "baseline_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Baseline Status" + }, + "blocks_release": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Blocks Release" + }, + "bucket": { + "enum": [ + "blocker", + "review_item" + ], + "title": "Bucket", + "type": "string" + }, + "capability_refs": { + "items": { + "type": "string" + }, + "title": "Capability Refs", + "type": "array" + }, + "capability_trace_refs": { + "items": { + "type": "string" + }, + "title": "Capability Trace Refs", + "type": "array" + }, + "check_id": { + "title": "Check Id", + "type": "string" + }, + "fingerprint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Fingerprint" + }, + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Id" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "support_hash": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Support Hash" + }, + "title": { + "title": "Title", + "type": "string" + } + }, + "required": [ + "bucket", + "check_id", + "severity", + "title" + ], + "title": "AgentHandoffBlockedBy", + "type": "object" + }, + "AgentHandoffGateV3": { + "additionalProperties": false, + "description": "Current gate projection with explicit execution applicability.", + "properties": { + "applicability": { + "enum": [ + "not_evaluated", + "verified", + "not_applicable", + "failed" + ], + "title": "Applicability", + "type": "string" + }, + "can_merge_without_human": { + "default": false, + "title": "Can Merge Without Human", + "type": "boolean" + }, + "ci_would_fail": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ci Would Fail" + }, + "decision": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Decision" + }, + "gating_signal": { + "const": "release_decision.decision", + "default": "release_decision.decision", + "title": "Gating Signal", + "type": "string" + }, + "merge_verdict": { + "enum": [ + "mergeable", + "human_review_required", + "insufficient_evidence", + "blocked", + "unknown" + ], + "title": "Merge Verdict", + "type": "string" + }, + "runtime_behavior_verified": { + "const": false, + "default": false, + "title": "Runtime Behavior Verified", + "type": "boolean" + }, + "static_analysis_only": { + "const": true, + "default": true, + "title": "Static Analysis Only", + "type": "boolean" + }, + "static_verdict_disclaimer": { + "default": "This verdict covers deterministic static evidence only. Agents Shipgate did not execute the agent or prove runtime behavior, tool routing, credential enforcement, or safety.", + "title": "Static Verdict Disclaimer", + "type": "string" + } + }, + "required": [ + "merge_verdict", + "applicability" + ], + "title": "AgentHandoffGateV3", + "type": "object" + }, + "AgentHandoffRemediationStep": { + "additionalProperties": false, + "properties": { + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Actor" + }, + "check_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Check Id" + }, + "command": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Command" + }, + "finding_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Finding Id" + }, + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Id" + }, + "kind": { + "title": "Kind", + "type": "string" + }, + "patch": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Patch" + }, + "reason": { + "title": "Reason", + "type": "string" + }, + "safety": { + "enum": [ + "allowed", + "forbidden", + "patch" + ], + "title": "Safety", + "type": "string" + }, + "target": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Target" + } + }, + "required": [ + "safety", + "kind", + "reason" + ], + "title": "AgentHandoffRemediationStep", + "type": "object" + }, + "AgentHandoffReproducibility": { + "additionalProperties": false, + "properties": { + "artifact_sha256": { + "additionalProperties": { + "type": "string" + }, + "title": "Artifact Sha256", + "type": "object" + }, + "baseline_sha256": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Baseline Sha256" + }, + "config_sha256": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Config Sha256" + }, + "decision_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Decision Id" + }, + "engine_requirement_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Engine Requirement Id" + }, + "executor_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Executor Id" + }, + "input_set_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Input Set Id" + }, + "policy_packs": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "title": "Policy Packs", + "type": "array" + }, + "request_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Request Id" + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Run Id" + }, + "subject_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Subject Id" + } + }, + "title": "AgentHandoffReproducibility", + "type": "object" + }, + "AgentHandoffSubject": { + "additionalProperties": false, + "properties": { + "base_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Base Ref" + }, + "changed_files": { + "items": { + "type": "string" + }, + "title": "Changed Files", + "type": "array" + }, + "config": { + "title": "Config", + "type": "string" + }, + "head_ref": { + "default": "HEAD", + "title": "Head Ref", + "type": "string" + }, + "workspace": { + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "workspace", + "config" + ], + "title": "AgentHandoffSubject", + "type": "object" + }, + "AgentHandoffTool": { + "additionalProperties": false, + "properties": { + "name": { + "default": "agents-shipgate", + "title": "Name", + "type": "string" + }, + "version": { + "default": "0.16.0b6", + "title": "Version", + "type": "string" + } + }, + "title": "AgentHandoffTool", + "type": "object" + }, + "CodingAgentAction": { + "discriminator": { + "mapping": { + "configure": "#/$defs/CodingAgentCommandAction", + "discover": "#/$defs/CodingAgentCommandAction", + "fetch_base": "#/$defs/CodingAgentFetchBaseAction", + "initialize": "#/$defs/CodingAgentCommandAction", + "install": "#/$defs/CodingAgentCommandAction", + "repair": "#/$defs/CodingAgentCommandAction", + "rerun": "#/$defs/CodingAgentCommandAction", + "verify": "#/$defs/CodingAgentCommandAction" + }, + "propertyName": "kind" + }, + "oneOf": [ + { + "$ref": "#/$defs/CodingAgentCommandAction" + }, + { + "$ref": "#/$defs/CodingAgentFetchBaseAction" + } + ] + }, + "CodingAgentCommandAction": { + "additionalProperties": false, + "description": "An executable, exact next step owned by the coding agent.", + "properties": { + "actor": { + "const": "coding_agent", + "default": "coding_agent", + "title": "Actor", + "type": "string" + }, + "command": { + "minLength": 1, + "title": "Command", + "type": "string" + }, + "expects": { + "default": null, + "title": "Expects", + "type": "null" + }, + "kind": { + "enum": [ + "verify", + "discover", + "configure", + "initialize", + "repair", + "install", + "rerun" + ], + "title": "Kind", + "type": "string" + }, + "why": { + "minLength": 1, + "title": "Why", + "type": "string" + } + }, + "required": [ + "actor", + "kind", + "command", + "expects", + "why" + ], + "title": "CodingAgentCommandAction", + "type": "object" + }, + "CodingAgentFetchBaseAction": { + "additionalProperties": false, + "description": "A structured input request when an exact fetch command is unavailable.\n\nShipgate never fetches refs itself. ``expects`` therefore names the exact\nref or artifact a caller must make available before rerunning verification.", + "properties": { + "actor": { + "const": "coding_agent", + "default": "coding_agent", + "title": "Actor", + "type": "string" + }, + "command": { + "default": null, + "title": "Command", + "type": "null" + }, + "expects": { + "minLength": 1, + "title": "Expects", + "type": "string" + }, + "kind": { + "const": "fetch_base", + "title": "Kind", + "type": "string" + }, + "why": { + "minLength": 1, + "title": "Why", + "type": "string" + } + }, + "required": [ + "actor", + "kind", + "command", + "expects", + "why" + ], + "title": "CodingAgentFetchBaseAction", + "type": "object" + }, + "CompleteAgentControl": { + "additionalProperties": false, + "description": "Terminal state: the coding agent may report the task complete.", + "properties": { + "allowed_next_commands": { + "items": { + "minLength": 1, + "type": "string" + }, + "maxItems": 0, + "title": "Allowed Next Commands", + "type": "array" + }, + "completion_allowed": { + "const": true, + "default": true, + "title": "Completion Allowed", + "type": "boolean" + }, + "human_review": { + "$ref": "#/$defs/NoHumanReview" + }, + "must_stop": { + "const": false, + "default": false, + "title": "Must Stop", + "type": "boolean" + }, + "next_action": { + "default": null, + "title": "Next Action", + "type": "null" + }, + "reason": { + "minLength": 1, + "title": "Reason", + "type": "string" + }, + "state": { + "const": "complete", + "title": "State", + "type": "string" + }, + "stop_reason": { + "default": null, + "title": "Stop Reason", + "type": "null" + }, + "verify_required": { + "const": false, + "default": false, + "title": "Verify Required", + "type": "boolean" + } + }, + "required": [ + "state", + "reason", + "completion_allowed", + "must_stop", + "verify_required", + "next_action", + "allowed_next_commands", + "human_review", + "stop_reason" + ], + "title": "CompleteAgentControl", + "type": "object" + }, + "HumanControlAction": { + "additionalProperties": false, + "description": "A human-owned route. Human actions never expose executable commands.", + "properties": { + "actor": { + "const": "human", + "default": "human", + "title": "Actor", + "type": "string" + }, + "command": { + "default": null, + "title": "Command", + "type": "null" + }, + "expects": { + "default": null, + "title": "Expects", + "type": "null" + }, + "kind": { + "enum": [ + "review", + "stop" + ], + "title": "Kind", + "type": "string" + }, + "why": { + "minLength": 1, + "title": "Why", + "type": "string" + } + }, + "required": [ + "actor", + "kind", + "command", + "expects", + "why" + ], + "title": "HumanControlAction", + "type": "object" + }, + "HumanReviewRequiredControl": { + "additionalProperties": false, + "description": "Stopping state: no further coding-agent action is authorized.", + "properties": { + "allowed_next_commands": { + "items": { + "minLength": 1, + "type": "string" + }, + "maxItems": 0, + "title": "Allowed Next Commands", + "type": "array" + }, + "completion_allowed": { + "const": false, + "default": false, + "title": "Completion Allowed", + "type": "boolean" + }, + "human_review": { + "$ref": "#/$defs/RequiredHumanReview" + }, + "must_stop": { + "const": true, + "default": true, + "title": "Must Stop", + "type": "boolean" + }, + "next_action": { + "$ref": "#/$defs/HumanControlAction" + }, + "reason": { + "minLength": 1, + "title": "Reason", + "type": "string" + }, + "state": { + "const": "human_review_required", + "title": "State", + "type": "string" + }, + "stop_reason": { + "minLength": 1, + "title": "Stop Reason", + "type": "string" + }, + "verify_required": { + "default": false, + "title": "Verify Required", + "type": "boolean" + } + }, + "required": [ + "state", + "reason", + "completion_allowed", + "must_stop", + "verify_required", + "next_action", + "allowed_next_commands", + "human_review", + "stop_reason" + ], + "title": "HumanReviewRequiredControl", + "type": "object" + }, + "NoHumanReview": { + "additionalProperties": false, + "description": "Exact negative human-review projection for non-stopping states.", + "properties": { + "required": { + "const": false, + "default": false, + "title": "Required", + "type": "boolean" + }, + "required_reviewers": { + "items": { + "type": "string" + }, + "maxItems": 0, + "title": "Required Reviewers", + "type": "array" + }, + "why": { + "default": null, + "title": "Why", + "type": "null" + } + }, + "required": [ + "required", + "why", + "required_reviewers" + ], + "title": "NoHumanReview", + "type": "object" + }, + "RequiredHumanReview": { + "additionalProperties": false, + "description": "Human-review evidence carried by the stopping state.", + "properties": { + "required": { + "const": true, + "default": true, + "title": "Required", + "type": "boolean" + }, + "required_reviewers": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Required Reviewers", + "type": "array" + }, + "why": { + "minLength": 1, + "title": "Why", + "type": "string" + } + }, + "required": [ + "required", + "why", + "required_reviewers" + ], + "title": "RequiredHumanReview", + "type": "object" + } + }, + "$id": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/agent-handoff-schema.v5.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "JSON Schema for agents-shipgate-reports/agent-handoff.json. Generated from agents_shipgate.schemas.agent_handoff.AgentHandoffArtifact. It is a compact projection for coding agents and does not gate releases; release_decision.decision remains the only gate.", + "else": { + "properties": { + "control": { + "properties": { + "state": { + "enum": [ + "agent_action_required", + "human_review_required" + ] + } + }, + "required": [ + "state" + ] + } + } + }, + "if": { + "properties": { + "gate": { + "properties": { + "can_merge_without_human": { + "const": true + } + }, + "required": [ + "can_merge_without_human" + ] + } + }, + "required": [ + "gate" + ] + }, + "properties": { + "artifacts": { + "additionalProperties": { + "type": "string" + }, + "title": "Artifacts", + "type": "object" + }, + "blocked_by": { + "items": { + "$ref": "#/$defs/AgentHandoffBlockedBy" + }, + "title": "Blocked By", + "type": "array" + }, + "capability_review": { + "additionalProperties": true, + "title": "Capability Review", + "type": "object" + }, + "contract_version": { + "title": "Contract Version", + "type": "string" + }, + "control": { + "$ref": "#/$defs/AgentControl" + }, + "fix_task": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Fix Task" + }, + "forbidden_actions": { + "items": { + "type": "string" + }, + "title": "Forbidden Actions", + "type": "array" + }, + "forbidden_file_edits": { + "items": { + "type": "string" + }, + "title": "Forbidden File Edits", + "type": "array" + }, + "gate": { + "$ref": "#/$defs/AgentHandoffGateV3" + }, + "operation": { + "enum": [ + "verify_pr", + "verify_local", + "verify_preview" + ], + "title": "Operation", + "type": "string" + }, + "remediation_plan": { + "items": { + "$ref": "#/$defs/AgentHandoffRemediationStep" + }, + "title": "Remediation Plan", + "type": "array" + }, + "reproducibility": { + "$ref": "#/$defs/AgentHandoffReproducibility" + }, + "schema_version": { + "const": "shipgate.agent_handoff/v5", + "default": "shipgate.agent_handoff/v5", + "title": "Schema Version", + "type": "string" + }, + "subject": { + "$ref": "#/$defs/AgentHandoffSubject" + }, + "tool": { + "$ref": "#/$defs/AgentHandoffTool" + } + }, + "required": [ + "contract_version", + "operation", + "subject", + "gate", + "control" + ], + "then": { + "properties": { + "control": { + "properties": { + "state": { + "const": "complete" + } + }, + "required": [ + "state" + ] + }, + "fix_task": { + "type": "null" + } + } + }, + "title": "Agents Shipgate Agent Handoff v5", + "type": "object" +} diff --git a/docs/agent-native-merge-contract.md b/docs/agent-native-merge-contract.md index 8789b932..cdaa4b9c 100644 --- a/docs/agent-native-merge-contract.md +++ b/docs/agent-native-merge-contract.md @@ -136,7 +136,7 @@ protected* — never a new way to decide. hash bindings when verify emitted them. It is content-addressed; event time is copied only from explicit input or CI payloads, never generated from `now()`, and it does not gate. Schema: - [`attestation-schema.v0.4.json`](attestation-schema.v0.4.json). + [`attestation-schema.v0.5.json`](attestation-schema.v0.5.json). - **Agent reads:** the attestation is a durable record for humans and registries, not a control signal — the agent still acts on `control.state` (contracts 3–5). diff --git a/docs/agent-result-schema.v2.json b/docs/agent-result-schema.v2.json index 1dcbfd6c..4771cec7 100644 --- a/docs/agent-result-schema.v2.json +++ b/docs/agent-result-schema.v2.json @@ -401,7 +401,7 @@ "type": "string" }, "version": { - "default": "0.16.0b5", + "default": "0.16.0b6", "title": "Version", "type": "string" } diff --git a/docs/agents/protocol.md b/docs/agents/protocol.md index 51114f64..7f6d8875 100644 --- a/docs/agents/protocol.md +++ b/docs/agents/protocol.md @@ -309,7 +309,7 @@ capability evidence requests, and host/MCP permission review. `shipgate.explain` deterministic check/finding explanation JSON. `shipgate.capabilities` returns capability lock or capability lock diff JSON. `shipgate.handoff` reads existing `verifier.json` / `report.json` / `verify-run.json` artifacts and returns exact -`shipgate.agent_handoff/v4`. These are projections only; the +`shipgate.agent_handoff/v5`. These are projections only; the release gate remains `report.json.release_decision.decision`. The MCP server is a static adapter only. It exposes no scan, verify, diff --git a/docs/ai-search-summary.md b/docs/ai-search-summary.md index b8f0c7c5..31f222bc 100644 --- a/docs/ai-search-summary.md +++ b/docs/ai-search-summary.md @@ -90,7 +90,7 @@ Per-agent guides cover [Codex](agents/use-with-codex.md), [Claude Code](agents/use-with-claude-code.md), and [Cursor](agents/use-with-cursor.md). -The current source tree is `0.16.0b5` (runtime contract v16); the latest +The current source tree is `0.16.0b6` (runtime contract v17); the latest published release remains `v0.15.0` until that beta is cut. In report v0.32, `passed` is an evidence-backed static verdict: the configured root has a complete reachable binding graph, every reachable action has complete, @@ -169,6 +169,6 @@ shipgate, and Agents-Shipgate. - Agent instructions: [`../AGENTS.md`](../AGENTS.md) - Machine-readable summary: [`../llms.txt`](../llms.txt) - Discovery metadata: [`../.well-known/agents-shipgate.json`](../.well-known/agents-shipgate.json) -- Report schema (current): [`report-schema.v0.33.json`](report-schema.v0.33.json) (v0.31 frozen at [`report-schema.v0.31.json`](report-schema.v0.31.json), v0.30 frozen at [`report-schema.v0.30.json`](report-schema.v0.30.json)) -- Packet schema (current): [`packet-schema.v0.10.json`](packet-schema.v0.10.json) (v0.9 frozen at [`packet-schema.v0.9.json`](packet-schema.v0.9.json)) +- Report schema (current): [`report-schema.v0.34.json`](report-schema.v0.34.json) (v0.33 frozen at [`report-schema.v0.33.json`](report-schema.v0.33.json)) +- Packet schema (current): [`packet-schema.v0.12.json`](packet-schema.v0.12.json) (v0.11 frozen at [`packet-schema.v0.11.json`](packet-schema.v0.11.json)) - Check catalog: [`checks.json`](checks.json) diff --git a/docs/architecture.md b/docs/architecture.md index 04ed6a7a..3db36725 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,8 +2,8 @@ A single-page summary of the `agents-shipgate` codebase for new contributors and AI coding agents extending the project. Current as of -2026-07-09; auto-checked against `agents-shipgate contract --json`: -runtime contract `16`, report schema `v0.33`, packet schema `v0.11`. +2026-07-13; auto-checked against `agents-shipgate contract --json`: +runtime contract `17`, report schema `v0.34`, packet schema `v0.12`. For the per-field stability contract, see [`../STABILITY.md`](../STABILITY.md). For the agent-facing field index, @@ -520,7 +520,7 @@ containment); files larger than 10 MB rejected. `scan` emits a reviewer-shaped artifact alongside `report.{md,json,sarif}` whenever `output.packet.enabled` is true (default). The packet has its -own JSON contract ([`packet-schema.v0.11.json`](packet-schema.v0.11.json)) +own JSON contract ([`packet-schema.v0.12.json`](packet-schema.v0.12.json)) so the report schema stays minimal. The packet is derived from the in-memory scan (manifest, tools, @@ -664,10 +664,10 @@ contract. Headlines: - **Manifest schema** stable across `0.x` (`version: "0.1"`). - **Report JSON shape** is additive across the `0.x` line. Current - `report_schema_version: "0.33"`; older schemas frozen as + `report_schema_version: "0.34"`; older schemas frozen as `docs/report-schema.v0.N.json`. - **Packet JSON shape** is additive across the `0.x` line. Current - `packet_schema_version: "0.11"`; older schemas frozen. + `packet_schema_version: "0.12"`; older schemas frozen. - **Exit codes**: `0` pass, `2` manifest config error, `3` input parse error, `4` other error, `6` baseline integrity failure (strict `baseline verify` only), `20` strict-mode gate failure. diff --git a/docs/attestation-schema.v0.5.json b/docs/attestation-schema.v0.5.json new file mode 100644 index 00000000..2662bde6 --- /dev/null +++ b/docs/attestation-schema.v0.5.json @@ -0,0 +1,772 @@ +{ + "$defs": { + "AttestationCapabilityDiffBindingV1": { + "additionalProperties": false, + "properties": { + "base_semantic_capability_set_hash": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Base Semantic Capability Set Hash" + }, + "capability_lock_diff_schema_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Capability Lock Diff Schema Version" + }, + "head_semantic_capability_set_hash": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Head Semantic Capability Set Hash" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "sha256": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sha256" + }, + "summary": { + "anyOf": [ + { + "$ref": "#/$defs/AttestationCapabilityDiffSummaryV1" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "title": "AttestationCapabilityDiffBindingV1", + "type": "object" + }, + "AttestationCapabilityDiffSummaryV1": { + "additionalProperties": false, + "properties": { + "added": { + "default": 0, + "minimum": 0, + "title": "Added", + "type": "integer" + }, + "changed": { + "default": 0, + "minimum": 0, + "title": "Changed", + "type": "integer" + }, + "evidence_changed": { + "default": 0, + "minimum": 0, + "title": "Evidence Changed", + "type": "integer" + }, + "reidentified": { + "default": 0, + "minimum": 0, + "title": "Reidentified", + "type": "integer" + }, + "removed": { + "default": 0, + "minimum": 0, + "title": "Removed", + "type": "integer" + }, + "unchanged": { + "default": 0, + "minimum": 0, + "title": "Unchanged", + "type": "integer" + } + }, + "title": "AttestationCapabilityDiffSummaryV1", + "type": "object" + }, + "AttestationCapabilityLockBindingV1": { + "additionalProperties": false, + "properties": { + "capability_count": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Capability Count" + }, + "capability_lock_schema_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Capability Lock Schema Version" + }, + "evidence_set_hash": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Evidence Set Hash" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "semantic_capability_set_hash": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Semantic Capability Set Hash" + }, + "sha256": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sha256" + }, + "source_set_hash": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Set Hash" + } + }, + "title": "AttestationCapabilityLockBindingV1", + "type": "object" + }, + "AttestationCapabilitySummaryV1": { + "additionalProperties": false, + "properties": { + "added": { + "default": 0, + "minimum": 0, + "title": "Added", + "type": "integer" + }, + "change_ids": { + "items": { + "type": "string" + }, + "title": "Change Ids", + "type": "array" + }, + "modified": { + "default": 0, + "minimum": 0, + "title": "Modified", + "type": "integer" + }, + "policy_weakened": { + "default": false, + "title": "Policy Weakened", + "type": "boolean" + }, + "removed": { + "default": 0, + "minimum": 0, + "title": "Removed", + "type": "integer" + }, + "trust_root_touched": { + "default": false, + "title": "Trust Root Touched", + "type": "boolean" + } + }, + "title": "AttestationCapabilitySummaryV1", + "type": "object" + }, + "AttestationHumanAckV1": { + "additionalProperties": false, + "properties": { + "acks": { + "items": { + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "type": "object" + }, + "title": "Acks", + "type": "array" + }, + "outstanding": { + "items": { + "type": "string" + }, + "title": "Outstanding", + "type": "array" + }, + "required": { + "default": false, + "title": "Required", + "type": "boolean" + }, + "satisfied": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Satisfied" + } + }, + "title": "AttestationHumanAckV1", + "type": "object" + }, + "AttestationOrgContextV1": { + "additionalProperties": false, + "properties": { + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Actor" + }, + "merge_sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Merge Sha" + }, + "org_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Org Id" + }, + "pr_number": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Pr Number" + }, + "repo": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Repo" + }, + "service": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Service" + }, + "tier": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tier" + }, + "workflow_run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Workflow Run Id" + } + }, + "title": "AttestationOrgContextV1", + "type": "object" + }, + "AttestationVerdictV1": { + "additionalProperties": false, + "properties": { + "applicability": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Applicability" + }, + "can_merge_without_human": { + "default": false, + "title": "Can Merge Without Human", + "type": "boolean" + }, + "decision": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Decision" + }, + "merge_verdict": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Merge Verdict" + } + }, + "title": "AttestationVerdictV1", + "type": "object" + }, + "ReleaseAttestationV1": { + "additionalProperties": false, + "description": "Deterministic local release attestation emitted by ``agents-shipgate attest``.", + "properties": { + "artifact_set_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Artifact Set Id" + }, + "artifact_sha256": { + "additionalProperties": { + "type": "string" + }, + "title": "Artifact Sha256", + "type": "object" + }, + "attestation_schema_version": { + "const": "0.5", + "default": "0.5", + "title": "Attestation Schema Version", + "type": "string" + }, + "base_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Base Ref" + }, + "base_sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Base Sha" + }, + "base_tree_sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Base Tree Sha" + }, + "branch": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Branch" + }, + "capability": { + "$ref": "#/$defs/AttestationCapabilitySummaryV1" + }, + "capability_diff": { + "anyOf": [ + { + "$ref": "#/$defs/AttestationCapabilityDiffBindingV1" + }, + { + "type": "null" + } + ], + "default": null + }, + "capability_lock": { + "$ref": "#/$defs/AttestationCapabilityLockBindingV1" + }, + "cli_version": { + "title": "Cli Version", + "type": "string" + }, + "decision_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Decision Id" + }, + "event_time": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Event Time" + }, + "head_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Head Ref" + }, + "head_sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Head Sha" + }, + "head_tree_sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Head Tree Sha" + }, + "human_ack": { + "$ref": "#/$defs/AttestationHumanAckV1" + }, + "mode": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mode" + }, + "org": { + "$ref": "#/$defs/AttestationOrgContextV1" + }, + "policy_packs": { + "items": { + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "type": "object" + }, + "title": "Policy Packs", + "type": "array" + }, + "policy_snapshot_sha256": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Policy Snapshot Sha256" + }, + "receipt_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Receipt Id" + }, + "redacted": { + "default": true, + "title": "Redacted", + "type": "boolean" + }, + "request_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Request Id" + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Run Id" + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Url" + }, + "source_verifier": { + "title": "Source Verifier", + "type": "string" + }, + "verdict": { + "$ref": "#/$defs/AttestationVerdictV1" + }, + "verification_receipt_sha256": { + "anyOf": [ + { + "pattern": "^[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Receipt Sha256" + }, + "verify_run_sha256": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verify Run Sha256" + } + }, + "required": [ + "cli_version", + "source_verifier", + "verdict", + "capability", + "capability_lock", + "human_ack" + ], + "title": "ReleaseAttestationV1", + "type": "object" + } + }, + "$id": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/attestation-schema.v0.5.json", + "$ref": "#/$defs/ReleaseAttestationV1", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "JSON Schema for deterministic, local release attestations emitted by agents-shipgate attest. It binds verifier/report artifacts plus static capability lock/diff hashes when available. It does not gate; release_decision.decision remains the only gate.", + "title": "Agents Shipgate Release Attestation v0.5" +} diff --git a/docs/autofix-policy.md b/docs/autofix-policy.md index b0090997..8a1813e2 100644 --- a/docs/autofix-policy.md +++ b/docs/autofix-policy.md @@ -241,7 +241,7 @@ from the hash so toggling `--suggest-patches` doesn't shift it. - [`checks.md`](checks.md) — full check catalog with rationale. - [`minimal-real-configs.md`](minimal-real-configs.md) — per-framework minimal manifests to build from. -- [`report-schema.v0.33.json`](report-schema.v0.33.json) — current JSON +- [`report-schema.v0.34.json`](report-schema.v0.34.json) — current JSON Schema for `report.json`. - [`AGENTS.md`](../AGENTS.md) — top-level agent instructions, install, trigger table. diff --git a/docs/baseline.md b/docs/baseline.md index 7554e12d..6254489e 100644 --- a/docs/baseline.md +++ b/docs/baseline.md @@ -70,7 +70,7 @@ fail CI. Reports keep the v0.1 payload contract and add baseline fields: -- `report_schema_version: "0.33"` in current reports +- `report_schema_version: "0.34"` in current reports - `baseline.path` - `baseline.matched_count` - `baseline.new_count` diff --git a/docs/distribution.md b/docs/distribution.md index a45634e6..7aa6093c 100644 --- a/docs/distribution.md +++ b/docs/distribution.md @@ -38,7 +38,7 @@ exact wheel and signs `safety-qualification.json`: | Environment variable | Required value | |---|---| | `SAFETY_QUALIFICATION_WHEEL_URL` | HTTPS URL for the exact qualified wheel | -| `SAFETY_QUALIFICATION_WHEEL_FILENAME` | Safe wheel basename, for example `agents_shipgate-0.16.0b5-py3-none-any.whl` | +| `SAFETY_QUALIFICATION_WHEEL_FILENAME` | Safe wheel basename, for example `agents_shipgate-0.16.0b6-py3-none-any.whl` | | `SAFETY_QUALIFICATION_JSON_URL` | HTTPS URL for the production-qualified JSON artifact | | `SAFETY_QUALIFICATION_SIGSTORE_BUNDLE_URL` | HTTPS URL for that JSON artifact's Sigstore bundle | | `SAFETY_QUALIFICATION_SIGNER_IDENTITY` | Exact trusted certificate identity configured for qualification promotion | diff --git a/docs/examples.md b/docs/examples.md index 860e1503..59ff7969 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -69,9 +69,10 @@ The static scan fixtures write: - `agents-shipgate-reports/report.sarif` when requested or when using the GitHub Action The JSON output is the stable contract for tools and coding agents. See -[report-schema.v0.33.json](report-schema.v0.33.json) (current; emitted reports -carry `report_schema_version: "0.33"`, adding typed policy-evidence support -over the root-reachable binding graph and retaining normalized semantic assessments +[report-schema.v0.34.json](report-schema.v0.34.json) (current; emitted reports +carry `report_schema_version: "0.34"`, binding verify-native request and +decision identities while retaining typed policy-evidence support over the +root-reachable binding graph and normalized semantic assessments and evidence-backed pass coverage while preserving policy-pack routing metadata -in `findings[].policy_routing`; v0.32 is frozen at -[report-schema.v0.32.json](report-schema.v0.32.json)). +in `findings[].policy_routing`; v0.33 is frozen at +[report-schema.v0.33.json](report-schema.v0.33.json)). diff --git a/docs/examples/capability-lock.v0.6.example.json b/docs/examples/capability-lock.v0.6.example.json index 95723880..08c04f8f 100644 --- a/docs/examples/capability-lock.v0.6.example.json +++ b/docs/examples/capability-lock.v0.6.example.json @@ -1,7 +1,7 @@ { "capability_lock_schema_version": "0.6", "experimental": false, - "cli_version": "0.16.0b5", + "cli_version": "0.16.0b6", "source": { "config_path": "shipgate.yaml", "manifest_dir": ".", diff --git a/docs/faq.md b/docs/faq.md index aadd0ee1..b708ccd7 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -104,7 +104,7 @@ schema. - **Markdown** — `agents-shipgate-reports/report.md`, for human review. - **JSON** — `agents-shipgate-reports/report.json`, machine-readable - (schema v0.33, current). Always parse this for programmatic use. + (schema v0.34, current). Always parse this for programmatic use. For release gating, read `release_decision.decision`; the legacy `summary.status` field is baseline-blind (kept for v0.7 callers). A `passed` decision requires complete, conflict-free static surface, effect, @@ -120,14 +120,14 @@ schema. ## What is the Release Evidence Packet? A reviewer-shaped synthesis of the scan, emitted alongside the report by -default. The packet is governed by [`docs/packet-schema.v0.11.json`](packet-schema.v0.11.json) +default. The packet is governed by [`docs/packet-schema.v0.12.json`](packet-schema.v0.12.json) and has fixed reviewer sections (release decision, evidence matrix, capability/intent, high-risk surface, tool-surface diff, action-surface diff, approval coverage, idempotency risk, scope coverage, memory isolation, human-in-the-loop, dynamic scenarios, and a `not_proven` section that always lists prompt robustness, runtime behavior, model correctness, and adversarial resistance verbatim). See -[STABILITY.md §Release Evidence Packet](../STABILITY.md#release-evidence-packet-v011). +[STABILITY.md §Release Evidence Packet](../STABILITY.md#release-evidence-packet-v012). Packet schema `0.9` adds semantic coverage and gap remediation, and packet §1 mirrors the report's `static_analysis_only: true`, `runtime_behavior_verified: false`, and canonical @@ -148,9 +148,9 @@ Skip emission with `--no-packet`; re-render later with ## Is it production-ready? v0.15.0 is the latest published pre-1.0 beta. The in-tree runtime is -`0.16.0b5`, which prevents heuristic evidence from being upgraded into -authoritative policy findings on top of the multi-host boundary and -root-reachable binding contracts for beta qualification. The manifest schema +`0.16.0b6`, which adds content-addressed verification receipts and reproducible +distributed-execution boundaries on top of typed policy evidence, the +multi-host boundary, and root-reachable binding contracts. The manifest schema remains stable across the 0.x series; see [`STABILITY.md`](../STABILITY.md). Public preview. diff --git a/docs/integrations.md b/docs/integrations.md index a9017c20..ad9a1f06 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -92,7 +92,7 @@ Action outputs: | `report_markdown` | Path to `report.md`. | | `report_sarif` | Path to `report.sarif`. | | `verifier_json` | Path to `verifier.json`. | -| `verify_run_json` | Path to `verify-run.json`, which validates against [`verify-run-schema.v2.json`](verify-run-schema.v2.json). | +| `verify_run_json` | Path to `verify-run.json`, which validates against [`verify-run-schema.v3.json`](verify-run-schema.v3.json). | | `run_id` | Stable verify-run input identity from `verify-run.json.run_id`. | | `pr_comment_markdown` | Path to `pr-comment.md`. | | `exit_code` | Agents Shipgate CLI exit code. Matches `release_decision.fail_policy.exit_code`. | diff --git a/docs/mcp-server.md b/docs/mcp-server.md index db0200ea..c2dfa561 100644 --- a/docs/mcp-server.md +++ b/docs/mcp-server.md @@ -31,7 +31,7 @@ Claude Code registration (`.mcp.json`): | `shipgate.preflight` | `{workspace?, config?, plan?, changed_files?, diff_text?, capability_request?, base_preflight?}` | exact `PreflightResultV3` | | `shipgate.explain` | `{check_id}` or `{fingerprint, report_path}` | deterministic check/finding explanation JSON | | `shipgate.capabilities` | `{config}` or `{base_lock, head_lock}` | capability lock or capability lock diff JSON | -| `shipgate.handoff` | `{verifier_path, report_path?, verify_run_path?}` | exact `shipgate.agent_handoff/v4` | +| `shipgate.handoff` | `{verifier_path, report_path?, verify_run_path?}` | exact `shipgate.agent_handoff/v5` | `shipgate.check` is the same protocol surface documented in [`agents/protocol.md`](agents/protocol.md). `shipgate.preflight` is proactive @@ -43,7 +43,7 @@ but it is not a second release verdict. The release gate remains `shipgate.handoff` is a read-only projection over existing verifier artifacts. It never runs `verify`, shells out to git, or writes `agent-handoff.json`; it -returns the same `shipgate.agent_handoff/v4` shape that `verify` writes for +returns the same `shipgate.agent_handoff/v5` shape that `verify` writes for agents that need a compact control/release-readiness object. ## Trust model diff --git a/docs/org-evidence-bundle-schema.v2.json b/docs/org-evidence-bundle-schema.v2.json new file mode 100644 index 00000000..46eb2436 --- /dev/null +++ b/docs/org-evidence-bundle-schema.v2.json @@ -0,0 +1,281 @@ +{ + "$defs": { + "OrgEvidenceBundleArtifactRef": { + "additionalProperties": false, + "properties": { + "path": { + "title": "Path", + "type": "string" + }, + "sha256": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sha256" + } + }, + "required": [ + "path" + ], + "title": "OrgEvidenceBundleArtifactRef", + "type": "object" + }, + "OrgEvidenceBundleV1": { + "additionalProperties": false, + "description": "Compact local artifact for org/fleet aggregation.\n\nThis is a consumer artifact only. It cannot override or recalculate the\nrelease gate; ``release_decision.decision`` remains the only gating signal.", + "properties": { + "artifact_set_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Artifact Set Id" + }, + "artifacts": { + "additionalProperties": { + "$ref": "#/$defs/OrgEvidenceBundleArtifactRef" + }, + "title": "Artifacts", + "type": "object" + }, + "attestation": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Attestation" + }, + "bundle_kind": { + "const": "org_evidence", + "default": "org_evidence", + "title": "Bundle Kind", + "type": "string" + }, + "cli_version": { + "title": "Cli Version", + "type": "string" + }, + "config": { + "title": "Config", + "type": "string" + }, + "decision_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Decision Id" + }, + "gating_signal": { + "const": "release_decision.decision", + "default": "release_decision.decision", + "title": "Gating Signal", + "type": "string" + }, + "host_grant_drift": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Host Grant Drift" + }, + "host_grants": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Host Grants" + }, + "org_evidence_bundle_schema_version": { + "const": "shipgate.org_evidence_bundle/v2", + "default": "shipgate.org_evidence_bundle/v2", + "title": "Org Evidence Bundle Schema Version", + "type": "string" + }, + "org_status": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Org Status" + }, + "organization": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Organization" + }, + "policy_packs": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "title": "Policy Packs", + "type": "array" + }, + "privacy": { + "additionalProperties": true, + "title": "Privacy", + "type": "object" + }, + "receipt_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Receipt Id" + }, + "registry_row": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Registry Row" + }, + "request_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Request Id" + }, + "source_attestation": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Attestation" + }, + "source_report": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Report" + }, + "source_verification_receipt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Verification Receipt" + }, + "source_verifier": { + "title": "Source Verifier", + "type": "string" + }, + "source_verify_run": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Verify Run" + }, + "workspace": { + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "cli_version", + "workspace", + "config", + "source_verifier" + ], + "title": "OrgEvidenceBundleV1", + "type": "object" + } + }, + "$id": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/org-evidence-bundle-schema.v2.json", + "$ref": "#/$defs/OrgEvidenceBundleV1", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "JSON Schema for agents-shipgate org bundle --json. This bundle aggregates local deterministic artifacts for fleet reporting and does not produce a release verdict.", + "title": "Agents Shipgate Organization Evidence Bundle v1" +} diff --git a/docs/overview.md b/docs/overview.md index e295b2b4..b8c924fc 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -45,7 +45,7 @@ surface before production-like permissions are granted. - [Concepts](concepts.md) - [Manifest v0.1](manifest-v0.1.md) - [Check catalog](checks.md) -- [Report schema v0.33](report-schema.v0.33.json) (current; v0.32 frozen at [report-schema.v0.32.json](report-schema.v0.32.json), v0.31 frozen at [report-schema.v0.31.json](report-schema.v0.31.json)) +- [Report schema v0.34](report-schema.v0.34.json) (current; v0.33 frozen at [report-schema.v0.33.json](report-schema.v0.33.json), with older references in the [index](INDEX.md#reference)) - [Evidence-backed `passed` contract](passed-verdict-contract.md) - [Trust model](trust-model.md) - [Agent instructions](../AGENTS.md) diff --git a/docs/packet-schema.v0.12.json b/docs/packet-schema.v0.12.json new file mode 100644 index 00000000..be6bc832 --- /dev/null +++ b/docs/packet-schema.v0.12.json @@ -0,0 +1,2239 @@ +{ + "$defs": { + "ActionSurfaceDiffSection": { + "additionalProperties": false, + "description": "\u00a73B \u2014 compact action-surface diff for reviewers.", + "properties": { + "base_kind": { + "default": "none", + "title": "Base Kind", + "type": "string" + }, + "blocking_reasons": { + "items": { + "type": "string" + }, + "title": "Blocking Reasons", + "type": "array" + }, + "enabled": { + "default": false, + "title": "Enabled", + "type": "boolean" + }, + "highlights": { + "items": { + "type": "string" + }, + "title": "Highlights", + "type": "array" + }, + "notes": { + "items": { + "type": "string" + }, + "title": "Notes", + "type": "array" + }, + "status": { + "enum": [ + "covered", + "partial", + "not_declared", + "missing", + "informational" + ], + "title": "Status", + "type": "string" + }, + "summary": { + "$ref": "#/$defs/ActionSurfaceDiffSummary" + } + }, + "required": [ + "status" + ], + "title": "ActionSurfaceDiffSection", + "type": "object" + }, + "ActionSurfaceDiffSummary": { + "additionalProperties": false, + "properties": { + "actions_added": { + "default": 0, + "title": "Actions Added", + "type": "integer" + }, + "actions_modified": { + "default": 0, + "title": "Actions Modified", + "type": "integer" + }, + "actions_removed": { + "default": 0, + "title": "Actions Removed", + "type": "integer" + }, + "approvals_removed": { + "default": 0, + "title": "Approvals Removed", + "type": "integer" + }, + "blocking_findings": { + "default": 0, + "title": "Blocking Findings", + "type": "integer" + }, + "effect_escalations": { + "default": 0, + "title": "Effect Escalations", + "type": "integer" + }, + "input_schema_expansions": { + "default": 0, + "title": "Input Schema Expansions", + "type": "integer" + }, + "risk_tags_added": { + "default": 0, + "title": "Risk Tags Added", + "type": "integer" + }, + "safeguards_removed": { + "default": 0, + "title": "Safeguards Removed", + "type": "integer" + }, + "scope_expansions": { + "default": 0, + "title": "Scope Expansions", + "type": "integer" + } + }, + "title": "ActionSurfaceDiffSummary", + "type": "object" + }, + "ApprovalCoverageRow": { + "additionalProperties": false, + "properties": { + "declared": { + "title": "Declared", + "type": "boolean" + }, + "gap_finding_ids": { + "items": { + "type": "string" + }, + "title": "Gap Finding Ids", + "type": "array" + }, + "provider": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Provider" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + }, + "tool": { + "title": "Tool", + "type": "string" + }, + "tool_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Id" + } + }, + "required": [ + "tool", + "declared" + ], + "title": "ApprovalCoverageRow", + "type": "object" + }, + "ApprovalCoverageSection": { + "additionalProperties": false, + "description": "\u00a74 \u2014 approval policy coverage.", + "properties": { + "gap_findings": { + "items": { + "$ref": "#/$defs/ReleaseDecisionItem" + }, + "title": "Gap Findings", + "type": "array" + }, + "rows": { + "items": { + "$ref": "#/$defs/ApprovalCoverageRow" + }, + "title": "Rows", + "type": "array" + }, + "status": { + "enum": [ + "covered", + "partial", + "not_declared", + "missing", + "informational" + ], + "title": "Status", + "type": "string" + } + }, + "required": [ + "status" + ], + "title": "ApprovalCoverageSection", + "type": "object" + }, + "BaselineDelta": { + "properties": { + "enabled": { + "title": "Enabled", + "type": "boolean" + }, + "matched_count": { + "default": 0, + "title": "Matched Count", + "type": "integer" + }, + "new_count": { + "default": 0, + "title": "New Count", + "type": "integer" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "resolved_count": { + "default": 0, + "title": "Resolved Count", + "type": "integer" + } + }, + "required": [ + "enabled" + ], + "title": "BaselineDelta", + "type": "object" + }, + "BindingCoverageDecision": { + "properties": { + "gap_count": { + "default": 0, + "title": "Gap Count", + "type": "integer" + }, + "pass_eligible": { + "default": false, + "title": "Pass Eligible", + "type": "boolean" + }, + "possible_tools": { + "default": 0, + "title": "Possible Tools", + "type": "integer" + }, + "reachable_tools": { + "default": 0, + "title": "Reachable Tools", + "type": "integer" + }, + "reason_counts": { + "additionalProperties": { + "type": "integer" + }, + "title": "Reason Counts", + "type": "object" + }, + "total_catalog_tools": { + "default": 0, + "title": "Total Catalog Tools", + "type": "integer" + }, + "unbound_tools": { + "default": 0, + "title": "Unbound Tools", + "type": "integer" + } + }, + "title": "BindingCoverageDecision", + "type": "object" + }, + "CapabilityIntentDiff": { + "additionalProperties": false, + "description": "\u00a72 \u2014 capability \u2194 intent diff.", + "properties": { + "declared_purpose": { + "items": { + "type": "string" + }, + "title": "Declared Purpose", + "type": "array" + }, + "divergence_findings": { + "items": { + "$ref": "#/$defs/ReleaseDecisionItem" + }, + "title": "Divergence Findings", + "type": "array" + }, + "observed_tool_ids": { + "items": { + "type": "string" + }, + "title": "Observed Tool Ids", + "type": "array" + }, + "observed_tools": { + "items": { + "type": "string" + }, + "title": "Observed Tools", + "type": "array" + }, + "prohibited_actions": { + "items": { + "type": "string" + }, + "title": "Prohibited Actions", + "type": "array" + }, + "rows": { + "items": { + "$ref": "#/$defs/CapabilityIntentRow" + }, + "title": "Rows", + "type": "array" + }, + "status": { + "enum": [ + "covered", + "partial", + "not_declared", + "missing", + "informational" + ], + "title": "Status", + "type": "string" + } + }, + "required": [ + "status" + ], + "title": "CapabilityIntentDiff", + "type": "object" + }, + "CapabilityIntentRow": { + "additionalProperties": false, + "description": "One declared/observed pair in the \u00a72 capability \u2194 intent diff.", + "properties": { + "declared": { + "items": { + "type": "string" + }, + "title": "Declared", + "type": "array" + }, + "divergent": { + "items": { + "type": "string" + }, + "title": "Divergent", + "type": "array" + }, + "label": { + "title": "Label", + "type": "string" + }, + "observed": { + "items": { + "type": "string" + }, + "title": "Observed", + "type": "array" + } + }, + "required": [ + "label" + ], + "title": "CapabilityIntentRow", + "type": "object" + }, + "CapabilityTraceEvidenceSummary": { + "additionalProperties": false, + "description": "Deterministic counts for opt-in local runtime trace evidence.", + "properties": { + "agent_trace_count": { + "default": 0, + "title": "Agent Trace Count", + "type": "integer" + }, + "api_trace_count": { + "default": 0, + "title": "Api Trace Count", + "type": "integer" + }, + "approval_trace_count": { + "default": 0, + "title": "Approval Trace Count", + "type": "integer" + }, + "matched_trace_count": { + "default": 0, + "title": "Matched Trace Count", + "type": "integer" + }, + "source_count": { + "default": 0, + "title": "Source Count", + "type": "integer" + }, + "trace_count": { + "default": 0, + "title": "Trace Count", + "type": "integer" + }, + "unmatched_trace_count": { + "default": 0, + "title": "Unmatched Trace Count", + "type": "integer" + }, + "warning_count": { + "default": 0, + "title": "Warning Count", + "type": "integer" + } + }, + "title": "CapabilityTraceEvidenceSummary", + "type": "object" + }, + "DynamicScenarioRequirement": { + "additionalProperties": false, + "properties": { + "finding_ids": { + "items": { + "type": "string" + }, + "title": "Finding Ids", + "type": "array" + }, + "scenario": { + "title": "Scenario", + "type": "string" + }, + "why": { + "title": "Why", + "type": "string" + } + }, + "required": [ + "scenario", + "why" + ], + "title": "DynamicScenarioRequirement", + "type": "object" + }, + "DynamicScenariosSection": { + "additionalProperties": false, + "description": "\u00a79 \u2014 required dynamic scenarios.", + "properties": { + "scenarios": { + "items": { + "$ref": "#/$defs/DynamicScenarioRequirement" + }, + "title": "Scenarios", + "type": "array" + }, + "status": { + "enum": [ + "covered", + "partial", + "not_declared", + "missing", + "informational" + ], + "title": "Status", + "type": "string" + } + }, + "required": [ + "status" + ], + "title": "DynamicScenariosSection", + "type": "object" + }, + "EvidenceCoverageDecision": { + "properties": { + "binding_coverage": { + "$ref": "#/$defs/BindingCoverageDecision" + }, + "evidence_gaps": { + "items": { + "$ref": "#/$defs/EvidenceGap" + }, + "title": "Evidence Gaps", + "type": "array" + }, + "human_review_recommended": { + "title": "Human Review Recommended", + "type": "boolean" + }, + "identity_coverage": { + "$ref": "#/$defs/IdentityCoverageDecision" + }, + "level": { + "title": "Level", + "type": "string" + }, + "low_confidence_tool_count": { + "title": "Low Confidence Tool Count", + "type": "integer" + }, + "policy_gap_count": { + "default": 0, + "title": "Policy Gap Count", + "type": "integer" + }, + "semantic_coverage": { + "$ref": "#/$defs/SemanticCoverageDecision" + }, + "source_warning_count": { + "title": "Source Warning Count", + "type": "integer" + } + }, + "required": [ + "level", + "human_review_recommended", + "source_warning_count", + "low_confidence_tool_count" + ], + "title": "EvidenceCoverageDecision", + "type": "object" + }, + "EvidenceGap": { + "description": "v0.26: one structured row per measurable evidence gap.\n\n``insufficient_evidence`` previously diagnosed without prescribing;\neach gap names the degraded subject and the specific next action\nthat raises extraction confidence. Purely explanatory \u2014 gating\nstill uses only the counts (the gap list is a projection of them).", + "properties": { + "kind": { + "enum": [ + "low_confidence_tool", + "source_warning", + "incomplete_surface", + "missing_effect_evidence", + "inferred_effect_only", + "conflicting_effect_evidence", + "missing_authority_evidence", + "partial_authority_evidence", + "conflicting_authority_evidence", + "invalid_semantic_annotation", + "incomplete_tool_identity", + "conflicting_tool_identity", + "unresolved_tool_selector", + "ambiguous_tool_selector", + "ambiguous_legacy_tool_identity", + "invalid_tool_binding", + "missing_binding_evidence", + "partial_binding_evidence", + "conflicting_binding_evidence", + "ambiguous_root_agent", + "unresolved_agent_binding", + "unresolved_bound_tool", + "incomplete_handoff_graph", + "invalid_binding_annotation", + "invalid_evidence_provenance", + "inferred_policy_applicability", + "mixed_policy_evidence", + "unknown_policy_evidence", + "conflicting_policy_evidence" + ], + "title": "Kind", + "type": "string" + }, + "next_action": { + "$ref": "#/$defs/EvidenceGapAction" + }, + "source_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Ref" + }, + "source_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Type" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "why": { + "title": "Why", + "type": "string" + } + }, + "required": [ + "kind", + "subject", + "why", + "next_action" + ], + "title": "EvidenceGap", + "type": "object" + }, + "EvidenceGapAction": { + "description": "One concrete, mechanically-executable step that closes a gap.\n\nMirrors the agent-mode ``next_actions[]`` error shape\n(``kind``/``command``/``path``/``why``/``expects``) so agents reuse\none routing vocabulary across error recovery and evidence repair.", + "properties": { + "accepted_values": { + "items": { + "type": "string" + }, + "title": "Accepted Values", + "type": "array" + }, + "auto_apply": { + "const": false, + "default": false, + "title": "Auto Apply", + "type": "boolean" + }, + "command": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Command" + }, + "declaration_template": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Declaration Template" + }, + "expects": { + "title": "Expects", + "type": "string" + }, + "kind": { + "enum": [ + "declare_tool_inventory", + "provide_source", + "review_warning", + "declare_action_effect", + "declare_action_authority", + "provide_complete_inventory", + "resolve_semantic_conflict", + "declare_source_identity", + "qualify_tool_selector", + "provide_tool_binding", + "resolve_tool_identity_conflict", + "regenerate_identity_artifact", + "declare_agent_root", + "declare_agent_bindings", + "provide_static_binding_source", + "provide_complete_binding_graph", + "resolve_binding_conflict", + "regenerate_binding_artifact", + "provide_policy_evidence", + "review_policy_evidence", + "resolve_policy_evidence_conflict" + ], + "title": "Kind", + "type": "string" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "requires_human_review": { + "const": true, + "default": true, + "title": "Requires Human Review", + "type": "boolean" + }, + "suggested_patch_kind": { + "const": "manual", + "default": "manual", + "title": "Suggested Patch Kind", + "type": "string" + }, + "why": { + "title": "Why", + "type": "string" + } + }, + "required": [ + "kind", + "why", + "expects" + ], + "title": "EvidenceGapAction", + "type": "object" + }, + "EvidenceMatrixRow": { + "additionalProperties": false, + "description": "One compact row in the packet-only evidence matrix.\n\nThe row is a derived review aid only. ``blocking_findings`` and\n``review_items`` are subsets copied from ``release_decision``; the\nmatrix never reclassifies findings or contributes to gate behavior.", + "properties": { + "blocking_findings": { + "items": { + "$ref": "#/$defs/ReleaseDecisionItem" + }, + "title": "Blocking Findings", + "type": "array" + }, + "confidence": { + "default": "unknown", + "enum": [ + "high", + "medium", + "low", + "mixed", + "unknown" + ], + "title": "Confidence", + "type": "string" + }, + "domain": { + "enum": [ + "Inventory", + "Schema", + "Auth", + "Approval", + "Confirmation", + "Idempotency", + "Side effects", + "Memory isolation", + "Human-in-the-loop evidence", + "Prompt/scope alignment", + "Retry/timeout", + "Baseline debt", + "Action-surface policy" + ], + "title": "Domain", + "type": "string" + }, + "evidence_present": { + "enum": [ + "covered", + "partial", + "not_declared", + "missing", + "informational" + ], + "title": "Evidence Present", + "type": "string" + }, + "evidence_source": { + "items": { + "type": "string" + }, + "title": "Evidence Source", + "type": "array" + }, + "missing_controls": { + "items": { + "type": "string" + }, + "title": "Missing Controls", + "type": "array" + }, + "review_items": { + "items": { + "$ref": "#/$defs/ReleaseDecisionItem" + }, + "title": "Review Items", + "type": "array" + } + }, + "required": [ + "domain", + "evidence_present" + ], + "title": "EvidenceMatrixRow", + "type": "object" + }, + "EvidenceMatrixSection": { + "additionalProperties": false, + "description": "Compact packet-only review matrix derived from public report JSON.", + "properties": { + "notes": { + "items": { + "type": "string" + }, + "title": "Notes", + "type": "array" + }, + "rows": { + "items": { + "$ref": "#/$defs/EvidenceMatrixRow" + }, + "title": "Rows", + "type": "array" + } + }, + "title": "EvidenceMatrixSection", + "type": "object" + }, + "FailPolicy": { + "properties": { + "ci_mode": { + "title": "Ci Mode", + "type": "string" + }, + "exit_code": { + "title": "Exit Code", + "type": "integer" + }, + "fail_on": { + "items": { + "enum": [ + "info", + "low", + "medium", + "high", + "critical" + ], + "type": "string" + }, + "title": "Fail On", + "type": "array" + }, + "new_findings_only": { + "default": false, + "title": "New Findings Only", + "type": "boolean" + }, + "would_fail_ci": { + "title": "Would Fail Ci", + "type": "boolean" + } + }, + "required": [ + "ci_mode", + "would_fail_ci", + "exit_code" + ], + "title": "FailPolicy", + "type": "object" + }, + "FindingSupport": { + "additionalProperties": false, + "description": "Authoritative support for finding confidence and release contribution.\n\nRule metadata may request a severity or block, but it cannot upgrade the\nunderlying evidence. ``support_hash`` binds baselines and audit surfaces\nto the predicate evidence that actually made the finding eligible.", + "properties": { + "blocking_eligible": { + "default": false, + "title": "Blocking Eligible", + "type": "boolean" + }, + "claim_ids": { + "items": { + "type": "string" + }, + "title": "Claim Ids", + "type": "array" + }, + "confidence": { + "default": "low", + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "evidence_bases": { + "items": { + "enum": [ + "reviewed_declaration", + "protocol_structure", + "typed_provider_fact", + "structural_scope", + "inferred_keyword", + "inferred_regex", + "protocol_default", + "unknown" + ], + "type": "string" + }, + "title": "Evidence Bases", + "type": "array" + }, + "policy_eligible": { + "default": false, + "title": "Policy Eligible", + "type": "boolean" + }, + "predicates": { + "items": { + "$ref": "#/$defs/PolicyPredicateEvidence" + }, + "title": "Predicates", + "type": "array" + }, + "status": { + "default": "matched", + "enum": [ + "matched", + "not_matched", + "indeterminate", + "conflicting" + ], + "title": "Status", + "type": "string" + }, + "support_hash": { + "title": "Support Hash", + "type": "string" + } + }, + "required": [ + "support_hash" + ], + "title": "FindingSupport", + "type": "object" + }, + "HighRiskSurfaceSection": { + "additionalProperties": false, + "description": "\u00a73 \u2014 high-risk tool surface.", + "properties": { + "high_risk_count": { + "default": 0, + "title": "High Risk Count", + "type": "integer" + }, + "status": { + "enum": [ + "covered", + "partial", + "not_declared", + "missing", + "informational" + ], + "title": "Status", + "type": "string" + }, + "tools": { + "items": { + "$ref": "#/$defs/HighRiskToolEntry" + }, + "title": "Tools", + "type": "array" + }, + "total_tools": { + "default": 0, + "title": "Total Tools", + "type": "integer" + } + }, + "required": [ + "status" + ], + "title": "HighRiskSurfaceSection", + "type": "object" + }, + "HighRiskToolEntry": { + "additionalProperties": false, + "properties": { + "has_approval_policy": { + "default": false, + "title": "Has Approval Policy", + "type": "boolean" + }, + "has_idempotency_policy": { + "default": false, + "title": "Has Idempotency Policy", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "provider": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Provider" + }, + "risk_tags": { + "items": { + "type": "string" + }, + "title": "Risk Tags", + "type": "array" + }, + "source_type": { + "title": "Source Type", + "type": "string" + }, + "tool_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Id" + } + }, + "required": [ + "name", + "source_type" + ], + "title": "HighRiskToolEntry", + "type": "object" + }, + "HitlSourceProvenance": { + "additionalProperties": false, + "properties": { + "detail": { + "title": "Detail", + "type": "string" + }, + "location": { + "title": "Location", + "type": "string" + }, + "ref": { + "title": "Ref", + "type": "string" + }, + "status": { + "enum": [ + "requirement_only", + "expected_but_absent", + "source_load_failed", + "loaded", + "loaded_with_warnings" + ], + "title": "Status", + "type": "string" + }, + "type": { + "enum": [ + "approval_trace", + "agent_trace", + "override_log", + "high_risk_exclusion", + "promotion_criteria", + "manifest_requirement" + ], + "title": "Type", + "type": "string" + } + }, + "required": [ + "type", + "ref", + "location", + "status", + "detail" + ], + "title": "HitlSourceProvenance", + "type": "object" + }, + "HumanInTheLoopEvidence": { + "additionalProperties": false, + "description": "\u00a78 \u2014 human-in-the-loop evidence.\n\n``trace_findings`` is kept for packet v0.1 compatibility. It now\ncarries HITL evidence gaps surfaced as findings, including the\noriginal trace-derived gaps and validation-config evidence gaps.", + "properties": { + "approval_required_tools": { + "items": { + "type": "string" + }, + "title": "Approval Required Tools", + "type": "array" + }, + "capability_trace_refs": { + "items": { + "type": "string" + }, + "title": "Capability Trace Refs", + "type": "array" + }, + "capability_trace_summary": { + "$ref": "#/$defs/CapabilityTraceEvidenceSummary" + }, + "confirmation_required_tools": { + "items": { + "type": "string" + }, + "title": "Confirmation Required Tools", + "type": "array" + }, + "human_review_recommended": { + "default": false, + "title": "Human Review Recommended", + "type": "boolean" + }, + "is_configured": { + "default": false, + "title": "Is Configured", + "type": "boolean" + }, + "provenance_mode": { + "default": "fresh_scan", + "enum": [ + "fresh_scan", + "rebuilt_from_findings", + "unavailable" + ], + "title": "Provenance Mode", + "type": "string" + }, + "runtime_control_disclaimer": { + "default": "HITL evidence is local review evidence only. Missing local evidence does not prove a runtime control is absent, and present local evidence does not certify runtime enforcement.", + "title": "Runtime Control Disclaimer", + "type": "string" + }, + "source_provenance": { + "items": { + "$ref": "#/$defs/HitlSourceProvenance" + }, + "title": "Source Provenance", + "type": "array" + }, + "status": { + "enum": [ + "covered", + "partial", + "not_declared", + "missing", + "informational" + ], + "title": "Status", + "type": "string" + }, + "trace_findings": { + "items": { + "$ref": "#/$defs/ReleaseDecisionItem" + }, + "title": "Trace Findings", + "type": "array" + } + }, + "required": [ + "status" + ], + "title": "HumanInTheLoopEvidence", + "type": "object" + }, + "IdempotencyRiskSection": { + "additionalProperties": false, + "description": "\u00a75 \u2014 idempotency / retry risk.", + "properties": { + "gap_findings": { + "items": { + "$ref": "#/$defs/ReleaseDecisionItem" + }, + "title": "Gap Findings", + "type": "array" + }, + "retry_policy_declared": { + "default": false, + "title": "Retry Policy Declared", + "type": "boolean" + }, + "rows": { + "items": { + "$ref": "#/$defs/IdempotencyRow" + }, + "title": "Rows", + "type": "array" + }, + "status": { + "enum": [ + "covered", + "partial", + "not_declared", + "missing", + "informational" + ], + "title": "Status", + "type": "string" + } + }, + "required": [ + "status" + ], + "title": "IdempotencyRiskSection", + "type": "object" + }, + "IdempotencyRow": { + "additionalProperties": false, + "properties": { + "declared": { + "title": "Declared", + "type": "boolean" + }, + "gap_finding_ids": { + "items": { + "type": "string" + }, + "title": "Gap Finding Ids", + "type": "array" + }, + "provider": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Provider" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + }, + "tool": { + "title": "Tool", + "type": "string" + }, + "tool_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Id" + } + }, + "required": [ + "tool", + "declared" + ], + "title": "IdempotencyRow", + "type": "object" + }, + "IdentityCoverageDecision": { + "properties": { + "ambiguous_name_count": { + "default": 0, + "title": "Ambiguous Name Count", + "type": "integer" + }, + "bound_tools": { + "default": 0, + "title": "Bound Tools", + "type": "integer" + }, + "canonical_tools": { + "default": 0, + "title": "Canonical Tools", + "type": "integer" + }, + "gap_count": { + "default": 0, + "title": "Gap Count", + "type": "integer" + }, + "pass_eligible_tools": { + "default": 0, + "title": "Pass Eligible Tools", + "type": "integer" + }, + "reason_counts": { + "additionalProperties": { + "type": "integer" + }, + "title": "Reason Counts", + "type": "object" + }, + "total_observations": { + "default": 0, + "title": "Total Observations", + "type": "integer" + } + }, + "title": "IdentityCoverageDecision", + "type": "object" + }, + "MemoryIsolationStatus": { + "additionalProperties": false, + "description": "\u00a77 \u2014 memory isolation. v0.1 always renders ``is_declared=False``;\na future manifest schema may add ``agent.memory`` and populate this\nsection. Until then the structural slot is preserved so packets\nhave a consistent shape across versions.", + "properties": { + "is_declared": { + "default": false, + "title": "Is Declared", + "type": "boolean" + }, + "notes": { + "default": "Manifest does not declare a memory isolation policy. The current manifest schema (v0.1) has no agent.memory field. See \u00a710 for the residual review item.", + "title": "Notes", + "type": "string" + }, + "status": { + "default": "not_declared", + "enum": [ + "covered", + "partial", + "not_declared", + "missing", + "informational" + ], + "title": "Status", + "type": "string" + } + }, + "title": "MemoryIsolationStatus", + "type": "object" + }, + "NotProvenItem": { + "additionalProperties": false, + "properties": { + "body": { + "title": "Body", + "type": "string" + }, + "label": { + "title": "Label", + "type": "string" + } + }, + "required": [ + "label", + "body" + ], + "title": "NotProvenItem", + "type": "object" + }, + "NotProvenSection": { + "additionalProperties": false, + "description": "\u00a710 \u2014 what Shipgate did NOT prove. Combines unconditional\ndisclaimers with per-run residuals so reviewers can see both the\nstatic scope and what's missing for this specific scan.", + "properties": { + "additional_residuals": { + "items": { + "type": "string" + }, + "title": "Additional Residuals", + "type": "array" + }, + "headline": { + "title": "Headline", + "type": "string" + }, + "low_confidence_tools": { + "items": { + "type": "string" + }, + "title": "Low Confidence Tools", + "type": "array" + }, + "source_warnings": { + "items": { + "type": "string" + }, + "title": "Source Warnings", + "type": "array" + }, + "suppressed_finding_ids": { + "items": { + "type": "string" + }, + "title": "Suppressed Finding Ids", + "type": "array" + }, + "unconditional": { + "items": { + "$ref": "#/$defs/NotProvenItem" + }, + "title": "Unconditional", + "type": "array" + } + }, + "required": [ + "headline" + ], + "title": "NotProvenSection", + "type": "object" + }, + "PolicyPredicateEvidence": { + "additionalProperties": false, + "description": "One tri-state policy predicate and the evidence that supports it.", + "properties": { + "claim_ids": { + "items": { + "type": "string" + }, + "title": "Claim Ids", + "type": "array" + }, + "confidence": { + "default": "low", + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "evidence_bases": { + "items": { + "enum": [ + "reviewed_declaration", + "protocol_structure", + "typed_provider_fact", + "structural_scope", + "inferred_keyword", + "inferred_regex", + "protocol_default", + "unknown" + ], + "type": "string" + }, + "title": "Evidence Bases", + "type": "array" + }, + "expected": { + "default": null, + "title": "Expected" + }, + "observed": { + "default": null, + "title": "Observed" + }, + "policy_eligible": { + "default": false, + "title": "Policy Eligible", + "type": "boolean" + }, + "predicate": { + "title": "Predicate", + "type": "string" + }, + "status": { + "enum": [ + "matched", + "not_matched", + "indeterminate", + "conflicting" + ], + "title": "Status", + "type": "string" + }, + "why": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Why" + } + }, + "required": [ + "predicate", + "status" + ], + "title": "PolicyPredicateEvidence", + "type": "object" + }, + "ReleaseDecisionItem": { + "properties": { + "baseline_status": { + "anyOf": [ + { + "enum": [ + "new", + "matched", + "resolved" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Baseline Status" + }, + "blocks_release": { + "default": false, + "title": "Blocks Release", + "type": "boolean" + }, + "capability_trace_refs": { + "items": { + "type": "string" + }, + "title": "Capability Trace Refs", + "type": "array" + }, + "check_id": { + "title": "Check Id", + "type": "string" + }, + "fingerprint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Fingerprint" + }, + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Id" + }, + "policy_evidence_source": { + "anyOf": [ + { + "$ref": "#/$defs/SourceReference" + }, + { + "type": "null" + } + ], + "default": null + }, + "severity": { + "enum": [ + "info", + "low", + "medium", + "high", + "critical" + ], + "title": "Severity", + "type": "string" + }, + "source": { + "anyOf": [ + { + "$ref": "#/$defs/SourceReference" + }, + { + "type": "null" + } + ], + "default": null + }, + "support": { + "anyOf": [ + { + "$ref": "#/$defs/FindingSupport" + }, + { + "type": "null" + } + ], + "default": null + }, + "title": { + "title": "Title", + "type": "string" + } + }, + "required": [ + "check_id", + "severity", + "title" + ], + "title": "ReleaseDecisionItem", + "type": "object" + }, + "ReleaseDecisionSection": { + "additionalProperties": false, + "description": "\u00a71 \u2014 release decision. Verdict derives from\n``release_decision.decision`` only; ``fail_policy`` is rendered as\nseparate CI behavior metadata, never as the verdict source.", + "properties": { + "baseline_delta": { + "$ref": "#/$defs/BaselineDelta" + }, + "blockers": { + "items": { + "$ref": "#/$defs/ReleaseDecisionItem" + }, + "title": "Blockers", + "type": "array" + }, + "decision": { + "enum": [ + "blocked", + "review_required", + "insufficient_evidence", + "passed" + ], + "title": "Decision", + "type": "string" + }, + "evidence_coverage": { + "$ref": "#/$defs/EvidenceCoverageDecision" + }, + "fail_policy": { + "$ref": "#/$defs/FailPolicy" + }, + "reason": { + "title": "Reason", + "type": "string" + }, + "review_items": { + "items": { + "$ref": "#/$defs/ReleaseDecisionItem" + }, + "title": "Review Items", + "type": "array" + }, + "runtime_behavior_verified": { + "const": false, + "default": false, + "title": "Runtime Behavior Verified", + "type": "boolean" + }, + "static_analysis_only": { + "const": true, + "default": true, + "title": "Static Analysis Only", + "type": "boolean" + }, + "static_verdict_disclaimer": { + "default": "This verdict covers deterministic static evidence only. Agents Shipgate did not execute the agent or prove runtime behavior, tool routing, credential enforcement, or safety.", + "title": "Static Verdict Disclaimer", + "type": "string" + }, + "verdict": { + "enum": [ + "PASSED", + "REVIEW REQUIRED", + "INSUFFICIENT EVIDENCE", + "BLOCKED" + ], + "title": "Verdict", + "type": "string" + } + }, + "required": [ + "decision", + "verdict", + "reason", + "evidence_coverage", + "baseline_delta", + "fail_policy" + ], + "title": "ReleaseDecisionSection", + "type": "object" + }, + "ScopeCoverageRow": { + "additionalProperties": false, + "properties": { + "declared": { + "title": "Declared", + "type": "boolean" + }, + "scope": { + "title": "Scope", + "type": "string" + }, + "used_by_tool_ids": { + "items": { + "type": "string" + }, + "title": "Used By Tool Ids", + "type": "array" + }, + "used_by_tools": { + "items": { + "type": "string" + }, + "title": "Used By Tools", + "type": "array" + } + }, + "required": [ + "scope", + "declared" + ], + "title": "ScopeCoverageRow", + "type": "object" + }, + "ScopeCoverageSection": { + "additionalProperties": false, + "description": "\u00a76 \u2014 scope coverage.", + "properties": { + "declared_scopes": { + "items": { + "type": "string" + }, + "title": "Declared Scopes", + "type": "array" + }, + "gap_findings": { + "items": { + "$ref": "#/$defs/ReleaseDecisionItem" + }, + "title": "Gap Findings", + "type": "array" + }, + "missing_declared": { + "items": { + "type": "string" + }, + "title": "Missing Declared", + "type": "array" + }, + "rows": { + "items": { + "$ref": "#/$defs/ScopeCoverageRow" + }, + "title": "Rows", + "type": "array" + }, + "status": { + "enum": [ + "covered", + "partial", + "not_declared", + "missing", + "informational" + ], + "title": "Status", + "type": "string" + }, + "unused_declared": { + "items": { + "type": "string" + }, + "title": "Unused Declared", + "type": "array" + } + }, + "required": [ + "status" + ], + "title": "ScopeCoverageSection", + "type": "object" + }, + "SemanticCoverageDecision": { + "description": "v0.29 pass eligibility across the normalized action surface.\n\nUnlike extraction-confidence thresholds, semantic gaps are\nzero-tolerance: any non-pass-eligible unknown/partial/conflicting\ndimension prevents ``passed``. Known authority review concerns (for\nexample ambient or unscoped credentials) are counted separately so\nthey deterministically route to ``review_required`` rather than\n``insufficient_evidence``.", + "properties": { + "gap_count": { + "default": 0, + "title": "Gap Count", + "type": "integer" + }, + "pass_eligible_actions": { + "default": 0, + "title": "Pass Eligible Actions", + "type": "integer" + }, + "reason_counts": { + "additionalProperties": { + "type": "integer" + }, + "title": "Reason Counts", + "type": "object" + }, + "review_concern_count": { + "default": 0, + "title": "Review Concern Count", + "type": "integer" + }, + "total_actions": { + "default": 0, + "title": "Total Actions", + "type": "integer" + } + }, + "title": "SemanticCoverageDecision", + "type": "object" + }, + "SourceReference": { + "additionalProperties": true, + "properties": { + "end_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End Line" + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Location" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "pointer": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Pointer" + }, + "ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ref" + }, + "start_column": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start Column" + }, + "start_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start Line" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "SourceReference", + "type": "object" + }, + "ToolSurfaceDiffSection": { + "additionalProperties": false, + "description": "\u00a73A \u2014 compact tool-surface diff for reviewers.", + "properties": { + "base_kind": { + "default": "none", + "title": "Base Kind", + "type": "string" + }, + "enabled": { + "default": false, + "title": "Enabled", + "type": "boolean" + }, + "highlights": { + "items": { + "type": "string" + }, + "title": "Highlights", + "type": "array" + }, + "notes": { + "items": { + "type": "string" + }, + "title": "Notes", + "type": "array" + }, + "status": { + "enum": [ + "covered", + "partial", + "not_declared", + "missing", + "informational" + ], + "title": "Status", + "type": "string" + }, + "summary": { + "$ref": "#/$defs/ToolSurfaceDiffSummary" + } + }, + "required": [ + "status" + ], + "title": "ToolSurfaceDiffSection", + "type": "object" + }, + "ToolSurfaceDiffSummary": { + "additionalProperties": false, + "properties": { + "accepted_debt": { + "default": 0, + "title": "Accepted Debt", + "type": "integer" + }, + "controls_added": { + "default": 0, + "title": "Controls Added", + "type": "integer" + }, + "controls_removed": { + "default": 0, + "title": "Controls Removed", + "type": "integer" + }, + "metadata_changes": { + "default": 0, + "title": "Metadata Changes", + "type": "integer" + }, + "new_findings": { + "default": 0, + "title": "New Findings", + "type": "integer" + }, + "new_high_risk_effects": { + "default": 0, + "title": "New High Risk Effects", + "type": "integer" + }, + "new_scopes": { + "default": 0, + "title": "New Scopes", + "type": "integer" + }, + "policy_drift_items": { + "default": 0, + "title": "Policy Drift Items", + "type": "integer" + }, + "removed_high_risk_effects": { + "default": 0, + "title": "Removed High Risk Effects", + "type": "integer" + }, + "removed_scopes": { + "default": 0, + "title": "Removed Scopes", + "type": "integer" + }, + "resolved_findings": { + "default": 0, + "title": "Resolved Findings", + "type": "integer" + }, + "tools_added": { + "default": 0, + "title": "Tools Added", + "type": "integer" + }, + "tools_changed": { + "default": 0, + "title": "Tools Changed", + "type": "integer" + }, + "tools_removed": { + "default": 0, + "title": "Tools Removed", + "type": "integer" + }, + "unchanged_findings": { + "default": 0, + "title": "Unchanged Findings", + "type": "integer" + } + }, + "title": "ToolSurfaceDiffSummary", + "type": "object" + } + }, + "$id": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/packet-schema.v0.12.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "JSON Schema for packet.json. Generated from agents_shipgate.schemas.packet.EvidencePacket. Do not edit by hand.", + "properties": { + "action_surface_diff": { + "$ref": "#/$defs/ActionSurfaceDiffSection" + }, + "agent": { + "additionalProperties": true, + "title": "Agent", + "type": "object" + }, + "approval_coverage": { + "$ref": "#/$defs/ApprovalCoverageSection" + }, + "capability_intent": { + "$ref": "#/$defs/CapabilityIntentDiff" + }, + "decision_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Decision Id" + }, + "dynamic_scenarios": { + "$ref": "#/$defs/DynamicScenariosSection" + }, + "engine_requirement_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Engine Requirement Id" + }, + "environment": { + "additionalProperties": true, + "title": "Environment", + "type": "object" + }, + "evidence_matrix": { + "$ref": "#/$defs/EvidenceMatrixSection" + }, + "generated_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Generated At" + }, + "high_risk_surface": { + "$ref": "#/$defs/HighRiskSurfaceSection" + }, + "human_in_the_loop": { + "$ref": "#/$defs/HumanInTheLoopEvidence" + }, + "idempotency_risk": { + "$ref": "#/$defs/IdempotencyRiskSection" + }, + "input_set_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Input Set Id" + }, + "memory_isolation": { + "$ref": "#/$defs/MemoryIsolationStatus" + }, + "not_proven": { + "$ref": "#/$defs/NotProvenSection" + }, + "packet_schema_version": { + "const": "0.12", + "default": "0.12", + "title": "Packet Schema Version", + "type": "string" + }, + "project": { + "additionalProperties": true, + "title": "Project", + "type": "object" + }, + "release_decision": { + "$ref": "#/$defs/ReleaseDecisionSection" + }, + "request_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Request Id" + }, + "run_id": { + "title": "Run Id", + "type": "string" + }, + "scope_coverage": { + "$ref": "#/$defs/ScopeCoverageSection" + }, + "subject_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Subject Id" + }, + "tool_surface_diff": { + "$ref": "#/$defs/ToolSurfaceDiffSection" + } + }, + "required": [ + "run_id", + "release_decision", + "capability_intent", + "high_risk_surface", + "approval_coverage", + "idempotency_risk", + "scope_coverage", + "memory_isolation", + "human_in_the_loop", + "dynamic_scenarios", + "not_proven" + ], + "title": "Agents Shipgate Release Evidence Packet v0.12", + "type": "object" +} diff --git a/docs/passed-verdict-contract.md b/docs/passed-verdict-contract.md index 1b6f1030..9e14bf1c 100644 --- a/docs/passed-verdict-contract.md +++ b/docs/passed-verdict-contract.md @@ -1,7 +1,7 @@ # Evidence-backed `passed` verdict -Starting with the Agents Shipgate `0.16.0b5` runtime (contract v16, report -schema v0.33), `release_decision.decision: passed` means the configured root +In the Agents Shipgate `0.16.0b6` runtime (contract v17, report schema v0.34), +`release_decision.decision: passed` means the configured root agent and its complete reachable tool/handoff graph were statically proven, and every reachable capability has complete, conflict-free static identity, binding, effect, and authority evidence, all applicable controls were evaluated, and no @@ -39,7 +39,8 @@ to known evidence by `--no-heuristics` or `human_ack`. Machine consumers should inspect `release_decision.evidence_coverage.semantic_coverage`, `binding_coverage`, `identity_coverage`, and `policy_gap_count`, then work `evidence_gaps[]` in -order. Packet schema v0.11 mirrors this contract, while capability standard +order. Current packet schema v0.12 mirrors this contract and binds the +verification request and decision, while capability standard v0.5 carries the same normalized assessment and binding hash in capability lock v0.6 and lock-diff v0.7 artifacts. diff --git a/docs/registry-schema.v0.4.json b/docs/registry-schema.v0.4.json new file mode 100644 index 00000000..4fe9af84 --- /dev/null +++ b/docs/registry-schema.v0.4.json @@ -0,0 +1,618 @@ +{ + "$defs": { + "RegistryRowV1": { + "additionalProperties": true, + "properties": { + "actor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Actor" + }, + "artifact_set_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Artifact Set Id" + }, + "artifact_sha256": { + "additionalProperties": { + "type": "string" + }, + "title": "Artifact Sha256", + "type": "object" + }, + "attestation_schema_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Attestation Schema Version" + }, + "base_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Base Ref" + }, + "base_sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Base Sha" + }, + "base_tree_sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Base Tree Sha" + }, + "branch": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Branch" + }, + "can_merge_without_human": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Can Merge Without Human" + }, + "capability_added": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Capability Added" + }, + "capability_change_ids": { + "items": { + "type": "string" + }, + "title": "Capability Change Ids", + "type": "array" + }, + "capability_diff": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Capability Diff" + }, + "capability_lock": { + "additionalProperties": true, + "title": "Capability Lock", + "type": "object" + }, + "capability_modified": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Capability Modified" + }, + "capability_removed": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Capability Removed" + }, + "cli_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cli Version" + }, + "decision": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Decision" + }, + "decision_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Decision Id" + }, + "event_time": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Event Time" + }, + "head_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Head Ref" + }, + "head_sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Head Sha" + }, + "head_tree_sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Head Tree Sha" + }, + "human_ack": { + "additionalProperties": true, + "title": "Human Ack", + "type": "object" + }, + "human_ack_outstanding": { + "items": { + "type": "string" + }, + "title": "Human Ack Outstanding", + "type": "array" + }, + "human_ack_required": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Human Ack Required" + }, + "human_ack_satisfied": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Human Ack Satisfied" + }, + "merge_sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Merge Sha" + }, + "merge_verdict": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Merge Verdict" + }, + "org_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Org Id" + }, + "policy_packs": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "title": "Policy Packs", + "type": "array" + }, + "policy_snapshot_sha256": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Policy Snapshot Sha256" + }, + "policy_weakened": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Policy Weakened" + }, + "pr_number": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Pr Number" + }, + "previous_row_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Previous Row Id" + }, + "previous_row_sha256": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Previous Row Sha256" + }, + "receipt_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Receipt Id" + }, + "registry_schema_version": { + "default": "0.4", + "title": "Registry Schema Version", + "type": "string" + }, + "repo": { + "default": "", + "title": "Repo", + "type": "string" + }, + "request_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Request Id" + }, + "row_id": { + "title": "Row Id", + "type": "string" + }, + "run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Run Id" + }, + "service": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Service" + }, + "source_attestation_sha256": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Attestation Sha256" + }, + "source_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Url" + }, + "source_verification_receipt_sha256": { + "anyOf": [ + { + "pattern": "^[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Verification Receipt Sha256" + }, + "source_verify_run_sha256": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Verify Run Sha256" + }, + "tier": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tier" + }, + "trust_root_touched": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Trust Root Touched" + }, + "workflow_run_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Workflow Run Id" + } + }, + "required": [ + "row_id" + ], + "title": "RegistryRowV1", + "type": "object" + }, + "RegistrySkippedRowV1": { + "additionalProperties": false, + "properties": { + "line": { + "minimum": 1, + "title": "Line", + "type": "integer" + }, + "reason": { + "title": "Reason", + "type": "string" + } + }, + "required": [ + "line", + "reason" + ], + "title": "RegistrySkippedRowV1", + "type": "object" + } + }, + "$id": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/registry-schema.v0.4.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "JSON Schema for agents-shipgate registry query --json. Rows are local, append-only projections of deterministic attestations. The registry does not produce release verdicts.", + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "registry": { + "title": "Registry", + "type": "string" + }, + "registry_schema_version": { + "const": "0.4", + "default": "0.4", + "title": "Registry Schema Version", + "type": "string" + }, + "rows": { + "items": { + "$ref": "#/$defs/RegistryRowV1" + }, + "title": "Rows", + "type": "array" + }, + "skipped_count": { + "default": 0, + "title": "Skipped Count", + "type": "integer" + }, + "skipped_rows": { + "items": { + "$ref": "#/$defs/RegistrySkippedRowV1" + }, + "title": "Skipped Rows", + "type": "array" + } + }, + "required": [ + "registry", + "count" + ], + "title": "Agents Shipgate Local Attestation Registry v0.4", + "type": "object" +} diff --git a/docs/report-reading-for-agents.md b/docs/report-reading-for-agents.md index 76040765..a955ac06 100644 --- a/docs/report-reading-for-agents.md +++ b/docs/report-reading-for-agents.md @@ -227,10 +227,10 @@ Surface the `next_action` to the user rather than scraping prose. The full diagn | Schema | Current | Frozen references | File | |---|---|---|---| -| Report | `0.33` | `0.32`, `0.31`, `0.30`, `0.29`, `0.28`, `0.27`, `0.26`, `0.25`, `0.24`, `0.23`, `0.22`, `0.21`, `0.20`, `0.19`, `0.18`, `0.17`, `0.16`, `0.15`, `0.14`, `0.13`, `0.12`, `0.11`, `0.10`, `0.9`, `0.8`, `0.7`, `0.6`, `0.5`, `0.4`, `0.3`, `0.2`, `0.1` | [`report-schema.v0.33.json`](report-schema.v0.33.json) | -| Packet | `0.10` | `0.9`, `0.8`, `0.7`, `0.6`, `0.5`, `0.4`, `0.3`, `0.2`, `0.1` | [`packet-schema.v0.10.json`](packet-schema.v0.10.json) | +| Report | `0.34` | `0.33`, `0.32`, `0.31`, `0.30`, `0.29`, `0.28`, `0.27`, `0.26`, `0.25`, `0.24`, `0.23`, `0.22`, `0.21`, `0.20`, `0.19`, `0.18`, `0.17`, `0.16`, `0.15`, `0.14`, `0.13`, `0.12`, `0.11`, `0.10`, `0.9`, `0.8`, `0.7`, `0.6`, `0.5`, `0.4`, `0.3`, `0.2`, `0.1` | [`report-schema.v0.34.json`](report-schema.v0.34.json) | +| Packet | `0.12` | `0.11`, `0.10`, `0.9`, `0.8`, `0.7`, `0.6`, `0.5`, `0.4`, `0.3`, `0.2`, `0.1` | [`packet-schema.v0.12.json`](packet-schema.v0.12.json) | | Manifest | `0.1` | — | [`manifest-v0.1.json`](manifest-v0.1.json) | -| CLI contract | `5` | — | `agents-shipgate contract --json` | +| CLI contract | `17` | — | `agents-shipgate contract --json` | To detect the version programmatically: diff --git a/docs/report-schema.v0.34.json b/docs/report-schema.v0.34.json new file mode 100644 index 00000000..3a22cc85 --- /dev/null +++ b/docs/report-schema.v0.34.json @@ -0,0 +1,7417 @@ +{ + "$defs": { + "ActionApprovalFact": { + "additionalProperties": false, + "properties": { + "required": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Required" + }, + "threshold": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Threshold" + } + }, + "required": [ + "required", + "threshold" + ], + "title": "ActionApprovalFact", + "type": "object" + }, + "ActionEvidenceFact": { + "additionalProperties": false, + "properties": { + "approval_ticket": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Approval Ticket" + }, + "owner": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Owner" + }, + "runbook": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Runbook" + } + }, + "required": [ + "approval_ticket", + "owner", + "runbook" + ], + "title": "ActionEvidenceFact", + "type": "object" + }, + "ActionFact": { + "additionalProperties": false, + "properties": { + "action_id": { + "title": "Action Id", + "type": "string" + }, + "agent_id": { + "title": "Agent Id", + "type": "string" + }, + "approval_policy": { + "$ref": "#/$defs/ActionApprovalFact" + }, + "effect": { + "enum": [ + "read", + "write", + "destructive", + "external_communication", + "financial_write", + "production_operation", + "privileged_data_access", + "code_execution", + "identity_access" + ], + "title": "Effect", + "type": "string" + }, + "evidence": { + "$ref": "#/$defs/ActionEvidenceFact" + }, + "hashes": { + "$ref": "#/$defs/ActionSurfaceHashes" + }, + "input_fields": { + "items": { + "type": "string" + }, + "title": "Input Fields", + "type": "array" + }, + "input_schema_hash": { + "title": "Input Schema Hash", + "type": "string" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "required_input_fields": { + "items": { + "type": "string" + }, + "title": "Required Input Fields", + "type": "array" + }, + "required_scopes": { + "items": { + "type": "string" + }, + "title": "Required Scopes", + "type": "array" + }, + "risk_tags": { + "items": { + "type": "string" + }, + "title": "Risk Tags", + "type": "array" + }, + "safeguards": { + "$ref": "#/$defs/ActionSafeguardsFact" + }, + "semantic_assessment": { + "anyOf": [ + { + "$ref": "#/$defs/ToolSemanticEvidence" + }, + { + "type": "null" + } + ], + "default": null + }, + "source_end_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source End Line" + }, + "source_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Id" + }, + "source_location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Location" + }, + "source_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Path" + }, + "source_pointer": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Pointer" + }, + "source_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Ref" + }, + "source_start_column": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Start Column" + }, + "source_start_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Start Line" + }, + "source_type": { + "title": "Source Type", + "type": "string" + }, + "tool_id": { + "title": "Tool Id", + "type": "string" + }, + "tool_name": { + "title": "Tool Name", + "type": "string" + } + }, + "required": [ + "action_id", + "agent_id", + "approval_policy", + "effect", + "evidence", + "hashes", + "input_fields", + "input_schema_hash", + "operation", + "provider", + "required_input_fields", + "required_scopes", + "risk_tags", + "safeguards", + "source_id", + "source_type", + "tool_id", + "tool_name" + ], + "title": "ActionFact", + "type": "object" + }, + "ActionSafeguardsFact": { + "additionalProperties": false, + "properties": { + "audit_log": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Audit Log" + }, + "dry_run": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dry Run" + }, + "idempotency": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Idempotency" + }, + "rollback": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Rollback" + } + }, + "required": [ + "audit_log", + "dry_run", + "idempotency", + "rollback" + ], + "title": "ActionSafeguardsFact", + "type": "object" + }, + "ActionSurfaceChange": { + "additionalProperties": false, + "properties": { + "action_id": { + "title": "Action Id", + "type": "string" + }, + "added": { + "items": { + "type": "string" + }, + "title": "Added", + "type": "array" + }, + "after": { + "default": null, + "title": "After" + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Agent Id" + }, + "before": { + "default": null, + "title": "Before" + }, + "operation": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Operation" + }, + "reason": { + "title": "Reason", + "type": "string" + }, + "removed": { + "items": { + "type": "string" + }, + "title": "Removed", + "type": "array" + }, + "severity": { + "default": "info", + "enum": [ + "info", + "low", + "medium", + "high", + "critical" + ], + "title": "Severity", + "type": "string" + }, + "source_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Path" + }, + "source_start_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Start Line" + }, + "tool_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Id" + }, + "tool_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Name" + }, + "type": { + "enum": [ + "ACTION_ADDED", + "ACTION_REMOVED", + "ACTION_MODIFIED", + "SCOPE_EXPANDED", + "EFFECT_ESCALATED", + "RISK_TAG_ADDED", + "APPROVAL_REMOVED", + "SAFEGUARD_REMOVED", + "INPUT_SCHEMA_EXPANDED" + ], + "title": "Type", + "type": "string" + } + }, + "required": [ + "action_id", + "added", + "after", + "agent_id", + "before", + "operation", + "reason", + "removed", + "severity", + "tool_name", + "type" + ], + "title": "ActionSurfaceChange", + "type": "object" + }, + "ActionSurfaceDiff": { + "additionalProperties": false, + "properties": { + "added": { + "items": { + "$ref": "#/$defs/ActionSurfaceChange" + }, + "title": "Added", + "type": "array" + }, + "base": { + "$ref": "#/$defs/ToolSurfaceDiffBase" + }, + "enabled": { + "default": false, + "title": "Enabled", + "type": "boolean" + }, + "modified": { + "items": { + "$ref": "#/$defs/ActionSurfaceChange" + }, + "title": "Modified", + "type": "array" + }, + "notes": { + "items": { + "type": "string" + }, + "title": "Notes", + "type": "array" + }, + "removed": { + "items": { + "$ref": "#/$defs/ActionSurfaceChange" + }, + "title": "Removed", + "type": "array" + }, + "summary": { + "$ref": "#/$defs/ActionSurfaceDiffSummary" + } + }, + "required": [ + "added", + "base", + "enabled", + "modified", + "notes", + "removed", + "summary" + ], + "title": "ActionSurfaceDiff", + "type": "object" + }, + "ActionSurfaceDiffSummary": { + "additionalProperties": false, + "properties": { + "actions_added": { + "default": 0, + "title": "Actions Added", + "type": "integer" + }, + "actions_modified": { + "default": 0, + "title": "Actions Modified", + "type": "integer" + }, + "actions_removed": { + "default": 0, + "title": "Actions Removed", + "type": "integer" + }, + "approvals_removed": { + "default": 0, + "title": "Approvals Removed", + "type": "integer" + }, + "blocking_findings": { + "default": 0, + "title": "Blocking Findings", + "type": "integer" + }, + "effect_escalations": { + "default": 0, + "title": "Effect Escalations", + "type": "integer" + }, + "input_schema_expansions": { + "default": 0, + "title": "Input Schema Expansions", + "type": "integer" + }, + "risk_tags_added": { + "default": 0, + "title": "Risk Tags Added", + "type": "integer" + }, + "safeguards_removed": { + "default": 0, + "title": "Safeguards Removed", + "type": "integer" + }, + "scope_expansions": { + "default": 0, + "title": "Scope Expansions", + "type": "integer" + } + }, + "required": [ + "actions_added", + "actions_modified", + "actions_removed", + "approvals_removed", + "blocking_findings", + "effect_escalations", + "input_schema_expansions", + "risk_tags_added", + "safeguards_removed", + "scope_expansions" + ], + "title": "ActionSurfaceDiffSummary", + "type": "object" + }, + "ActionSurfaceFacts": { + "additionalProperties": false, + "properties": { + "actions": { + "items": { + "$ref": "#/$defs/ActionFact" + }, + "title": "Actions", + "type": "array" + }, + "snapshot_version": { + "default": "0.4", + "title": "Snapshot Version", + "type": "string" + } + }, + "required": [ + "actions", + "snapshot_version" + ], + "title": "ActionSurfaceFacts", + "type": "object" + }, + "ActionSurfaceHashes": { + "additionalProperties": false, + "properties": { + "identity_hash": { + "title": "Identity Hash", + "type": "string" + }, + "policy_hash": { + "title": "Policy Hash", + "type": "string" + }, + "risk_hash": { + "title": "Risk Hash", + "type": "string" + }, + "schema_hash": { + "title": "Schema Hash", + "type": "string" + } + }, + "required": [ + "identity_hash", + "policy_hash", + "risk_hash", + "schema_hash" + ], + "title": "ActionSurfaceHashes", + "type": "object" + }, + "AgentBindingGraphAssessment": { + "additionalProperties": false, + "properties": { + "agents": { + "items": { + "$ref": "#/$defs/AgentBindingNode" + }, + "title": "Agents", + "type": "array" + }, + "handoff_edges": { + "items": { + "$ref": "#/$defs/AgentHandoffBindingEdge" + }, + "title": "Handoff Edges", + "type": "array" + }, + "issues": { + "items": { + "$ref": "#/$defs/AgentBindingIssue" + }, + "title": "Issues", + "type": "array" + }, + "pass_eligible": { + "default": false, + "title": "Pass Eligible", + "type": "boolean" + }, + "possible_tool_ids": { + "items": { + "type": "string" + }, + "title": "Possible Tool Ids", + "type": "array" + }, + "reachable_tool_ids": { + "items": { + "type": "string" + }, + "title": "Reachable Tool Ids", + "type": "array" + }, + "root_agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Root Agent Id" + }, + "status": { + "enum": [ + "declared", + "structural", + "partial", + "unknown", + "conflicting" + ], + "title": "Status", + "type": "string" + }, + "tool_edges": { + "items": { + "$ref": "#/$defs/AgentToolBindingEdge" + }, + "title": "Tool Edges", + "type": "array" + }, + "unbound_tool_ids": { + "items": { + "type": "string" + }, + "title": "Unbound Tool Ids", + "type": "array" + } + }, + "required": [ + "status" + ], + "title": "AgentBindingGraphAssessment", + "type": "object" + }, + "AgentBindingIssue": { + "additionalProperties": false, + "properties": { + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Agent Id" + }, + "kind": { + "enum": [ + "missing_binding_evidence", + "partial_binding_evidence", + "conflicting_binding_evidence", + "ambiguous_root_agent", + "unresolved_agent_binding", + "unresolved_bound_tool", + "incomplete_handoff_graph", + "invalid_binding_annotation" + ], + "title": "Kind", + "type": "string" + }, + "message": { + "title": "Message", + "type": "string" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + }, + "source_pointer": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Pointer" + }, + "tool_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Id" + } + }, + "required": [ + "kind", + "message" + ], + "title": "AgentBindingIssue", + "type": "object" + }, + "AgentBindingNode": { + "additionalProperties": false, + "properties": { + "agent_id": { + "title": "Agent Id", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "source_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Id" + }, + "source_pointer": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Pointer" + }, + "source_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Ref" + } + }, + "required": [ + "agent_id", + "name" + ], + "title": "AgentBindingNode", + "type": "object" + }, + "AgentHandoffBindingEdge": { + "additionalProperties": false, + "properties": { + "complete": { + "default": true, + "title": "Complete", + "type": "boolean" + }, + "confidence": { + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "edge_type": { + "enum": [ + "subagent", + "handoff" + ], + "title": "Edge Type", + "type": "string" + }, + "provenance_kind": { + "enum": [ + "static_declaration", + "ast_extraction", + "keyword_heuristic", + "regex_heuristic", + "policy_pack", + "runtime_trace" + ], + "title": "Provenance Kind", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "source_agent_id": { + "title": "Source Agent Id", + "type": "string" + }, + "source_pointer": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Pointer" + }, + "target_agent_id": { + "title": "Target Agent Id", + "type": "string" + } + }, + "required": [ + "source_agent_id", + "target_agent_id", + "edge_type", + "confidence", + "provenance_kind", + "source" + ], + "title": "AgentHandoffBindingEdge", + "type": "object" + }, + "AgentSummary": { + "additionalProperties": false, + "description": "Top-level summary block shaped for one-fetch agent consumption.\n\nDeterministic projection of (``release_decision``, ``findings[].agent_action``).\nA coding agent that wants the headline numbers can read this block\ninstead of traversing arrays. All fields are derived; this block\ncannot disagree with the underlying data.", + "properties": { + "auto_appliable_patches": { + "default": 0, + "title": "Auto Appliable Patches", + "type": "integer" + }, + "blocker_count": { + "default": 0, + "title": "Blocker Count", + "type": "integer" + }, + "first_recommended_action": { + "anyOf": [ + { + "$ref": "#/$defs/AgentSummaryAction" + }, + { + "type": "null" + } + ], + "default": null + }, + "headline": { + "title": "Headline", + "type": "string" + }, + "needs_human_review": { + "default": 0, + "title": "Needs Human Review", + "type": "integer" + }, + "review_item_count": { + "default": 0, + "title": "Review Item Count", + "type": "integer" + }, + "verdict": { + "enum": [ + "blocked", + "review_required", + "insufficient_evidence", + "passed" + ], + "title": "Verdict", + "type": "string" + } + }, + "required": [ + "auto_appliable_patches", + "blocker_count", + "first_recommended_action", + "headline", + "needs_human_review", + "review_item_count", + "verdict" + ], + "title": "AgentSummary", + "type": "object" + }, + "AgentSummaryAction": { + "additionalProperties": false, + "description": "A single recommended next step shaped for direct agent consumption.\n\nMirrors the ``next_actions[]`` shape used elsewhere in the contract\n(kind/command/why) so callers that already handle diagnostic\nnext_actions can reuse the same renderer here.", + "properties": { + "command": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Command" + }, + "kind": { + "default": "command", + "enum": [ + "command", + "info" + ], + "title": "Kind", + "type": "string" + }, + "why": { + "title": "Why", + "type": "string" + } + }, + "required": [ + "command", + "kind", + "why" + ], + "title": "AgentSummaryAction", + "type": "object" + }, + "AgentToolBindingEdge": { + "additionalProperties": false, + "properties": { + "agent_id": { + "title": "Agent Id", + "type": "string" + }, + "complete": { + "default": true, + "title": "Complete", + "type": "boolean" + }, + "confidence": { + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "edge_type": { + "enum": [ + "direct_tool", + "tool_node", + "toolset", + "workflow", + "subagent", + "handoff" + ], + "title": "Edge Type", + "type": "string" + }, + "provenance_kind": { + "enum": [ + "static_declaration", + "ast_extraction", + "keyword_heuristic", + "regex_heuristic", + "policy_pack", + "runtime_trace" + ], + "title": "Provenance Kind", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "source_pointer": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Pointer" + }, + "tool_id": { + "title": "Tool Id", + "type": "string" + } + }, + "required": [ + "agent_id", + "tool_id", + "edge_type", + "confidence", + "provenance_kind", + "source" + ], + "title": "AgentToolBindingEdge", + "type": "object" + }, + "AppendPointerPatch": { + "additionalProperties": false, + "description": "Append a value to the list at a JSON pointer.", + "properties": { + "confidence": { + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "kind": { + "const": "append_pointer", + "default": "append_pointer", + "title": "Kind", + "type": "string" + }, + "pointer": { + "title": "Pointer", + "type": "string" + }, + "rationale": { + "title": "Rationale", + "type": "string" + }, + "target_file": { + "title": "Target File", + "type": "string" + }, + "target_format": { + "enum": [ + "yaml", + "json" + ], + "title": "Target Format", + "type": "string" + }, + "target_sha256": { + "title": "Target Sha256", + "type": "string" + }, + "value": { + "title": "Value" + } + }, + "required": [ + "target_file", + "pointer", + "value", + "target_format", + "confidence", + "rationale", + "target_sha256" + ], + "title": "AppendPointerPatch", + "type": "object" + }, + "AuthoritySemanticEvidence": { + "additionalProperties": false, + "properties": { + "auth_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Auth Type" + }, + "claims": { + "items": { + "$ref": "#/$defs/SemanticClaimEvidence" + }, + "title": "Claims", + "type": "array" + }, + "credential_mode": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Credential Mode" + }, + "issues": { + "items": { + "$ref": "#/$defs/SemanticIssueEvidence" + }, + "title": "Issues", + "type": "array" + }, + "mode": { + "enum": [ + "none", + "scoped", + "unscoped", + "ambient", + "unknown" + ], + "title": "Mode", + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "title": "Scopes", + "type": "array" + }, + "status": { + "enum": [ + "declared", + "structural", + "partial", + "unknown", + "conflicting" + ], + "title": "Status", + "type": "string" + } + }, + "required": [ + "status", + "mode" + ], + "title": "AuthoritySemanticEvidence", + "type": "object" + }, + "BaselineDelta": { + "properties": { + "enabled": { + "title": "Enabled", + "type": "boolean" + }, + "matched_count": { + "default": 0, + "title": "Matched Count", + "type": "integer" + }, + "new_count": { + "default": 0, + "title": "New Count", + "type": "integer" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "resolved_count": { + "default": 0, + "title": "Resolved Count", + "type": "integer" + } + }, + "required": [ + "enabled", + "matched_count", + "new_count", + "resolved_count" + ], + "title": "BaselineDelta", + "type": "object" + }, + "BaselineSummary": { + "properties": { + "matched_count": { + "default": 0, + "title": "Matched Count", + "type": "integer" + }, + "new_count": { + "default": 0, + "title": "New Count", + "type": "integer" + }, + "path": { + "title": "Path", + "type": "string" + }, + "resolved_count": { + "default": 0, + "title": "Resolved Count", + "type": "integer" + } + }, + "required": [ + "path" + ], + "title": "BaselineSummary", + "type": "object" + }, + "BindingCoverageDecision": { + "properties": { + "gap_count": { + "default": 0, + "title": "Gap Count", + "type": "integer" + }, + "pass_eligible": { + "default": false, + "title": "Pass Eligible", + "type": "boolean" + }, + "possible_tools": { + "default": 0, + "title": "Possible Tools", + "type": "integer" + }, + "reachable_tools": { + "default": 0, + "title": "Reachable Tools", + "type": "integer" + }, + "reason_counts": { + "additionalProperties": { + "type": "integer" + }, + "title": "Reason Counts", + "type": "object" + }, + "total_catalog_tools": { + "default": 0, + "title": "Total Catalog Tools", + "type": "integer" + }, + "unbound_tools": { + "default": 0, + "title": "Unbound Tools", + "type": "integer" + } + }, + "title": "BindingCoverageDecision", + "type": "object" + }, + "BindingSemanticEvidence": { + "additionalProperties": false, + "properties": { + "claims": { + "items": { + "$ref": "#/$defs/SemanticClaimEvidence" + }, + "title": "Claims", + "type": "array" + }, + "confidence": { + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "issues": { + "items": { + "$ref": "#/$defs/SemanticIssueEvidence" + }, + "title": "Issues", + "type": "array" + }, + "pass_eligible": { + "default": false, + "title": "Pass Eligible", + "type": "boolean" + }, + "reachable_path": { + "items": { + "type": "string" + }, + "title": "Reachable Path", + "type": "array" + }, + "root_agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Root Agent Id" + }, + "status": { + "enum": [ + "declared", + "structural", + "partial", + "unknown", + "conflicting" + ], + "title": "Status", + "type": "string" + } + }, + "required": [ + "status", + "confidence" + ], + "title": "BindingSemanticEvidence", + "type": "object" + }, + "BindingSurfaceDiff": { + "additionalProperties": false, + "properties": { + "added_handoffs": { + "items": { + "type": "string" + }, + "title": "Added Handoffs", + "type": "array" + }, + "added_reachable_tool_ids": { + "items": { + "type": "string" + }, + "title": "Added Reachable Tool Ids", + "type": "array" + }, + "base_report_schema_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Base Report Schema Version" + }, + "enabled": { + "default": false, + "title": "Enabled", + "type": "boolean" + }, + "notes": { + "items": { + "type": "string" + }, + "title": "Notes", + "type": "array" + }, + "removed_handoffs": { + "items": { + "type": "string" + }, + "title": "Removed Handoffs", + "type": "array" + }, + "removed_reachable_tool_ids": { + "items": { + "type": "string" + }, + "title": "Removed Reachable Tool Ids", + "type": "array" + } + }, + "title": "BindingSurfaceDiff", + "type": "object" + }, + "CapabilityChangeBlock": { + "additionalProperties": false, + "description": "The diff-derived capability delta, grouped by direction.\n\nReviewer-facing projection over ``action_surface_diff`` /\n``tool_surface_diff`` (roadmap \u00a77.1). Four member lists \u2014\n``added`` / ``removed`` / ``broadened`` / ``narrowed`` \u2014 plus\n``enabled`` (mirrors the surface-diff enabled flag: ``False`` when no\nbase is available, so the block is a stable empty shape rather than\nabsent). Never gates on its own.", + "properties": { + "added": { + "items": { + "$ref": "#/$defs/CapabilityChangeMember" + }, + "title": "Added", + "type": "array" + }, + "broadened": { + "items": { + "$ref": "#/$defs/CapabilityChangeMember" + }, + "title": "Broadened", + "type": "array" + }, + "enabled": { + "default": false, + "title": "Enabled", + "type": "boolean" + }, + "narrowed": { + "items": { + "$ref": "#/$defs/CapabilityChangeMember" + }, + "title": "Narrowed", + "type": "array" + }, + "removed": { + "items": { + "$ref": "#/$defs/CapabilityChangeMember" + }, + "title": "Removed", + "type": "array" + } + }, + "required": [ + "added", + "broadened", + "enabled", + "narrowed", + "removed" + ], + "title": "CapabilityChangeBlock", + "type": "object" + }, + "CapabilityChangeMember": { + "additionalProperties": false, + "description": "One capability member in a base\u2192head delta.\n\nCarries enough to identify the capability \u2014 ``tool`` plus the\n``action`` and ``scope`` it concerns \u2014 and, for ``broadened`` /\n``narrowed`` directions, the ``before_scope`` / ``after_scope`` so a\nreviewer can see exactly how the scope moved. Membership changes\n(``added`` / ``removed``) leave the before/after scope fields ``None``.\n\nA deterministic ``id`` supplied by the capability-change builder keeps\nthe member stable across runs for byte-equivalent output.", + "properties": { + "action": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Action" + }, + "after_capability_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "After Capability Id" + }, + "after_scope": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "After Scope" + }, + "before_capability_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Before Capability Id" + }, + "before_scope": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Before Scope" + }, + "changed_hashes": { + "items": { + "enum": [ + "identity_hash", + "binding_hash", + "effect_hash", + "authority_hash", + "control_hash", + "schema_hash", + "risk_hash", + "evidence_hash" + ], + "type": "string" + }, + "title": "Changed Hashes", + "type": "array" + }, + "confidence": { + "default": "medium", + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "direction": { + "enum": [ + "added", + "removed", + "broadened", + "narrowed" + ], + "title": "Direction", + "type": "string" + }, + "id": { + "title": "Id", + "type": "string" + }, + "provenance_kind": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Provenance Kind" + }, + "rationale": { + "default": "", + "title": "Rationale", + "type": "string" + }, + "related_finding_ids": { + "items": { + "type": "string" + }, + "title": "Related Finding Ids", + "type": "array" + }, + "release_impact": { + "default": "informational", + "enum": [ + "none", + "informational", + "review_required", + "blocks_release", + "insufficient_evidence" + ], + "title": "Release Impact", + "type": "string" + }, + "risk_tags": { + "items": { + "type": "string" + }, + "title": "Risk Tags", + "type": "array" + }, + "scope": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Scope" + }, + "semantic_changes": { + "items": { + "$ref": "#/$defs/CapabilitySemanticChange" + }, + "title": "Semantic Changes", + "type": "array" + }, + "semantic_direction": { + "default": "unknown", + "enum": [ + "added", + "removed", + "broadened", + "narrowed", + "mixed", + "unknown", + "evidence_only" + ], + "title": "Semantic Direction", + "type": "string" + }, + "subject_kind": { + "default": "unknown", + "enum": [ + "tool", + "action", + "scope", + "policy", + "ci", + "baseline", + "agent_instruction", + "manifest", + "unknown" + ], + "title": "Subject Kind", + "type": "string" + }, + "tool": { + "title": "Tool", + "type": "string" + } + }, + "required": [ + "action", + "after_scope", + "before_scope", + "confidence", + "direction", + "id", + "provenance_kind", + "rationale", + "related_finding_ids", + "release_impact", + "risk_tags", + "scope", + "subject_kind", + "tool" + ], + "title": "CapabilityChangeMember", + "type": "object" + }, + "CapabilityFact": { + "properties": { + "auth_scopes": { + "items": { + "type": "string" + }, + "title": "Auth Scopes", + "type": "array" + }, + "capability": { + "title": "Capability", + "type": "string" + }, + "control_status": { + "enum": [ + "missing", + "partial", + "present", + "unknown" + ], + "title": "Control Status", + "type": "string" + }, + "effect": { + "anyOf": [ + { + "enum": [ + "read", + "write", + "destructive", + "external_communication", + "financial_write", + "production_operation", + "privileged_data_access", + "code_execution", + "identity_access" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Effect" + }, + "id": { + "title": "Id", + "type": "string" + }, + "included_reason": { + "enum": [ + "high_risk_tag", + "wildcard_exposure", + "referenced_by_critical_finding", + "referenced_by_high_finding", + "referenced_by_medium_finding" + ], + "title": "Included Reason", + "type": "string" + }, + "owner": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Owner" + }, + "related_findings": { + "items": { + "type": "string" + }, + "title": "Related Findings", + "type": "array" + }, + "risk_tags": { + "items": { + "type": "string" + }, + "title": "Risk Tags", + "type": "array" + }, + "semantic_assessment": { + "anyOf": [ + { + "$ref": "#/$defs/ToolSemanticEvidence" + }, + { + "type": "null" + } + ], + "default": null + }, + "source_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Id" + }, + "source_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Ref" + }, + "source_type": { + "title": "Source Type", + "type": "string" + }, + "tool_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Id" + }, + "tool_name": { + "title": "Tool Name", + "type": "string" + } + }, + "required": [ + "auth_scopes", + "capability", + "control_status", + "id", + "included_reason", + "owner", + "related_findings", + "risk_tags", + "source_ref", + "source_type", + "tool_name" + ], + "title": "CapabilityFact", + "type": "object" + }, + "CapabilityPolicyEvidence": { + "additionalProperties": false, + "description": "Capability-level audit evidence for a policy match.\n\nThis is explanatory metadata only. It lets reviewers see which durable\ncapability fact matched a policy rule without folding that metadata into\nthe legacy ``Finding.evidence`` fingerprint input.", + "properties": { + "authority": { + "additionalProperties": true, + "title": "Authority", + "type": "object" + }, + "capability_id": { + "title": "Capability Id", + "type": "string" + }, + "controls": { + "additionalProperties": true, + "title": "Controls", + "type": "object" + }, + "effect": { + "additionalProperties": true, + "title": "Effect", + "type": "object" + }, + "hashes": { + "additionalProperties": { + "type": "string" + }, + "title": "Hashes", + "type": "object" + }, + "identity": { + "additionalProperties": true, + "title": "Identity", + "type": "object" + }, + "matched_predicates": { + "additionalProperties": true, + "title": "Matched Predicates", + "type": "object" + }, + "source": { + "anyOf": [ + { + "$ref": "#/$defs/SourceReference" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "capability_id", + "identity", + "effect", + "authority", + "controls", + "hashes" + ], + "title": "CapabilityPolicyEvidence", + "type": "object" + }, + "CapabilityRuntimeEvidence": { + "additionalProperties": false, + "description": "Top-level audit block for declared local runtime trace artifacts.", + "properties": { + "enabled": { + "default": false, + "title": "Enabled", + "type": "boolean" + }, + "matched": { + "items": { + "$ref": "#/$defs/CapabilityTraceEvidenceV1" + }, + "title": "Matched", + "type": "array" + }, + "notes": { + "items": { + "type": "string" + }, + "title": "Notes", + "type": "array" + }, + "source_provenance": { + "items": { + "$ref": "#/$defs/SourceReference" + }, + "title": "Source Provenance", + "type": "array" + }, + "summary": { + "$ref": "#/$defs/CapabilityTraceEvidenceSummary" + }, + "unmatched": { + "items": { + "$ref": "#/$defs/CapabilityTraceEvidenceV1" + }, + "title": "Unmatched", + "type": "array" + } + }, + "title": "CapabilityRuntimeEvidence", + "type": "object" + }, + "CapabilitySemanticChange": { + "additionalProperties": false, + "description": "One semantic explanation for a capability delta.", + "properties": { + "after": { + "default": null, + "title": "After" + }, + "before": { + "default": null, + "title": "Before" + }, + "field": { + "title": "Field", + "type": "string" + }, + "kind": { + "title": "Kind", + "type": "string" + }, + "rationale": { + "title": "Rationale", + "type": "string" + } + }, + "required": [ + "kind", + "field", + "rationale" + ], + "title": "CapabilitySemanticChange", + "type": "object" + }, + "CapabilityTraceEvidenceSummary": { + "additionalProperties": false, + "description": "Deterministic counts for opt-in local runtime trace evidence.", + "properties": { + "agent_trace_count": { + "default": 0, + "title": "Agent Trace Count", + "type": "integer" + }, + "api_trace_count": { + "default": 0, + "title": "Api Trace Count", + "type": "integer" + }, + "approval_trace_count": { + "default": 0, + "title": "Approval Trace Count", + "type": "integer" + }, + "matched_trace_count": { + "default": 0, + "title": "Matched Trace Count", + "type": "integer" + }, + "source_count": { + "default": 0, + "title": "Source Count", + "type": "integer" + }, + "trace_count": { + "default": 0, + "title": "Trace Count", + "type": "integer" + }, + "unmatched_trace_count": { + "default": 0, + "title": "Unmatched Trace Count", + "type": "integer" + }, + "warning_count": { + "default": 0, + "title": "Warning Count", + "type": "integer" + } + }, + "title": "CapabilityTraceEvidenceSummary", + "type": "object" + }, + "CapabilityTraceEvidenceV1": { + "additionalProperties": false, + "description": "Allowlisted local trace event linked to a durable capability fact.\n\nRaw prompts, messages, tool arguments, outputs, and arbitrary payloads\nare intentionally absent. ``observed`` contains only normalized scalar\nevidence from the allowlist enforced by ``inputs.traces``.", + "properties": { + "capability_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Capability Id" + }, + "event_hash": { + "title": "Event Hash", + "type": "string" + }, + "id": { + "title": "Id", + "type": "string" + }, + "match_reason": { + "enum": [ + "capability_id", + "tool_name", + "unknown_tool", + "ambiguous_tool", + "invalid_capability_id", + "missing_tool_name" + ], + "title": "Match Reason", + "type": "string" + }, + "matched": { + "default": false, + "title": "Matched", + "type": "boolean" + }, + "matched_capability_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Matched Capability Id" + }, + "observed": { + "additionalProperties": true, + "title": "Observed", + "type": "object" + }, + "operation": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Operation" + }, + "provider": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Provider" + }, + "source": { + "anyOf": [ + { + "$ref": "#/$defs/SourceReference" + }, + { + "type": "null" + } + ], + "default": null + }, + "source_hash": { + "title": "Source Hash", + "type": "string" + }, + "source_type": { + "title": "Source Type", + "type": "string" + }, + "tool_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Name" + } + }, + "required": [ + "id", + "source_type", + "match_reason", + "event_hash", + "source_hash" + ], + "title": "CapabilityTraceEvidenceV1", + "type": "object" + }, + "CodexPluginAppSummary": { + "additionalProperties": true, + "properties": { + "connector_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Connector Id" + }, + "location": { + "$ref": "#/$defs/CodexPluginSourceLocation" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "plugin": { + "title": "Plugin", + "type": "string" + } + }, + "required": [ + "plugin", + "name", + "path" + ], + "title": "CodexPluginAppSummary", + "type": "object" + }, + "CodexPluginComponentPathIssue": { + "additionalProperties": true, + "properties": { + "component": { + "title": "Component", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "plugin": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Plugin" + }, + "reason": { + "title": "Reason", + "type": "string" + }, + "source_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Ref" + } + }, + "required": [ + "component", + "path", + "reason" + ], + "title": "CodexPluginComponentPathIssue", + "type": "object" + }, + "CodexPluginHookStub": { + "additionalProperties": true, + "properties": { + "command": { + "title": "Command", + "type": "string" + }, + "confidence": { + "default": "medium", + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "location": { + "$ref": "#/$defs/CodexPluginSourceLocation" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "plugin": { + "title": "Plugin", + "type": "string" + }, + "risk_tags": { + "items": { + "type": "string" + }, + "title": "Risk Tags", + "type": "array" + } + }, + "required": [ + "plugin", + "name", + "command", + "path" + ], + "title": "CodexPluginHookStub", + "type": "object" + }, + "CodexPluginMarketplaceSummary": { + "additionalProperties": true, + "properties": { + "missing_policy_entries": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "title": "Missing Policy Entries", + "type": "array" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Name" + }, + "path": { + "title": "Path", + "type": "string" + }, + "plugin_count": { + "default": 0, + "title": "Plugin Count", + "type": "integer" + }, + "skipped_entries": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "title": "Skipped Entries", + "type": "array" + }, + "source_id": { + "title": "Source Id", + "type": "string" + } + }, + "required": [ + "source_id", + "path" + ], + "title": "CodexPluginMarketplaceSummary", + "type": "object" + }, + "CodexPluginMcpServerStub": { + "additionalProperties": true, + "properties": { + "command": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Command" + }, + "inventory_loaded": { + "default": false, + "title": "Inventory Loaded", + "type": "boolean" + }, + "inventory_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Inventory Path" + }, + "location": { + "$ref": "#/$defs/CodexPluginSourceLocation" + }, + "path": { + "title": "Path", + "type": "string" + }, + "plugin": { + "title": "Plugin", + "type": "string" + }, + "server": { + "title": "Server", + "type": "string" + } + }, + "required": [ + "plugin", + "server", + "path" + ], + "title": "CodexPluginMcpServerStub", + "type": "object" + }, + "CodexPluginSkillSummary": { + "additionalProperties": true, + "properties": { + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Description" + }, + "duplicate": { + "default": false, + "title": "Duplicate", + "type": "boolean" + }, + "location": { + "$ref": "#/$defs/CodexPluginSourceLocation" + }, + "missing_fields": { + "items": { + "type": "string" + }, + "title": "Missing Fields", + "type": "array" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Name" + }, + "path": { + "title": "Path", + "type": "string" + }, + "plugin": { + "title": "Plugin", + "type": "string" + } + }, + "required": [ + "plugin", + "path" + ], + "title": "CodexPluginSkillSummary", + "type": "object" + }, + "CodexPluginSourceLocation": { + "additionalProperties": false, + "properties": { + "source_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Path" + }, + "source_pointer": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Pointer" + }, + "source_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Ref" + }, + "source_start_column": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Start Column" + }, + "source_start_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Start Line" + } + }, + "title": "CodexPluginSourceLocation", + "type": "object" + }, + "CodexPluginSummary": { + "additionalProperties": true, + "properties": { + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Description" + }, + "duplicate_name": { + "default": false, + "title": "Duplicate Name", + "type": "boolean" + }, + "duplicate_root": { + "default": false, + "title": "Duplicate Root", + "type": "boolean" + }, + "location": { + "$ref": "#/$defs/CodexPluginSourceLocation" + }, + "manifest_path": { + "title": "Manifest Path", + "type": "string" + }, + "marketplace": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Marketplace" + }, + "missing_fields": { + "items": { + "type": "string" + }, + "title": "Missing Fields", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "name_mismatch": { + "default": false, + "title": "Name Mismatch", + "type": "boolean" + }, + "root_path": { + "title": "Root Path", + "type": "string" + }, + "source_id": { + "title": "Source Id", + "type": "string" + }, + "version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Version" + } + }, + "required": [ + "source_id", + "name", + "root_path", + "manifest_path" + ], + "title": "CodexPluginSummary", + "type": "object" + }, + "CodexPluginSurface": { + "additionalProperties": true, + "properties": { + "app_count": { + "default": 0, + "title": "App Count", + "type": "integer" + }, + "apps": { + "items": { + "$ref": "#/$defs/CodexPluginAppSummary" + }, + "title": "Apps", + "type": "array" + }, + "component_path_issues": { + "items": { + "$ref": "#/$defs/CodexPluginComponentPathIssue" + }, + "title": "Component Path Issues", + "type": "array" + }, + "hook_stub_count": { + "default": 0, + "title": "Hook Stub Count", + "type": "integer" + }, + "hook_stubs": { + "items": { + "$ref": "#/$defs/CodexPluginHookStub" + }, + "title": "Hook Stubs", + "type": "array" + }, + "marketplace_count": { + "default": 0, + "title": "Marketplace Count", + "type": "integer" + }, + "marketplaces": { + "items": { + "$ref": "#/$defs/CodexPluginMarketplaceSummary" + }, + "title": "Marketplaces", + "type": "array" + }, + "mcp_inventory_file_count": { + "default": 0, + "title": "Mcp Inventory File Count", + "type": "integer" + }, + "mcp_inventory_files": { + "items": { + "type": "string" + }, + "title": "Mcp Inventory Files", + "type": "array" + }, + "mcp_server_stub_count": { + "default": 0, + "title": "Mcp Server Stub Count", + "type": "integer" + }, + "mcp_server_stubs": { + "items": { + "$ref": "#/$defs/CodexPluginMcpServerStub" + }, + "title": "Mcp Server Stubs", + "type": "array" + }, + "plugin_count": { + "default": 0, + "title": "Plugin Count", + "type": "integer" + }, + "plugins": { + "items": { + "$ref": "#/$defs/CodexPluginSummary" + }, + "title": "Plugins", + "type": "array" + }, + "skill_count": { + "default": 0, + "title": "Skill Count", + "type": "integer" + }, + "skills": { + "items": { + "$ref": "#/$defs/CodexPluginSkillSummary" + }, + "title": "Skills", + "type": "array" + }, + "warnings": { + "items": { + "type": "string" + }, + "title": "Warnings", + "type": "array" + } + }, + "title": "CodexPluginSurface", + "type": "object" + }, + "ContributionRule": { + "additionalProperties": false, + "description": "Per-finding audit row explaining how a finding contributed to the\nrelease decision.\n\nAdditive in v0.17. Every finding in `report.findings` produces\nexactly one ContributionRule. Reading the contribution rule is\nsufficient to predict the gate outcome for that finding without\nre-deriving the decision logic; the set of valid `(rule, category)`\npairs is the contract documented in STABILITY.md \"Release decision\ntruth table\".", + "properties": { + "category": { + "enum": [ + "blocker", + "review_item", + "excluded" + ], + "title": "Category", + "type": "string" + }, + "check_id": { + "title": "Check Id", + "type": "string" + }, + "finding_id": { + "title": "Finding Id", + "type": "string" + }, + "fingerprint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Fingerprint" + }, + "rationale": { + "title": "Rationale", + "type": "string" + }, + "rule": { + "enum": [ + "policy_block_new", + "severity_block_new", + "policy_baseline_accepted", + "severity_baseline_accepted", + "review_required", + "sub_threshold", + "unsupported_evidence", + "suppressed" + ], + "title": "Rule", + "type": "string" + } + }, + "required": [ + "category", + "check_id", + "finding_id", + "fingerprint", + "rationale", + "rule" + ], + "title": "ContributionRule", + "type": "object" + }, + "DeclaredIntention": { + "properties": { + "id": { + "title": "Id", + "type": "string" + }, + "intent_tags": { + "items": { + "type": "string" + }, + "title": "Intent Tags", + "type": "array" + }, + "kind": { + "enum": [ + "declared_purpose", + "prohibited_action", + "instruction_preview" + ], + "title": "Kind", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "text": { + "title": "Text", + "type": "string" + } + }, + "required": [ + "id", + "intent_tags", + "kind", + "source", + "text" + ], + "title": "DeclaredIntention", + "type": "object" + }, + "EffectSemanticEvidence": { + "additionalProperties": false, + "properties": { + "claims": { + "items": { + "$ref": "#/$defs/SemanticClaimEvidence" + }, + "title": "Claims", + "type": "array" + }, + "confidence": { + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "issues": { + "items": { + "$ref": "#/$defs/SemanticIssueEvidence" + }, + "title": "Issues", + "type": "array" + }, + "status": { + "enum": [ + "declared", + "structural", + "protocol_default", + "inferred", + "unknown", + "conflicting" + ], + "title": "Status", + "type": "string" + } + }, + "required": [ + "status", + "confidence" + ], + "title": "EffectSemanticEvidence", + "type": "object" + }, + "EffectivePolicy": { + "additionalProperties": false, + "description": "Normalized effective-policy snapshot block (roadmap \u00a75.3).\n\nA semantic \u2014 not text \u2014 view of the release policy surface, so the\nbase-vs-head comparator can answer \"was the gate weakened?\" without a\ntext diff. Every field is derived deterministically from the manifest\n(plus, for ``baseline_fingerprints``, the accepted-debt findings), and\nevery list/dict is emitted in sorted order so two builds of the same\nmanifest serialize byte-identically.\n\nFields are chosen so the weakening / expansion classifiers can answer\nthe \u00a75.3 questions by set/rank comparison rather than counting:\n\n- ``ci_mode`` \u2014 was the CI gate weakened (strict -> advisory)?\n- ``fail_on`` \u2014 was the fail-on severity set loosened (head \u2282 base)?\n- ``severity_overrides`` \u2014 was a check's severity lowered across a tier?\n- ``suppressed_check_ids`` \u2014 was a blocking check newly suppressed?\n- ``waiver_scopes`` \u2014 was a waiver scope expanded (head \u228b base)?\n- ``baseline_fingerprints`` \u2014 was the accepted-debt baseline expanded?\n- ``baseline_integrity_mode`` \u2014 was baseline tamper-checking weakened?\n- ``ci_gate_present`` \u2014 was Shipgate CI removed from an opted-in repo?", + "properties": { + "baseline_fingerprints": { + "items": { + "type": "string" + }, + "title": "Baseline Fingerprints", + "type": "array" + }, + "baseline_integrity_mode": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Baseline Integrity Mode" + }, + "ci_gate_present": { + "default": true, + "title": "Ci Gate Present", + "type": "boolean" + }, + "ci_mode": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ci Mode" + }, + "fail_on": { + "items": { + "type": "string" + }, + "title": "Fail On", + "type": "array" + }, + "severity_overrides": { + "additionalProperties": { + "type": "string" + }, + "title": "Severity Overrides", + "type": "object" + }, + "suppressed_check_ids": { + "items": { + "type": "string" + }, + "title": "Suppressed Check Ids", + "type": "array" + }, + "waiver_scopes": { + "items": { + "type": "string" + }, + "title": "Waiver Scopes", + "type": "array" + } + }, + "required": [ + "baseline_fingerprints", + "baseline_integrity_mode", + "ci_gate_present", + "ci_mode", + "fail_on", + "severity_overrides", + "suppressed_check_ids", + "waiver_scopes" + ], + "title": "EffectivePolicy", + "type": "object" + }, + "EvidenceCoverageDecision": { + "properties": { + "binding_coverage": { + "$ref": "#/$defs/BindingCoverageDecision" + }, + "evidence_gaps": { + "items": { + "$ref": "#/$defs/EvidenceGap" + }, + "title": "Evidence Gaps", + "type": "array" + }, + "human_review_recommended": { + "title": "Human Review Recommended", + "type": "boolean" + }, + "identity_coverage": { + "$ref": "#/$defs/IdentityCoverageDecision" + }, + "level": { + "title": "Level", + "type": "string" + }, + "low_confidence_tool_count": { + "title": "Low Confidence Tool Count", + "type": "integer" + }, + "policy_gap_count": { + "default": 0, + "title": "Policy Gap Count", + "type": "integer" + }, + "semantic_coverage": { + "$ref": "#/$defs/SemanticCoverageDecision" + }, + "source_warning_count": { + "title": "Source Warning Count", + "type": "integer" + } + }, + "required": [ + "human_review_recommended", + "level", + "low_confidence_tool_count", + "source_warning_count" + ], + "title": "EvidenceCoverageDecision", + "type": "object" + }, + "EvidenceGap": { + "description": "v0.26: one structured row per measurable evidence gap.\n\n``insufficient_evidence`` previously diagnosed without prescribing;\neach gap names the degraded subject and the specific next action\nthat raises extraction confidence. Purely explanatory \u2014 gating\nstill uses only the counts (the gap list is a projection of them).", + "properties": { + "kind": { + "enum": [ + "low_confidence_tool", + "source_warning", + "incomplete_surface", + "missing_effect_evidence", + "inferred_effect_only", + "conflicting_effect_evidence", + "missing_authority_evidence", + "partial_authority_evidence", + "conflicting_authority_evidence", + "invalid_semantic_annotation", + "incomplete_tool_identity", + "conflicting_tool_identity", + "unresolved_tool_selector", + "ambiguous_tool_selector", + "ambiguous_legacy_tool_identity", + "invalid_tool_binding", + "missing_binding_evidence", + "partial_binding_evidence", + "conflicting_binding_evidence", + "ambiguous_root_agent", + "unresolved_agent_binding", + "unresolved_bound_tool", + "incomplete_handoff_graph", + "invalid_binding_annotation", + "invalid_evidence_provenance", + "inferred_policy_applicability", + "mixed_policy_evidence", + "unknown_policy_evidence", + "conflicting_policy_evidence" + ], + "title": "Kind", + "type": "string" + }, + "next_action": { + "$ref": "#/$defs/EvidenceGapAction" + }, + "source_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Ref" + }, + "source_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Type" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "why": { + "title": "Why", + "type": "string" + } + }, + "required": [ + "kind", + "subject", + "why", + "next_action" + ], + "title": "EvidenceGap", + "type": "object" + }, + "EvidenceGapAction": { + "description": "One concrete, mechanically-executable step that closes a gap.\n\nMirrors the agent-mode ``next_actions[]`` error shape\n(``kind``/``command``/``path``/``why``/``expects``) so agents reuse\none routing vocabulary across error recovery and evidence repair.", + "properties": { + "accepted_values": { + "items": { + "type": "string" + }, + "title": "Accepted Values", + "type": "array" + }, + "auto_apply": { + "const": false, + "default": false, + "title": "Auto Apply", + "type": "boolean" + }, + "command": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Command" + }, + "declaration_template": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Declaration Template" + }, + "expects": { + "title": "Expects", + "type": "string" + }, + "kind": { + "enum": [ + "declare_tool_inventory", + "provide_source", + "review_warning", + "declare_action_effect", + "declare_action_authority", + "provide_complete_inventory", + "resolve_semantic_conflict", + "declare_source_identity", + "qualify_tool_selector", + "provide_tool_binding", + "resolve_tool_identity_conflict", + "regenerate_identity_artifact", + "declare_agent_root", + "declare_agent_bindings", + "provide_static_binding_source", + "provide_complete_binding_graph", + "resolve_binding_conflict", + "regenerate_binding_artifact", + "provide_policy_evidence", + "review_policy_evidence", + "resolve_policy_evidence_conflict" + ], + "title": "Kind", + "type": "string" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "requires_human_review": { + "const": true, + "default": true, + "title": "Requires Human Review", + "type": "boolean" + }, + "suggested_patch_kind": { + "const": "manual", + "default": "manual", + "title": "Suggested Patch Kind", + "type": "string" + }, + "why": { + "title": "Why", + "type": "string" + } + }, + "required": [ + "kind", + "why", + "expects" + ], + "title": "EvidenceGapAction", + "type": "object" + }, + "FailPolicy": { + "properties": { + "ci_mode": { + "title": "Ci Mode", + "type": "string" + }, + "exit_code": { + "title": "Exit Code", + "type": "integer" + }, + "fail_on": { + "items": { + "enum": [ + "info", + "low", + "medium", + "high", + "critical" + ], + "type": "string" + }, + "title": "Fail On", + "type": "array" + }, + "new_findings_only": { + "default": false, + "title": "New Findings Only", + "type": "boolean" + }, + "would_fail_ci": { + "title": "Would Fail Ci", + "type": "boolean" + } + }, + "required": [ + "ci_mode", + "exit_code", + "fail_on", + "new_findings_only", + "would_fail_ci" + ], + "title": "FailPolicy", + "type": "object" + }, + "Finding": { + "additionalProperties": true, + "properties": { + "agent_action": { + "enum": [ + "auto_apply", + "propose_patch_for_review", + "escalate_to_human", + "suppress_with_reason", + "informational" + ], + "type": "string" + }, + "agent_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Agent Id" + }, + "autofix_safe": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Autofix Safe" + }, + "baseline_status": { + "anyOf": [ + { + "enum": [ + "new", + "matched", + "resolved" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Baseline Status" + }, + "blocks_release": { + "default": false, + "title": "Blocks Release", + "type": "boolean" + }, + "capability_policy_evidence": { + "anyOf": [ + { + "$ref": "#/$defs/CapabilityPolicyEvidence" + }, + { + "type": "null" + } + ], + "default": null + }, + "capability_refs": { + "items": { + "type": "string" + }, + "title": "Capability Refs", + "type": "array" + }, + "capability_trace_refs": { + "items": { + "type": "string" + }, + "title": "Capability Trace Refs", + "type": "array" + }, + "category": { + "title": "Category", + "type": "string" + }, + "check_id": { + "title": "Check Id", + "type": "string" + }, + "confidence": { + "default": "medium", + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "docs_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Docs Url" + }, + "evidence": { + "additionalProperties": true, + "title": "Evidence", + "type": "object" + }, + "fingerprint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Fingerprint" + }, + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Id" + }, + "patches": { + "anyOf": [ + { + "items": { + "discriminator": { + "mapping": { + "append_pointer": "#/$defs/AppendPointerPatch", + "manual": "#/$defs/ManualPatch", + "remove_pointer": "#/$defs/RemovePointerPatch", + "set_pointer": "#/$defs/SetPointerPatch" + }, + "propertyName": "kind" + }, + "oneOf": [ + { + "$ref": "#/$defs/SetPointerPatch" + }, + { + "$ref": "#/$defs/AppendPointerPatch" + }, + { + "$ref": "#/$defs/RemovePointerPatch" + }, + { + "$ref": "#/$defs/ManualPatch" + } + ] + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Patches" + }, + "policy_evidence_source": { + "anyOf": [ + { + "$ref": "#/$defs/SourceReference" + }, + { + "type": "null" + } + ], + "default": null + }, + "policy_routing": { + "anyOf": [ + { + "$ref": "#/$defs/PolicyRoutingMetadata" + }, + { + "type": "null" + } + ], + "default": null + }, + "provenance_kind": { + "enum": [ + "static_declaration", + "ast_extraction", + "keyword_heuristic", + "regex_heuristic", + "policy_pack", + "runtime_trace" + ], + "type": "string" + }, + "recommendation": { + "title": "Recommendation", + "type": "string" + }, + "requires_human_review": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Requires Human Review" + }, + "severity": { + "enum": [ + "info", + "low", + "medium", + "high", + "critical" + ], + "title": "Severity", + "type": "string" + }, + "source": { + "anyOf": [ + { + "$ref": "#/$defs/SourceReference" + }, + { + "type": "null" + } + ], + "default": null + }, + "suggested_patch_kind": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suggested Patch Kind" + }, + "support": { + "anyOf": [ + { + "$ref": "#/$defs/FindingSupport" + }, + { + "type": "null" + } + ], + "default": null + }, + "suppressed": { + "default": false, + "title": "Suppressed", + "type": "boolean" + }, + "suppression_reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Suppression Reason" + }, + "title": { + "title": "Title", + "type": "string" + }, + "tool_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Id" + }, + "tool_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Name" + } + }, + "required": [ + "agent_action", + "baseline_status", + "blocks_release", + "capability_refs", + "category", + "check_id", + "confidence", + "evidence", + "fingerprint", + "id", + "provenance_kind", + "recommendation", + "severity", + "suppressed", + "title" + ], + "title": "Finding", + "type": "object" + }, + "FindingSupport": { + "additionalProperties": false, + "description": "Authoritative support for finding confidence and release contribution.\n\nRule metadata may request a severity or block, but it cannot upgrade the\nunderlying evidence. ``support_hash`` binds baselines and audit surfaces\nto the predicate evidence that actually made the finding eligible.", + "properties": { + "blocking_eligible": { + "default": false, + "title": "Blocking Eligible", + "type": "boolean" + }, + "claim_ids": { + "items": { + "type": "string" + }, + "title": "Claim Ids", + "type": "array" + }, + "confidence": { + "default": "low", + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "evidence_bases": { + "items": { + "enum": [ + "reviewed_declaration", + "protocol_structure", + "typed_provider_fact", + "structural_scope", + "inferred_keyword", + "inferred_regex", + "protocol_default", + "unknown" + ], + "type": "string" + }, + "title": "Evidence Bases", + "type": "array" + }, + "policy_eligible": { + "default": false, + "title": "Policy Eligible", + "type": "boolean" + }, + "predicates": { + "items": { + "$ref": "#/$defs/PolicyPredicateEvidence" + }, + "title": "Predicates", + "type": "array" + }, + "status": { + "default": "matched", + "enum": [ + "matched", + "not_matched", + "indeterminate", + "conflicting" + ], + "title": "Status", + "type": "string" + }, + "support_hash": { + "title": "Support Hash", + "type": "string" + } + }, + "required": [ + "support_hash" + ], + "title": "FindingSupport", + "type": "object" + }, + "HeuristicsFilter": { + "additionalProperties": false, + "description": "v0.21: top-level envelope describing the ``--no-heuristics``\nfilter pass.\n\nEmitted on every report regardless of whether the flag was set, so\nconsumers always read the same shape. When the flag is unset,\n``enabled=False`` and the count fields are zero \u2014 the active\nfinding set is unchanged. When the flag is set, every finding whose\n``provenance_kind`` is in ``excluded_provenance_kinds`` is marked\n``suppressed=True`` with ``suppression_reason=\"filtered by\n--no-heuristics\"`` BEFORE the release decision is built, so heuristic\nfindings can no longer gate release. Filtered findings stay in\n``findings[]`` for transparency; the audit envelope here records\naggregate counts.\n\nEarns the contract weight of ``Finding.provenance_kind`` (shipped\nv0.15) by giving it a first-class consumer. Same shape pattern as\n``PrivacyAudit``: an envelope that proves the filter ran and tells\na reviewer/agent which findings were excluded and why.", + "properties": { + "enabled": { + "default": false, + "title": "Enabled", + "type": "boolean" + }, + "excluded_provenance_kinds": { + "items": { + "type": "string" + }, + "title": "Excluded Provenance Kinds", + "type": "array" + }, + "filtered_by_kind": { + "additionalProperties": { + "type": "integer" + }, + "title": "Filtered By Kind", + "type": "object" + }, + "filtered_finding_count": { + "default": 0, + "title": "Filtered Finding Count", + "type": "integer" + } + }, + "required": [ + "enabled", + "excluded_provenance_kinds", + "filtered_by_kind", + "filtered_finding_count" + ], + "title": "HeuristicsFilter", + "type": "object" + }, + "HumanAck": { + "additionalProperties": false, + "description": "Human-acknowledgement state block (Decision 4 shape).\n\n``required`` is set by the human-ack builder when a trust-root\nweakening finding needs declared human authority; ``satisfied`` is\n``True`` when every required acknowledgement is present. ``acks``\nlists the declared entries; ``outstanding`` lists the surfaces still\nneeding one. The default empty shape is not required and therefore\nsatisfied.", + "properties": { + "acks": { + "items": { + "$ref": "#/$defs/HumanAckEntry" + }, + "title": "Acks", + "type": "array" + }, + "outstanding": { + "items": { + "type": "string" + }, + "title": "Outstanding", + "type": "array" + }, + "required": { + "default": false, + "title": "Required", + "type": "boolean" + }, + "satisfied": { + "default": true, + "title": "Satisfied", + "type": "boolean" + } + }, + "required": [ + "acks", + "outstanding", + "required", + "satisfied" + ], + "title": "HumanAck", + "type": "object" + }, + "HumanAckEntry": { + "additionalProperties": false, + "description": "One declared human-acknowledgement record.\n\nWithin the static boundary acknowledgement can only be *declared*\nevidence (roadmap \u00a75.4) \u2014 never inferred. An entry names the human\n``owner``, the ``reason``, an optional ``expires`` date (copied\nverbatim from the source, never generated), and the\n``affected_surface`` the ack covers.", + "properties": { + "affected_surface": { + "title": "Affected Surface", + "type": "string" + }, + "expires": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Expires" + }, + "owner": { + "title": "Owner", + "type": "string" + }, + "reason": { + "title": "Reason", + "type": "string" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + } + }, + "required": [ + "affected_surface", + "expires", + "owner", + "reason", + "source" + ], + "title": "HumanAckEntry", + "type": "object" + }, + "IdentityCoverageDecision": { + "properties": { + "ambiguous_name_count": { + "default": 0, + "title": "Ambiguous Name Count", + "type": "integer" + }, + "bound_tools": { + "default": 0, + "title": "Bound Tools", + "type": "integer" + }, + "canonical_tools": { + "default": 0, + "title": "Canonical Tools", + "type": "integer" + }, + "gap_count": { + "default": 0, + "title": "Gap Count", + "type": "integer" + }, + "pass_eligible_tools": { + "default": 0, + "title": "Pass Eligible Tools", + "type": "integer" + }, + "reason_counts": { + "additionalProperties": { + "type": "integer" + }, + "title": "Reason Counts", + "type": "object" + }, + "total_observations": { + "default": 0, + "title": "Total Observations", + "type": "integer" + } + }, + "title": "IdentityCoverageDecision", + "type": "object" + }, + "LoadedPolicyPack": { + "properties": { + "id": { + "title": "Id", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "owner": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Owner" + }, + "path": { + "title": "Path", + "type": "string" + }, + "rule_count": { + "title": "Rule Count", + "type": "integer" + }, + "sha256": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sha256" + }, + "sha256_status": { + "default": "unpinned", + "enum": [ + "unpinned", + "verified" + ], + "title": "Sha256 Status", + "type": "string" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + }, + "version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Version" + } + }, + "required": [ + "id", + "name", + "path", + "rule_count" + ], + "title": "LoadedPolicyPack", + "type": "object" + }, + "ManualPatch": { + "additionalProperties": false, + "description": "No machine-applicable change. Carries human-readable instructions.\n\nUsed for every finding whose check ID has no v0.6 non-manual generator\nand for findings (like trace flips, per C6) that are intentionally\nnever auto-patched.", + "properties": { + "instructions": { + "title": "Instructions", + "type": "string" + }, + "kind": { + "const": "manual", + "default": "manual", + "title": "Kind", + "type": "string" + } + }, + "required": [ + "instructions" + ], + "title": "ManualPatch", + "type": "object" + }, + "Misalignment": { + "properties": { + "capability_refs": { + "items": { + "type": "string" + }, + "title": "Capability Refs", + "type": "array" + }, + "finding_refs": { + "items": { + "type": "string" + }, + "title": "Finding Refs", + "type": "array" + }, + "gap": { + "title": "Gap", + "type": "string" + }, + "id": { + "title": "Id", + "type": "string" + }, + "intention_refs": { + "items": { + "type": "string" + }, + "title": "Intention Refs", + "type": "array" + }, + "kind": { + "enum": [ + "policy_gap", + "scope_drift", + "prohibited_action_present", + "control_missing", + "intent_mismatch", + "undetected_gap" + ], + "title": "Kind", + "type": "string" + }, + "policy_requirement": { + "title": "Policy Requirement", + "type": "string" + }, + "release_implication": { + "title": "Release Implication", + "type": "string" + }, + "severity": { + "enum": [ + "info", + "low", + "medium", + "high", + "critical" + ], + "title": "Severity", + "type": "string" + }, + "tool_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Name" + } + }, + "required": [ + "capability_refs", + "finding_refs", + "gap", + "id", + "intention_refs", + "kind", + "policy_requirement", + "release_implication", + "severity", + "tool_name" + ], + "title": "Misalignment", + "type": "object" + }, + "PolicyApprovalRouting": { + "additionalProperties": false, + "description": "Non-enforcing approval-routing metadata from a policy-pack rule.", + "properties": { + "enforced": { + "const": false, + "default": false, + "title": "Enforced", + "type": "boolean" + }, + "min_approvals": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Min Approvals" + }, + "required": { + "default": false, + "title": "Required", + "type": "boolean" + }, + "teams": { + "items": { + "type": "string" + }, + "title": "Teams", + "type": "array" + } + }, + "title": "PolicyApprovalRouting", + "type": "object" + }, + "PolicyAudit": { + "additionalProperties": false, + "description": "v0.17 (M1) top-of-report audit envelope for policy decisions\napplied during scan.\n\nCarries severity-override audit today; M2 (baseline integrity) and\nM5 (plugin validation) will land sibling fields here so the audit\nenvelope stays stable across the trust-hardening releases.", + "properties": { + "severity_overrides_applied": { + "items": { + "$ref": "#/$defs/SeverityOverrideAuditEntry" + }, + "title": "Severity Overrides Applied", + "type": "array" + } + }, + "title": "PolicyAudit", + "type": "object" + }, + "PolicyPredicateEvidence": { + "additionalProperties": false, + "description": "One tri-state policy predicate and the evidence that supports it.", + "properties": { + "claim_ids": { + "items": { + "type": "string" + }, + "title": "Claim Ids", + "type": "array" + }, + "confidence": { + "default": "low", + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "evidence_bases": { + "items": { + "enum": [ + "reviewed_declaration", + "protocol_structure", + "typed_provider_fact", + "structural_scope", + "inferred_keyword", + "inferred_regex", + "protocol_default", + "unknown" + ], + "type": "string" + }, + "title": "Evidence Bases", + "type": "array" + }, + "expected": { + "default": null, + "title": "Expected" + }, + "observed": { + "default": null, + "title": "Observed" + }, + "policy_eligible": { + "default": false, + "title": "Policy Eligible", + "type": "boolean" + }, + "predicate": { + "title": "Predicate", + "type": "string" + }, + "status": { + "enum": [ + "matched", + "not_matched", + "indeterminate", + "conflicting" + ], + "title": "Status", + "type": "string" + }, + "why": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Why" + } + }, + "required": [ + "predicate", + "status" + ], + "title": "PolicyPredicateEvidence", + "type": "object" + }, + "PolicyRoutingMetadata": { + "additionalProperties": false, + "description": "Reviewer routing metadata carried outside finding evidence.", + "properties": { + "approval": { + "$ref": "#/$defs/PolicyApprovalRouting" + }, + "owner": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Owner" + }, + "reviewers": { + "items": { + "type": "string" + }, + "title": "Reviewers", + "type": "array" + } + }, + "title": "PolicyRoutingMetadata", + "type": "object" + }, + "PrivacyAudit": { + "additionalProperties": false, + "description": "Top-level audit envelope proving the default redaction pass ran.", + "properties": { + "enabled": { + "default": true, + "title": "Enabled", + "type": "boolean" + }, + "notes": { + "items": { + "type": "string" + }, + "title": "Notes", + "type": "array" + }, + "output_surfaces": { + "items": { + "type": "string" + }, + "title": "Output Surfaces", + "type": "array" + }, + "redacted_occurrence_count": { + "default": 0, + "title": "Redacted Occurrence Count", + "type": "integer" + }, + "redacted_paths": { + "items": { + "$ref": "#/$defs/RedactedPathSummary" + }, + "title": "Redacted Paths", + "type": "array" + }, + "rules_version": { + "title": "Rules Version", + "type": "string" + }, + "sensitive_field_inventory_version": { + "title": "Sensitive Field Inventory Version", + "type": "string" + } + }, + "required": [ + "enabled", + "notes", + "output_surfaces", + "redacted_occurrence_count", + "redacted_paths", + "rules_version", + "sensitive_field_inventory_version" + ], + "title": "PrivacyAudit", + "type": "object" + }, + "ProtectedSurfaceChange": { + "additionalProperties": false, + "description": "One touched protected path / policy surface (trust root).\n\nTier A trust-root protection (roadmap \u00a75.1/\u00a75.2) records *which*\nprotected surface a PR touched. ``path`` is the changed file; ``kind``\nis the classifier bucket it matched (e.g. ``manifest``, ``ci_gate``,\n``policy``); ``glob`` is the matched pattern. ``related_finding_ids``\nlinks to the ordinary ``SHIP-VERIFY-*`` findings that gate.", + "properties": { + "glob": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Glob" + }, + "kind": { + "default": "unknown", + "title": "Kind", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "related_finding_ids": { + "items": { + "type": "string" + }, + "title": "Related Finding Ids", + "type": "array" + } + }, + "required": [ + "glob", + "kind", + "path", + "related_finding_ids" + ], + "title": "ProtectedSurfaceChange", + "type": "object" + }, + "RedactedPathSummary": { + "additionalProperties": false, + "description": "One privacy-audit row for a structural output path.\n\nThe row intentionally carries only aggregate counts and secret kinds,\nnever the original value or a hash/verifier of that value.", + "properties": { + "count": { + "default": 0, + "title": "Count", + "type": "integer" + }, + "kinds": { + "items": { + "type": "string" + }, + "title": "Kinds", + "type": "array" + }, + "path": { + "title": "Path", + "type": "string" + } + }, + "required": [ + "count", + "kinds", + "path" + ], + "title": "RedactedPathSummary", + "type": "object" + }, + "ReleaseConsequence": { + "properties": { + "blocker_misalignment_count": { + "default": 0, + "title": "Blocker Misalignment Count", + "type": "integer" + }, + "decision": { + "enum": [ + "blocked", + "review_required", + "insufficient_evidence", + "passed" + ], + "title": "Decision", + "type": "string" + }, + "fail_policy": { + "$ref": "#/$defs/FailPolicy" + }, + "review_misalignment_count": { + "default": 0, + "title": "Review Misalignment Count", + "type": "integer" + }, + "summary": { + "title": "Summary", + "type": "string" + } + }, + "required": [ + "blocker_misalignment_count", + "decision", + "fail_policy", + "review_misalignment_count", + "summary" + ], + "title": "ReleaseConsequence", + "type": "object" + }, + "ReleaseDecision": { + "properties": { + "baseline_delta": { + "$ref": "#/$defs/BaselineDelta" + }, + "blockers": { + "items": { + "$ref": "#/$defs/ReleaseDecisionItem" + }, + "title": "Blockers", + "type": "array" + }, + "contribution_rules": { + "items": { + "$ref": "#/$defs/ContributionRule" + }, + "title": "Contribution Rules", + "type": "array" + }, + "decision": { + "enum": [ + "blocked", + "review_required", + "insufficient_evidence", + "passed" + ], + "title": "Decision", + "type": "string" + }, + "evidence_coverage": { + "$ref": "#/$defs/EvidenceCoverageDecision" + }, + "fail_policy": { + "$ref": "#/$defs/FailPolicy" + }, + "reason": { + "title": "Reason", + "type": "string" + }, + "review_items": { + "items": { + "$ref": "#/$defs/ReleaseDecisionItem" + }, + "title": "Review Items", + "type": "array" + }, + "runtime_behavior_verified": { + "const": false, + "default": false, + "title": "Runtime Behavior Verified", + "type": "boolean" + }, + "static_analysis_only": { + "const": true, + "default": true, + "title": "Static Analysis Only", + "type": "boolean" + }, + "static_verdict_disclaimer": { + "default": "This verdict covers deterministic static evidence only. Agents Shipgate did not execute the agent or prove runtime behavior, tool routing, credential enforcement, or safety.", + "title": "Static Verdict Disclaimer", + "type": "string" + } + }, + "required": [ + "baseline_delta", + "blockers", + "contribution_rules", + "decision", + "evidence_coverage", + "fail_policy", + "reason", + "review_items" + ], + "title": "ReleaseDecision", + "type": "object" + }, + "ReleaseDecisionItem": { + "properties": { + "baseline_status": { + "anyOf": [ + { + "enum": [ + "new", + "matched", + "resolved" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Baseline Status" + }, + "blocks_release": { + "default": false, + "title": "Blocks Release", + "type": "boolean" + }, + "capability_refs": { + "items": { + "type": "string" + }, + "title": "Capability Refs", + "type": "array" + }, + "capability_trace_refs": { + "items": { + "type": "string" + }, + "title": "Capability Trace Refs", + "type": "array" + }, + "check_id": { + "title": "Check Id", + "type": "string" + }, + "fingerprint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Fingerprint" + }, + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Id" + }, + "policy_evidence_source": { + "anyOf": [ + { + "$ref": "#/$defs/SourceReference" + }, + { + "type": "null" + } + ], + "default": null + }, + "severity": { + "enum": [ + "info", + "low", + "medium", + "high", + "critical" + ], + "title": "Severity", + "type": "string" + }, + "source": { + "anyOf": [ + { + "$ref": "#/$defs/SourceReference" + }, + { + "type": "null" + } + ], + "default": null + }, + "support": { + "anyOf": [ + { + "$ref": "#/$defs/FindingSupport" + }, + { + "type": "null" + } + ], + "default": null + }, + "title": { + "title": "Title", + "type": "string" + } + }, + "required": [ + "baseline_status", + "blocks_release", + "capability_refs", + "check_id", + "fingerprint", + "id", + "severity", + "title" + ], + "title": "ReleaseDecisionItem", + "type": "object" + }, + "RemovePointerPatch": { + "additionalProperties": false, + "description": "Remove the node at a JSON pointer.", + "properties": { + "confidence": { + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "kind": { + "const": "remove_pointer", + "default": "remove_pointer", + "title": "Kind", + "type": "string" + }, + "pointer": { + "title": "Pointer", + "type": "string" + }, + "rationale": { + "title": "Rationale", + "type": "string" + }, + "target_file": { + "title": "Target File", + "type": "string" + }, + "target_format": { + "enum": [ + "yaml", + "json" + ], + "title": "Target Format", + "type": "string" + }, + "target_sha256": { + "title": "Target Sha256", + "type": "string" + } + }, + "required": [ + "target_file", + "pointer", + "target_format", + "confidence", + "rationale", + "target_sha256" + ], + "title": "RemovePointerPatch", + "type": "object" + }, + "ReportSummary": { + "properties": { + "critical_count": { + "default": 0, + "title": "Critical Count", + "type": "integer" + }, + "evidence_coverage": { + "default": "static", + "title": "Evidence Coverage", + "type": "string" + }, + "high_count": { + "default": 0, + "title": "High Count", + "type": "integer" + }, + "human_review_recommended": { + "default": false, + "title": "Human Review Recommended", + "type": "boolean" + }, + "info_count": { + "default": 0, + "title": "Info Count", + "type": "integer" + }, + "low_count": { + "default": 0, + "title": "Low Count", + "type": "integer" + }, + "medium_count": { + "default": 0, + "title": "Medium Count", + "type": "integer" + }, + "status": { + "title": "Status", + "type": "string" + }, + "suppressed_count": { + "default": 0, + "title": "Suppressed Count", + "type": "integer" + } + }, + "required": [ + "status" + ], + "title": "ReportSummary", + "type": "object" + }, + "ReviewerSummary": { + "additionalProperties": false, + "description": "Top-level summary block shaped for one-fetch reviewer consumption.\n\nDeterministic projection of the reviewer lens surfaces\n(``tool_surface_diff``, capability/intent diff, ``action_surface_diff``,\nevidence matrix) and the audit envelopes (``policy_audit``,\n``privacy_audit``, baseline integrity findings). A reviewer who wants\nheadline activity counts and a recommended starting surface can read\nthis block instead of opening every lens / audit envelope.\n\nParallels ``AgentSummary`` but for the audit/lens dimensions:\n``AgentSummary`` answers \"what should an agent do next?\" and\n``ReviewerSummary`` answers \"what should a reviewer look at first?\".\n\nAll fields are derived; this block cannot disagree with the\nunderlying lens/audit data.", + "properties": { + "action_surface_changes": { + "default": 0, + "title": "Action Surface Changes", + "type": "integer" + }, + "baseline_integrity_issues": { + "default": 0, + "title": "Baseline Integrity Issues", + "type": "integer" + }, + "capability_misalignments": { + "default": 0, + "title": "Capability Misalignments", + "type": "integer" + }, + "evidence_matrix_gaps": { + "default": 0, + "title": "Evidence Matrix Gaps", + "type": "integer" + }, + "first_recommended_surface": { + "anyOf": [ + { + "$ref": "#/$defs/ReviewerSurfacePointer" + }, + { + "type": "null" + } + ], + "default": null + }, + "headline": { + "title": "Headline", + "type": "string" + }, + "privacy_redactions": { + "default": 0, + "title": "Privacy Redactions", + "type": "integer" + }, + "severity_overrides_applied": { + "default": 0, + "title": "Severity Overrides Applied", + "type": "integer" + }, + "severity_overrides_tier_crossed": { + "default": 0, + "title": "Severity Overrides Tier Crossed", + "type": "integer" + }, + "tool_surface_changes": { + "default": 0, + "title": "Tool Surface Changes", + "type": "integer" + }, + "verdict": { + "enum": [ + "blocked", + "review_required", + "insufficient_evidence", + "passed" + ], + "title": "Verdict", + "type": "string" + } + }, + "required": [ + "action_surface_changes", + "baseline_integrity_issues", + "capability_misalignments", + "evidence_matrix_gaps", + "first_recommended_surface", + "headline", + "privacy_redactions", + "severity_overrides_applied", + "severity_overrides_tier_crossed", + "tool_surface_changes", + "verdict" + ], + "title": "ReviewerSummary", + "type": "object" + }, + "ReviewerSurfacePointer": { + "additionalProperties": false, + "description": "A single recommended reviewer entry point.\n\nMirrors the ``next_actions[]`` shape (kind/path/why) used by other\ncontract surfaces so the same renderer pattern works here. ``kind``\nclassifies the surface family (``release_decision`` /``lens`` /\n``audit`` /``evidence_matrix``); ``name`` is the canonical\nmachine-readable identifier; ``path`` is a dotted JSON path the\nreviewer can use to navigate. ``why`` is one sentence of reviewer\nrationale, suitable for a PR comment lead.", + "properties": { + "kind": { + "enum": [ + "release_decision", + "lens", + "audit", + "evidence_matrix" + ], + "title": "Kind", + "type": "string" + }, + "name": { + "enum": [ + "tool_surface_diff", + "capability_intent_diff", + "action_surface_diff", + "evidence_matrix", + "policy_audit", + "privacy_audit", + "baseline_integrity", + "release_decision" + ], + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "why": { + "title": "Why", + "type": "string" + } + }, + "required": [ + "kind", + "name", + "path", + "why" + ], + "title": "ReviewerSurfacePointer", + "type": "object" + }, + "SemanticClaimEvidence": { + "additionalProperties": false, + "properties": { + "basis": { + "default": "unknown", + "enum": [ + "reviewed_declaration", + "protocol_structure", + "typed_provider_fact", + "structural_scope", + "inferred_keyword", + "inferred_regex", + "protocol_default", + "unknown" + ], + "title": "Basis", + "type": "string" + }, + "claim_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Claim Id" + }, + "confidence": { + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "dimension": { + "enum": [ + "identity", + "binding", + "effect", + "authority" + ], + "title": "Dimension", + "type": "string" + }, + "evidence": { + "additionalProperties": true, + "title": "Evidence", + "type": "object" + }, + "policy_eligible": { + "default": false, + "title": "Policy Eligible", + "type": "boolean" + }, + "provenance_kind": { + "enum": [ + "static_declaration", + "ast_extraction", + "keyword_heuristic", + "regex_heuristic", + "policy_pack", + "runtime_trace" + ], + "title": "Provenance Kind", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "source_pointer": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Pointer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "dimension", + "value", + "confidence", + "provenance_kind", + "source" + ], + "title": "SemanticClaimEvidence", + "type": "object" + }, + "SemanticCoverageDecision": { + "description": "v0.29 pass eligibility across the normalized action surface.\n\nUnlike extraction-confidence thresholds, semantic gaps are\nzero-tolerance: any non-pass-eligible unknown/partial/conflicting\ndimension prevents ``passed``. Known authority review concerns (for\nexample ambient or unscoped credentials) are counted separately so\nthey deterministically route to ``review_required`` rather than\n``insufficient_evidence``.", + "properties": { + "gap_count": { + "default": 0, + "title": "Gap Count", + "type": "integer" + }, + "pass_eligible_actions": { + "default": 0, + "title": "Pass Eligible Actions", + "type": "integer" + }, + "reason_counts": { + "additionalProperties": { + "type": "integer" + }, + "title": "Reason Counts", + "type": "object" + }, + "review_concern_count": { + "default": 0, + "title": "Review Concern Count", + "type": "integer" + }, + "total_actions": { + "default": 0, + "title": "Total Actions", + "type": "integer" + } + }, + "title": "SemanticCoverageDecision", + "type": "object" + }, + "SemanticIssueEvidence": { + "additionalProperties": false, + "properties": { + "dimension": { + "enum": [ + "identity", + "binding", + "effect", + "authority" + ], + "title": "Dimension", + "type": "string" + }, + "kind": { + "enum": [ + "incomplete_tool_identity", + "conflicting_tool_identity", + "unresolved_tool_selector", + "ambiguous_tool_selector", + "ambiguous_legacy_tool_identity", + "invalid_tool_binding", + "incomplete_surface", + "missing_effect_evidence", + "inferred_effect_only", + "conflicting_effect_evidence", + "missing_authority_evidence", + "partial_authority_evidence", + "conflicting_authority_evidence", + "invalid_semantic_annotation", + "missing_binding_evidence", + "partial_binding_evidence", + "conflicting_binding_evidence", + "ambiguous_root_agent", + "unresolved_agent_binding", + "unresolved_bound_tool", + "incomplete_handoff_graph", + "invalid_binding_annotation", + "invalid_evidence_provenance" + ], + "title": "Kind", + "type": "string" + }, + "message": { + "title": "Message", + "type": "string" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + }, + "source_pointer": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Pointer" + } + }, + "required": [ + "kind", + "dimension", + "message" + ], + "title": "SemanticIssueEvidence", + "type": "object" + }, + "SetPointerPatch": { + "additionalProperties": false, + "description": "Set the value at a JSON pointer inside a YAML or JSON file.", + "properties": { + "confidence": { + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "kind": { + "const": "set_pointer", + "default": "set_pointer", + "title": "Kind", + "type": "string" + }, + "pointer": { + "title": "Pointer", + "type": "string" + }, + "rationale": { + "title": "Rationale", + "type": "string" + }, + "target_file": { + "title": "Target File", + "type": "string" + }, + "target_format": { + "enum": [ + "yaml", + "json" + ], + "title": "Target Format", + "type": "string" + }, + "target_sha256": { + "title": "Target Sha256", + "type": "string" + }, + "value": { + "title": "Value" + } + }, + "required": [ + "target_file", + "pointer", + "value", + "target_format", + "confidence", + "rationale", + "target_sha256" + ], + "title": "SetPointerPatch", + "type": "object" + }, + "SeverityOverrideAuditEntry": { + "additionalProperties": false, + "description": "One row in ``ReadinessReport.policy_audit.severity_overrides_applied``.\n\nv0.17 (M1). Surfaces every manifest-driven severity override so a\nreviewer can see what was downgraded (or upgraded) without diving\ninto per-finding evidence. Emitted regardless of whether the override\nmatched any active finding \u2014 entries for checks that did not fire\nstill document reviewer intent.", + "properties": { + "applied_severity": { + "enum": [ + "info", + "low", + "medium", + "high", + "critical" + ], + "title": "Applied Severity", + "type": "string" + }, + "check_id": { + "title": "Check Id", + "type": "string" + }, + "default_severity": { + "enum": [ + "info", + "low", + "medium", + "high", + "critical" + ], + "title": "Default Severity", + "type": "string" + }, + "direction": { + "default": "same", + "enum": [ + "downgrade", + "upgrade", + "same" + ], + "title": "Direction", + "type": "string" + }, + "expires": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Expires" + }, + "manifest_path": { + "title": "Manifest Path", + "type": "string" + }, + "reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reason" + }, + "tier_crossed": { + "default": false, + "title": "Tier Crossed", + "type": "boolean" + } + }, + "required": [ + "check_id", + "default_severity", + "applied_severity", + "manifest_path" + ], + "title": "SeverityOverrideAuditEntry", + "type": "object" + }, + "SourceReference": { + "additionalProperties": true, + "properties": { + "end_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End Line" + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Location" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "pointer": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Pointer" + }, + "ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ref" + }, + "start_column": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start Column" + }, + "start_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start Line" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "SourceReference", + "type": "object" + }, + "SuggestedScenario": { + "properties": { + "expected_control": { + "title": "Expected Control", + "type": "string" + }, + "given": { + "title": "Given", + "type": "string" + }, + "id": { + "title": "Id", + "type": "string" + }, + "scenario_type": { + "enum": [ + "approval", + "confirmation", + "idempotency_retry", + "least_privilege_scope", + "prohibited_action", + "wildcard_inventory", + "schema_boundary", + "prompt_scope_alignment", + "test_case_coverage" + ], + "title": "Scenario Type", + "type": "string" + }, + "source_findings": { + "items": { + "type": "string" + }, + "title": "Source Findings", + "type": "array" + }, + "source_misalignments": { + "items": { + "type": "string" + }, + "title": "Source Misalignments", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + } + }, + "required": [ + "expected_control", + "given", + "id", + "scenario_type", + "source_findings", + "source_misalignments", + "title" + ], + "title": "SuggestedScenario", + "type": "object" + }, + "ToolIdentityEvidence": { + "additionalProperties": false, + "properties": { + "binding_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Binding Id" + }, + "claims": { + "items": { + "$ref": "#/$defs/SemanticClaimEvidence" + }, + "title": "Claims", + "type": "array" + }, + "issues": { + "items": { + "$ref": "#/$defs/SemanticIssueEvidence" + }, + "title": "Issues", + "type": "array" + }, + "observation_ids": { + "items": { + "type": "string" + }, + "title": "Observation Ids", + "type": "array" + }, + "pass_eligible": { + "title": "Pass Eligible", + "type": "boolean" + }, + "primary_observation_id": { + "title": "Primary Observation Id", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "status": { + "enum": [ + "declared", + "structural", + "partial", + "unknown", + "conflicting" + ], + "title": "Status", + "type": "string" + }, + "tool_id": { + "title": "Tool Id", + "type": "string" + } + }, + "required": [ + "tool_id", + "status", + "provider", + "primary_observation_id", + "pass_eligible" + ], + "title": "ToolIdentityEvidence", + "type": "object" + }, + "ToolSemanticEvidence": { + "additionalProperties": false, + "description": "Public semantic assessment attached to action and capability facts.", + "properties": { + "authority": { + "$ref": "#/$defs/AuthoritySemanticEvidence" + }, + "binding": { + "$ref": "#/$defs/BindingSemanticEvidence" + }, + "conservative_effect": { + "enum": [ + "read", + "write", + "destructive", + "external_communication", + "financial_write", + "production_operation", + "privileged_data_access", + "code_execution", + "identity_access" + ], + "title": "Conservative Effect", + "type": "string" + }, + "effect": { + "$ref": "#/$defs/EffectSemanticEvidence" + }, + "identity": { + "anyOf": [ + { + "$ref": "#/$defs/ToolIdentityEvidence" + }, + { + "type": "null" + } + ], + "default": null + }, + "pass_eligible": { + "title": "Pass Eligible", + "type": "boolean" + } + }, + "required": [ + "conservative_effect", + "effect", + "authority", + "pass_eligible" + ], + "title": "ToolSemanticEvidence", + "type": "object" + }, + "ToolSurfaceControlChange": { + "additionalProperties": false, + "properties": { + "control": { + "enum": [ + "approval_policy", + "confirmation_policy", + "idempotency_evidence" + ], + "title": "Control", + "type": "string" + }, + "kind": { + "enum": [ + "added", + "removed", + "changed" + ], + "title": "Kind", + "type": "string" + }, + "reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reason" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source" + }, + "source_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Path" + }, + "source_start_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Start Line" + }, + "tool": { + "title": "Tool", + "type": "string" + }, + "tool_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Id" + } + }, + "required": [ + "control", + "kind", + "reason", + "source", + "tool" + ], + "title": "ToolSurfaceControlChange", + "type": "object" + }, + "ToolSurfaceControlFact": { + "additionalProperties": false, + "properties": { + "kind": { + "enum": [ + "approval_policy", + "confirmation_policy", + "idempotency_evidence" + ], + "title": "Kind", + "type": "string" + }, + "reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reason" + }, + "source": { + "title": "Source", + "type": "string" + }, + "tool": { + "title": "Tool", + "type": "string" + }, + "tool_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Id" + } + }, + "required": [ + "kind", + "reason", + "source", + "tool" + ], + "title": "ToolSurfaceControlFact", + "type": "object" + }, + "ToolSurfaceDiff": { + "additionalProperties": false, + "properties": { + "base": { + "$ref": "#/$defs/ToolSurfaceDiffBase" + }, + "controls": { + "items": { + "$ref": "#/$defs/ToolSurfaceControlChange" + }, + "title": "Controls", + "type": "array" + }, + "enabled": { + "default": false, + "title": "Enabled", + "type": "boolean" + }, + "finding_deltas": { + "$ref": "#/$defs/ToolSurfaceFindingDeltas" + }, + "high_risk_effects": { + "items": { + "$ref": "#/$defs/ToolSurfaceHighRiskEffectChange" + }, + "title": "High Risk Effects", + "type": "array" + }, + "metadata_changes": { + "items": { + "$ref": "#/$defs/ToolSurfaceMetadataChange" + }, + "title": "Metadata Changes", + "type": "array" + }, + "notes": { + "items": { + "type": "string" + }, + "title": "Notes", + "type": "array" + }, + "policy_drift": { + "items": { + "$ref": "#/$defs/ToolSurfacePolicyDrift" + }, + "title": "Policy Drift", + "type": "array" + }, + "scopes": { + "items": { + "$ref": "#/$defs/ToolSurfaceScopeChange" + }, + "title": "Scopes", + "type": "array" + }, + "summary": { + "$ref": "#/$defs/ToolSurfaceDiffSummary" + }, + "tools": { + "items": { + "$ref": "#/$defs/ToolSurfaceToolChange" + }, + "title": "Tools", + "type": "array" + } + }, + "required": [ + "base", + "controls", + "enabled", + "finding_deltas", + "high_risk_effects", + "metadata_changes", + "notes", + "policy_drift", + "scopes", + "summary", + "tools" + ], + "title": "ToolSurfaceDiff", + "type": "object" + }, + "ToolSurfaceDiffBase": { + "additionalProperties": false, + "properties": { + "baseline_schema_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Baseline Schema Version" + }, + "kind": { + "default": "none", + "enum": [ + "none", + "report", + "baseline" + ], + "title": "Kind", + "type": "string" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "report_schema_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Report Schema Version" + } + }, + "required": [ + "baseline_schema_version", + "kind", + "path", + "report_schema_version" + ], + "title": "ToolSurfaceDiffBase", + "type": "object" + }, + "ToolSurfaceDiffSummary": { + "additionalProperties": false, + "properties": { + "accepted_debt": { + "default": 0, + "title": "Accepted Debt", + "type": "integer" + }, + "controls_added": { + "default": 0, + "title": "Controls Added", + "type": "integer" + }, + "controls_removed": { + "default": 0, + "title": "Controls Removed", + "type": "integer" + }, + "metadata_changes": { + "default": 0, + "title": "Metadata Changes", + "type": "integer" + }, + "new_findings": { + "default": 0, + "title": "New Findings", + "type": "integer" + }, + "new_high_risk_effects": { + "default": 0, + "title": "New High Risk Effects", + "type": "integer" + }, + "new_scopes": { + "default": 0, + "title": "New Scopes", + "type": "integer" + }, + "policy_drift_items": { + "default": 0, + "title": "Policy Drift Items", + "type": "integer" + }, + "removed_high_risk_effects": { + "default": 0, + "title": "Removed High Risk Effects", + "type": "integer" + }, + "removed_scopes": { + "default": 0, + "title": "Removed Scopes", + "type": "integer" + }, + "resolved_findings": { + "default": 0, + "title": "Resolved Findings", + "type": "integer" + }, + "tools_added": { + "default": 0, + "title": "Tools Added", + "type": "integer" + }, + "tools_changed": { + "default": 0, + "title": "Tools Changed", + "type": "integer" + }, + "tools_removed": { + "default": 0, + "title": "Tools Removed", + "type": "integer" + }, + "unchanged_findings": { + "default": 0, + "title": "Unchanged Findings", + "type": "integer" + } + }, + "required": [ + "accepted_debt", + "controls_added", + "controls_removed", + "metadata_changes", + "new_findings", + "new_high_risk_effects", + "new_scopes", + "policy_drift_items", + "removed_high_risk_effects", + "removed_scopes", + "resolved_findings", + "tools_added", + "tools_changed", + "tools_removed", + "unchanged_findings" + ], + "title": "ToolSurfaceDiffSummary", + "type": "object" + }, + "ToolSurfaceFacts": { + "additionalProperties": false, + "properties": { + "controls": { + "items": { + "$ref": "#/$defs/ToolSurfaceControlFact" + }, + "title": "Controls", + "type": "array" + }, + "policies": { + "items": { + "$ref": "#/$defs/ToolSurfacePolicyFact" + }, + "title": "Policies", + "type": "array" + }, + "scopes": { + "items": { + "$ref": "#/$defs/ToolSurfaceScopeFact" + }, + "title": "Scopes", + "type": "array" + }, + "tools": { + "items": { + "$ref": "#/$defs/ToolSurfaceToolFact" + }, + "title": "Tools", + "type": "array" + } + }, + "required": [ + "controls", + "policies", + "scopes", + "tools" + ], + "title": "ToolSurfaceFacts", + "type": "object" + }, + "ToolSurfaceFieldChange": { + "additionalProperties": false, + "properties": { + "after": { + "default": null, + "title": "After" + }, + "before": { + "default": null, + "title": "Before" + }, + "field": { + "title": "Field", + "type": "string" + } + }, + "required": [ + "after", + "before", + "field" + ], + "title": "ToolSurfaceFieldChange", + "type": "object" + }, + "ToolSurfaceFindingDeltaItem": { + "additionalProperties": false, + "properties": { + "baseline_status": { + "anyOf": [ + { + "enum": [ + "new", + "matched", + "resolved" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Baseline Status" + }, + "check_id": { + "title": "Check Id", + "type": "string" + }, + "fingerprint": { + "title": "Fingerprint", + "type": "string" + }, + "severity": { + "enum": [ + "info", + "low", + "medium", + "high", + "critical" + ], + "title": "Severity", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "tool_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Name" + } + }, + "required": [ + "baseline_status", + "check_id", + "fingerprint", + "severity", + "title", + "tool_name" + ], + "title": "ToolSurfaceFindingDeltaItem", + "type": "object" + }, + "ToolSurfaceFindingDeltas": { + "additionalProperties": false, + "properties": { + "accepted_debt": { + "items": { + "$ref": "#/$defs/ToolSurfaceFindingDeltaItem" + }, + "title": "Accepted Debt", + "type": "array" + }, + "new_findings": { + "items": { + "$ref": "#/$defs/ToolSurfaceFindingDeltaItem" + }, + "title": "New Findings", + "type": "array" + }, + "resolved_findings": { + "items": { + "$ref": "#/$defs/ToolSurfaceFindingDeltaItem" + }, + "title": "Resolved Findings", + "type": "array" + }, + "unchanged_findings": { + "items": { + "$ref": "#/$defs/ToolSurfaceFindingDeltaItem" + }, + "title": "Unchanged Findings", + "type": "array" + } + }, + "required": [ + "accepted_debt", + "new_findings", + "resolved_findings", + "unchanged_findings" + ], + "title": "ToolSurfaceFindingDeltas", + "type": "object" + }, + "ToolSurfaceHashes": { + "additionalProperties": false, + "properties": { + "annotations": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Annotations" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Description" + }, + "input_schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Input Schema" + }, + "output_schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Output Schema" + }, + "parameters": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Parameters" + }, + "source_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Ref" + } + }, + "required": [ + "annotations", + "description", + "input_schema", + "output_schema", + "parameters", + "source_ref" + ], + "title": "ToolSurfaceHashes", + "type": "object" + }, + "ToolSurfaceHighRiskEffectChange": { + "additionalProperties": false, + "properties": { + "kind": { + "enum": [ + "added", + "removed", + "changed" + ], + "title": "Kind", + "type": "string" + }, + "source_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Path" + }, + "source_start_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Start Line" + }, + "tag": { + "title": "Tag", + "type": "string" + }, + "tool": { + "title": "Tool", + "type": "string" + }, + "tool_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Id" + } + }, + "required": [ + "kind", + "tag", + "tool" + ], + "title": "ToolSurfaceHighRiskEffectChange", + "type": "object" + }, + "ToolSurfaceMetadataChange": { + "additionalProperties": false, + "properties": { + "after": { + "default": null, + "title": "After" + }, + "before": { + "default": null, + "title": "Before" + }, + "kind": { + "enum": [ + "added", + "removed", + "changed" + ], + "title": "Kind", + "type": "string" + }, + "metadata": { + "title": "Metadata", + "type": "string" + }, + "source_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Path" + }, + "source_start_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Start Line" + }, + "tool": { + "title": "Tool", + "type": "string" + }, + "tool_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Id" + } + }, + "required": [ + "after", + "before", + "kind", + "metadata", + "tool" + ], + "title": "ToolSurfaceMetadataChange", + "type": "object" + }, + "ToolSurfacePolicyDrift": { + "additionalProperties": false, + "properties": { + "after_hash": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "After Hash" + }, + "after_summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "After Summary" + }, + "before_hash": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Before Hash" + }, + "before_summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Before Summary" + }, + "key": { + "title": "Key", + "type": "string" + }, + "kind": { + "enum": [ + "added", + "removed", + "changed" + ], + "title": "Kind", + "type": "string" + }, + "policy_kind": { + "title": "Policy Kind", + "type": "string" + } + }, + "required": [ + "after_hash", + "after_summary", + "before_hash", + "before_summary", + "key", + "kind", + "policy_kind" + ], + "title": "ToolSurfacePolicyDrift", + "type": "object" + }, + "ToolSurfacePolicyFact": { + "additionalProperties": false, + "properties": { + "key": { + "title": "Key", + "type": "string" + }, + "kind": { + "title": "Kind", + "type": "string" + }, + "summary": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Summary" + }, + "value_hash": { + "title": "Value Hash", + "type": "string" + } + }, + "required": [ + "key", + "kind", + "summary", + "value_hash" + ], + "title": "ToolSurfacePolicyFact", + "type": "object" + }, + "ToolSurfaceScopeChange": { + "additionalProperties": false, + "properties": { + "broad": { + "default": false, + "title": "Broad", + "type": "boolean" + }, + "kind": { + "enum": [ + "added", + "removed", + "changed" + ], + "title": "Kind", + "type": "string" + }, + "scope": { + "title": "Scope", + "type": "string" + }, + "scope_kind": { + "enum": [ + "tool_required", + "manifest_declared" + ], + "title": "Scope Kind", + "type": "string" + }, + "tool_ids": { + "items": { + "type": "string" + }, + "title": "Tool Ids", + "type": "array" + }, + "tool_names": { + "items": { + "type": "string" + }, + "title": "Tool Names", + "type": "array" + } + }, + "required": [ + "broad", + "kind", + "scope", + "scope_kind", + "tool_names" + ], + "title": "ToolSurfaceScopeChange", + "type": "object" + }, + "ToolSurfaceScopeFact": { + "additionalProperties": false, + "properties": { + "broad": { + "default": false, + "title": "Broad", + "type": "boolean" + }, + "kind": { + "enum": [ + "tool_required", + "manifest_declared" + ], + "title": "Kind", + "type": "string" + }, + "scope": { + "title": "Scope", + "type": "string" + }, + "tool_ids": { + "items": { + "type": "string" + }, + "title": "Tool Ids", + "type": "array" + }, + "tool_names": { + "items": { + "type": "string" + }, + "title": "Tool Names", + "type": "array" + } + }, + "required": [ + "broad", + "kind", + "scope", + "tool_names" + ], + "title": "ToolSurfaceScopeFact", + "type": "object" + }, + "ToolSurfaceSummary": { + "properties": { + "high_risk_tools": { + "title": "High Risk Tools", + "type": "integer" + }, + "missing_descriptions": { + "default": 0, + "title": "Missing Descriptions", + "type": "integer" + }, + "sources": { + "additionalProperties": { + "type": "integer" + }, + "title": "Sources", + "type": "object" + }, + "total_tools": { + "title": "Total Tools", + "type": "integer" + }, + "wildcard_tools": { + "default": 0, + "title": "Wildcard Tools", + "type": "integer" + } + }, + "required": [ + "total_tools", + "high_risk_tools" + ], + "title": "ToolSurfaceSummary", + "type": "object" + }, + "ToolSurfaceToolChange": { + "additionalProperties": false, + "properties": { + "changes": { + "items": { + "$ref": "#/$defs/ToolSurfaceFieldChange" + }, + "title": "Changes", + "type": "array" + }, + "kind": { + "enum": [ + "added", + "removed", + "changed" + ], + "title": "Kind", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "provider": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Provider" + }, + "source_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Id" + }, + "source_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Path" + }, + "source_start_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Start Line" + }, + "source_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Type" + }, + "tool_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Tool Id" + } + }, + "required": [ + "changes", + "kind", + "name", + "source_id", + "source_type" + ], + "title": "ToolSurfaceToolChange", + "type": "object" + }, + "ToolSurfaceToolFact": { + "additionalProperties": false, + "properties": { + "auth_scopes": { + "items": { + "type": "string" + }, + "title": "Auth Scopes", + "type": "array" + }, + "extraction_confidence": { + "default": "low", + "enum": [ + "low", + "medium", + "high" + ], + "title": "Extraction Confidence", + "type": "string" + }, + "has_description": { + "default": false, + "title": "Has Description", + "type": "boolean" + }, + "hashes": { + "$ref": "#/$defs/ToolSurfaceHashes" + }, + "name": { + "title": "Name", + "type": "string" + }, + "owner": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Owner" + }, + "provider": { + "default": "", + "title": "Provider", + "type": "string" + }, + "risk_tags": { + "items": { + "type": "string" + }, + "title": "Risk Tags", + "type": "array" + }, + "source_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Id" + }, + "source_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Ref" + }, + "source_type": { + "title": "Source Type", + "type": "string" + }, + "tool_id": { + "default": "", + "title": "Tool Id", + "type": "string" + } + }, + "required": [ + "auth_scopes", + "extraction_confidence", + "has_description", + "hashes", + "name", + "owner", + "risk_tags", + "source_id", + "source_ref", + "source_type" + ], + "title": "ToolSurfaceToolFact", + "type": "object" + }, + "VerifierCapabilityDeltaSummary": { + "additionalProperties": false, + "description": "Counts of the capability delta, by direction.\n\nEqual by construction to the lengths of the matching\n``CapabilityChangeBlock`` member lists (roadmap \u00a78 required test:\nreviewer/verifier capability counts equal the underlying rollup).", + "properties": { + "added": { + "default": 0, + "title": "Added", + "type": "integer" + }, + "broadened": { + "default": 0, + "title": "Broadened", + "type": "integer" + }, + "narrowed": { + "default": 0, + "title": "Narrowed", + "type": "integer" + }, + "removed": { + "default": 0, + "title": "Removed", + "type": "integer" + } + }, + "required": [ + "added", + "broadened", + "narrowed", + "removed" + ], + "title": "VerifierCapabilityDeltaSummary", + "type": "object" + }, + "VerifierReasonCodeCount": { + "additionalProperties": false, + "description": "One ``(reason_code, count)`` pair for the verifier summary.\n\nA list of these (rather than a dict) keeps the ordering explicit and\nbyte-stable; the builder sorts them deterministically.", + "properties": { + "count": { + "default": 0, + "title": "Count", + "type": "integer" + }, + "reason_code": { + "title": "Reason Code", + "type": "string" + } + }, + "required": [ + "count", + "reason_code" + ], + "title": "VerifierReasonCodeCount", + "type": "object" + }, + "VerifierSummary": { + "additionalProperties": false, + "description": "Top-level verifier composition alias (Decision 5 shape).\n\nA byte-stable projection bundling the release verdict, finding counts,\nthe capability delta summary, and trust-root / acknowledgement flags\nfor one-fetch controller consumption. Composition only \u2014 it cannot\nintroduce a finding-independent blocker, and ``verdict`` MUST mirror\n``release_decision.decision`` (Principle 2; enforced by a property\ntest). Parallels ``agent_summary`` / ``reviewer_summary``.\n\n``top_reason_codes`` is capped at the top five by the builder, ranked\nseverity desc, count desc, then reason code asc.", + "properties": { + "by_reason_code": { + "additionalProperties": { + "type": "integer" + }, + "title": "By Reason Code", + "type": "object" + }, + "by_severity": { + "additionalProperties": { + "type": "integer" + }, + "title": "By Severity", + "type": "object" + }, + "capability_delta_summary": { + "$ref": "#/$defs/VerifierCapabilityDeltaSummary" + }, + "human_ack_required": { + "default": false, + "title": "Human Ack Required", + "type": "boolean" + }, + "human_ack_satisfied": { + "default": true, + "title": "Human Ack Satisfied", + "type": "boolean" + }, + "policy_weakened": { + "default": false, + "title": "Policy Weakened", + "type": "boolean" + }, + "protected_surface_touched": { + "default": false, + "title": "Protected Surface Touched", + "type": "boolean" + }, + "top_reason_codes": { + "items": { + "$ref": "#/$defs/VerifierReasonCodeCount" + }, + "title": "Top Reason Codes", + "type": "array" + }, + "verdict": { + "enum": [ + "blocked", + "review_required", + "insufficient_evidence", + "passed" + ], + "title": "Verdict", + "type": "string" + } + }, + "required": [ + "by_reason_code", + "by_severity", + "capability_delta_summary", + "human_ack_required", + "human_ack_satisfied", + "policy_weakened", + "protected_surface_touched", + "top_reason_codes", + "verdict" + ], + "title": "VerifierSummary", + "type": "object" + } + }, + "$id": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/report-schema.v0.34.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": true, + "description": "JSON Schema for the Agents Shipgate Tool-Use Readiness Report. Generated from agents_shipgate.schemas.report.ReadinessReport with post-processing to preserve the v0.5 public contract. Do not edit by hand.", + "properties": { + "action_surface_diff": { + "$ref": "#/$defs/ActionSurfaceDiff" + }, + "action_surface_facts": { + "$ref": "#/$defs/ActionSurfaceFacts" + }, + "agent": { + "additionalProperties": true, + "title": "Agent", + "type": "object" + }, + "agent_summary": { + "$ref": "#/$defs/AgentSummary" + }, + "anthropic_surface": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Anthropic Surface" + }, + "api_surface": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Api Surface" + }, + "baseline": { + "anyOf": [ + { + "$ref": "#/$defs/BaselineSummary" + }, + { + "type": "null" + } + ], + "default": null + }, + "binding_surface_diff": { + "$ref": "#/$defs/BindingSurfaceDiff" + }, + "binding_surface_facts": { + "$ref": "#/$defs/AgentBindingGraphAssessment" + }, + "capability_change": { + "$ref": "#/$defs/CapabilityChangeBlock" + }, + "capability_facts": { + "items": { + "$ref": "#/$defs/CapabilityFact" + }, + "title": "Capability Facts", + "type": "array" + }, + "capability_runtime_evidence": { + "$ref": "#/$defs/CapabilityRuntimeEvidence" + }, + "codex_plugin_surface": { + "anyOf": [ + { + "$ref": "#/$defs/CodexPluginSurface" + }, + { + "type": "null" + } + ], + "default": null + }, + "decision_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Decision Id" + }, + "declared_intentions": { + "items": { + "$ref": "#/$defs/DeclaredIntention" + }, + "title": "Declared Intentions", + "type": "array" + }, + "effective_policy": { + "$ref": "#/$defs/EffectivePolicy" + }, + "engine_requirement_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Engine Requirement Id" + }, + "environment": { + "additionalProperties": true, + "title": "Environment", + "type": "object" + }, + "findings": { + "items": { + "$ref": "#/$defs/Finding" + }, + "title": "Findings", + "type": "array" + }, + "frameworks": { + "additionalProperties": true, + "properties": { + "conductor": { + "additionalProperties": true, + "required": [ + "dynamic_tool_surface_count", + "human_checkpoint_count", + "llm_task_count", + "mcp_call_task_count", + "mcp_discovery_task_count", + "structurally_checkpointed_mcp_call_count", + "sub_workflow_task_count", + "task_count", + "unsupported_capability_count", + "warnings", + "workflow_count", + "workflow_file_count" + ], + "type": "object" + }, + "crewai": { + "additionalProperties": true, + "required": [ + "agent_count", + "class_tool_count", + "crew_count", + "dynamic_tool_surface_count", + "function_tool_count", + "prebuilt_tool_count", + "python_entrypoint_count", + "tool_inventory_file_count", + "warnings" + ], + "type": "object" + }, + "google_adk": { + "additionalProperties": true, + "required": [ + "agent_config_count", + "agent_count", + "callback_count", + "dynamic_toolset_count", + "eval_file_count", + "function_tool_count", + "long_running_tool_count", + "plugin_count", + "python_entrypoint_count", + "sub_agent_count", + "tool_inventory_file_count", + "toolset_count", + "trace_sample_count", + "warnings" + ], + "type": "object" + }, + "langchain": { + "additionalProperties": true, + "required": [ + "agent_tool_binding_count", + "dynamic_tool_surface_count", + "function_tool_count", + "python_entrypoint_count", + "structured_tool_count", + "tool_inventory_file_count", + "tool_node_count", + "warnings" + ], + "type": "object" + } + }, + "title": "Frameworks", + "type": "object" + }, + "generated_reports": { + "additionalProperties": { + "type": "string" + }, + "title": "Generated Reports", + "type": "object" + }, + "heuristics_filter": { + "$ref": "#/$defs/HeuristicsFilter" + }, + "human_ack": { + "$ref": "#/$defs/HumanAck" + }, + "input_set_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Input Set Id" + }, + "loaded_adapters": { + "items": { + "additionalProperties": true, + "required": [ + "distribution", + "name", + "runtime_errors", + "source_type", + "validation_errors", + "validation_status", + "value", + "version" + ], + "type": "object" + }, + "title": "Loaded Adapters", + "type": "array" + }, + "loaded_plugins": { + "items": { + "additionalProperties": true, + "required": [ + "check_id", + "distribution", + "name", + "runtime_errors", + "validation_errors", + "validation_status", + "value", + "version" + ], + "type": "object" + }, + "title": "Loaded Plugins", + "type": "array" + }, + "loaded_policy_packs": { + "items": { + "$ref": "#/$defs/LoadedPolicyPack" + }, + "title": "Loaded Policy Packs", + "type": "array" + }, + "manifest_dir": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Manifest Dir" + }, + "misalignments": { + "items": { + "$ref": "#/$defs/Misalignment" + }, + "title": "Misalignments", + "type": "array" + }, + "policy_audit": { + "$ref": "#/$defs/PolicyAudit" + }, + "policy_evidence_gaps": { + "items": { + "$ref": "#/$defs/EvidenceGap" + }, + "title": "Policy Evidence Gaps", + "type": "array" + }, + "privacy_audit": { + "$ref": "#/$defs/PrivacyAudit" + }, + "project": { + "additionalProperties": true, + "title": "Project", + "type": "object" + }, + "protected_surface_changes": { + "items": { + "$ref": "#/$defs/ProtectedSurfaceChange" + }, + "title": "Protected Surface Changes", + "type": "array" + }, + "recommended_actions": { + "items": { + "type": "string" + }, + "title": "Recommended Actions", + "type": "array" + }, + "release_consequence": { + "$ref": "#/$defs/ReleaseConsequence" + }, + "release_decision": { + "$ref": "#/$defs/ReleaseDecision" + }, + "report_schema_version": { + "const": "0.34" + }, + "request_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Request Id" + }, + "reviewer_summary": { + "$ref": "#/$defs/ReviewerSummary" + }, + "run_id": { + "title": "Run Id", + "type": "string" + }, + "schema_version": { + "const": "0.1" + }, + "source_warnings": { + "items": { + "type": "string" + }, + "title": "Source Warnings", + "type": "array" + }, + "subject_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Subject Id" + }, + "suggested_scenarios": { + "items": { + "$ref": "#/$defs/SuggestedScenario" + }, + "title": "Suggested Scenarios", + "type": "array" + }, + "summary": { + "$ref": "#/$defs/ReportSummary" + }, + "tool_catalog": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "title": "Tool Catalog", + "type": "array" + }, + "tool_inventory": { + "items": { + "additionalProperties": true, + "required": [ + "auth_scopes", + "confidence", + "name", + "risk_tags", + "source_type" + ], + "type": "object" + }, + "title": "Tool Inventory", + "type": "array" + }, + "tool_surface": { + "$ref": "#/$defs/ToolSurfaceSummary" + }, + "tool_surface_diff": { + "$ref": "#/$defs/ToolSurfaceDiff" + }, + "tool_surface_facts": { + "$ref": "#/$defs/ToolSurfaceFacts" + }, + "verifier_summary": { + "$ref": "#/$defs/VerifierSummary" + } + }, + "required": [ + "action_surface_diff", + "action_surface_facts", + "agent", + "agent_summary", + "capability_change", + "capability_facts", + "codex_plugin_surface", + "declared_intentions", + "effective_policy", + "environment", + "findings", + "frameworks", + "generated_reports", + "heuristics_filter", + "human_ack", + "loaded_adapters", + "loaded_plugins", + "loaded_policy_packs", + "misalignments", + "policy_audit", + "privacy_audit", + "project", + "protected_surface_changes", + "recommended_actions", + "release_consequence", + "release_decision", + "report_schema_version", + "reviewer_summary", + "run_id", + "schema_version", + "source_warnings", + "suggested_scenarios", + "summary", + "tool_inventory", + "tool_surface", + "tool_surface_diff", + "tool_surface_facts", + "verifier_summary" + ], + "title": "Agents Shipgate Readiness Report v0.34", + "type": "object" +} diff --git a/docs/report-sensitive-fields.json b/docs/report-sensitive-fields.json index 244268ca..043afdd7 100644 --- a/docs/report-sensitive-fields.json +++ b/docs/report-sensitive-fields.json @@ -14,6 +14,11 @@ {"surface": "report", "path": "schema_version", "classification": "public_control_metadata"}, {"surface": "report", "path": "report_schema_version", "classification": "public_control_metadata"}, {"surface": "report", "path": "run_id", "classification": "hash_only"}, + {"surface": "report", "path": "request_id", "classification": "hash_only"}, + {"surface": "report", "path": "subject_id", "classification": "hash_only"}, + {"surface": "report", "path": "input_set_id", "classification": "hash_only"}, + {"surface": "report", "path": "engine_requirement_id", "classification": "hash_only"}, + {"surface": "report", "path": "decision_id", "classification": "hash_only"}, {"surface": "report", "path": "manifest_dir", "classification": "path_metadata"}, {"surface": "report", "path": "project", "classification": "free_text"}, {"surface": "report", "path": "agent", "classification": "free_text"}, diff --git a/docs/report-v1-consolidation-rc.md b/docs/report-v1-consolidation-rc.md index bb858aa6..a6d610d0 100644 --- a/docs/report-v1-consolidation-rc.md +++ b/docs/report-v1-consolidation-rc.md @@ -2,7 +2,7 @@ > Status: **proposal** (no behavior change in this document). Target: > freeze a v1.0 report schema whose top-level surface stops growing. -> Written 2026-06; current runtime is `report_schema_version: "0.33"`. +> Written 2026-06; current runtime is `report_schema_version: "0.34"`. ## Problem diff --git a/docs/target-repo-agent-snippets.md b/docs/target-repo-agent-snippets.md index b7f8233a..586adc3d 100644 --- a/docs/target-repo-agent-snippets.md +++ b/docs/target-repo-agent-snippets.md @@ -75,7 +75,8 @@ verifier artifact can do so. For committed PR/CI verification, run `agents-shipgate verify --base origin/main --head HEAD --json` after making the base ref available; it never -fetches. Read `agents-shipgate-reports/agent-handoff.json` first for +fetches. Validate `agents-shipgate-reports/verification-receipt.json` first, +then read `agents-shipgate-reports/agent-handoff.json` for `gate.merge_verdict`, `gate.can_merge_without_human`, and `control`; then read `agents-shipgate-reports/verifier.json` for detailed control context, `agents-shipgate-reports/verify-run.json` for reproducibility metadata, and @@ -182,7 +183,8 @@ verifier artifact can do so. For committed PR/CI verification, run `agents-shipgate verify --base origin/main --head HEAD --json` after making the base ref available; it never -fetches. Read `agents-shipgate-reports/agent-handoff.json` first for +fetches. Validate `agents-shipgate-reports/verification-receipt.json` first, +then read `agents-shipgate-reports/agent-handoff.json` for `gate.merge_verdict`, `gate.can_merge_without_human`, and `control`; then read `agents-shipgate-reports/verifier.json` for detailed control context, `agents-shipgate-reports/verify-run.json` for reproducibility metadata, and @@ -267,7 +269,8 @@ For local verification, run: For committed PR/CI verification, run `agents-shipgate verify --base origin/main --head HEAD --json` after making the base ref available; it never -fetches. Read `agents-shipgate-reports/agent-handoff.json` first for +fetches. Validate `agents-shipgate-reports/verification-receipt.json` first, +then read `agents-shipgate-reports/agent-handoff.json` for `gate.merge_verdict`, `gate.can_merge_without_human`, and `control`; then read `agents-shipgate-reports/verifier.json` for detailed control context, `agents-shipgate-reports/verify-run.json` for reproducibility metadata, and diff --git a/docs/verification-artifact-manifest-schema.v1.json b/docs/verification-artifact-manifest-schema.v1.json new file mode 100644 index 00000000..bb7af5f4 --- /dev/null +++ b/docs/verification-artifact-manifest-schema.v1.json @@ -0,0 +1,78 @@ +{ + "$defs": { + "VerificationArtifactRef": { + "additionalProperties": false, + "properties": { + "media_type": { + "title": "Media Type", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "sha256": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Sha256", + "type": "string" + }, + "size_bytes": { + "minimum": 0, + "title": "Size Bytes", + "type": "integer" + } + }, + "required": [ + "path", + "sha256", + "size_bytes", + "media_type" + ], + "title": "VerificationArtifactRef", + "type": "object" + } + }, + "$id": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/verification-artifact-manifest-schema.v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "Content-addressed manifest of assembled verification artifacts.", + "properties": { + "artifact_set_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Artifact Set Id", + "type": "string" + }, + "artifacts": { + "additionalProperties": { + "$ref": "#/$defs/VerificationArtifactRef" + }, + "minProperties": 1, + "title": "Artifacts", + "type": "object" + }, + "decision_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Decision Id", + "type": "string" + }, + "request_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Request Id", + "type": "string" + }, + "schema_version": { + "const": "shipgate.verification_artifact_manifest/v1", + "default": "shipgate.verification_artifact_manifest/v1", + "title": "Schema Version", + "type": "string" + } + }, + "required": [ + "artifact_set_id", + "request_id", + "decision_id", + "artifacts" + ], + "title": "Agents Shipgate Verification Artifact Manifest v1", + "type": "object" +} diff --git a/docs/verification-plan-schema.v1.json b/docs/verification-plan-schema.v1.json new file mode 100644 index 00000000..a8cc5669 --- /dev/null +++ b/docs/verification-plan-schema.v1.json @@ -0,0 +1,445 @@ +{ + "$defs": { + "VerificationBlob": { + "additionalProperties": false, + "properties": { + "git_blob_oid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Git Blob Oid" + }, + "path": { + "title": "Path", + "type": "string" + }, + "sha256": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Sha256", + "type": "string" + }, + "size_bytes": { + "minimum": 0, + "title": "Size Bytes", + "type": "integer" + }, + "source": { + "enum": [ + "git_blob", + "worktree", + "generated", + "external_input", + "artifact" + ], + "title": "Source", + "type": "string" + } + }, + "required": [ + "path", + "sha256", + "size_bytes", + "source" + ], + "title": "VerificationBlob", + "type": "object" + }, + "VerificationEngineRequirement": { + "additionalProperties": false, + "properties": { + "adapter_set_sha256": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Adapter Set Sha256", + "type": "string" + }, + "dependency_set_sha256": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Dependency Set Sha256", + "type": "string" + }, + "engine_distribution_sha256": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Engine Distribution Sha256", + "type": "string" + }, + "engine_requirement_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Engine Requirement Id", + "type": "string" + }, + "package": { + "default": "agents-shipgate", + "title": "Package", + "type": "string" + }, + "platform": { + "title": "Platform", + "type": "string" + }, + "plugin_set_sha256": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Plugin Set Sha256", + "type": "string" + }, + "policy_catalog_sha256": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Policy Catalog Sha256", + "type": "string" + }, + "python_implementation": { + "title": "Python Implementation", + "type": "string" + }, + "python_version": { + "title": "Python Version", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "engine_requirement_id", + "version", + "python_implementation", + "python_version", + "platform", + "engine_distribution_sha256", + "dependency_set_sha256", + "adapter_set_sha256", + "plugin_set_sha256", + "policy_catalog_sha256" + ], + "title": "VerificationEngineRequirement", + "type": "object" + }, + "VerificationGitSubject": { + "additionalProperties": false, + "properties": { + "base_commit_sha": { + "anyOf": [ + { + "pattern": "^[0-9a-f]{40,64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Base Commit Sha" + }, + "base_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Base Ref" + }, + "base_tree_sha": { + "anyOf": [ + { + "pattern": "^[0-9a-f]{40,64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Base Tree Sha" + }, + "head_commit_sha": { + "anyOf": [ + { + "pattern": "^[0-9a-f]{40,64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Head Commit Sha" + }, + "head_ref": { + "title": "Head Ref", + "type": "string" + }, + "head_tree_sha": { + "anyOf": [ + { + "pattern": "^[0-9a-f]{40,64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Head Tree Sha" + }, + "merge_base_sha": { + "anyOf": [ + { + "pattern": "^[0-9a-f]{40,64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Merge Base Sha" + }, + "repository_id": { + "title": "Repository Id", + "type": "string" + }, + "snapshot_kind": { + "enum": [ + "committed_tree", + "worktree_overlay" + ], + "title": "Snapshot Kind", + "type": "string" + }, + "source_head_commit_sha": { + "anyOf": [ + { + "pattern": "^[0-9a-f]{40,64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Head Commit Sha" + }, + "worktree_overlay_sha256": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Worktree Overlay Sha256" + } + }, + "required": [ + "repository_id", + "head_ref", + "snapshot_kind" + ], + "title": "VerificationGitSubject", + "type": "object" + }, + "VerificationInputSet": { + "additionalProperties": false, + "properties": { + "baseline": { + "anyOf": [ + { + "$ref": "#/$defs/VerificationBlob" + }, + { + "type": "null" + } + ], + "default": null + }, + "changed_files": { + "items": { + "$ref": "#/$defs/VerificationBlob" + }, + "title": "Changed Files", + "type": "array" + }, + "changed_paths": { + "items": { + "type": "string" + }, + "title": "Changed Paths", + "type": "array" + }, + "config": { + "$ref": "#/$defs/VerificationBlob" + }, + "diff": { + "$ref": "#/$defs/VerificationBlob" + }, + "diff_from": { + "anyOf": [ + { + "$ref": "#/$defs/VerificationBlob" + }, + { + "type": "null" + } + ], + "default": null + }, + "evaluation_date": { + "title": "Evaluation Date", + "type": "string" + }, + "input_set_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Input Set Id", + "type": "string" + }, + "options": { + "additionalProperties": true, + "title": "Options", + "type": "object" + }, + "policy_packs": { + "items": { + "$ref": "#/$defs/VerificationBlob" + }, + "title": "Policy Packs", + "type": "array" + }, + "tool_sources": { + "items": { + "$ref": "#/$defs/VerificationBlob" + }, + "title": "Tool Sources", + "type": "array" + } + }, + "required": [ + "input_set_id", + "evaluation_date", + "config", + "diff" + ], + "title": "VerificationInputSet", + "type": "object" + }, + "VerificationSubject": { + "additionalProperties": false, + "properties": { + "git": { + "$ref": "#/$defs/VerificationGitSubject" + }, + "subject_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Subject Id", + "type": "string" + } + }, + "required": [ + "subject_id", + "git" + ], + "title": "VerificationSubject", + "type": "object" + }, + "VerificationTask": { + "additionalProperties": false, + "properties": { + "input_paths": { + "items": { + "type": "string" + }, + "title": "Input Paths", + "type": "array" + }, + "kind": { + "enum": [ + "extract", + "normalize", + "evaluate" + ], + "title": "Kind", + "type": "string" + }, + "shard": { + "minimum": 0, + "title": "Shard", + "type": "integer" + }, + "shard_count": { + "minimum": 1, + "title": "Shard Count", + "type": "integer" + }, + "task_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Task Id", + "type": "string" + } + }, + "required": [ + "task_id", + "kind", + "shard", + "shard_count" + ], + "title": "VerificationTask", + "type": "object" + } + }, + "$id": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/verification-plan-schema.v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "Content-addressed immutable verification request plan.", + "properties": { + "engine": { + "$ref": "#/$defs/VerificationEngineRequirement" + }, + "inputs": { + "$ref": "#/$defs/VerificationInputSet" + }, + "request_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Request Id", + "type": "string" + }, + "schema_version": { + "const": "shipgate.verification_plan/v1", + "default": "shipgate.verification_plan/v1", + "title": "Schema Version", + "type": "string" + }, + "subject": { + "$ref": "#/$defs/VerificationSubject" + }, + "tasks": { + "items": { + "$ref": "#/$defs/VerificationTask" + }, + "maxItems": 1, + "minItems": 1, + "title": "Tasks", + "type": "array" + } + }, + "required": [ + "request_id", + "subject", + "inputs", + "engine", + "tasks" + ], + "title": "Agents Shipgate Verification Plan v1", + "type": "object" +} diff --git a/docs/verification-receipt-schema.v1.json b/docs/verification-receipt-schema.v1.json new file mode 100644 index 00000000..22de62bd --- /dev/null +++ b/docs/verification-receipt-schema.v1.json @@ -0,0 +1,201 @@ +{ + "$defs": { + "VerificationArtifactManifest": { + "additionalProperties": false, + "properties": { + "artifact_set_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Artifact Set Id", + "type": "string" + }, + "artifacts": { + "additionalProperties": { + "$ref": "#/$defs/VerificationArtifactRef" + }, + "minProperties": 1, + "title": "Artifacts", + "type": "object" + }, + "decision_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Decision Id", + "type": "string" + }, + "request_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Request Id", + "type": "string" + }, + "schema_version": { + "const": "shipgate.verification_artifact_manifest/v1", + "default": "shipgate.verification_artifact_manifest/v1", + "title": "Schema Version", + "type": "string" + } + }, + "required": [ + "artifact_set_id", + "request_id", + "decision_id", + "artifacts" + ], + "title": "VerificationArtifactManifest", + "type": "object" + }, + "VerificationArtifactRef": { + "additionalProperties": false, + "properties": { + "media_type": { + "title": "Media Type", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "sha256": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Sha256", + "type": "string" + }, + "size_bytes": { + "minimum": 0, + "title": "Size Bytes", + "type": "integer" + } + }, + "required": [ + "path", + "sha256", + "size_bytes", + "media_type" + ], + "title": "VerificationArtifactRef", + "type": "object" + } + }, + "$id": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/verification-receipt-schema.v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "Terminal closure record written after every artifact is finalized.", + "properties": { + "artifact_manifest": { + "$ref": "#/$defs/VerificationArtifactManifest" + }, + "artifact_set_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Artifact Set Id", + "type": "string" + }, + "attempt_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Attempt Id" + }, + "can_merge_without_human": { + "title": "Can Merge Without Human", + "type": "boolean" + }, + "decision": { + "anyOf": [ + { + "enum": [ + "passed", + "review_required", + "insufficient_evidence", + "blocked" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Decision" + }, + "decision_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Decision Id", + "type": "string" + }, + "engine_requirement_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Engine Requirement Id", + "type": "string" + }, + "executor_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Executor Id", + "type": "string" + }, + "input_set_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Input Set Id", + "type": "string" + }, + "merge_verdict": { + "enum": [ + "mergeable", + "human_review_required", + "insufficient_evidence", + "blocked", + "unknown" + ], + "title": "Merge Verdict", + "type": "string" + }, + "receipt_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Receipt Id", + "type": "string" + }, + "request_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Request Id", + "type": "string" + }, + "schema_version": { + "const": "shipgate.verification_receipt/v1", + "default": "shipgate.verification_receipt/v1", + "title": "Schema Version", + "type": "string" + }, + "subject_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Subject Id", + "type": "string" + }, + "unit_result_ids": { + "items": { + "type": "string" + }, + "minItems": 1, + "title": "Unit Result Ids", + "type": "array" + } + }, + "required": [ + "receipt_id", + "request_id", + "subject_id", + "input_set_id", + "engine_requirement_id", + "executor_id", + "decision_id", + "artifact_set_id", + "unit_result_ids", + "merge_verdict", + "can_merge_without_human", + "artifact_manifest" + ], + "title": "Agents Shipgate Verification Receipt v1", + "type": "object" +} diff --git a/docs/verification-reproducibility.md b/docs/verification-reproducibility.md new file mode 100644 index 00000000..089a7674 --- /dev/null +++ b/docs/verification-reproducibility.md @@ -0,0 +1,141 @@ +# Verification Identity and Reproduction + +Agents Shipgate `0.16.0b6` makes a verification request, its execution, its +decision, and its emitted files independently identifiable. This closes the +previous ambiguity where a mutable branch name, a local cache hit, or an +unbound collection of JSON files could be presented as the same verification. + +The terminal trust root is +`agents-shipgate-reports/verification-receipt.json`, validated by +[`verification-receipt-schema.v1.json`](verification-receipt-schema.v1.json). +Read it before the handoff or report. A receipt is written last and only after +all referenced artifacts exist. + +## Identity graph + +```text +Git subject ──> subject_id ──┐ +input blobs ─> input_set_id ├─> request_id ─> unit_result_ids +engine set ──> engine_id ───┤ │ +tasks ───────> task_ids ────┘ v + decision_id + artifact_set_id + │ + v + receipt_id +``` + +All IDs use SHA-256 over canonical UTF-8 JSON with sorted keys. The request +binds: + +- resolved base, head, tree, merge-base, and source/evaluated commit facts; +- an exact committed tree or a hashed working-tree overlay; +- the manifest, tool sources, policy packs, baseline, comparison report, + changed-file content, diff content, evaluation date, and behavior-affecting + options; +- the Agents Shipgate version and installed package-content digest, + Python/runtime requirements, installed dependency RECORD closure, adapter + set, plugin distribution set, and policy catalog; and +- the normalized task list. + +Committed snapshots are materialized from Git objects with `git ls-tree` and +`git cat-file`. They do not use `git archive`, so `.gitattributes` +`export-ignore` and `export-subst` cannot change the evaluated bytes. Symlinks +and submodules fail closed for archived verification inputs. + +`attempt_id` is diagnostic and deliberately excluded from `receipt_id`. +Changing an authoritative input, result, decision, or artifact changes the +corresponding content ID. Reusing a base-scan cache does not change the public +verification identity or artifacts, and cached reports are accepted only when +their sidecar content hash validates. + +## Local verification and portable execution validation + +Normal `agents-shipgate verify` emits the complete graph automatically: + +```text +verification-plan.json +verification-input.diff +verification-base-report.json (when a base comparison is evaluated) +verification-unit-result.json +verify-run.json +verifier.json +agent-handoff.json +verification-artifacts.json +verification-receipt.json +``` + +`verification prepare` can create a portable request plan and exact diff before +evaluation for schedulers that want to inspect or transport inputs: + +```bash +agents-shipgate verification prepare --workspace . --config shipgate.yaml \ + --base origin/main --head HEAD \ + --out agents-shipgate-reports/verification-plan.json +``` + +Preparation does not evaluate policy and cannot produce a verifier or receipt. +The current v1 CLI uses the plan emitted by a normal verifier run when replacing +its local execution-validation unit with a transported worker result: + +```bash +agents-shipgate verify --workspace . --config shipgate.yaml \ + --base origin/main --head HEAD --ci-mode advisory --format json + +agents-shipgate verification worker \ + --plan agents-shipgate-reports/verification-plan.json \ + --diff agents-shipgate-reports/verification-input.diff \ + --workspace . \ + --out agents-shipgate-reports/verification-unit-result.json + +agents-shipgate verification assemble \ + --plan agents-shipgate-reports/verification-plan.json \ + --unit-result agents-shipgate-reports/verification-unit-result.json \ + --verifier agents-shipgate-reports/verifier.json \ + --artifacts-root agents-shipgate-reports \ + --out agents-shipgate-reports/verification-receipt.json + +agents-shipgate verification reproduce \ + --receipt agents-shipgate-reports/verification-receipt.json \ + --artifacts-root agents-shipgate-reports +``` + +A worker validates the installed engine and every supplied input hash, +including the exact diff bytes and bundled base report, and may emit normalized +intermediate representation only. The schema rejects worker-provided +`decision`, `merge_verdict`, `control`, or `can_merge_without_human` fields. +The assembler is the sole decision closure and rejects missing, failed, +foreign-request, duplicate, or wrong-task results. + +The v1 protocol intentionally defines one deterministic `evaluate` task. The +worker command validates execution compatibility and input transport; it does +not offload the scan or policy engine. The verifier remains the sole policy +engine and the assembler only re-closes its decision over the supplied worker +unit. Therefore this release establishes a portable fail-closed execution +boundary, not distributed evaluation, parallel speedup, or arbitrary sharding. +Those claims require deterministic extraction/evaluation IR, partitioning, and +cross-executor equivalence qualification that are not present in v1. + +## Time and GitHub Actions + +Expiry-sensitive baseline and severity-override evaluation uses the plan's +`evaluation_date`, derived from the evaluated head commit unless explicitly +declared during plan preparation. It never depends on a worker's wall clock. + +The GitHub Action evaluates `${{ github.sha }}` by default and separately +records `${{ github.event.pull_request.head.sha }}` when present. This prevents +the source PR head from being confused with GitHub's synthetic merge commit. +The Action exports `request_id`, `receipt_id`, `decision_id`, +`artifact_set_id`, and `verification_receipt_json` only after validating the +receipt and all referenced hashes. + +## Boundaries + +This contract proves artifact and input identity and closure of one receipt +graph around a deterministic static evaluation. It does not independently +prove that an executor performed the bound evaluation, runtime agent behavior, +remote service state, credential enforcement, build provenance, or that an +executor is trustworthy merely because it self-reports a matching descriptor. +Signatures, trusted execution, and CI identity remain separate distribution +controls. Legacy verify-run, handoff, attestation, registry, and organization +artifacts remain readable, but only current artifacts carrying the receipt +graph qualify as reproducible verification evidence. diff --git a/docs/verification-unit-result-schema.v1.json b/docs/verification-unit-result-schema.v1.json new file mode 100644 index 00000000..09d0513f --- /dev/null +++ b/docs/verification-unit-result-schema.v1.json @@ -0,0 +1,90 @@ +{ + "$defs": { + "VerificationExecutor": { + "additionalProperties": false, + "properties": { + "engine_requirement_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Engine Requirement Id", + "type": "string" + }, + "executor_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Executor Id", + "type": "string" + }, + "runtime_sha256": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Runtime Sha256", + "type": "string" + } + }, + "required": [ + "executor_id", + "engine_requirement_id", + "runtime_sha256" + ], + "title": "VerificationExecutor", + "type": "object" + } + }, + "$id": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/verification-unit-result-schema.v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "Decision-free normalized worker output for offline assembly.", + "properties": { + "executor": { + "$ref": "#/$defs/VerificationExecutor" + }, + "issues": { + "items": { + "type": "string" + }, + "title": "Issues", + "type": "array" + }, + "normalized_ir": { + "additionalProperties": true, + "title": "Normalized Ir", + "type": "object" + }, + "request_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Request Id", + "type": "string" + }, + "schema_version": { + "const": "shipgate.verification_unit_result/v1", + "default": "shipgate.verification_unit_result/v1", + "title": "Schema Version", + "type": "string" + }, + "status": { + "enum": [ + "succeeded", + "failed" + ], + "title": "Status", + "type": "string" + }, + "task_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Task Id", + "type": "string" + }, + "unit_result_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Unit Result Id", + "type": "string" + } + }, + "required": [ + "unit_result_id", + "request_id", + "task_id", + "executor", + "status" + ], + "title": "Agents Shipgate Verification Unit Result v1", + "type": "object" +} diff --git a/docs/verifier-schema.v0.5.json b/docs/verifier-schema.v0.5.json new file mode 100644 index 00000000..f13fe9c5 --- /dev/null +++ b/docs/verifier-schema.v0.5.json @@ -0,0 +1,2311 @@ +{ + "$defs": { + "AgentActionRequiredControl": { + "additionalProperties": false, + "description": "Non-terminal state with one exact coding-agent-owned next step.", + "properties": { + "allowed_next_commands": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Allowed Next Commands", + "type": "array" + }, + "completion_allowed": { + "const": false, + "default": false, + "title": "Completion Allowed", + "type": "boolean" + }, + "human_review": { + "$ref": "#/$defs/NoHumanReview" + }, + "must_stop": { + "const": false, + "default": false, + "title": "Must Stop", + "type": "boolean" + }, + "next_action": { + "$ref": "#/$defs/CodingAgentAction" + }, + "reason": { + "minLength": 1, + "title": "Reason", + "type": "string" + }, + "state": { + "const": "agent_action_required", + "title": "State", + "type": "string" + }, + "stop_reason": { + "default": null, + "title": "Stop Reason", + "type": "null" + }, + "verify_required": { + "default": false, + "title": "Verify Required", + "type": "boolean" + } + }, + "required": [ + "state", + "reason", + "completion_allowed", + "must_stop", + "verify_required", + "next_action", + "allowed_next_commands", + "human_review", + "stop_reason" + ], + "title": "AgentActionRequiredControl", + "type": "object" + }, + "AgentControl": { + "discriminator": { + "mapping": { + "agent_action_required": "#/$defs/AgentActionRequiredControl", + "complete": "#/$defs/CompleteAgentControl", + "human_review_required": "#/$defs/HumanReviewRequiredControl" + }, + "propertyName": "state" + }, + "oneOf": [ + { + "$ref": "#/$defs/CompleteAgentControl" + }, + { + "$ref": "#/$defs/AgentActionRequiredControl" + }, + { + "$ref": "#/$defs/HumanReviewRequiredControl" + } + ] + }, + "BaselineDelta": { + "properties": { + "enabled": { + "title": "Enabled", + "type": "boolean" + }, + "matched_count": { + "default": 0, + "title": "Matched Count", + "type": "integer" + }, + "new_count": { + "default": 0, + "title": "New Count", + "type": "integer" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "resolved_count": { + "default": 0, + "title": "Resolved Count", + "type": "integer" + } + }, + "required": [ + "enabled" + ], + "title": "BaselineDelta", + "type": "object" + }, + "BindingCoverageDecision": { + "properties": { + "gap_count": { + "default": 0, + "title": "Gap Count", + "type": "integer" + }, + "pass_eligible": { + "default": false, + "title": "Pass Eligible", + "type": "boolean" + }, + "possible_tools": { + "default": 0, + "title": "Possible Tools", + "type": "integer" + }, + "reachable_tools": { + "default": 0, + "title": "Reachable Tools", + "type": "integer" + }, + "reason_counts": { + "additionalProperties": { + "type": "integer" + }, + "title": "Reason Counts", + "type": "object" + }, + "total_catalog_tools": { + "default": 0, + "title": "Total Catalog Tools", + "type": "integer" + }, + "unbound_tools": { + "default": 0, + "title": "Unbound Tools", + "type": "integer" + } + }, + "title": "BindingCoverageDecision", + "type": "object" + }, + "CodingAgentAction": { + "discriminator": { + "mapping": { + "configure": "#/$defs/CodingAgentCommandAction", + "discover": "#/$defs/CodingAgentCommandAction", + "fetch_base": "#/$defs/CodingAgentFetchBaseAction", + "initialize": "#/$defs/CodingAgentCommandAction", + "install": "#/$defs/CodingAgentCommandAction", + "repair": "#/$defs/CodingAgentCommandAction", + "rerun": "#/$defs/CodingAgentCommandAction", + "verify": "#/$defs/CodingAgentCommandAction" + }, + "propertyName": "kind" + }, + "oneOf": [ + { + "$ref": "#/$defs/CodingAgentCommandAction" + }, + { + "$ref": "#/$defs/CodingAgentFetchBaseAction" + } + ] + }, + "CodingAgentCommandAction": { + "additionalProperties": false, + "description": "An executable, exact next step owned by the coding agent.", + "properties": { + "actor": { + "const": "coding_agent", + "default": "coding_agent", + "title": "Actor", + "type": "string" + }, + "command": { + "minLength": 1, + "title": "Command", + "type": "string" + }, + "expects": { + "default": null, + "title": "Expects", + "type": "null" + }, + "kind": { + "enum": [ + "verify", + "discover", + "configure", + "initialize", + "repair", + "install", + "rerun" + ], + "title": "Kind", + "type": "string" + }, + "why": { + "minLength": 1, + "title": "Why", + "type": "string" + } + }, + "required": [ + "actor", + "kind", + "command", + "expects", + "why" + ], + "title": "CodingAgentCommandAction", + "type": "object" + }, + "CodingAgentFetchBaseAction": { + "additionalProperties": false, + "description": "A structured input request when an exact fetch command is unavailable.\n\nShipgate never fetches refs itself. ``expects`` therefore names the exact\nref or artifact a caller must make available before rerunning verification.", + "properties": { + "actor": { + "const": "coding_agent", + "default": "coding_agent", + "title": "Actor", + "type": "string" + }, + "command": { + "default": null, + "title": "Command", + "type": "null" + }, + "expects": { + "minLength": 1, + "title": "Expects", + "type": "string" + }, + "kind": { + "const": "fetch_base", + "title": "Kind", + "type": "string" + }, + "why": { + "minLength": 1, + "title": "Why", + "type": "string" + } + }, + "required": [ + "actor", + "kind", + "command", + "expects", + "why" + ], + "title": "CodingAgentFetchBaseAction", + "type": "object" + }, + "CompleteAgentControl": { + "additionalProperties": false, + "description": "Terminal state: the coding agent may report the task complete.", + "properties": { + "allowed_next_commands": { + "items": { + "minLength": 1, + "type": "string" + }, + "maxItems": 0, + "title": "Allowed Next Commands", + "type": "array" + }, + "completion_allowed": { + "const": true, + "default": true, + "title": "Completion Allowed", + "type": "boolean" + }, + "human_review": { + "$ref": "#/$defs/NoHumanReview" + }, + "must_stop": { + "const": false, + "default": false, + "title": "Must Stop", + "type": "boolean" + }, + "next_action": { + "default": null, + "title": "Next Action", + "type": "null" + }, + "reason": { + "minLength": 1, + "title": "Reason", + "type": "string" + }, + "state": { + "const": "complete", + "title": "State", + "type": "string" + }, + "stop_reason": { + "default": null, + "title": "Stop Reason", + "type": "null" + }, + "verify_required": { + "const": false, + "default": false, + "title": "Verify Required", + "type": "boolean" + } + }, + "required": [ + "state", + "reason", + "completion_allowed", + "must_stop", + "verify_required", + "next_action", + "allowed_next_commands", + "human_review", + "stop_reason" + ], + "title": "CompleteAgentControl", + "type": "object" + }, + "ContributionRule": { + "additionalProperties": false, + "description": "Per-finding audit row explaining how a finding contributed to the\nrelease decision.\n\nAdditive in v0.17. Every finding in `report.findings` produces\nexactly one ContributionRule. Reading the contribution rule is\nsufficient to predict the gate outcome for that finding without\nre-deriving the decision logic; the set of valid `(rule, category)`\npairs is the contract documented in STABILITY.md \"Release decision\ntruth table\".", + "properties": { + "category": { + "enum": [ + "blocker", + "review_item", + "excluded" + ], + "title": "Category", + "type": "string" + }, + "check_id": { + "title": "Check Id", + "type": "string" + }, + "finding_id": { + "title": "Finding Id", + "type": "string" + }, + "fingerprint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Fingerprint" + }, + "rationale": { + "title": "Rationale", + "type": "string" + }, + "rule": { + "enum": [ + "policy_block_new", + "severity_block_new", + "policy_baseline_accepted", + "severity_baseline_accepted", + "review_required", + "sub_threshold", + "unsupported_evidence", + "suppressed" + ], + "title": "Rule", + "type": "string" + } + }, + "required": [ + "finding_id", + "check_id", + "category", + "rule", + "rationale" + ], + "title": "ContributionRule", + "type": "object" + }, + "EvidenceCoverageDecision": { + "properties": { + "binding_coverage": { + "$ref": "#/$defs/BindingCoverageDecision" + }, + "evidence_gaps": { + "items": { + "$ref": "#/$defs/EvidenceGap" + }, + "title": "Evidence Gaps", + "type": "array" + }, + "human_review_recommended": { + "title": "Human Review Recommended", + "type": "boolean" + }, + "identity_coverage": { + "$ref": "#/$defs/IdentityCoverageDecision" + }, + "level": { + "title": "Level", + "type": "string" + }, + "low_confidence_tool_count": { + "title": "Low Confidence Tool Count", + "type": "integer" + }, + "policy_gap_count": { + "default": 0, + "title": "Policy Gap Count", + "type": "integer" + }, + "semantic_coverage": { + "$ref": "#/$defs/SemanticCoverageDecision" + }, + "source_warning_count": { + "title": "Source Warning Count", + "type": "integer" + } + }, + "required": [ + "level", + "human_review_recommended", + "source_warning_count", + "low_confidence_tool_count" + ], + "title": "EvidenceCoverageDecision", + "type": "object" + }, + "EvidenceGap": { + "description": "v0.26: one structured row per measurable evidence gap.\n\n``insufficient_evidence`` previously diagnosed without prescribing;\neach gap names the degraded subject and the specific next action\nthat raises extraction confidence. Purely explanatory \u2014 gating\nstill uses only the counts (the gap list is a projection of them).", + "properties": { + "kind": { + "enum": [ + "low_confidence_tool", + "source_warning", + "incomplete_surface", + "missing_effect_evidence", + "inferred_effect_only", + "conflicting_effect_evidence", + "missing_authority_evidence", + "partial_authority_evidence", + "conflicting_authority_evidence", + "invalid_semantic_annotation", + "incomplete_tool_identity", + "conflicting_tool_identity", + "unresolved_tool_selector", + "ambiguous_tool_selector", + "ambiguous_legacy_tool_identity", + "invalid_tool_binding", + "missing_binding_evidence", + "partial_binding_evidence", + "conflicting_binding_evidence", + "ambiguous_root_agent", + "unresolved_agent_binding", + "unresolved_bound_tool", + "incomplete_handoff_graph", + "invalid_binding_annotation", + "invalid_evidence_provenance", + "inferred_policy_applicability", + "mixed_policy_evidence", + "unknown_policy_evidence", + "conflicting_policy_evidence" + ], + "title": "Kind", + "type": "string" + }, + "next_action": { + "$ref": "#/$defs/EvidenceGapAction" + }, + "source_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Ref" + }, + "source_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Type" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "why": { + "title": "Why", + "type": "string" + } + }, + "required": [ + "kind", + "subject", + "why", + "next_action" + ], + "title": "EvidenceGap", + "type": "object" + }, + "EvidenceGapAction": { + "description": "One concrete, mechanically-executable step that closes a gap.\n\nMirrors the agent-mode ``next_actions[]`` error shape\n(``kind``/``command``/``path``/``why``/``expects``) so agents reuse\none routing vocabulary across error recovery and evidence repair.", + "properties": { + "accepted_values": { + "items": { + "type": "string" + }, + "title": "Accepted Values", + "type": "array" + }, + "auto_apply": { + "const": false, + "default": false, + "title": "Auto Apply", + "type": "boolean" + }, + "command": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Command" + }, + "declaration_template": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Declaration Template" + }, + "expects": { + "title": "Expects", + "type": "string" + }, + "kind": { + "enum": [ + "declare_tool_inventory", + "provide_source", + "review_warning", + "declare_action_effect", + "declare_action_authority", + "provide_complete_inventory", + "resolve_semantic_conflict", + "declare_source_identity", + "qualify_tool_selector", + "provide_tool_binding", + "resolve_tool_identity_conflict", + "regenerate_identity_artifact", + "declare_agent_root", + "declare_agent_bindings", + "provide_static_binding_source", + "provide_complete_binding_graph", + "resolve_binding_conflict", + "regenerate_binding_artifact", + "provide_policy_evidence", + "review_policy_evidence", + "resolve_policy_evidence_conflict" + ], + "title": "Kind", + "type": "string" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "requires_human_review": { + "const": true, + "default": true, + "title": "Requires Human Review", + "type": "boolean" + }, + "suggested_patch_kind": { + "const": "manual", + "default": "manual", + "title": "Suggested Patch Kind", + "type": "string" + }, + "why": { + "title": "Why", + "type": "string" + } + }, + "required": [ + "kind", + "why", + "expects" + ], + "title": "EvidenceGapAction", + "type": "object" + }, + "FailPolicy": { + "properties": { + "ci_mode": { + "title": "Ci Mode", + "type": "string" + }, + "exit_code": { + "title": "Exit Code", + "type": "integer" + }, + "fail_on": { + "items": { + "enum": [ + "info", + "low", + "medium", + "high", + "critical" + ], + "type": "string" + }, + "title": "Fail On", + "type": "array" + }, + "new_findings_only": { + "default": false, + "title": "New Findings Only", + "type": "boolean" + }, + "would_fail_ci": { + "title": "Would Fail Ci", + "type": "boolean" + } + }, + "required": [ + "ci_mode", + "would_fail_ci", + "exit_code" + ], + "title": "FailPolicy", + "type": "object" + }, + "FindingSupport": { + "additionalProperties": false, + "description": "Authoritative support for finding confidence and release contribution.\n\nRule metadata may request a severity or block, but it cannot upgrade the\nunderlying evidence. ``support_hash`` binds baselines and audit surfaces\nto the predicate evidence that actually made the finding eligible.", + "properties": { + "blocking_eligible": { + "default": false, + "title": "Blocking Eligible", + "type": "boolean" + }, + "claim_ids": { + "items": { + "type": "string" + }, + "title": "Claim Ids", + "type": "array" + }, + "confidence": { + "default": "low", + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "evidence_bases": { + "items": { + "enum": [ + "reviewed_declaration", + "protocol_structure", + "typed_provider_fact", + "structural_scope", + "inferred_keyword", + "inferred_regex", + "protocol_default", + "unknown" + ], + "type": "string" + }, + "title": "Evidence Bases", + "type": "array" + }, + "policy_eligible": { + "default": false, + "title": "Policy Eligible", + "type": "boolean" + }, + "predicates": { + "items": { + "$ref": "#/$defs/PolicyPredicateEvidence" + }, + "title": "Predicates", + "type": "array" + }, + "status": { + "default": "matched", + "enum": [ + "matched", + "not_matched", + "indeterminate", + "conflicting" + ], + "title": "Status", + "type": "string" + }, + "support_hash": { + "title": "Support Hash", + "type": "string" + } + }, + "required": [ + "support_hash" + ], + "title": "FindingSupport", + "type": "object" + }, + "HumanControlAction": { + "additionalProperties": false, + "description": "A human-owned route. Human actions never expose executable commands.", + "properties": { + "actor": { + "const": "human", + "default": "human", + "title": "Actor", + "type": "string" + }, + "command": { + "default": null, + "title": "Command", + "type": "null" + }, + "expects": { + "default": null, + "title": "Expects", + "type": "null" + }, + "kind": { + "enum": [ + "review", + "stop" + ], + "title": "Kind", + "type": "string" + }, + "why": { + "minLength": 1, + "title": "Why", + "type": "string" + } + }, + "required": [ + "actor", + "kind", + "command", + "expects", + "why" + ], + "title": "HumanControlAction", + "type": "object" + }, + "HumanReviewRequiredControl": { + "additionalProperties": false, + "description": "Stopping state: no further coding-agent action is authorized.", + "properties": { + "allowed_next_commands": { + "items": { + "minLength": 1, + "type": "string" + }, + "maxItems": 0, + "title": "Allowed Next Commands", + "type": "array" + }, + "completion_allowed": { + "const": false, + "default": false, + "title": "Completion Allowed", + "type": "boolean" + }, + "human_review": { + "$ref": "#/$defs/RequiredHumanReview" + }, + "must_stop": { + "const": true, + "default": true, + "title": "Must Stop", + "type": "boolean" + }, + "next_action": { + "$ref": "#/$defs/HumanControlAction" + }, + "reason": { + "minLength": 1, + "title": "Reason", + "type": "string" + }, + "state": { + "const": "human_review_required", + "title": "State", + "type": "string" + }, + "stop_reason": { + "minLength": 1, + "title": "Stop Reason", + "type": "string" + }, + "verify_required": { + "default": false, + "title": "Verify Required", + "type": "boolean" + } + }, + "required": [ + "state", + "reason", + "completion_allowed", + "must_stop", + "verify_required", + "next_action", + "allowed_next_commands", + "human_review", + "stop_reason" + ], + "title": "HumanReviewRequiredControl", + "type": "object" + }, + "IdentityCoverageDecision": { + "properties": { + "ambiguous_name_count": { + "default": 0, + "title": "Ambiguous Name Count", + "type": "integer" + }, + "bound_tools": { + "default": 0, + "title": "Bound Tools", + "type": "integer" + }, + "canonical_tools": { + "default": 0, + "title": "Canonical Tools", + "type": "integer" + }, + "gap_count": { + "default": 0, + "title": "Gap Count", + "type": "integer" + }, + "pass_eligible_tools": { + "default": 0, + "title": "Pass Eligible Tools", + "type": "integer" + }, + "reason_counts": { + "additionalProperties": { + "type": "integer" + }, + "title": "Reason Counts", + "type": "object" + }, + "total_observations": { + "default": 0, + "title": "Total Observations", + "type": "integer" + } + }, + "title": "IdentityCoverageDecision", + "type": "object" + }, + "NoHumanReview": { + "additionalProperties": false, + "description": "Exact negative human-review projection for non-stopping states.", + "properties": { + "required": { + "const": false, + "default": false, + "title": "Required", + "type": "boolean" + }, + "required_reviewers": { + "items": { + "type": "string" + }, + "maxItems": 0, + "title": "Required Reviewers", + "type": "array" + }, + "why": { + "default": null, + "title": "Why", + "type": "null" + } + }, + "required": [ + "required", + "why", + "required_reviewers" + ], + "title": "NoHumanReview", + "type": "object" + }, + "PolicyPredicateEvidence": { + "additionalProperties": false, + "description": "One tri-state policy predicate and the evidence that supports it.", + "properties": { + "claim_ids": { + "items": { + "type": "string" + }, + "title": "Claim Ids", + "type": "array" + }, + "confidence": { + "default": "low", + "enum": [ + "low", + "medium", + "high" + ], + "title": "Confidence", + "type": "string" + }, + "evidence_bases": { + "items": { + "enum": [ + "reviewed_declaration", + "protocol_structure", + "typed_provider_fact", + "structural_scope", + "inferred_keyword", + "inferred_regex", + "protocol_default", + "unknown" + ], + "type": "string" + }, + "title": "Evidence Bases", + "type": "array" + }, + "expected": { + "default": null, + "title": "Expected" + }, + "observed": { + "default": null, + "title": "Observed" + }, + "policy_eligible": { + "default": false, + "title": "Policy Eligible", + "type": "boolean" + }, + "predicate": { + "title": "Predicate", + "type": "string" + }, + "status": { + "enum": [ + "matched", + "not_matched", + "indeterminate", + "conflicting" + ], + "title": "Status", + "type": "string" + }, + "why": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Why" + } + }, + "required": [ + "predicate", + "status" + ], + "title": "PolicyPredicateEvidence", + "type": "object" + }, + "ReleaseDecision": { + "properties": { + "baseline_delta": { + "$ref": "#/$defs/BaselineDelta" + }, + "blockers": { + "items": { + "$ref": "#/$defs/ReleaseDecisionItem" + }, + "title": "Blockers", + "type": "array" + }, + "contribution_rules": { + "items": { + "$ref": "#/$defs/ContributionRule" + }, + "title": "Contribution Rules", + "type": "array" + }, + "decision": { + "enum": [ + "blocked", + "review_required", + "insufficient_evidence", + "passed" + ], + "title": "Decision", + "type": "string" + }, + "evidence_coverage": { + "$ref": "#/$defs/EvidenceCoverageDecision" + }, + "fail_policy": { + "$ref": "#/$defs/FailPolicy" + }, + "reason": { + "title": "Reason", + "type": "string" + }, + "review_items": { + "items": { + "$ref": "#/$defs/ReleaseDecisionItem" + }, + "title": "Review Items", + "type": "array" + }, + "runtime_behavior_verified": { + "const": false, + "default": false, + "title": "Runtime Behavior Verified", + "type": "boolean" + }, + "static_analysis_only": { + "const": true, + "default": true, + "title": "Static Analysis Only", + "type": "boolean" + }, + "static_verdict_disclaimer": { + "default": "This verdict covers deterministic static evidence only. Agents Shipgate did not execute the agent or prove runtime behavior, tool routing, credential enforcement, or safety.", + "title": "Static Verdict Disclaimer", + "type": "string" + } + }, + "required": [ + "decision", + "reason", + "evidence_coverage", + "baseline_delta", + "fail_policy" + ], + "title": "ReleaseDecision", + "type": "object" + }, + "ReleaseDecisionItem": { + "properties": { + "baseline_status": { + "anyOf": [ + { + "enum": [ + "new", + "matched", + "resolved" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Baseline Status" + }, + "blocks_release": { + "default": false, + "title": "Blocks Release", + "type": "boolean" + }, + "capability_refs": { + "items": { + "type": "string" + }, + "title": "Capability Refs", + "type": "array" + }, + "capability_trace_refs": { + "items": { + "type": "string" + }, + "title": "Capability Trace Refs", + "type": "array" + }, + "check_id": { + "title": "Check Id", + "type": "string" + }, + "fingerprint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Fingerprint" + }, + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Id" + }, + "policy_evidence_source": { + "anyOf": [ + { + "$ref": "#/$defs/SourceReference" + }, + { + "type": "null" + } + ], + "default": null + }, + "severity": { + "enum": [ + "info", + "low", + "medium", + "high", + "critical" + ], + "title": "Severity", + "type": "string" + }, + "source": { + "anyOf": [ + { + "$ref": "#/$defs/SourceReference" + }, + { + "type": "null" + } + ], + "default": null + }, + "support": { + "anyOf": [ + { + "$ref": "#/$defs/FindingSupport" + }, + { + "type": "null" + } + ], + "default": null + }, + "title": { + "title": "Title", + "type": "string" + } + }, + "required": [ + "check_id", + "severity", + "title" + ], + "title": "ReleaseDecisionItem", + "type": "object" + }, + "RequiredHumanReview": { + "additionalProperties": false, + "description": "Human-review evidence carried by the stopping state.", + "properties": { + "required": { + "const": true, + "default": true, + "title": "Required", + "type": "boolean" + }, + "required_reviewers": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Required Reviewers", + "type": "array" + }, + "why": { + "minLength": 1, + "title": "Why", + "type": "string" + } + }, + "required": [ + "required", + "why", + "required_reviewers" + ], + "title": "RequiredHumanReview", + "type": "object" + }, + "SemanticCoverageDecision": { + "description": "v0.29 pass eligibility across the normalized action surface.\n\nUnlike extraction-confidence thresholds, semantic gaps are\nzero-tolerance: any non-pass-eligible unknown/partial/conflicting\ndimension prevents ``passed``. Known authority review concerns (for\nexample ambient or unscoped credentials) are counted separately so\nthey deterministically route to ``review_required`` rather than\n``insufficient_evidence``.", + "properties": { + "gap_count": { + "default": 0, + "title": "Gap Count", + "type": "integer" + }, + "pass_eligible_actions": { + "default": 0, + "title": "Pass Eligible Actions", + "type": "integer" + }, + "reason_counts": { + "additionalProperties": { + "type": "integer" + }, + "title": "Reason Counts", + "type": "object" + }, + "review_concern_count": { + "default": 0, + "title": "Review Concern Count", + "type": "integer" + }, + "total_actions": { + "default": 0, + "title": "Total Actions", + "type": "integer" + } + }, + "title": "SemanticCoverageDecision", + "type": "object" + }, + "SourceReference": { + "additionalProperties": true, + "properties": { + "end_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "End Line" + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Location" + }, + "path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Path" + }, + "pointer": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Pointer" + }, + "ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Ref" + }, + "start_column": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start Column" + }, + "start_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Start Line" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "SourceReference", + "type": "object" + }, + "VerifierCapabilityChange": { + "additionalProperties": false, + "description": "One reviewer-facing capability change projected for verifier output.", + "properties": { + "change_bucket": { + "enum": [ + "added", + "modified", + "removed" + ], + "title": "Change Bucket", + "type": "string" + }, + "change_type": { + "title": "Change Type", + "type": "string" + }, + "id": { + "title": "Id", + "type": "string" + }, + "impact": { + "default": "informational", + "enum": [ + "blocks_release", + "review_required", + "insufficient_evidence", + "informational", + "none" + ], + "title": "Impact", + "type": "string" + }, + "rationale": { + "title": "Rationale", + "type": "string" + }, + "related_finding_ids": { + "items": { + "type": "string" + }, + "title": "Related Finding Ids", + "type": "array" + }, + "source_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Path" + }, + "source_start_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Start Line" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "subject_kind": { + "title": "Subject Kind", + "type": "string" + } + }, + "required": [ + "id", + "change_type", + "change_bucket", + "subject_kind", + "subject", + "rationale" + ], + "title": "VerifierCapabilityChange", + "type": "object" + }, + "VerifierCapabilityReview": { + "additionalProperties": false, + "description": "Derived capability-review rollup for PR comments and Action outputs.\n\nThis is a projection only. It never gates independently of\n``report.json.release_decision.decision``.", + "properties": { + "added": { + "default": 0, + "title": "Added", + "type": "integer" + }, + "modified": { + "default": 0, + "title": "Modified", + "type": "integer" + }, + "notes": { + "items": { + "type": "string" + }, + "title": "Notes", + "type": "array" + }, + "policy_weakened": { + "default": false, + "title": "Policy Weakened", + "type": "boolean" + }, + "removed": { + "default": 0, + "title": "Removed", + "type": "integer" + }, + "top_changes": { + "items": { + "$ref": "#/$defs/VerifierCapabilityChange" + }, + "title": "Top Changes", + "type": "array" + }, + "trust_root_touched": { + "default": false, + "title": "Trust Root Touched", + "type": "boolean" + } + }, + "title": "VerifierCapabilityReview", + "type": "object" + }, + "VerifierFixTask": { + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "actor": { + "const": "human" + } + }, + "required": [ + "actor" + ] + }, + "then": { + "properties": { + "safe_to_attempt": { + "const": false + } + } + } + }, + { + "if": { + "properties": { + "actor": { + "const": "coding_agent" + }, + "safe_to_attempt": { + "const": true + } + }, + "required": [ + "actor", + "safe_to_attempt" + ] + }, + "then": { + "properties": { + "verification_command": { + "minLength": 1, + "pattern": "\\S", + "type": "string" + } + }, + "required": [ + "verification_command" + ] + } + } + ], + "description": "The single repair task a verify run hands to whoever acts next.\n\nRouting is deterministic and projected from the head scan \u2014 never an LLM\njudgment. ``coding_agent`` + ``safe_to_attempt=True`` means the gating\ngaps are mechanical (every gating finding is ``autofix_safe``): the agent\nmay fix them and re-run ``verification_command``. ``human`` +\n``safe_to_attempt=False`` means an authority gap a coding agent must not\ninvent its way past \u2014 missing approval/idempotency evidence, a weakened\npolicy, or a touched trust root. ``forbidden_shortcuts`` are the\nreward-hacking moves that are never acceptable for either actor.\n``patches`` (v0.12+) carries the machine-applicable suggested patches for\nthe gating findings when verify ran with ``--suggest-patches`` and the\ntask routes to the coding agent.", + "properties": { + "actor": { + "enum": [ + "coding_agent", + "human" + ], + "title": "Actor", + "type": "string" + }, + "allowed_repairs": { + "items": { + "$ref": "#/$defs/VerifierRepair" + }, + "title": "Allowed Repairs", + "type": "array" + }, + "forbidden_repairs": { + "items": { + "$ref": "#/$defs/VerifierRepair" + }, + "title": "Forbidden Repairs", + "type": "array" + }, + "forbidden_shortcuts": { + "items": { + "type": "string" + }, + "title": "Forbidden Shortcuts", + "type": "array" + }, + "instructions": { + "items": { + "type": "string" + }, + "title": "Instructions", + "type": "array" + }, + "patches": { + "items": { + "$ref": "#/$defs/VerifierFixTaskPatch" + }, + "title": "Patches", + "type": "array" + }, + "safe_to_attempt": { + "title": "Safe To Attempt", + "type": "boolean" + }, + "verification_command": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Verification Command" + } + }, + "required": [ + "actor", + "safe_to_attempt" + ], + "title": "VerifierFixTask", + "type": "object" + }, + "VerifierFixTaskPatch": { + "additionalProperties": false, + "description": "A machine-applicable patch projected into the fix task.\n\nRepair aid only \u2014 never a gate input. ``patch`` carries the\ndiscriminated Patch payload (``set_pointer`` / ``append_pointer`` /\n``remove_pointer``) exactly as the head scan emitted it; ``manual``\npatches are intentionally excluded because their guidance already\nappears in ``instructions``.", + "properties": { + "check_id": { + "default": "", + "title": "Check Id", + "type": "string" + }, + "finding_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Finding Id" + }, + "patch": { + "additionalProperties": true, + "title": "Patch", + "type": "object" + } + }, + "title": "VerifierFixTaskPatch", + "type": "object" + }, + "VerifierRepair": { + "additionalProperties": false, + "description": "One deterministic repair affordance or prohibition.\n\nThe verifier owns the actor and safety boundary. These rows are not model\nsuggestions: they are a structured projection of remediation metadata and\ntrust-root rules so coding agents can distinguish mechanical fixes from\nhuman-only authority decisions.", + "properties": { + "actor": { + "enum": [ + "coding_agent", + "human" + ], + "title": "Actor", + "type": "string" + }, + "check_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Check Id" + }, + "command": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Command" + }, + "finding_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Finding Id" + }, + "id": { + "title": "Id", + "type": "string" + }, + "kind": { + "title": "Kind", + "type": "string" + }, + "reason": { + "title": "Reason", + "type": "string" + }, + "target": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Target" + } + }, + "required": [ + "id", + "actor", + "kind", + "reason" + ], + "title": "VerifierRepair", + "type": "object" + } + }, + "$id": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/verifier-schema.v0.5.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "decision": { + "const": "passed" + } + }, + "required": [ + "decision" + ] + }, + "then": { + "properties": { + "applicability": { + "const": "verified" + }, + "can_merge_without_human": { + "const": true + }, + "capability_review": { + "properties": { + "policy_weakened": { + "const": false + }, + "trust_root_touched": { + "const": false + } + } + }, + "control": { + "properties": { + "state": { + "const": "complete" + } + }, + "required": [ + "state" + ] + }, + "execution": { + "const": "succeeded" + }, + "fix_task": { + "type": "null" + }, + "head_status": { + "const": "succeeded" + }, + "merge_verdict": { + "const": "mergeable" + }, + "release_decision": { + "properties": { + "blockers": { + "maxItems": 0 + }, + "decision": { + "const": "passed" + }, + "evidence_coverage": { + "properties": { + "evidence_gaps": { + "maxItems": 0 + }, + "human_review_recommended": { + "const": false + } + } + }, + "review_items": { + "maxItems": 0 + } + } + } + } + } + }, + { + "else": { + "properties": { + "control": { + "properties": { + "state": { + "enum": [ + "agent_action_required", + "human_review_required" + ] + } + }, + "required": [ + "state" + ] + } + } + }, + "if": { + "properties": { + "can_merge_without_human": { + "const": true + } + }, + "required": [ + "can_merge_without_human" + ] + }, + "then": { + "oneOf": [ + { + "properties": { + "applicability": { + "const": "verified" + }, + "decision": { + "const": "passed" + }, + "execution": { + "const": "succeeded" + } + } + }, + { + "properties": { + "applicability": { + "const": "not_applicable" + }, + "decision": { + "type": "null" + }, + "execution": { + "const": "skipped" + } + } + } + ], + "properties": { + "control": { + "properties": { + "state": { + "const": "complete" + } + }, + "required": [ + "state" + ] + } + } + } + } + ], + "description": "JSON Schema for verifier.json. Generated from agents_shipgate.schemas.verifier.VerifierArtifact. Do not edit by hand.", + "properties": { + "agent_summary": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Agent Summary" + }, + "applicability": { + "default": "not_evaluated", + "enum": [ + "not_evaluated", + "verified", + "not_applicable", + "failed" + ], + "title": "Applicability", + "type": "string" + }, + "artifacts": { + "additionalProperties": { + "type": "string" + }, + "title": "Artifacts", + "type": "object" + }, + "base_notes": { + "items": { + "type": "string" + }, + "title": "Base Notes", + "type": "array" + }, + "base_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Base Ref" + }, + "base_report_json": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Base Report Json" + }, + "base_status": { + "default": "not_requested", + "enum": [ + "not_requested", + "skipped", + "diff_from_provided", + "ref_missing", + "archive_failed", + "missing_manifest", + "scan_failed", + "cache_hit", + "succeeded" + ], + "title": "Base Status", + "type": "string" + }, + "base_tree_sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Base Tree Sha" + }, + "can_merge_without_human": { + "default": false, + "title": "Can Merge Without Human", + "type": "boolean" + }, + "capability_review": { + "$ref": "#/$defs/VerifierCapabilityReview" + }, + "changed_files": { + "items": { + "type": "string" + }, + "title": "Changed Files", + "type": "array" + }, + "config": { + "title": "Config", + "type": "string" + }, + "control": { + "$ref": "#/$defs/AgentControl" + }, + "decision": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Decision" + }, + "decision_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Decision Id" + }, + "diff_text_available": { + "default": false, + "title": "Diff Text Available", + "type": "boolean" + }, + "engine_requirement_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Engine Requirement Id" + }, + "execution": { + "default": "not_run", + "enum": [ + "not_run", + "succeeded", + "skipped", + "failed" + ], + "title": "Execution", + "type": "string" + }, + "executor_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Executor Id" + }, + "fix_task": { + "anyOf": [ + { + "$ref": "#/$defs/VerifierFixTask" + }, + { + "type": "null" + } + ], + "default": null + }, + "forbidden_actions": { + "items": { + "type": "string" + }, + "title": "Forbidden Actions", + "type": "array" + }, + "forbidden_file_edits": { + "items": { + "type": "string" + }, + "title": "Forbidden File Edits", + "type": "array" + }, + "head_exit_code": { + "default": 0, + "title": "Head Exit Code", + "type": "integer" + }, + "head_ref": { + "default": "HEAD", + "title": "Head Ref", + "type": "string" + }, + "head_report_json": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Head Report Json" + }, + "head_status": { + "default": "not_run", + "enum": [ + "not_run", + "succeeded", + "skipped", + "failed" + ], + "title": "Head Status", + "type": "string" + }, + "head_tree_sha": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Head Tree Sha" + }, + "headline": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Headline" + }, + "input_set_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Input Set Id" + }, + "merge_verdict": { + "default": "unknown", + "enum": [ + "mergeable", + "human_review_required", + "insufficient_evidence", + "blocked", + "unknown" + ], + "title": "Merge Verdict", + "type": "string" + }, + "mode": { + "default": "advisory", + "title": "Mode", + "type": "string" + }, + "release_decision": { + "anyOf": [ + { + "$ref": "#/$defs/ReleaseDecision" + }, + { + "type": "null" + } + ], + "default": null + }, + "request_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Request Id" + }, + "reviewer_summary": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Reviewer Summary" + }, + "runtime_behavior_verified": { + "const": false, + "default": false, + "title": "Runtime Behavior Verified", + "type": "boolean" + }, + "static_analysis_only": { + "const": true, + "default": true, + "title": "Static Analysis Only", + "type": "boolean" + }, + "static_verdict_disclaimer": { + "default": "This verdict covers deterministic static evidence only. Agents Shipgate did not execute the agent or prove runtime behavior, tool routing, credential enforcement, or safety.", + "title": "Static Verdict Disclaimer", + "type": "string" + }, + "subject_id": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Subject Id" + }, + "trigger": { + "additionalProperties": true, + "title": "Trigger", + "type": "object" + }, + "verifier_schema_version": { + "const": "0.5", + "default": "0.5", + "title": "Verifier Schema Version", + "type": "string" + }, + "workspace": { + "title": "Workspace", + "type": "string" + } + }, + "required": [ + "workspace", + "config", + "control" + ], + "title": "Agents Shipgate Verifier Artifact v0.5", + "type": "object" +} diff --git a/docs/verify-run-schema.v3.json b/docs/verify-run-schema.v3.json new file mode 100644 index 00000000..1560be1c --- /dev/null +++ b/docs/verify-run-schema.v3.json @@ -0,0 +1,1137 @@ +{ + "$defs": { + "AgentActionRequiredControl": { + "additionalProperties": false, + "description": "Non-terminal state with one exact coding-agent-owned next step.", + "properties": { + "allowed_next_commands": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Allowed Next Commands", + "type": "array" + }, + "completion_allowed": { + "const": false, + "default": false, + "title": "Completion Allowed", + "type": "boolean" + }, + "human_review": { + "$ref": "#/$defs/NoHumanReview" + }, + "must_stop": { + "const": false, + "default": false, + "title": "Must Stop", + "type": "boolean" + }, + "next_action": { + "$ref": "#/$defs/CodingAgentAction" + }, + "reason": { + "minLength": 1, + "title": "Reason", + "type": "string" + }, + "state": { + "const": "agent_action_required", + "title": "State", + "type": "string" + }, + "stop_reason": { + "default": null, + "title": "Stop Reason", + "type": "null" + }, + "verify_required": { + "default": false, + "title": "Verify Required", + "type": "boolean" + } + }, + "required": [ + "state", + "reason", + "completion_allowed", + "must_stop", + "verify_required", + "next_action", + "allowed_next_commands", + "human_review", + "stop_reason" + ], + "title": "AgentActionRequiredControl", + "type": "object" + }, + "AgentControl": { + "discriminator": { + "mapping": { + "agent_action_required": "#/$defs/AgentActionRequiredControl", + "complete": "#/$defs/CompleteAgentControl", + "human_review_required": "#/$defs/HumanReviewRequiredControl" + }, + "propertyName": "state" + }, + "oneOf": [ + { + "$ref": "#/$defs/CompleteAgentControl" + }, + { + "$ref": "#/$defs/AgentActionRequiredControl" + }, + { + "$ref": "#/$defs/HumanReviewRequiredControl" + } + ] + }, + "CodingAgentAction": { + "discriminator": { + "mapping": { + "configure": "#/$defs/CodingAgentCommandAction", + "discover": "#/$defs/CodingAgentCommandAction", + "fetch_base": "#/$defs/CodingAgentFetchBaseAction", + "initialize": "#/$defs/CodingAgentCommandAction", + "install": "#/$defs/CodingAgentCommandAction", + "repair": "#/$defs/CodingAgentCommandAction", + "rerun": "#/$defs/CodingAgentCommandAction", + "verify": "#/$defs/CodingAgentCommandAction" + }, + "propertyName": "kind" + }, + "oneOf": [ + { + "$ref": "#/$defs/CodingAgentCommandAction" + }, + { + "$ref": "#/$defs/CodingAgentFetchBaseAction" + } + ] + }, + "CodingAgentCommandAction": { + "additionalProperties": false, + "description": "An executable, exact next step owned by the coding agent.", + "properties": { + "actor": { + "const": "coding_agent", + "default": "coding_agent", + "title": "Actor", + "type": "string" + }, + "command": { + "minLength": 1, + "title": "Command", + "type": "string" + }, + "expects": { + "default": null, + "title": "Expects", + "type": "null" + }, + "kind": { + "enum": [ + "verify", + "discover", + "configure", + "initialize", + "repair", + "install", + "rerun" + ], + "title": "Kind", + "type": "string" + }, + "why": { + "minLength": 1, + "title": "Why", + "type": "string" + } + }, + "required": [ + "actor", + "kind", + "command", + "expects", + "why" + ], + "title": "CodingAgentCommandAction", + "type": "object" + }, + "CodingAgentFetchBaseAction": { + "additionalProperties": false, + "description": "A structured input request when an exact fetch command is unavailable.\n\nShipgate never fetches refs itself. ``expects`` therefore names the exact\nref or artifact a caller must make available before rerunning verification.", + "properties": { + "actor": { + "const": "coding_agent", + "default": "coding_agent", + "title": "Actor", + "type": "string" + }, + "command": { + "default": null, + "title": "Command", + "type": "null" + }, + "expects": { + "minLength": 1, + "title": "Expects", + "type": "string" + }, + "kind": { + "const": "fetch_base", + "title": "Kind", + "type": "string" + }, + "why": { + "minLength": 1, + "title": "Why", + "type": "string" + } + }, + "required": [ + "actor", + "kind", + "command", + "expects", + "why" + ], + "title": "CodingAgentFetchBaseAction", + "type": "object" + }, + "CompleteAgentControl": { + "additionalProperties": false, + "description": "Terminal state: the coding agent may report the task complete.", + "properties": { + "allowed_next_commands": { + "items": { + "minLength": 1, + "type": "string" + }, + "maxItems": 0, + "title": "Allowed Next Commands", + "type": "array" + }, + "completion_allowed": { + "const": true, + "default": true, + "title": "Completion Allowed", + "type": "boolean" + }, + "human_review": { + "$ref": "#/$defs/NoHumanReview" + }, + "must_stop": { + "const": false, + "default": false, + "title": "Must Stop", + "type": "boolean" + }, + "next_action": { + "default": null, + "title": "Next Action", + "type": "null" + }, + "reason": { + "minLength": 1, + "title": "Reason", + "type": "string" + }, + "state": { + "const": "complete", + "title": "State", + "type": "string" + }, + "stop_reason": { + "default": null, + "title": "Stop Reason", + "type": "null" + }, + "verify_required": { + "const": false, + "default": false, + "title": "Verify Required", + "type": "boolean" + } + }, + "required": [ + "state", + "reason", + "completion_allowed", + "must_stop", + "verify_required", + "next_action", + "allowed_next_commands", + "human_review", + "stop_reason" + ], + "title": "CompleteAgentControl", + "type": "object" + }, + "HumanControlAction": { + "additionalProperties": false, + "description": "A human-owned route. Human actions never expose executable commands.", + "properties": { + "actor": { + "const": "human", + "default": "human", + "title": "Actor", + "type": "string" + }, + "command": { + "default": null, + "title": "Command", + "type": "null" + }, + "expects": { + "default": null, + "title": "Expects", + "type": "null" + }, + "kind": { + "enum": [ + "review", + "stop" + ], + "title": "Kind", + "type": "string" + }, + "why": { + "minLength": 1, + "title": "Why", + "type": "string" + } + }, + "required": [ + "actor", + "kind", + "command", + "expects", + "why" + ], + "title": "HumanControlAction", + "type": "object" + }, + "HumanReviewRequiredControl": { + "additionalProperties": false, + "description": "Stopping state: no further coding-agent action is authorized.", + "properties": { + "allowed_next_commands": { + "items": { + "minLength": 1, + "type": "string" + }, + "maxItems": 0, + "title": "Allowed Next Commands", + "type": "array" + }, + "completion_allowed": { + "const": false, + "default": false, + "title": "Completion Allowed", + "type": "boolean" + }, + "human_review": { + "$ref": "#/$defs/RequiredHumanReview" + }, + "must_stop": { + "const": true, + "default": true, + "title": "Must Stop", + "type": "boolean" + }, + "next_action": { + "$ref": "#/$defs/HumanControlAction" + }, + "reason": { + "minLength": 1, + "title": "Reason", + "type": "string" + }, + "state": { + "const": "human_review_required", + "title": "State", + "type": "string" + }, + "stop_reason": { + "minLength": 1, + "title": "Stop Reason", + "type": "string" + }, + "verify_required": { + "default": false, + "title": "Verify Required", + "type": "boolean" + } + }, + "required": [ + "state", + "reason", + "completion_allowed", + "must_stop", + "verify_required", + "next_action", + "allowed_next_commands", + "human_review", + "stop_reason" + ], + "title": "HumanReviewRequiredControl", + "type": "object" + }, + "NoHumanReview": { + "additionalProperties": false, + "description": "Exact negative human-review projection for non-stopping states.", + "properties": { + "required": { + "const": false, + "default": false, + "title": "Required", + "type": "boolean" + }, + "required_reviewers": { + "items": { + "type": "string" + }, + "maxItems": 0, + "title": "Required Reviewers", + "type": "array" + }, + "why": { + "default": null, + "title": "Why", + "type": "null" + } + }, + "required": [ + "required", + "why", + "required_reviewers" + ], + "title": "NoHumanReview", + "type": "object" + }, + "RequiredHumanReview": { + "additionalProperties": false, + "description": "Human-review evidence carried by the stopping state.", + "properties": { + "required": { + "const": true, + "default": true, + "title": "Required", + "type": "boolean" + }, + "required_reviewers": { + "items": { + "minLength": 1, + "type": "string" + }, + "title": "Required Reviewers", + "type": "array" + }, + "why": { + "minLength": 1, + "title": "Why", + "type": "string" + } + }, + "required": [ + "required", + "why", + "required_reviewers" + ], + "title": "RequiredHumanReview", + "type": "object" + }, + "VerificationBlob": { + "additionalProperties": false, + "properties": { + "git_blob_oid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Git Blob Oid" + }, + "path": { + "title": "Path", + "type": "string" + }, + "sha256": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Sha256", + "type": "string" + }, + "size_bytes": { + "minimum": 0, + "title": "Size Bytes", + "type": "integer" + }, + "source": { + "enum": [ + "git_blob", + "worktree", + "generated", + "external_input", + "artifact" + ], + "title": "Source", + "type": "string" + } + }, + "required": [ + "path", + "sha256", + "size_bytes", + "source" + ], + "title": "VerificationBlob", + "type": "object" + }, + "VerificationEngineRequirement": { + "additionalProperties": false, + "properties": { + "adapter_set_sha256": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Adapter Set Sha256", + "type": "string" + }, + "dependency_set_sha256": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Dependency Set Sha256", + "type": "string" + }, + "engine_distribution_sha256": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Engine Distribution Sha256", + "type": "string" + }, + "engine_requirement_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Engine Requirement Id", + "type": "string" + }, + "package": { + "default": "agents-shipgate", + "title": "Package", + "type": "string" + }, + "platform": { + "title": "Platform", + "type": "string" + }, + "plugin_set_sha256": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Plugin Set Sha256", + "type": "string" + }, + "policy_catalog_sha256": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Policy Catalog Sha256", + "type": "string" + }, + "python_implementation": { + "title": "Python Implementation", + "type": "string" + }, + "python_version": { + "title": "Python Version", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "engine_requirement_id", + "version", + "python_implementation", + "python_version", + "platform", + "engine_distribution_sha256", + "dependency_set_sha256", + "adapter_set_sha256", + "plugin_set_sha256", + "policy_catalog_sha256" + ], + "title": "VerificationEngineRequirement", + "type": "object" + }, + "VerificationExecutor": { + "additionalProperties": false, + "properties": { + "engine_requirement_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Engine Requirement Id", + "type": "string" + }, + "executor_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Executor Id", + "type": "string" + }, + "runtime_sha256": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Runtime Sha256", + "type": "string" + } + }, + "required": [ + "executor_id", + "engine_requirement_id", + "runtime_sha256" + ], + "title": "VerificationExecutor", + "type": "object" + }, + "VerificationGitSubject": { + "additionalProperties": false, + "properties": { + "base_commit_sha": { + "anyOf": [ + { + "pattern": "^[0-9a-f]{40,64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Base Commit Sha" + }, + "base_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Base Ref" + }, + "base_tree_sha": { + "anyOf": [ + { + "pattern": "^[0-9a-f]{40,64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Base Tree Sha" + }, + "head_commit_sha": { + "anyOf": [ + { + "pattern": "^[0-9a-f]{40,64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Head Commit Sha" + }, + "head_ref": { + "title": "Head Ref", + "type": "string" + }, + "head_tree_sha": { + "anyOf": [ + { + "pattern": "^[0-9a-f]{40,64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Head Tree Sha" + }, + "merge_base_sha": { + "anyOf": [ + { + "pattern": "^[0-9a-f]{40,64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Merge Base Sha" + }, + "repository_id": { + "title": "Repository Id", + "type": "string" + }, + "snapshot_kind": { + "enum": [ + "committed_tree", + "worktree_overlay" + ], + "title": "Snapshot Kind", + "type": "string" + }, + "source_head_commit_sha": { + "anyOf": [ + { + "pattern": "^[0-9a-f]{40,64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Source Head Commit Sha" + }, + "worktree_overlay_sha256": { + "anyOf": [ + { + "pattern": "^sha256:[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Worktree Overlay Sha256" + } + }, + "required": [ + "repository_id", + "head_ref", + "snapshot_kind" + ], + "title": "VerificationGitSubject", + "type": "object" + }, + "VerificationInputSet": { + "additionalProperties": false, + "properties": { + "baseline": { + "anyOf": [ + { + "$ref": "#/$defs/VerificationBlob" + }, + { + "type": "null" + } + ], + "default": null + }, + "changed_files": { + "items": { + "$ref": "#/$defs/VerificationBlob" + }, + "title": "Changed Files", + "type": "array" + }, + "changed_paths": { + "items": { + "type": "string" + }, + "title": "Changed Paths", + "type": "array" + }, + "config": { + "$ref": "#/$defs/VerificationBlob" + }, + "diff": { + "$ref": "#/$defs/VerificationBlob" + }, + "diff_from": { + "anyOf": [ + { + "$ref": "#/$defs/VerificationBlob" + }, + { + "type": "null" + } + ], + "default": null + }, + "evaluation_date": { + "title": "Evaluation Date", + "type": "string" + }, + "input_set_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Input Set Id", + "type": "string" + }, + "options": { + "additionalProperties": true, + "title": "Options", + "type": "object" + }, + "policy_packs": { + "items": { + "$ref": "#/$defs/VerificationBlob" + }, + "title": "Policy Packs", + "type": "array" + }, + "tool_sources": { + "items": { + "$ref": "#/$defs/VerificationBlob" + }, + "title": "Tool Sources", + "type": "array" + } + }, + "required": [ + "input_set_id", + "evaluation_date", + "config", + "diff" + ], + "title": "VerificationInputSet", + "type": "object" + }, + "VerificationPlan": { + "additionalProperties": false, + "properties": { + "engine": { + "$ref": "#/$defs/VerificationEngineRequirement" + }, + "inputs": { + "$ref": "#/$defs/VerificationInputSet" + }, + "request_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Request Id", + "type": "string" + }, + "schema_version": { + "const": "shipgate.verification_plan/v1", + "default": "shipgate.verification_plan/v1", + "title": "Schema Version", + "type": "string" + }, + "subject": { + "$ref": "#/$defs/VerificationSubject" + }, + "tasks": { + "items": { + "$ref": "#/$defs/VerificationTask" + }, + "maxItems": 1, + "minItems": 1, + "title": "Tasks", + "type": "array" + } + }, + "required": [ + "request_id", + "subject", + "inputs", + "engine", + "tasks" + ], + "title": "VerificationPlan", + "type": "object" + }, + "VerificationSubject": { + "additionalProperties": false, + "properties": { + "git": { + "$ref": "#/$defs/VerificationGitSubject" + }, + "subject_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Subject Id", + "type": "string" + } + }, + "required": [ + "subject_id", + "git" + ], + "title": "VerificationSubject", + "type": "object" + }, + "VerificationTask": { + "additionalProperties": false, + "properties": { + "input_paths": { + "items": { + "type": "string" + }, + "title": "Input Paths", + "type": "array" + }, + "kind": { + "enum": [ + "extract", + "normalize", + "evaluate" + ], + "title": "Kind", + "type": "string" + }, + "shard": { + "minimum": 0, + "title": "Shard", + "type": "integer" + }, + "shard_count": { + "minimum": 1, + "title": "Shard Count", + "type": "integer" + }, + "task_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Task Id", + "type": "string" + } + }, + "required": [ + "task_id", + "kind", + "shard", + "shard_count" + ], + "title": "VerificationTask", + "type": "object" + }, + "VerifyRunArtifactRef": { + "additionalProperties": false, + "properties": { + "path": { + "title": "Path", + "type": "string" + }, + "sha256": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sha256" + } + }, + "required": [ + "path" + ], + "title": "VerifyRunArtifactRef", + "type": "object" + }, + "VerifyRunOutcome": { + "additionalProperties": false, + "else": { + "properties": { + "control": { + "properties": { + "state": { + "enum": [ + "agent_action_required", + "human_review_required" + ] + } + }, + "required": [ + "state" + ] + } + } + }, + "if": { + "properties": { + "can_merge_without_human": { + "const": true + } + }, + "required": [ + "can_merge_without_human" + ] + }, + "properties": { + "applicability": { + "enum": [ + "not_evaluated", + "verified", + "not_applicable", + "failed" + ], + "title": "Applicability", + "type": "string" + }, + "base_status": { + "title": "Base Status", + "type": "string" + }, + "can_merge_without_human": { + "default": false, + "title": "Can Merge Without Human", + "type": "boolean" + }, + "control": { + "$ref": "#/$defs/AgentControl" + }, + "decision": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Decision" + }, + "execution": { + "enum": [ + "not_run", + "succeeded", + "skipped", + "failed" + ], + "title": "Execution", + "type": "string" + }, + "exit_code": { + "title": "Exit Code", + "type": "integer" + }, + "merge_verdict": { + "title": "Merge Verdict", + "type": "string" + } + }, + "required": [ + "exit_code", + "base_status", + "execution", + "applicability", + "merge_verdict", + "control" + ], + "then": { + "oneOf": [ + { + "properties": { + "applicability": { + "const": "verified" + }, + "decision": { + "const": "passed" + }, + "execution": { + "const": "succeeded" + } + } + }, + { + "properties": { + "applicability": { + "const": "not_applicable" + }, + "decision": { + "type": "null" + }, + "execution": { + "const": "skipped" + } + } + } + ], + "properties": { + "control": { + "properties": { + "state": { + "const": "complete" + } + }, + "required": [ + "state" + ] + } + } + }, + "title": "VerifyRunOutcome", + "type": "object" + } + }, + "$id": "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/verify-run-schema.v3.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "JSON Schema for agents-shipgate-reports/verify-run.json. Generated from agents_shipgate.schemas.verify_run.VerifyRunArtifact. Do not edit by hand.", + "properties": { + "artifacts": { + "additionalProperties": { + "$ref": "#/$defs/VerifyRunArtifactRef" + }, + "title": "Artifacts", + "type": "object" + }, + "decision_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Decision Id", + "type": "string" + }, + "executor": { + "$ref": "#/$defs/VerificationExecutor" + }, + "outcome": { + "$ref": "#/$defs/VerifyRunOutcome" + }, + "plan": { + "$ref": "#/$defs/VerificationPlan" + }, + "request_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Request Id", + "type": "string" + }, + "run_id": { + "pattern": "^sha256:[0-9a-f]{64}$", + "title": "Run Id", + "type": "string" + }, + "schema_version": { + "const": "shipgate.verify_run/v3", + "default": "shipgate.verify_run/v3", + "title": "Schema Version", + "type": "string" + }, + "unit_result_ids": { + "items": { + "type": "string" + }, + "minItems": 1, + "title": "Unit Result Ids", + "type": "array" + } + }, + "required": [ + "request_id", + "run_id", + "plan", + "executor", + "unit_result_ids", + "decision_id", + "outcome" + ], + "title": "Agents Shipgate Verify Run v3", + "type": "object" +} diff --git a/examples/agent-protocol/expected/block-stop.json b/examples/agent-protocol/expected/block-stop.json index 9dfcd09e..216e16eb 100644 --- a/examples/agent-protocol/expected/block-stop.json +++ b/examples/agent-protocol/expected/block-stop.json @@ -3,7 +3,7 @@ "agent": "codex", "tool": { "name": "agents-shipgate", - "version": "0.16.0b5" + "version": "0.16.0b6" }, "subject": { "agent": "codex" diff --git a/examples/agent-protocol/expected/missing-install.json b/examples/agent-protocol/expected/missing-install.json index e0aaa45d..c767cfbd 100644 --- a/examples/agent-protocol/expected/missing-install.json +++ b/examples/agent-protocol/expected/missing-install.json @@ -3,7 +3,7 @@ "agent": "codex", "tool": { "name": "agents-shipgate", - "version": "0.16.0b5" + "version": "0.16.0b6" }, "subject": { "agent": "codex" diff --git a/examples/agent-protocol/expected/policy-bypass.json b/examples/agent-protocol/expected/policy-bypass.json index b8e8f4d0..1cbeb069 100644 --- a/examples/agent-protocol/expected/policy-bypass.json +++ b/examples/agent-protocol/expected/policy-bypass.json @@ -3,7 +3,7 @@ "agent": "codex", "tool": { "name": "agents-shipgate", - "version": "0.16.0b5" + "version": "0.16.0b6" }, "subject": { "agent": "codex" diff --git a/examples/agent-protocol/expected/repair-after.json b/examples/agent-protocol/expected/repair-after.json index 5f35ef77..25a48a2a 100644 --- a/examples/agent-protocol/expected/repair-after.json +++ b/examples/agent-protocol/expected/repair-after.json @@ -3,7 +3,7 @@ "agent": "codex", "tool": { "name": "agents-shipgate", - "version": "0.16.0b5" + "version": "0.16.0b6" }, "subject": { "agent": "codex" diff --git a/examples/agent-protocol/expected/repair-before.json b/examples/agent-protocol/expected/repair-before.json index e23d2766..cd735a0d 100644 --- a/examples/agent-protocol/expected/repair-before.json +++ b/examples/agent-protocol/expected/repair-before.json @@ -3,7 +3,7 @@ "agent": "codex", "tool": { "name": "agents-shipgate", - "version": "0.16.0b5" + "version": "0.16.0b6" }, "subject": { "agent": "codex" diff --git a/examples/agent-protocol/expected/stale-install.json b/examples/agent-protocol/expected/stale-install.json index 999c8b36..abef4514 100644 --- a/examples/agent-protocol/expected/stale-install.json +++ b/examples/agent-protocol/expected/stale-install.json @@ -3,7 +3,7 @@ "agent": "codex", "tool": { "name": "agents-shipgate", - "version": "0.16.0b5" + "version": "0.16.0b6" }, "subject": { "agent": "codex" diff --git a/llms-full.txt b/llms-full.txt index 4945b498..8ff064af 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -155,7 +155,8 @@ agents-shipgate verify --workspace . --config shipgate.yaml \ ``` For local uncommitted verifier work, omit `--base`/`--head` so the working tree -is scanned. Read `agents-shipgate-reports/agent-handoff.json` first and lead +is scanned. Validate `agents-shipgate-reports/verification-receipt.json` +first, then read `agents-shipgate-reports/agent-handoff.json` and lead with `control.state`, then `gate.merge_verdict` (`mergeable | human_review_required | insufficient_evidence | blocked | unknown`), `gate.can_merge_without_human`, `next_action`, @@ -388,7 +389,7 @@ restate version archaeology here): - Audit envelopes: `release_decision.contribution_rules[]`, `policy_audit`, `privacy_audit`, `heuristics_filter` — explanatory, never a second gate -The current schema is [`docs/report-schema.v0.33.json`](docs/report-schema.v0.33.json). Emitted reports carry `report_schema_version: "0.33"`; typed predicate support prevents heuristic evidence from being upgraded by policy severity or block metadata. A `passed` result requires a complete root-reachable static binding graph plus complete, conflict-free identity, effect, authority, and applicable-policy evidence for every reachable action. Every release decision explicitly carries `static_analysis_only: true`, `runtime_behavior_verified: false`, and `static_verdict_disclaimer`; packet §1 mirrors them. Binding, semantic, and policy-applicability gaps are not Findings and cannot be suppressed or baselined. See [`docs/passed-verdict-contract.md`](docs/passed-verdict-contract.md) for the exact claim and [`docs/agent-contract-current.md`](docs/agent-contract-current.md) for version history. v0.32 remains frozen at [`docs/report-schema.v0.32.json`](docs/report-schema.v0.32.json). +The current schema is [`docs/report-schema.v0.34.json`](docs/report-schema.v0.34.json). Emitted reports carry `report_schema_version: "0.34"`; typed predicate support prevents heuristic evidence from being upgraded by policy severity or block metadata, and verify-native reports bind the content-addressed request and decision. A `passed` result requires a complete root-reachable static binding graph plus complete, conflict-free identity, effect, authority, and applicable-policy evidence for every reachable action. Every release decision explicitly carries `static_analysis_only: true`, `runtime_behavior_verified: false`, and `static_verdict_disclaimer`; packet §1 mirrors them. Binding, semantic, and policy-applicability gaps are not Findings and cannot be suppressed or baselined. See [`docs/passed-verdict-contract.md`](docs/passed-verdict-contract.md), [`docs/verification-reproducibility.md`](docs/verification-reproducibility.md), and [`docs/agent-contract-current.md`](docs/agent-contract-current.md). v0.33 remains frozen at [`docs/report-schema.v0.33.json`](docs/report-schema.v0.33.json). **Release gating signal**: prefer `release_decision.decision` (`"blocked" | "review_required" | "insufficient_evidence" | "passed"`) over `summary.status`. The new field is **baseline-aware** — a baseline-matched critical surfaces in `release_decision.review_items` (accepted debt), not `release_decision.blockers`. `summary.status` stays baseline-blind for v0.7 compatibility, so a baseline-matched-only critical produces both `summary.status = "release_blockers_detected"` AND `release_decision.decision = "review_required"` (intentional divergence — see [STABILITY.md](STABILITY.md#release_decisiondecision-vs-summarystatus)). `insufficient_evidence` (added v0.14) signals that the scan saw too many low-confidence tools or source-loader warnings to be trustworthy; consumers that switch on the enum must fall back to `review_required` for unknown future values. @@ -468,7 +469,7 @@ validation and [`docs/manifest-v0.1.md`](docs/manifest-v0.1.md) for prose. ### Where is the report schema? Parse `agents-shipgate-reports/report.json` and validate against -[`docs/report-schema.v0.33.json`](docs/report-schema.v0.33.json) (current). +[`docs/report-schema.v0.34.json`](docs/report-schema.v0.34.json) (current). Older reports (`report_schema_version: "0.10"`) validate against the frozen [`docs/report-schema.v0.10.json`](docs/report-schema.v0.10.json). Do not scrape Markdown when JSON is available. @@ -506,7 +507,7 @@ For the short, current statement of "which fields to read", see [`docs/agent-con | What | Path | Stable | |---|---|---| | Manifest schema | [`docs/manifest-v0.1.json`](docs/manifest-v0.1.json) | `0.1` | -| Report schema (current) | [`docs/report-schema.v0.33.json`](docs/report-schema.v0.33.json) | `0.33` | +| Report schema (current) | [`docs/report-schema.v0.34.json`](docs/report-schema.v0.34.json) | `0.34` | | Report schema (v0.32 frozen reference) | [`docs/report-schema.v0.32.json`](docs/report-schema.v0.32.json) | `0.32` | | Report schema (v0.31 frozen reference) | [`docs/report-schema.v0.31.json`](docs/report-schema.v0.31.json) | `0.31` | | Report schema (v0.30 frozen reference) | [`docs/report-schema.v0.30.json`](docs/report-schema.v0.30.json) | `0.30` | @@ -515,8 +516,12 @@ For the short, current statement of "which fields to read", see [`docs/agent-con | Report schema (v0.27 frozen reference) | [`docs/report-schema.v0.27.json`](docs/report-schema.v0.27.json) | `0.27` | | Report schema (v0.26 frozen reference) | [`docs/report-schema.v0.26.json`](docs/report-schema.v0.26.json) | `0.26` | | Report schema (v0.25 frozen reference) | [`docs/report-schema.v0.25.json`](docs/report-schema.v0.25.json) | `0.25` | -| Verify-run schema | [`docs/verify-run-schema.v2.json`](docs/verify-run-schema.v2.json) | `shipgate.verify_run/v2` | -| Agent handoff schema | [`docs/agent-handoff-schema.v4.json`](docs/agent-handoff-schema.v4.json) | `shipgate.agent_handoff/v4` | +| Verify-run schema | [`docs/verify-run-schema.v3.json`](docs/verify-run-schema.v3.json) | `shipgate.verify_run/v3` | +| Verification plan schema | [`docs/verification-plan-schema.v1.json`](docs/verification-plan-schema.v1.json) | `shipgate.verification_plan/v1` | +| Verification unit result schema | [`docs/verification-unit-result-schema.v1.json`](docs/verification-unit-result-schema.v1.json) | `shipgate.verification_unit_result/v1` | +| Verification artifact manifest schema | [`docs/verification-artifact-manifest-schema.v1.json`](docs/verification-artifact-manifest-schema.v1.json) | `shipgate.verification_artifact_manifest/v1` | +| Verification receipt schema | [`docs/verification-receipt-schema.v1.json`](docs/verification-receipt-schema.v1.json) | `shipgate.verification_receipt/v1` | +| Agent handoff schema | [`docs/agent-handoff-schema.v5.json`](docs/agent-handoff-schema.v5.json) | `shipgate.agent_handoff/v5` | | Agent boundary result schema | [`docs/agent-boundary-result-schema.v1.json`](docs/agent-boundary-result-schema.v1.json) | `shipgate.agent_boundary_result/v1` | | Codex boundary result schema (deprecated frozen projection) | [`docs/codex-boundary-result-schema.v2.json`](docs/codex-boundary-result-schema.v2.json) | `shipgate.codex_boundary_result/v2` | | Report schema (v0.24 frozen reference) | [`docs/report-schema.v0.24.json`](docs/report-schema.v0.24.json) | `0.24` | @@ -538,10 +543,10 @@ For the short, current statement of "which fields to read", see [`docs/agent-con | Report schema (v0.8 frozen reference) | [`docs/report-schema.v0.8.json`](docs/report-schema.v0.8.json) | `0.8` | | Report schema (v0.7 frozen reference) | [`docs/report-schema.v0.7.json`](docs/report-schema.v0.7.json) | `0.7` | | Report schema (v0.6 frozen reference) | [`docs/report-schema.v0.6.json`](docs/report-schema.v0.6.json) | `0.6` | -| Packet schema (Release Evidence Packet, latest) | [`docs/packet-schema.v0.11.json`](docs/packet-schema.v0.11.json) | `0.11` | +| Packet schema (Release Evidence Packet, latest) | [`docs/packet-schema.v0.12.json`](docs/packet-schema.v0.12.json) | `0.12` | | Agent result schema (current) | [`docs/agent-result-schema.v2.json`](docs/agent-result-schema.v2.json) | `agent_result_v2` | -| Verifier schema (current) | [`docs/verifier-schema.v0.4.json`](docs/verifier-schema.v0.4.json) | `0.4` | -| Agent handoff schema (current) | [`docs/agent-handoff-schema.v4.json`](docs/agent-handoff-schema.v4.json) | `shipgate.agent_handoff/v4` | +| Verifier schema (current) | [`docs/verifier-schema.v0.5.json`](docs/verifier-schema.v0.5.json) | `0.5` | +| Agent handoff schema (current) | [`docs/agent-handoff-schema.v5.json`](docs/agent-handoff-schema.v5.json) | `shipgate.agent_handoff/v5` | | Preflight schema (current) | [`docs/preflight-schema.v0.3.json`](docs/preflight-schema.v0.3.json) | `0.3` | | Host-grants inventory schema | [`docs/host-grants-inventory-schema.v0.2.json`](docs/host-grants-inventory-schema.v0.2.json) | `0.2` | | Host-grants baseline schema | [`docs/host-grants-baseline-schema.v0.2.json`](docs/host-grants-baseline-schema.v0.2.json) | `0.2` | @@ -608,7 +613,7 @@ NOT prove. Use `--no-packet` / `--packet-format` on `scan`, and `agents-shipgate evidence-packet --from ` to re-render. The full packet contract (fixed sections, disclaimers, `evidence_matrix` rules) lives in -[STABILITY.md §Release Evidence Packet](STABILITY.md#release-evidence-packet-v011) +[STABILITY.md §Release Evidence Packet](STABILITY.md#release-evidence-packet-v012) and [`docs/agent-contract-current.md`](docs/agent-contract-current.md#read-these-for-release-review). Exit codes (stable): @@ -1029,22 +1034,26 @@ Verify the installed CLI contract locally before relying on hard-coded docs: agents-shipgate contract --json ``` -Runtime contract v16 retains the v15 host-neutral boundary, v14 unambiguous -`AgentControl`, and v13 root-reachable binding contracts. It adds typed -evidence bases and tri-state policy applicability across scan, verify, packet, -and handoff projections. Agents +Runtime contract v17 retains the v16 typed policy-evidence, v15 host-neutral +boundary, v14 unambiguous `AgentControl`, and v13 root-reachable binding +contracts. It adds a content-addressed verification request, decision-free +worker result, artifact manifest, and terminal receipt shared by local and +distributed verification. Agents switch on `control.state`; `decision` remains diagnostic and `release_decision.decision` remains the release gate. Contract v14 requires `completion_allowed == (state == "complete")` and -`must_stop == (state == "human_review_required")`. Report v0.33 prevents rule -metadata from upgrading heuristic evidence; packet v0.11, capability standard -v0.5, capability lock v0.6, and capability-lock diff v0.7 project the same -typed support. +`must_stop == (state == "human_review_required")`. Report v0.34, packet v0.12, +verifier v0.5, verify-run v3, and handoff v5 bind their projections to the +same request and decision IDs. The terminal receipt hashes the complete +artifact set; see [Verification Identity and Reproduction](verification-reproducibility.md). The runtime contract also exposes the local agent command spec: `primary_commands{}`, `commands{}`, `default_paths{}`, `artifacts{}`, `agent_read_order[]`, `verifier_read_order[]`, `merge_verdicts[]`, `release_decisions[]`, `do_not_auto_assert[]`, `verifier_schema_version`, -`verify_run_schema_version`, `agent_handoff_schema_version`, +`verify_run_schema_version`, `verification_plan_schema_version`, +`verification_unit_result_schema_version`, +`verification_artifact_manifest_schema_version`, +`verification_receipt_schema_version`, `agent_handoff_schema_version`, `agent_handoff_schema_path`, `agent_handoff_artifact`, `agent_boundary_result_schema_version`, the deprecated `codex_boundary_result_schema_version`, `attestation_schema_version`, @@ -1070,31 +1079,32 @@ Downstream repos generated with `.shipgate/agent-contract.json`. - Latest release: `v0.15.0` -- In-tree runtime: `0.16.0b5` — see [pyproject.toml](../pyproject.toml) -- Runtime contract: `16` (minimum control contract: `14`) -- Current report schema: `0.33` — [`docs/report-schema.v0.33.json`](report-schema.v0.33.json) -- Current packet schema: `0.11` — [`docs/packet-schema.v0.11.json`](packet-schema.v0.11.json) +- In-tree runtime: `0.16.0b6` — see [pyproject.toml](../pyproject.toml) +- Runtime contract: `17` (minimum control contract: `14`) +- Current report schema: `0.34` — [`docs/report-schema.v0.34.json`](report-schema.v0.34.json) +- Current packet schema: `0.12` — [`docs/packet-schema.v0.12.json`](packet-schema.v0.12.json) - Current shared agent result schema: `agent_result_v2` — [`docs/agent-result-schema.v2.json`](agent-result-schema.v2.json) -- Current verifier schema: `0.4` — [`docs/verifier-schema.v0.4.json`](verifier-schema.v0.4.json) -- Current verify-run schema: `shipgate.verify_run/v2` — [`docs/verify-run-schema.v2.json`](verify-run-schema.v2.json) -- Current agent handoff schema: `shipgate.agent_handoff/v4` — [`docs/agent-handoff-schema.v4.json`](agent-handoff-schema.v4.json) +- Current verifier schema: `0.5` — [`docs/verifier-schema.v0.5.json`](verifier-schema.v0.5.json) +- Current verify-run schema: `shipgate.verify_run/v3` — [`docs/verify-run-schema.v3.json`](verify-run-schema.v3.json) +- Current verification identity schemas: [`plan v1`](verification-plan-schema.v1.json), [`unit result v1`](verification-unit-result-schema.v1.json), [`artifact manifest v1`](verification-artifact-manifest-schema.v1.json), and [`terminal receipt v1`](verification-receipt-schema.v1.json) +- Current agent handoff schema: `shipgate.agent_handoff/v5` — [`docs/agent-handoff-schema.v5.json`](agent-handoff-schema.v5.json) - Current agent boundary result schema: `shipgate.agent_boundary_result/v1` — [`docs/agent-boundary-result-schema.v1.json`](agent-boundary-result-schema.v1.json) - Frozen deprecated Codex projection: `shipgate.codex_boundary_result/v2` — [`docs/codex-boundary-result-schema.v2.json`](codex-boundary-result-schema.v2.json) - Current preflight schema: `0.3` — [`docs/preflight-schema.v0.3.json`](preflight-schema.v0.3.json) -- Current downstream local agent contract schema: `5` +- Current downstream local agent contract schema: `6` - Current capability standard: `0.5` — [`docs/capability-standard.md`](capability-standard.md) - Current capability lock schema: `0.6` — [`docs/capability-lock-schema.v0.6.json`](capability-lock-schema.v0.6.json) - Current capability lock diff schema: `0.7` — [`docs/capability-lock-diff-schema.v0.7.json`](capability-lock-diff-schema.v0.7.json) -- Current attestation schema: `0.4` — [`docs/attestation-schema.v0.4.json`](attestation-schema.v0.4.json) -- Current registry schema: `0.3` — [`docs/registry-schema.v0.3.json`](registry-schema.v0.3.json) -- Current org evidence bundle schema: `shipgate.org_evidence_bundle/v1` — [`docs/org-evidence-bundle-schema.v1.json`](org-evidence-bundle-schema.v1.json) +- Current attestation schema: `0.5` — [`docs/attestation-schema.v0.5.json`](attestation-schema.v0.5.json) +- Current registry schema: `0.4` — [`docs/registry-schema.v0.4.json`](registry-schema.v0.4.json) +- Current org evidence bundle schema: `shipgate.org_evidence_bundle/v2` — [`docs/org-evidence-bundle-schema.v2.json`](org-evidence-bundle-schema.v2.json) - Current host-grants inventory, baseline, and drift schemas: `0.2` — [`inventory`](host-grants-inventory-schema.v0.2.json), [`baseline`](host-grants-baseline-schema.v0.2.json), [`drift`](host-grants-drift-schema.v0.2.json) - Current trigger catalog schema: `0.2` — [`docs/triggers.json`](triggers.json) - Current governance benchmark catalog schema: `0.2` — [`docs/governance-benchmark-catalog-schema.v0.2.json`](governance-benchmark-catalog-schema.v0.2.json) - Current governance benchmark result schema: `0.2` — [`docs/governance-benchmark-result-schema.v0.2.json`](governance-benchmark-result-schema.v0.2.json) -- Frozen-reference report schemas: frozen [`v0.32`](report-schema.v0.32.json), frozen [`v0.31`](report-schema.v0.31.json), frozen [`v0.30`](report-schema.v0.30.json), frozen [`v0.29`](report-schema.v0.29.json), frozen [`v0.28`](report-schema.v0.28.json), frozen [`v0.27`](report-schema.v0.27.json), frozen [`v0.26`](report-schema.v0.26.json), frozen [`v0.25`](report-schema.v0.25.json), frozen [`v0.24`](report-schema.v0.24.json), frozen [`v0.23`](report-schema.v0.23.json), frozen [`v0.22`](report-schema.v0.22.json), frozen [`v0.21`](report-schema.v0.21.json), frozen [`v0.20`](report-schema.v0.20.json), frozen [`v0.19`](report-schema.v0.19.json), frozen [`v0.18`](report-schema.v0.18.json), frozen [`v0.17`](report-schema.v0.17.json), frozen [`v0.16`](report-schema.v0.16.json), frozen [`v0.15`](report-schema.v0.15.json), frozen [`v0.14`](report-schema.v0.14.json), frozen [`v0.13`](report-schema.v0.13.json), frozen [`v0.12`](report-schema.v0.12.json), frozen [`v0.11`](report-schema.v0.11.json), frozen [`v0.10`](report-schema.v0.10.json), frozen [`v0.9`](report-schema.v0.9.json), frozen [`v0.8`](report-schema.v0.8.json), frozen [`v0.7`](report-schema.v0.7.json), frozen [`v0.6`](report-schema.v0.6.json), older +- Frozen-reference report schemas: frozen [`v0.33`](report-schema.v0.33.json), frozen [`v0.32`](report-schema.v0.32.json), frozen [`v0.31`](report-schema.v0.31.json), frozen [`v0.30`](report-schema.v0.30.json), and older versions listed in [`docs/INDEX.md`](INDEX.md#reference) - Frozen-reference packet schemas live in [`docs/INDEX.md`](INDEX.md#reference). -- Boundary v1, verifier v0.1/v0.2/v0.3, verify-run v1, handoff v1/v2/v3, and preflight +- Boundary v1, verifier v0.1–v0.4, verify-run v1/v2, handoff v1–v4, and preflight v0.1/v0.2 remain frozen references for legacy readers. - Frozen experimental capability lock and governance benchmark result schemas live in [`docs/INDEX.md`](INDEX.md#reference). @@ -1106,11 +1116,13 @@ one decision engine. - **PR / controller flow** — an autonomous coding agent deciding *continue, repair, or stop*. Prefer + validate `agents-shipgate-reports/verification-receipt.json`, then read `agents-shipgate-reports/agent-handoff.json` for the compact - `shipgate.agent_handoff/v4` view: lead with `control.state`, then read + `shipgate.agent_handoff/v5` view: lead with `control.state`, then read `control.next_action`, `gate.merge_verdict`, and `reproducibility.run_id` for the - stable verify identity. `verifier.json` remains the authoritative controller - substrate and `verify-run.json` remains the reproducibility record; finally + content-addressed verify identity. `verifier.json` remains the authoritative + controller substrate and `verify-run.json` remains the detailed run + projection; finally confirm `report.json.release_decision.decision` for the release gate. `.well-known/agents-shipgate.json` → `agent_read_order` is the machine-readable cross-artifact order. `verifier_read_order` remains the @@ -1300,7 +1312,7 @@ separately), local input hashes (`config_sha256`, `baseline_sha256`, emitted files. It has no wall-clock timestamp and is not a second gate. `agents-shipgate-reports/agent-handoff.json` carries -`schema_version: "shipgate.agent_handoff/v4"` and top-level sections +`schema_version: "shipgate.agent_handoff/v5"` and top-level sections `gate`, `control`, `fix_task`, `blocked_by[]`, `remediation_plan[]`, `capability_review`, `reproducibility`, and `artifacts`. `gate.decision` mirrors `release_decision.decision`; `gate.merge_verdict` @@ -1317,8 +1329,8 @@ from existing artifacts with: agents-shipgate agent handoff --from agents-shipgate-reports/verifier.json --json ``` -In `agents-shipgate-reports/verifier.json`, read the v0.4 fields below (full -schema [`docs/verifier-schema.v0.4.json`](verifier-schema.v0.4.json)). **Lead +In `agents-shipgate-reports/verifier.json`, read the v0.5 fields below (full +schema [`docs/verifier-schema.v0.5.json`](verifier-schema.v0.5.json)). **Lead with `control.state`.** Every field below is a mirror or deterministic projection of `report.json`; `release_decision.decision` remains the gate. @@ -1350,7 +1362,7 @@ with `control.state`.** Every field below is a mirror or deterministic projectio - `decision` — mirror of `release_decision.decision` (or `null` when no scan ran). - `headline` — single-sentence, PR-comment-friendly summary (or `null`). - `control.human_review` and `control.next_action` are the only serialized - human-review and next-action authority in verifier v0.4. + human-review and next-action authority in verifier v0.5. - `AgentController`, `VerifierNextAction`, and `VerifierHumanReview` remain importable only as deprecated v0.1/v0.2 reader models. Verifier v0.4 does not emit or invoke the retired `build_agent_controller` projector. @@ -1544,8 +1556,8 @@ For reviewer-shaped output, also read the **Release Evidence Packet** at `[pdf]` extras are installed). The packet is a supporting/provisional reviewer projection, not a second gate. Packet outputs are redacted by the same default privacy layer as the report. The packet has fixed reviewer sections governed by -[`docs/packet-schema.v0.11.json`](packet-schema.v0.11.json) — see -[STABILITY.md §Release Evidence Packet](../STABILITY.md#release-evidence-packet-v011). +[`docs/packet-schema.v0.12.json`](packet-schema.v0.12.json) — see +[STABILITY.md §Release Evidence Packet](../STABILITY.md#release-evidence-packet-v012). Packet schema `0.9` carries the report's evidence-backed semantic coverage and gap remediation contract. Packet §1 also mirrors `static_analysis_only=true`, `runtime_behavior_verified=false`, and @@ -1598,9 +1610,9 @@ Companion prompt: [`prompts/explain-finding-to-user.md`](../prompts/explain-find - [STABILITY.md](../STABILITY.md) — full alpha stability contract. Source of truth for everything above. - [AGENTS.md](../AGENTS.md) — agent-facing instructions: install, run, single-turn flow, error semantics. -- [`docs/report-schema.v0.33.json`](report-schema.v0.33.json) — machine-validatable JSON Schema for the current report. +- [`docs/report-schema.v0.34.json`](report-schema.v0.34.json) — machine-validatable JSON Schema for the current report. - [`docs/privacy.md`](privacy.md) and [`docs/report-sensitive-fields.json`](report-sensitive-fields.json) — default redaction behavior and sensitive-field inventory. -- [`docs/packet-schema.v0.11.json`](packet-schema.v0.11.json) — machine-validatable JSON Schema for the current packet. +- [`docs/packet-schema.v0.12.json`](packet-schema.v0.12.json) — machine-validatable JSON Schema for the current packet. - [`docs/checks.json`](checks.json) — check catalog, including `mvp_tier` for MVP/readiness triage. ## See also @@ -3142,7 +3154,7 @@ from the hash so toggling `--suggest-patches` doesn't shift it. - [`checks.md`](checks.md) — full check catalog with rationale. - [`minimal-real-configs.md`](minimal-real-configs.md) — per-framework minimal manifests to build from. -- [`report-schema.v0.33.json`](report-schema.v0.33.json) — current JSON +- [`report-schema.v0.34.json`](report-schema.v0.34.json) — current JSON Schema for `report.json`. - [`AGENTS.md`](../AGENTS.md) — top-level agent instructions, install, trigger table. diff --git a/llms.txt b/llms.txt index 94006bad..c97b9c16 100644 --- a/llms.txt +++ b/llms.txt @@ -13,7 +13,7 @@ - Publisher URL: https://threemoonslab.com/ - License: Apache-2.0 - Latest public release: v0.15.0 -- Current source-tree runtime: 0.16.0b5 (contract 16; pre-release) +- Current source-tree runtime: 0.16.0b6 (contract 17; pre-release) - Canonical repository: https://github.com/ThreeMoonsLab/agents-shipgate - Do not use: Agent Shipcheck, Agent Shipgate, agents shipgate, Agents-Shipgate @@ -61,15 +61,17 @@ - Markdown report: `agents-shipgate-reports/report.md`. - JSON report: `agents-shipgate-reports/report.json`. -- JSON report schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/report-schema.v0.33.json +- JSON report schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/report-schema.v0.34.json - Evidence-backed passed contract: https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/passed-verdict-contract.md - Machine verdict boundary: `release_decision.static_analysis_only=true`, `runtime_behavior_verified=false`, plus `static_verdict_disclaimer`; packet §1 mirrors these fields. - Release Evidence Packet (Markdown / JSON / HTML, optional PDF): `agents-shipgate-reports/packet.{md,json,html}`. -- Packet schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/packet-schema.v0.11.json +- Packet schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/packet-schema.v0.12.json - SARIF report: `agents-shipgate-reports/report.sarif`. - Verifier orchestration record (ongoing-PR verify): `agents-shipgate-reports/verifier.json`. - Agent handoff artifact (preferred compact agent projection for verify): `agents-shipgate-reports/agent-handoff.json`. -- Agent handoff schema: https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/agent-handoff-schema.v4.json +- Terminal verification receipt (read and validate first): `agents-shipgate-reports/verification-receipt.json`. +- Verification receipt schema: https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/verification-receipt-schema.v1.json +- Agent handoff schema: https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/agent-handoff-schema.v5.json - PR comment (ongoing-PR verify): `agents-shipgate-reports/pr-comment.md`. - Proactive preflight routing JSON: `agents-shipgate preflight --workspace . --plan - --json` emits `preflight_schema_version: "0.3"`; switch on `control.state`. It routes protected-surface edits, host permission requests, and high-risk capability evidence gaps but is not a release verdict. - Preflight schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/preflight-schema.v0.3.json @@ -78,9 +80,9 @@ - Verify base capability lock and diff, when the base scan can be materialized: `agents-shipgate-reports/base.capabilities.lock.json`, `agents-shipgate-reports/capability-lock-diff.{json,md}`. - Capability lock schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/capability-lock-schema.v0.6.json - Capability lock diff schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/capability-lock-diff-schema.v0.7.json -- Attestation schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/attestation-schema.v0.4.json -- Registry schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/registry-schema.v0.3.json -- Org evidence bundle schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/org-evidence-bundle-schema.v1.json +- Attestation schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/attestation-schema.v0.5.json +- Registry schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/registry-schema.v0.4.json +- Org evidence bundle schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/org-evidence-bundle-schema.v2.json - Host-grants inventory schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/host-grants-inventory-schema.v0.2.json - Host-grants baseline schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/host-grants-baseline-schema.v0.2.json - Host-grants drift schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/host-grants-drift-schema.v0.2.json @@ -100,7 +102,7 @@ - Before editing protected surfaces, run `agents-shipgate preflight --workspace . --plan - --json` with a `PreflightPlanV1` object; stop when `requires_human_review` is true. - Configure the minimal preview route: `agents-shipgate init --workspace . --write --json`. CI and agent-instruction trust roots are separate reviewed changes. - Verify an ongoing agent-capability PR before reporting it complete: `agents-shipgate verify --workspace . --config shipgate.yaml --base origin/main --head HEAD --ci-mode advisory --format json`. -- Read `agent-handoff.json` first and switch on `control.state`; then read `gate.merge_verdict`, `gate.can_merge_without_human`, `next_action`, `fix_task`, and `capability_review.top_changes`. The unified control model requires runtime contract 14. Then read `report.json.release_decision.decision`; it is the release gate. +- Validate `verification-receipt.json` first, then read `agent-handoff.json` and switch on `control.state`; read `gate.merge_verdict`, `gate.can_merge_without_human`, `next_action`, `fix_task`, and `capability_review.top_changes`. The unified control model requires runtime contract 14. Then read `report.json.release_decision.decision`; it is the release gate. - Run the verify-native demo fixture: `agents-shipgate fixture run ai_generated_refund_pr`. - Scan a static repo/report fixture when you need the lower-level report path: `agents-shipgate fixture run support_refund_agent`. - Export a stable static capability lock: `agents-shipgate capability export -c shipgate.yaml`. @@ -155,16 +157,17 @@ - Zero-install detector (stdlib-only Python; same structural verdict as `agents-shipgate detect --json` — emits the canonical `DetectResult` fields plus `script_version`, but NOT the CLI's `diagnostics` or `next_actions` arrays): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/tools/shipgate-detect.py - Zero-install paths overview (single-file detector, uvx, GitHub Action): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/zero-install.md - Manifest schema: https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/manifest-v0.1.json -- Report schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/report-schema.v0.33.json +- Report schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/report-schema.v0.34.json - Privacy/redaction docs: https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/privacy.md -- Packet schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/packet-schema.v0.11.json +- Packet schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/packet-schema.v0.12.json - Preflight schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/preflight-schema.v0.3.json - Capability standard: https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/capability-standard.md - Capability lock schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/capability-lock-schema.v0.6.json -- Capability lock diff schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/capability-lock-diff-schema.v0.5.json +- Capability lock diff schema (current): https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/capability-lock-diff-schema.v0.7.json - Governance benchmark: https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/governance-benchmark.md - Current agent contract: https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/agent-contract-current.md -- Agent handoff schema: https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/agent-handoff-schema.v4.json +- Verification identity contract: https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/verification-reproducibility.md +- Agent handoff schema: https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/agent-handoff-schema.v5.json ## Category vocabulary diff --git a/plugins/agents-shipgate/.codex-plugin/plugin.json b/plugins/agents-shipgate/.codex-plugin/plugin.json index 7b1ee7d9..092f8aab 100644 --- a/plugins/agents-shipgate/.codex-plugin/plugin.json +++ b/plugins/agents-shipgate/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agents-shipgate", - "version": "0.16.0b5", + "version": "0.16.0b6", "description": "Run Agents Shipgate Tool-Use Readiness workflows from Codex.", "author": { "name": "Three Moons Lab", diff --git a/plugins/agents-shipgate/skills/agents-shipgate/SKILL.md b/plugins/agents-shipgate/skills/agents-shipgate/SKILL.md index 11fbbeaf..8a5b6d38 100644 --- a/plugins/agents-shipgate/skills/agents-shipgate/SKILL.md +++ b/plugins/agents-shipgate/skills/agents-shipgate/SKILL.md @@ -20,7 +20,7 @@ Do not use it for general linting, runtime monitoring, evals, model-output quali 5. Default first-time CI to advisory mode. Do not enable release-blocking CI or save a baseline until a human has reviewed current findings. 6. For local agent control, run `shipgate check --agent codex --workspace . --format agent-boundary-json` and read the stdout `shipgate.agent_boundary_result/v1` object. Switch on `control.state`; follow only `control.next_action`, `control.allowed_next_commands`, and `control.human_review`. Treat `decision` as diagnostic context only. 7. Before editing `shipgate.yaml`, Shipgate CI, AGENTS/CLAUDE/Cursor rules, policy packs, baselines, waivers, suppressions, Codex hooks/config, Codex plugin manifests, `.mcp.json`, `.app.json`, or `SKILL.md`, plan to run `agents-shipgate verify` before completion and route trust-root review to a human when the verifier requires it. -8. For full PR verification, read `agents-shipgate-reports/agent-handoff.json` first and switch on `control.state`, then read `verifier.json` for detailed control state, `verify-run.json` for reproducibility metadata, and `report.json` for reviewer detail; `report.json.release_decision.decision` remains the release gate. +8. For full PR verification, validate `agents-shipgate-reports/verification-receipt.json` first, then read `agent-handoff.json` and switch on `control.state`; read `verifier.json` for detailed control state, `verify-run.json` for the request graph, and `report.json` for reviewer detail. `report.json.release_decision.decision` remains the release gate. 9. Auto-apply only high-confidence safe patches. Do not auto-assert action effect, action authority, agent bindings, approval, confirmation, idempotency, broad-scope, prohibited-action, or runtime-trace evidence. 10. Ensure `.gitignore` covers `agents-shipgate-reports/` before committing. diff --git a/plugins/agents-shipgate/skills/agents-shipgate/references/report-reading.md b/plugins/agents-shipgate/skills/agents-shipgate/references/report-reading.md index db3e7974..87c649eb 100644 --- a/plugins/agents-shipgate/skills/agents-shipgate/references/report-reading.md +++ b/plugins/agents-shipgate/skills/agents-shipgate/references/report-reading.md @@ -1,6 +1,7 @@ # Reading Agents Shipgate Reports -For verify runs, read `agents-shipgate-reports/agent-handoff.json` first. Then +For verify runs, validate `agents-shipgate-reports/verification-receipt.json` +first. Then read `agents-shipgate-reports/agent-handoff.json`. After that, read `agents-shipgate-reports/verifier.json` for detailed control context and `agents-shipgate-reports/report.json` for findings. Do not scrape Markdown. diff --git a/plugins/claude-code/.claude-plugin/plugin.json b/plugins/claude-code/.claude-plugin/plugin.json index ea734e6c..2a13b891 100644 --- a/plugins/claude-code/.claude-plugin/plugin.json +++ b/plugins/claude-code/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agents-shipgate", - "version": "0.16.0b5", + "version": "0.16.0b6", "description": "Run Agents Shipgate Tool-Use Readiness workflows from Claude Code. Skill-only: the plugin supplies the agents-shipgate skill and the /agents-shipgate:shipgate command; the scanner runs through the agents-shipgate CLI installed in the local environment (pipx install agents-shipgate, runtime contract 15). For the deterministic hooks (trigger check after edits, full verify at Stop), run: agents-shipgate install-hooks --target claude-code --write.", "author": { "name": "Three Moons Lab", diff --git a/plugins/claude-code/commands/shipgate.md b/plugins/claude-code/commands/shipgate.md index ee65d5d8..55f75f8c 100644 --- a/plugins/claude-code/commands/shipgate.md +++ b/plugins/claude-code/commands/shipgate.md @@ -48,7 +48,8 @@ Required behavior (do not skip): 1. Set `AGENTS_SHIPGATE_AGENT_MODE=1` for every CLI call so errors emit a `next_action` JSON line on stderr. 2. Run `agents-shipgate contract --json` when available and use it to verify the installed CLI's schema versions and gating signal. -3. For verifier runs, parse `agents-shipgate-reports/agent-handoff.json` first, +3. For verifier runs, validate `agents-shipgate-reports/verification-receipt.json` first, + then parse `agents-shipgate-reports/agent-handoff.json`, then `verifier.json`, `verify-run.json`, and `report.json.release_decision.decision` as the release gate. 4. For check runs, parse stdout as `shipgate.agent_boundary_result/v1` and @@ -77,7 +78,8 @@ permissions, policies, CI gates, or `shipgate.yaml`, run the verifier: agents-shipgate verify --base origin/main --head HEAD --json ``` -Read `agents-shipgate-reports/agent-handoff.json` first and lead with +Validate `agents-shipgate-reports/verification-receipt.json` first, then read +`agents-shipgate-reports/agent-handoff.json` and lead with `gate.merge_verdict` (a deterministic projection of `release_decision.decision`, which remains the gate in `report.json`), then the authoritative substrate `agents-shipgate-reports/verifier.json` and supporting/provisional diff --git a/plugins/claude-code/skills/agents-shipgate/SKILL.md b/plugins/claude-code/skills/agents-shipgate/SKILL.md index 12caf111..07b593f5 100644 --- a/plugins/claude-code/skills/agents-shipgate/SKILL.md +++ b/plugins/claude-code/skills/agents-shipgate/SKILL.md @@ -48,7 +48,7 @@ Always: 1. Set `AGENTS_SHIPGATE_AGENT_MODE=1` so errors emit a `next_action` JSON line on stderr (auto-enabled inside Claude Code via the harness's `CLAUDECODE=1` env var, and Cursor via `CURSOR_TRACE_ID`). 2. For local agent control, run `shipgate check --agent claude-code --workspace . --format agent-boundary-json` and read the stdout `shipgate.agent_boundary_result/v1` object. Switch on `control.state`; follow only `control.next_action`, `control.allowed_next_commands`, and `control.human_review`. Treat `decision` as diagnostic context only. -3. For verifier runs, parse `agents-shipgate-reports/agent-handoff.json` first, then `verifier.json` and `verify-run.json`: +3. For verifier runs, validate `agents-shipgate-reports/verification-receipt.json` first, then parse `agent-handoff.json`, `verifier.json`, and `verify-run.json`: `control.state`, `merge_verdict`, `can_merge_without_human`, `control.next_action`, `fix_task`, and `capability_review.top_changes`. Then parse `agents-shipgate-reports/report.json.release_decision.decision`; it is the @@ -74,12 +74,13 @@ For non-GitHub CI (GitLab, CircleCI, Jenkins, Azure Pipelines, Buildkite, Bitbuc - **CLI surface** follows the current 0.x contract line — see https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/STABILITY.md. - **Installed CLI contract**: when available, run `agents-shipgate contract --json` to verify local schema versions, capability/research surfaces, `release_decision.decision`, and manual-review signal fields. Older installs should use [`docs/agent-contract-current.md`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/agent-contract-current.md) or upgrade before automating against the local contract command. -- **Verifier JSON**: `verifier_schema_version: "0.3"`. Switch on `control.state`, then read `merge_verdict`, `can_merge_without_human`, `control.next_action`, `fix_task`, `capability_review.top_changes`, `trust_root_touched`, and `policy_weakened` before summarizing an AI-generated PR. `merge_verdict` is a deterministic projection; the gate remains `report.json.release_decision.decision`. -- **Verify run JSON**: `verify-run.json` uses `schema_version: "shipgate.verify_run/v2"` and records stable run identity, subject refs, input hashes, execution, applicability, release outcome, and artifact hashes. It is the reproducibility artifact for `verify`; do not treat it as a second gate. -- **Report JSON**: `report_schema_version: "0.33"`. Read `release_decision.decision` first. A `passed` decision requires a complete root-reachable static binding graph plus complete, conflict-free identity, effect, and authority evidence for every reachable action; it does not prove runtime behavior. Preserve `release_decision.static_analysis_only=true`, `runtime_behavior_verified=false`, and `static_verdict_disclaimer` in summaries. Read `release_decision.evidence_coverage.binding_coverage`, `semantic_coverage`, `identity_coverage`, and `policy_gap_count`, then work every `evidence_gaps[].next_action` in order. Binding, semantic, and policy-applicability gaps are not Findings and cannot be suppressed, baselined, severity-overridden, cleared by `--no-heuristics`, or satisfied by `human_ack`; binding, effect, and authority declarations are human assertions and must never be auto-written. Use `tool_catalog[]` for diagnostics and `tool_inventory[]` for the proven reachable surface. The current schema is [`docs/report-schema.v0.33.json`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.33.json); v0.32 is a frozen compatibility reference. See the [current agent contract](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/agent-contract-current.md#read-these-first-for-release-gating) and [evidence-backed passed contract](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/passed-verdict-contract.md). +- **Verifier JSON**: `verifier_schema_version: "0.5"`. Switch on `control.state`, then read `merge_verdict`, `can_merge_without_human`, `control.next_action`, `fix_task`, `capability_review.top_changes`, `trust_root_touched`, and `policy_weakened` before summarizing an AI-generated PR. `merge_verdict` is a deterministic projection; the gate remains `report.json.release_decision.decision`. +- **Verification receipt**: `verification-receipt.json` uses `schema_version: "shipgate.verification_receipt/v1"` and is written last. Validate it before trusting any projected verdict; it content-addresses the request, executor, unit result, decision, and complete artifact set. +- **Verify run JSON**: `verify-run.json` uses `schema_version: "shipgate.verify_run/v3"`, embeds the content-addressed plan and executor, and binds unit-result and decision IDs. `run_id` is an exact compatibility alias of `request_id`; do not treat the run projection as a second gate. +- **Report JSON**: `report_schema_version: "0.34"`. Read `release_decision.decision` first. A `passed` decision requires a complete root-reachable static binding graph plus complete, conflict-free identity, effect, and authority evidence for every reachable action; it does not prove runtime behavior. Preserve `release_decision.static_analysis_only=true`, `runtime_behavior_verified=false`, and `static_verdict_disclaimer` in summaries. Read `release_decision.evidence_coverage.binding_coverage`, `semantic_coverage`, `identity_coverage`, and `policy_gap_count`, then work every `evidence_gaps[].next_action` in order. Binding, semantic, and policy-applicability gaps are not Findings and cannot be suppressed, baselined, severity-overridden, cleared by `--no-heuristics`, or satisfied by `human_ack`; binding, effect, and authority declarations are human assertions and must never be auto-written. Use `tool_catalog[]` for diagnostics and `tool_inventory[]` for the proven reachable surface. The current schema is [`docs/report-schema.v0.34.json`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.34.json); v0.33 is a frozen compatibility reference. See the [current agent contract](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/agent-contract-current.md), [verification identity contract](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/verification-reproducibility.md), and [evidence-backed passed contract](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/passed-verdict-contract.md). These binding-backed fields require runtime contract v13; the unambiguous `AgentControl` projection requires runtime contract v14. A contract-v12 CLI emits the frozen v0.30 report and must not be described using the v0.31 binding claim. -- **Release Evidence Packet**: `agents-shipgate-reports/packet.{md,json,html}` (and `packet.pdf` with the `[pdf]` extras) is a supporting/provisional reviewer artifact. Packet v0.11 projects binding and semantic coverage under the release decision; it never creates a second gate. See [`docs/packet-schema.v0.11.json`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/packet-schema.v0.11.json) and [STABILITY.md §Release Evidence Packet](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/STABILITY.md#release-evidence-packet-v011). -- **Capability standard**: `agents-shipgate capability export` emits a stable static capability lock (`capability_lock_schema_version: "0.5"`) and `agents-shipgate capability diff` emits a stable semantic diff (`capability_lock_diff_schema_version: "0.6"`). These artifacts implement capability standard v0.4, are supporting/provisional and non-gating, exclude runtime trace evidence, and are documented in [`docs/capability-standard.md`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/capability-standard.md). +- **Release Evidence Packet**: `agents-shipgate-reports/packet.{md,json,html}` (and `packet.pdf` with the `[pdf]` extras) is a supporting/provisional reviewer artifact. Packet v0.12 projects the verification request and decision IDs plus binding and semantic coverage; it never creates a second gate. See [`docs/packet-schema.v0.12.json`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/packet-schema.v0.12.json) and [STABILITY.md §Release Evidence Packet](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/STABILITY.md#release-evidence-packet-v012). +- **Capability standard**: `agents-shipgate capability export` emits a stable static capability lock (`capability_lock_schema_version: "0.6"`) and `agents-shipgate capability diff` emits a stable semantic diff (`capability_lock_diff_schema_version: "0.7"`). These artifacts implement capability standard v0.5, are supporting/provisional and non-gating, exclude runtime trace evidence, and are documented in [`docs/capability-standard.md`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/capability-standard.md). - **Governance benchmark**: `benchmark/agent-pr-governance/cases.yaml` and `scripts/run_governance_benchmark.py` are the stable research benchmark substrate (`governance_benchmark_result_schema_version: "0.2"`), not a release gate. See [`docs/governance-benchmark.md`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/governance-benchmark.md). - **Single source of truth for the contract**: [`docs/agent-contract-current.md`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/agent-contract-current.md). When the schema bumps, that file updates first. - **Exit codes**: `0` pass, `2` config error, `3` parse error, `4` other error, `20` strict-mode gate failure. diff --git a/pyproject.toml b/pyproject.toml index 86f6ffea..07a1845e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "agents-shipgate" -version = "0.16.0b5" +version = "0.16.0b6" description = "The deterministic merge gate for AI-generated agent capability changes. Agent release readiness for tool-using AI agents. CLI + GitHub Action. Scans MCP, OpenAPI, OpenAI Agents SDK, Anthropic, Google ADK, LangChain, CrewAI, OpenAI API, Codex config, Codex plugin, n8n, Conductor OSS workflow JSON." readme = "README.md" requires-python = ">=3.12" diff --git a/samples/conductor_agent/expected/report.json b/samples/conductor_agent/expected/report.json index def63ab6..4ba10ed1 100644 --- a/samples/conductor_agent/expected/report.json +++ b/samples/conductor_agent/expected/report.json @@ -1,7 +1,12 @@ { "schema_version": "0.1", - "report_schema_version": "0.33", + "report_schema_version": "0.34", "run_id": "agents_shipgate_68a230995652f841", + "request_id": null, + "subject_id": null, + "input_set_id": null, + "engine_requirement_id": null, + "decision_id": null, "manifest_dir": "/samples/conductor_agent", "project": { "name": "conductor-agent" @@ -1369,4 +1374,4 @@ } ] } -} \ No newline at end of file +} diff --git a/samples/simple_crewai_agent/expected/report.json b/samples/simple_crewai_agent/expected/report.json index ca9df653..544d66eb 100644 --- a/samples/simple_crewai_agent/expected/report.json +++ b/samples/simple_crewai_agent/expected/report.json @@ -1,7 +1,12 @@ { "schema_version": "0.1", - "report_schema_version": "0.33", + "report_schema_version": "0.34", "run_id": "agents_shipgate_9702bae6947b59b3", + "request_id": null, + "subject_id": null, + "input_set_id": null, + "engine_requirement_id": null, + "decision_id": null, "manifest_dir": "/samples/simple_crewai_agent", "project": { "name": "simple-crewai-agent" @@ -2706,4 +2711,4 @@ } ] } -} \ No newline at end of file +} diff --git a/samples/simple_langchain_agent/expected/report.json b/samples/simple_langchain_agent/expected/report.json index 8e7bdb9b..b8f43001 100644 --- a/samples/simple_langchain_agent/expected/report.json +++ b/samples/simple_langchain_agent/expected/report.json @@ -1,7 +1,12 @@ { "schema_version": "0.1", - "report_schema_version": "0.33", + "report_schema_version": "0.34", "run_id": "agents_shipgate_ad594d77df1d5943", + "request_id": null, + "subject_id": null, + "input_set_id": null, + "engine_requirement_id": null, + "decision_id": null, "manifest_dir": "/samples/simple_langchain_agent", "project": { "name": "simple-langchain-agent" @@ -1694,4 +1699,4 @@ "human_ack_satisfied": true, "top_reason_codes": [] } -} \ No newline at end of file +} diff --git a/samples/simple_openai_api_agent/expected/report.json b/samples/simple_openai_api_agent/expected/report.json index 8f25c66c..db08551b 100644 --- a/samples/simple_openai_api_agent/expected/report.json +++ b/samples/simple_openai_api_agent/expected/report.json @@ -1,7 +1,12 @@ { "schema_version": "0.1", - "report_schema_version": "0.33", - "run_id": "agents_shipgate_151b2a90d910c866", + "report_schema_version": "0.34", + "run_id": "agents_shipgate_156121deae9690a5", + "request_id": null, + "subject_id": null, + "input_set_id": null, + "engine_requirement_id": null, + "decision_id": null, "manifest_dir": "/samples/simple_openai_api_agent", "project": { "name": "simple-openai-api-agent", @@ -91,16 +96,41 @@ "confidence": "high", "policy_eligible": true, "blocking_eligible": true, - "claim_ids": [], + "claim_ids": [ + "clm_d7302d32eaefab6d707f" + ], "evidence_bases": [ - "protocol_structure" + "protocol_structure", + "reviewed_declaration" ], "predicates": [ { - "predicate": "deterministic_action_rule", + "predicate": "builtin_control_effect", + "status": "matched", + "expected": [ + "external_communication" + ], + "observed": [ + "external_communication" + ], + "confidence": "high", + "claim_ids": [ + "clm_d7302d32eaefab6d707f" + ], + "evidence_bases": [ + "reviewed_declaration" + ], + "policy_eligible": true, + "why": null + }, + { + "predicate": "missing_builtin_controls", "status": "matched", - "expected": null, - "observed": "agent:simple-openai-api-agent/api-refund-assistant:action_v2_9d10be42545cd7457766e72c52ab9ba754eee7aa97c9613ae60ba4bc4048af82", + "expected": "all required controls present", + "observed": [ + "confirmation.required", + "safeguards.audit_log" + ], "confidence": "high", "claim_ids": [], "evidence_bases": [ @@ -110,7 +140,7 @@ "why": null } ], - "support_hash": "sha256:0b26a3779bfbd1539b1d6c1a4168e605f9bfaafc0af7186a4a7565d49b47d64c" + "support_hash": "sha256:56f41ff5a1d00010eb3d3cc46c67ae554a73b51a4ac1688bc60f225100f7d002" } }, { @@ -139,16 +169,42 @@ "confidence": "high", "policy_eligible": true, "blocking_eligible": true, - "claim_ids": [], + "claim_ids": [ + "clm_5a97ce2724d487c4af0c" + ], "evidence_bases": [ - "protocol_structure" + "protocol_structure", + "reviewed_declaration" ], "predicates": [ { - "predicate": "deterministic_action_rule", + "predicate": "builtin_control_effect", "status": "matched", - "expected": null, - "observed": "agent:simple-openai-api-agent/api-refund-assistant:action_v2_b5ba1b1612ff86ebfe4ad43459c8c26a79e44eb61a5a2f7496ab5d9166b4cfd5", + "expected": [ + "financial_write" + ], + "observed": [ + "financial_write" + ], + "confidence": "high", + "claim_ids": [ + "clm_5a97ce2724d487c4af0c" + ], + "evidence_bases": [ + "reviewed_declaration" + ], + "policy_eligible": true, + "why": null + }, + { + "predicate": "missing_builtin_controls", + "status": "matched", + "expected": "all required controls present", + "observed": [ + "approval.required", + "safeguards.audit_log", + "safeguards.idempotency" + ], "confidence": "high", "claim_ids": [], "evidence_bases": [ @@ -158,7 +214,7 @@ "why": null } ], - "support_hash": "sha256:7045a6360ba4f27a7b90fc4b3b138b391332f05307f836677791b41ffbd8b972" + "support_hash": "sha256:dbcc11262ca197ff45370bda3751529808664ab2f1b50314ad816bc79138600c" } } ], @@ -2872,16 +2928,41 @@ "confidence": "high", "policy_eligible": true, "blocking_eligible": true, - "claim_ids": [], + "claim_ids": [ + "clm_d7302d32eaefab6d707f" + ], "evidence_bases": [ - "protocol_structure" + "protocol_structure", + "reviewed_declaration" ], "predicates": [ { - "predicate": "deterministic_action_rule", + "predicate": "builtin_control_effect", + "status": "matched", + "expected": [ + "external_communication" + ], + "observed": [ + "external_communication" + ], + "confidence": "high", + "claim_ids": [ + "clm_d7302d32eaefab6d707f" + ], + "evidence_bases": [ + "reviewed_declaration" + ], + "policy_eligible": true, + "why": null + }, + { + "predicate": "missing_builtin_controls", "status": "matched", - "expected": null, - "observed": "agent:simple-openai-api-agent/api-refund-assistant:action_v2_9d10be42545cd7457766e72c52ab9ba754eee7aa97c9613ae60ba4bc4048af82", + "expected": "all required controls present", + "observed": [ + "confirmation.required", + "safeguards.audit_log" + ], "confidence": "high", "claim_ids": [], "evidence_bases": [ @@ -2891,7 +2972,7 @@ "why": null } ], - "support_hash": "sha256:0b26a3779bfbd1539b1d6c1a4168e605f9bfaafc0af7186a4a7565d49b47d64c" + "support_hash": "sha256:56f41ff5a1d00010eb3d3cc46c67ae554a73b51a4ac1688bc60f225100f7d002" }, "capability_trace_refs": [], "recommendation": "Declare confirmation policy and safeguards.audit_log for this external communication action.", @@ -2941,16 +3022,42 @@ "confidence": "high", "policy_eligible": true, "blocking_eligible": true, - "claim_ids": [], + "claim_ids": [ + "clm_5a97ce2724d487c4af0c" + ], "evidence_bases": [ - "protocol_structure" + "protocol_structure", + "reviewed_declaration" ], "predicates": [ { - "predicate": "deterministic_action_rule", + "predicate": "builtin_control_effect", "status": "matched", - "expected": null, - "observed": "agent:simple-openai-api-agent/api-refund-assistant:action_v2_b5ba1b1612ff86ebfe4ad43459c8c26a79e44eb61a5a2f7496ab5d9166b4cfd5", + "expected": [ + "financial_write" + ], + "observed": [ + "financial_write" + ], + "confidence": "high", + "claim_ids": [ + "clm_5a97ce2724d487c4af0c" + ], + "evidence_bases": [ + "reviewed_declaration" + ], + "policy_eligible": true, + "why": null + }, + { + "predicate": "missing_builtin_controls", + "status": "matched", + "expected": "all required controls present", + "observed": [ + "approval.required", + "safeguards.audit_log", + "safeguards.idempotency" + ], "confidence": "high", "claim_ids": [], "evidence_bases": [ @@ -2960,7 +3067,7 @@ "why": null } ], - "support_hash": "sha256:7045a6360ba4f27a7b90fc4b3b138b391332f05307f836677791b41ffbd8b972" + "support_hash": "sha256:dbcc11262ca197ff45370bda3751529808664ab2f1b50314ad816bc79138600c" }, "capability_trace_refs": [], "recommendation": "Declare approval.required, safeguards.audit_log, and safeguards.idempotency for this financial write action.", @@ -4070,4 +4177,4 @@ } ] } -} \ No newline at end of file +} diff --git a/samples/support_refund_agent/expected/packet.html b/samples/support_refund_agent/expected/packet.html index 75a89a84..6a3d0e15 100644 --- a/samples/support_refund_agent/expected/packet.html +++ b/samples/support_refund_agent/expected/packet.html @@ -26,4 +26,4 @@ .status-missing { color: #7f1d1d; } .status-informational { color: #555; } .meta { color: #555; font-size: 0.92rem; } -

Release Evidence Packet

Project: support-refund-agent · Agent: refund-assistant · Environment: production_like
Run id: agents_shipgate_e25de5ef59b7d500 · Generated at: 2026-01-01T00:00:00+00:00 · Packet schema: 0.11

This packet is a reviewer-shaped synthesis of a static Agents Shipgate scan. See §10 for what the packet does not prove.

§1 Release decision — BLOCKED

  • Decision: blocked
  • Reason: 5 active findings block release.
  • Blockers: 5
  • Review items: 9

CI gate behavior (informational)

  • ci_mode: advisory, would_fail_ci: false, exit code: 0
  • Note: CI behavior is metadata about the run gate, not the verdict. The verdict above derives from release_decision.decision.

Static semantic coverage

  • Pass-eligible actions: 0/7
  • Evidence gaps: 7
  • Known authority review concerns: 1
  • Reasons: conflicting_binding_evidence=7, unscoped_authority=1

Blockers

  • SHIP-POLICY-APPROVAL-MISSING (critical): stripe.create_refund lacks a declared approval policy
  • SHIP-SIDEFX-IDEMPOTENCY-MISSING (critical): stripe.create_refund lacks idempotency evidence
  • SHIP-ACTION-EXTERNAL-COMMUNICATION-AUDIT-MISSING (high): gmail.send_customer_email has external communication capability without required controls
  • SHIP-ACTION-EXTERNAL-COMMUNICATION-AUDIT-MISSING (high): stripe.create_refund has external communication capability without required controls
  • SHIP-ACTION-FINANCIAL-WRITE-CONTROL-MISSING (critical): stripe.create_refund has financial write capability without required controls

Review items

  • SHIP-AUTH-MANIFEST-BROAD-SCOPE (high): Manifest declares broad permission scopes
  • SHIP-AUTH-SCOPE-COVERAGE-MISSING (high): shopify.cancel_order requires scopes not declared in the manifest
  • SHIP-AUTH-SCOPE-COVERAGE-MISSING (high): support.search_kb requires scopes not declared in the manifest
  • SHIP-AUTH-MISSING-SCOPE (high): refund_status_lookup lacks declared auth scopes
  • SHIP-AUTH-SCOPE-COVERAGE-MISSING (high): gmail.send_customer_email requires scopes not declared in the manifest
  • SHIP-POLICY-CONFIRMATION-MISSING (high): stripe.create_refund lacks a declared confirmation policy
  • SHIP-POLICY-CONFIRMATION-MISSING (high): gmail.send_customer_email lacks a declared confirmation policy
  • SHIP-MANIFEST-HIGH-RISK-OWNER-MISSING (high): shopify.cancel_order is high-risk but has no owner
  • SHIP-MANIFEST-UNUSED-SCOPE (medium): Manifest declares unused permission scope zendesk:tickets:read

§1A Evidence matrix — compact review summary

  • Evidence Matrix Light is derived from public report.json only. Release decisions, CI exit behavior, and baseline semantics remain owned by release_decision. Domain rows intentionally overlap; a single finding can appear in multiple rows when it is relevant to each review lens.
DomainEvidence presentEvidence sourceConfidenceMissing controlsBlocking findingsReview items
Inventorycoveredtool_inventory; tool_surface; +1 moremedium
Schemacoveredtool_surface_facts.tools[].hashesmedium
Authpartialtool_surface_facts.scopes; tool_inventory[].auth_scopes; +1 moremixedSHIP-AUTH-MANIFEST-BROAD-SCOPE: Manifest declares broad permission scopes; SHIP-AUTH-SCOPE-COVERAGE-MISSING on shopify.cancel_order: shopify.cancel_order requires scopes not declared in the manifest; +4 moreSHIP-AUTH-MANIFEST-BROAD-SCOPE (high); SHIP-AUTH-SCOPE-COVERAGE-MISSING (high); +4 more
Approvalpartialtool_surface_facts.controls[kind=approval_policy]; findings[]highSHIP-POLICY-APPROVAL-MISSING on stripe.create_refund: stripe.create_refund lacks a declared approval policy; SHIP-ACTION-FINANCIAL-WRITE-CONTROL-MISSING on stripe.create_refund: stripe.create_refund has financial write capability without required controlsSHIP-POLICY-APPROVAL-MISSING (critical); SHIP-ACTION-FINANCIAL-WRITE-CONTROL-MISSING (critical)
Confirmationpartialtool_surface_facts.controls[kind=confirmation_policy]; findings[]highSHIP-POLICY-CONFIRMATION-MISSING on stripe.create_refund: stripe.create_refund lacks a declared confirmation policy; SHIP-POLICY-CONFIRMATION-MISSING on gmail.send_customer_email: gmail.send_customer_email lacks a declared confirmation policySHIP-POLICY-CONFIRMATION-MISSING (high); SHIP-POLICY-CONFIRMATION-MISSING (high)
Idempotencypartialtool_surface_facts.controls[kind=idempotency_evidence]; action_surface_facts.actions[].safeguards.idempotency; +1 morehighSHIP-SIDEFX-IDEMPOTENCY-MISSING on stripe.create_refund: stripe.create_refund lacks idempotency evidence; SHIP-ACTION-FINANCIAL-WRITE-CONTROL-MISSING on stripe.create_refund: stripe.create_refund has financial write capability without required controlsSHIP-SIDEFX-IDEMPOTENCY-MISSING (critical); SHIP-ACTION-FINANCIAL-WRITE-CONTROL-MISSING (critical)
Side effectspartialtool_inventory[].risk_tags; action_surface_facts.actions[].effect; +1 morehighSHIP-POLICY-APPROVAL-MISSING on stripe.create_refund: stripe.create_refund lacks a declared approval policy; SHIP-POLICY-CONFIRMATION-MISSING on stripe.create_refund: stripe.create_refund lacks a declared confirmation policy; +5 moreSHIP-POLICY-APPROVAL-MISSING (critical); SHIP-SIDEFX-IDEMPOTENCY-MISSING (critical); +3 moreSHIP-POLICY-CONFIRMATION-MISSING (high); SHIP-POLICY-CONFIRMATION-MISSING (high)
Memory isolationnot_declaredunknown
Human-in-the-loop evidencenot_declaredunknown
Prompt/scope alignmentcovereddeclared_intentions; misalignments; +1 moremedium
Retry/timeoutnot_declaredunknown
Baseline debtinformationalunknown
Action-surface policypartialaction_surface_facts.actions; findings[].blocks_release; +1 morehighSHIP-ACTION-EXTERNAL-COMMUNICATION-AUDIT-MISSING on gmail.send_customer_email: gmail.send_customer_email has external communication capability without required controls; SHIP-ACTION-EXTERNAL-COMMUNICATION-AUDIT-MISSING on stripe.create_refund: stripe.create_refund has external communication capability without required controls; +1 moreSHIP-ACTION-EXTERNAL-COMMUNICATION-AUDIT-MISSING (high); SHIP-ACTION-EXTERNAL-COMMUNICATION-AUDIT-MISSING (high); +1 more

§2 Capability ↔ Intent diff — covered

Declared

  • Purpose: answer refund policy questions
  • Purpose: prepare refund requests for human review
  • Purpose: update support ticket notes
  • Prohibited: issue refund without approval
  • Prohibited: cancel order without explicit confirmation
  • Prohibited: send external email without preview

Observed tools

  • gmail.send_customer_email
  • refund_status_lookup
  • send_email_preview
  • shopify.cancel_order
  • stripe.create_refund
  • support.search_kb
  • zendesk.update_ticket

§3 High-risk tool surface — partial

Total tools: 7 · High-risk: 5

ToolSourceRisk tagsApprovalIdempotency
gmail.send_customer_emailmcpcustomer_communication, external_writenono
send_email_previewmcpread_onlynono
shopify.cancel_orderopenapidestructive, writeyesyes
stripe.create_refundopenapiexternal_write, financial_action, writenono
support.search_kbmcpread_onlynono

§3A Tool-surface diff — not declared

Status: disabled — No --diff-from report or v0.3 baseline snapshot was provided.
Base: none

§3B Action-surface diff — not declared

Status: disabled — No action-surface comparison source was provided.
Base: none

§4 Approval policy coverage — partial

ToolDeclaredSourceGap finding(s)
shopify.cancel_orderyespolicies
stripe.create_refundnofp_973ea0ef2110ca9a

Gap findings

  • SHIP-POLICY-APPROVAL-MISSING (critical): stripe.create_refund lacks a declared approval policy

§5 Idempotency / retry risk — partial

Retry policy: not declared

ToolDeclaredSourceGap finding(s)
shopify.cancel_orderyespolicies
stripe.create_refundnofp_2cf0d6c77d9c3eee

Gap findings

  • SHIP-SIDEFX-IDEMPOTENCY-MISSING (critical): stripe.create_refund lacks idempotency evidence

§6 Scope coverage — missing

Declared scopes

  • zendesk:tickets:read
  • zendesk:tickets:write
  • stripe:*
ScopeDeclaredUsed by tools
gmail:sendnogmail.send_customer_email
shopify:orders:writenoshopify.cancel_order
stripe:*yes
stripe:refunds:writeyesstripe.create_refund
support:kb:readnosupport.search_kb
zendesk:tickets:readyes
zendesk:tickets:writeyeszendesk.update_ticket

Unused declared scopes

  • zendesk:tickets:read

Used by tools but not declared

  • gmail:send
  • shopify:orders:write
  • support:kb:read

Gap findings

  • SHIP-AUTH-SCOPE-COVERAGE-MISSING (high): shopify.cancel_order requires scopes not declared in the manifest
  • SHIP-AUTH-SCOPE-COVERAGE-MISSING (high): support.search_kb requires scopes not declared in the manifest
  • SHIP-AUTH-SCOPE-COVERAGE-MISSING (high): gmail.send_customer_email requires scopes not declared in the manifest
  • SHIP-MANIFEST-UNUSED-SCOPE (medium): Manifest declares unused permission scope zendesk:tickets:read

§7 Memory isolation — not declared

Manifest does not declare a memory isolation policy. The current manifest schema (v0.1) has no agent.memory field. See §10 for the residual review item.

§8 Human-in-the-loop evidence — covered

  • Configured: yes
  • Human review recommended: yes
  • Provenance mode: fresh_scan
  • Capability-linked trace rows: 0/0 matched (0 source(s))
  • HITL evidence is local review evidence only. Missing local evidence does not prove a runtime control is absent, and present local evidence does not certify runtime enforcement.

Approval-required tools

  • shopify.cancel_order

Confirmation-required tools

  • shopify.cancel_order

§9 Required dynamic scenarios — partial

  • Manual review for SHIP-ACTION-EXTERNAL-COMMUNICATION-AUDIT-MISSING — Declare confirmation policy and safeguards.audit_log for this external communication action.
    Related finding(s): fp_1c94d2d2693dccdf, fp_e042ce7813b97a2d
  • Manual review for SHIP-ACTION-FINANCIAL-WRITE-CONTROL-MISSING — Declare approval.required, safeguards.audit_log, and safeguards.idempotency for this financial write action.
    Related finding(s): fp_dfa27ad5b52d8fd6
  • Manual review for SHIP-AUTH-MANIFEST-BROAD-SCOPE — Replace broad manifest permission scopes with the narrowest scopes needed for this release.
    Related finding(s): fp_df4a990cca9f936b
  • Manual review for SHIP-AUTH-MISSING-SCOPE — Declare operation-specific auth scopes for refund_status_lookup, or explicitly declare anonymous authority when the operation requires no credentials.
    Related finding(s): fp_519cb82f038efd10
  • Manual review for SHIP-AUTH-SCOPE-COVERAGE-MISSING — Add the required scopes for shopify.cancel_order to permissions.scopes or narrow the tool's declared auth requirements.
    Related finding(s): fp_095f3a5337124f6e, fp_1fd01b4ed2e41d51, fp_24d610b0d4324190
  • Manual review for SHIP-MANIFEST-HIGH-RISK-OWNER-MISSING — Declare an owner for each high-risk production tool in risk_overrides.tools.
    Related finding(s): fp_674fa79ae9993422
  • Manual review for SHIP-MANIFEST-UNUSED-SCOPE — Remove unused manifest scopes or add tool metadata showing why they are required.
    Related finding(s): fp_609d62f4dc434961
  • Manual review for SHIP-POLICY-APPROVAL-MISSING — Declare an approval policy for stripe.create_refund or remove this tool from the release.
    Related finding(s): fp_973ea0ef2110ca9a
  • Manual review for SHIP-POLICY-CONFIRMATION-MISSING — Declare a user confirmation policy for stripe.create_refund or remove this action from the release.
    Related finding(s): fp_c762eebfadaf39d9, fp_fae2921fd2d0cbd5
  • Manual review for SHIP-SIDEFX-IDEMPOTENCY-MISSING — Add an idempotency key, idempotent annotation, or declared idempotency policy for stripe.create_refund.
    Related finding(s): fp_2cf0d6c77d9c3eee
  • Re-run scan after resolving source warnings — Source loaders emitted warnings; some tool surfaces may have been parsed with reduced confidence.

§10 What this packet did NOT prove

Agents Shipgate is an advisory tool: the deterministic merge gate for AI-generated agent capability changes, run as a local-first, static Tool-Use Readiness review. The packet below is derived from a scan; it does not, by itself, prove the following properties:

  • Prompt robustness. Whether the agent's prompt holds up under jailbreaks, persona drift, indirect prompt injection, or adversarial inputs.
  • Runtime behavior. Whether the agent actually invokes only the declared tools, respects approval gates at runtime, or follows policy under load. Static config is not runtime evidence.
  • Model correctness. Whether the underlying model produces correct outputs, calls the right tools, or stays within the declared scope. The packet does not benchmark the model.
  • Adversarial resistance. Whether the agent withstands red-team or penetration testing. The packet does not run scenarios; it organizes evidence.

Per-run residuals

  • Source warnings:
    • MCP source declares wildcard tool exposure
  • Low-confidence tool extractions: none
  • Suppressed findings in effect: none
  • Memory isolation is not modeled by the v0.1 manifest schema; no static evidence is available.
+

Release Evidence Packet

Project: support-refund-agent · Agent: refund-assistant · Environment: production_like
Run id: agents_shipgate_e25de5ef59b7d500 · Generated at: 2026-01-01T00:00:00+00:00 · Packet schema: 0.12

This packet is a reviewer-shaped synthesis of a static Agents Shipgate scan. See §10 for what the packet does not prove.

§1 Release decision — BLOCKED

  • Decision: blocked
  • Reason: 5 active findings block release.
  • Blockers: 5
  • Review items: 9

CI gate behavior (informational)

  • ci_mode: advisory, would_fail_ci: false, exit code: 0
  • Note: CI behavior is metadata about the run gate, not the verdict. The verdict above derives from release_decision.decision.

Static semantic coverage

  • Pass-eligible actions: 0/7
  • Evidence gaps: 7
  • Known authority review concerns: 1
  • Reasons: conflicting_binding_evidence=7, unscoped_authority=1

Blockers

  • SHIP-POLICY-APPROVAL-MISSING (critical): stripe.create_refund lacks a declared approval policy
  • SHIP-SIDEFX-IDEMPOTENCY-MISSING (critical): stripe.create_refund lacks idempotency evidence
  • SHIP-ACTION-EXTERNAL-COMMUNICATION-AUDIT-MISSING (high): gmail.send_customer_email has external communication capability without required controls
  • SHIP-ACTION-EXTERNAL-COMMUNICATION-AUDIT-MISSING (high): stripe.create_refund has external communication capability without required controls
  • SHIP-ACTION-FINANCIAL-WRITE-CONTROL-MISSING (critical): stripe.create_refund has financial write capability without required controls

Review items

  • SHIP-AUTH-MANIFEST-BROAD-SCOPE (high): Manifest declares broad permission scopes
  • SHIP-AUTH-SCOPE-COVERAGE-MISSING (high): shopify.cancel_order requires scopes not declared in the manifest
  • SHIP-AUTH-SCOPE-COVERAGE-MISSING (high): support.search_kb requires scopes not declared in the manifest
  • SHIP-AUTH-MISSING-SCOPE (high): refund_status_lookup lacks declared auth scopes
  • SHIP-AUTH-SCOPE-COVERAGE-MISSING (high): gmail.send_customer_email requires scopes not declared in the manifest
  • SHIP-POLICY-CONFIRMATION-MISSING (high): stripe.create_refund lacks a declared confirmation policy
  • SHIP-POLICY-CONFIRMATION-MISSING (high): gmail.send_customer_email lacks a declared confirmation policy
  • SHIP-MANIFEST-HIGH-RISK-OWNER-MISSING (high): shopify.cancel_order is high-risk but has no owner
  • SHIP-MANIFEST-UNUSED-SCOPE (medium): Manifest declares unused permission scope zendesk:tickets:read

§1A Evidence matrix — compact review summary

  • Evidence Matrix Light is derived from public report.json only. Release decisions, CI exit behavior, and baseline semantics remain owned by release_decision. Domain rows intentionally overlap; a single finding can appear in multiple rows when it is relevant to each review lens.
DomainEvidence presentEvidence sourceConfidenceMissing controlsBlocking findingsReview items
Inventorycoveredtool_inventory; tool_surface; +1 moremedium
Schemacoveredtool_surface_facts.tools[].hashesmedium
Authpartialtool_surface_facts.scopes; tool_inventory[].auth_scopes; +1 moremixedSHIP-AUTH-MANIFEST-BROAD-SCOPE: Manifest declares broad permission scopes; SHIP-AUTH-SCOPE-COVERAGE-MISSING on shopify.cancel_order: shopify.cancel_order requires scopes not declared in the manifest; +4 moreSHIP-AUTH-MANIFEST-BROAD-SCOPE (high); SHIP-AUTH-SCOPE-COVERAGE-MISSING (high); +4 more
Approvalpartialtool_surface_facts.controls[kind=approval_policy]; findings[]highSHIP-POLICY-APPROVAL-MISSING on stripe.create_refund: stripe.create_refund lacks a declared approval policy; SHIP-ACTION-FINANCIAL-WRITE-CONTROL-MISSING on stripe.create_refund: stripe.create_refund has financial write capability without required controlsSHIP-POLICY-APPROVAL-MISSING (critical); SHIP-ACTION-FINANCIAL-WRITE-CONTROL-MISSING (critical)
Confirmationpartialtool_surface_facts.controls[kind=confirmation_policy]; findings[]highSHIP-POLICY-CONFIRMATION-MISSING on stripe.create_refund: stripe.create_refund lacks a declared confirmation policy; SHIP-POLICY-CONFIRMATION-MISSING on gmail.send_customer_email: gmail.send_customer_email lacks a declared confirmation policySHIP-POLICY-CONFIRMATION-MISSING (high); SHIP-POLICY-CONFIRMATION-MISSING (high)
Idempotencypartialtool_surface_facts.controls[kind=idempotency_evidence]; action_surface_facts.actions[].safeguards.idempotency; +1 morehighSHIP-SIDEFX-IDEMPOTENCY-MISSING on stripe.create_refund: stripe.create_refund lacks idempotency evidence; SHIP-ACTION-FINANCIAL-WRITE-CONTROL-MISSING on stripe.create_refund: stripe.create_refund has financial write capability without required controlsSHIP-SIDEFX-IDEMPOTENCY-MISSING (critical); SHIP-ACTION-FINANCIAL-WRITE-CONTROL-MISSING (critical)
Side effectspartialtool_inventory[].risk_tags; action_surface_facts.actions[].effect; +1 morehighSHIP-POLICY-APPROVAL-MISSING on stripe.create_refund: stripe.create_refund lacks a declared approval policy; SHIP-POLICY-CONFIRMATION-MISSING on stripe.create_refund: stripe.create_refund lacks a declared confirmation policy; +5 moreSHIP-POLICY-APPROVAL-MISSING (critical); SHIP-SIDEFX-IDEMPOTENCY-MISSING (critical); +3 moreSHIP-POLICY-CONFIRMATION-MISSING (high); SHIP-POLICY-CONFIRMATION-MISSING (high)
Memory isolationnot_declaredunknown
Human-in-the-loop evidencenot_declaredunknown
Prompt/scope alignmentcovereddeclared_intentions; misalignments; +1 moremedium
Retry/timeoutnot_declaredunknown
Baseline debtinformationalunknown
Action-surface policypartialaction_surface_facts.actions; findings[].blocks_release; +1 morehighSHIP-ACTION-EXTERNAL-COMMUNICATION-AUDIT-MISSING on gmail.send_customer_email: gmail.send_customer_email has external communication capability without required controls; SHIP-ACTION-EXTERNAL-COMMUNICATION-AUDIT-MISSING on stripe.create_refund: stripe.create_refund has external communication capability without required controls; +1 moreSHIP-ACTION-EXTERNAL-COMMUNICATION-AUDIT-MISSING (high); SHIP-ACTION-EXTERNAL-COMMUNICATION-AUDIT-MISSING (high); +1 more

§2 Capability ↔ Intent diff — covered

Declared

  • Purpose: answer refund policy questions
  • Purpose: prepare refund requests for human review
  • Purpose: update support ticket notes
  • Prohibited: issue refund without approval
  • Prohibited: cancel order without explicit confirmation
  • Prohibited: send external email without preview

Observed tools

  • gmail.send_customer_email
  • refund_status_lookup
  • send_email_preview
  • shopify.cancel_order
  • stripe.create_refund
  • support.search_kb
  • zendesk.update_ticket

§3 High-risk tool surface — partial

Total tools: 7 · High-risk: 5

ToolSourceRisk tagsApprovalIdempotency
gmail.send_customer_emailmcpcustomer_communication, external_writenono
send_email_previewmcpread_onlynono
shopify.cancel_orderopenapidestructive, writeyesyes
stripe.create_refundopenapiexternal_write, financial_action, writenono
support.search_kbmcpread_onlynono

§3A Tool-surface diff — not declared

Status: disabled — No --diff-from report or v0.3 baseline snapshot was provided.
Base: none

§3B Action-surface diff — not declared

Status: disabled — No action-surface comparison source was provided.
Base: none

§4 Approval policy coverage — partial

ToolDeclaredSourceGap finding(s)
shopify.cancel_orderyespolicies
stripe.create_refundnofp_973ea0ef2110ca9a

Gap findings

  • SHIP-POLICY-APPROVAL-MISSING (critical): stripe.create_refund lacks a declared approval policy

§5 Idempotency / retry risk — partial

Retry policy: not declared

ToolDeclaredSourceGap finding(s)
shopify.cancel_orderyespolicies
stripe.create_refundnofp_2cf0d6c77d9c3eee

Gap findings

  • SHIP-SIDEFX-IDEMPOTENCY-MISSING (critical): stripe.create_refund lacks idempotency evidence

§6 Scope coverage — missing

Declared scopes

  • zendesk:tickets:read
  • zendesk:tickets:write
  • stripe:*
ScopeDeclaredUsed by tools
gmail:sendnogmail.send_customer_email
shopify:orders:writenoshopify.cancel_order
stripe:*yes
stripe:refunds:writeyesstripe.create_refund
support:kb:readnosupport.search_kb
zendesk:tickets:readyes
zendesk:tickets:writeyeszendesk.update_ticket

Unused declared scopes

  • zendesk:tickets:read

Used by tools but not declared

  • gmail:send
  • shopify:orders:write
  • support:kb:read

Gap findings

  • SHIP-AUTH-SCOPE-COVERAGE-MISSING (high): shopify.cancel_order requires scopes not declared in the manifest
  • SHIP-AUTH-SCOPE-COVERAGE-MISSING (high): support.search_kb requires scopes not declared in the manifest
  • SHIP-AUTH-SCOPE-COVERAGE-MISSING (high): gmail.send_customer_email requires scopes not declared in the manifest
  • SHIP-MANIFEST-UNUSED-SCOPE (medium): Manifest declares unused permission scope zendesk:tickets:read

§7 Memory isolation — not declared

Manifest does not declare a memory isolation policy. The current manifest schema (v0.1) has no agent.memory field. See §10 for the residual review item.

§8 Human-in-the-loop evidence — covered

  • Configured: yes
  • Human review recommended: yes
  • Provenance mode: fresh_scan
  • Capability-linked trace rows: 0/0 matched (0 source(s))
  • HITL evidence is local review evidence only. Missing local evidence does not prove a runtime control is absent, and present local evidence does not certify runtime enforcement.

Approval-required tools

  • shopify.cancel_order

Confirmation-required tools

  • shopify.cancel_order

§9 Required dynamic scenarios — partial

  • Manual review for SHIP-ACTION-EXTERNAL-COMMUNICATION-AUDIT-MISSING — Declare confirmation policy and safeguards.audit_log for this external communication action.
    Related finding(s): fp_1c94d2d2693dccdf, fp_e042ce7813b97a2d
  • Manual review for SHIP-ACTION-FINANCIAL-WRITE-CONTROL-MISSING — Declare approval.required, safeguards.audit_log, and safeguards.idempotency for this financial write action.
    Related finding(s): fp_dfa27ad5b52d8fd6
  • Manual review for SHIP-AUTH-MANIFEST-BROAD-SCOPE — Replace broad manifest permission scopes with the narrowest scopes needed for this release.
    Related finding(s): fp_df4a990cca9f936b
  • Manual review for SHIP-AUTH-MISSING-SCOPE — Declare operation-specific auth scopes for refund_status_lookup, or explicitly declare anonymous authority when the operation requires no credentials.
    Related finding(s): fp_519cb82f038efd10
  • Manual review for SHIP-AUTH-SCOPE-COVERAGE-MISSING — Add the required scopes for shopify.cancel_order to permissions.scopes or narrow the tool's declared auth requirements.
    Related finding(s): fp_095f3a5337124f6e, fp_1fd01b4ed2e41d51, fp_24d610b0d4324190
  • Manual review for SHIP-MANIFEST-HIGH-RISK-OWNER-MISSING — Declare an owner for each high-risk production tool in risk_overrides.tools.
    Related finding(s): fp_674fa79ae9993422
  • Manual review for SHIP-MANIFEST-UNUSED-SCOPE — Remove unused manifest scopes or add tool metadata showing why they are required.
    Related finding(s): fp_609d62f4dc434961
  • Manual review for SHIP-POLICY-APPROVAL-MISSING — Declare an approval policy for stripe.create_refund or remove this tool from the release.
    Related finding(s): fp_973ea0ef2110ca9a
  • Manual review for SHIP-POLICY-CONFIRMATION-MISSING — Declare a user confirmation policy for stripe.create_refund or remove this action from the release.
    Related finding(s): fp_c762eebfadaf39d9, fp_fae2921fd2d0cbd5
  • Manual review for SHIP-SIDEFX-IDEMPOTENCY-MISSING — Add an idempotency key, idempotent annotation, or declared idempotency policy for stripe.create_refund.
    Related finding(s): fp_2cf0d6c77d9c3eee
  • Re-run scan after resolving source warnings — Source loaders emitted warnings; some tool surfaces may have been parsed with reduced confidence.

§10 What this packet did NOT prove

Agents Shipgate is an advisory tool: the deterministic merge gate for AI-generated agent capability changes, run as a local-first, static Tool-Use Readiness review. The packet below is derived from a scan; it does not, by itself, prove the following properties:

  • Prompt robustness. Whether the agent's prompt holds up under jailbreaks, persona drift, indirect prompt injection, or adversarial inputs.
  • Runtime behavior. Whether the agent actually invokes only the declared tools, respects approval gates at runtime, or follows policy under load. Static config is not runtime evidence.
  • Model correctness. Whether the underlying model produces correct outputs, calls the right tools, or stays within the declared scope. The packet does not benchmark the model.
  • Adversarial resistance. Whether the agent withstands red-team or penetration testing. The packet does not run scenarios; it organizes evidence.

Per-run residuals

  • Source warnings:
    • MCP source declares wildcard tool exposure
  • Low-confidence tool extractions: none
  • Suppressed findings in effect: none
  • Memory isolation is not modeled by the v0.1 manifest schema; no static evidence is available.
diff --git a/samples/support_refund_agent/expected/packet.json b/samples/support_refund_agent/expected/packet.json index c7540271..6f33e7d1 100644 --- a/samples/support_refund_agent/expected/packet.json +++ b/samples/support_refund_agent/expected/packet.json @@ -202,6 +202,7 @@ ], "status": "covered" }, + "decision_id": null, "dynamic_scenarios": { "scenarios": [ { @@ -286,6 +287,7 @@ ], "status": "partial" }, + "engine_requirement_id": null, "environment": { "promotion_from": "staging", "promotion_to": "production_like", @@ -1643,6 +1645,7 @@ ], "status": "partial" }, + "input_set_id": null, "memory_isolation": { "is_declared": false, "notes": "Manifest does not declare a memory isolation policy. The current manifest schema (v0.1) has no agent.memory field. See \u00a710 for the residual review item.", @@ -1677,7 +1680,7 @@ } ] }, - "packet_schema_version": "0.11", + "packet_schema_version": "0.12", "project": { "name": "support-refund-agent", "owner": "support-platform", @@ -2696,6 +2699,7 @@ "static_verdict_disclaimer": "This verdict covers deterministic static evidence only. Agents Shipgate did not execute the agent or prove runtime behavior, tool routing, credential enforcement, or safety.", "verdict": "BLOCKED" }, + "request_id": null, "run_id": "agents_shipgate_e25de5ef59b7d500", "scope_coverage": { "declared_scopes": [ @@ -2871,6 +2875,7 @@ "zendesk:tickets:read" ] }, + "subject_id": null, "tool_surface_diff": { "base_kind": "none", "enabled": false, diff --git a/samples/support_refund_agent/expected/packet.md b/samples/support_refund_agent/expected/packet.md index 137664d1..48a254b1 100644 --- a/samples/support_refund_agent/expected/packet.md +++ b/samples/support_refund_agent/expected/packet.md @@ -5,7 +5,7 @@ - Environment: production\_like - Run id: agents\_shipgate\_e25de5ef59b7d500 - Generated at: 2026-01-01T00:00:00\+00:00 -- Packet schema: 0\.11 +- Packet schema: 0\.12 This packet is a reviewer-shaped synthesis of a static Agents Shipgate scan. See §10 for what the packet does *not* prove. diff --git a/samples/support_refund_agent/expected/report.json b/samples/support_refund_agent/expected/report.json index 48e84572..3e03d485 100644 --- a/samples/support_refund_agent/expected/report.json +++ b/samples/support_refund_agent/expected/report.json @@ -1,7 +1,12 @@ { "schema_version": "0.1", - "report_schema_version": "0.33", - "run_id": "agents_shipgate_ff9302d24e1b6891", + "report_schema_version": "0.34", + "run_id": "agents_shipgate_e25de5ef59b7d500", + "request_id": null, + "subject_id": null, + "input_set_id": null, + "engine_requirement_id": null, + "decision_id": null, "manifest_dir": "/samples/support_refund_agent", "project": { "name": "support-refund-agent", @@ -180,16 +185,45 @@ "confidence": "high", "policy_eligible": true, "blocking_eligible": true, - "claim_ids": [], + "claim_ids": [ + "clm_188aecba394542ac226f", + "clm_7ec6f046c34a18b719f2", + "clm_e6c199e33c2159754789" + ], "evidence_bases": [ - "protocol_structure" + "protocol_structure", + "reviewed_declaration" ], "predicates": [ { - "predicate": "deterministic_action_rule", + "predicate": "builtin_control_effect", + "status": "matched", + "expected": [ + "external_communication" + ], + "observed": [ + "external_communication" + ], + "confidence": "high", + "claim_ids": [ + "clm_188aecba394542ac226f", + "clm_7ec6f046c34a18b719f2", + "clm_e6c199e33c2159754789" + ], + "evidence_bases": [ + "reviewed_declaration" + ], + "policy_eligible": true, + "why": null + }, + { + "predicate": "missing_builtin_controls", "status": "matched", - "expected": null, - "observed": "agent:support-refund-agent/refund-assistant:action_v2_49bfadd2eb7605a1065c24081f890481ee2f37e9718152ae533c8e43ddaab17a", + "expected": "all required controls present", + "observed": [ + "confirmation.required", + "safeguards.audit_log" + ], "confidence": "high", "claim_ids": [], "evidence_bases": [ @@ -199,7 +233,7 @@ "why": null } ], - "support_hash": "sha256:2da4af706588e6b22a8dcb7b25419cf3fa22214d1fb76ea543afef8f4936a3f4" + "support_hash": "sha256:0ca4ffebad9baaac2e0095108c61fdf4722cfcc4c6e68932eb47dee4583d7df2" } }, { @@ -228,16 +262,41 @@ "confidence": "high", "policy_eligible": true, "blocking_eligible": true, - "claim_ids": [], + "claim_ids": [ + "clm_1fee9ee6ea224541b56d" + ], "evidence_bases": [ - "protocol_structure" + "protocol_structure", + "reviewed_declaration" ], "predicates": [ { - "predicate": "deterministic_action_rule", + "predicate": "builtin_control_effect", + "status": "matched", + "expected": [ + "external_communication" + ], + "observed": [ + "external_communication" + ], + "confidence": "high", + "claim_ids": [ + "clm_1fee9ee6ea224541b56d" + ], + "evidence_bases": [ + "reviewed_declaration" + ], + "policy_eligible": true, + "why": null + }, + { + "predicate": "missing_builtin_controls", "status": "matched", - "expected": null, - "observed": "agent:support-refund-agent/refund-assistant:action_v2_6f2d18a55f2189a165089034897d52ada547bb19afe20036cb7ec3119c2d95ca", + "expected": "all required controls present", + "observed": [ + "confirmation.required", + "safeguards.audit_log" + ], "confidence": "high", "claim_ids": [], "evidence_bases": [ @@ -247,7 +306,7 @@ "why": null } ], - "support_hash": "sha256:742ea8da905eef5df5d2f9a3e9bea9a3bbc84eb4710409e8f41c5389f86af494" + "support_hash": "sha256:335b539322d07aad72eebeff04ace89b40c354da4ee45ba40b52ef885296fe4d" } }, { @@ -276,16 +335,44 @@ "confidence": "high", "policy_eligible": true, "blocking_eligible": true, - "claim_ids": [], + "claim_ids": [ + "clm_019b1bf05a33e2269c31", + "clm_c8e45c8de58cf645c026" + ], "evidence_bases": [ - "protocol_structure" + "protocol_structure", + "reviewed_declaration" ], "predicates": [ { - "predicate": "deterministic_action_rule", + "predicate": "builtin_control_effect", + "status": "matched", + "expected": [ + "financial_write" + ], + "observed": [ + "financial_write" + ], + "confidence": "high", + "claim_ids": [ + "clm_019b1bf05a33e2269c31", + "clm_c8e45c8de58cf645c026" + ], + "evidence_bases": [ + "reviewed_declaration" + ], + "policy_eligible": true, + "why": null + }, + { + "predicate": "missing_builtin_controls", "status": "matched", - "expected": null, - "observed": "agent:support-refund-agent/refund-assistant:action_v2_6f2d18a55f2189a165089034897d52ada547bb19afe20036cb7ec3119c2d95ca", + "expected": "all required controls present", + "observed": [ + "approval.required", + "safeguards.audit_log", + "safeguards.idempotency" + ], "confidence": "high", "claim_ids": [], "evidence_bases": [ @@ -295,7 +382,7 @@ "why": null } ], - "support_hash": "sha256:742ea8da905eef5df5d2f9a3e9bea9a3bbc84eb4710409e8f41c5389f86af494" + "support_hash": "sha256:624f2e0f92ae88c456cfad5b76f245bf721c166c9f0cc25afe0a0c06b68f7718" } } ], @@ -728,6 +815,54 @@ "requires_human_review": true } }, + { + "kind": "mixed_policy_evidence", + "subject": "support.search_kb [tool_v2_445a251aa6adc1c69f7b318b68f7ad47f01b629d3c24d723331a661735cd353a]", + "source_type": null, + "source_ref": ".agents-shipgate/mcp-tools.json", + "why": "builtin-effect-control-applicability: Policy applicability mixes authoritative and heuristic evidence.", + "next_action": { + "kind": "review_policy_evidence", + "command": "agents-shipgate verify --workspace . --config shipgate.yaml --ci-mode advisory --format json", + "path": "shipgate.yaml#action_surface.actions[tool='support.search_kb'].effect", + "why": "Heuristic or unknown evidence cannot create a blocking policy finding.", + "expects": "Provide reviewed or structural evidence for every indeterminate predicate.", + "accepted_values": [ + "reviewed_declaration", + "protocol_structure", + "typed_provider_fact", + "structural_scope" + ], + "suggested_patch_kind": "manual", + "declaration_template": null, + "auto_apply": false, + "requires_human_review": true + } + }, + { + "kind": "mixed_policy_evidence", + "subject": "send_email_preview [tool_v2_2c9ee6ae2b9f0259ba2dad40a30fb18a8bdc3a732ffc207936fdd49c80e1bc9e]", + "source_type": null, + "source_ref": "inventories/sdk-tools.json", + "why": "builtin-effect-control-applicability: Policy applicability mixes authoritative and heuristic evidence.", + "next_action": { + "kind": "review_policy_evidence", + "command": "agents-shipgate verify --workspace . --config shipgate.yaml --ci-mode advisory --format json", + "path": "shipgate.yaml#action_surface.actions[tool='send_email_preview'].effect", + "why": "Heuristic or unknown evidence cannot create a blocking policy finding.", + "expects": "Provide reviewed or structural evidence for every indeterminate predicate.", + "accepted_values": [ + "reviewed_declaration", + "protocol_structure", + "typed_provider_fact", + "structural_scope" + ], + "suggested_patch_kind": "manual", + "declaration_template": null, + "auto_apply": false, + "requires_human_review": true + } + }, { "kind": "inferred_policy_applicability", "subject": "tool_v2_2c9ee6ae2b9f0259ba2dad40a30fb18a8bdc3a732ffc207936fdd49c80e1bc9e", @@ -946,7 +1081,7 @@ "conflicting_binding_evidence": 1 } }, - "policy_gap_count": 7 + "policy_gap_count": 9 }, "baseline_delta": { "enabled": false, @@ -6154,16 +6289,45 @@ "confidence": "high", "policy_eligible": true, "blocking_eligible": true, - "claim_ids": [], + "claim_ids": [ + "clm_188aecba394542ac226f", + "clm_7ec6f046c34a18b719f2", + "clm_e6c199e33c2159754789" + ], "evidence_bases": [ - "protocol_structure" + "protocol_structure", + "reviewed_declaration" ], "predicates": [ { - "predicate": "deterministic_action_rule", + "predicate": "builtin_control_effect", "status": "matched", - "expected": null, - "observed": "agent:support-refund-agent/refund-assistant:action_v2_49bfadd2eb7605a1065c24081f890481ee2f37e9718152ae533c8e43ddaab17a", + "expected": [ + "external_communication" + ], + "observed": [ + "external_communication" + ], + "confidence": "high", + "claim_ids": [ + "clm_188aecba394542ac226f", + "clm_7ec6f046c34a18b719f2", + "clm_e6c199e33c2159754789" + ], + "evidence_bases": [ + "reviewed_declaration" + ], + "policy_eligible": true, + "why": null + }, + { + "predicate": "missing_builtin_controls", + "status": "matched", + "expected": "all required controls present", + "observed": [ + "confirmation.required", + "safeguards.audit_log" + ], "confidence": "high", "claim_ids": [], "evidence_bases": [ @@ -6173,7 +6337,7 @@ "why": null } ], - "support_hash": "sha256:2da4af706588e6b22a8dcb7b25419cf3fa22214d1fb76ea543afef8f4936a3f4" + "support_hash": "sha256:0ca4ffebad9baaac2e0095108c61fdf4722cfcc4c6e68932eb47dee4583d7df2" }, "capability_trace_refs": [], "recommendation": "Declare confirmation policy and safeguards.audit_log for this external communication action.", @@ -6224,16 +6388,41 @@ "confidence": "high", "policy_eligible": true, "blocking_eligible": true, - "claim_ids": [], + "claim_ids": [ + "clm_1fee9ee6ea224541b56d" + ], "evidence_bases": [ - "protocol_structure" + "protocol_structure", + "reviewed_declaration" ], "predicates": [ { - "predicate": "deterministic_action_rule", + "predicate": "builtin_control_effect", "status": "matched", - "expected": null, - "observed": "agent:support-refund-agent/refund-assistant:action_v2_6f2d18a55f2189a165089034897d52ada547bb19afe20036cb7ec3119c2d95ca", + "expected": [ + "external_communication" + ], + "observed": [ + "external_communication" + ], + "confidence": "high", + "claim_ids": [ + "clm_1fee9ee6ea224541b56d" + ], + "evidence_bases": [ + "reviewed_declaration" + ], + "policy_eligible": true, + "why": null + }, + { + "predicate": "missing_builtin_controls", + "status": "matched", + "expected": "all required controls present", + "observed": [ + "confirmation.required", + "safeguards.audit_log" + ], "confidence": "high", "claim_ids": [], "evidence_bases": [ @@ -6243,7 +6432,7 @@ "why": null } ], - "support_hash": "sha256:742ea8da905eef5df5d2f9a3e9bea9a3bbc84eb4710409e8f41c5389f86af494" + "support_hash": "sha256:335b539322d07aad72eebeff04ace89b40c354da4ee45ba40b52ef885296fe4d" }, "capability_trace_refs": [], "recommendation": "Declare confirmation policy and safeguards.audit_log for this external communication action.", @@ -6295,16 +6484,44 @@ "confidence": "high", "policy_eligible": true, "blocking_eligible": true, - "claim_ids": [], + "claim_ids": [ + "clm_019b1bf05a33e2269c31", + "clm_c8e45c8de58cf645c026" + ], "evidence_bases": [ - "protocol_structure" + "protocol_structure", + "reviewed_declaration" ], "predicates": [ { - "predicate": "deterministic_action_rule", + "predicate": "builtin_control_effect", + "status": "matched", + "expected": [ + "financial_write" + ], + "observed": [ + "financial_write" + ], + "confidence": "high", + "claim_ids": [ + "clm_019b1bf05a33e2269c31", + "clm_c8e45c8de58cf645c026" + ], + "evidence_bases": [ + "reviewed_declaration" + ], + "policy_eligible": true, + "why": null + }, + { + "predicate": "missing_builtin_controls", "status": "matched", - "expected": null, - "observed": "agent:support-refund-agent/refund-assistant:action_v2_6f2d18a55f2189a165089034897d52ada547bb19afe20036cb7ec3119c2d95ca", + "expected": "all required controls present", + "observed": [ + "approval.required", + "safeguards.audit_log", + "safeguards.idempotency" + ], "confidence": "high", "claim_ids": [], "evidence_bases": [ @@ -6314,7 +6531,7 @@ "why": null } ], - "support_hash": "sha256:742ea8da905eef5df5d2f9a3e9bea9a3bbc84eb4710409e8f41c5389f86af494" + "support_hash": "sha256:624f2e0f92ae88c456cfad5b76f245bf721c166c9f0cc25afe0a0c06b68f7718" }, "capability_trace_refs": [], "recommendation": "Declare approval.required, safeguards.audit_log, and safeguards.idempotency for this financial write action.", @@ -10244,6 +10461,54 @@ "MCP source declares wildcard tool exposure" ], "policy_evidence_gaps": [ + { + "kind": "mixed_policy_evidence", + "subject": "support.search_kb [tool_v2_445a251aa6adc1c69f7b318b68f7ad47f01b629d3c24d723331a661735cd353a]", + "source_type": null, + "source_ref": ".agents-shipgate/mcp-tools.json", + "why": "builtin-effect-control-applicability: Policy applicability mixes authoritative and heuristic evidence.", + "next_action": { + "kind": "review_policy_evidence", + "command": "agents-shipgate verify --workspace . --config shipgate.yaml --ci-mode advisory --format json", + "path": "shipgate.yaml#action_surface.actions[tool='support.search_kb'].effect", + "why": "Heuristic or unknown evidence cannot create a blocking policy finding.", + "expects": "Provide reviewed or structural evidence for every indeterminate predicate.", + "accepted_values": [ + "reviewed_declaration", + "protocol_structure", + "typed_provider_fact", + "structural_scope" + ], + "suggested_patch_kind": "manual", + "declaration_template": null, + "auto_apply": false, + "requires_human_review": true + } + }, + { + "kind": "mixed_policy_evidence", + "subject": "send_email_preview [tool_v2_2c9ee6ae2b9f0259ba2dad40a30fb18a8bdc3a732ffc207936fdd49c80e1bc9e]", + "source_type": null, + "source_ref": "inventories/sdk-tools.json", + "why": "builtin-effect-control-applicability: Policy applicability mixes authoritative and heuristic evidence.", + "next_action": { + "kind": "review_policy_evidence", + "command": "agents-shipgate verify --workspace . --config shipgate.yaml --ci-mode advisory --format json", + "path": "shipgate.yaml#action_surface.actions[tool='send_email_preview'].effect", + "why": "Heuristic or unknown evidence cannot create a blocking policy finding.", + "expects": "Provide reviewed or structural evidence for every indeterminate predicate.", + "accepted_values": [ + "reviewed_declaration", + "protocol_structure", + "typed_provider_fact", + "structural_scope" + ], + "suggested_patch_kind": "manual", + "declaration_template": null, + "auto_apply": false, + "requires_human_review": true + } + }, { "kind": "inferred_policy_applicability", "subject": "tool_v2_2c9ee6ae2b9f0259ba2dad40a30fb18a8bdc3a732ffc207936fdd49c80e1bc9e", @@ -10551,4 +10816,4 @@ } ] } -} \ No newline at end of file +} diff --git a/scripts/generate_schemas.py b/scripts/generate_schemas.py index f62bd5e4..67f3772b 100644 --- a/scripts/generate_schemas.py +++ b/scripts/generate_schemas.py @@ -18,10 +18,15 @@ (from agents_shipgate.schemas.packet.EvidencePacket) - docs/verifier-schema.v0..json (from agents_shipgate.schemas.verifier.VerifierArtifact) -- docs/verify-run-schema.v2.json +- docs/verify-run-schema.v3.json (from agents_shipgate.schemas.verify_run. VerifyRunArtifact) -- docs/agent-handoff-schema.v3.json +- docs/verification-plan-schema.v1.json +- docs/verification-unit-result-schema.v1.json +- docs/verification-artifact-manifest-schema.v1.json +- docs/verification-receipt-schema.v1.json + (from agents_shipgate.schemas.verification_identity) +- docs/agent-handoff-schema.v5.json (from agents_shipgate.schemas.agent_handoff. AgentHandoffArtifact) - docs/agent-result-schema.v2.json @@ -35,10 +40,10 @@ - docs/org-governance-schema.v0.1.json (from agents_shipgate.schemas.org_governance. OrgGovernanceStatusV1) -- docs/org-evidence-bundle-schema.v1.json +- docs/org-evidence-bundle-schema.v2.json (from agents_shipgate.schemas.org_evidence_bundle. OrgEvidenceBundleArtifactV1) -- docs/registry-schema.v0.3.json +- docs/registry-schema.v0.4.json (from agents_shipgate.schemas.registry. RegistryQueryResultV1) - docs/host-grants-inventory-schema.v0.2.json @@ -48,10 +53,10 @@ (from HostGrantsBaselineArtifactV2) - docs/host-grants-drift-schema.v0.2.json (from HostGrantsDriftArtifactV2) -- docs/capability-lock-schema.v0.5.json +- docs/capability-lock-schema.v0.6.json (from agents_shipgate.schemas.capabilities. CapabilityLockFileArtifactV1) -- docs/capability-lock-diff-schema.v0.6.json +- docs/capability-lock-diff-schema.v0.7.json (from agents_shipgate.schemas.capabilities. CapabilityLockDiffArtifactV1) - docs/governance-benchmark-catalog-schema.v0.2.json @@ -1306,19 +1311,76 @@ def build_verify_run_schema() -> tuple[Path, str]: schema = VerifyRunArtifact.model_json_schema() schema["$id"] = ( "https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/" - "main/docs/verify-run-schema.v2.json" + "main/docs/verify-run-schema.v3.json" ) schema["$schema"] = "https://json-schema.org/draft/2020-12/schema" - schema["title"] = "Agents Shipgate Verify Run v2" + schema["title"] = "Agents Shipgate Verify Run v3" schema["description"] = ( "JSON Schema for agents-shipgate-reports/verify-run.json. Generated " "from agents_shipgate.schemas.verify_run.VerifyRunArtifact. Do not " "edit by hand." ) - target = DOCS / "verify-run-schema.v2.json" + target = DOCS / "verify-run-schema.v3.json" return target, _canonical_json(schema) +def _verification_identity_schema( + *, model, filename: str, title: str, description: str +) -> tuple[Path, str]: + schema = model.model_json_schema() + schema["$id"] = ( + f"https://raw.githubusercontent.com/ThreeMoonsLab/agents-shipgate/main/docs/{filename}" + ) + schema["$schema"] = "https://json-schema.org/draft/2020-12/schema" + schema["title"] = title + schema["description"] = description + return DOCS / filename, _canonical_json(schema) + + +def build_verification_plan_schema() -> tuple[Path, str]: + from agents_shipgate.schemas.verification_identity import VerificationPlan + + return _verification_identity_schema( + model=VerificationPlan, + filename="verification-plan-schema.v1.json", + title="Agents Shipgate Verification Plan v1", + description="Content-addressed immutable verification request plan.", + ) + + +def build_verification_unit_result_schema() -> tuple[Path, str]: + from agents_shipgate.schemas.verification_identity import VerificationUnitResult + + return _verification_identity_schema( + model=VerificationUnitResult, + filename="verification-unit-result-schema.v1.json", + title="Agents Shipgate Verification Unit Result v1", + description="Decision-free normalized worker output for offline assembly.", + ) + + +def build_verification_artifact_manifest_schema() -> tuple[Path, str]: + from agents_shipgate.schemas.verification_identity import VerificationArtifactManifest + + return _verification_identity_schema( + model=VerificationArtifactManifest, + filename="verification-artifact-manifest-schema.v1.json", + title="Agents Shipgate Verification Artifact Manifest v1", + description="Content-addressed manifest of assembled verification artifacts.", + ) + + +def build_verification_receipt_schema() -> tuple[Path, str]: + from agents_shipgate.schemas.verification_identity import VerificationReceipt + + return _verification_identity_schema( + model=VerificationReceipt, + filename="verification-receipt-schema.v1.json", + title="Agents Shipgate Verification Receipt v1", + description="Terminal closure record written after every artifact is finalized.", + ) + + def build_agent_handoff_schema() -> tuple[Path, str]: """Generate the current agent-handoff schema.""" @@ -1662,9 +1724,7 @@ def build_host_grants_drift_schema() -> tuple[Path, str]: ) schema["$schema"] = "https://json-schema.org/draft/2020-12/schema" schema["title"] = f"Agents Shipgate Host Grants Drift v{minor}" - schema["description"] = ( - "JSON Schema for scope-aware host-grant drift and incomparability." - ) + schema["description"] = "JSON Schema for scope-aware host-grant drift and incomparability." target = DOCS / f"host-grants-drift-schema.v{minor}.json" return target, _canonical_json(schema) @@ -1679,6 +1739,10 @@ def build_host_grants_drift_schema() -> tuple[Path, str]: ("packet", build_packet_schema), ("verifier", build_verifier_schema), ("verify_run", build_verify_run_schema), + ("verification_plan", build_verification_plan_schema), + ("verification_unit_result", build_verification_unit_result_schema), + ("verification_artifact_manifest", build_verification_artifact_manifest_schema), + ("verification_receipt", build_verification_receipt_schema), ("agent_handoff", build_agent_handoff_schema), ("agent_result", build_agent_result_schema), # codex_boundary_result v2 is a frozen compatibility schema and is not diff --git a/scripts/github_action_outputs.py b/scripts/github_action_outputs.py index d39faf21..9758cb3c 100644 --- a/scripts/github_action_outputs.py +++ b/scripts/github_action_outputs.py @@ -5,7 +5,9 @@ from pathlib import Path from typing import Any +from agents_shipgate.core.verification_identity import validate_receipt_artifacts from agents_shipgate.schemas.agent_control import validate_agent_control +from agents_shipgate.schemas.verification_identity import VerificationReceipt TRUST_ROOT_CHECK_ID = "SHIP-VERIFY-TRUST-ROOT-TOUCHED" POLICY_WEAKENING_CHECK_ID = "SHIP-VERIFY-POLICY-WEAKENED" @@ -77,6 +79,7 @@ def extract_outputs(output_dir: Path) -> dict[str, object]: verifier_json = output_dir / "verifier.json" verify_run_json = output_dir / "verify-run.json" agent_handoff_json = output_dir / "agent-handoff.json" + verification_receipt_json = output_dir / "verification-receipt.json" pr_comment_markdown = output_dir / "pr-comment.md" check_annotations_json = output_dir / "check-annotations.json" capability_lock_json = output_dir / "capabilities.lock.json" @@ -90,6 +93,25 @@ def extract_outputs(output_dir: Path) -> dict[str, object]: payload = _load_json(report_json) verifier_payload = _load_json(verifier_json) verify_run = _load_json(verify_run_json) + receipt: VerificationReceipt | None = None + if verifier_json.is_file() and verify_run.get("schema_version") == "shipgate.verify_run/v3": + receipt = VerificationReceipt.model_validate(_load_json(verification_receipt_json)) + validate_receipt_artifacts(receipt, root=output_dir) + identities = { + "request_id": receipt.request_id, + "decision_id": receipt.decision_id, + } + for name, document in ( + ("report", payload), + ("verifier", verifier_payload), + ("verify-run", verify_run), + ): + for key, expected in identities.items(): + actual = document.get(key) + if actual != expected: + raise ValueError( + f"{name} {key} does not match the terminal verification receipt" + ) summary = payload.get("summary") or {} baseline = payload.get("baseline") or {} adk_surface = (payload.get("frameworks") or {}).get("google_adk") or {} @@ -136,7 +158,12 @@ def extract_outputs(output_dir: Path) -> dict[str, object]: "verifier_json": verifier_json, "verify_run_json": verify_run_json, "agent_handoff_json": agent_handoff_json, + "verification_receipt_json": verification_receipt_json, "run_id": verify_run.get("run_id", ""), + "request_id": receipt.request_id if receipt is not None else "", + "receipt_id": receipt.receipt_id if receipt is not None else "", + "decision_id": receipt.decision_id if receipt is not None else "", + "artifact_set_id": receipt.artifact_set_id if receipt is not None else "", "pr_comment_markdown": pr_comment_markdown, "check_annotations_json": check_annotations_json, "capability_lock_json": capability_lock_json, diff --git a/scripts/run_safety_qualification.py b/scripts/run_safety_qualification.py index 539e7f4c..c547a13e 100644 --- a/scripts/run_safety_qualification.py +++ b/scripts/run_safety_qualification.py @@ -48,6 +48,7 @@ WilsonIntervalV1, production_safety_requirements, ) +from agents_shipgate.schemas.verification_identity import VerificationReceipt from agents_shipgate.schemas.verifier import VerifierArtifact from agents_shipgate.schemas.verify_run import VerifyRunArtifact @@ -192,11 +193,11 @@ def _receipt_artifact( root: Path, entry: SafetyReceiptEntryV1, key: str, - receipt: VerifyRunArtifact, + receipt: VerificationReceipt, ) -> tuple[Path, dict[str, Any]]: - ref = receipt.artifacts.get(key) - if ref is None or not ref.sha256: - raise ValueError(f"verify-run receipt is missing content-addressed {key}") + ref = receipt.artifact_manifest.artifacts.get(key) + if ref is None: + raise ValueError(f"terminal receipt is missing content-addressed {key}") artifact_path = _confined_path(root, ref.path) if not artifact_path.is_file(): raise ValueError(f"verify-run {key} artifact is missing: {ref.path}") @@ -222,22 +223,41 @@ def _evaluate_receipt( receipt_hash = sha256_file(receipt_path) if receipt_hash != entry.receipt_sha256: raise ValueError(f"verify-run receipt hash mismatch: {entry.receipt_path}") - receipt = VerifyRunArtifact.model_validate(_read_json_object(receipt_path)) - if receipt.run_id != entry.run_id: - raise ValueError("receipt-index run_id does not match verify-run receipt") - if receipt.tool.name != "agents-shipgate" or receipt.tool.version != wheel_version: - raise ValueError("verify-run tool version is not the exact built wheel version") - if receipt.subject.base_ref is None: + receipt = VerificationReceipt.model_validate(_read_json_object(receipt_path)) + identity_bindings = { + "request_id": (entry.request_id, receipt.request_id), + "receipt_id": (entry.receipt_id, receipt.receipt_id), + "decision_id": (entry.decision_id, receipt.decision_id), + "artifact_set_id": (entry.artifact_set_id, receipt.artifact_set_id), + "run_id": (entry.run_id, receipt.request_id), + } + for name, (indexed, actual) in identity_bindings.items(): + if indexed != actual: + raise ValueError(f"receipt-index {name} does not match terminal receipt") + _verify_run_path, verify_run_payload = _receipt_artifact( + root=artifact_root, + entry=entry, + key="verify_run_json", + receipt=receipt, + ) + verify_run = VerifyRunArtifact.model_validate(verify_run_payload) + if verify_run.plan.engine.version != wheel_version: + raise ValueError("verification plan engine is not the exact built wheel version") + if verify_run.request_id != receipt.request_id: + raise ValueError("verify-run request does not match terminal receipt") + subject = verify_run.plan.subject.git + inputs = verify_run.plan.inputs + if subject.base_ref is None: raise ValueError("qualification receipt must identify a base ref") - if not receipt.subject.base_tree_sha or not receipt.subject.head_tree_sha: + if not subject.base_tree_sha or not subject.head_tree_sha: raise ValueError("qualification receipt must bind base and head tree SHAs") - if receipt.subject.base_tree_sha == receipt.subject.head_tree_sha: + if subject.base_tree_sha == subject.head_tree_sha: raise ValueError("qualification receipt base and head trees must differ") - if receipt.outcome.base_status not in {"succeeded", "cache_hit"}: + if verify_run.outcome.base_status != "succeeded": raise ValueError("qualification receipt base verifier did not succeed") - if receipt.outcome.execution != "succeeded": + if verify_run.outcome.execution != "succeeded": raise ValueError("qualification receipt head verifier did not succeed") - if not receipt.inputs.config_sha256: + if not inputs.config.sha256: raise ValueError("qualification receipt is missing its config digest") verifier_path, verifier_payload = _receipt_artifact( @@ -272,39 +292,37 @@ def _evaluate_receipt( if report_model.release_decision is None: raise ValueError("qualification report release_decision failed typed validation") report_decision = report_model.release_decision.decision - receipt_decision = receipt.outcome.decision + receipt_decision = receipt.decision verifier_decision = verifier.decision if receipt_decision not in DECISIONS: raise ValueError(f"verify-run emitted unknown decision: {receipt_decision!r}") if report_decision != receipt_decision or verifier_decision != receipt_decision: raise ValueError("verify-run, verifier, and report decisions are not identical") expected_merge = EXPECTED_MERGE_VERDICT[receipt_decision] - if receipt.outcome.merge_verdict != expected_merge: + if receipt.merge_verdict != expected_merge: raise ValueError("verify-run decision and merge_verdict are inconsistent") expected_can_merge = receipt_decision == "passed" - if receipt.outcome.can_merge_without_human != expected_can_merge: + if receipt.can_merge_without_human != expected_can_merge: raise ValueError("verify-run decision and can_merge_without_human are inconsistent") - if verifier.merge_verdict != receipt.outcome.merge_verdict: + if verifier.merge_verdict != receipt.merge_verdict: raise ValueError("verifier and verify-run merge_verdict values differ") - if verifier.can_merge_without_human != receipt.outcome.can_merge_without_human: + if verifier.can_merge_without_human != receipt.can_merge_without_human: raise ValueError("verifier and verify-run merge permission values differ") if verifier.head_status != "succeeded": raise ValueError(f"verifier artifact did not succeed: {verifier_path.name}") - if verifier.base_status not in {"succeeded", "cache_hit"}: + if verifier.base_status != "succeeded": raise ValueError("verifier artifact base run did not succeed") - if verifier.head_exit_code != receipt.outcome.exit_code: + if verifier.head_exit_code != verify_run.outcome.exit_code: raise ValueError("verifier and verify-run exit codes differ") - if verifier.config != receipt.subject.config: - raise ValueError("verifier and verify-run config bindings differ") - if verifier.mode != receipt.inputs.ci_mode: + if verifier.config != inputs.config.path: + raise ValueError("verifier and verification plan config bindings differ") + if verifier.mode != inputs.options.get("ci_mode"): raise ValueError("verifier and verify-run CI modes differ") - if verifier.artifacts.get("report_json") != receipt.artifacts["report_json"].path: - raise ValueError("verifier and verify-run report artifact paths differ") if ( - verifier.base_tree_sha != receipt.subject.base_tree_sha - or verifier.head_tree_sha != receipt.subject.head_tree_sha - or verifier.base_ref != receipt.subject.base_ref - or verifier.head_ref != receipt.subject.head_ref + verifier.base_tree_sha != subject.base_tree_sha + or verifier.head_tree_sha != subject.head_tree_sha + or verifier.base_ref != subject.base_ref + or verifier.head_ref != subject.head_ref ): raise ValueError("verifier and verify-run subject bindings differ") diff --git a/skills/agents-shipgate/SKILL.md b/skills/agents-shipgate/SKILL.md index 12caf111..07b593f5 100644 --- a/skills/agents-shipgate/SKILL.md +++ b/skills/agents-shipgate/SKILL.md @@ -48,7 +48,7 @@ Always: 1. Set `AGENTS_SHIPGATE_AGENT_MODE=1` so errors emit a `next_action` JSON line on stderr (auto-enabled inside Claude Code via the harness's `CLAUDECODE=1` env var, and Cursor via `CURSOR_TRACE_ID`). 2. For local agent control, run `shipgate check --agent claude-code --workspace . --format agent-boundary-json` and read the stdout `shipgate.agent_boundary_result/v1` object. Switch on `control.state`; follow only `control.next_action`, `control.allowed_next_commands`, and `control.human_review`. Treat `decision` as diagnostic context only. -3. For verifier runs, parse `agents-shipgate-reports/agent-handoff.json` first, then `verifier.json` and `verify-run.json`: +3. For verifier runs, validate `agents-shipgate-reports/verification-receipt.json` first, then parse `agent-handoff.json`, `verifier.json`, and `verify-run.json`: `control.state`, `merge_verdict`, `can_merge_without_human`, `control.next_action`, `fix_task`, and `capability_review.top_changes`. Then parse `agents-shipgate-reports/report.json.release_decision.decision`; it is the @@ -74,12 +74,13 @@ For non-GitHub CI (GitLab, CircleCI, Jenkins, Azure Pipelines, Buildkite, Bitbuc - **CLI surface** follows the current 0.x contract line — see https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/STABILITY.md. - **Installed CLI contract**: when available, run `agents-shipgate contract --json` to verify local schema versions, capability/research surfaces, `release_decision.decision`, and manual-review signal fields. Older installs should use [`docs/agent-contract-current.md`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/agent-contract-current.md) or upgrade before automating against the local contract command. -- **Verifier JSON**: `verifier_schema_version: "0.3"`. Switch on `control.state`, then read `merge_verdict`, `can_merge_without_human`, `control.next_action`, `fix_task`, `capability_review.top_changes`, `trust_root_touched`, and `policy_weakened` before summarizing an AI-generated PR. `merge_verdict` is a deterministic projection; the gate remains `report.json.release_decision.decision`. -- **Verify run JSON**: `verify-run.json` uses `schema_version: "shipgate.verify_run/v2"` and records stable run identity, subject refs, input hashes, execution, applicability, release outcome, and artifact hashes. It is the reproducibility artifact for `verify`; do not treat it as a second gate. -- **Report JSON**: `report_schema_version: "0.33"`. Read `release_decision.decision` first. A `passed` decision requires a complete root-reachable static binding graph plus complete, conflict-free identity, effect, and authority evidence for every reachable action; it does not prove runtime behavior. Preserve `release_decision.static_analysis_only=true`, `runtime_behavior_verified=false`, and `static_verdict_disclaimer` in summaries. Read `release_decision.evidence_coverage.binding_coverage`, `semantic_coverage`, `identity_coverage`, and `policy_gap_count`, then work every `evidence_gaps[].next_action` in order. Binding, semantic, and policy-applicability gaps are not Findings and cannot be suppressed, baselined, severity-overridden, cleared by `--no-heuristics`, or satisfied by `human_ack`; binding, effect, and authority declarations are human assertions and must never be auto-written. Use `tool_catalog[]` for diagnostics and `tool_inventory[]` for the proven reachable surface. The current schema is [`docs/report-schema.v0.33.json`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.33.json); v0.32 is a frozen compatibility reference. See the [current agent contract](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/agent-contract-current.md#read-these-first-for-release-gating) and [evidence-backed passed contract](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/passed-verdict-contract.md). +- **Verifier JSON**: `verifier_schema_version: "0.5"`. Switch on `control.state`, then read `merge_verdict`, `can_merge_without_human`, `control.next_action`, `fix_task`, `capability_review.top_changes`, `trust_root_touched`, and `policy_weakened` before summarizing an AI-generated PR. `merge_verdict` is a deterministic projection; the gate remains `report.json.release_decision.decision`. +- **Verification receipt**: `verification-receipt.json` uses `schema_version: "shipgate.verification_receipt/v1"` and is written last. Validate it before trusting any projected verdict; it content-addresses the request, executor, unit result, decision, and complete artifact set. +- **Verify run JSON**: `verify-run.json` uses `schema_version: "shipgate.verify_run/v3"`, embeds the content-addressed plan and executor, and binds unit-result and decision IDs. `run_id` is an exact compatibility alias of `request_id`; do not treat the run projection as a second gate. +- **Report JSON**: `report_schema_version: "0.34"`. Read `release_decision.decision` first. A `passed` decision requires a complete root-reachable static binding graph plus complete, conflict-free identity, effect, and authority evidence for every reachable action; it does not prove runtime behavior. Preserve `release_decision.static_analysis_only=true`, `runtime_behavior_verified=false`, and `static_verdict_disclaimer` in summaries. Read `release_decision.evidence_coverage.binding_coverage`, `semantic_coverage`, `identity_coverage`, and `policy_gap_count`, then work every `evidence_gaps[].next_action` in order. Binding, semantic, and policy-applicability gaps are not Findings and cannot be suppressed, baselined, severity-overridden, cleared by `--no-heuristics`, or satisfied by `human_ack`; binding, effect, and authority declarations are human assertions and must never be auto-written. Use `tool_catalog[]` for diagnostics and `tool_inventory[]` for the proven reachable surface. The current schema is [`docs/report-schema.v0.34.json`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/report-schema.v0.34.json); v0.33 is a frozen compatibility reference. See the [current agent contract](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/agent-contract-current.md), [verification identity contract](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/verification-reproducibility.md), and [evidence-backed passed contract](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/passed-verdict-contract.md). These binding-backed fields require runtime contract v13; the unambiguous `AgentControl` projection requires runtime contract v14. A contract-v12 CLI emits the frozen v0.30 report and must not be described using the v0.31 binding claim. -- **Release Evidence Packet**: `agents-shipgate-reports/packet.{md,json,html}` (and `packet.pdf` with the `[pdf]` extras) is a supporting/provisional reviewer artifact. Packet v0.11 projects binding and semantic coverage under the release decision; it never creates a second gate. See [`docs/packet-schema.v0.11.json`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/packet-schema.v0.11.json) and [STABILITY.md §Release Evidence Packet](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/STABILITY.md#release-evidence-packet-v011). -- **Capability standard**: `agents-shipgate capability export` emits a stable static capability lock (`capability_lock_schema_version: "0.5"`) and `agents-shipgate capability diff` emits a stable semantic diff (`capability_lock_diff_schema_version: "0.6"`). These artifacts implement capability standard v0.4, are supporting/provisional and non-gating, exclude runtime trace evidence, and are documented in [`docs/capability-standard.md`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/capability-standard.md). +- **Release Evidence Packet**: `agents-shipgate-reports/packet.{md,json,html}` (and `packet.pdf` with the `[pdf]` extras) is a supporting/provisional reviewer artifact. Packet v0.12 projects the verification request and decision IDs plus binding and semantic coverage; it never creates a second gate. See [`docs/packet-schema.v0.12.json`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/packet-schema.v0.12.json) and [STABILITY.md §Release Evidence Packet](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/STABILITY.md#release-evidence-packet-v012). +- **Capability standard**: `agents-shipgate capability export` emits a stable static capability lock (`capability_lock_schema_version: "0.6"`) and `agents-shipgate capability diff` emits a stable semantic diff (`capability_lock_diff_schema_version: "0.7"`). These artifacts implement capability standard v0.5, are supporting/provisional and non-gating, exclude runtime trace evidence, and are documented in [`docs/capability-standard.md`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/capability-standard.md). - **Governance benchmark**: `benchmark/agent-pr-governance/cases.yaml` and `scripts/run_governance_benchmark.py` are the stable research benchmark substrate (`governance_benchmark_result_schema_version: "0.2"`), not a release gate. See [`docs/governance-benchmark.md`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/governance-benchmark.md). - **Single source of truth for the contract**: [`docs/agent-contract-current.md`](https://github.com/ThreeMoonsLab/agents-shipgate/blob/main/docs/agent-contract-current.md). When the schema bumps, that file updates first. - **Exit codes**: `0` pass, `2` config error, `3` parse error, `4` other error, `20` strict-mode gate failure. diff --git a/src/agents_shipgate/__init__.py b/src/agents_shipgate/__init__.py index 0a5a12b5..9a0f5e42 100644 --- a/src/agents_shipgate/__init__.py +++ b/src/agents_shipgate/__init__.py @@ -1,3 +1,3 @@ """Agents Shipgate package.""" -__version__ = "0.16.0b5" +__version__ = "0.16.0b6" diff --git a/src/agents_shipgate/cli/attest.py b/src/agents_shipgate/cli/attest.py index b2859b63..d849b5b9 100644 --- a/src/agents_shipgate/cli/attest.py +++ b/src/agents_shipgate/cli/attest.py @@ -25,10 +25,12 @@ from agents_shipgate import __version__ from agents_shipgate.config.loader import load_manifest from agents_shipgate.core.errors import InputParseError +from agents_shipgate.core.verification_identity import validate_receipt_artifacts from agents_shipgate.schemas.attestation import ( ATTESTATION_SCHEMA_VERSION, ReleaseAttestationV1, ) +from agents_shipgate.schemas.verification_identity import VerificationReceipt def _attest_command( @@ -127,6 +129,16 @@ def _attest_command( if verify_run_path is not None and verify_run_path.is_file() else None ) + receipt_path = source.with_name("verification-receipt.json") + receipt_payload = _load_optional_json_object(receipt_path) + receipt_sha256 = None + if verifier.get("verifier_schema_version") == "0.5": + if not receipt_payload: + raise typer.Exit(3) + receipt = VerificationReceipt.model_validate(receipt_payload) + validate_receipt_artifacts(receipt, root=source.parent) + receipt_payload = receipt.model_dump(mode="json") + receipt_sha256 = hashlib.sha256(receipt_path.read_bytes()).hexdigest() org_context, run_context = _context_from_inputs( config=config, explicit={ @@ -155,6 +167,8 @@ def _attest_command( report=report, verify_run=verify_run_payload, verify_run_sha256=verify_run_sha256, + verification_receipt=receipt_payload, + verification_receipt_sha256=receipt_sha256, org_context=org_context, run_context=run_context, ) @@ -178,6 +192,8 @@ def build_attestation_payload( report: dict[str, Any] | None = None, verify_run: dict[str, Any] | None = None, verify_run_sha256: str | None = None, + verification_receipt: dict[str, Any] | None = None, + verification_receipt_sha256: str | None = None, org_context: dict[str, Any] | None = None, run_context: dict[str, Any] | None = None, ) -> dict[str, Any]: @@ -202,6 +218,7 @@ def build_attestation_payload( human_review = _obj(verifier.get("human_review")) report = report or {} verify_run = verify_run or {} + verification_receipt = verification_receipt or {} human_ack = _obj(report.get("human_ack")) effective_policy = report.get("effective_policy") org_context = _clean_org_context(org_context or {}) @@ -214,6 +231,11 @@ def build_attestation_payload( "source_verifier": source.name if redacted else str(source), "redacted": redacted, "run_id": _clean_str(verify_run.get("run_id")), + "request_id": _clean_str(verification_receipt.get("request_id")), + "receipt_id": _clean_str(verification_receipt.get("receipt_id")), + "decision_id": _clean_str(verification_receipt.get("decision_id")), + "artifact_set_id": _clean_str(verification_receipt.get("artifact_set_id")), + "verification_receipt_sha256": _clean_str(verification_receipt_sha256), "verify_run_sha256": _clean_str(verify_run_sha256), "event_time": run_context.get("event_time"), "source_url": run_context.get("source_url"), @@ -587,7 +609,7 @@ def _canonical_sha256(obj: Any) -> str: def _verify_run_policy_packs(verify_run: dict[str, Any]) -> list[dict[str, str | int | None]]: - inputs = _obj(verify_run.get("inputs")) + inputs = _obj(_obj(verify_run.get("plan")).get("inputs")) or _obj(verify_run.get("inputs")) rows: list[dict[str, str | int | None]] = [] for item in inputs.get("policy_packs") or []: if not isinstance(item, dict): diff --git a/src/agents_shipgate/cli/discovery/agent_instructions/renderers/agents_md.py b/src/agents_shipgate/cli/discovery/agent_instructions/renderers/agents_md.py index d763f693..80edc5ee 100644 --- a/src/agents_shipgate/cli/discovery/agent_instructions/renderers/agents_md.py +++ b/src/agents_shipgate/cli/discovery/agent_instructions/renderers/agents_md.py @@ -54,7 +54,8 @@ def render_block() -> str: For committed PR/CI verification, run `agents-shipgate verify --base origin/main --head HEAD --json` after making the base ref available; it never -fetches. Read `agents-shipgate-reports/agent-handoff.json` first for +fetches. Validate `agents-shipgate-reports/verification-receipt.json` first, +then read `agents-shipgate-reports/agent-handoff.json` for `gate.merge_verdict`, `gate.can_merge_without_human`, and `control`; then read `agents-shipgate-reports/verifier.json` for detailed control context, `agents-shipgate-reports/verify-run.json` for reproducibility metadata, and diff --git a/src/agents_shipgate/cli/discovery/agent_instructions/renderers/claude_md.py b/src/agents_shipgate/cli/discovery/agent_instructions/renderers/claude_md.py index 5cc96acd..1cc1db10 100644 --- a/src/agents_shipgate/cli/discovery/agent_instructions/renderers/claude_md.py +++ b/src/agents_shipgate/cli/discovery/agent_instructions/renderers/claude_md.py @@ -46,7 +46,8 @@ def render_block() -> str: For committed PR/CI verification, run `agents-shipgate verify --base origin/main --head HEAD --json` after making the base ref available; it never -fetches. Read `agents-shipgate-reports/agent-handoff.json` first for +fetches. Validate `agents-shipgate-reports/verification-receipt.json` first, +then read `agents-shipgate-reports/agent-handoff.json` for `gate.merge_verdict`, `gate.can_merge_without_human`, and `control`; then read `agents-shipgate-reports/verifier.json` for detailed control context, `agents-shipgate-reports/verify-run.json` for reproducibility metadata, and diff --git a/src/agents_shipgate/cli/discovery/agent_instructions/renderers/cursor.py b/src/agents_shipgate/cli/discovery/agent_instructions/renderers/cursor.py index a239ba76..efabc995 100644 --- a/src/agents_shipgate/cli/discovery/agent_instructions/renderers/cursor.py +++ b/src/agents_shipgate/cli/discovery/agent_instructions/renderers/cursor.py @@ -74,7 +74,8 @@ def render_file() -> str: For committed PR/CI verification, run `agents-shipgate verify --base origin/main --head HEAD --json` after making the base ref available; it never -fetches. Read `agents-shipgate-reports/agent-handoff.json` first for +fetches. Validate `agents-shipgate-reports/verification-receipt.json` first, +then read `agents-shipgate-reports/agent-handoff.json` for `gate.merge_verdict`, `gate.can_merge_without_human`, and `control`; then read `agents-shipgate-reports/verifier.json` for detailed control context, `agents-shipgate-reports/verify-run.json` for reproducibility metadata, and diff --git a/src/agents_shipgate/cli/discovery/local_contract.py b/src/agents_shipgate/cli/discovery/local_contract.py index eba89a75..c77074c7 100644 --- a/src/agents_shipgate/cli/discovery/local_contract.py +++ b/src/agents_shipgate/cli/discovery/local_contract.py @@ -39,12 +39,16 @@ REGISTRY_SCHEMA_VERSION, RELEASE_DECISIONS, TRIGGER_CATALOG_SCHEMA_VERSION, + VERIFICATION_ARTIFACT_MANIFEST_SCHEMA_VERSION, + VERIFICATION_PLAN_SCHEMA_VERSION, + VERIFICATION_RECEIPT_SCHEMA_VERSION, + VERIFICATION_UNIT_RESULT_SCHEMA_VERSION, VERIFIER_READ_ORDER, VERIFY_RUN_SCHEMA_VERSION, ) from agents_shipgate.schemas.verifier import VerifierArtifact -LOCAL_CONTRACT_SCHEMA_VERSION = "5" +LOCAL_CONTRACT_SCHEMA_VERSION = "6" LOCAL_CONTRACT_RELATIVE_PATH = ".shipgate/agent-contract.json" @@ -66,6 +70,10 @@ class LocalAgentContract(BaseModel): gating_signal: str verifier_schema_version: str verify_run_schema_version: str + verification_plan_schema_version: str + verification_unit_result_schema_version: str + verification_artifact_manifest_schema_version: str + verification_receipt_schema_version: str agent_handoff_schema_version: str agent_handoff_schema_path: str agent_handoff_artifact: str @@ -111,6 +119,12 @@ def build_local_agent_contract() -> LocalAgentContract: VerifierArtifact.model_fields["verifier_schema_version"].default ), verify_run_schema_version=VERIFY_RUN_SCHEMA_VERSION, + verification_plan_schema_version=VERIFICATION_PLAN_SCHEMA_VERSION, + verification_unit_result_schema_version=VERIFICATION_UNIT_RESULT_SCHEMA_VERSION, + verification_artifact_manifest_schema_version=( + VERIFICATION_ARTIFACT_MANIFEST_SCHEMA_VERSION + ), + verification_receipt_schema_version=VERIFICATION_RECEIPT_SCHEMA_VERSION, agent_handoff_schema_version=AGENT_HANDOFF_SCHEMA_VERSION, agent_handoff_schema_path=AGENT_HANDOFF_SCHEMA_PATH, agent_handoff_artifact=ARTIFACTS["agent_handoff"], diff --git a/src/agents_shipgate/cli/main.py b/src/agents_shipgate/cli/main.py index 2277f837..abe5c07c 100644 --- a/src/agents_shipgate/cli/main.py +++ b/src/agents_shipgate/cli/main.py @@ -37,6 +37,7 @@ from agents_shipgate.cli.self_check import self_check from agents_shipgate.cli.skill import skill_app from agents_shipgate.cli.trigger import trigger as _trigger_command +from agents_shipgate.cli.verification import verification_app from agents_shipgate.cli.verify import verify as _verify_command from agents_shipgate.core.logging import configure_logging @@ -110,10 +111,7 @@ app.command( "findings", hidden=True, - help=( - "Filter findings from a `report.json` by provenance kind for " - "reviewer triage." - ), + help=("Filter findings from a `report.json` by provenance kind for reviewer triage."), )(_findings_command) app.command( "trigger", @@ -141,10 +139,7 @@ app.command( "install-hooks", hidden=True, - help=( - "Install advisory local coding-agent hooks. Currently supports " - "--target claude-code." - ), + help=("Install advisory local coding-agent hooks. Currently supports --target claude-code."), )(_install_hooks_command) app.command( @@ -177,6 +172,8 @@ def _mcp_serve_command() -> None: except _ConfigError as exc: typer.echo(f"Config error: {exc}", err=True) raise typer.Exit(2) from exc + + _register_scan.register(app) _register_list_checks.register(app) _register_contract.register(app) @@ -198,6 +195,7 @@ def _mcp_serve_command() -> None: app.add_typer(mcp_app, name="mcp", hidden=True) app.add_typer(org_app, name="org", hidden=True) app.add_typer(registry_app, name="registry", hidden=True) +app.add_typer(verification_app, name="verification", hidden=True) logger = logging.getLogger(__name__) diff --git a/src/agents_shipgate/cli/org.py b/src/agents_shipgate/cli/org.py index 151c3123..208e172e 100644 --- a/src/agents_shipgate/cli/org.py +++ b/src/agents_shipgate/cli/org.py @@ -23,11 +23,13 @@ host_grant_drift_payload, render_org_status_markdown, ) +from agents_shipgate.core.verification_identity import validate_receipt_artifacts from agents_shipgate.schemas.attestation import ReleaseAttestationV1 from agents_shipgate.schemas.org_evidence_bundle import ( OrgEvidenceBundleArtifactRef, OrgEvidenceBundleV1, ) +from agents_shipgate.schemas.verification_identity import VerificationReceipt ORG_GOVERNANCE_EXIT_CODE = 20 @@ -89,9 +91,7 @@ def org_status( baseline_path = baseline if baseline.is_absolute() else root / baseline host_baseline_path = None if host_baseline is not None: - host_baseline_path = ( - host_baseline if host_baseline.is_absolute() else root / host_baseline - ) + host_baseline_path = host_baseline if host_baseline.is_absolute() else root / host_baseline payload = build_org_governance_status( manifest=manifest, @@ -220,6 +220,24 @@ def org_bundle( ) report = _load_optional_json_object(report_path) verify_run = _load_optional_json_object(verify_run_path) + receipt_path = _artifact_path( + verifier_path, + verifier, + "verification_receipt_json", + "verification-receipt.json", + ) + receipt = _load_optional_json_object(receipt_path) + verified_receipt: VerificationReceipt | None = None + if verifier.get("verifier_schema_version") == "0.5": + try: + if receipt is None: + raise ValueError("current verifier evidence is missing verification-receipt.json") + verified_receipt = VerificationReceipt.model_validate(receipt) + validate_receipt_artifacts(verified_receipt, root=verifier_path.parent) + receipt = verified_receipt.model_dump(mode="json") + except (ValueError, OSError) as exc: + typer.echo(f"Verification receipt error: {exc}", err=True) + raise typer.Exit(3) from exc attestation_path = _resolve_attestation_path(verifier_path, attestation) attestation_payload = _load_optional_json_object(attestation_path) if attestation_payload is None: @@ -230,11 +248,27 @@ def org_bundle( report=report, verify_run=verify_run, verify_run_sha256=_file_sha256(verify_run_path), + verification_receipt=receipt, + verification_receipt_sha256=_file_sha256(receipt_path), org_context=_org_context_from_manifest(manifest), ) attestation_model = ReleaseAttestationV1.model_validate( _normalize_attestation_payload(attestation_payload) ) + if verified_receipt is not None: + expected_receipt_identity = { + "request_id": verified_receipt.request_id, + "receipt_id": verified_receipt.receipt_id, + "decision_id": verified_receipt.decision_id, + "artifact_set_id": verified_receipt.artifact_set_id, + } + for field, expected in expected_receipt_identity.items(): + if getattr(attestation_model, field) != expected: + typer.echo( + f"Attestation {field} does not match the terminal verification receipt.", + err=True, + ) + raise typer.Exit(3) attestation_json = attestation_model.model_dump(mode="json") registry_row = _row_from_attestation( attestation_json, @@ -265,6 +299,7 @@ def org_bundle( "report": report_path, "verify_run": verify_run_path, "attestation": attestation_path, + "verification_receipt": receipt_path, } ) if out is not None: @@ -297,12 +332,17 @@ def org_bundle( if attestation_path is not None and attestation_path.is_file() else None ), + source_verification_receipt=( + _display_path(receipt_path, root) if receipt_path.is_file() else None + ), + request_id=attestation_json.get("request_id"), + receipt_id=attestation_json.get("receipt_id"), + decision_id=attestation_json.get("decision_id"), + artifact_set_id=attestation_json.get("artifact_set_id"), attestation=attestation_json, registry_row=registry_row.model_dump(mode="json"), org_status=org_status_payload.model_dump(mode="json", exclude_none=False), - policy_packs=[ - item.model_dump(mode="json") for item in org_status_payload.policy_packs - ], + policy_packs=[item.model_dump(mode="json") for item in org_status_payload.policy_packs], host_grants=_host_grant_summary(inventory, registry_path=registry), host_grant_drift=drift, privacy=_obj(report.get("privacy_audit")) if report is not None else {}, @@ -359,11 +399,11 @@ def _load_optional_json_object(path: Path | None) -> dict[str, Any] | None: def _normalize_attestation_payload(payload: dict[str, Any]) -> dict[str, Any]: version = payload.get("attestation_schema_version") - if version != "0.3": + if version not in {"0.3", "0.4"}: return payload upgraded = { **payload, - "attestation_schema_version": "0.4", + "attestation_schema_version": "0.5", } for field in ( "run_id", @@ -373,6 +413,11 @@ def _normalize_attestation_payload(payload: dict[str, Any]) -> dict[str, Any]: "branch", "base_sha", "head_sha", + "request_id", + "receipt_id", + "decision_id", + "artifact_set_id", + "verification_receipt_sha256", ): upgraded.setdefault(field, None) upgraded.setdefault("policy_packs", []) diff --git a/src/agents_shipgate/cli/registry.py b/src/agents_shipgate/cli/registry.py index a3e9f59a..927eac7b 100644 --- a/src/agents_shipgate/cli/registry.py +++ b/src/agents_shipgate/cli/registry.py @@ -82,6 +82,11 @@ def _row_from_attestation( "attestation_schema_version": attestation.get("attestation_schema_version"), "cli_version": attestation.get("cli_version"), "run_id": attestation.get("run_id"), + "request_id": attestation.get("request_id"), + "receipt_id": attestation.get("receipt_id"), + "decision_id": attestation.get("decision_id"), + "artifact_set_id": attestation.get("artifact_set_id"), + "source_verification_receipt_sha256": attestation.get("verification_receipt_sha256"), "source_attestation_sha256": source_attestation_sha256, "source_verify_run_sha256": attestation.get("verify_run_sha256"), "base_ref": attestation.get("base_ref"), @@ -103,9 +108,7 @@ def _row_from_attestation( "human_ack_satisfied": ( human_ack_satisfied if isinstance(human_ack_satisfied, bool) else None ), - "human_ack_outstanding": [ - str(item) for item in human_ack.get("outstanding") or [] - ], + "human_ack_outstanding": [str(item) for item in human_ack.get("outstanding") or []], "human_ack": human_ack, "policy_snapshot_sha256": attestation.get("policy_snapshot_sha256"), "artifact_sha256": attestation.get("artifact_sha256"), @@ -130,9 +133,12 @@ def _row_from_attestation( def _row_id(row: dict[str, Any] | RegistryRowV1) -> str: payload = _row_identity_payload(row) - return "att_" + hashlib.sha256( - json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8") - ).hexdigest()[:16] + return ( + "att_" + + hashlib.sha256( + json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8") + ).hexdigest()[:16] + ) def _row_identity_payload(row: dict[str, Any] | RegistryRowV1) -> dict[str, Any]: @@ -211,9 +217,7 @@ def _coerce_row(value: dict[str, Any]) -> RegistryRowV1: satisfied = human_ack.get("satisfied") value["human_ack_satisfied"] = satisfied if isinstance(satisfied, bool) else None if "human_ack_outstanding" not in value and isinstance(human_ack, dict): - value["human_ack_outstanding"] = [ - str(item) for item in human_ack.get("outstanding") or [] - ] + value["human_ack_outstanding"] = [str(item) for item in human_ack.get("outstanding") or []] if "row_id" not in value: value["row_id"] = _row_id(value) row = RegistryRowV1.model_validate(value) @@ -265,11 +269,7 @@ def registry_ingest( existing = _load_rows(registry) source_attestation_sha256 = hashlib.sha256(raw).hexdigest() prior = next( - ( - item - for item in existing - if item.source_attestation_sha256 == source_attestation_sha256 - ), + (item for item in existing if item.source_attestation_sha256 == source_attestation_sha256), None, ) if prior is not None: @@ -280,7 +280,9 @@ def registry_ingest( data, repo=repo, source_attestation_sha256=source_attestation_sha256, - previous_row=_last_row_for_repo(existing, repo or (data.get("org") or {}).get("repo") or ""), + previous_row=_last_row_for_repo( + existing, repo or (data.get("org") or {}).get("repo") or "" + ), ) status = "ingested" registry.parent.mkdir(parents=True, exist_ok=True) @@ -300,9 +302,7 @@ def registry_query( service: str | None = typer.Option(None, "--service", help="Filter by service."), tier: str | None = typer.Option(None, "--tier", help="Filter by tier."), actor: str | None = typer.Option(None, "--actor", help="Filter by CI/merge actor."), - verdict: str | None = typer.Option( - None, "--verdict", help="Filter by merge_verdict." - ), + verdict: str | None = typer.Option(None, "--verdict", help="Filter by merge_verdict."), capability_id: str | None = typer.Option( None, "--capability-id", help="Filter rows whose change_ids contain this id." ), @@ -524,8 +524,7 @@ def _registry_summary_payload( 1 for row in rows for pack in row.policy_packs - if isinstance(pack, dict) - and pack.get("status") not in {None, "verified"} + if isinstance(pack, dict) and pack.get("status") not in {None, "verified"} ), ) diff --git a/src/agents_shipgate/cli/verification.py b/src/agents_shipgate/cli/verification.py new file mode 100644 index 00000000..cdda9826 --- /dev/null +++ b/src/agents_shipgate/cli/verification.py @@ -0,0 +1,522 @@ +"""Offline verification-plan, worker-IR, assembly, and reproduction commands.""" + +from __future__ import annotations + +import json +import tempfile +from pathlib import Path +from typing import Any + +import typer + +from agents_shipgate.cli.verify.git import ( + archive_tree, + commit_date, + commit_sha, + diff_context, + ensure_git_workspace, + merge_base_sha, + ref_exists, + repository_identity, + tree_sha, + working_tree_context, +) +from agents_shipgate.core.agent_handoff import build_agent_handoff +from agents_shipgate.core.errors import ConfigError, InputParseError +from agents_shipgate.core.verification_identity import ( + build_executor, + build_terminal_receipt, + build_unit_result, + build_verification_plan, + sha256_file, + validate_engine_requirement, + validate_plan_inputs, + validate_receipt_artifacts, +) +from agents_shipgate.packet.json_packet import load_packet_json, write_packet_json +from agents_shipgate.report.json_report import report_json_payload +from agents_shipgate.schemas.report import ReadinessReport +from agents_shipgate.schemas.verification_identity import ( + VerificationPlan, + VerificationReceipt, + VerificationUnitResult, + content_id, +) +from agents_shipgate.schemas.verifier import VerifierArtifact +from agents_shipgate.schemas.verify_run import ( + VerifyRunArtifactRef, + VerifyRunOutcome, + build_verify_run_artifact, +) + +verification_app = typer.Typer(no_args_is_help=True) + + +@verification_app.command("prepare") +def prepare( + workspace: Path = typer.Option(Path("."), "--workspace"), + config: Path = typer.Option(Path("shipgate.yaml"), "--config", "-c"), + base: str | None = typer.Option(None, "--base"), + head: str | None = typer.Option(None, "--head"), + baseline: Path | None = typer.Option(None, "--baseline"), + diff_from: Path | None = typer.Option(None, "--diff-from"), + policy_packs: list[Path] | None = typer.Option(None, "--policy-pack"), + ci_mode: str = typer.Option("advisory", "--ci-mode"), + no_plugins: bool = typer.Option(False, "--no-plugins"), + no_heuristics: bool = typer.Option(False, "--no-heuristics"), + evaluation_date: str | None = typer.Option(None, "--evaluation-date"), + out: Path = typer.Option(Path("agents-shipgate-reports/verification-plan.json"), "--out"), +) -> None: + """Prepare one portable, content-addressed plan without evaluating policy.""" + + root = ensure_git_workspace(workspace.resolve()) + if head is not None and base is None: + raise typer.BadParameter("--head requires --base so the prepared diff has an exact range") + head_ref = head or "HEAD" + if not ref_exists(root, head_ref): + raise typer.BadParameter(f"head ref is unavailable locally: {head_ref}") + changed, diff_text = diff_context(root, base, head_ref) if base else working_tree_context(root) + resolved_date = evaluation_date or commit_date(root, head_ref) + config_relative = _under(root, config).relative_to(root) + policy_paths = [_under(root, path) for path in policy_packs or []] + baseline_path = _under(root, baseline) if baseline is not None else None + diff_from_path = _under(root, diff_from) if diff_from is not None else None + + if head is None: + plan = build_verification_plan( + git_root=root, + input_root=root, + config_path=root / config_relative, + config_logical_path=config_relative.as_posix(), + base_ref=base, + head_ref=head_ref, + archived_head=False, + **_git_identity(root, base, head_ref), + changed_files=changed, + diff_text=diff_text, + baseline_path=baseline_path, + diff_from_path=diff_from_path, + policy_pack_paths=policy_paths, + evaluation_date=resolved_date, + options={ + "ci_mode": ci_mode, + "no_heuristics": no_heuristics, + "plugins_enabled": not no_plugins, + }, + plugins_enabled=False if no_plugins else None, + ) + else: + with tempfile.TemporaryDirectory(prefix="agents-shipgate-plan-") as tmp: + snapshot = Path(tmp) / "snapshot" + archive_tree(root, head_ref, snapshot) + plan = build_verification_plan( + git_root=root, + input_root=snapshot, + config_path=snapshot / config_relative, + config_logical_path=config_relative.as_posix(), + base_ref=base, + head_ref=head_ref, + archived_head=True, + **_git_identity(root, base, head_ref), + changed_files=changed, + diff_text=diff_text, + baseline_path=_map(snapshot, root, baseline_path), + diff_from_path=diff_from_path, + policy_pack_paths=[_map(snapshot, root, path) for path in policy_paths], + evaluation_date=resolved_date, + options={ + "ci_mode": ci_mode, + "no_heuristics": no_heuristics, + "plugins_enabled": not no_plugins, + }, + plugins_enabled=False if no_plugins else None, + ) + diff_path = out.with_name("verification-input.diff") + diff_path.parent.mkdir(parents=True, exist_ok=True) + diff_path.write_text(diff_text, encoding="utf-8") + _write_model(out, plan) + typer.echo( + json.dumps( + { + "request_id": plan.request_id, + "plan": str(out), + "diff": str(diff_path), + } + ) + ) + + +@verification_app.command("worker") +def worker( + plan_path: Path = typer.Option(..., "--plan"), + workspace: Path = typer.Option(Path("."), "--workspace"), + diff_path: Path | None = typer.Option(None, "--diff"), + out: Path = typer.Option( + Path("agents-shipgate-reports/verification-unit-result.json"), "--out" + ), +) -> None: + """Validate one task's immutable inputs and emit decision-free worker IR.""" + + plan = VerificationPlan.model_validate(_load_json(plan_path)) + resolved_diff = diff_path or plan_path.with_name(plan.inputs.diff.path) + try: + validate_engine_requirement( + plan.engine, + plugins_enabled=_plugins_enabled(plan), + ) + validate_plan_inputs( + plan, + root=workspace, + diff_path=resolved_diff, + bundle_root=plan_path.parent, + ) + _validate_git_subject(plan, workspace) + except ValueError as exc: + raise InputParseError(str(exc)) from exc + unit = build_unit_result( + plan=plan, + normalized_ir={ + "input_set_id": plan.inputs.input_set_id, + "subject_id": plan.subject.subject_id, + "validated_paths": sorted( + { + plan.inputs.config.path, + plan.inputs.diff.path, + *(blob.path for blob in plan.inputs.tool_sources), + *(blob.path for blob in plan.inputs.policy_packs), + } + ), + }, + ) + _write_model(out, unit) + typer.echo(json.dumps({"unit_result_id": unit.unit_result_id, "result": str(out)})) + + +@verification_app.command("assemble") +def assemble( + plan_path: Path = typer.Option(..., "--plan"), + unit_paths: list[Path] = typer.Option(..., "--unit-result"), + verifier_path: Path = typer.Option(..., "--verifier"), + artifacts_root: Path = typer.Option(Path("."), "--artifacts-root"), + out: Path = typer.Option(Path("agents-shipgate-reports/verification-receipt.json"), "--out"), +) -> None: + """Validate worker IR and close the sole-engine verifier projections.""" + + plan = VerificationPlan.model_validate(_load_json(plan_path)) + units = [VerificationUnitResult.model_validate(_load_json(path)) for path in unit_paths] + verifier = VerifierArtifact.model_validate(_load_json(verifier_path)) + try: + validate_engine_requirement( + plan.engine, + plugins_enabled=_plugins_enabled(plan), + ) + except ValueError as exc: + raise InputParseError(str(exc)) from exc + verifier_mirrors = { + "request_id": plan.request_id, + "subject_id": plan.subject.subject_id, + "input_set_id": plan.inputs.input_set_id, + "engine_requirement_id": plan.engine.engine_requirement_id, + } + for field, expected in verifier_mirrors.items(): + if getattr(verifier, field) != expected: + raise InputParseError(f"verifier {field} does not match the plan") + if not units: + raise InputParseError("assembly requires at least one worker result") + if len({unit.unit_result_id for unit in units}) != len(units): + raise InputParseError("assembly rejects duplicate worker results") + executor_ids = {unit.executor.executor_id for unit in units} + if len(executor_ids) != 1: + raise InputParseError("assembly requires one exact executor identity") + if any(unit.request_id != plan.request_id for unit in units): + raise InputParseError("worker result request_id does not match the plan") + if any(unit.status != "succeeded" for unit in units): + raise InputParseError("failed worker results cannot produce a terminal receipt") + expected_decision_id = content_id( + { + "request_id": plan.request_id, + "unit_result_ids": sorted(unit.unit_result_id for unit in units), + "decision": verifier.decision, + "merge_verdict": verifier.merge_verdict, + "can_merge_without_human": verifier.can_merge_without_human, + } + ) + resolved_artifact_root = artifacts_root.resolve() + regenerated_artifacts = { + "agent_handoff_json", + "verification_artifact_manifest_json", + "verification_input_diff", + "verification_plan_json", + "verification_receipt_json", + "verification_unit_result_json", + "verify_run_json", + "verifier_json", + } + artifact_paths = { + name: _resolve_artifact_ref(resolved_artifact_root, value) + for name, value in verifier.artifacts.items() + if name not in regenerated_artifacts + } + artifact_paths.update( + { + "verification_plan_json": plan_path.resolve(), + "verification_input_diff": plan_path.with_name(plan.inputs.diff.path).resolve(), + **{ + f"verification_unit_result_{index}": path.resolve() + for index, path in enumerate(unit_paths) + }, + "verifier_json": verifier_path.resolve(), + } + ) + diff_artifact = artifact_paths["verification_input_diff"] + if not diff_artifact.is_file(): + raise InputParseError(f"prepared diff artifact is missing: {plan.inputs.diff.path}") + executor = build_executor(plan.engine) + if executor.executor_id not in executor_ids: + raise InputParseError("worker executor does not satisfy the prepared engine") + + verifier_payload = verifier.model_dump(mode="json") + verifier_payload.update( + { + "request_id": plan.request_id, + "subject_id": plan.subject.subject_id, + "input_set_id": plan.inputs.input_set_id, + "engine_requirement_id": plan.engine.engine_requirement_id, + "executor_id": executor.executor_id, + "decision_id": expected_decision_id, + } + ) + verifier = VerifierArtifact.model_validate(verifier_payload) + + report_path = artifact_paths.get("report_json") + report: ReadinessReport | None = None + if report_path is not None: + report = ReadinessReport.model_validate(_load_json(report_path)) + report.request_id = plan.request_id + report.subject_id = plan.subject.subject_id + report.input_set_id = plan.inputs.input_set_id + report.engine_requirement_id = plan.engine.engine_requirement_id + report.decision_id = expected_decision_id + report_path.write_text( + json.dumps(report_json_payload(report), indent=2), + encoding="utf-8", + ) + + packet_path = artifact_paths.get("packet_json") + if packet_path is not None: + packet = load_packet_json(packet_path.read_text(encoding="utf-8")) + packet.request_id = plan.request_id + packet.subject_id = plan.subject.subject_id + packet.input_set_id = plan.inputs.input_set_id + packet.engine_requirement_id = plan.engine.engine_requirement_id + packet.decision_id = expected_decision_id + write_packet_json(packet, packet_path) + + _write_model(verifier_path, verifier) + verify_run_path = artifact_paths.get( + "verify_run_json", resolved_artifact_root / "verify-run.json" + ) + verify_run_refs = { + name: VerifyRunArtifactRef( + path=path.relative_to(resolved_artifact_root).as_posix(), + sha256=_sha256_hex(path), + ) + for name, path in sorted(artifact_paths.items()) + if name + not in { + "verify_run_json", + "agent_handoff_json", + "verification_plan_json", + "verification_input_diff", + } + and path.is_file() + and _is_under(path, resolved_artifact_root) + } + outcome = VerifyRunOutcome( + exit_code=verifier.head_exit_code, + base_status=verifier.base_status, + execution=verifier.execution, + applicability=verifier.applicability, + decision=verifier.decision, + merge_verdict=verifier.merge_verdict, + can_merge_without_human=verifier.can_merge_without_human, + control=verifier.control, + ) + verify_run = build_verify_run_artifact( + plan=plan, + executor=executor, + unit_result_ids=[unit.unit_result_id for unit in units], + outcome=outcome, + artifacts=verify_run_refs, + ) + _write_model(verify_run_path, verify_run) + artifact_paths["verify_run_json"] = verify_run_path + + handoff_path = artifact_paths.get( + "agent_handoff_json", resolved_artifact_root / "agent-handoff.json" + ) + handoff = build_agent_handoff( + verifier=verifier, + report=report, + verify_run=verify_run, + ) + _write_model(handoff_path, handoff) + artifact_paths["agent_handoff_json"] = handoff_path + + manifest, receipt = build_terminal_receipt( + plan=plan, + unit_results=units, + decision=verifier.decision, + merge_verdict=verifier.merge_verdict, + can_merge_without_human=verifier.can_merge_without_human, + artifact_paths=artifact_paths, + artifact_root=resolved_artifact_root, + ) + if not _is_under(out, resolved_artifact_root): + raise InputParseError("receipt output must remain under --artifacts-root") + _write_model(resolved_artifact_root / "verification-artifacts.json", manifest) + _write_model(out, receipt) + typer.echo(json.dumps({"receipt_id": receipt.receipt_id, "receipt": str(out)})) + + +@verification_app.command("reproduce") +def reproduce( + receipt_path: Path = typer.Option(..., "--receipt"), + artifacts_root: Path = typer.Option(Path("."), "--artifacts-root"), +) -> None: + """Validate a terminal receipt and every artifact hash without execution.""" + + receipt = VerificationReceipt.model_validate(_load_json(receipt_path)) + try: + validate_receipt_artifacts(receipt, root=artifacts_root.resolve()) + except ValueError as exc: + raise InputParseError(str(exc)) from exc + typer.echo(json.dumps({"valid": True, "receipt_id": receipt.receipt_id})) + + +def _load_json(path: Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise InputParseError(f"Could not read {path}: {exc}") from exc + if not isinstance(value, dict): + raise InputParseError(f"Expected one JSON object in {path}") + return value + + +def _write_model(path: Path, value: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + json.dumps(value.model_dump(mode="json"), indent=2, sort_keys=True), + encoding="utf-8", + ) + + +def _under(root: Path, path: Path) -> Path: + candidate = path.resolve() if path.is_absolute() else (root / path).resolve() + if candidate != root and root not in candidate.parents: + raise ConfigError(f"Path escapes workspace: {path}") + return candidate + + +def _map(snapshot: Path, root: Path, path: Path | None) -> Path | None: + if path is None: + return None + return snapshot / path.resolve().relative_to(root.resolve()) + + +def _resolve_artifact_ref(root: Path, value: str) -> Path: + logical = Path(value) + candidates: list[Path] = [] + if not logical.is_absolute(): + candidates.append((root / logical).resolve()) + candidates.append((root / logical.name).resolve()) + valid = sorted( + { + candidate + for candidate in candidates + if candidate.is_file() and (candidate == root or root in candidate.parents) + } + ) + if len(valid) != 1: + raise InputParseError(f"artifact reference {value!r} did not resolve uniquely under {root}") + return valid[0] + + +def _sha256_hex(path: Path) -> str: + import hashlib + + digest = hashlib.sha256(path.read_bytes()).hexdigest() + return digest + + +def _is_under(path: Path, root: Path) -> bool: + resolved = path.resolve() + return resolved == root or root in resolved.parents + + +def _plugins_enabled(plan: VerificationPlan) -> bool | None: + value = plan.inputs.options.get("plugins_enabled") + return value if isinstance(value, bool) else None + + +def _validate_git_subject(plan: VerificationPlan, workspace: Path) -> None: + root = ensure_git_workspace(workspace.resolve()) + subject = plan.subject.git + if repository_identity(root) != subject.repository_id: + raise ValueError("worker repository identity does not match the plan") + + if subject.head_commit_sha is not None: + if commit_sha(root, subject.head_commit_sha) != subject.head_commit_sha: + raise ValueError("worker lacks the plan's exact head commit") + if tree_sha(root, subject.head_commit_sha) != subject.head_tree_sha: + raise ValueError("worker head tree does not match the plan") + if subject.base_commit_sha is not None: + if commit_sha(root, subject.base_commit_sha) != subject.base_commit_sha: + raise ValueError("worker lacks the plan's exact base commit") + if tree_sha(root, subject.base_commit_sha) != subject.base_tree_sha: + raise ValueError("worker base tree does not match the plan") + if subject.merge_base_sha is not None: + if subject.base_commit_sha is None or subject.head_commit_sha is None: + raise ValueError("plan merge base lacks exact base/head commits") + actual_merge_base = merge_base_sha( + root, + subject.base_commit_sha, + subject.head_commit_sha, + ) + if actual_merge_base != subject.merge_base_sha: + raise ValueError("worker merge base does not match the plan") + if subject.snapshot_kind == "worktree_overlay": + if commit_sha(root, "HEAD") != subject.head_commit_sha: + raise ValueError("worker HEAD does not match the worktree-overlay plan") + rows: list[dict[str, Any]] = [] + for relative in plan.inputs.changed_paths: + candidate = (root / relative).resolve() + if candidate != root and root not in candidate.parents: + raise ValueError(f"plan changed path escapes workspace: {relative}") + rows.append( + { + "path": relative, + "status": "present" if candidate.is_file() else "deleted", + "sha256": sha256_file(candidate) if candidate.is_file() else None, + } + ) + actual_overlay = content_id(rows) if rows else None + if actual_overlay != subject.worktree_overlay_sha256: + raise ValueError("worker worktree overlay does not match the plan") + + +def _git_identity(root: Path, base: str | None, head: str) -> dict[str, str | None]: + base_commit = commit_sha(root, base) if base else None + head_commit = commit_sha(root, head) + return { + "repository_id": repository_identity(root), + "base_commit_sha": base_commit, + "base_tree_sha": tree_sha(root, base) if base and base_commit else None, + "head_commit_sha": head_commit, + "head_tree_sha": tree_sha(root, head) if head_commit else None, + "merge_base_sha": merge_base_sha(root, base, head) if base and base_commit else None, + } + + +__all__ = ["verification_app"] diff --git a/src/agents_shipgate/cli/verify/git.py b/src/agents_shipgate/cli/verify/git.py index 2c48446c..66a7e7f6 100644 --- a/src/agents_shipgate/cli/verify/git.py +++ b/src/agents_shipgate/cli/verify/git.py @@ -1,10 +1,11 @@ from __future__ import annotations -import io +import os +import re import subprocess -import tarfile from dataclasses import dataclass from pathlib import Path +from urllib.parse import urlsplit from agents_shipgate.core.errors import ConfigError @@ -63,18 +64,14 @@ def detect_default_base(workspace: Path, head: str = "HEAD") -> str | None: return detect_default_base_with_notes(workspace, head).base -def detect_default_base_with_notes( - workspace: Path, head: str = "HEAD" -) -> DefaultBaseDetection: +def detect_default_base_with_notes(workspace: Path, head: str = "HEAD") -> DefaultBaseDetection: """Return the implicit base plus warnings for skipped local defaults.""" head_sha = commit_sha(workspace, head) if head_sha is None: return DefaultBaseDetection(base=None, notes=[]) candidates: list[str] = [] - origin_head = _run_git( - workspace, ["rev-parse", "--abbrev-ref", "origin/HEAD"], check=False - ) + origin_head = _run_git(workspace, ["rev-parse", "--abbrev-ref", "origin/HEAD"], check=False) if origin_head.returncode == 0: name = origin_head.stdout.strip() if name and name != "origin/HEAD": @@ -148,6 +145,50 @@ def tree_sha(workspace: Path, ref: str) -> str: return result.stdout.strip() +def merge_base_sha(workspace: Path, base: str, head: str) -> str | None: + result = _run_git(workspace, ["merge-base", base, head], check=False) + if result.returncode != 0: + return None + return result.stdout.strip() or None + + +def commit_date(workspace: Path, ref: str) -> str: + return _run_git(workspace, ["show", "-s", "--format=%cs", ref]).stdout.strip() + + +def repository_identity(workspace: Path) -> str: + """Return a credential-free stable repository locator.""" + + remote = _run_git(workspace, ["remote", "get-url", "origin"], check=False) + value = remote.stdout.strip() if remote.returncode == 0 else "" + normalized = _normalize_repository_url(value) + return normalized or f"local:{workspace.name}" + + +def _normalize_repository_url(value: str) -> str | None: + """Normalize common HTTPS/SSH Git locators without credentials.""" + + if not value: + return None + host = "" + path = "" + if "://" in value: + parsed = urlsplit(value) + host = (parsed.hostname or "").lower() + path = parsed.path + else: + scp = re.fullmatch(r"(?:[^@/:]+@)?([^/:]+):(.+)", value) + if scp: + host = scp.group(1).lower() + path = scp.group(2) + normalized_path = path.strip("/") + if normalized_path.endswith(".git"): + normalized_path = normalized_path[:-4] + if not host or not normalized_path: + return None + return f"{host}/{normalized_path}" + + def git_path(workspace: Path, path: str) -> Path: result = _run_git(workspace, ["rev-parse", "--git-path", path]) resolved = Path(result.stdout.strip()) @@ -197,14 +238,30 @@ def working_tree_context(workspace: Path) -> tuple[list[str], str]: def archive_tree(workspace: Path, ref: str, destination: Path) -> None: + """Materialize exact Git blobs without export-ignore or substitutions.""" + destination.mkdir(parents=True, exist_ok=True) - archive = _run_git( - workspace, - ["archive", "--format=tar", ref], - text=False, - ) - with tarfile.open(fileobj=io.BytesIO(archive.stdout), mode="r:") as tar: - _safe_extract(tar, destination) + listing = _run_git(workspace, ["ls-tree", "-r", "-z", ref], text=False).stdout + root = destination.resolve() + for raw in listing.split(b"\0"): + if not raw: + continue + metadata, raw_path = raw.split(b"\t", 1) + mode, object_type, oid = metadata.decode("ascii").split(" ", 2) + path_text = raw_path.decode("utf-8", errors="strict") + target = (root / path_text).resolve() + if target != root and root not in target.parents: + raise ConfigError(f"Git tree path escapes destination: {path_text}") + if object_type != "blob" or mode in {"120000", "160000"}: + raise ConfigError( + f"Git tree contains unsupported external binding at {path_text} " + f"(mode {mode}, type {object_type})." + ) + target.parent.mkdir(parents=True, exist_ok=True) + blob = _run_git(workspace, ["cat-file", "blob", oid], text=False).stdout + target.write_bytes(blob) + if mode == "100755": + os.chmod(target, 0o755) def _run_git( @@ -238,29 +295,15 @@ def staged_paths_under(workspace: Path, subdir: str) -> list[str]: """ prefix = subdir.rstrip("/") + "/" - result = _run_git( - workspace, ["diff", "--cached", "--name-only", "--relative"], check=False - ) + result = _run_git(workspace, ["diff", "--cached", "--name-only", "--relative"], check=False) if result.returncode != 0: return [] - return [ - line.strip() - for line in result.stdout.splitlines() - if line.strip().startswith(prefix) - ] - - -def _safe_extract(tar: tarfile.TarFile, destination: Path) -> None: - root = destination.resolve() - for member in tar.getmembers(): - target = (root / member.name).resolve() - if target != root and root not in target.parents: - raise ConfigError(f"Base archive contains path outside destination: {member.name}") - tar.extractall(root, filter="data") + return [line.strip() for line in result.stdout.splitlines() if line.strip().startswith(prefix)] __all__ = [ "archive_tree", + "commit_date", "commit_sha", "DefaultBaseDetection", "detect_default_base", @@ -268,7 +311,9 @@ def _safe_extract(tar: tarfile.TarFile, destination: Path) -> None: "diff_context", "ensure_git_workspace", "git_path", + "merge_base_sha", "read_file_at_ref", + "repository_identity", "ref_exists", "staged_paths_under", "tree_sha", diff --git a/src/agents_shipgate/cli/verify/orchestrator.py b/src/agents_shipgate/cli/verify/orchestrator.py index 918d9c88..04373824 100644 --- a/src/agents_shipgate/cli/verify/orchestrator.py +++ b/src/agents_shipgate/cli/verify/orchestrator.py @@ -7,6 +7,7 @@ import shlex import shutil import tempfile +from datetime import date from pathlib import Path from typing import Any @@ -24,6 +25,14 @@ render_capability_lock_json, ) from agents_shipgate.core.errors import AgentsShipgateError, ConfigError, InputParseError +from agents_shipgate.core.evaluation_clock import use_evaluation_date +from agents_shipgate.core.verification_identity import ( + build_executor, + build_terminal_receipt, + build_unit_result, + build_verification_plan, +) +from agents_shipgate.packet.json_packet import load_packet_json, write_packet_json from agents_shipgate.report.capability_lock_diff_markdown import ( render_capability_lock_diff_markdown, ) @@ -39,6 +48,7 @@ from agents_shipgate.schemas.capabilities import CapabilityLockDiffV1, CapabilityLockFileV1 from agents_shipgate.schemas.report import ReadinessReport, ReleaseDecision from agents_shipgate.schemas.verification import VerificationContext +from agents_shipgate.schemas.verification_identity import VerificationPlan, content_id from agents_shipgate.schemas.verifier import ( MergeVerdict, VerifierArtifact, @@ -50,10 +60,7 @@ ) from agents_shipgate.schemas.verify_run import ( VerifyRunArtifactRef, - VerifyRunInputs, VerifyRunOutcome, - VerifyRunPolicyPack, - VerifyRunSubject, build_verify_run_artifact, ) from agents_shipgate.triggers import evaluate @@ -62,12 +69,16 @@ from .fix_task import FORBIDDEN_SHORTCUTS, build_fix_task from .git import ( archive_tree, + commit_date, + commit_sha, detect_default_base_with_notes, diff_context, ensure_git_workspace, git_path, + merge_base_sha, read_file_at_ref, ref_exists, + repository_identity, tree_sha, working_tree_context, ) @@ -235,6 +246,8 @@ def run_verify( ) return verifier, None, 2 + verification_date = commit_date(git_root, head) + if base is None and auto_base: detection = detect_default_base_with_notes(git_root, head) base_notes.extend(detection.notes) @@ -393,6 +406,7 @@ def run_verify( plugins_enabled=plugins_enabled, no_heuristics=no_heuristics, verbose=verbose, + evaluation_date=verification_date, ) base_notes.extend(cache_notes) @@ -402,6 +416,8 @@ def run_verify( scan_error: BaseException | None = None head_tmp: tempfile.TemporaryDirectory[str] | None = None head_config_path = config_path + head_input_root = git_root + head_baseline_path = baseline_path head_policy_pack_paths = policy_pack_paths head_tree: str | None = None head_capability_lock: CapabilityLockFileV1 | None = None @@ -416,6 +432,7 @@ def capture_capability_lock(lock: CapabilityLockFileV1) -> None: head_tmp = tempfile.TemporaryDirectory(prefix="agents-shipgate-verify-head-") head_tree_dir = Path(head_tmp.name) / "head" archive_tree(git_root, head, head_tree_dir) + head_input_root = head_tree_dir head_tree = tree_sha(git_root, head) head_config_path = head_tree_dir / config_relative if not head_config_path.is_file(): @@ -427,30 +444,36 @@ def capture_capability_lock(lock: CapabilityLockFileV1) -> None: tree_dir=head_tree_dir, policy_packs=policy_pack_paths or [], ) - report, head_exit_code = run_scan( - config_path=head_config_path, - output_dir=out_dir, - formats=HEAD_FORMATS, - ci_mode=ci_mode, - fail_on=fail_on, - baseline_path=baseline_path, - diff_from_path=base_report, - baseline_mode=baseline_mode, - policy_pack_paths=head_policy_pack_paths, - plugins_enabled=plugins_enabled, - verbose=verbose, - suggest_patches=suggest_patches, - packet_enabled=True, - packet_formats=HEAD_PACKET_FORMATS, - no_heuristics=no_heuristics, - verification_context=VerificationContext( - changed_files=changed_files, - diff_text=diff_text, - diff_text_available=bool(diff_text), - trigger_result=trigger, - ), - capability_lock_callback=capture_capability_lock, - ) + head_baseline_path = _map_optional_tree_path( + git_root=git_root, + tree_dir=head_tree_dir, + path=baseline_path, + ) + with use_evaluation_date(date.fromisoformat(verification_date)): + report, head_exit_code = run_scan( + config_path=head_config_path, + output_dir=out_dir, + formats=HEAD_FORMATS, + ci_mode=ci_mode, + fail_on=fail_on, + baseline_path=head_baseline_path, + diff_from_path=base_report, + baseline_mode=baseline_mode, + policy_pack_paths=head_policy_pack_paths, + plugins_enabled=plugins_enabled, + verbose=verbose, + suggest_patches=suggest_patches, + packet_enabled=True, + packet_formats=HEAD_PACKET_FORMATS, + no_heuristics=no_heuristics, + verification_context=VerificationContext( + changed_files=changed_files, + diff_text=diff_text, + diff_text_available=bool(diff_text), + trigger_result=trigger, + ), + capability_lock_callback=capture_capability_lock, + ) head_exit_code = _apply_strict_plugins( report, head_exit_code, strict_plugins=strict_plugins ) @@ -512,14 +535,26 @@ def capture_capability_lock(lock: CapabilityLockFileV1) -> None: pr_comment_path, report=report, git_root=git_root, - config_path=config_path, - baseline_path=baseline_path, - policy_pack_paths=policy_pack_paths or [], + config_path=head_config_path, + baseline_path=head_baseline_path, + policy_pack_paths=head_policy_pack_paths or [], plugins_enabled=plugins_enabled, no_heuristics=no_heuristics, fail_on=fail_on, pr_comment_style=pr_comment_style, capability_lock_diff=capability_lock_diff, + input_root=head_input_root, + diff_text=diff_text, + diff_from_path=base_report, + verification_options={ + "archive_head": archive_head, + "baseline_mode": baseline_mode, + "strict_plugins": strict_plugins, + "suggest_patches": suggest_patches, + "source_head_commit_sha": os.getenv("SOURCE_HEAD_SHA") or None, + "evaluated_head_commit_sha": os.getenv("GITHUB_SHA") or None, + }, + evaluation_date=verification_date, ) except Exception: if scan_error is None: @@ -543,6 +578,7 @@ def _prepare_base_report( plugins_enabled: bool | None, no_heuristics: bool, verbose: bool, + evaluation_date: str, ) -> tuple[ VerifierBaseStatus, str | None, @@ -564,16 +600,20 @@ def _prepare_base_report( policy_packs=policy_packs, plugins_enabled=plugins_enabled, no_heuristics=no_heuristics, + evaluation_date=evaluation_date, ) - if cache_report.exists(): + if cache_report.exists() and _cache_report_valid(cache_report): base_lock, lock_notes = _load_cached_capability_lock(cache_report) return ( - "cache_hit", + "succeeded", base_tree, cache_report, base_lock, - [f"Reused cached base report for tree {base_tree}.", *lock_notes], + [f"Base report resolved for tree {base_tree}.", *lock_notes], ) + if cache_report.exists(): + notes.append("Discarded base cache entry whose content hash did not validate.") + cache_report.unlink(missing_ok=True) with tempfile.TemporaryDirectory(prefix="agents-shipgate-verify-") as tmp: tmp_root = Path(tmp) @@ -607,7 +647,10 @@ def capture_base_capability_lock(lock: CapabilityLockFileV1) -> None: base_capability_lock = lock try: - with _without_github_step_summary(): + with ( + _without_github_step_summary(), + use_evaluation_date(date.fromisoformat(evaluation_date)), + ): _base_report, base_exit = run_scan( config_path=base_config, output_dir=base_out, @@ -665,7 +708,7 @@ def capture_base_capability_lock(lock: CapabilityLockFileV1) -> None: else: notes.append("Base scan did not produce a capability lock; diff artifact disabled.") _prune_base_scan_cache(cache_report.parents[1], keep=BASE_CACHE_KEEP_ENTRIES) - notes.append(f"Cached base report for tree {base_tree}.") + notes.append(f"Base report resolved for tree {base_tree}.") return "succeeded", base_tree, cache_report, base_capability_lock, notes @@ -678,18 +721,31 @@ def _cache_report_path( policy_packs: list[Path], plugins_enabled: bool | None, no_heuristics: bool, + evaluation_date: str, ) -> Path: payload = { "version": 1, "agents_shipgate_version": __version__, "base_tree": base_tree, "config": config_relative.as_posix(), - "baseline": ( - _display_path(baseline_path.resolve(), git_root) if baseline_path is not None else None - ), - "policy_packs": [_display_path(path.resolve(), git_root) for path in policy_packs], + "baseline": { + "path": ( + _display_path(baseline_path.resolve(), git_root) + if baseline_path is not None + else None + ), + "sha256": _sha256_file(baseline_path), + }, + "policy_packs": [ + { + "path": _display_path(path.resolve(), git_root), + "sha256": _sha256_file(path), + } + for path in policy_packs + ], "plugins_enabled": plugins_enabled, "no_heuristics": no_heuristics, + "evaluation_date": evaluation_date, } key = hashlib.sha256( json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8") @@ -709,10 +765,25 @@ def _copy_report_to_cache(source_report: Path, cache_report: Path) -> None: try: shutil.copy2(source_report, temp_path) temp_path.replace(cache_report) + cache_report.with_suffix(".sha256").write_text( + f"{_sha256_file(cache_report)}\n", + encoding="ascii", + ) finally: temp_path.unlink(missing_ok=True) +def _cache_report_valid(cache_report: Path) -> bool: + digest_path = cache_report.with_suffix(".sha256") + if not digest_path.is_file(): + return False + try: + expected = digest_path.read_text(encoding="ascii").strip() + except OSError: + return False + return bool(expected and expected == _sha256_file(cache_report)) + + def _base_capability_lock_cache_path(cache_report: Path) -> Path: return cache_report.with_name("capabilities.lock.json") @@ -1123,6 +1194,12 @@ def _artifact_paths( "verifier_json": out_dir / "verifier.json", "verify_run_json": out_dir / "verify-run.json", "agent_handoff_json": out_dir / "agent-handoff.json", + "verification_plan_json": out_dir / "verification-plan.json", + "verification_input_diff": out_dir / "verification-input.diff", + "verification_base_report_json": out_dir / "verification-base-report.json", + "verification_unit_result_json": out_dir / "verification-unit-result.json", + "verification_artifact_manifest_json": out_dir / "verification-artifacts.json", + "verification_receipt_json": out_dir / "verification-receipt.json", "pr_comment": out_dir / "pr-comment.md", } if include_scan_artifacts: @@ -1140,7 +1217,18 @@ def _artifact_paths( return { key: _display_path(path.resolve(), git_root) for key, path in candidates.items() - if key in {"verifier_json", "verify_run_json", "agent_handoff_json", "pr_comment"} + if key + in { + "verifier_json", + "verify_run_json", + "agent_handoff_json", + "verification_plan_json", + "verification_input_diff", + "verification_unit_result_json", + "verification_artifact_manifest_json", + "verification_receipt_json", + "pr_comment", + } or path.exists() } @@ -1166,12 +1254,19 @@ def _remove_scan_artifacts(out_dir: Path) -> None: def _clear_trusted_handoff(out_dir: Path) -> None: - """Remove any prior handoff before constructing a new trusted projection.""" + """Remove every prior terminal/projection artifact before a new run.""" - path = out_dir / "agent-handoff.json" - if path.is_file() or path.is_symlink(): - with contextlib.suppress(OSError): - path.unlink() + for name in ( + "agent-handoff.json", + "verification-plan.json", + "verification-unit-result.json", + "verification-artifacts.json", + "verification-receipt.json", + ): + path = out_dir / name + if path.is_file() or path.is_symlink(): + with contextlib.suppress(OSError): + path.unlink() def _write_artifacts( @@ -1190,6 +1285,11 @@ def _write_artifacts( fail_on: list[str] | None, pr_comment_style: str, capability_lock_diff: CapabilityLockDiffV1 | None = None, + input_root: Path | None = None, + diff_text: str = "", + diff_from_path: Path | None = None, + verification_options: dict[str, Any] | None = None, + evaluation_date: str | None = None, ) -> None: verifier_path.parent.mkdir(parents=True, exist_ok=True) verifier_path.write_text( @@ -1209,17 +1309,152 @@ def _write_artifacts( # in-memory report can still produce the canonical public payload. if report is not None: report_json_payload(report) - verify_run = _write_verify_run_artifact( + # Always preserve the actionable control handoff, including typed input + # failures that cannot produce a reproducible request identity. A complete + # run overwrites this projection after the verify-run artifact is built. + handoff_path = verifier_path.with_name("agent-handoff.json") + initial_handoff = build_agent_handoff( verifier=verifier, report=report, - path=verify_run_path, + verify_run=None, + ) + handoff_path.write_text( + json.dumps(initial_handoff.model_dump(mode="json"), indent=2, sort_keys=True), + encoding="utf-8", + ) + # A missing or invalid manifest cannot yield a reproducible request + # identity. Error artifacts remain useful diagnostics, but they are not a + # trusted receipt and downstream consumers must reject their absence. + if not config_path.is_file() or not ref_exists(git_root, verifier.head_ref): + return + resolved_input_root = (input_root or git_root).resolve() + logical_config = ( + config_path.resolve().relative_to(resolved_input_root).as_posix() + if resolved_input_root in config_path.resolve().parents + else _display_path(config_path.resolve(), git_root) + ) + resolved_date = evaluation_date or commit_date(git_root, verifier.head_ref) + base_commit = commit_sha(git_root, verifier.base_ref) if verifier.base_ref else None + head_commit = commit_sha(git_root, verifier.head_ref) + portable_diff_from_path: Path | None = None + if diff_from_path is not None and diff_from_path.is_file(): + portable_diff_from_path = verifier_path.with_name("verification-base-report.json") + shutil.copyfile(diff_from_path, portable_diff_from_path) + verifier.artifacts["verification_base_report_json"] = _display_path( + portable_diff_from_path.resolve(), git_root + ) + plan = build_verification_plan( git_root=git_root, + input_root=resolved_input_root, config_path=config_path, + config_logical_path=logical_config, + base_ref=verifier.base_ref, + head_ref=verifier.head_ref, + archived_head=resolved_input_root != git_root.resolve(), + repository_id=repository_identity(git_root), + base_commit_sha=base_commit, + base_tree_sha=( + tree_sha(git_root, verifier.base_ref) if verifier.base_ref and base_commit else None + ), + head_commit_sha=head_commit, + head_tree_sha=(tree_sha(git_root, verifier.head_ref) if head_commit else None), + merge_base_sha=( + merge_base_sha(git_root, verifier.base_ref, verifier.head_ref) + if verifier.base_ref and base_commit + else None + ), + changed_files=verifier.changed_files, + diff_text=diff_text, baseline_path=baseline_path, + diff_from_path=portable_diff_from_path, policy_pack_paths=policy_pack_paths, + evaluation_date=resolved_date, + options={ + "ci_mode": verifier.mode, + "fail_on": sorted(fail_on or []), + "no_heuristics": no_heuristics, + "plugins_enabled": plugins_enabled is not False, + **(verification_options or {}), + }, plugins_enabled=plugins_enabled, - no_heuristics=no_heuristics, - fail_on=fail_on, + ) + plan_path = verifier_path.with_name("verification-plan.json") + diff_input_path = verifier_path.with_name("verification-input.diff") + diff_input_path.write_text(diff_text, encoding="utf-8") + plan_path.write_text( + json.dumps(plan.model_dump(mode="json"), indent=2, sort_keys=True), + encoding="utf-8", + ) + unit_result = build_unit_result( + plan=plan, + status=("succeeded" if verifier.execution in {"succeeded", "skipped"} else "failed"), + normalized_ir={ + "execution": verifier.execution, + "report_run_id": report.run_id if report is not None else None, + "report_schema_version": (report.report_schema_version if report is not None else None), + }, + ) + unit_path = verifier_path.with_name("verification-unit-result.json") + unit_path.write_text( + json.dumps(unit_result.model_dump(mode="json"), indent=2, sort_keys=True), + encoding="utf-8", + ) + executor = build_executor(plan.engine) + decision_id = content_id( + { + "request_id": plan.request_id, + "unit_result_ids": [unit_result.unit_result_id], + "decision": verifier.decision, + "merge_verdict": verifier.merge_verdict, + "can_merge_without_human": verifier.can_merge_without_human, + } + ) + verifier.request_id = plan.request_id + verifier.subject_id = plan.subject.subject_id + verifier.input_set_id = plan.inputs.input_set_id + verifier.engine_requirement_id = plan.engine.engine_requirement_id + verifier.executor_id = executor.executor_id + verifier.decision_id = decision_id + if report is not None: + report.request_id = plan.request_id + report.subject_id = plan.subject.subject_id + report.input_set_id = plan.inputs.input_set_id + report.engine_requirement_id = plan.engine.engine_requirement_id + report.decision_id = decision_id + report_path = verifier_path.with_name("report.json") + if report_path.is_file(): + report_path.write_text( + json.dumps(report_json_payload(report), indent=2), + encoding="utf-8", + ) + packet_path = verifier_path.with_name("packet.json") + if packet_path.is_file(): + packet = load_packet_json(packet_path.read_text(encoding="utf-8")) + packet.request_id = plan.request_id + packet.subject_id = plan.subject.subject_id + packet.input_set_id = plan.inputs.input_set_id + packet.engine_requirement_id = plan.engine.engine_requirement_id + packet.decision_id = decision_id + write_packet_json(packet, packet_path) + verifier_path.write_text( + json.dumps(verifier.model_dump(mode="json"), indent=2), + encoding="utf-8", + ) + pr_comment_path.write_text( + render_pr_comment( + verifier, + report=report, + style=pr_comment_style, + capability_lock_diff=capability_lock_diff, + ), + encoding="utf-8", + ) + verify_run = _write_verify_run_artifact( + verifier=verifier, + path=verify_run_path, + git_root=git_root, + plan=plan, + unit_result_id=unit_result.unit_result_id, ) # Fail closed if this projection ever disagrees with the verifier/report # substrate. The handoff artifact is additive, but an inconsistent handoff @@ -1229,46 +1464,68 @@ def _write_artifacts( report=report, verify_run=verify_run, ) - verifier_path.with_name("agent-handoff.json").write_text( + handoff_path.write_text( json.dumps(handoff.model_dump(mode="json"), indent=2, sort_keys=True), encoding="utf-8", ) + if unit_result.status != "succeeded": + # Failed executions retain their plan, failed unit IR, verifier, + # verify-run, and actionable handoff, but never receive a terminal + # success receipt. + return + identity_names = { + "verification_plan_json", + "verification_input_diff", + "verification_unit_result_json", + "verification_artifact_manifest_json", + "verification_receipt_json", + } + artifact_paths = { + name: _resolve_under_workspace(git_root, Path(value)) + for name, value in verifier.artifacts.items() + if name not in identity_names + } + artifact_paths.update( + { + "verification_plan_json": plan_path, + "verification_input_diff": diff_input_path, + "verification_unit_result_json": unit_path, + "agent_handoff_json": handoff_path, + "verify_run_json": verify_run_path, + } + ) + manifest, receipt = build_terminal_receipt( + plan=plan, + unit_results=[unit_result], + decision=verifier.decision, + merge_verdict=verifier.merge_verdict, + can_merge_without_human=verifier.can_merge_without_human, + artifact_paths=artifact_paths, + artifact_root=verifier_path.parent, + attempt_id=None, + ) + manifest_path = verifier_path.with_name("verification-artifacts.json") + manifest_path.write_text( + json.dumps(manifest.model_dump(mode="json"), indent=2, sort_keys=True), + encoding="utf-8", + ) + # Terminal receipt is written last. Its presence means every referenced + # artifact existed and was hashed after final serialization. + receipt_path = verifier_path.with_name("verification-receipt.json") + receipt_path.write_text( + json.dumps(receipt.model_dump(mode="json"), indent=2, sort_keys=True), + encoding="utf-8", + ) def _write_verify_run_artifact( *, verifier: VerifierArtifact, - report: ReadinessReport | None, path: Path, git_root: Path, - config_path: Path, - baseline_path: Path | None, - policy_pack_paths: list[Path], - plugins_enabled: bool | None, - no_heuristics: bool, - fail_on: list[str] | None, + plan: VerificationPlan, + unit_result_id: str, ) -> Any: - subject = VerifyRunSubject( - workspace=".", - config=_display_path(config_path.resolve(), git_root), - base_ref=verifier.base_ref, - head_ref=verifier.head_ref, - base_tree_sha=verifier.base_tree_sha, - head_tree_sha=verifier.head_tree_sha, - ) - inputs = VerifyRunInputs( - config_sha256=_sha256_file(config_path), - baseline_sha256=_sha256_file(baseline_path) if baseline_path is not None else None, - policy_packs=_verify_run_policy_packs( - report=report, - policy_pack_paths=policy_pack_paths, - git_root=git_root, - ), - plugins_enabled=plugins_enabled, - no_heuristics=no_heuristics, - ci_mode=verifier.mode, - fail_on=list(fail_on or []), - ) outcome = VerifyRunOutcome( exit_code=verifier.head_exit_code, base_status=verifier.base_status, @@ -1280,8 +1537,9 @@ def _write_verify_run_artifact( control=verifier.control, ) artifact = build_verify_run_artifact( - subject=subject, - inputs=inputs, + plan=plan, + executor=build_executor(plan.engine), + unit_result_ids=[unit_result_id], outcome=outcome, artifacts=_verify_run_artifact_refs(verifier, git_root=git_root), ) @@ -1292,43 +1550,21 @@ def _write_verify_run_artifact( return artifact -def _verify_run_policy_packs( - *, - report: ReadinessReport | None, - policy_pack_paths: list[Path], - git_root: Path, -) -> list[VerifyRunPolicyPack]: - if report is not None and report.loaded_policy_packs: - return [ - VerifyRunPolicyPack( - id=pack.id, - name=pack.name, - version=pack.version, - path=pack.path, - sha256=pack.sha256, - sha256_status=pack.sha256_status, - rule_count=pack.rule_count, - ) - for pack in report.loaded_policy_packs - ] - return [ - VerifyRunPolicyPack( - path=_display_path(path.resolve(), git_root), - sha256=_sha256_file(path), - sha256_status="unpinned", - ) - for path in policy_pack_paths - ] - - def _verify_run_artifact_refs( verifier: VerifierArtifact, *, git_root: Path, ) -> dict[str, VerifyRunArtifactRef]: refs: dict[str, VerifyRunArtifactRef] = {} + terminal_identity_artifacts = { + "verify_run_json", + "verification_plan_json", + "verification_unit_result_json", + "verification_artifact_manifest_json", + "verification_receipt_json", + } for key, value in sorted(verifier.artifacts.items()): - if key == "verify_run_json": + if key in terminal_identity_artifacts: continue artifact_path = _resolve_under_workspace(git_root, Path(value)) refs[key] = VerifyRunArtifactRef( diff --git a/src/agents_shipgate/core/agent_handoff.py b/src/agents_shipgate/core/agent_handoff.py index fdac08bf..d7967029 100644 --- a/src/agents_shipgate/core/agent_handoff.py +++ b/src/agents_shipgate/core/agent_handoff.py @@ -35,7 +35,7 @@ def build_agent_handoff( verify_run_payload = _model_payload(verify_run) if verify_run is not None else {} verify_run_model = ( VerifyRunArtifact.model_validate(verify_run_payload) - if verify_run_payload.get("schema_version") == "shipgate.verify_run/v2" + if verify_run_payload.get("schema_version") == "shipgate.verify_run/v3" else None ) if verify_run_model is not None: @@ -167,9 +167,7 @@ def _blocked_by(release_decision: dict[str, Any]) -> list[AgentHandoffBlockedBy] blocks_release=_bool_or_none(item.get("blocks_release")), capability_refs=_str_list(item.get("capability_refs")), capability_trace_refs=_str_list(item.get("capability_trace_refs")), - support_hash=_str_or_none( - _dict(item.get("support")).get("support_hash") - ), + support_hash=_str_or_none(_dict(item.get("support")).get("support_hash")), ) ) return out @@ -230,12 +228,26 @@ def _patch_target(patch: dict[str, Any]) -> str | None: def _reproducibility(verify_run: dict[str, Any]) -> AgentHandoffReproducibility: - inputs = _dict(verify_run.get("inputs")) + plan = _dict(verify_run.get("plan")) + inputs = _dict(plan.get("inputs")) or _dict(verify_run.get("inputs")) + subject = _dict(plan.get("subject")) + engine = _dict(plan.get("engine")) + executor = _dict(verify_run.get("executor")) artifacts = _dict(verify_run.get("artifacts")) return AgentHandoffReproducibility( run_id=_str_or_none(verify_run.get("run_id")), - config_sha256=_str_or_none(inputs.get("config_sha256")), - baseline_sha256=_str_or_none(inputs.get("baseline_sha256")), + request_id=_str_or_none(verify_run.get("request_id")), + subject_id=_str_or_none(subject.get("subject_id")), + input_set_id=_str_or_none(inputs.get("input_set_id")), + engine_requirement_id=_str_or_none(engine.get("engine_requirement_id")), + executor_id=_str_or_none(executor.get("executor_id")), + decision_id=_str_or_none(verify_run.get("decision_id")), + config_sha256=_str_or_none( + _dict(inputs.get("config")).get("sha256") or inputs.get("config_sha256") + ), + baseline_sha256=_str_or_none( + _dict(inputs.get("baseline")).get("sha256") or inputs.get("baseline_sha256") + ), policy_packs=[ dict(item) for item in inputs.get("policy_packs") or [] if isinstance(item, dict) ], diff --git a/src/agents_shipgate/core/baseline.py b/src/agents_shipgate/core/baseline.py index 295ebad5..90cead47 100644 --- a/src/agents_shipgate/core/baseline.py +++ b/src/agents_shipgate/core/baseline.py @@ -21,6 +21,7 @@ ) from agents_shipgate.core.check_ids import LEGACY_CHECK_ID_ALIASES from agents_shipgate.core.errors import InputParseError +from agents_shipgate.core.evaluation_clock import evaluation_date from agents_shipgate.core.findings.identity import legacy_policy_routing_fingerprint from agents_shipgate.schemas.baseline import ( BaselineFile, @@ -88,8 +89,7 @@ def baseline_from_report( scanner_version = scanner_version or _SCANNER_VERSION recorded_at = now or utc_now_isoformat() prior_by_fp: dict[str, BaselineFinding] = { - entry.fingerprint: entry - for entry in (prior_baseline.findings if prior_baseline else []) + entry.fingerprint: entry for entry in (prior_baseline.findings if prior_baseline else []) } findings: list[BaselineFinding] = [] for finding in _active_findings(report.findings): @@ -196,8 +196,7 @@ def write_baseline( if audit_log_path is None: audit_log_path = path.parent / "baseline-audit.log" prior_fps: set[str] = { - entry.fingerprint - for entry in (prior_baseline.findings if prior_baseline else []) + entry.fingerprint for entry in (prior_baseline.findings if prior_baseline else []) } new_fps: set[str] = {entry.fingerprint for entry in baseline.findings} audit_entry = BaselineAuditEntry( @@ -288,9 +287,7 @@ def baseline_status_payload( "tool_name": finding.tool_name, "owner": owner, "reason": reason, - "recorded_at": ( - provenance.recorded_at if provenance is not None else None - ), + "recorded_at": (provenance.recorded_at if provenance is not None else None), "age_days": age_days, "expires": expires.isoformat() if expires is not None else None, "days_until_expiry": days_until_expiry, @@ -365,9 +362,7 @@ def apply_baseline( legacy_fingerprints: Sequence[LegacyFingerprintEntry] | None = None, ) -> BaselineSummary: baseline_by_fingerprint = { - finding.fingerprint: finding - for finding in baseline.findings - if finding.fingerprint + finding.fingerprint: finding for finding in baseline.findings if finding.fingerprint } baseline_fingerprints = set(baseline_by_fingerprint) current_active_fingerprints: set[str] = set() @@ -387,8 +382,7 @@ def apply_baseline( index, ) legacy_identity_unambiguous = ( - finding.tool_name is None - or len(tool_ids_by_name.get(finding.tool_name, set())) <= 1 + finding.tool_name is None or len(tool_ids_by_name.get(finding.tool_name, set())) <= 1 ) if not legacy_identity_unambiguous: legacy_candidates.clear() @@ -399,12 +393,8 @@ def apply_baseline( continue current_active_fingerprints.add(fingerprint) entry = baseline_by_fingerprint.get(fingerprint) - current_support_hash = ( - finding.support.support_hash if finding.support is not None else None - ) - support_matches = ( - entry is not None and entry.support_hash == current_support_hash - ) + current_support_hash = finding.support.support_hash if finding.support is not None else None + support_matches = entry is not None and entry.support_hash == current_support_hash if fingerprint in baseline_fingerprints and support_matches: finding.baseline_status = "matched" matched += 1 @@ -424,9 +414,7 @@ def apply_baseline( matched_count=matched, new_count=new, resolved_count=len( - baseline_fingerprints - - current_active_fingerprints - - matched_legacy_fingerprints + baseline_fingerprints - current_active_fingerprints - matched_legacy_fingerprints ), ) @@ -460,9 +448,7 @@ def _preserve_created_at_when_content_matches( """ if prior_baseline is None: return baseline - if _baseline_content_identity(prior_baseline) != _baseline_content_identity( - baseline - ): + if _baseline_content_identity(prior_baseline) != _baseline_content_identity(baseline): return baseline return baseline.model_copy(update={"created_at": prior_baseline.created_at}) @@ -558,7 +544,7 @@ def verify_baseline( issues.extend(audit_issues) if not any(issue.kind == "invalid_audit_log" for issue in audit_issues): issues.extend(_verify_entry_provenance(baseline, log_path)) - issues.extend(_verify_entry_expiry(baseline, today or date.today())) + issues.extend(_verify_entry_expiry(baseline, today or evaluation_date())) issues.extend(_verify_deprecated_check_ids(baseline)) return issues @@ -581,9 +567,7 @@ def baseline_resolved_fingerprints( set out of that function to keep its signature backward-compatible with the existing public contract. """ - baseline_fingerprints = { - entry.fingerprint for entry in baseline.findings if entry.fingerprint - } + baseline_fingerprints = {entry.fingerprint for entry in baseline.findings if entry.fingerprint} active: set[str] = set() legacy_matched: set[str] = set() tool_ids_by_name: dict[str, set[str]] = {} @@ -599,8 +583,7 @@ def baseline_resolved_fingerprints( index, ) legacy_identity_unambiguous = ( - finding.tool_name is None - or len(tool_ids_by_name.get(finding.tool_name, set())) <= 1 + finding.tool_name is None or len(tool_ids_by_name.get(finding.tool_name, set())) <= 1 ) if not legacy_identity_unambiguous: legacy_candidates.clear() @@ -786,9 +769,7 @@ def _verify_entry_provenance( return issues -def _verify_entry_expiry( - baseline: BaselineFile, today: date -) -> list[BaselineIntegrityIssue]: +def _verify_entry_expiry(baseline: BaselineFile, today: date) -> list[BaselineIntegrityIssue]: issues: list[BaselineIntegrityIssue] = [] for entry in baseline.findings: if entry.provenance is None or entry.provenance.expires is None: diff --git a/src/agents_shipgate/core/evaluation_clock.py b/src/agents_shipgate/core/evaluation_clock.py new file mode 100644 index 00000000..4e8fdb51 --- /dev/null +++ b/src/agents_shipgate/core/evaluation_clock.py @@ -0,0 +1,28 @@ +"""Run-scoped deterministic date used by verification policy evaluation.""" + +from __future__ import annotations + +from collections.abc import Iterator +from contextlib import contextmanager +from contextvars import ContextVar +from datetime import date + +_EVALUATION_DATE: ContextVar[date | None] = ContextVar( + "agents_shipgate_evaluation_date", default=None +) + + +def evaluation_date() -> date: + return _EVALUATION_DATE.get() or date.today() + + +@contextmanager +def use_evaluation_date(value: date) -> Iterator[None]: + token = _EVALUATION_DATE.set(value) + try: + yield + finally: + _EVALUATION_DATE.reset(token) + + +__all__ = ["evaluation_date", "use_evaluation_date"] diff --git a/src/agents_shipgate/core/severity_overrides.py b/src/agents_shipgate/core/severity_overrides.py index 35e73ba8..0c945b72 100644 --- a/src/agents_shipgate/core/severity_overrides.py +++ b/src/agents_shipgate/core/severity_overrides.py @@ -38,6 +38,7 @@ from agents_shipgate.core.check_ids import expands_to_check_id from agents_shipgate.core.errors import ConfigError +from agents_shipgate.core.evaluation_clock import evaluation_date from agents_shipgate.schemas.checks import CheckMetadata from agents_shipgate.schemas.common import Severity from agents_shipgate.schemas.report import ( @@ -168,7 +169,7 @@ def resolve_severity_overrides( and audit purposes. Floor enforcement still uses the catalog floor (the static gate floor for the check class). """ - today = today or date.today() + today = today or evaluation_date() catalog_by_id = _catalog_index(catalog) known_ids = _known_check_ids(catalog) ack_by_id = _ack_by_check_id(acknowledgements) @@ -223,10 +224,7 @@ def resolve_severity_overrides( # ``test_dynamic_default_aggregator_completeness`` catches it # pre-merge; ``assert`` is unsafe because it's stripped under # ``python -O``. - if ( - target_metadata.dynamic_default - and check_id not in extra_defaults - ): + if target_metadata.dynamic_default and check_id not in extra_defaults: raise RuntimeError( f"internal: dynamic-default check {check_id!r} missing " f"from extra_known_check_defaults; the aggregator in " @@ -244,9 +242,7 @@ def resolve_severity_overrides( # call-site that aggregates action-policy declarations. catalog_default_severity = target_metadata.default_severity dynamic_default = extra_defaults.get(check_id) - if dynamic_default is not None and _is_weaker( - catalog_default_severity, dynamic_default - ): + if dynamic_default is not None and _is_weaker(catalog_default_severity, dynamic_default): default_severity = dynamic_default else: default_severity = catalog_default_severity @@ -334,8 +330,7 @@ def _enforce_ack_expiry( if not expired: return bullets = "\n".join( - f" - {ack.check_id}: expired on {ack.expires.isoformat()}" - for ack in expired + f" - {ack.check_id}: expired on {ack.expires.isoformat()}" for ack in expired ) plural = "s" if len(expired) > 1 else "" raise ConfigError( diff --git a/src/agents_shipgate/core/verification_identity.py b/src/agents_shipgate/core/verification_identity.py new file mode 100644 index 00000000..976af3b2 --- /dev/null +++ b/src/agents_shipgate/core/verification_identity.py @@ -0,0 +1,860 @@ +"""Builders and validators for the reproducible verification identity graph.""" + +from __future__ import annotations + +import hashlib +import importlib.metadata +import json +import platform +import tomllib +from pathlib import Path +from typing import Any + +import yaml +from packaging.requirements import Requirement + +from agents_shipgate import __version__ +from agents_shipgate.checks.registry import check_catalog +from agents_shipgate.inputs.protocol import REGISTRY +from agents_shipgate.schemas.verification_identity import ( + VerificationArtifactManifest, + VerificationArtifactRef, + VerificationBlob, + VerificationEngineRequirement, + VerificationExecutor, + VerificationGitSubject, + VerificationInputSet, + VerificationPlan, + VerificationReceipt, + VerificationSubject, + VerificationTask, + VerificationUnitResult, + content_id, +) + + +def sha256_bytes(value: bytes) -> str: + return f"sha256:{hashlib.sha256(value).hexdigest()}" + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return f"sha256:{digest.hexdigest()}" + + +def build_blob(*, path: Path, logical_path: str, source: str) -> VerificationBlob: + data = path.read_bytes() + return VerificationBlob( + path=logical_path, + sha256=sha256_bytes(data), + size_bytes=len(data), + source=source, # type: ignore[arg-type] + ) + + +def build_engine_requirement(*, plugins_enabled: bool | None) -> VerificationEngineRequirement: + engine_distribution_sha256 = _engine_distribution_sha256() + source_project = _source_project_metadata() + if source_project is None: + metadata = importlib.metadata.metadata("agents-shipgate") + declared_requirements = metadata.get_all("Requires-Dist") or [] + else: + declared_requirements = list(source_project.get("dependencies") or []) + if plugins_enabled is not False: + declared_requirements.extend(_plugin_dependency_requirements()) + requirements = _installed_dependency_closure(declared_requirements) + adapters = sorted(REGISTRY.clone()._adapters) # noqa: SLF001 - public contract digest. + plugin_entries = _effective_plugin_entries( + source_project=source_project, + engine_distribution_sha256=engine_distribution_sha256, + ) + if plugins_enabled is not False and any( + entry.endswith(":unresolved-distribution") for entry in plugin_entries + ): + raise ValueError( + "an enabled Agents Shipgate plugin has no resolvable distribution identity" + ) + checks = [row.model_dump(mode="json") for row in check_catalog(plugins_enabled=plugins_enabled)] + payload = { + "package": "agents-shipgate", + "version": __version__, + "python_implementation": platform.python_implementation(), + "python_version": platform.python_version(), + "platform": platform.system().lower(), + "engine_distribution_sha256": engine_distribution_sha256, + "dependency_set_sha256": content_id(requirements), + "adapter_set_sha256": content_id(adapters), + "plugin_set_sha256": content_id( + {"enabled": plugins_enabled is not False, "entries": plugin_entries} + ), + "policy_catalog_sha256": content_id(checks), + } + return VerificationEngineRequirement( + engine_requirement_id=content_id(payload), + **payload, + ) + + +def build_executor(engine: VerificationEngineRequirement) -> VerificationExecutor: + payload = { + "engine_requirement_id": engine.engine_requirement_id, + "runtime_sha256": content_id( + { + "engine_requirement_id": engine.engine_requirement_id, + "canonical_json": "sorted-keys-utf8-v1", + } + ), + } + return VerificationExecutor(executor_id=content_id(payload), **payload) + + +def validate_engine_requirement( + required: VerificationEngineRequirement, + *, + plugins_enabled: bool | None, +) -> None: + """Require the local installed engine to equal the plan descriptor.""" + + actual = build_engine_requirement(plugins_enabled=plugins_enabled) + if actual != required: + mismatches = [ + field + for field in type(required).model_fields + if getattr(actual, field) != getattr(required, field) + ] + raise ValueError( + "local verification engine does not satisfy the plan: " + ", ".join(sorted(mismatches)) + ) + + +def build_verification_plan( + *, + git_root: Path, + input_root: Path, + config_path: Path, + config_logical_path: str, + base_ref: str | None, + head_ref: str, + archived_head: bool, + repository_id: str, + base_commit_sha: str | None, + base_tree_sha: str | None, + head_commit_sha: str | None, + head_tree_sha: str | None, + merge_base_sha: str | None, + changed_files: list[str], + diff_text: str, + baseline_path: Path | None, + diff_from_path: Path | None, + policy_pack_paths: list[Path], + evaluation_date: str, + options: dict[str, Any], + plugins_enabled: bool | None, + diff_logical_path: str = "verification-input.diff", +) -> VerificationPlan: + overlay = _worktree_overlay(git_root, changed_files) if not archived_head else [] + overlay_hash = content_id(overlay) if overlay else None + git_subject = VerificationGitSubject( + repository_id=repository_id, + base_ref=base_ref, + base_commit_sha=base_commit_sha, + base_tree_sha=base_tree_sha, + head_ref=head_ref, + source_head_commit_sha=( + str(options.get("source_head_commit_sha")) + if options.get("source_head_commit_sha") + else None + ), + head_commit_sha=head_commit_sha, + head_tree_sha=head_tree_sha, + merge_base_sha=merge_base_sha, + snapshot_kind="committed_tree" if archived_head else "worktree_overlay", + worktree_overlay_sha256=overlay_hash, + ) + subject = VerificationSubject(subject_id=content_id(git_subject), git=git_subject) + + config_blob = build_blob( + path=config_path, + logical_path=config_logical_path, + source="git_blob" if archived_head else "worktree", + ) + diff_bytes = diff_text.encode("utf-8") + diff_blob = VerificationBlob( + path=diff_logical_path, + sha256=sha256_bytes(diff_bytes), + size_bytes=len(diff_bytes), + source="generated", + ) + tool_sources = _manifest_tool_source_blobs( + config_path=config_path, + input_root=input_root, + source="git_blob" if archived_head else "worktree", + ) + policy_blobs = _blobs( + policy_pack_paths, + root=input_root, + source="git_blob" if archived_head else "external_input", + ) + changed_blobs = _existing_changed_blobs( + changed_files, + root=input_root if archived_head else git_root, + source="git_blob" if archived_head else "worktree", + ) + baseline_blob = _optional_blob(baseline_path, root=input_root) + diff_from_blob = _optional_blob(diff_from_path, root=input_root) + inputs_payload = { + "evaluation_date": evaluation_date, + "config": config_blob, + "diff": diff_blob, + "baseline": baseline_blob, + "diff_from": diff_from_blob, + "policy_packs": policy_blobs, + "tool_sources": tool_sources, + "changed_paths": sorted(set(changed_files)), + "changed_files": changed_blobs, + "options": options, + } + inputs = VerificationInputSet( + input_set_id=content_id( + { + key: ( + value.model_dump(mode="json") + if hasattr(value, "model_dump") + else [item.model_dump(mode="json") for item in value] + if isinstance(value, list) and value and hasattr(value[0], "model_dump") + else value + ) + for key, value in inputs_payload.items() + } + ), + **inputs_payload, + ) + engine = build_engine_requirement(plugins_enabled=plugins_enabled) + task_payload = { + "kind": "evaluate", + "shard": 0, + "shard_count": 1, + "input_paths": sorted( + { + config_blob.path, + diff_blob.path, + *(blob.path for blob in tool_sources), + *(b.path for b in policy_blobs), + } + ), + } + task = VerificationTask(task_id=content_id(task_payload), **task_payload) + request_payload = { + "subject_id": subject.subject_id, + "input_set_id": inputs.input_set_id, + "engine_requirement_id": engine.engine_requirement_id, + "task_ids": [task.task_id], + } + return VerificationPlan( + request_id=content_id(request_payload), + subject=subject, + inputs=inputs, + engine=engine, + tasks=[task], + ) + + +def build_unit_result( + *, + plan: VerificationPlan, + normalized_ir: dict[str, Any], + status: str = "succeeded", + issues: list[str] | None = None, +) -> VerificationUnitResult: + executor = build_executor(plan.engine) + payload = { + "request_id": plan.request_id, + "task_id": plan.tasks[0].task_id, + "executor": executor, + "status": status, + "normalized_ir": normalized_ir, + "issues": list(issues or []), + } + return VerificationUnitResult( + unit_result_id=content_id( + { + key: value.model_dump(mode="json") if hasattr(value, "model_dump") else value + for key, value in payload.items() + } + ), + **payload, + ) + + +def build_terminal_receipt( + *, + plan: VerificationPlan, + unit_results: list[VerificationUnitResult], + decision: str | None, + merge_verdict: str, + can_merge_without_human: bool, + artifact_paths: dict[str, Path], + artifact_root: Path | None = None, + attempt_id: str | None = None, +) -> tuple[VerificationArtifactManifest, VerificationReceipt]: + executor_ids = {result.executor.executor_id for result in unit_results} + if len(executor_ids) != 1: + raise ValueError("all unit results must bind one exact executor") + for result in unit_results: + if result.request_id != plan.request_id: + raise ValueError("unit result request_id does not match plan") + if result.task_id not in {task.task_id for task in plan.tasks}: + raise ValueError("unit result task is not present in the plan") + if result.executor.engine_requirement_id != plan.engine.engine_requirement_id: + raise ValueError("unit result executor does not satisfy the plan engine") + if result.status != "succeeded": + raise ValueError("failed unit results cannot produce a terminal receipt") + if {result.task_id for result in unit_results} != {task.task_id for task in plan.tasks}: + raise ValueError("terminal receipt requires exactly one result for every task") + if len(unit_results) != len(plan.tasks): + raise ValueError("terminal receipt requires exactly one result for every task") + + decision_payload = { + "request_id": plan.request_id, + "unit_result_ids": sorted(result.unit_result_id for result in unit_results), + "decision": decision, + "merge_verdict": merge_verdict, + "can_merge_without_human": can_merge_without_human, + } + decision_id = content_id(decision_payload) + existing_paths = { + name: path.resolve() for name, path in sorted(artifact_paths.items()) if path.is_file() + } + if not existing_paths: + raise ValueError("terminal receipt requires at least one artifact") + resolved_root = ( + artifact_root.resolve() + if artifact_root is not None + else next(iter(existing_paths.values())).parent + ) + refs: dict[str, VerificationArtifactRef] = {} + for name, path in existing_paths.items(): + try: + logical_path = path.relative_to(resolved_root).as_posix() + except ValueError as exc: + raise ValueError(f"receipt artifact {name!r} escapes the artifact root") from exc + refs[name] = VerificationArtifactRef( + path=logical_path, + sha256=sha256_file(path), + size_bytes=path.stat().st_size, + media_type=_media_type(path), + ) + manifest_payload = { + "request_id": plan.request_id, + "decision_id": decision_id, + "artifacts": refs, + } + manifest = VerificationArtifactManifest( + artifact_set_id=content_id( + { + key: ( + {name: ref.model_dump(mode="json") for name, ref in value.items()} + if key == "artifacts" + else value + ) + for key, value in manifest_payload.items() + } + ), + **manifest_payload, + ) + receipt_payload = { + "request_id": plan.request_id, + "subject_id": plan.subject.subject_id, + "input_set_id": plan.inputs.input_set_id, + "engine_requirement_id": plan.engine.engine_requirement_id, + "executor_id": next(iter(executor_ids)), + "decision_id": decision_id, + "artifact_set_id": manifest.artifact_set_id, + "unit_result_ids": sorted(result.unit_result_id for result in unit_results), + "attempt_id": attempt_id, + "decision": decision, + "merge_verdict": merge_verdict, + "can_merge_without_human": can_merge_without_human, + "artifact_manifest": manifest, + } + return manifest, VerificationReceipt( + receipt_id=content_id( + { + key: value.model_dump(mode="json") if hasattr(value, "model_dump") else value + for key, value in receipt_payload.items() + if key != "attempt_id" + } + ), + **receipt_payload, + ) + + +def validate_receipt_artifacts(receipt: VerificationReceipt, *, root: Path) -> None: + resolved: dict[str, Path] = {} + for name, ref in receipt.artifact_manifest.artifacts.items(): + path = Path(ref.path) + candidate = path if path.is_absolute() else root / path + if not candidate.is_file(): + raise ValueError(f"receipt artifact {name!r} is missing: {ref.path}") + if sha256_file(candidate) != ref.sha256: + raise ValueError(f"receipt artifact {name!r} hash does not match") + if candidate.stat().st_size != ref.size_bytes: + raise ValueError(f"receipt artifact {name!r} size does not match") + resolved[name] = candidate + + manifest_path = root / "verification-artifacts.json" + if not manifest_path.is_file(): + raise ValueError("terminal artifact manifest is missing") + external_manifest = VerificationArtifactManifest.model_validate( + _load_json_object(manifest_path) + ) + if external_manifest != receipt.artifact_manifest: + raise ValueError("terminal artifact manifest disagrees with the receipt") + + plan_path = resolved.get("verification_plan_json") + if plan_path is None: + raise ValueError("receipt does not bind a verification plan") + plan = VerificationPlan.model_validate(_load_json_object(plan_path)) + diff_path = resolved.get("verification_input_diff") + if diff_path is None: + raise ValueError("receipt does not bind the verification diff input") + if sha256_file(diff_path) != plan.inputs.diff.sha256: + raise ValueError("receipt diff artifact disagrees with the verification plan") + if diff_path.stat().st_size != plan.inputs.diff.size_bytes: + raise ValueError("receipt diff artifact size disagrees with the verification plan") + if plan.inputs.diff_from is not None: + base_report_path = resolved.get("verification_base_report_json") + if base_report_path is None: + raise ValueError("receipt does not bind the verification base report input") + if sha256_file(base_report_path) != plan.inputs.diff_from.sha256: + raise ValueError("receipt base report disagrees with the verification plan") + if base_report_path.stat().st_size != plan.inputs.diff_from.size_bytes: + raise ValueError("receipt base report size disagrees with the verification plan") + mirrors = { + "request_id": plan.request_id, + "subject_id": plan.subject.subject_id, + "input_set_id": plan.inputs.input_set_id, + "engine_requirement_id": plan.engine.engine_requirement_id, + } + for field, expected in mirrors.items(): + if getattr(receipt, field) != expected: + raise ValueError(f"receipt {field} disagrees with the verification plan") + + unit_paths = [ + path + for name, path in sorted(resolved.items()) + if name.startswith("verification_unit_result") + ] + if not unit_paths: + raise ValueError("receipt does not bind any verification unit results") + units = [VerificationUnitResult.model_validate(_load_json_object(path)) for path in unit_paths] + if sorted(unit.unit_result_id for unit in units) != sorted(receipt.unit_result_ids): + raise ValueError("receipt unit_result_ids disagree with the bound unit artifacts") + if any(unit.request_id != receipt.request_id for unit in units): + raise ValueError("receipt contains a unit result for another request") + if any(unit.status != "succeeded" for unit in units): + raise ValueError("terminal receipt contains a failed unit result") + if {unit.executor.executor_id for unit in units} != {receipt.executor_id}: + raise ValueError("receipt executor_id disagrees with its unit results") + + verify_run_path = resolved.get("verify_run_json") + if verify_run_path is None: + raise ValueError("receipt does not bind verify-run.json") + from agents_shipgate.schemas.verify_run import VerifyRunArtifact + + verify_run = VerifyRunArtifact.model_validate(_load_json_object(verify_run_path)) + if verify_run.request_id != receipt.request_id: + raise ValueError("verify-run request_id disagrees with the receipt") + if verify_run.decision_id != receipt.decision_id: + raise ValueError("verify-run decision_id disagrees with the receipt") + if verify_run.executor.executor_id != receipt.executor_id: + raise ValueError("verify-run executor_id disagrees with the receipt") + if sorted(verify_run.unit_result_ids) != sorted(receipt.unit_result_ids): + raise ValueError("verify-run unit_result_ids disagree with the receipt") + outcome = verify_run.outcome + if ( + outcome.decision, + outcome.merge_verdict, + outcome.can_merge_without_human, + ) != ( + receipt.decision, + receipt.merge_verdict, + receipt.can_merge_without_human, + ): + raise ValueError("verify-run outcome disagrees with the receipt") + + _validate_projection_ids(receipt, resolved) + + +def _load_json_object(path: Path) -> dict[str, Any]: + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise ValueError(f"could not parse receipt artifact {path.name}: {exc}") from exc + if not isinstance(payload, dict): + raise ValueError(f"receipt artifact {path.name} must contain one JSON object") + return payload + + +def _validate_projection_ids( + receipt: VerificationReceipt, + resolved: dict[str, Path], +) -> None: + expected = { + "request_id": receipt.request_id, + "subject_id": receipt.subject_id, + "input_set_id": receipt.input_set_id, + "engine_requirement_id": receipt.engine_requirement_id, + "decision_id": receipt.decision_id, + } + for name in ("report_json", "packet_json", "verifier_json"): + path = resolved.get(name) + if path is None: + continue + payload = _load_json_object(path) + for field, value in expected.items(): + if payload.get(field) != value: + raise ValueError(f"receipt artifact {name!r} has inconsistent {field}") + handoff_path = resolved.get("agent_handoff_json") + if handoff_path is not None: + handoff = _load_json_object(handoff_path) + reproducibility = handoff.get("reproducibility") + if not isinstance(reproducibility, dict): + raise ValueError("agent handoff lacks reproducibility identity") + handoff_expected = {**expected, "executor_id": receipt.executor_id} + for field, value in handoff_expected.items(): + if reproducibility.get(field) != value: + raise ValueError(f"agent handoff has inconsistent {field}") + + +def validate_plan_inputs( + plan: VerificationPlan, + *, + root: Path, + diff_path: Path | None = None, + bundle_root: Path | None = None, +) -> None: + """Fail closed unless every portable plan blob matches the supplied root.""" + + blobs = [ + plan.inputs.config, + *plan.inputs.tool_sources, + *plan.inputs.policy_packs, + *plan.inputs.changed_files, + ] + if plan.inputs.baseline is not None: + blobs.append(plan.inputs.baseline) + if plan.inputs.diff_from is not None: + blobs.append(plan.inputs.diff_from) + resolved_root = root.resolve() + resolved_bundle_root = (bundle_root or resolved_root).resolve() + for blob in blobs: + candidate_root = ( + resolved_bundle_root + if blob.source in {"external_input", "generated"} + else resolved_root + ) + candidate = (candidate_root / blob.path).resolve() + if candidate != candidate_root and candidate_root not in candidate.parents: + raise ValueError(f"plan input escapes supplied root: {blob.path}") + if not candidate.is_file(): + raise ValueError(f"plan input is missing: {blob.path}") + if sha256_file(candidate) != blob.sha256: + raise ValueError(f"plan input hash does not match: {blob.path}") + if candidate.stat().st_size != blob.size_bytes: + raise ValueError(f"plan input size does not match: {blob.path}") + resolved_diff = (diff_path or (resolved_root / plan.inputs.diff.path)).resolve() + if not resolved_diff.is_file(): + raise ValueError(f"plan diff input is missing: {plan.inputs.diff.path}") + if sha256_file(resolved_diff) != plan.inputs.diff.sha256: + raise ValueError("plan diff input hash does not match") + if resolved_diff.stat().st_size != plan.inputs.diff.size_bytes: + raise ValueError("plan diff input size does not match") + + +def _manifest_tool_source_blobs( + *, config_path: Path, input_root: Path, source: str +) -> list[VerificationBlob]: + raw = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {} + rows = raw.get("tool_sources") if isinstance(raw, dict) else [] + paths: list[Path] = [] + for row in rows or []: + if not isinstance(row, dict) or not isinstance(row.get("path"), str): + continue + candidate = (config_path.parent / row["path"]).resolve() + if input_root.resolve() not in candidate.parents and candidate != input_root.resolve(): + raise ValueError(f"tool source escapes verification input root: {row['path']}") + paths.extend(_expand_path(candidate)) + return _blobs(paths, root=input_root, source=source) + + +def _expand_path(path: Path) -> list[Path]: + if path.is_file(): + return [path] + if path.is_dir(): + return sorted(candidate for candidate in path.rglob("*") if candidate.is_file()) + return [] + + +def _blobs(paths: list[Path], *, root: Path, source: str) -> list[VerificationBlob]: + out: list[VerificationBlob] = [] + for path in sorted({candidate.resolve() for candidate in paths if candidate.is_file()}): + logical = path.relative_to(root.resolve()).as_posix() + out.append(build_blob(path=path, logical_path=logical, source=source)) + return sorted(out, key=lambda blob: blob.path) + + +def _optional_blob(path: Path | None, *, root: Path) -> VerificationBlob | None: + if path is None or not path.is_file(): + return None + resolved = path.resolve() + try: + logical = resolved.relative_to(root.resolve()).as_posix() + except ValueError: + logical = path.name + return build_blob(path=resolved, logical_path=logical, source="external_input") + + +def _existing_changed_blobs(paths: list[str], *, root: Path, source: str) -> list[VerificationBlob]: + candidates = [(root / path).resolve() for path in paths] + return _blobs(candidates, root=root, source=source) + + +def _worktree_overlay(root: Path, paths: list[str]) -> list[dict[str, Any]]: + rows: list[dict[str, Any]] = [] + for relative in sorted(set(paths)): + candidate = (root / relative).resolve() + if root.resolve() not in candidate.parents: + raise ValueError(f"worktree path escapes repository: {relative}") + rows.append( + { + "path": relative, + "status": "present" if candidate.is_file() else "deleted", + "sha256": sha256_file(candidate) if candidate.is_file() else None, + } + ) + return rows + + +def _media_type(path: Path) -> str: + if path.suffix == ".json": + return "application/json" + if path.suffix in {".md", ".txt"}: + return "text/plain; charset=utf-8" + if path.suffix == ".sarif": + return "application/sarif+json" + return "application/octet-stream" + + +def _installed_dependency_closure(requirements: list[str]) -> list[str]: + pending = list(requirements) + seen: set[str] = set() + resolved: list[str] = [] + while pending: + raw = pending.pop() + try: + requirement = Requirement(raw) + except Exception: # noqa: BLE001 - malformed installed metadata fails visibly below. + resolved.append(f"invalid:{raw}") + continue + if requirement.marker is not None and not requirement.marker.evaluate(): + continue + name = _normalized_distribution_name(requirement.name) + if name in seen: + continue + seen.add(name) + try: + distribution = importlib.metadata.distribution(requirement.name) + except importlib.metadata.PackageNotFoundError: + # Optional/extras requirements not installed are still identity + # relevant because their absence affects available adapters. + resolved.append(f"{name}=absent") + continue + resolved.append( + f"{name}=={distribution.version}@{_distribution_record_sha256(distribution)}" + ) + pending.extend(distribution.metadata.get_all("Requires-Dist") or []) + return sorted(resolved) + + +def _engine_distribution_sha256() -> str: + package_root = Path(__file__).resolve().parents[1] + files: dict[str, str] = {} + _add_distribution_tree(files, package_root, logical_root="") + if not (package_root / "_meta").is_dir(): + # A source checkout does not contain Hatch's force-included wheel + # directories. Project the exact force-include mapping into the same + # logical package paths so a checkout and its built wheel have one + # engine-content identity. + repository_root = package_root.parents[1] + mappings = ( + ("samples", "_fixtures"), + ("AGENTS.md", "_meta/AGENTS.md"), + ("STABILITY.md", "_meta/STABILITY.md"), + ("docs/triggers.json", "_meta/triggers.json"), + (".claude/commands/shipgate.md", "_meta/claude-command/shipgate.md"), + ("docs/checks", "_meta/checks"), + ( + "policies/codex-boundary.shipgate.yaml", + "_meta/policies/codex-boundary.shipgate.yaml", + ), + ( + "policies/mcp-permissions.shipgate.yaml", + "_meta/policies/mcp-permissions.shipgate.yaml", + ), + ( + "policies/host-boundary.shipgate.yaml", + "_meta/policies/host-boundary.shipgate.yaml", + ), + ( + "policies/templates/agent-boundary-default.shipgate.yaml", + "_meta/policies/agent-boundary.shipgate.yaml", + ), + ("policies/templates", "_meta/policies/templates"), + ("adoption-kits", "_adoption_kits"), + ) + for source, logical in mappings: + candidate = repository_root / source + if not candidate.exists(): + raise ValueError(f"source distribution input is missing: {source}") + _add_distribution_tree(files, candidate, logical_root=logical) + return content_id([{"path": path, "sha256": digest} for path, digest in sorted(files.items())]) + + +def _add_distribution_tree( + files: dict[str, str], + source: Path, + *, + logical_root: str, +) -> None: + candidates = [source] if source.is_file() else sorted(source.rglob("*")) + for path in candidates: + if not path.is_file() or "__pycache__" in path.parts or path.suffix in {".pyc", ".pyo"}: + continue + relative = "" if source.is_file() else path.relative_to(source).as_posix() + logical = "/".join(part for part in (logical_root, relative) if part) + digest = sha256_file(path) + existing = files.get(logical) + if existing is not None and existing != digest: + raise ValueError(f"distribution inputs conflict at logical path: {logical}") + files[logical] = digest + + +def _distribution_record_sha256( + distribution: importlib.metadata.Distribution, +) -> str: + record = distribution.read_text("RECORD") + if record is not None: + return sha256_bytes(record.encode("utf-8")) + metadata = distribution.read_text("METADATA") or "" + return sha256_bytes(metadata.encode("utf-8")) + + +def _source_project_metadata() -> dict[str, Any] | None: + package_root = Path(__file__).resolve().parents[1] + if (package_root / "_meta").is_dir(): + return None + pyproject = package_root.parents[1] / "pyproject.toml" + if not pyproject.is_file(): + return None + payload = tomllib.loads(pyproject.read_text(encoding="utf-8")) + project = payload.get("project") + return project if isinstance(project, dict) else None + + +def _effective_plugin_entries( + *, + source_project: dict[str, Any] | None, + engine_distribution_sha256: str, +) -> list[str]: + entries: list[str] = [] + for entry in importlib.metadata.entry_points(): + if not entry.group.startswith("agents_shipgate"): + continue + distribution = getattr(entry, "dist", None) + distribution_name = str( + distribution.metadata.get("Name") if distribution is not None else "" + ) + if source_project is not None and _normalized_distribution_name(distribution_name) == ( + "agents-shipgate" + ): + continue + entries.append( + f"{entry.group}:{entry.name}:{entry.value}:" + f"{_entry_point_distribution(entry, engine_distribution_sha256)}" + ) + if source_project is not None: + for group, rows in (source_project.get("entry-points") or {}).items(): + if not str(group).startswith("agents_shipgate") or not isinstance(rows, dict): + continue + for name, value in rows.items(): + entries.append( + f"{group}:{name}:{value}:agents-shipgate=={__version__}@" + f"{engine_distribution_sha256}" + ) + return sorted(set(entries)) + + +def _plugin_dependency_requirements() -> list[str]: + requirements: list[str] = [] + seen: set[str] = set() + for entry in importlib.metadata.entry_points(): + if not entry.group.startswith("agents_shipgate"): + continue + distribution = getattr(entry, "dist", None) + if distribution is None: + continue + name = _normalized_distribution_name(str(distribution.metadata.get("Name") or "unknown")) + if name == "agents-shipgate" or name in seen: + continue + seen.add(name) + requirements.extend(distribution.metadata.get_all("Requires-Dist") or []) + return requirements + + +def _entry_point_distribution( + entry: importlib.metadata.EntryPoint, + engine_distribution_sha256: str, +) -> str: + distribution = getattr(entry, "dist", None) + distributions = [distribution] if distribution is not None else [] + if not distributions: + top_level = entry.value.split(":", 1)[0].split(".", 1)[0] + for name in importlib.metadata.packages_distributions().get(top_level, []): + try: + distributions.append(importlib.metadata.distribution(name)) + except importlib.metadata.PackageNotFoundError: + continue + if not distributions: + return "unresolved-distribution" + rows = [] + for item in distributions: + name = _normalized_distribution_name(str(item.metadata.get("Name") or "unknown")) + version = str(item.version or "unknown") + digest = ( + engine_distribution_sha256 + if name == "agents-shipgate" + else _distribution_record_sha256(item) + ) + rows.append(f"{name}=={version}@{digest}") + return ",".join(sorted(set(rows))) + + +def _normalized_distribution_name(value: str) -> str: + return value.lower().replace("_", "-").replace(".", "-") + + +__all__ = [ + "build_engine_requirement", + "build_executor", + "build_terminal_receipt", + "build_unit_result", + "build_verification_plan", + "sha256_file", + "validate_receipt_artifacts", + "validate_engine_requirement", + "validate_plan_inputs", +] diff --git a/src/agents_shipgate/packet/json_packet.py b/src/agents_shipgate/packet/json_packet.py index 377c2f76..948ca791 100644 --- a/src/agents_shipgate/packet/json_packet.py +++ b/src/agents_shipgate/packet/json_packet.py @@ -84,13 +84,14 @@ def load_packet_json(payload: dict[str, Any] | str | bytes) -> EvidencePacket: version = payload_dict.get("packet_schema_version") legacy_version = ( version - if version in {"0.1", "0.2", "0.3", "0.4", "0.5", "0.6", "0.7", "0.8", "0.9", "0.10"} + if version + in {"0.1", "0.2", "0.3", "0.4", "0.5", "0.6", "0.7", "0.8", "0.9", "0.10", "0.11"} else None ) if version == "0.1": payload_dict = { **payload_dict, - "packet_schema_version": "0.11", + "packet_schema_version": "0.12", "tool_surface_diff": { "status": "not_declared", "enabled": False, @@ -105,41 +106,43 @@ def load_packet_json(payload: dict[str, Any] | str | bytes) -> EvidencePacket: _upgrade_evidence_matrix_v06(payload_dict) _upgrade_hitl_v07(payload_dict) elif version == "0.2": - payload_dict = {**payload_dict, "packet_schema_version": "0.11"} + payload_dict = {**payload_dict, "packet_schema_version": "0.12"} _upgrade_hitl_v03(payload_dict) _upgrade_action_surface_v05(payload_dict) _upgrade_evidence_matrix_v06(payload_dict) _upgrade_hitl_v07(payload_dict) elif version == "0.3": - payload_dict = {**payload_dict, "packet_schema_version": "0.11"} + payload_dict = {**payload_dict, "packet_schema_version": "0.12"} _upgrade_action_surface_v05(payload_dict) _upgrade_evidence_matrix_v06(payload_dict) _upgrade_hitl_v07(payload_dict) elif version == "0.4": - payload_dict = {**payload_dict, "packet_schema_version": "0.11"} + payload_dict = {**payload_dict, "packet_schema_version": "0.12"} _upgrade_action_surface_v05(payload_dict) _upgrade_evidence_matrix_v06(payload_dict) _upgrade_hitl_v07(payload_dict) elif version == "0.5": - payload_dict = {**payload_dict, "packet_schema_version": "0.11"} + payload_dict = {**payload_dict, "packet_schema_version": "0.12"} _upgrade_evidence_matrix_v06(payload_dict) _upgrade_hitl_v07(payload_dict) elif version == "0.6": - payload_dict = {**payload_dict, "packet_schema_version": "0.11"} + payload_dict = {**payload_dict, "packet_schema_version": "0.12"} _upgrade_hitl_v07(payload_dict) elif version == "0.7": - payload_dict = {**payload_dict, "packet_schema_version": "0.11"} + payload_dict = {**payload_dict, "packet_schema_version": "0.12"} elif version == "0.8": - payload_dict = {**payload_dict, "packet_schema_version": "0.11"} + payload_dict = {**payload_dict, "packet_schema_version": "0.12"} elif version == "0.9": - payload_dict = {**payload_dict, "packet_schema_version": "0.11"} + payload_dict = {**payload_dict, "packet_schema_version": "0.12"} elif version == "0.10": - payload_dict = {**payload_dict, "packet_schema_version": "0.11"} - elif version != "0.11": + payload_dict = {**payload_dict, "packet_schema_version": "0.12"} + elif version == "0.11": + payload_dict = {**payload_dict, "packet_schema_version": "0.12"} + elif version != "0.12": raise PacketSchemaError( "unsupported packet_schema_version: " f"{version!r}; expected '0.1', '0.2', '0.3', '0.4', '0.5', " - "'0.6', '0.7', '0.8', '0.9', '0.10', or '0.11'" + "'0.6', '0.7', '0.8', '0.9', '0.10', '0.11', or '0.12'" ) if legacy_version is not None: diff --git a/src/agents_shipgate/schemas/agent_handoff.py b/src/agents_shipgate/schemas/agent_handoff.py index 34e2d9dc..2a27527b 100644 --- a/src/agents_shipgate/schemas/agent_handoff.py +++ b/src/agents_shipgate/schemas/agent_handoff.py @@ -7,10 +7,11 @@ from agents_shipgate import __version__ from agents_shipgate.schemas.agent_control import AgentControl from agents_shipgate.schemas.disclaimers import STATIC_VERDICT_DISCLAIMER +from agents_shipgate.schemas.verification_identity import CONTENT_ID_PATTERN from agents_shipgate.schemas.verifier import Applicability, MergeVerdict, map_merge_verdict -AGENT_HANDOFF_SCHEMA_VERSION = "shipgate.agent_handoff/v4" -AGENT_HANDOFF_SCHEMA_PATH = "docs/agent-handoff-schema.v4.json" +AGENT_HANDOFF_SCHEMA_VERSION = "shipgate.agent_handoff/v5" +AGENT_HANDOFF_SCHEMA_PATH = "docs/agent-handoff-schema.v5.json" AgentHandoffOperation = Literal["verify_pr", "verify_local", "verify_preview"] RemediationPlanSafety = Literal["allowed", "forbidden", "patch"] @@ -99,6 +100,12 @@ class AgentHandoffReproducibility(BaseModel): model_config = ConfigDict(extra="forbid") run_id: str | None = None + request_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + subject_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + input_set_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + engine_requirement_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + executor_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + decision_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) config_sha256: str | None = None baseline_sha256: str | None = None policy_packs: list[dict[str, Any]] = Field(default_factory=list) @@ -205,7 +212,7 @@ class AgentHandoffArtifact(BaseModel): }, ) - schema_version: Literal["shipgate.agent_handoff/v4"] = AGENT_HANDOFF_SCHEMA_VERSION + schema_version: Literal["shipgate.agent_handoff/v5"] = AGENT_HANDOFF_SCHEMA_VERSION contract_version: str tool: AgentHandoffTool = Field(default_factory=AgentHandoffTool) operation: AgentHandoffOperation diff --git a/src/agents_shipgate/schemas/attestation.py b/src/agents_shipgate/schemas/attestation.py index dda11a7a..ce7bc563 100644 --- a/src/agents_shipgate/schemas/attestation.py +++ b/src/agents_shipgate/schemas/attestation.py @@ -2,9 +2,11 @@ from typing import Literal -from pydantic import BaseModel, ConfigDict, Field, RootModel +from pydantic import BaseModel, ConfigDict, Field, RootModel, model_validator -ATTESTATION_SCHEMA_VERSION = "0.4" +from agents_shipgate.schemas.verification_identity import CONTENT_ID_PATTERN + +ATTESTATION_SCHEMA_VERSION = "0.5" class AttestationVerdictV1(BaseModel): @@ -88,12 +90,17 @@ class ReleaseAttestationV1(BaseModel): model_config = ConfigDict(extra="forbid") - attestation_schema_version: Literal["0.4"] = ATTESTATION_SCHEMA_VERSION + attestation_schema_version: Literal["0.5"] = ATTESTATION_SCHEMA_VERSION cli_version: str org: AttestationOrgContextV1 = Field(default_factory=AttestationOrgContextV1) source_verifier: str redacted: bool = True run_id: str | None = None + request_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + receipt_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + decision_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + artifact_set_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + verification_receipt_sha256: str | None = Field(default=None, pattern=r"^[0-9a-f]{64}$") verify_run_sha256: str | None = None event_time: str | None = None source_url: str | None = None @@ -114,6 +121,21 @@ class ReleaseAttestationV1(BaseModel): policy_packs: list[dict[str, str | int | None]] = Field(default_factory=list) artifact_sha256: dict[str, str] = Field(default_factory=dict) + @model_validator(mode="after") + def _receipt_identity_is_all_or_none(self) -> ReleaseAttestationV1: + fields = ( + self.request_id, + self.receipt_id, + self.decision_id, + self.artifact_set_id, + self.verification_receipt_sha256, + ) + if any(value is not None for value in fields) and not all( + value is not None for value in fields + ): + raise ValueError("verification receipt identity must be complete when present") + return self + class ReleaseAttestationArtifactV1(RootModel[ReleaseAttestationV1]): root: ReleaseAttestationV1 diff --git a/src/agents_shipgate/schemas/contract.py b/src/agents_shipgate/schemas/contract.py index e342ed15..7619961c 100644 --- a/src/agents_shipgate/schemas/contract.py +++ b/src/agents_shipgate/schemas/contract.py @@ -37,19 +37,25 @@ from agents_shipgate.schemas.preflight import PREFLIGHT_SCHEMA_VERSION from agents_shipgate.schemas.registry import REGISTRY_SCHEMA_VERSION from agents_shipgate.schemas.report import ReadinessReport +from agents_shipgate.schemas.verification_identity import ( + VERIFICATION_ARTIFACT_MANIFEST_SCHEMA_VERSION, + VERIFICATION_PLAN_SCHEMA_VERSION, + VERIFICATION_RECEIPT_SCHEMA_VERSION, + VERIFICATION_UNIT_RESULT_SCHEMA_VERSION, +) from agents_shipgate.schemas.verifier import VerifierArtifact from agents_shipgate.schemas.verify_run import VERIFY_RUN_SCHEMA_VERSION -CONTRACT_VERSION: Literal["16"] = "16" +CONTRACT_VERSION: Literal["17"] = "17" MINIMUM_CONTROL_CONTRACT_VERSION: Literal["14"] = "14" GATING_SIGNAL: Literal["release_decision.decision"] = "release_decision.decision" AGENT_RESULT_SCHEMA_VERSION: Literal["agent_result_v2"] = "agent_result_v2" AGENT_RESULT_SCHEMA_PATH: Literal["docs/agent-result-schema.v2.json"] = ( "docs/agent-result-schema.v2.json" ) -AGENT_BOUNDARY_RESULT_SCHEMA_PATH: Literal[ +AGENT_BOUNDARY_RESULT_SCHEMA_PATH: Literal["docs/agent-boundary-result-schema.v1.json"] = ( "docs/agent-boundary-result-schema.v1.json" -] = "docs/agent-boundary-result-schema.v1.json" +) TRIGGER_CATALOG_SCHEMA_VERSION: Literal["0.2"] = "0.2" AGENT_RESULT_CONTROL_FIELDS: tuple[str, ...] = ( "decision", @@ -83,6 +89,10 @@ "registry", "org_governance", "org_evidence_bundle", + "verification_plan", + "verification_unit_result", + "verification_artifact_manifest", + "verification_receipt", "host_grants_inventory", "host_grants_baseline", "host_grants_drift", @@ -186,6 +196,29 @@ "agents-shipgate verify --workspace . --config shipgate.yaml " "--base origin/main --head HEAD --ci-mode advisory --json" ), + "verification_prepare": ( + "agents-shipgate verification prepare --workspace . --config shipgate.yaml " + "--base origin/main --head HEAD --out " + "agents-shipgate-reports/verification-plan.json" + ), + "verification_worker": ( + "agents-shipgate verification worker --plan " + "agents-shipgate-reports/verification-plan.json --workspace . --out " + "agents-shipgate-reports/verification-unit-result.json" + ), + "verification_assemble": ( + "agents-shipgate verification assemble --plan " + "agents-shipgate-reports/verification-plan.json --unit-result " + "agents-shipgate-reports/verification-unit-result.json --verifier " + "agents-shipgate-reports/verifier.json --artifacts-root " + "agents-shipgate-reports --out " + "agents-shipgate-reports/verification-receipt.json" + ), + "verification_reproduce": ( + "agents-shipgate verification reproduce --receipt " + "agents-shipgate-reports/verification-receipt.json --artifacts-root " + "agents-shipgate-reports" + ), "agent_handoff": ( "agents-shipgate agent handoff --from agents-shipgate-reports/verifier.json --json" ), @@ -220,6 +253,10 @@ "verifier": "agents-shipgate-reports/verifier.json", "verify_run": "agents-shipgate-reports/verify-run.json", "agent_handoff": "agents-shipgate-reports/agent-handoff.json", + "verification_plan": "agents-shipgate-reports/verification-plan.json", + "verification_unit_result": "agents-shipgate-reports/verification-unit-result.json", + "verification_artifact_manifest": "agents-shipgate-reports/verification-artifacts.json", + "verification_receipt": "agents-shipgate-reports/verification-receipt.json", "report": "agents-shipgate-reports/report.json", "pr_comment": "agents-shipgate-reports/pr-comment.md", "packet": "agents-shipgate-reports/packet.json", @@ -230,6 +267,9 @@ "registry": ".agents-shipgate/registry.jsonl", } AGENT_READ_ORDER: tuple[str, ...] = ( + "verification-receipt.json", + "verification-receipt.json.request_id", + "verification-receipt.json.receipt_id", "agent-handoff.json", "agent-handoff.json.control.state", "verifier.json.control.state", @@ -246,6 +286,8 @@ "fix_task", "capability_review.top_changes", "release_decision.decision", + "request_id", + "decision_id", ) MERGE_VERDICTS: tuple[str, ...] = ( "mergeable", @@ -294,6 +336,10 @@ class ContractPayload(BaseModel): packet_schema_version: str verifier_schema_version: str verify_run_schema_version: str + verification_plan_schema_version: str + verification_unit_result_schema_version: str + verification_artifact_manifest_schema_version: str + verification_receipt_schema_version: str agent_handoff_schema_version: str agent_handoff_schema_path: str agent_handoff_artifact: str @@ -350,6 +396,12 @@ def build_contract_payload() -> ContractPayload: packet_schema_version=str(packet_schema_version), verifier_schema_version=str(verifier_schema_version), verify_run_schema_version=VERIFY_RUN_SCHEMA_VERSION, + verification_plan_schema_version=VERIFICATION_PLAN_SCHEMA_VERSION, + verification_unit_result_schema_version=VERIFICATION_UNIT_RESULT_SCHEMA_VERSION, + verification_artifact_manifest_schema_version=( + VERIFICATION_ARTIFACT_MANIFEST_SCHEMA_VERSION + ), + verification_receipt_schema_version=VERIFICATION_RECEIPT_SCHEMA_VERSION, agent_handoff_schema_version=AGENT_HANDOFF_SCHEMA_VERSION, agent_handoff_schema_path=AGENT_HANDOFF_SCHEMA_PATH, agent_handoff_artifact=ARTIFACTS["agent_handoff"], @@ -371,8 +423,7 @@ def build_contract_payload() -> ContractPayload: trigger_catalog_schema_version=TRIGGER_CATALOG_SCHEMA_VERSION, deprecated_surfaces={ "codex-boundary-json": ( - "Deprecated compatibility projection through 0.16.x; use " - "agent-boundary-json." + "Deprecated compatibility projection through 0.16.x; use agent-boundary-json." ) }, external_integration_surfaces=list(EXTERNAL_INTEGRATION_SURFACES), @@ -434,6 +485,10 @@ def build_contract_payload() -> ContractPayload: "SUPPORTED_INPUTS", "TRIGGER_CATALOG_SCHEMA_VERSION", "VERIFY_RUN_SCHEMA_VERSION", + "VERIFICATION_ARTIFACT_MANIFEST_SCHEMA_VERSION", + "VERIFICATION_PLAN_SCHEMA_VERSION", + "VERIFICATION_RECEIPT_SCHEMA_VERSION", + "VERIFICATION_UNIT_RESULT_SCHEMA_VERSION", "VERIFIER_READ_ORDER", "ContractPayload", "build_contract_payload", diff --git a/src/agents_shipgate/schemas/org_evidence_bundle.py b/src/agents_shipgate/schemas/org_evidence_bundle.py index f063b2f9..758f1062 100644 --- a/src/agents_shipgate/schemas/org_evidence_bundle.py +++ b/src/agents_shipgate/schemas/org_evidence_bundle.py @@ -4,7 +4,9 @@ from pydantic import BaseModel, ConfigDict, Field, RootModel, model_validator -ORG_EVIDENCE_BUNDLE_SCHEMA_VERSION = "shipgate.org_evidence_bundle/v1" +from agents_shipgate.schemas.verification_identity import CONTENT_ID_PATTERN + +ORG_EVIDENCE_BUNDLE_SCHEMA_VERSION = "shipgate.org_evidence_bundle/v2" class OrgEvidenceBundleArtifactRef(BaseModel): @@ -23,7 +25,7 @@ class OrgEvidenceBundleV1(BaseModel): model_config = ConfigDict(extra="forbid") - org_evidence_bundle_schema_version: Literal["shipgate.org_evidence_bundle/v1"] = ( + org_evidence_bundle_schema_version: Literal["shipgate.org_evidence_bundle/v2"] = ( ORG_EVIDENCE_BUNDLE_SCHEMA_VERSION ) gating_signal: Literal["release_decision.decision"] = "release_decision.decision" @@ -36,6 +38,11 @@ class OrgEvidenceBundleV1(BaseModel): source_report: str | None = None source_verify_run: str | None = None source_attestation: str | None = None + source_verification_receipt: str | None = None + request_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + receipt_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + decision_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + artifact_set_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) attestation: dict[str, Any] | None = None registry_row: dict[str, Any] | None = None org_status: dict[str, Any] | None = None @@ -49,6 +56,11 @@ class OrgEvidenceBundleV1(BaseModel): def _not_a_second_gate(self) -> OrgEvidenceBundleV1: if self.gating_signal != "release_decision.decision": raise ValueError("org evidence bundles must name release_decision.decision") + identity = (self.request_id, self.receipt_id, self.decision_id, self.artifact_set_id) + if any(value is not None for value in identity) and not all( + value is not None for value in identity + ): + raise ValueError("org evidence receipt identity must be complete when present") return self diff --git a/src/agents_shipgate/schemas/packet.py b/src/agents_shipgate/schemas/packet.py index bc580c89..cbd8b328 100644 --- a/src/agents_shipgate/schemas/packet.py +++ b/src/agents_shipgate/schemas/packet.py @@ -35,6 +35,7 @@ ActionSurfaceDiffSummary, ToolSurfaceDiffSummary, ) +from agents_shipgate.schemas.verification_identity import CONTENT_ID_PATTERN VerdictLabel = Literal[ "PASSED", @@ -359,9 +360,14 @@ class EvidencePacket(BaseModel): # human_in_the_loop. release_decision.decision remains the only gate. # v0.8: release-decision evidence coverage carries the additive # evidence-backed semantic coverage and gap remediation contract. - packet_schema_version: Literal["0.11"] = "0.11" + packet_schema_version: Literal["0.12"] = "0.12" generated_at: str | None = None run_id: str + request_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + subject_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + input_set_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + engine_requirement_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + decision_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) project: dict[str, Any] = Field(default_factory=dict) agent: dict[str, Any] = Field(default_factory=dict) environment: dict[str, Any] = Field(default_factory=dict) diff --git a/src/agents_shipgate/schemas/registry.py b/src/agents_shipgate/schemas/registry.py index a3032227..6fb78f2e 100644 --- a/src/agents_shipgate/schemas/registry.py +++ b/src/agents_shipgate/schemas/registry.py @@ -4,7 +4,9 @@ from pydantic import BaseModel, ConfigDict, Field -REGISTRY_SCHEMA_VERSION = "0.3" +from agents_shipgate.schemas.verification_identity import CONTENT_ID_PATTERN + +REGISTRY_SCHEMA_VERSION = "0.4" class RegistryRowV1(BaseModel): @@ -28,6 +30,11 @@ class RegistryRowV1(BaseModel): attestation_schema_version: str | None = None cli_version: str | None = None run_id: str | None = None + request_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + receipt_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + decision_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + artifact_set_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + source_verification_receipt_sha256: str | None = Field(default=None, pattern=r"^[0-9a-f]{64}$") source_attestation_sha256: str | None = None source_verify_run_sha256: str | None = None base_ref: str | None = None @@ -66,7 +73,7 @@ class RegistrySkippedRowV1(BaseModel): class RegistryQueryResultV1(BaseModel): model_config = ConfigDict(extra="forbid") - registry_schema_version: Literal["0.3"] = REGISTRY_SCHEMA_VERSION + registry_schema_version: Literal["0.4"] = REGISTRY_SCHEMA_VERSION registry: str count: int skipped_count: int = 0 @@ -77,7 +84,7 @@ class RegistryQueryResultV1(BaseModel): class RegistryBypassReportV1(BaseModel): model_config = ConfigDict(extra="forbid") - registry_schema_version: Literal["0.3"] = REGISTRY_SCHEMA_VERSION + registry_schema_version: Literal["0.4"] = REGISTRY_SCHEMA_VERSION registry: str bypass_count: int skipped_count: int = 0 @@ -88,7 +95,7 @@ class RegistryBypassReportV1(BaseModel): class RegistrySummaryV1(BaseModel): model_config = ConfigDict(extra="forbid") - registry_schema_version: Literal["0.3"] = REGISTRY_SCHEMA_VERSION + registry_schema_version: Literal["0.4"] = REGISTRY_SCHEMA_VERSION registry: str count: int skipped_count: int = 0 @@ -118,7 +125,7 @@ class RegistryVerificationIssueV1(BaseModel): class RegistryVerificationResultV1(BaseModel): model_config = ConfigDict(extra="forbid") - registry_schema_version: Literal["0.3"] = REGISTRY_SCHEMA_VERSION + registry_schema_version: Literal["0.4"] = REGISTRY_SCHEMA_VERSION registry: str row_count: int skipped_count: int = 0 diff --git a/src/agents_shipgate/schemas/report.py b/src/agents_shipgate/schemas/report.py index f1e44468..1679ff16 100644 --- a/src/agents_shipgate/schemas/report.py +++ b/src/agents_shipgate/schemas/report.py @@ -35,6 +35,7 @@ ToolSurfaceDiff, ToolSurfaceFacts, ) +from agents_shipgate.schemas.verification_identity import CONTENT_ID_PATTERN class CapabilityPolicyEvidence(BaseModel): @@ -1008,8 +1009,14 @@ class ReadinessReport(BaseModel): # v0.32: required Conductor OSS workflow summary fields. # v0.33: typed evidence basis, predicate support, and unsuppressible # indeterminate-policy evidence gaps. - report_schema_version: str = "0.33" + # v0.34: content-addressed verification request and decision bindings. + report_schema_version: str = "0.34" run_id: str + request_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + subject_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + input_set_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + engine_requirement_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + decision_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) # v0.6 (per C13): absolute path to the directory containing # shipgate.yaml. apply-patches uses this to enforce a containment # check on every patch's target_file. Optional for backwards diff --git a/src/agents_shipgate/schemas/safety_qualification.py b/src/agents_shipgate/schemas/safety_qualification.py index a153a2b5..bf8a9f8e 100644 --- a/src/agents_shipgate/schemas/safety_qualification.py +++ b/src/agents_shipgate/schemas/safety_qualification.py @@ -9,9 +9,9 @@ from agents_shipgate.schemas.common import ReleaseDecisionStatus from agents_shipgate.schemas.disclaimers import STATIC_VERDICT_DISCLAIMER -SAFETY_CORPUS_SCHEMA_VERSION = "shipgate.safety_corpus/v3" -SAFETY_RECEIPT_INDEX_SCHEMA_VERSION = "shipgate.safety_receipt_index/v3" -SAFETY_QUALIFICATION_SCHEMA_VERSION = "shipgate.safety_qualification/v3" +SAFETY_CORPUS_SCHEMA_VERSION = "shipgate.safety_corpus/v4" +SAFETY_RECEIPT_INDEX_SCHEMA_VERSION = "shipgate.safety_receipt_index/v4" +SAFETY_QUALIFICATION_SCHEMA_VERSION = "shipgate.safety_qualification/v4" SafetyProfile = Literal[ "mcp", @@ -190,7 +190,7 @@ class FrozenSafetyCorpusV1(BaseModel): model_config = ConfigDict(extra="forbid") - schema_version: Literal["shipgate.safety_corpus/v3"] = SAFETY_CORPUS_SCHEMA_VERSION + schema_version: Literal["shipgate.safety_corpus/v4"] = SAFETY_CORPUS_SCHEMA_VERSION corpus_id: str = Field(min_length=1) labels_frozen_before_evaluation: Literal[True] outputs_hidden_from_labelers: Literal[True] @@ -238,7 +238,7 @@ def _validate_relative_path(value: str) -> str: class SafetyReceiptEntryV1(BaseModel): - """Index row for one real, content-addressed verify-run receipt.""" + """Index row for one real, terminal verification receipt.""" model_config = ConfigDict(extra="forbid") @@ -247,6 +247,10 @@ class SafetyReceiptEntryV1(BaseModel): artifact_root: str receipt_sha256: str = Field(pattern=SHA256_PATTERN) run_id: str = Field(pattern=r"^sha256:[0-9a-f]{64}$") + request_id: str = Field(pattern=r"^sha256:[0-9a-f]{64}$") + receipt_id: str = Field(pattern=r"^sha256:[0-9a-f]{64}$") + decision_id: str = Field(pattern=r"^sha256:[0-9a-f]{64}$") + artifact_set_id: str = Field(pattern=r"^sha256:[0-9a-f]{64}$") execution_mode: Literal["verify"] = "verify" fallback_used: Literal[False] = False @@ -269,7 +273,7 @@ class SafetyReceiptIndexV1(BaseModel): model_config = ConfigDict(extra="forbid") - schema_version: Literal["shipgate.safety_receipt_index/v3"] = ( + schema_version: Literal["shipgate.safety_receipt_index/v4"] = ( SAFETY_RECEIPT_INDEX_SCHEMA_VERSION ) wheel_sha256: str = Field(pattern=SHA256_PATTERN) @@ -285,7 +289,8 @@ class SafetyReceiptIndexV1(BaseModel): def _upgrade_v1_schema(cls, value: str) -> str: return ( SAFETY_RECEIPT_INDEX_SCHEMA_VERSION - if value in { + if value + in { "shipgate.safety_receipt_index/v1", "shipgate.safety_receipt_index/v2", } @@ -405,7 +410,7 @@ def production_safety_requirements() -> SafetyQualificationRequirementsV1: minimum_blocked_exact=30, minimum_review_exact=19, minimum_insufficient_evidence_exact=19, - required_report_schema_version="0.33", + required_report_schema_version="0.34", ) @@ -517,7 +522,7 @@ class SafetyQualificationResultV1(BaseModel): model_config = ConfigDict(extra="forbid") - schema_version: Literal["shipgate.safety_qualification/v3"] = ( + schema_version: Literal["shipgate.safety_qualification/v4"] = ( SAFETY_QUALIFICATION_SCHEMA_VERSION ) qualification_tier: QualificationTier @@ -540,7 +545,8 @@ class SafetyQualificationResultV1(BaseModel): def _upgrade_v1_schema(cls, value: str) -> str: return ( SAFETY_QUALIFICATION_SCHEMA_VERSION - if value in { + if value + in { "shipgate.safety_qualification/v1", "shipgate.safety_qualification/v2", } diff --git a/src/agents_shipgate/schemas/verification_identity.py b/src/agents_shipgate/schemas/verification_identity.py new file mode 100644 index 00000000..a114158d --- /dev/null +++ b/src/agents_shipgate/schemas/verification_identity.py @@ -0,0 +1,407 @@ +"""Content-addressed identities for reproducible verification execution. + +The models in this module deliberately separate a verification request from an +execution attempt and from the eventual decision/artifact closure. All +authoritative identifiers are SHA-256 hashes of canonical JSON payloads. An +``attempt_id`` is diagnostic only and is never accepted as a trust binding. +""" + +from __future__ import annotations + +import hashlib +import json +import re +from pathlib import PurePosixPath +from typing import Any, Literal + +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator + +VERIFICATION_PLAN_SCHEMA_VERSION = "shipgate.verification_plan/v1" +VERIFICATION_UNIT_RESULT_SCHEMA_VERSION = "shipgate.verification_unit_result/v1" +VERIFICATION_ARTIFACT_MANIFEST_SCHEMA_VERSION = "shipgate.verification_artifact_manifest/v1" +VERIFICATION_RECEIPT_SCHEMA_VERSION = "shipgate.verification_receipt/v1" +CONTENT_ID_PATTERN = r"^sha256:[0-9a-f]{64}$" +GIT_OBJECT_PATTERN = r"^[0-9a-f]{40,64}$" + + +def canonical_json(value: Any) -> bytes: + """Return the one wire representation used by every identity hash.""" + + if isinstance(value, BaseModel): + value = value.model_dump(mode="json", exclude_none=False) + return json.dumps( + value, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + ).encode("utf-8") + + +def content_id(value: Any) -> str: + return f"sha256:{hashlib.sha256(canonical_json(value)).hexdigest()}" + + +def _identity_payload(model: BaseModel, identity_field: str) -> dict[str, Any]: + return model.model_dump( + mode="json", + exclude={identity_field, "schema_version"}, + exclude_none=False, + ) + + +def _validate_portable_path(value: str) -> str: + path = PurePosixPath(value) + if ( + not value + or "\\" in value + or path.is_absolute() + or ".." in path.parts + or path.as_posix() != value + ): + raise ValueError("artifact and input paths must be normalized portable relative paths") + return value + + +class VerificationBlob(BaseModel): + model_config = ConfigDict(extra="forbid") + + path: str + sha256: str = Field(pattern=CONTENT_ID_PATTERN) + size_bytes: int = Field(ge=0) + source: Literal[ + "git_blob", + "worktree", + "generated", + "external_input", + "artifact", + ] + git_blob_oid: str | None = None + + _path_is_portable = field_validator("path")(_validate_portable_path) + + +class VerificationGitSubject(BaseModel): + model_config = ConfigDict(extra="forbid") + + repository_id: str + base_ref: str | None = None + base_commit_sha: str | None = Field(default=None, pattern=GIT_OBJECT_PATTERN) + base_tree_sha: str | None = Field(default=None, pattern=GIT_OBJECT_PATTERN) + head_ref: str + source_head_commit_sha: str | None = Field(default=None, pattern=GIT_OBJECT_PATTERN) + head_commit_sha: str | None = Field(default=None, pattern=GIT_OBJECT_PATTERN) + head_tree_sha: str | None = Field(default=None, pattern=GIT_OBJECT_PATTERN) + merge_base_sha: str | None = Field(default=None, pattern=GIT_OBJECT_PATTERN) + snapshot_kind: Literal["committed_tree", "worktree_overlay"] + worktree_overlay_sha256: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + + +class VerificationSubject(BaseModel): + model_config = ConfigDict(extra="forbid") + + subject_id: str = Field(pattern=CONTENT_ID_PATTERN) + git: VerificationGitSubject + + @model_validator(mode="after") + def _subject_id_is_content_addressed(self) -> VerificationSubject: + expected = content_id(self.git) + if self.subject_id != expected: + raise ValueError("subject_id must hash the resolved Git subject") + return self + + +class VerificationInputSet(BaseModel): + model_config = ConfigDict(extra="forbid") + + input_set_id: str = Field(pattern=CONTENT_ID_PATTERN) + evaluation_date: str + config: VerificationBlob + diff: VerificationBlob + baseline: VerificationBlob | None = None + diff_from: VerificationBlob | None = None + policy_packs: list[VerificationBlob] = Field(default_factory=list) + tool_sources: list[VerificationBlob] = Field(default_factory=list) + changed_paths: list[str] = Field(default_factory=list) + changed_files: list[VerificationBlob] = Field(default_factory=list) + options: dict[str, Any] = Field(default_factory=dict) + + @model_validator(mode="after") + def _input_set_id_is_content_addressed(self) -> VerificationInputSet: + for value in self.changed_paths: + _validate_portable_path(value) + if self.changed_paths != sorted(set(self.changed_paths)): + raise ValueError("changed_paths must be sorted and unique") + for name, blobs in ( + ("policy_packs", self.policy_packs), + ("tool_sources", self.tool_sources), + ("changed_files", self.changed_files), + ): + paths = [blob.path for blob in blobs] + if paths != sorted(set(paths)): + raise ValueError(f"{name} paths must be sorted and unique") + expected = content_id(_identity_payload(self, "input_set_id")) + if self.input_set_id != expected: + raise ValueError("input_set_id must hash the complete normalized input set") + return self + + +class VerificationEngineRequirement(BaseModel): + model_config = ConfigDict(extra="forbid") + + engine_requirement_id: str = Field(pattern=CONTENT_ID_PATTERN) + package: str = "agents-shipgate" + version: str + python_implementation: str + python_version: str + platform: str + engine_distribution_sha256: str = Field(pattern=CONTENT_ID_PATTERN) + dependency_set_sha256: str = Field(pattern=CONTENT_ID_PATTERN) + adapter_set_sha256: str = Field(pattern=CONTENT_ID_PATTERN) + plugin_set_sha256: str = Field(pattern=CONTENT_ID_PATTERN) + policy_catalog_sha256: str = Field(pattern=CONTENT_ID_PATTERN) + + @model_validator(mode="after") + def _engine_id_is_content_addressed(self) -> VerificationEngineRequirement: + expected = content_id(_identity_payload(self, "engine_requirement_id")) + if self.engine_requirement_id != expected: + raise ValueError("engine_requirement_id must hash the engine requirements") + return self + + +class VerificationTask(BaseModel): + model_config = ConfigDict(extra="forbid") + + task_id: str = Field(pattern=CONTENT_ID_PATTERN) + kind: Literal["extract", "normalize", "evaluate"] + shard: int = Field(ge=0) + shard_count: int = Field(ge=1) + input_paths: list[str] = Field(default_factory=list) + + @model_validator(mode="after") + def _task_id_is_content_addressed(self) -> VerificationTask: + if self.input_paths != sorted(set(self.input_paths)): + raise ValueError("task input_paths must be sorted and unique") + expected = content_id(_identity_payload(self, "task_id")) + if self.task_id != expected: + raise ValueError("task_id must hash the normalized task") + return self + + +class VerificationPlan(BaseModel): + model_config = ConfigDict(extra="forbid") + + schema_version: Literal["shipgate.verification_plan/v1"] = VERIFICATION_PLAN_SCHEMA_VERSION + request_id: str = Field(pattern=CONTENT_ID_PATTERN) + subject: VerificationSubject + inputs: VerificationInputSet + engine: VerificationEngineRequirement + tasks: list[VerificationTask] = Field(min_length=1, max_length=1) + + @model_validator(mode="after") + def _request_id_is_content_addressed(self) -> VerificationPlan: + if len({task.task_id for task in self.tasks}) != len(self.tasks): + raise ValueError("verification plan task IDs must be unique") + payload = { + "subject_id": self.subject.subject_id, + "input_set_id": self.inputs.input_set_id, + "engine_requirement_id": self.engine.engine_requirement_id, + "task_ids": [task.task_id for task in self.tasks], + } + expected = content_id(payload) + if self.request_id != expected: + raise ValueError("request_id must hash the verification plan identity") + return self + + +class VerificationExecutor(BaseModel): + model_config = ConfigDict(extra="forbid") + + executor_id: str = Field(pattern=CONTENT_ID_PATTERN) + engine_requirement_id: str = Field(pattern=CONTENT_ID_PATTERN) + runtime_sha256: str = Field(pattern=CONTENT_ID_PATTERN) + + @model_validator(mode="after") + def _executor_id_is_content_addressed(self) -> VerificationExecutor: + expected = content_id(_identity_payload(self, "executor_id")) + if self.executor_id != expected: + raise ValueError("executor_id must hash the executor descriptor") + return self + + +class VerificationUnitResult(BaseModel): + """Decision-free normalized worker output consumed by the assembler.""" + + model_config = ConfigDict(extra="forbid") + + schema_version: Literal["shipgate.verification_unit_result/v1"] = ( + VERIFICATION_UNIT_RESULT_SCHEMA_VERSION + ) + unit_result_id: str = Field(pattern=CONTENT_ID_PATTERN) + request_id: str = Field(pattern=CONTENT_ID_PATTERN) + task_id: str = Field(pattern=CONTENT_ID_PATTERN) + executor: VerificationExecutor + status: Literal["succeeded", "failed"] + normalized_ir: dict[str, Any] = Field(default_factory=dict) + issues: list[str] = Field(default_factory=list) + + @model_validator(mode="after") + def _unit_result_id_is_content_addressed(self) -> VerificationUnitResult: + forbidden = {"decision", "merge_verdict", "can_merge_without_human", "control"} + if _contains_forbidden_key(self.normalized_ir, forbidden): + raise ValueError("workers may emit normalized IR, never release decisions") + expected = content_id(_identity_payload(self, "unit_result_id")) + if self.unit_result_id != expected: + raise ValueError("unit_result_id must hash the normalized worker result") + return self + + +def _contains_forbidden_key(value: Any, forbidden: set[str]) -> bool: + if isinstance(value, dict): + return bool(forbidden.intersection(value)) or any( + _contains_forbidden_key(item, forbidden) for item in value.values() + ) + if isinstance(value, list): + return any(_contains_forbidden_key(item, forbidden) for item in value) + return False + + +class VerificationArtifactRef(BaseModel): + model_config = ConfigDict(extra="forbid") + + path: str + sha256: str = Field(pattern=CONTENT_ID_PATTERN) + size_bytes: int = Field(ge=0) + media_type: str + + _path_is_portable = field_validator("path")(_validate_portable_path) + + +class VerificationArtifactManifest(BaseModel): + model_config = ConfigDict(extra="forbid") + + schema_version: Literal["shipgate.verification_artifact_manifest/v1"] = ( + VERIFICATION_ARTIFACT_MANIFEST_SCHEMA_VERSION + ) + artifact_set_id: str = Field(pattern=CONTENT_ID_PATTERN) + request_id: str = Field(pattern=CONTENT_ID_PATTERN) + decision_id: str = Field(pattern=CONTENT_ID_PATTERN) + artifacts: dict[str, VerificationArtifactRef] = Field(min_length=1) + + @model_validator(mode="after") + def _artifact_set_id_is_content_addressed(self) -> VerificationArtifactManifest: + expected = content_id(_identity_payload(self, "artifact_set_id")) + if self.artifact_set_id != expected: + raise ValueError("artifact_set_id must hash the complete artifact manifest") + return self + + +class VerificationReceipt(BaseModel): + """Terminal closure record. It is valid only after every artifact exists.""" + + model_config = ConfigDict(extra="forbid") + + schema_version: Literal["shipgate.verification_receipt/v1"] = ( + VERIFICATION_RECEIPT_SCHEMA_VERSION + ) + receipt_id: str = Field(pattern=CONTENT_ID_PATTERN) + request_id: str = Field(pattern=CONTENT_ID_PATTERN) + subject_id: str = Field(pattern=CONTENT_ID_PATTERN) + input_set_id: str = Field(pattern=CONTENT_ID_PATTERN) + engine_requirement_id: str = Field(pattern=CONTENT_ID_PATTERN) + executor_id: str = Field(pattern=CONTENT_ID_PATTERN) + decision_id: str = Field(pattern=CONTENT_ID_PATTERN) + artifact_set_id: str = Field(pattern=CONTENT_ID_PATTERN) + unit_result_ids: list[str] = Field(min_length=1) + attempt_id: str | None = None + decision: ( + Literal[ + "passed", + "review_required", + "insufficient_evidence", + "blocked", + ] + | None + ) = None + merge_verdict: Literal[ + "mergeable", + "human_review_required", + "insufficient_evidence", + "blocked", + "unknown", + ] + can_merge_without_human: bool + artifact_manifest: VerificationArtifactManifest + + @model_validator(mode="after") + def _receipt_closes_one_identity_graph(self) -> VerificationReceipt: + if len(set(self.unit_result_ids)) != len(self.unit_result_ids): + raise ValueError("receipt unit_result_ids must be unique") + if any(not re.fullmatch(CONTENT_ID_PATTERN, value) for value in self.unit_result_ids): + raise ValueError("receipt unit_result_ids must be SHA-256 content IDs") + expected_decision_id = content_id( + { + "request_id": self.request_id, + "unit_result_ids": sorted(self.unit_result_ids), + "decision": self.decision, + "merge_verdict": self.merge_verdict, + "can_merge_without_human": self.can_merge_without_human, + } + ) + if self.decision_id != expected_decision_id: + raise ValueError("receipt decision_id must hash its request, units, and outcome") + decision_projection = { + "passed": ("mergeable", True), + "review_required": ("human_review_required", False), + "insufficient_evidence": ("insufficient_evidence", False), + "blocked": ("blocked", False), + } + if self.decision in decision_projection: + expected_merge, expected_can_merge = decision_projection[self.decision] + if (self.merge_verdict, self.can_merge_without_human) != ( + expected_merge, + expected_can_merge, + ): + raise ValueError("receipt outcome contradicts its release decision") + elif (self.merge_verdict, self.can_merge_without_human) not in { + ("mergeable", True), + ("unknown", False), + }: + raise ValueError("receipt outcome contradicts a decision-free execution") + if self.artifact_manifest.request_id != self.request_id: + raise ValueError("receipt and artifact manifest request_id disagree") + if self.artifact_manifest.decision_id != self.decision_id: + raise ValueError("receipt and artifact manifest decision_id disagree") + if self.artifact_manifest.artifact_set_id != self.artifact_set_id: + raise ValueError("receipt and artifact manifest artifact_set_id disagree") + expected = content_id( + self.model_dump( + mode="json", + exclude={"receipt_id", "schema_version", "attempt_id"}, + exclude_none=False, + ) + ) + if self.receipt_id != expected: + raise ValueError("receipt_id must hash the complete terminal receipt") + return self + + +__all__ = [ + "VERIFICATION_ARTIFACT_MANIFEST_SCHEMA_VERSION", + "VERIFICATION_PLAN_SCHEMA_VERSION", + "VERIFICATION_RECEIPT_SCHEMA_VERSION", + "VERIFICATION_UNIT_RESULT_SCHEMA_VERSION", + "VerificationArtifactManifest", + "VerificationArtifactRef", + "VerificationBlob", + "VerificationEngineRequirement", + "VerificationExecutor", + "VerificationGitSubject", + "VerificationInputSet", + "VerificationPlan", + "VerificationReceipt", + "VerificationSubject", + "VerificationTask", + "VerificationUnitResult", + "canonical_json", + "content_id", +] diff --git a/src/agents_shipgate/schemas/verifier.py b/src/agents_shipgate/schemas/verifier.py index af09d345..d2a479e0 100644 --- a/src/agents_shipgate/schemas/verifier.py +++ b/src/agents_shipgate/schemas/verifier.py @@ -8,6 +8,7 @@ from agents_shipgate.schemas.common import ReleaseDecisionStatus from agents_shipgate.schemas.disclaimers import STATIC_VERDICT_DISCLAIMER from agents_shipgate.schemas.report import ReleaseDecision +from agents_shipgate.schemas.verification_identity import CONTENT_ID_PATTERN VerifierBaseStatus = Literal[ "not_requested", @@ -424,11 +425,17 @@ class VerifierArtifact(BaseModel): }, ) - verifier_schema_version: Literal["0.4"] = "0.4" + verifier_schema_version: Literal["0.5"] = "0.5" static_analysis_only: Literal[True] = True runtime_behavior_verified: Literal[False] = False static_verdict_disclaimer: str = STATIC_VERDICT_DISCLAIMER workspace: str + request_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + subject_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + input_set_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + engine_requirement_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + executor_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) + decision_id: str | None = Field(default=None, pattern=CONTENT_ID_PATTERN) config: str base_ref: str | None = None head_ref: str = "HEAD" @@ -471,14 +478,14 @@ def _normalize_legacy_control(cls, data: Any) -> Any: return data normalized = dict(data) legacy_version = normalized.get("verifier_schema_version") - legacy = legacy_version in {"0.1", "0.2", "0.3"} + legacy = legacy_version in {"0.1", "0.2", "0.3", "0.4"} if not legacy: - # Current v0.4 artifacts must already carry the authoritative + # Current v0.5 artifacts must already carry the authoritative # control union. Silently synthesizing a missing or malformed # current control would turn an internal consistency failure into # a trusted handoff. Only the frozen v0.2 reader is normalized. return normalized - normalized["verifier_schema_version"] = "0.4" + normalized["verifier_schema_version"] = "0.5" execution = normalized.get("execution") or normalized.get("head_status") execution = execution or "not_run" @@ -523,9 +530,7 @@ def _normalize_legacy_control(cls, data: Any) -> Any: dict(legacy_controller) if isinstance(legacy_controller, dict) else {} ) if "completion_allowed" not in legacy_payload: - legacy_payload["completion_allowed"] = bool( - normalized.get("can_merge_without_human") - ) + legacy_payload["completion_allowed"] = bool(normalized.get("can_merge_without_human")) for key in ("first_next_action", "human_review"): if key in normalized: legacy_payload[key] = normalized[key] diff --git a/src/agents_shipgate/schemas/verify_run.py b/src/agents_shipgate/schemas/verify_run.py index 73944d29..84a25149 100644 --- a/src/agents_shipgate/schemas/verify_run.py +++ b/src/agents_shipgate/schemas/verify_run.py @@ -2,14 +2,21 @@ import hashlib import json +import re from typing import Literal from pydantic import BaseModel, ConfigDict, Field, model_validator from agents_shipgate import __version__ from agents_shipgate.schemas.agent_control import AgentControl +from agents_shipgate.schemas.verification_identity import ( + CONTENT_ID_PATTERN, + VerificationExecutor, + VerificationPlan, + content_id, +) -VERIFY_RUN_SCHEMA_VERSION = "shipgate.verify_run/v2" +VERIFY_RUN_SCHEMA_VERSION = "shipgate.verify_run/v3" class VerifyRunTool(BaseModel): @@ -195,10 +202,12 @@ def _control_projects_outcome(self) -> VerifyRunOutcome: return self -class VerifyRunArtifact(BaseModel): +class VerifyRunArtifactV2(BaseModel): + """Frozen reader for the pre-identity verify-run contract.""" + model_config = ConfigDict(extra="forbid") - schema_version: Literal["shipgate.verify_run/v2"] = VERIFY_RUN_SCHEMA_VERSION + schema_version: Literal["shipgate.verify_run/v2"] = "shipgate.verify_run/v2" run_id: str tool: VerifyRunTool subject: VerifyRunSubject @@ -207,7 +216,7 @@ class VerifyRunArtifact(BaseModel): artifacts: dict[str, VerifyRunArtifactRef] = Field(default_factory=dict) @model_validator(mode="after") - def _run_id_matches_identity(self) -> VerifyRunArtifact: + def _run_id_matches_identity(self) -> VerifyRunArtifactV2: expected = compute_verify_run_id(subject=self.subject, inputs=self.inputs, tool=self.tool) if self.run_id != expected: raise ValueError( @@ -216,6 +225,49 @@ def _run_id_matches_identity(self) -> VerifyRunArtifact: return self +class VerifyRunArtifact(BaseModel): + """Current run projection bound to a content-addressed request plan.""" + + model_config = ConfigDict(extra="forbid") + + schema_version: Literal["shipgate.verify_run/v3"] = VERIFY_RUN_SCHEMA_VERSION + request_id: str = Field(pattern=CONTENT_ID_PATTERN) + # Deprecated for one compatibility cycle; it is an exact alias, never a + # separately-computed identity. + run_id: str = Field(pattern=CONTENT_ID_PATTERN) + plan: VerificationPlan + executor: VerificationExecutor + unit_result_ids: list[str] = Field(min_length=1) + decision_id: str = Field(pattern=CONTENT_ID_PATTERN) + outcome: VerifyRunOutcome + artifacts: dict[str, VerifyRunArtifactRef] = Field(default_factory=dict) + + @model_validator(mode="after") + def _identity_graph_is_consistent(self) -> VerifyRunArtifact: + if self.run_id != self.request_id: + raise ValueError("verify-run run_id must be the exact deprecated request_id alias") + if self.request_id != self.plan.request_id: + raise ValueError("verify-run request_id must match its verification plan") + if self.executor.engine_requirement_id != self.plan.engine.engine_requirement_id: + raise ValueError("verify-run executor does not satisfy the plan engine") + if len(set(self.unit_result_ids)) != len(self.unit_result_ids): + raise ValueError("verify-run unit_result_ids must be unique") + if any(not re.fullmatch(CONTENT_ID_PATTERN, value) for value in self.unit_result_ids): + raise ValueError("verify-run unit_result_ids must be SHA-256 content IDs") + expected = content_id( + { + "request_id": self.request_id, + "unit_result_ids": sorted(self.unit_result_ids), + "decision": self.outcome.decision, + "merge_verdict": self.outcome.merge_verdict, + "can_merge_without_human": self.outcome.can_merge_without_human, + } + ) + if self.decision_id != expected: + raise ValueError("verify-run decision_id must hash its request, units, and outcome") + return self + + def compute_verify_run_id( *, subject: VerifyRunSubject, @@ -235,22 +287,28 @@ def compute_verify_run_id( def build_verify_run_artifact( *, - subject: VerifyRunSubject, - inputs: VerifyRunInputs, + plan: VerificationPlan, + executor: VerificationExecutor, + unit_result_ids: list[str], outcome: VerifyRunOutcome, artifacts: dict[str, VerifyRunArtifactRef], - tool: VerifyRunTool | None = None, ) -> VerifyRunArtifact: - resolved_tool = tool or VerifyRunTool(version=__version__) + decision_id = content_id( + { + "request_id": plan.request_id, + "unit_result_ids": sorted(unit_result_ids), + "decision": outcome.decision, + "merge_verdict": outcome.merge_verdict, + "can_merge_without_human": outcome.can_merge_without_human, + } + ) return VerifyRunArtifact( - run_id=compute_verify_run_id( - subject=subject, - inputs=inputs, - tool=resolved_tool, - ), - tool=resolved_tool, - subject=subject, - inputs=inputs, + request_id=plan.request_id, + run_id=plan.request_id, + plan=plan, + executor=executor, + unit_result_ids=sorted(unit_result_ids), + decision_id=decision_id, outcome=outcome, artifacts=artifacts, ) @@ -260,6 +318,7 @@ def build_verify_run_artifact( "VERIFY_RUN_SCHEMA_VERSION", "VerifyRunArtifact", "VerifyRunArtifactV1", + "VerifyRunArtifactV2", "VerifyRunArtifactRef", "VerifyRunInputs", "VerifyRunOutcome", diff --git a/tests/golden/agent_protocol/claude-code-block-stop.json b/tests/golden/agent_protocol/claude-code-block-stop.json index ac097a29..d52c6e69 100644 --- a/tests/golden/agent_protocol/claude-code-block-stop.json +++ b/tests/golden/agent_protocol/claude-code-block-stop.json @@ -3,7 +3,7 @@ "agent": "claude-code", "tool": { "name": "agents-shipgate", - "version": "0.16.0b5" + "version": "0.16.0b6" }, "subject": { "agent": "claude-code" diff --git a/tests/golden/agent_protocol/codex-block-stop.json b/tests/golden/agent_protocol/codex-block-stop.json index 9dfcd09e..216e16eb 100644 --- a/tests/golden/agent_protocol/codex-block-stop.json +++ b/tests/golden/agent_protocol/codex-block-stop.json @@ -3,7 +3,7 @@ "agent": "codex", "tool": { "name": "agents-shipgate", - "version": "0.16.0b5" + "version": "0.16.0b6" }, "subject": { "agent": "codex" diff --git a/tests/golden/agent_protocol/codex-repair-after.json b/tests/golden/agent_protocol/codex-repair-after.json index 5f35ef77..25a48a2a 100644 --- a/tests/golden/agent_protocol/codex-repair-after.json +++ b/tests/golden/agent_protocol/codex-repair-after.json @@ -3,7 +3,7 @@ "agent": "codex", "tool": { "name": "agents-shipgate", - "version": "0.16.0b5" + "version": "0.16.0b6" }, "subject": { "agent": "codex" diff --git a/tests/golden/agent_protocol/codex-repair-before.json b/tests/golden/agent_protocol/codex-repair-before.json index e23d2766..cd735a0d 100644 --- a/tests/golden/agent_protocol/codex-repair-before.json +++ b/tests/golden/agent_protocol/codex-repair-before.json @@ -3,7 +3,7 @@ "agent": "codex", "tool": { "name": "agents-shipgate", - "version": "0.16.0b5" + "version": "0.16.0b6" }, "subject": { "agent": "codex" diff --git a/tests/golden/agent_protocol/cursor-block-stop.json b/tests/golden/agent_protocol/cursor-block-stop.json index 19fcd321..a3291c8d 100644 --- a/tests/golden/agent_protocol/cursor-block-stop.json +++ b/tests/golden/agent_protocol/cursor-block-stop.json @@ -3,7 +3,7 @@ "agent": "cursor", "tool": { "name": "agents-shipgate", - "version": "0.16.0b5" + "version": "0.16.0b6" }, "subject": { "agent": "cursor" diff --git a/tests/golden/agent_protocol/missing-install.json b/tests/golden/agent_protocol/missing-install.json index e0aaa45d..c767cfbd 100644 --- a/tests/golden/agent_protocol/missing-install.json +++ b/tests/golden/agent_protocol/missing-install.json @@ -3,7 +3,7 @@ "agent": "codex", "tool": { "name": "agents-shipgate", - "version": "0.16.0b5" + "version": "0.16.0b6" }, "subject": { "agent": "codex" diff --git a/tests/golden/agent_protocol/stale-install.json b/tests/golden/agent_protocol/stale-install.json index 999c8b36..abef4514 100644 --- a/tests/golden/agent_protocol/stale-install.json +++ b/tests/golden/agent_protocol/stale-install.json @@ -3,7 +3,7 @@ "agent": "codex", "tool": { "name": "agents-shipgate", - "version": "0.16.0b5" + "version": "0.16.0b6" }, "subject": { "agent": "codex" diff --git a/tests/golden/codex_boundary_result/agents_requirement_removed.json b/tests/golden/codex_boundary_result/agents_requirement_removed.json index 6e6aacbb..bb8105cb 100644 --- a/tests/golden/codex_boundary_result/agents_requirement_removed.json +++ b/tests/golden/codex_boundary_result/agents_requirement_removed.json @@ -3,7 +3,7 @@ "agent": "codex", "tool": { "name": "agents-shipgate", - "version": "0.16.0b5" + "version": "0.16.0b6" }, "subject": { "agent": "codex" diff --git a/tests/golden/codex_boundary_result/docs_only.json b/tests/golden/codex_boundary_result/docs_only.json index 96db17ac..df9a0647 100644 --- a/tests/golden/codex_boundary_result/docs_only.json +++ b/tests/golden/codex_boundary_result/docs_only.json @@ -3,7 +3,7 @@ "agent": "codex", "tool": { "name": "agents-shipgate", - "version": "0.16.0b5" + "version": "0.16.0b6" }, "subject": { "agent": "codex" diff --git a/tests/golden/codex_boundary_result/github_action_removed.json b/tests/golden/codex_boundary_result/github_action_removed.json index 9665d142..f258c7a8 100644 --- a/tests/golden/codex_boundary_result/github_action_removed.json +++ b/tests/golden/codex_boundary_result/github_action_removed.json @@ -3,7 +3,7 @@ "agent": "codex", "tool": { "name": "agents-shipgate", - "version": "0.16.0b5" + "version": "0.16.0b6" }, "subject": { "agent": "codex" diff --git a/tests/golden/codex_boundary_result/malformed_toml.json b/tests/golden/codex_boundary_result/malformed_toml.json index df37cc54..c9935823 100644 --- a/tests/golden/codex_boundary_result/malformed_toml.json +++ b/tests/golden/codex_boundary_result/malformed_toml.json @@ -3,7 +3,7 @@ "agent": "codex", "tool": { "name": "agents-shipgate", - "version": "0.16.0b5" + "version": "0.16.0b6" }, "subject": { "agent": "codex" diff --git a/tests/golden/codex_boundary_result/mcp_auto_approve_write.json b/tests/golden/codex_boundary_result/mcp_auto_approve_write.json index a88a5b44..72da1cda 100644 --- a/tests/golden/codex_boundary_result/mcp_auto_approve_write.json +++ b/tests/golden/codex_boundary_result/mcp_auto_approve_write.json @@ -3,7 +3,7 @@ "agent": "codex", "tool": { "name": "agents-shipgate", - "version": "0.16.0b5" + "version": "0.16.0b6" }, "subject": { "agent": "codex" diff --git a/tests/golden/codex_boundary_result/network_wildcard.json b/tests/golden/codex_boundary_result/network_wildcard.json index 22342584..3ba64f6a 100644 --- a/tests/golden/codex_boundary_result/network_wildcard.json +++ b/tests/golden/codex_boundary_result/network_wildcard.json @@ -3,7 +3,7 @@ "agent": "codex", "tool": { "name": "agents-shipgate", - "version": "0.16.0b5" + "version": "0.16.0b6" }, "subject": { "agent": "codex" diff --git a/tests/golden/codex_boundary_result/python_refactor.json b/tests/golden/codex_boundary_result/python_refactor.json index b459b89e..80b7ca1d 100644 --- a/tests/golden/codex_boundary_result/python_refactor.json +++ b/tests/golden/codex_boundary_result/python_refactor.json @@ -3,7 +3,7 @@ "agent": "codex", "tool": { "name": "agents-shipgate", - "version": "0.16.0b5" + "version": "0.16.0b6" }, "subject": { "agent": "codex" diff --git a/tests/golden/codex_boundary_result/unknown_permission_key.json b/tests/golden/codex_boundary_result/unknown_permission_key.json index ca2052e7..0ea27d4f 100644 --- a/tests/golden/codex_boundary_result/unknown_permission_key.json +++ b/tests/golden/codex_boundary_result/unknown_permission_key.json @@ -3,7 +3,7 @@ "agent": "codex", "tool": { "name": "agents-shipgate", - "version": "0.16.0b5" + "version": "0.16.0b6" }, "subject": { "agent": "codex" diff --git a/tests/test_adapter_static_only.py b/tests/test_adapter_static_only.py index c05c6a4a..94785563 100644 --- a/tests/test_adapter_static_only.py +++ b/tests/test_adapter_static_only.py @@ -262,8 +262,7 @@ class AllowedException: surface="attr_call:subprocess.run", line=532, snippet=( - "subprocess.run(['git', 'ls-files', '--others', " - "'--exclude-standard'], **run_kwargs)" + "subprocess.run(['git', 'ls-files', '--others', '--exclude-standard'], **run_kwargs)" ), rationale=( "git-untracked enumeration: ``git ls-files --others " @@ -292,7 +291,7 @@ class AllowedException: AllowedException( relative_path="cli/verify/git.py", surface="import:subprocess", - line=4, + line=5, snippet="import subprocess", rationale=( "Verify uses local git commands to resolve refs, collect diffs, " @@ -303,11 +302,11 @@ class AllowedException: AllowedException( relative_path="cli/verify/git.py", surface="attr_call:subprocess.run", - line=218, + line=275, snippet="subprocess.run(cmd, capture_output=True, check=check, text=text, timeout=60)", rationale=( "_run_git helper for verify: executes fixed git argv assembled " - "inside Shipgate (rev-parse, diff, show, archive, and the " + "inside Shipgate (rev-parse, diff, ls-tree, cat-file, and the " "default-base detection, which only reads local refs). " "Capture-only, no shell, no user-code execution, and no fetch." ), @@ -332,8 +331,7 @@ class AllowedException: surface="attr_call:subprocess.run", line=374, snippet=( - "subprocess.run(['git', *args], cwd=cwd, check=True, " - "capture_output=True, text=True)" + "subprocess.run(['git', *args], cwd=cwd, check=True, capture_output=True, text=True)" ), rationale=( "_git helper for the ai_generated_refund_pr fixture: runs local " @@ -461,11 +459,10 @@ def _lookup_allowed( return None -def _violation_allowed( - relative_path: str, surface: str, line: int, snippet: str -) -> bool: +def _violation_allowed(relative_path: str, surface: str, line: int, snippet: str) -> bool: return _lookup_allowed(relative_path, surface, line, snippet) is not None + # Bare-name forbidden builtins. Catches direct calls like ``exec("...")``. FORBIDDEN_NAME_CALLS: frozenset[str] = frozenset( { @@ -578,9 +575,7 @@ def _violation_allowed( # can be resolved to a canonical ``importlib.resources.`` chain that # the ``importlib.resources.`` prefix in ``FORBIDDEN_ATTR_CALL_PREFIXES`` # catches. -TRACKED_NON_FORBIDDEN_MODULES: frozenset[str] = frozenset( - {"os", "importlib.resources"} -) +TRACKED_NON_FORBIDDEN_MODULES: frozenset[str] = frozenset({"os", "importlib.resources"}) def _is_forbidden_chain(chain: str) -> bool: @@ -595,8 +590,7 @@ def _is_forbidden_module_import(module: str) -> bool: if module in ALLOWED_FORBIDDEN_MODULE_IMPORTS: return False return any( - module == forbidden or module.startswith(f"{forbidden}.") - for forbidden in FORBIDDEN_MODULES + module == forbidden or module.startswith(f"{forbidden}.") for forbidden in FORBIDDEN_MODULES ) @@ -636,8 +630,7 @@ def _resolve_attribute_chain_all( # still call out structurally). return [".".join(parts)] return [ - ".".join(canonical_root.split(".") + parts[1:]) - for canonical_root in module_aliases[root] + ".".join(canonical_root.split(".") + parts[1:]) for canonical_root in module_aliases[root] ] @@ -761,8 +754,7 @@ def _scan_source(source: str, path: Path) -> list[Violation]: line=node.lineno, surface=f"import:{canonical}", message=( - f"{rel}:{node.lineno}: forbidden from-import " - f"of {canonical!r}" + f"{rel}:{node.lineno}: forbidden from-import of {canonical!r}" ), snippet=ast.unparse(node), ) @@ -782,10 +774,7 @@ def _scan_source(source: str, path: Path) -> list[Violation]: Violation( line=node.lineno, surface=f"name_call:{func.id}", - message=( - f"{rel}:{node.lineno}: forbidden builtin call " - f"{func.id!r}" - ), + message=(f"{rel}:{node.lineno}: forbidden builtin call {func.id!r}"), snippet=ast.unparse(node), ) ) @@ -806,10 +795,7 @@ def _scan_source(source: str, path: Path) -> list[Violation]: Violation( line=node.lineno, surface=f"attr_call:{canonical}", - message=( - f"{rel}:{node.lineno}: forbidden call " - f"{canonical!r}{via}" - ), + message=(f"{rel}:{node.lineno}: forbidden call {canonical!r}{via}"), snippet=ast.unparse(node), ) ) @@ -824,9 +810,7 @@ def _scan_source(source: str, path: Path) -> list[Violation]: Violation( line=node.lineno, surface=f"attr_call:{chain}", - message=( - f"{rel}:{node.lineno}: forbidden call {chain!r}" - ), + message=(f"{rel}:{node.lineno}: forbidden call {chain!r}"), snippet=ast.unparse(node), ) ) @@ -838,9 +822,7 @@ def _scanner_sources() -> list[Path]: """v0.18 (PR #2): every .py file under src/agents_shipgate/ (not just ``inputs/``). Skips ``__pycache__`` debris. """ - return sorted( - p for p in SCANNER_DIR.rglob("*.py") if "__pycache__" not in p.parts - ) + return sorted(p for p in SCANNER_DIR.rglob("*.py") if "__pycache__" not in p.parts) @pytest.mark.parametrize( @@ -867,17 +849,11 @@ def test_scanner_source_contains_no_forbidden_calls_or_imports( rel = str(scanner_source.relative_to(SCANNER_DIR)) source = scanner_source.read_text(encoding="utf-8") violations = _scan_source(source, scanner_source) - unallowed = [ - v - for v in violations - if not _violation_allowed(rel, v.surface, v.line, v.snippet) - ] + unallowed = [v for v in violations if not _violation_allowed(rel, v.surface, v.line, v.snippet)] assert not unallowed, ( "Trust-model invariant violation under src/agents_shipgate/:\n " + "\n ".join( - f"{v.message} (surface={v.surface!r}, " - f"snippet={v.snippet!r})" - for v in unallowed + f"{v.message} (surface={v.surface!r}, snippet={v.snippet!r})" for v in unallowed ) + "\n\n" + "The scanner MUST NOT execute or import user code. Parse with " @@ -1006,13 +982,11 @@ def test_scanner_source_contains_no_forbidden_calls_or_imports( "forbidden call 'os.system'", ), ( - "import os as runner\nrunner.execve('/bin/sh', ['sh'])\n" - "import pathlib as runner\n", + "import os as runner\nrunner.execve('/bin/sh', ['sh'])\nimport pathlib as runner\n", "forbidden call 'os.execve'", ), ( - "from os import system\nsystem('ls')\n" - "from pathlib import system\n", + "from os import system\nsystem('ls')\nfrom pathlib import system\n", "forbidden from-import of 'os.system'", ), # Even when the FORBIDDEN binding comes *after* the safe one, @@ -1100,9 +1074,7 @@ def test_scanner_source_contains_no_forbidden_calls_or_imports( ], ids=lambda x: x if isinstance(x, str) and len(x) < 40 else "case", ) -def test_lint_scanner_catches_known_violation_shapes( - snippet: str, expected_substring: str -) -> None: +def test_lint_scanner_catches_known_violation_shapes(snippet: str, expected_substring: str) -> None: """The scanner itself has fingers: each forbidden shape must be detected. This is the negative-control test for the lint. Without it, a refactor @@ -1189,9 +1161,7 @@ def test_allowlist_entry_matches_real_surface(exc: AllowedException) -> None: quadruple match restricts each entry to a single call site. """ path = SCANNER_DIR / exc.relative_path - assert path.exists(), ( - f"allowlisted file does not exist: {exc.relative_path}" - ) + assert path.exists(), f"allowlisted file does not exist: {exc.relative_path}" source = path.read_text(encoding="utf-8") violations = _scan_source(source, path) same_surface = [v for v in violations if v.surface == exc.surface] @@ -1258,12 +1228,8 @@ def test_no_unallowlisted_forbidden_surface_in_scanner() -> None: for path in _scanner_sources(): rel = str(path.relative_to(SCANNER_DIR)) for violation in _scan_source(path.read_text(encoding="utf-8"), path): - if not _violation_allowed( - rel, violation.surface, violation.line, violation.snippet - ): - unallowed.append( - (rel, violation.line, violation.surface, violation.snippet) - ) + if not _violation_allowed(rel, violation.surface, violation.line, violation.snippet): + unallowed.append((rel, violation.line, violation.surface, violation.snippet)) assert unallowed == [], ( "Scanner has forbidden surfaces with no allowlist entry. " "Add an ALLOWED_EXCEPTIONS entry with prose rationale, or " @@ -1295,9 +1261,7 @@ def test_allowed_exceptions_pin_subprocess_run_per_call_site() -> None: for exc in ALLOWED_EXCEPTIONS: by_file_surface.setdefault((exc.relative_path, exc.surface), []).append(exc) - triggers_subprocess_run = by_file_surface.get( - ("triggers.py", "attr_call:subprocess.run"), [] - ) + triggers_subprocess_run = by_file_surface.get(("triggers.py", "attr_call:subprocess.run"), []) assert len(triggers_subprocess_run) == 3, ( f"Expected 3 distinct AllowedException entries for " f"triggers.py subprocess.run calls (one per call site at " diff --git a/tests/test_agent_handoff.py b/tests/test_agent_handoff.py index 8f78303c..e10053d6 100644 --- a/tests/test_agent_handoff.py +++ b/tests/test_agent_handoff.py @@ -32,7 +32,7 @@ def _verifier_payload() -> dict: human_review_required=True, ) return { - "verifier_schema_version": "0.4", + "verifier_schema_version": "0.5", "workspace": "/tmp/repo", "config": "shipgate.yaml", "execution": "succeeded", @@ -138,7 +138,7 @@ def test_agent_handoff_projects_verifier_report_and_verify_run() -> None: }, ) - assert handoff.schema_version == "shipgate.agent_handoff/v4" + assert handoff.schema_version == "shipgate.agent_handoff/v5" assert handoff.gate.decision == "blocked" assert handoff.gate.merge_verdict == "blocked" assert handoff.gate.ci_would_fail is True @@ -154,7 +154,7 @@ def test_agent_handoff_projects_verifier_report_and_verify_run() -> None: def test_agent_handoff_schema_validates_sample_projection() -> None: - schema = json.loads((ROOT / "docs" / "agent-handoff-schema.v4.json").read_text()) + schema = json.loads((ROOT / "docs" / "agent-handoff-schema.v5.json").read_text()) payload = build_agent_handoff(verifier=_verifier_payload()).model_dump(mode="json") Draft202012Validator(schema).validate(payload) @@ -220,7 +220,7 @@ def test_agent_handoff_cli_rerenders_existing_artifacts(tmp_path: Path) -> None: emitted = json.loads(result.output) written = json.loads(out_path.read_text(encoding="utf-8")) assert emitted == written - assert emitted["schema_version"] == "shipgate.agent_handoff/v4" + assert emitted["schema_version"] == "shipgate.agent_handoff/v5" assert emitted["gate"]["decision"] == "blocked" diff --git a/tests/test_agent_instructions_apply.py b/tests/test_agent_instructions_apply.py index 26ed6bc3..ed786c8d 100644 --- a/tests/test_agent_instructions_apply.py +++ b/tests/test_agent_instructions_apply.py @@ -189,33 +189,36 @@ def test_claude_command_current_file_matches_renderer() -> None: def test_local_contract_renderer_has_required_fields() -> None: payload = json.loads(render_local_contract_file()) - assert payload["schema_version"] == "5" - assert payload["contract_version"] == "16" + assert payload["schema_version"] == "6" + assert payload["contract_version"] == "17" assert "verify_local" not in payload["primary_commands"] assert payload["primary_commands"]["verify_pr"].startswith("agents-shipgate verify") assert payload["commands"]["verify_local"].startswith("agents-shipgate verify") assert payload["primary_commands"]["host_audit"].startswith("shipgate audit --host") - assert payload["agent_handoff_schema_version"] == "shipgate.agent_handoff/v4" + assert payload["agent_handoff_schema_version"] == "shipgate.agent_handoff/v5" assert payload["agent_handoff_artifact"] == "agents-shipgate-reports/agent-handoff.json" - assert payload["attestation_schema_version"] == "0.4" - assert payload["registry_schema_version"] == "0.3" - assert payload["org_evidence_bundle_schema_version"] == ("shipgate.org_evidence_bundle/v1") - assert payload["agent_boundary_result_schema_version"] == ( - "shipgate.agent_boundary_result/v1" - ) + assert payload["attestation_schema_version"] == "0.5" + assert payload["registry_schema_version"] == "0.4" + assert payload["org_evidence_bundle_schema_version"] == ("shipgate.org_evidence_bundle/v2") + assert payload["agent_boundary_result_schema_version"] == ("shipgate.agent_boundary_result/v1") assert payload["host_grants_inventory_schema_version"] == "0.2" assert payload["host_grants_baseline_schema_version"] == "0.2" assert payload["host_grants_drift_schema_version"] == "0.2" assert payload["trigger_catalog_schema_version"] == "0.2" assert payload["gating_signal"] == "release_decision.decision" assert payload["default_paths"]["local_contract"] == ".shipgate/agent-contract.json" - assert payload["verifier_read_order"][:6] == [ + assert payload["verifier_read_order"] == [ "control.state", "execution", "merge_verdict", "applicability", "can_merge_without_human", "control.next_action", + "fix_task", + "capability_review.top_changes", + "release_decision.decision", + "request_id", + "decision_id", ] diff --git a/tests/test_agent_instructions_renderers.py b/tests/test_agent_instructions_renderers.py index bc18d06d..e994c3c4 100644 --- a/tests/test_agent_instructions_renderers.py +++ b/tests/test_agent_instructions_renderers.py @@ -45,10 +45,10 @@ REPO_ROOT = Path(__file__).resolve().parent.parent EXPECTED_CLAUDE_CODE_SKILL_RENDER_SHA256 = { ".claude/skills/agents-shipgate/SKILL.md": ( - "02e780f5a1506d948e4c1d77f6ee4c6b4193227a4fd2ced081847d1fb2e5fbd0" + "bc5cd31a5c4d4f6a1ebf6a04db3f80480e7cc5f9ab2b7a6f7e3f62e8ddfc3937" ), ".claude/skills/agents-shipgate/ci-recipes/advisory-pr-comment.yml": ( - "44648891d77a12f562913e4402c08bca4be6cff4aa8c69b7950ea97e7d5cc17b" + "7fcf37dacee5cc64263d21c6be3a5c7e99b4e6fb45d96b3dda7bdd0603aff6cd" ), ".claude/skills/agents-shipgate/prompts/add-shipgate-to-repo.md": ( "53296f41b7c2bc8538555a4361707de8b990748b7a5d80ae4ce066af83af8fa7" @@ -80,7 +80,7 @@ } EXPECTED_CODEX_SKILL_RENDER_SHA256 = { ".agents/skills/agents-shipgate/SKILL.md": ( - "ad6ca3c53872f1d7e2d8a42794a766f51f0c50a8b4599bb3b76ddd2e31260af1" + "4cbd6a9b978bb142908b8601f52fa1b8fe6a0aa89ecd05eec28f3b00f470ff04" ), ".agents/skills/agents-shipgate/agents/openai.yaml": ( "aa511e933ff663dcd1e0d2af3da2a7101206ce2bb1bb98c4dae801bb3f4e42ef" @@ -92,7 +92,7 @@ "97a8eb98fc560405c690581ac5542b2f94783480f2266af36f566ae12600e2cb" ), ".agents/skills/agents-shipgate/references/report-reading.md": ( - "b652d59a846ccf131df71cf284e10f2e5a72c6c1da6d5454067f1a6a457452d5" + "a5dd0eec215e973c403a22bdbbf0eca76a1796e20be5ae9623829c9d11d2a8dc" ), } @@ -160,31 +160,29 @@ def test_committed_claude_command_matches_renderer() -> None: def test_local_contract_renderer_exposes_agent_operational_fields() -> None: payload = json.loads(render_local_contract_file()) - assert payload["schema_version"] == "5" + assert payload["schema_version"] == "6" assert payload["agents_shipgate_version"] - assert payload["contract_version"] == "16" + assert payload["contract_version"] == "17" assert payload["minimum_control_contract_version"] == "14" assert payload["primary_commands"]["verify_pr"].startswith("agents-shipgate verify") assert payload["primary_commands"]["host_audit"].startswith("shipgate audit --host") assert "verify_local" not in payload["primary_commands"] assert payload["commands"]["verify_local"].startswith("agents-shipgate verify") - assert payload["verifier_schema_version"] == "0.4" - assert payload["verify_run_schema_version"] == "shipgate.verify_run/v2" - assert payload["agent_handoff_schema_version"] == "shipgate.agent_handoff/v4" - assert payload["agent_handoff_schema_path"] == "docs/agent-handoff-schema.v4.json" + assert payload["verifier_schema_version"] == "0.5" + assert payload["verify_run_schema_version"] == "shipgate.verify_run/v3" + assert payload["agent_handoff_schema_version"] == "shipgate.agent_handoff/v5" + assert payload["agent_handoff_schema_path"] == "docs/agent-handoff-schema.v5.json" assert payload["agent_handoff_artifact"] == "agents-shipgate-reports/agent-handoff.json" assert payload["codex_boundary_result_schema_version"] == "shipgate.codex_boundary_result/v2" - assert payload["agent_boundary_result_schema_version"] == ( - "shipgate.agent_boundary_result/v1" - ) + assert payload["agent_boundary_result_schema_version"] == ("shipgate.agent_boundary_result/v1") assert payload["agent_boundary_result_schema_path"] == ( "docs/agent-boundary-result-schema.v1.json" ) assert payload["agent_result_schema_version"] == "agent_result_v2" assert payload["agent_result_schema_path"] == "docs/agent-result-schema.v2.json" - assert payload["attestation_schema_version"] == "0.4" - assert payload["registry_schema_version"] == "0.3" - assert payload["org_evidence_bundle_schema_version"] == ("shipgate.org_evidence_bundle/v1") + assert payload["attestation_schema_version"] == "0.5" + assert payload["registry_schema_version"] == "0.4" + assert payload["org_evidence_bundle_schema_version"] == ("shipgate.org_evidence_bundle/v2") assert payload["host_grants_inventory_schema_version"] == "0.2" assert payload["host_grants_baseline_schema_version"] == "0.2" assert payload["host_grants_drift_schema_version"] == "0.2" @@ -211,6 +209,9 @@ def test_local_contract_renderer_exposes_agent_operational_fields() -> None: assert payload["artifacts"]["verifier"] == "agents-shipgate-reports/verifier.json" assert payload["artifacts"]["verify_run"] == "agents-shipgate-reports/verify-run.json" assert payload["agent_read_order"] == [ + "verification-receipt.json", + "verification-receipt.json.request_id", + "verification-receipt.json.receipt_id", "agent-handoff.json", "agent-handoff.json.control.state", "verifier.json.control.state", diff --git a/tests/test_agent_mode.py b/tests/test_agent_mode.py index de49c716..431bdca4 100644 --- a/tests/test_agent_mode.py +++ b/tests/test_agent_mode.py @@ -220,7 +220,7 @@ def test_verify_json_shortcut_prints_verifier_artifact(tmp_path: Path) -> None: assert result.exit_code == 0, result.output payload = json.loads(result.output) - assert payload["verifier_schema_version"] == "0.4" + assert payload["verifier_schema_version"] == "0.5" assert payload["merge_verdict"] == "insufficient_evidence" assert payload["can_merge_without_human"] is False assert payload["control"]["state"] == "human_review_required" @@ -230,7 +230,7 @@ def test_verify_json_shortcut_prints_verifier_artifact(tmp_path: Path) -> None: handoff_path = repo / "agents-shipgate-reports" / "agent-handoff.json" assert handoff_path.is_file() handoff = json.loads(handoff_path.read_text(encoding="utf-8")) - assert handoff["schema_version"] == "shipgate.agent_handoff/v4" + assert handoff["schema_version"] == "shipgate.agent_handoff/v5" assert handoff["operation"] == "verify_pr" assert not (repo / "agents-shipgate-reports" / "agent-result.json").exists() @@ -253,7 +253,7 @@ def test_verify_preview_writes_agent_handoff(tmp_path: Path) -> None: handoff = json.loads( (repo / "agents-shipgate-reports" / "agent-handoff.json").read_text(encoding="utf-8") ) - assert handoff["schema_version"] == "shipgate.agent_handoff/v4" + assert handoff["schema_version"] == "shipgate.agent_handoff/v5" assert handoff["operation"] == "verify_preview" assert handoff["gate"]["decision"] is None assert handoff["control"]["state"] == "agent_action_required" @@ -269,7 +269,7 @@ def test_verify_format_json_still_prints_full_verifier_artifact( assert result.exit_code == 0, result.output payload = json.loads(result.output) - assert payload["verifier_schema_version"] == "0.4" + assert payload["verifier_schema_version"] == "0.5" assert payload["execution"] == "succeeded" assert payload["head_status"] == "succeeded" assert payload["trigger"]["run_shipgate"] is True @@ -286,7 +286,7 @@ def test_verify_agent_environment_defaults_to_verifier_json( assert result.exit_code == 0, result.output payload = json.loads(result.output) - assert payload["verifier_schema_version"] == "0.4" + assert payload["verifier_schema_version"] == "0.5" assert payload["merge_verdict"] == "insufficient_evidence" diff --git a/tests/test_attest.py b/tests/test_attest.py index 642904e0..9c1a2980 100644 --- a/tests/test_attest.py +++ b/tests/test_attest.py @@ -13,9 +13,7 @@ runner = CliRunner() REPO_ROOT = Path(__file__).resolve().parent.parent -SCHEMA = json.loads( - (REPO_ROOT / "docs/attestation-schema.v0.4.json").read_text(encoding="utf-8") -) +SCHEMA = json.loads((REPO_ROOT / "docs/attestation-schema.v0.5.json").read_text(encoding="utf-8")) def _verifier_payload() -> dict: @@ -45,9 +43,7 @@ def _verifier_payload() -> dict: "report_json": "agents-shipgate-reports/report.json", "pr_comment": "agents-shipgate-reports/pr-comment.md", "capability_lock": "agents-shipgate-reports/capabilities.lock.json", - "capability_lock_diff_json": ( - "agents-shipgate-reports/capability-lock-diff.json" - ), + "capability_lock_diff_json": ("agents-shipgate-reports/capability-lock-diff.json"), }, } @@ -86,7 +82,7 @@ def test_build_attestation_core_fields() -> None: redacted=True, report=_report_payload(), ) - assert att["attestation_schema_version"] == "0.4" + assert att["attestation_schema_version"] == "0.5" assert att["run_id"] is None assert att["verify_run_sha256"] is None assert att["event_time"] is None @@ -193,9 +189,9 @@ def test_human_ack_and_policy_hash_from_report() -> None: ], } expected = hashlib.sha256( - json.dumps( - report["effective_policy"], sort_keys=True, separators=(",", ":") - ).encode("utf-8") + json.dumps(report["effective_policy"], sort_keys=True, separators=(",", ":")).encode( + "utf-8" + ) ).hexdigest() assert att["policy_snapshot_sha256"] == expected @@ -278,9 +274,7 @@ def test_artifact_hashes_resolve_siblings_of_verifier(tmp_path: Path) -> None: "pr_comment": "/somewhere/else/pr-comment.md", "missing": "/somewhere/else/nope.json", } - att = build_attestation_payload( - verifier, source=tmp_path / "verifier.json", redacted=True - ) + att = build_attestation_payload(verifier, source=tmp_path / "verifier.json", redacted=True) assert att["artifact_sha256"]["pr_comment"] == hashlib.sha256(b"hello").hexdigest() assert att["artifact_sha256"]["report_json"] == hashlib.sha256(b"{}").hexdigest() assert "missing" not in att["artifact_sha256"] # absent files are skipped @@ -333,9 +327,7 @@ def test_capability_artifact_bindings_resolve_siblings(tmp_path: Path) -> None: "capability_count": 2, } assert att["capability_diff"]["path"] == "capability-lock-diff.json" - assert att["capability_diff"]["sha256"] == hashlib.sha256( - diff_text.encode("utf-8") - ).hexdigest() + assert att["capability_diff"]["sha256"] == hashlib.sha256(diff_text.encode("utf-8")).hexdigest() assert att["capability_diff"]["capability_lock_diff_schema_version"] == "0.4" assert att["capability_diff"]["base_semantic_capability_set_hash"] == "sem_base" assert att["capability_diff"]["head_semantic_capability_set_hash"] == "sem_head" @@ -358,12 +350,8 @@ def test_attestation_validates_against_schema(report) -> None: def test_attest_cli_writes_json_and_enriches_from_sibling_report(tmp_path: Path) -> None: - (tmp_path / "verifier.json").write_text( - json.dumps(_verifier_payload()), encoding="utf-8" - ) - (tmp_path / "report.json").write_text( - json.dumps(_report_payload()), encoding="utf-8" - ) + (tmp_path / "verifier.json").write_text(json.dumps(_verifier_payload()), encoding="utf-8") + (tmp_path / "report.json").write_text(json.dumps(_report_payload()), encoding="utf-8") out = tmp_path / "attestation.json" result = runner.invoke( @@ -384,12 +372,8 @@ def test_attest_cli_writes_json_and_enriches_from_sibling_report(tmp_path: Path) def test_attest_cli_reads_org_config_and_github_actions_env( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - (tmp_path / "verifier.json").write_text( - json.dumps(_verifier_payload()), encoding="utf-8" - ) - (tmp_path / "report.json").write_text( - json.dumps(_report_payload()), encoding="utf-8" - ) + (tmp_path / "verifier.json").write_text(json.dumps(_verifier_payload()), encoding="utf-8") + (tmp_path / "report.json").write_text(json.dumps(_report_payload()), encoding="utf-8") (tmp_path / "shipgate.yaml").write_text( """ version: "0.1" @@ -470,9 +454,7 @@ def test_attest_cli_reads_org_config_and_github_actions_env( def test_attest_cli_out_without_json_prints_written_message(tmp_path: Path) -> None: - (tmp_path / "verifier.json").write_text( - json.dumps(_verifier_payload()), encoding="utf-8" - ) + (tmp_path / "verifier.json").write_text(json.dumps(_verifier_payload()), encoding="utf-8") out = tmp_path / "attestation.json" result = runner.invoke( app, ["attest", "--from", str(tmp_path / "verifier.json"), "--out", str(out)] diff --git a/tests/test_capability_domain.py b/tests/test_capability_domain.py index 4bb79ac1..364bfb64 100644 --- a/tests/test_capability_domain.py +++ b/tests/test_capability_domain.py @@ -120,9 +120,7 @@ def test_scope_and_broad_scope_facts_reuse_scope_parser() -> None: assert fact.identity.scope == ("stripe:*", "stripe:refunds:write") assert fact.authority.scopes == ("stripe:*", "stripe:refunds:write") assert fact.authority.broad_scopes == tuple( - scope - for scope in fact.authority.scopes - if Scope.parse(scope).is_broad() + scope for scope in fact.authority.scopes if Scope.parse(scope).is_broad() ) @@ -389,4 +387,4 @@ def test_building_capability_facts_does_not_change_action_fact_output() -> None: def test_capability_substrate_uses_current_report_schema_version() -> None: - assert ReadinessReport.model_fields["report_schema_version"].default == "0.33" + assert ReadinessReport.model_fields["report_schema_version"].default == "0.34" diff --git a/tests/test_cli.py b/tests/test_cli.py index 862ae609..ec8ebdce 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -53,6 +53,10 @@ REGISTRY_SCHEMA_VERSION, RELEASE_DECISIONS, TRIGGER_CATALOG_SCHEMA_VERSION, + VERIFICATION_ARTIFACT_MANIFEST_SCHEMA_VERSION, + VERIFICATION_PLAN_SCHEMA_VERSION, + VERIFICATION_RECEIPT_SCHEMA_VERSION, + VERIFICATION_UNIT_RESULT_SCHEMA_VERSION, VERIFIER_READ_ORDER, VERIFY_RUN_SCHEMA_VERSION, ) @@ -259,6 +263,10 @@ def test_cli_contract_json_outputs_runtime_contract(): "packet_schema_version", "verifier_schema_version", "verify_run_schema_version", + "verification_plan_schema_version", + "verification_unit_result_schema_version", + "verification_artifact_manifest_schema_version", + "verification_receipt_schema_version", "agent_handoff_schema_version", "agent_handoff_schema_path", "agent_handoff_artifact", @@ -310,6 +318,12 @@ def test_cli_contract_json_outputs_runtime_contract(): VerifierArtifact.model_fields["verifier_schema_version"].default ), "verify_run_schema_version": VERIFY_RUN_SCHEMA_VERSION, + "verification_plan_schema_version": VERIFICATION_PLAN_SCHEMA_VERSION, + "verification_unit_result_schema_version": (VERIFICATION_UNIT_RESULT_SCHEMA_VERSION), + "verification_artifact_manifest_schema_version": ( + VERIFICATION_ARTIFACT_MANIFEST_SCHEMA_VERSION + ), + "verification_receipt_schema_version": VERIFICATION_RECEIPT_SCHEMA_VERSION, "agent_handoff_schema_version": AGENT_HANDOFF_SCHEMA_VERSION, "agent_handoff_schema_path": AGENT_HANDOFF_SCHEMA_PATH, "agent_handoff_artifact": ARTIFACTS["agent_handoff"], @@ -333,8 +347,7 @@ def test_cli_contract_json_outputs_runtime_contract(): "trigger_catalog_schema_version": TRIGGER_CATALOG_SCHEMA_VERSION, "deprecated_surfaces": { "codex-boundary-json": ( - "Deprecated compatibility projection through 0.16.x; " - "use agent-boundary-json." + "Deprecated compatibility projection through 0.16.x; use agent-boundary-json." ) }, "external_integration_surfaces": list(EXTERNAL_INTEGRATION_SURFACES), diff --git a/tests/test_conductor.py b/tests/test_conductor.py index 70798671..7ac41187 100644 --- a/tests/test_conductor.py +++ b/tests/test_conductor.py @@ -55,7 +55,7 @@ def test_conductor_static_mcp_call_and_human_checkpoint(tmp_path): ci_mode="advisory", ) - assert report.report_schema_version == "0.33" + assert report.report_schema_version == "0.34" surface = report.frameworks["conductor"] assert surface["workflow_count"] == 1 assert surface["mcp_call_task_count"] == 1 @@ -157,12 +157,7 @@ def test_conductor_human_checkpoint_does_not_cross_sibling_branch(tmp_path): _write_manifest(project, "workflow.json") payload = inspect_sources(config_path=project / "shipgate.yaml") - assert ( - payload["frameworks"]["conductor"][ - "structurally_checkpointed_mcp_call_count" - ] - == 0 - ) + assert payload["frameworks"]["conductor"]["structurally_checkpointed_mcp_call_count"] == 0 def test_conductor_flat_arguments_compatibility_and_unsupported_capability(tmp_path): @@ -210,9 +205,7 @@ def test_conductor_detect_and_init_positive_and_negative_controls(tmp_path): ) detected = detect_workspace(positive) assert any(item.type == "conductor" for item in detected.frameworks) - assert detected.suggested_sources == [ - {"type": "conductor", "path": "workflows/agent.json"} - ] + assert detected.suggested_sources == [{"type": "conductor", "path": "workflows/agent.json"}] rendered = render_auto_manifest(positive, detected) assert "type: conductor" in rendered assert "path: workflows/agent.json" in rendered @@ -276,7 +269,7 @@ def test_conductor_multiple_sources_optional_failure_and_bulk_array(tmp_path): _write_text(project / "bulk.json", json.dumps([first, second])) _write_text( project / "shipgate.yaml", - '''version: "0.1" + """version: "0.1" project: name: conductor-multi agent: @@ -293,7 +286,7 @@ def test_conductor_multiple_sources_optional_failure_and_bulk_array(tmp_path): - id: bulk type: conductor path: bulk.json -''', +""", ) payload = inspect_sources(config_path=project / "shipgate.yaml") @@ -305,9 +298,7 @@ def test_conductor_multiple_sources_optional_failure_and_bulk_array(tmp_path): def test_report_schema_v032_pins_conductor_summary_fields(): - schema = json.loads( - Path("docs/report-schema.v0.32.json").read_text(encoding="utf-8") - ) + schema = json.loads(Path("docs/report-schema.v0.32.json").read_text(encoding="utf-8")) conductor = schema["properties"]["frameworks"]["properties"]["conductor"] assert set(conductor["required"]) == { "workflow_file_count", @@ -418,7 +409,7 @@ def _write_workflow(path, tasks, *, schema=2): def _write_manifest(project, workflow_path): _write_text( project / "shipgate.yaml", - f'''version: "0.1" + f"""version: "0.1" project: name: conductor-test agent: @@ -431,7 +422,7 @@ def _write_manifest(project, workflow_path): - id: conductor type: conductor path: {workflow_path} -''', +""", ) diff --git a/tests/test_evidence_packet.py b/tests/test_evidence_packet.py index b43682f9..fd42e117 100644 --- a/tests/test_evidence_packet.py +++ b/tests/test_evidence_packet.py @@ -148,7 +148,7 @@ def test_packet_emits_alongside_report_by_default(tmp_path): out, packet = _scan_with_packet(tmp_path) for name in ("packet.md", "packet.json", "packet.html"): assert (out / name).exists(), name - assert packet.packet_schema_version == "0.11" + assert packet.packet_schema_version == "0.12" def test_packet_v07_keeps_capability_refs_on_release_items(): @@ -939,7 +939,7 @@ def test_load_packet_json_upgrades_v02_hitl_fields(tmp_path): upgraded = load_packet_json(payload) - assert upgraded.packet_schema_version == "0.11" + assert upgraded.packet_schema_version == "0.12" assert upgraded.evidence_matrix.notes assert upgraded.action_surface_diff.status == "not_declared" assert upgraded.action_surface_diff.enabled is False @@ -962,7 +962,7 @@ def test_load_packet_json_upgrades_v01_to_v05(tmp_path): upgraded = load_packet_json(payload) - assert upgraded.packet_schema_version == "0.11" + assert upgraded.packet_schema_version == "0.12" assert upgraded.evidence_matrix.notes assert upgraded.tool_surface_diff.status == "not_declared" assert upgraded.tool_surface_diff.enabled is False @@ -983,7 +983,7 @@ def test_load_packet_json_upgrades_v04_action_surface_section(tmp_path): upgraded = load_packet_json(payload) - assert upgraded.packet_schema_version == "0.11" + assert upgraded.packet_schema_version == "0.12" assert upgraded.action_surface_diff.status == "not_declared" assert upgraded.action_surface_diff.enabled is False assert upgraded.evidence_matrix.notes @@ -997,7 +997,7 @@ def test_load_packet_json_upgrades_v05_evidence_matrix(tmp_path): upgraded = load_packet_json(payload) - assert upgraded.packet_schema_version == "0.11" + assert upgraded.packet_schema_version == "0.12" assert len(upgraded.evidence_matrix.rows) == 13 assert any("older packet schema" in note for note in upgraded.evidence_matrix.notes) @@ -1030,15 +1030,13 @@ def test_legacy_passed_packet_downgrades_to_actionable_insufficient_evidence( "review_items": [], } ) - release["fail_policy"].update( - {"ci_mode": ci_mode, "would_fail_ci": False, "exit_code": 0} - ) + release["fail_policy"].update({"ci_mode": ci_mode, "would_fail_ci": False, "exit_code": 0}) release["evidence_coverage"].pop("semantic_coverage", None) release["evidence_coverage"]["evidence_gaps"] = [] upgraded = load_packet_json(payload) - assert upgraded.packet_schema_version == "0.11" + assert upgraded.packet_schema_version == "0.12" assert upgraded.release_decision.decision == "insufficient_evidence" assert upgraded.release_decision.verdict == "INSUFFICIENT EVIDENCE" assert "not a v0.8 safety statement" in upgraded.release_decision.reason @@ -1274,7 +1272,7 @@ def test_evidence_packet_writes_packet_json_when_format_includes_json(tmp_path): # The written packet.json must round-trip. payload = (target / "packet.json").read_text(encoding="utf-8") reloaded = load_packet_json(payload) - assert reloaded.packet_schema_version == "0.11" + assert reloaded.packet_schema_version == "0.12" def test_evidence_packet_pdf_only_exits_zero_when_weasyprint_missing(tmp_path, monkeypatch): @@ -1401,7 +1399,7 @@ def test_evidence_packet_cli_round_trips(tmp_path): ) assert result.exit_code == 0, result.output payload = json.loads(result.output) - assert payload["packet_schema_version"] == "0.11" + assert payload["packet_schema_version"] == "0.12" assert payload["run_id"] == packet.run_id diff --git a/tests/test_governance_benchmark.py b/tests/test_governance_benchmark.py index 46ae0434..8ded6fae 100644 --- a/tests/test_governance_benchmark.py +++ b/tests/test_governance_benchmark.py @@ -164,9 +164,7 @@ def test_unknown_case_id_raises_config_error(tmp_path: Path) -> None: def test_capability_expectation_failure_exits_one(tmp_path: Path) -> None: data = yaml.safe_load(CATALOG_PATH.read_text(encoding="utf-8")) - data["cases"] = [ - case for case in data["cases"] if case["id"] == "mcp-safe-read-tool-added" - ] + data["cases"] = [case for case in data["cases"] if case["id"] == "mcp-safe-read-tool-added"] data["cases"][0]["capability_expectations"][0]["tool_name"] = "support.nope" catalog = GovernanceBenchmarkCatalogV1.model_validate(data) @@ -227,7 +225,7 @@ def test_governance_benchmark_schemas_validate_catalog_and_result(tmp_path: Path def test_governance_benchmark_preserves_public_schema_boundaries() -> None: - assert ReadinessReport.model_fields["report_schema_version"].default == "0.33" + assert ReadinessReport.model_fields["report_schema_version"].default == "0.34" assert CAPABILITY_LOCK_SCHEMA_VERSION == "0.6" assert CAPABILITY_LOCK_DIFF_SCHEMA_VERSION == "0.7" assert GOVERNANCE_BENCHMARK_RESULT_SCHEMA_VERSION == "0.2" diff --git a/tests/test_local_contract.py b/tests/test_local_contract.py index f101ea7e..5d0507c7 100644 --- a/tests/test_local_contract.py +++ b/tests/test_local_contract.py @@ -29,6 +29,10 @@ def test_local_agent_contract_is_minimal_agent_operational_payload() -> None: "gating_signal", "verifier_schema_version", "verify_run_schema_version", + "verification_plan_schema_version", + "verification_unit_result_schema_version", + "verification_artifact_manifest_schema_version", + "verification_receipt_schema_version", "agent_handoff_schema_version", "agent_handoff_schema_path", "agent_handoff_artifact", @@ -85,29 +89,35 @@ def test_local_agent_contract_is_minimal_agent_operational_payload() -> None: assert payload["artifacts"]["verify_run"] == "agents-shipgate-reports/verify-run.json" assert payload["artifacts"]["agent_handoff"] == "agents-shipgate-reports/agent-handoff.json" assert payload["agent_read_order"] == [ + "verification-receipt.json", + "verification-receipt.json.request_id", + "verification-receipt.json.receipt_id", "agent-handoff.json", "agent-handoff.json.control.state", "verifier.json.control.state", "verify-run.json", "report.json.release_decision.decision", ] - assert payload["verifier_read_order"][0] == "control.state" + assert payload["verifier_read_order"][:3] == [ + "control.state", + "execution", + "merge_verdict", + ] + assert payload["verifier_read_order"][-2:] == ["request_id", "decision_id"] assert payload["gating_signal"] == GATING_SIGNAL - assert payload["verifier_schema_version"] == "0.4" - assert payload["verify_run_schema_version"] == "shipgate.verify_run/v2" - assert payload["agent_handoff_schema_version"] == "shipgate.agent_handoff/v4" - assert payload["agent_handoff_schema_path"] == "docs/agent-handoff-schema.v4.json" + assert payload["verifier_schema_version"] == "0.5" + assert payload["verify_run_schema_version"] == "shipgate.verify_run/v3" + assert payload["agent_handoff_schema_version"] == "shipgate.agent_handoff/v5" + assert payload["agent_handoff_schema_path"] == "docs/agent-handoff-schema.v5.json" assert payload["agent_handoff_artifact"] == "agents-shipgate-reports/agent-handoff.json" assert payload["codex_boundary_result_schema_version"] == "shipgate.codex_boundary_result/v2" - assert payload["agent_boundary_result_schema_version"] == ( - "shipgate.agent_boundary_result/v1" - ) + assert payload["agent_boundary_result_schema_version"] == ("shipgate.agent_boundary_result/v1") assert payload["agent_boundary_result_schema_path"] == ( "docs/agent-boundary-result-schema.v1.json" ) - assert payload["attestation_schema_version"] == "0.4" - assert payload["registry_schema_version"] == "0.3" - assert payload["org_evidence_bundle_schema_version"] == ("shipgate.org_evidence_bundle/v1") + assert payload["attestation_schema_version"] == "0.5" + assert payload["registry_schema_version"] == "0.4" + assert payload["org_evidence_bundle_schema_version"] == ("shipgate.org_evidence_bundle/v2") assert payload["host_grants_inventory_schema_version"] == "0.2" assert payload["host_grants_baseline_schema_version"] == "0.2" assert payload["host_grants_drift_schema_version"] == "0.2" diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index 0c59668b..e7e53b8f 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -205,7 +205,7 @@ def test_mcp_handoff_handler_is_read_only(tmp_path: Path) -> None: payload = shipgate_handoff(verifier_path=str(output_dir / "verifier.json")) - assert payload["schema_version"] == "shipgate.agent_handoff/v4" + assert payload["schema_version"] == "shipgate.agent_handoff/v5" assert payload["gate"]["merge_verdict"] == "mergeable" assert payload["control"]["state"] == "complete" assert _snapshot(tmp_path) == before diff --git a/tests/test_org_governance.py b/tests/test_org_governance.py index 958910b1..5c8d51b9 100644 --- a/tests/test_org_governance.py +++ b/tests/test_org_governance.py @@ -67,9 +67,7 @@ def test_manifest_organization_parses_and_validates_structure(tmp_path: Path) -> assert manifest.organization is not None assert manifest.organization.id == "acme" - assert manifest.organization.teams["agent-platform"].reviewers == [ - "@acme/agent-platform" - ] + assert manifest.organization.teams["agent-platform"].reviewers == ["@acme/agent-platform"] assert manifest.organization.exception_policy.max_age_days == 180 @@ -141,9 +139,7 @@ def test_org_status_reports_verified_policy_pack_and_registry(tmp_path: Path) -> def test_org_status_exit_20_for_unpinned_sourced_policy_pack(tmp_path: Path) -> None: - (tmp_path / "org-pack.yaml").write_text( - "name: Org Pack\nrules: []\n", encoding="utf-8" - ) + (tmp_path / "org-pack.yaml").write_text("name: Org Pack\nrules: []\n", encoding="utf-8") _write_minimal_manifest( tmp_path, """ @@ -522,18 +518,14 @@ def test_org_bundle_projects_platform_artifacts_without_second_gate( ) verifier_payload = json.loads((reports / "verifier.json").read_text(encoding="utf-8")) report_payload = json.loads((reports / "report.json").read_text(encoding="utf-8")) - verify_run_payload = json.loads( - (reports / "verify-run.json").read_text(encoding="utf-8") - ) + verify_run_payload = json.loads((reports / "verify-run.json").read_text(encoding="utf-8")) attestation_payload = build_attestation_payload( verifier_payload, source=reports / "verifier.json", redacted=True, report=report_payload, verify_run=verify_run_payload, - verify_run_sha256=hashlib.sha256( - (reports / "verify-run.json").read_bytes() - ).hexdigest(), + verify_run_sha256=hashlib.sha256((reports / "verify-run.json").read_bytes()).hexdigest(), org_context={ "org_id": "acme", "repo": "org/support", @@ -541,11 +533,14 @@ def test_org_bundle_projects_platform_artifacts_without_second_gate( "tier": "production", }, ) - attestation_rendered = json.dumps( - attestation_payload, - indent=2, - sort_keys=True, - ) + "\n" + attestation_rendered = ( + json.dumps( + attestation_payload, + indent=2, + sort_keys=True, + ) + + "\n" + ) (reports / "attestation.json").write_text(attestation_rendered, encoding="utf-8") attestation_sha256 = hashlib.sha256(attestation_rendered.encode("utf-8")).hexdigest() out = reports / "org-evidence-bundle.json" @@ -570,9 +565,7 @@ def test_org_bundle_projects_platform_artifacts_without_second_gate( assert result.exit_code == 0, result.output payload = json.loads(result.output) assert payload == json.loads(out.read_text(encoding="utf-8")) - assert payload["org_evidence_bundle_schema_version"] == ( - "shipgate.org_evidence_bundle/v1" - ) + assert payload["org_evidence_bundle_schema_version"] == ("shipgate.org_evidence_bundle/v2") assert payload["gating_signal"] == "release_decision.decision" assert payload["attestation"]["run_id"] == "sha256:" + "a" * 64 assert payload["registry_row"]["repo"] == "org/support" @@ -698,7 +691,7 @@ def test_org_bundle_accepts_v03_attestation_file(tmp_path: Path) -> None: assert result.exit_code == 0, result.output payload = json.loads(result.output) - assert payload["attestation"]["attestation_schema_version"] == "0.4" + assert payload["attestation"]["attestation_schema_version"] == "0.5" assert payload["attestation"]["run_id"] is None assert payload["attestation"]["policy_packs"] == [] diff --git a/tests/test_p0_verification_identity_canaries.py b/tests/test_p0_verification_identity_canaries.py new file mode 100644 index 00000000..05aa9263 --- /dev/null +++ b/tests/test_p0_verification_identity_canaries.py @@ -0,0 +1,221 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import pytest +from pydantic import ValidationError + +from agents_shipgate.core.verification_identity import ( + build_terminal_receipt, + build_unit_result, + validate_receipt_artifacts, +) +from agents_shipgate.schemas.verification_identity import ( + VerificationArtifactRef, + VerificationBlob, + VerificationEngineRequirement, + VerificationGitSubject, + VerificationInputSet, + VerificationPlan, + VerificationReceipt, + VerificationSubject, + VerificationTask, + content_id, +) + + +def _plan() -> VerificationPlan: + git = VerificationGitSubject( + repository_id="https://example.test/org/repo.git", + base_ref="origin/main", + base_commit_sha="a" * 40, + base_tree_sha="b" * 40, + head_ref="HEAD", + source_head_commit_sha="c" * 40, + head_commit_sha="d" * 40, + head_tree_sha="e" * 40, + merge_base_sha="a" * 40, + snapshot_kind="committed_tree", + ) + subject = VerificationSubject(subject_id=content_id(git), git=git) + config = VerificationBlob( + path="shipgate.yaml", + sha256="sha256:" + "1" * 64, + size_bytes=12, + source="git_blob", + ) + diff = VerificationBlob( + path="verification-input.diff", + sha256="sha256:" + "2" * 64, + size_bytes=0, + source="generated", + ) + inputs_payload = { + "evaluation_date": "2026-07-13", + "config": config, + "diff": diff, + "baseline": None, + "diff_from": None, + "policy_packs": [], + "tool_sources": [], + "changed_paths": [], + "changed_files": [], + "options": {"ci_mode": "advisory"}, + } + inputs = VerificationInputSet( + input_set_id=content_id( + { + key: value.model_dump(mode="json") if hasattr(value, "model_dump") else value + for key, value in inputs_payload.items() + } + ), + **inputs_payload, + ) + engine_payload = { + "version": "0.16.0b6", + "python_implementation": "CPython", + "python_version": "3.12.0", + "platform": "linux", + "engine_distribution_sha256": "sha256:" + "8" * 64, + "dependency_set_sha256": "sha256:" + "3" * 64, + "adapter_set_sha256": "sha256:" + "4" * 64, + "plugin_set_sha256": "sha256:" + "5" * 64, + "policy_catalog_sha256": "sha256:" + "6" * 64, + } + engine = VerificationEngineRequirement( + engine_requirement_id=content_id({"package": "agents-shipgate", **engine_payload}), + **engine_payload, + ) + task_payload = { + "kind": "evaluate", + "shard": 0, + "shard_count": 1, + "input_paths": ["shipgate.yaml"], + } + task = VerificationTask(task_id=content_id(task_payload), **task_payload) + request_payload = { + "subject_id": subject.subject_id, + "input_set_id": inputs.input_set_id, + "engine_requirement_id": engine.engine_requirement_id, + "task_ids": [task.task_id], + } + return VerificationPlan( + request_id=content_id(request_payload), + subject=subject, + inputs=inputs, + engine=engine, + tasks=[task], + ) + + +@pytest.mark.parametrize("case", range(64), ids=lambda case: f"identity-canary-{case:02d}") +def test_64_identity_canaries_fail_closed(case: int, tmp_path: Path) -> None: + plan = _plan() + bucket = case // 16 + variant = case % 16 + if bucket == 0: + payload = plan.subject.model_dump(mode="json") + payload["git"]["repository_id"] = f"https://attacker.test/{variant}" + with pytest.raises(ValidationError): + VerificationSubject.model_validate(payload) + elif bucket == 1: + payload = plan.inputs.model_dump(mode="json") + payload["options"][f"tamper_{variant}"] = True + with pytest.raises(ValidationError): + VerificationInputSet.model_validate(payload) + elif bucket == 2: + payload = plan.engine.model_dump(mode="json") + payload["version"] = f"tampered-{variant}" + with pytest.raises(ValidationError): + VerificationEngineRequirement.model_validate(payload) + else: + artifact = tmp_path / f"artifact-{variant}.json" + artifact.write_text(json.dumps({"case": variant}), encoding="utf-8") + unit = build_unit_result(plan=plan, normalized_ir={"case": variant}) + _manifest, receipt = build_terminal_receipt( + plan=plan, + unit_results=[unit], + decision="passed", + merge_verdict="mergeable", + can_merge_without_human=True, + artifact_paths={"report": artifact}, + ) + payload = receipt.model_dump(mode="json") + payload["merge_verdict"] = f"tampered-{variant}" + with pytest.raises(ValidationError): + VerificationReceipt.model_validate(payload) + + +def test_worker_ir_cannot_smuggle_a_release_decision() -> None: + with pytest.raises(ValidationError, match="never release decisions"): + build_unit_result(plan=_plan(), normalized_ir={"decision": "passed"}) + + +def test_engine_distribution_digest_is_part_of_engine_identity() -> None: + payload = _plan().engine.model_dump(mode="json") + payload["engine_distribution_sha256"] = "sha256:" + "9" * 64 + with pytest.raises(ValidationError): + VerificationEngineRequirement.model_validate(payload) + + +def test_worker_ir_cannot_nest_a_release_decision() -> None: + with pytest.raises(ValidationError, match="never release decisions"): + build_unit_result( + plan=_plan(), + normalized_ir={"payload": [{"control": {"state": "complete"}}]}, + ) + + +def test_attempt_id_is_not_authoritative(tmp_path: Path) -> None: + plan = _plan() + unit = build_unit_result(plan=plan, normalized_ir={"catalog": []}) + artifact = tmp_path / "report.json" + artifact.write_text("{}", encoding="utf-8") + _, first = build_terminal_receipt( + plan=plan, + unit_results=[unit], + decision="passed", + merge_verdict="mergeable", + can_merge_without_human=True, + artifact_paths={"report": artifact}, + attempt_id="attempt:one", + ) + _, second = build_terminal_receipt( + plan=plan, + unit_results=[unit], + decision="passed", + merge_verdict="mergeable", + can_merge_without_human=True, + artifact_paths={"report": artifact}, + attempt_id="attempt:two", + ) + assert first.receipt_id == second.receipt_id + + +def test_receipt_artifact_paths_cannot_escape_the_bundle() -> None: + with pytest.raises(ValidationError, match="portable relative paths"): + VerificationArtifactRef( + path="../outside.json", + sha256="sha256:" + "1" * 64, + size_bytes=1, + media_type="application/json", + ) + + +def test_receipt_artifact_tamper_is_rejected(tmp_path: Path) -> None: + plan = _plan() + unit = build_unit_result(plan=plan, normalized_ir={"catalog": []}) + artifact = tmp_path / "report.json" + artifact.write_text("{}", encoding="utf-8") + _, receipt = build_terminal_receipt( + plan=plan, + unit_results=[unit], + decision="passed", + merge_verdict="mergeable", + can_merge_without_human=True, + artifact_paths={"report": artifact}, + ) + artifact.write_text('{"tampered":true}', encoding="utf-8") + with pytest.raises(ValueError, match="hash does not match"): + validate_receipt_artifacts(receipt, root=tmp_path) diff --git a/tests/test_public_surface_contract.py b/tests/test_public_surface_contract.py index ca5b2174..a4626cad 100644 --- a/tests/test_public_surface_contract.py +++ b/tests/test_public_surface_contract.py @@ -340,6 +340,10 @@ def test_well_known_metadata_lists_packet_outputs(): "feedback_json", "attestation_json", "org_evidence_bundle_json", + "verification_plan_json", + "verification_unit_result_json", + "verification_artifact_manifest_json", + "verification_receipt_json", "host_grants_json", "org_status_json", ): @@ -370,6 +374,21 @@ def test_well_known_metadata_lists_packet_outputs(): assert data.get("agent_result_control_fields") == contract["agent_result_control_fields"] assert data.get("verifier_schema_version") == contract["verifier_schema_version"] assert data.get("verify_run_schema_version") == contract["verify_run_schema_version"] + assert ( + data.get("verification_plan_schema_version") == contract["verification_plan_schema_version"] + ) + assert ( + data.get("verification_unit_result_schema_version") + == contract["verification_unit_result_schema_version"] + ) + assert ( + data.get("verification_artifact_manifest_schema_version") + == contract["verification_artifact_manifest_schema_version"] + ) + assert ( + data.get("verification_receipt_schema_version") + == contract["verification_receipt_schema_version"] + ) assert data.get("agent_handoff_schema_version") == contract["agent_handoff_schema_version"] assert data.get("agent_handoff_schema_path") == contract["agent_handoff_schema_path"] assert data.get("agent_handoff_artifact") == contract["agent_handoff_artifact"] @@ -403,6 +422,7 @@ def test_well_known_metadata_lists_packet_outputs(): assert artifacts.get("local_contract") == (".shipgate/agent-contract.json") assert artifacts.get("verify_run") == contract["artifacts"]["verify_run"] assert artifacts.get("agent_handoff") == contract["artifacts"]["agent_handoff"] + assert artifacts.get("verification_receipt") == (contract["artifacts"]["verification_receipt"]) report_url = schemas.get("report", "") assert CURRENT_REPORT_SCHEMA in report_url, ( f".well-known schemas.report must point to {CURRENT_REPORT_SCHEMA}; got {report_url!r}." @@ -452,7 +472,7 @@ def test_well_known_metadata_lists_packet_outputs(): f"registry schema; got {registry_url!r}." ) bundle_url = schemas.get("org_evidence_bundle", "") - assert "org-evidence-bundle-schema.v1.json" in bundle_url + assert "org-evidence-bundle-schema.v2.json" in bundle_url assert data.get("org_evidence_bundle_schema_version") == (ORG_EVIDENCE_BUNDLE_SCHEMA_VERSION) host_grants_url = schemas.get("host_grants_inventory", "") assert ( @@ -478,8 +498,18 @@ def test_well_known_metadata_lists_packet_outputs(): ".well-known schemas.governance_benchmark_result must point to the " f"current result schema; got {benchmark_result_url!r}." ) - assert "verify_run" in schemas and "verify-run-schema.v2.json" in schemas["verify_run"] - assert "agent_handoff" in schemas and "agent-handoff-schema.v4.json" in schemas["agent_handoff"] + assert "verify_run" in schemas and "verify-run-schema.v3.json" in schemas["verify_run"] + assert "verification_plan" in schemas + assert "verification-plan-schema.v1.json" in schemas["verification_plan"] + assert "verification_unit_result" in schemas + assert "verification-unit-result-schema.v1.json" in schemas["verification_unit_result"] + assert "verification_artifact_manifest" in schemas + assert ( + "verification-artifact-manifest-schema.v1.json" in schemas["verification_artifact_manifest"] + ) + assert "verification_receipt" in schemas + assert "verification-receipt-schema.v1.json" in schemas["verification_receipt"] + assert "agent_handoff" in schemas and "agent-handoff-schema.v5.json" in schemas["agent_handoff"] assert ( "codex_boundary_result" in schemas and "codex-boundary-result-schema.v2.json" in schemas["codex_boundary_result"] @@ -564,9 +594,9 @@ def test_architecture_doc_contract_stamp_matches_runtime(): "`agents-shipgate contract --json`: runtime contract `N`, " "report schema `vX.Y`, packet schema `vX.Y`.'" ) - assert stamp.group("date") == "2026-07-09", ( + assert stamp.group("date") == "2026-07-13", ( "docs/architecture.md contract-check date must stay pinned to " - "2026-07-09 until a deliberate architecture-doc refresh moves it." + "2026-07-13 until a deliberate architecture-doc refresh moves it." ) assert stamp.group("contract") == CONTRACT_VERSION, ( f"docs/architecture.md says runtime contract " @@ -1127,12 +1157,10 @@ def test_triggers_evaluator_smoke(): ): host_change = evaluate(paths=[host_path]) assert host_change["run_shipgate"] is True, ( - f"Host boundary change {host_path!r} must trigger Shipgate; " - f"got {host_change!r}." + f"Host boundary change {host_path!r} must trigger Shipgate; got {host_change!r}." ) assert any( - match["surface_class"] == "host_boundary" - for match in host_change["matched_rules"] + match["surface_class"] == "host_boundary" for match in host_change["matched_rules"] ) conductor_change = evaluate( @@ -1141,8 +1169,7 @@ def test_triggers_evaluator_smoke(): ) assert conductor_change["run_shipgate"] is True assert any( - match["surface_class"] == "capability" - for match in conductor_change["matched_rules"] + match["surface_class"] == "capability" for match in conductor_change["matched_rules"] ) decorator = evaluate( paths=["agent.py"], @@ -2473,7 +2500,7 @@ def test_no_singular_underscore_module_name(relpath): # Read-first artifact vs the runtime contract's agent_read_order # --------------------------------------------------------------------------- -READ_FIRST_PATTERN = re.compile(r"[Rr]ead\s+`([^`]+)`\s+first") +READ_FIRST_PATTERN = re.compile(r"(?:[Rr]ead|[Vv]alidate)\s+`([^`]+)`\s+first") # Prose surfaces that tell a coding agent which verify artifact to read # first. .well-known/agents-shipgate.json carries the machine-readable # agent_read_order (validated against the contract elsewhere in this @@ -2489,16 +2516,16 @@ def test_no_singular_underscore_module_name(relpath): @pytest.mark.parametrize("relpath", READ_FIRST_SURFACES) def test_read_first_instructions_match_contract_agent_read_order(relpath): """Every 'Read `` first' instruction must name the first - artifact in the runtime contract's agent_read_order (agent-handoff.json - since contract v7), optionally with a reports-dir prefix or a field - path suffix. README shipped both 'read verifier.json first' and - 'read agent-handoff.json first' simultaneously until v0.14.x; this + artifact in the runtime contract's agent_read_order + (verification-receipt.json since contract v17), optionally with a + reports-dir prefix or a field path suffix. README shipped contradictory + first-artifact instructions before this invariant; this pins the prose surfaces to the contract so the contradiction cannot return. verifier.json stays the authoritative controller substrate — mentioning it is fine, telling an agent to read it *first* is not.""" contract = build_contract_payload().model_dump(mode="json") first_artifact = contract["agent_read_order"][0] - assert first_artifact == "agent-handoff.json", ( + assert first_artifact == "verification-receipt.json", ( "contract agent_read_order[0] changed; sweep the read-first " "prose on READ_FIRST_SURFACES, then update this pin." ) diff --git a/tests/test_registry_ledger.py b/tests/test_registry_ledger.py index 3481a2c9..3b2b89ca 100644 --- a/tests/test_registry_ledger.py +++ b/tests/test_registry_ledger.py @@ -212,7 +212,7 @@ def test_ingest_writes_v03_rows_and_uses_attestation_org_repo(tmp_path: Path) -> assert result.exit_code == 0, result.output row = json.loads(ledger.read_text(encoding="utf-8")) - assert row["registry_schema_version"] == "0.3" + assert row["registry_schema_version"] == "0.4" assert row["repo"] == "org/from-org" assert row["org_id"] == "acme" assert row["service"] == "support-agent" @@ -245,7 +245,7 @@ def test_bypass_report_excludes_mergeable_and_acknowledged_rows(tmp_path: Path) assert result.exit_code == 0, result.output payload = json.loads(result.output) - assert payload["registry_schema_version"] == "0.3" + assert payload["registry_schema_version"] == "0.4" assert payload["bypass_count"] == 1 assert payload["rows"][0]["repo"] == "org/a" assert payload["rows"][0]["merge_verdict"] == "blocked" @@ -267,9 +267,7 @@ def test_query_reports_malformed_registry_rows(tmp_path: Path) -> None: payload = json.loads(result.output) assert payload["count"] == 1 assert payload["skipped_count"] == 2 - invalid_json_reason = ( - "invalid_json: Expecting property name enclosed in double quotes" - ) + invalid_json_reason = "invalid_json: Expecting property name enclosed in double quotes" assert payload["skipped_rows"] == [ {"line": 2, "reason": invalid_json_reason}, {"line": 3, "reason": "row must be a JSON object"}, @@ -336,7 +334,7 @@ def test_query_filters_org_fields_and_human_ack(tmp_path: Path) -> None: assert result.exit_code == 0, result.output payload = json.loads(result.output) - assert payload["registry_schema_version"] == "0.3" + assert payload["registry_schema_version"] == "0.4" assert payload["count"] == 1 assert payload["rows"][0]["repo"] == "org/support" @@ -359,7 +357,7 @@ def test_registry_summary_and_verify_hash_chain(tmp_path: Path) -> None: ) assert summary.exit_code == 0, summary.output summary_payload = json.loads(summary.output) - assert summary_payload["registry_schema_version"] == "0.3" + assert summary_payload["registry_schema_version"] == "0.4" assert summary_payload["count"] == 2 assert summary_payload["by_repo"] == {"org/support": 2} assert summary_payload["by_merge_verdict"] == {"blocked": 1, "mergeable": 1} @@ -375,7 +373,7 @@ def test_registry_summary_and_verify_hash_chain(tmp_path: Path) -> None: ) assert verify.exit_code == 0, verify.output verify_payload = json.loads(verify.output) - assert verify_payload["registry_schema_version"] == "0.3" + assert verify_payload["registry_schema_version"] == "0.4" assert verify_payload["ok"] is True assert verify_payload["issue_count"] == 0 diff --git a/tests/test_safety_qualification.py b/tests/test_safety_qualification.py index 06f3db85..3ff07ac9 100644 --- a/tests/test_safety_qualification.py +++ b/tests/test_safety_qualification.py @@ -1,5 +1,6 @@ from __future__ import annotations +import hashlib import json import zipfile from pathlib import Path @@ -8,6 +9,11 @@ from agents_shipgate.core.agent_control import derive_agent_control from agents_shipgate.core.errors import ConfigError +from agents_shipgate.core.verification_identity import ( + build_executor, + build_terminal_receipt, + build_unit_result, +) from agents_shipgate.schemas.agent_control import HumanControlAction from agents_shipgate.schemas.disclaimers import STATIC_VERDICT_DISCLAIMER from agents_shipgate.schemas.report import ReadinessReport @@ -23,13 +29,19 @@ compute_frozen_labels_sha256, production_safety_requirements, ) +from agents_shipgate.schemas.verification_identity import ( + VerificationBlob, + VerificationEngineRequirement, + VerificationGitSubject, + VerificationInputSet, + VerificationPlan, + VerificationSubject, + VerificationTask, + content_id, +) from agents_shipgate.schemas.verifier import VerifierArtifact from agents_shipgate.schemas.verify_run import ( - VerifyRunArtifactRef, - VerifyRunInputs, VerifyRunOutcome, - VerifyRunSubject, - VerifyRunTool, build_verify_run_artifact, ) from scripts.run_safety_qualification import ( @@ -43,7 +55,85 @@ ) DECISIONS = ("passed", "review_required", "insufficient_evidence", "blocked") -VERSION = "0.16.0b5" +VERSION = "0.16.0b6" + + +def _verification_plan(case_id: str) -> VerificationPlan: + git = VerificationGitSubject( + repository_id="https://example.test/qualification.git", + base_ref="origin/main", + base_commit_sha="a" * 40, + base_tree_sha=hashlib.sha256(f"base-{case_id}".encode()).hexdigest(), + head_ref="HEAD", + head_commit_sha="b" * 40, + head_tree_sha=hashlib.sha256(f"head-{case_id}".encode()).hexdigest(), + merge_base_sha="a" * 40, + snapshot_kind="committed_tree", + ) + subject = VerificationSubject(subject_id=content_id(git), git=git) + config = VerificationBlob( + path="shipgate.yaml", + sha256="sha256:" + "1" * 64, + size_bytes=12, + source="git_blob", + ) + diff = VerificationBlob( + path="verification-input.diff", + sha256="sha256:" + "2" * 64, + size_bytes=0, + source="generated", + ) + input_payload = { + "evaluation_date": "2026-07-13", + "config": config, + "diff": diff, + "baseline": None, + "diff_from": None, + "policy_packs": [], + "tool_sources": [], + "changed_paths": [], + "changed_files": [], + "options": {"ci_mode": "advisory"}, + } + inputs = VerificationInputSet( + input_set_id=content_id( + { + key: value.model_dump(mode="json") if hasattr(value, "model_dump") else value + for key, value in input_payload.items() + } + ), + **input_payload, + ) + engine_payload = { + "version": VERSION, + "python_implementation": "CPython", + "python_version": "3.12.0", + "platform": "test", + "engine_distribution_sha256": "sha256:" + "8" * 64, + "dependency_set_sha256": "sha256:" + "3" * 64, + "adapter_set_sha256": "sha256:" + "4" * 64, + "plugin_set_sha256": "sha256:" + "5" * 64, + "policy_catalog_sha256": "sha256:" + "6" * 64, + } + engine = VerificationEngineRequirement( + engine_requirement_id=content_id({"package": "agents-shipgate", **engine_payload}), + **engine_payload, + ) + task_payload = {"kind": "evaluate", "shard": 0, "shard_count": 1, "input_paths": []} + task = VerificationTask(task_id=content_id(task_payload), **task_payload) + request_payload = { + "subject_id": subject.subject_id, + "input_set_id": inputs.input_set_id, + "engine_requirement_id": engine.engine_requirement_id, + "task_ids": [task.task_id], + } + return VerificationPlan( + request_id=content_id(request_payload), + subject=subject, + inputs=inputs, + engine=engine, + tasks=[task], + ) def _human_label(role: str, reviewer: str, decision: str) -> IndependentHumanLabelV1: @@ -106,7 +196,7 @@ def _test_requirements() -> SafetyQualificationRequirementsV1: minimum_blocked_exact=1, minimum_review_exact=1, minimum_insufficient_evidence_exact=1, - required_report_schema_version="0.33", + required_report_schema_version="0.34", ) @@ -118,8 +208,8 @@ def _write_json(path: Path, value: object) -> None: def _write_wheel(path: Path) -> None: with zipfile.ZipFile(path, "w") as archive: archive.writestr( - "agents_shipgate-0.16.0b5.dist-info/METADATA", - "Metadata-Version: 2.4\nName: agents-shipgate\nVersion: 0.16.0b5\n", + "agents_shipgate-0.16.0b6.dist-info/METADATA", + "Metadata-Version: 2.4\nName: agents-shipgate\nVersion: 0.16.0b6\n", ) @@ -163,7 +253,7 @@ def _fixture( actual_overrides: dict[str, str] | None = None, disagreement_case: str | None = None, ) -> tuple[Path, Path, Path, Path]: - wheel = tmp_path / "agents_shipgate-0.16.0b5-py3-none-any.whl" + wheel = tmp_path / "agents_shipgate-0.16.0b6-py3-none-any.whl" _write_wheel(wheel) policy = tmp_path / "qualification-policy.json" _write_json(policy, {"policy": "beta-exact", "version": 1}) @@ -189,12 +279,29 @@ def _fixture( entries: list[SafetyReceiptEntryV1] = [] for case in cases: actual = (actual_overrides or {}).get(case.id, case.expected_decision) + plan = _verification_plan(case.id) + unit = build_unit_result(plan=plan, normalized_ir={"case_id": case.id}) + executor = build_executor(plan.engine) + decision_id = content_id( + { + "request_id": plan.request_id, + "unit_result_ids": [unit.unit_result_id], + "decision": actual, + "merge_verdict": EXPECTED_MERGE_VERDICT[actual], + "can_merge_without_human": actual == "passed", + } + ) artifact_root = tmp_path / "artifacts" / case.id reports = artifact_root / "agents-shipgate-reports" report_path = reports / "report.json" verifier_path = reports / "verifier.json" report = ReadinessReport( run_id=f"run-{case.id}", + request_id=plan.request_id, + subject_id=plan.subject.subject_id, + input_set_id=plan.inputs.input_set_id, + engine_requirement_id=plan.engine.engine_requirement_id, + decision_id=decision_id, project={"name": "qualification-fixture"}, agent={"name": "fixture-agent"}, environment={"name": "test"}, @@ -250,12 +357,18 @@ def _fixture( ) verifier = VerifierArtifact( workspace=".", + request_id=plan.request_id, + subject_id=plan.subject.subject_id, + input_set_id=plan.inputs.input_set_id, + engine_requirement_id=plan.engine.engine_requirement_id, + executor_id=executor.executor_id, + decision_id=decision_id, config="shipgate.yaml", base_ref="origin/main", head_ref="HEAD", base_status="succeeded", - base_tree_sha=f"base-{case.id}", - head_tree_sha=f"head-{case.id}", + base_tree_sha=hashlib.sha256(f"base-{case.id}".encode()).hexdigest(), + head_tree_sha=hashlib.sha256(f"head-{case.id}".encode()).hexdigest(), execution="succeeded", head_status="succeeded", head_exit_code=0, @@ -272,21 +385,10 @@ def _fixture( verifier_path, verifier.model_dump(mode="json"), ) - tool = VerifyRunTool(version=VERSION) - subject = VerifyRunSubject( - config="shipgate.yaml", - base_ref="origin/main", - head_ref="HEAD", - base_tree_sha=f"base-{case.id}", - head_tree_sha=f"head-{case.id}", - ) - receipt = build_verify_run_artifact( - tool=tool, - subject=subject, - inputs=VerifyRunInputs( - config_sha256="config-" + case.id, - ci_mode="advisory", - ), + verify_run = build_verify_run_artifact( + plan=plan, + executor=executor, + unit_result_ids=[unit.unit_result_id], outcome=VerifyRunOutcome( exit_code=0, base_status="succeeded", @@ -297,15 +399,20 @@ def _fixture( can_merge_without_human=actual == "passed", control=verifier.control, ), - artifacts={ - "report_json": VerifyRunArtifactRef( - path="agents-shipgate-reports/report.json", - sha256=sha256_file(report_path), - ), - "verifier_json": VerifyRunArtifactRef( - path="agents-shipgate-reports/verifier.json", - sha256=sha256_file(verifier_path), - ), + artifacts={}, + ) + verify_run_path = reports / "verify-run.json" + _write_json(verify_run_path, verify_run.model_dump(mode="json")) + _manifest, receipt = build_terminal_receipt( + plan=plan, + unit_results=[unit], + decision=actual, + merge_verdict=EXPECTED_MERGE_VERDICT[actual], + can_merge_without_human=actual == "passed", + artifact_paths={ + "report_json": report_path, + "verifier_json": verifier_path, + "verify_run_json": verify_run_path, }, ) receipt_path = tmp_path / "receipts" / f"{case.id}.json" @@ -314,9 +421,13 @@ def _fixture( SafetyReceiptEntryV1( case_id=case.id, receipt_path=receipt_path.relative_to(tmp_path).as_posix(), - artifact_root=artifact_root.relative_to(tmp_path).as_posix(), + artifact_root=reports.relative_to(tmp_path).as_posix(), receipt_sha256=sha256_file(receipt_path), - run_id=receipt.run_id, + run_id=receipt.request_id, + request_id=receipt.request_id, + receipt_id=receipt.receipt_id, + decision_id=receipt.decision_id, + artifact_set_id=receipt.artifact_set_id, ) ) diff --git a/tests/test_safety_qualification_release.py b/tests/test_safety_qualification_release.py index a9c221a1..f799f45d 100644 --- a/tests/test_safety_qualification_release.py +++ b/tests/test_safety_qualification_release.py @@ -26,15 +26,15 @@ verify_release_qualification, ) -VERSION = "0.16.0b5" +VERSION = "0.16.0b6" REPO_ROOT = Path(__file__).resolve().parent.parent def _write_wheel(path: Path) -> None: with zipfile.ZipFile(path, "w") as archive: archive.writestr( - "agents_shipgate-0.16.0b5.dist-info/METADATA", - "Metadata-Version: 2.4\nName: agents-shipgate\nVersion: 0.16.0b5\n", + "agents_shipgate-0.16.0b6.dist-info/METADATA", + "Metadata-Version: 2.4\nName: agents-shipgate\nVersion: 0.16.0b6\n", ) @@ -177,7 +177,7 @@ def _production_result(wheel: Path) -> SafetyQualificationResultV1: def _fixture(tmp_path: Path) -> tuple[Path, Path]: tmp_path.mkdir(parents=True, exist_ok=True) - wheel = tmp_path / "agents_shipgate-0.16.0b5-py3-none-any.whl" + wheel = tmp_path / "agents_shipgate-0.16.0b6-py3-none-any.whl" _write_wheel(wheel) qualification = tmp_path / "safety-qualification.json" qualification.write_text( diff --git a/tests/test_schema_boundaries.py b/tests/test_schema_boundaries.py index 7ed8acb8..9c9d07e2 100644 --- a/tests/test_schema_boundaries.py +++ b/tests/test_schema_boundaries.py @@ -256,11 +256,16 @@ def test_representative_schema_payloads_keep_wire_fields() -> None: tool_surface=ToolSurfaceSummary(total_tools=0, high_risk_tools=0), ) report_payload = report_json_payload(report) - assert report_payload["report_schema_version"] == "0.33" + assert report_payload["report_schema_version"] == "0.34" assert list(report_payload) == [ "schema_version", "report_schema_version", "run_id", + "request_id", + "subject_id", + "input_set_id", + "engine_requirement_id", + "decision_id", "manifest_dir", "project", "agent", @@ -338,7 +343,7 @@ def test_representative_schema_payloads_keep_wire_fields() -> None: not_proven=NotProvenSection(headline="not proven"), ) packet_payload = serialize_packet_json(packet) - assert packet_payload["packet_schema_version"] == "0.11" + assert packet_payload["packet_schema_version"] == "0.12" assert "generated_at" not in packet_payload assert "action_surface_diff" in packet_payload assert report_payload["capability_runtime_evidence"]["enabled"] is False @@ -387,6 +392,12 @@ def test_representative_schema_payloads_keep_wire_fields() -> None: packet_schema_version="0.9", verifier_schema_version="0.2", verify_run_schema_version="shipgate.verify_run/v1", + verification_plan_schema_version="shipgate.verification_plan/v1", + verification_unit_result_schema_version="shipgate.verification_unit_result/v1", + verification_artifact_manifest_schema_version=( + "shipgate.verification_artifact_manifest/v1" + ), + verification_receipt_schema_version="shipgate.verification_receipt/v1", agent_handoff_schema_version="shipgate.agent_handoff/v2", agent_handoff_schema_path="docs/agent-handoff-schema.v2.json", agent_handoff_artifact="agents-shipgate-reports/agent-handoff.json", @@ -399,9 +410,9 @@ def test_representative_schema_payloads_keep_wire_fields() -> None: capability_standard_version="0.3", governance_benchmark_catalog_schema_version="0.2", governance_benchmark_result_schema_version="0.2", - attestation_schema_version="0.4", - registry_schema_version="0.3", - org_evidence_bundle_schema_version="shipgate.org_evidence_bundle/v1", + attestation_schema_version="0.5", + registry_schema_version="0.4", + org_evidence_bundle_schema_version="shipgate.org_evidence_bundle/v2", host_grants_inventory_schema_version="0.1", host_grants_baseline_schema_version="0.1", host_grants_drift_schema_version="0.1", @@ -445,6 +456,12 @@ def test_representative_schema_payloads_keep_wire_fields() -> None: "packet_schema_version": "0.9", "verifier_schema_version": "0.2", "verify_run_schema_version": "shipgate.verify_run/v1", + "verification_plan_schema_version": "shipgate.verification_plan/v1", + "verification_unit_result_schema_version": "shipgate.verification_unit_result/v1", + "verification_artifact_manifest_schema_version": ( + "shipgate.verification_artifact_manifest/v1" + ), + "verification_receipt_schema_version": "shipgate.verification_receipt/v1", "agent_handoff_schema_version": "shipgate.agent_handoff/v2", "agent_handoff_schema_path": "docs/agent-handoff-schema.v2.json", "agent_handoff_artifact": "agents-shipgate-reports/agent-handoff.json", @@ -457,9 +474,9 @@ def test_representative_schema_payloads_keep_wire_fields() -> None: "capability_standard_version": "0.3", "governance_benchmark_catalog_schema_version": "0.2", "governance_benchmark_result_schema_version": "0.2", - "attestation_schema_version": "0.4", - "registry_schema_version": "0.3", - "org_evidence_bundle_schema_version": "shipgate.org_evidence_bundle/v1", + "attestation_schema_version": "0.5", + "registry_schema_version": "0.4", + "org_evidence_bundle_schema_version": "shipgate.org_evidence_bundle/v2", "host_grants_inventory_schema_version": "0.1", "host_grants_baseline_schema_version": "0.1", "host_grants_drift_schema_version": "0.1", diff --git a/tests/test_v07_metadata_roundtrip.py b/tests/test_v07_metadata_roundtrip.py index 8cd97307..23395a54 100644 --- a/tests/test_v07_metadata_roundtrip.py +++ b/tests/test_v07_metadata_roundtrip.py @@ -240,7 +240,7 @@ def test_package_version_is_current_in_tree_runtime(): """Guard against bumping schemas while leaving package metadata behind.""" import agents_shipgate - assert agents_shipgate.__version__ == "0.16.0b5", ( + assert agents_shipgate.__version__ == "0.16.0b6", ( f"package version is {agents_shipgate.__version__!r}; " - "expected 0.16.0b5 for the current in-tree runtime" + "expected 0.16.0b6 for the current in-tree runtime" ) diff --git a/tests/test_verification_git_snapshot.py b/tests/test_verification_git_snapshot.py new file mode 100644 index 00000000..a748ada5 --- /dev/null +++ b/tests/test_verification_git_snapshot.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import subprocess +from pathlib import Path + +import pytest + +from agents_shipgate.cli.verify.git import archive_tree, repository_identity +from agents_shipgate.core.errors import ConfigError + + +def _git(root: Path, *args: str) -> None: + subprocess.run(["git", "-C", str(root), *args], check=True, capture_output=True) + + +def _repo(tmp_path: Path) -> Path: + root = tmp_path / "repo" + root.mkdir() + _git(root, "init") + _git(root, "config", "user.email", "test@example.test") + _git(root, "config", "user.name", "Test") + return root + + +def test_exact_snapshot_does_not_honor_export_ignore(tmp_path: Path) -> None: + root = _repo(tmp_path) + (root / ".gitattributes").write_text("secret.yaml export-ignore\n", encoding="utf-8") + (root / "secret.yaml").write_text("policy: bound\n", encoding="utf-8") + _git(root, "add", ".") + _git(root, "commit", "-m", "fixture") + out = tmp_path / "snapshot" + archive_tree(root, "HEAD", out) + assert (out / "secret.yaml").read_text(encoding="utf-8") == "policy: bound\n" + + +def test_exact_snapshot_rejects_git_symlink_bindings(tmp_path: Path) -> None: + root = _repo(tmp_path) + (root / "target").write_text("value", encoding="utf-8") + (root / "link").symlink_to("target") + _git(root, "add", ".") + _git(root, "commit", "-m", "fixture") + with pytest.raises(ConfigError, match="unsupported external binding"): + archive_tree(root, "HEAD", tmp_path / "snapshot") + + +def test_repository_identity_normalizes_ssh_and_https_remotes(tmp_path: Path) -> None: + root = _repo(tmp_path) + _git(root, "remote", "add", "origin", "git@GitHub.com:ThreeMoonsLab/shipgate.git") + assert repository_identity(root) == "github.com/ThreeMoonsLab/shipgate" + _git( + root, + "remote", + "set-url", + "origin", + "https://token@example.test/org/repo.git?credential=secret", + ) + assert repository_identity(root) == "example.test/org/repo" diff --git a/tests/test_verifier_control_contract.py b/tests/test_verifier_control_contract.py index 419ded98..57ae67da 100644 --- a/tests/test_verifier_control_contract.py +++ b/tests/test_verifier_control_contract.py @@ -10,15 +10,15 @@ from agents_shipgate.core.agent_control import derive_agent_control from agents_shipgate.core.agent_handoff import build_agent_handoff +from agents_shipgate.core.verification_identity import ( + build_executor, + build_unit_result, + build_verification_plan, +) from agents_shipgate.schemas.agent_control import HumanControlAction from agents_shipgate.schemas.disclaimers import STATIC_VERDICT_DISCLAIMER from agents_shipgate.schemas.verifier import VerifierArtifact, map_merge_verdict -from agents_shipgate.schemas.verify_run import ( - VerifyRunInputs, - VerifyRunOutcome, - VerifyRunSubject, - build_verify_run_artifact, -) +from agents_shipgate.schemas.verify_run import VerifyRunOutcome, build_verify_run_artifact ROOT = Path(__file__).resolve().parent.parent @@ -65,11 +65,35 @@ def _passed_verifier() -> VerifierArtifact: ) -def test_control_is_byte_identical_across_verifier_run_and_handoff() -> None: - verifier = _passed_verifier() - run = build_verify_run_artifact( - subject=VerifyRunSubject(config="shipgate.yaml"), - inputs=VerifyRunInputs(config_sha256="sha256:config"), +def _passed_run(verifier: VerifierArtifact): + plan = build_verification_plan( + git_root=ROOT, + input_root=ROOT, + config_path=ROOT / "shipgate.yaml", + config_logical_path="shipgate.yaml", + base_ref=None, + head_ref="HEAD", + archived_head=True, + repository_id="local:test", + base_commit_sha=None, + base_tree_sha=None, + head_commit_sha="a" * 40, + head_tree_sha="b" * 40, + merge_base_sha=None, + changed_files=[], + diff_text="", + baseline_path=None, + diff_from_path=None, + policy_pack_paths=[], + evaluation_date="2026-07-13", + options={"plugins_enabled": False}, + plugins_enabled=False, + ) + unit = build_unit_result(plan=plan, normalized_ir={"test": "control-projection"}) + return build_verify_run_artifact( + plan=plan, + executor=build_executor(plan.engine), + unit_result_ids=[unit.unit_result_id], outcome=VerifyRunOutcome( exit_code=0, base_status="not_requested", @@ -82,6 +106,11 @@ def test_control_is_byte_identical_across_verifier_run_and_handoff() -> None: ), artifacts={}, ) + + +def test_control_is_byte_identical_across_verifier_run_and_handoff() -> None: + verifier = _passed_verifier() + run = _passed_run(verifier) handoff = build_agent_handoff(verifier=verifier, verify_run=run) expected = verifier.control.model_dump_json() @@ -91,21 +120,7 @@ def test_control_is_byte_identical_across_verifier_run_and_handoff() -> None: def test_handoff_rejects_tampered_current_verify_run_outcome() -> None: verifier = _passed_verifier() - run = build_verify_run_artifact( - subject=VerifyRunSubject(config="shipgate.yaml"), - inputs=VerifyRunInputs(config_sha256="sha256:config"), - outcome=VerifyRunOutcome( - exit_code=0, - base_status="not_requested", - execution="succeeded", - applicability="verified", - decision="passed", - merge_verdict="mergeable", - can_merge_without_human=True, - control=verifier.control, - ), - artifacts={}, - ).model_dump(mode="json") + run = _passed_run(verifier).model_dump(mode="json") run["outcome"]["decision"] = "blocked" with pytest.raises(ValidationError): @@ -115,9 +130,9 @@ def test_handoff_rejects_tampered_current_verify_run_outcome() -> None: @pytest.mark.parametrize( ("schema_path", "control_path"), [ - ("docs/verifier-schema.v0.3.json", ("control",)), - ("docs/agent-handoff-schema.v3.json", ("control",)), - ("docs/verify-run-schema.v2.json", ("outcome", "control")), + ("docs/verifier-schema.v0.5.json", ("control",)), + ("docs/agent-handoff-schema.v5.json", ("control",)), + ("docs/verify-run-schema.v3.json", ("outcome", "control")), ], ) def test_generated_public_schemas_reject_contradictory_control( @@ -125,26 +140,12 @@ def test_generated_public_schemas_reject_contradictory_control( control_path: tuple[str, ...], ) -> None: verifier = _passed_verifier() - run = build_verify_run_artifact( - subject=VerifyRunSubject(config="shipgate.yaml"), - inputs=VerifyRunInputs(config_sha256="sha256:config"), - outcome=VerifyRunOutcome( - exit_code=0, - base_status="not_requested", - execution="succeeded", - applicability="verified", - decision="passed", - merge_verdict="mergeable", - can_merge_without_human=True, - control=verifier.control, - ), - artifacts={}, - ) + run = _passed_run(verifier) handoff = build_agent_handoff(verifier=verifier, verify_run=run) payload_by_schema = { - "docs/verifier-schema.v0.3.json": verifier.model_dump(mode="json"), - "docs/agent-handoff-schema.v3.json": handoff.model_dump(mode="json"), - "docs/verify-run-schema.v2.json": run.model_dump(mode="json"), + "docs/verifier-schema.v0.5.json": verifier.model_dump(mode="json"), + "docs/agent-handoff-schema.v5.json": handoff.model_dump(mode="json"), + "docs/verify-run-schema.v3.json": run.model_dump(mode="json"), } payload = deepcopy(payload_by_schema[schema_path]) control = payload diff --git a/tests/test_verify.py b/tests/test_verify.py index 49ac27ba..b1a4aca1 100644 --- a/tests/test_verify.py +++ b/tests/test_verify.py @@ -143,7 +143,8 @@ def test_verify_missing_config_docs_only_diff_fails_closed(tmp_path: Path) -> No "agents-shipgate verify --preview --json" ) assert (out_dir / "verifier.json").is_file() - assert (out_dir / "verify-run.json").is_file() + assert not (out_dir / "verify-run.json").exists() + assert not (out_dir / "verification-receipt.json").exists() assert (out_dir / "agent-handoff.json").is_file() assert (out_dir / "pr-comment.md").is_file() assert not (out_dir / "agent-result.json").exists() @@ -1004,7 +1005,11 @@ def test_verify_base_scan_cache_hit_skips_second_base_scan( assert first.exit_code == 0, first.output assert second.exit_code == 0, second.output - assert json.loads(second.output)["base_status"] == "cache_hit" + # Cache provenance is deliberately excluded from public identity and + # operational projections; cold and warm executions both serialize the + # deterministic state of the base evaluation. + assert json.loads(first.output)["base_status"] == "succeeded" + assert json.loads(second.output)["base_status"] == "succeeded" cache_path = json.loads(second.output)["base_report_json"] assert "agents-shipgate-reports/.cache" not in cache_path assert not (repo / "agents-shipgate-reports" / ".cache").exists() diff --git a/tests/test_verify_orchestrator.py b/tests/test_verify_orchestrator.py index aa91aa08..85e57baf 100644 --- a/tests/test_verify_orchestrator.py +++ b/tests/test_verify_orchestrator.py @@ -1,10 +1,14 @@ from __future__ import annotations +import json import shutil import subprocess from pathlib import Path +from agents_shipgate.cli.verification import assemble, worker from agents_shipgate.cli.verify.orchestrator import run_verify +from agents_shipgate.core.verification_identity import validate_receipt_artifacts +from agents_shipgate.schemas.verification_identity import VerificationReceipt REPO_ROOT = Path(__file__).resolve().parent.parent @@ -69,6 +73,36 @@ def test_verify_threads_changed_files_into_head_scan(tmp_path): assert (out_dir / "capabilities.lock.json").is_file() assert (out_dir / "base.capabilities.lock.json").is_file() assert (out_dir / "capability-lock-diff.json").is_file() + diff_input = out_dir / "verification-input.diff" + assert diff_input.is_file() + distributed_unit = out_dir / "distributed-unit.json" + worker( + plan_path=out_dir / "verification-plan.json", + workspace=repo, + diff_path=diff_input, + out=distributed_unit, + ) + assemble( + plan_path=out_dir / "verification-plan.json", + unit_paths=[distributed_unit], + verifier_path=out_dir / "verifier.json", + artifacts_root=out_dir, + out=out_dir / "verification-receipt.json", + ) + receipt_path = out_dir / "verification-receipt.json" + receipt = VerificationReceipt.model_validate( + json.loads(receipt_path.read_text(encoding="utf-8")) + ) + validate_receipt_artifacts(receipt, root=out_dir) + first_bytes = receipt_path.read_bytes() + assemble( + plan_path=out_dir / "verification-plan.json", + unit_paths=[distributed_unit], + verifier_path=out_dir / "verifier.json", + artifacts_root=out_dir, + out=receipt_path, + ) + assert receipt_path.read_bytes() == first_bytes def test_verify_threads_uncommitted_worktree_files_into_head_scan(tmp_path): @@ -85,6 +119,10 @@ def test_verify_threads_uncommitted_worktree_files_into_head_scan(tmp_path): _git(repo, "commit", "-m", "base") (repo / "AGENTS.md").write_text("uncommitted instructions\n", encoding="utf-8") + (repo / ".claude" / "commands").mkdir(parents=True) + (repo / ".claude" / "commands" / "review.md").write_text("review command\n", encoding="utf-8") + (repo / ".claude-plugin").mkdir() + (repo / ".claude-plugin" / "marketplace.json").write_text("{}\n", encoding="utf-8") _verifier, report, _exit_code = run_verify( workspace=repo, diff --git a/tests/test_verify_run.py b/tests/test_verify_run.py index 34f5f412..f67ce761 100644 --- a/tests/test_verify_run.py +++ b/tests/test_verify_run.py @@ -5,125 +5,204 @@ from agents_shipgate.core.agent_control import derive_agent_control from agents_shipgate.schemas.agent_control import HumanControlAction +from agents_shipgate.schemas.verification_identity import ( + VerificationBlob, + VerificationEngineRequirement, + VerificationExecutor, + VerificationGitSubject, + VerificationInputSet, + VerificationPlan, + VerificationSubject, + VerificationTask, + content_id, +) from agents_shipgate.schemas.verify_run import ( VerifyRunArtifact, - VerifyRunArtifactRef, - VerifyRunInputs, VerifyRunOutcome, - VerifyRunSubject, - VerifyRunTool, build_verify_run_artifact, ) -def test_verify_run_id_excludes_outcome_and_artifact_hashes() -> None: - tool = VerifyRunTool(version="test-version") - subject = VerifyRunSubject( - config="shipgate.yaml", +def _plan(*, no_heuristics: bool = False) -> tuple[VerificationPlan, VerificationExecutor]: + git = VerificationGitSubject( + repository_id="https://example.test/org/repo.git", base_ref="origin/main", + base_commit_sha="a" * 40, + base_tree_sha="b" * 40, head_ref="HEAD", - base_tree_sha="base", - head_tree_sha="head", + head_commit_sha="c" * 40, + head_tree_sha="d" * 40, + merge_base_sha="a" * 40, + snapshot_kind="committed_tree", ) - inputs = VerifyRunInputs( - config_sha256="sha256:config", - baseline_sha256=None, - plugins_enabled=False, - no_heuristics=False, - ci_mode="advisory", + subject = VerificationSubject(subject_id=content_id(git), git=git) + blob = VerificationBlob( + path="shipgate.yaml", + sha256="sha256:" + "1" * 64, + size_bytes=10, + source="git_blob", ) - passed = build_verify_run_artifact( - tool=tool, - subject=subject, - inputs=inputs, - outcome=VerifyRunOutcome( - exit_code=0, - base_status="succeeded", - execution="succeeded", - applicability="verified", - decision="passed", - merge_verdict="mergeable", - can_merge_without_human=True, - control=derive_agent_control(reason="Static verification passed."), + diff = VerificationBlob( + path="verification-input.diff", + sha256="sha256:" + "2" * 64, + size_bytes=0, + source="generated", + ) + input_payload = { + "evaluation_date": "2026-07-13", + "config": blob, + "diff": diff, + "baseline": None, + "diff_from": None, + "policy_packs": [], + "tool_sources": [], + "changed_paths": [], + "changed_files": [], + "options": {"no_heuristics": no_heuristics}, + } + inputs = VerificationInputSet( + input_set_id=content_id( + { + key: value.model_dump(mode="json") if hasattr(value, "model_dump") else value + for key, value in input_payload.items() + } ), - artifacts={ - "verifier_json": VerifyRunArtifactRef( - path="agents-shipgate-reports/verifier.json", - sha256="sha256:verifier-a", - ) - }, + **input_payload, ) - blocked = build_verify_run_artifact( - tool=tool, + engine_payload = { + "version": "test-version", + "python_implementation": "CPython", + "python_version": "3.12.0", + "platform": "linux", + "engine_distribution_sha256": "sha256:" + "8" * 64, + "dependency_set_sha256": "sha256:" + "3" * 64, + "adapter_set_sha256": "sha256:" + "4" * 64, + "plugin_set_sha256": "sha256:" + "5" * 64, + "policy_catalog_sha256": "sha256:" + "6" * 64, + } + engine = VerificationEngineRequirement( + engine_requirement_id=content_id({"package": "agents-shipgate", **engine_payload}), + **engine_payload, + ) + task_payload = {"kind": "evaluate", "shard": 0, "shard_count": 1, "input_paths": []} + task = VerificationTask(task_id=content_id(task_payload), **task_payload) + request_payload = { + "subject_id": subject.subject_id, + "input_set_id": inputs.input_set_id, + "engine_requirement_id": engine.engine_requirement_id, + "task_ids": [task.task_id], + } + plan = VerificationPlan( + request_id=content_id(request_payload), subject=subject, inputs=inputs, - outcome=VerifyRunOutcome( - exit_code=20, - base_status="succeeded", - execution="succeeded", - applicability="verified", - decision="blocked", - merge_verdict="blocked", - can_merge_without_human=False, - control=derive_agent_control( - reason="A blocking policy condition requires a human.", - next_action=HumanControlAction( - kind="stop", - why="A blocking policy condition requires a human.", - ), - human_review_required=True, - ), - ), - artifacts={ - "verifier_json": VerifyRunArtifactRef( - path="agents-shipgate-reports/verifier.json", - sha256="sha256:verifier-b", - ), - "report_json": VerifyRunArtifactRef( - path="agents-shipgate-reports/report.json", - sha256="sha256:report-b", - ), - }, + engine=engine, + tasks=[task], ) + executor_payload = { + "engine_requirement_id": engine.engine_requirement_id, + "runtime_sha256": "sha256:" + "7" * 64, + } + executor = VerificationExecutor( + executor_id=content_id(executor_payload), + **executor_payload, + ) + return plan, executor - assert passed.run_id == blocked.run_id - changed_inputs = build_verify_run_artifact( - tool=tool, - subject=subject, - inputs=inputs.model_copy(update={"no_heuristics": True}), - outcome=passed.outcome, - artifacts=passed.artifacts, +def _passed_outcome() -> VerifyRunOutcome: + return VerifyRunOutcome( + exit_code=0, + base_status="succeeded", + execution="succeeded", + applicability="verified", + decision="passed", + merge_verdict="mergeable", + can_merge_without_human=True, + control=derive_agent_control(reason="Static verification passed."), ) - assert changed_inputs.run_id != passed.run_id -def test_verify_run_rejects_run_id_that_does_not_match_identity() -> None: - artifact = build_verify_run_artifact( - tool=VerifyRunTool(version="test-version"), - subject=VerifyRunSubject(config="shipgate.yaml"), - inputs=VerifyRunInputs(config_sha256="sha256:config"), - outcome=VerifyRunOutcome( - exit_code=0, - base_status="skipped", - execution="skipped", - applicability="not_applicable", - decision=None, - merge_verdict="mergeable", - can_merge_without_human=True, - control=derive_agent_control( - reason="The deterministic trigger found no applicable changes." +def test_run_id_is_exact_request_alias_and_decision_has_separate_identity() -> None: + plan, executor = _plan() + passed = build_verify_run_artifact( + plan=plan, + executor=executor, + unit_result_ids=["sha256:" + "8" * 64], + outcome=_passed_outcome(), + artifacts={}, + ) + blocked_outcome = VerifyRunOutcome( + exit_code=20, + base_status="succeeded", + execution="succeeded", + applicability="verified", + decision="blocked", + merge_verdict="blocked", + can_merge_without_human=False, + control=derive_agent_control( + reason="A blocking policy condition requires a human.", + next_action=HumanControlAction( + kind="stop", why="A blocking policy condition requires a human." ), + human_review_required=True, ), + ) + blocked = build_verify_run_artifact( + plan=plan, + executor=executor, + unit_result_ids=["sha256:" + "8" * 64], + outcome=blocked_outcome, + artifacts={}, + ) + assert passed.run_id == passed.request_id == plan.request_id + assert blocked.request_id == passed.request_id + assert blocked.decision_id != passed.decision_id + + changed_plan, changed_executor = _plan(no_heuristics=True) + changed = build_verify_run_artifact( + plan=changed_plan, + executor=changed_executor, + unit_result_ids=["sha256:" + "8" * 64], + outcome=_passed_outcome(), + artifacts={}, + ) + assert changed.request_id != passed.request_id + + +def test_verify_run_rejects_run_id_that_is_not_request_alias() -> None: + plan, executor = _plan() + artifact = build_verify_run_artifact( + plan=plan, + executor=executor, + unit_result_ids=["sha256:" + "8" * 64], + outcome=_passed_outcome(), artifacts={}, ) payload = artifact.model_dump(mode="json") payload["run_id"] = "sha256:" + "0" * 64 - with pytest.raises(ValidationError): VerifyRunArtifact.model_validate(payload) +@pytest.mark.parametrize( + "unit_result_ids", + [[], ["not-a-content-id"], ["sha256:" + "8" * 64] * 2], +) +def test_verify_run_rejects_missing_malformed_or_duplicate_unit_ids( + unit_result_ids: list[str], +) -> None: + plan, executor = _plan() + with pytest.raises(ValidationError): + build_verify_run_artifact( + plan=plan, + executor=executor, + unit_result_ids=unit_result_ids, + outcome=_passed_outcome(), + artifacts={}, + ) + + def test_verify_run_rejects_noncomplete_control_for_merge_authority() -> None: human = derive_agent_control( reason="Human review is required.", From 5929706c62402d21330ca734be71d458ba048317 Mon Sep 17 00:00:00 2001 From: Pengfei Hu Date: Mon, 13 Jul 2026 23:40:08 -0700 Subject: [PATCH 2/3] Fix clean Action runtime dependency --- pyproject.toml | 1 + tests/test_packaging.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 07a1845e..7eaf221e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ + "packaging>=24,<27", "pydantic>=2.13.4,<3", "pyyaml>=6.0.3,<7", "ruamel.yaml>=0.19.1,<1", diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 0b0c9c33..ad402c4b 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -3,9 +3,11 @@ import subprocess import sys import zipfile +from email import message_from_bytes from pathlib import Path import pytest +from packaging.requirements import Requirement REPO_ROOT = Path(__file__).resolve().parent.parent @@ -43,6 +45,21 @@ def test_wheel_includes_adoption_kits(built_wheel: Path) -> None: assert "agents_shipgate/_meta/claude-command/shipgate.md" in names +def test_wheel_declares_verification_identity_runtime_dependencies( + built_wheel: Path, +) -> None: + with zipfile.ZipFile(built_wheel) as archive: + metadata_path = next( + name for name in archive.namelist() if name.endswith(".dist-info/METADATA") + ) + metadata = message_from_bytes(archive.read(metadata_path)) + requirements = { + Requirement(value).name.lower().replace("_", "-") + for value in metadata.get_all("Requires-Dist", []) + } + assert "packaging" in requirements + + def test_wheel_includes_org_policy_templates(built_wheel: Path) -> None: with zipfile.ZipFile(built_wheel) as archive: names = set(archive.namelist()) From e32b369b04b9f28390c8bf9a2b29a5b2daee58df Mon Sep 17 00:00:00 2001 From: Pengfei Hu Date: Tue, 14 Jul 2026 14:25:57 -0700 Subject: [PATCH 3/3] Prevent backdated commits from extending trust --- CHANGELOG.md | 4 + STABILITY.md | 2 +- docs/verification-reproducibility.md | 18 +++- src/agents_shipgate/core/baseline.py | 4 +- src/agents_shipgate/core/evaluation_clock.py | 26 +++++- .../core/severity_overrides.py | 11 +-- tests/test_baseline_integrity.py | 7 +- .../test_p0_verification_identity_canaries.py | 19 +++++ tests/test_severity_override_floor.py | 43 ++++++++++ tests/test_verify_orchestrator.py | 84 ++++++++++++++++++- 10 files changed, 202 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdad25a5..57f22d5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,10 @@ and records a pull request's source head separately. It exports receipt, request, decision, and artifact-set identities only after validating every terminal artifact hash. +- **Non-forgeable trust decay.** The content-bound commit evaluation date + remains reproducibility provenance, but cannot extend reviewer-owned trust. + Baseline, acknowledgement, and severity-override expiry use the later of that + date and the verifier wall clock, so a forged backdated commit fails closed. - **Evidence-basis policy gate (P0, `0.16.0b5`).** Semantic claims and risk hints now carry a typed evidence basis, stable claim IDs, and derived policy diff --git a/STABILITY.md b/STABILITY.md index 27536bb7..f690707f 100644 --- a/STABILITY.md +++ b/STABILITY.md @@ -605,7 +605,7 @@ In `agents-shipgate-reports/report.json`, the following are guaranteed: - `loaded_policy_packs[].{id, name, version, path, source, sha256, sha256_status, owner, rule_count}` (v0.27+) — deterministic policy-pack distribution and ownership metadata for organization audit. `sha256_status` is `"verified"` when a manifest pin matched and `"unpinned"` otherwise. Hash mismatch still fails closed during pack loading; this metadata never introduces a second release verdict. - `loaded_plugins[].{name, value, distribution, version, check_id}` - `loaded_plugins[].{validation_status, validation_errors, runtime_errors}` (v0.17+ / M5; `dynamic_default_not_supported` added v0.18) — plugin validation provenance, required + present on every entry. `validation_status` is one of `valid | load_failed | bad_signature | bad_metadata | dynamic_default_not_supported | id_collision | bad_floor`; the two error lists are always present and empty for clean plugins. Invalid plugins still appear in this array (with `check_id: null` for entries that failed before metadata parsing), so reviewers can see what was skipped without reading scanner logs. Plugin findings whose `check_id` does not match the declared metadata are dropped at runtime and recorded under `runtime_errors`. `dynamic_default_not_supported` (v0.18+) rejects plugins declaring `AGENTS_SHIPGATE_METADATA.dynamic_default=True` — plugins have no path to wire into `core/dynamic_defaults.py`'s aggregator, so a swing check would never receive a manifest-effective default and would be silently bypassable. -- `policy_audit.severity_overrides_applied[].{check_id, default_severity, applied_severity, manifest_path, reason, tier_crossed, direction, expires}` (v0.17+ / M1) — top-of-report audit envelope for severity overrides applied during scan. Always present on emitted scans (empty when no overrides applied); required + non-nullable on the wire. `direction` is one of `downgrade | upgrade | same`. `tier_crossed=true` indicates the override crossed a severity tier boundary (critical / high / medium-low); tier-crossing downgrades require a matching `checks.acknowledge_overrides` entry, which is reflected in `reason`. `expires` is an ISO-8601 date carried from the matching acknowledgement (or the rich-form override entry); on/past this date the manifest fails to load with exit 2. +- `policy_audit.severity_overrides_applied[].{check_id, default_severity, applied_severity, manifest_path, reason, tier_crossed, direction, expires}` (v0.17+ / M1) — top-of-report audit envelope for severity overrides applied during scan. Always present on emitted scans (empty when no overrides applied); required + non-nullable on the wire. `direction` is one of `downgrade | upgrade | same`. `tier_crossed=true` indicates the override crossed a severity tier boundary (critical / high / medium-low); tier-crossing downgrades require a matching `checks.acknowledge_overrides` entry, which is reflected in `reason`. `expires` is an ISO-8601 date carried from the matching acknowledgement (or the rich-form override entry); on/past this date the manifest fails to load with exit 2. Verify computes the hard-expiry date as the later of its wall-clock date and the content-bound `evaluation_date`; commit timestamps can never extend trust. - `privacy_audit.{enabled, rules_version, sensitive_field_inventory_version, redacted_occurrence_count, redacted_paths, output_surfaces, notes}` (v0.18+) — top-level audit envelope proving the default-on privacy layer ran before public artifacts were emitted. `redacted_paths[]` contains `{path, count, kinds}` aggregate rows only; it never includes raw values or raw-value hashes. Redaction is best-effort pattern/key based and does not claim complete secret-scanner coverage. - `reviewer_summary.{verdict, headline, tool_surface_changes, capability_misalignments, action_surface_changes, evidence_matrix_gaps, severity_overrides_applied, severity_overrides_tier_crossed, privacy_redactions, baseline_integrity_issues, first_recommended_surface}` (v0.20+) — top-level deterministic projection of the reviewer lens surfaces and audit envelopes; the reviewer-side parallel to `agent_summary`. Required + always present on emitted scans (mirroring the `agent_summary` contract). `verdict` mirrors `release_decision.decision` and is added/removed in lockstep with `AgentSummary.verdict` and `ReleaseDecisionStatus`. `first_recommended_surface` is `{kind, name, path, why}` where `kind` ∈ `{release_decision, lens, audit, evidence_matrix}` and `name` ∈ `{tool_surface_diff, capability_intent_diff, action_surface_diff, evidence_matrix, policy_audit, privacy_audit, baseline_integrity, release_decision}`; the pointer is `null` only when verdict is `passed` AND every count above is zero. The priority order encoded by `first_recommended_surface` is documented in [`docs/agent-contract-current.md`](docs/agent-contract-current.md). Same inputs always produce the same output; this block cannot disagree with the underlying lens/audit data. - `heuristics_filter.{enabled, excluded_provenance_kinds, filtered_finding_count, filtered_by_kind}` (v0.21+) — top-level audit envelope describing the `--no-heuristics` CLI filter pass. Required + always present on emitted scans regardless of whether the flag was set (envelope shape is stable). When `enabled` is `False` the count fields are zero and no findings have been mutated by the filter. When `enabled` is `True`, every finding whose `provenance_kind` is in `excluded_provenance_kinds` has been marked `suppressed=True` with `suppression_reason="filtered by --no-heuristics"` BEFORE the release decision is built — those findings remain in `findings[]` for transparency but no longer gate release. `excluded_provenance_kinds` is the stable list `["keyword_heuristic", "regex_heuristic"]` (the only two `ProvenanceKind` values describing token/regex matches; `static_declaration`, `ast_extraction`, `policy_pack`, and `runtime_trace` are never filtered). The filter never un-suppresses a finding; manifest-driven suppression reasons are preserved verbatim when they overlap with the filter (the envelope still counts the overlap so reviewers see the filter's effective scope). diff --git a/docs/verification-reproducibility.md b/docs/verification-reproducibility.md index 089a7674..3ac70f24 100644 --- a/docs/verification-reproducibility.md +++ b/docs/verification-reproducibility.md @@ -117,9 +117,21 @@ cross-executor equivalence qualification that are not present in v1. ## Time and GitHub Actions -Expiry-sensitive baseline and severity-override evaluation uses the plan's -`evaluation_date`, derived from the evaluated head commit unless explicitly -declared during plan preparation. It never depends on a worker's wall clock. +The plan's `evaluation_date` is content-bound provenance, derived from the +evaluated head commit unless explicitly declared during plan preparation. It +is not authority to extend reviewer-owned trust. Baseline exceptions, severity +overrides, and override acknowledgements use the later of `evaluation_date` +and the verifier's wall-clock date. A backdated commit therefore cannot keep an +expired grant active; a future-dated commit can only make the gate more +restrictive. Explicit test-only `today` injection remains available inside the +library, but neither `verify` nor portable workers expose it as a bypass. + +This means hard trust decay is intentionally monotonic rather than timeless: +re-evaluating an old request after an exception expires may produce a stricter +decision. The historical receipt still proves the exact artifacts and decision +assembled at its evaluation; it does not renew expired human consent. Workers +do not evaluate these expiries and their local clocks cannot weaken the main +verifier's result. The GitHub Action evaluates `${{ github.sha }}` by default and separately records `${{ github.event.pull_request.head.sha }}` when present. This prevents diff --git a/src/agents_shipgate/core/baseline.py b/src/agents_shipgate/core/baseline.py index 90cead47..4c4f275c 100644 --- a/src/agents_shipgate/core/baseline.py +++ b/src/agents_shipgate/core/baseline.py @@ -21,7 +21,7 @@ ) from agents_shipgate.core.check_ids import LEGACY_CHECK_ID_ALIASES from agents_shipgate.core.errors import InputParseError -from agents_shipgate.core.evaluation_clock import evaluation_date +from agents_shipgate.core.evaluation_clock import trust_expiry_date from agents_shipgate.core.findings.identity import legacy_policy_routing_fingerprint from agents_shipgate.schemas.baseline import ( BaselineFile, @@ -544,7 +544,7 @@ def verify_baseline( issues.extend(audit_issues) if not any(issue.kind == "invalid_audit_log" for issue in audit_issues): issues.extend(_verify_entry_provenance(baseline, log_path)) - issues.extend(_verify_entry_expiry(baseline, today or evaluation_date())) + issues.extend(_verify_entry_expiry(baseline, today or trust_expiry_date())) issues.extend(_verify_deprecated_check_ids(baseline)) return issues diff --git a/src/agents_shipgate/core/evaluation_clock.py b/src/agents_shipgate/core/evaluation_clock.py index 4e8fdb51..8b0f5e30 100644 --- a/src/agents_shipgate/core/evaluation_clock.py +++ b/src/agents_shipgate/core/evaluation_clock.py @@ -1,4 +1,15 @@ -"""Run-scoped deterministic date used by verification policy evaluation.""" +"""Evaluation and trust-decay clocks used by static verification. + +``evaluation_date`` is content-bound provenance. A verifier may derive it +from the evaluated commit so deterministic, non-security-sensitive evaluation +can be reproduced. + +Hard expiry is different: a commit author controls commit timestamps and must +never be able to extend a human acknowledgement, severity override, or +baseline exception. ``trust_expiry_date`` therefore uses the later of the +content-bound evaluation date and the verifier's wall-clock date. This split +is intentional and security-sensitive. +""" from __future__ import annotations @@ -16,6 +27,17 @@ def evaluation_date() -> date: return _EVALUATION_DATE.get() or date.today() +def trust_expiry_date() -> date: + """Return the conservative date for reviewer-owned trust expiry. + + The value can never be earlier than the verifier's wall clock. A + backdated commit therefore cannot keep an expired trust grant active. A + future evaluation date may expire a grant early, which is conservative. + """ + + return max(evaluation_date(), date.today()) + + @contextmanager def use_evaluation_date(value: date) -> Iterator[None]: token = _EVALUATION_DATE.set(value) @@ -25,4 +47,4 @@ def use_evaluation_date(value: date) -> Iterator[None]: _EVALUATION_DATE.reset(token) -__all__ = ["evaluation_date", "use_evaluation_date"] +__all__ = ["evaluation_date", "trust_expiry_date", "use_evaluation_date"] diff --git a/src/agents_shipgate/core/severity_overrides.py b/src/agents_shipgate/core/severity_overrides.py index 0c945b72..bec4fe4f 100644 --- a/src/agents_shipgate/core/severity_overrides.py +++ b/src/agents_shipgate/core/severity_overrides.py @@ -25,9 +25,10 @@ ``ConfigError`` (exit 2) before the scan completes; there is no warning path. This makes "I'll add an expiry and forget" impossible. -The function set here is pure: no I/O, no environment, no time -indirection beyond the explicit ``today`` parameter (defaulted to -``date.today()`` for production callers, injectable for tests). +The function set here is pure except for its production default clock. The +explicit ``today`` parameter remains injectable for tests. Production callers +use the conservative trust-expiry clock, which is never earlier than the +verifier's wall clock even when a commit carries a backdated timestamp. """ from __future__ import annotations @@ -38,7 +39,7 @@ from agents_shipgate.core.check_ids import expands_to_check_id from agents_shipgate.core.errors import ConfigError -from agents_shipgate.core.evaluation_clock import evaluation_date +from agents_shipgate.core.evaluation_clock import trust_expiry_date from agents_shipgate.schemas.checks import CheckMetadata from agents_shipgate.schemas.common import Severity from agents_shipgate.schemas.report import ( @@ -169,7 +170,7 @@ def resolve_severity_overrides( and audit purposes. Floor enforcement still uses the catalog floor (the static gate floor for the check class). """ - today = today or evaluation_date() + today = today or trust_expiry_date() catalog_by_id = _catalog_index(catalog) known_ids = _known_check_ids(catalog) ack_by_id = _ack_by_check_id(acknowledgements) diff --git a/tests/test_baseline_integrity.py b/tests/test_baseline_integrity.py index 6dde8507..10be7084 100644 --- a/tests/test_baseline_integrity.py +++ b/tests/test_baseline_integrity.py @@ -46,6 +46,7 @@ ) from agents_shipgate.core.context import ScanContext from agents_shipgate.core.domain import Agent +from agents_shipgate.core.evaluation_clock import use_evaluation_date from agents_shipgate.schemas.baseline import ( BaselineFile, BaselineFinding, @@ -444,7 +445,11 @@ def test_verify_baseline_entry_expired(tmp_path): hash_after=compute_baseline_hash_from_file(baseline_path), ), ) - issues = verify_baseline(baseline_path, audit_path, today=date.today()) + # A forged, backdated commit evaluation date cannot make an expired + # reviewer exception current again. Omit explicit ``today`` so this uses + # the production trust-expiry clock. + with use_evaluation_date(date.today() - timedelta(days=3650)): + issues = verify_baseline(baseline_path, audit_path) kinds = {issue.kind for issue in issues} assert "entry_expired" in kinds diff --git a/tests/test_p0_verification_identity_canaries.py b/tests/test_p0_verification_identity_canaries.py index 05aa9263..114dfca3 100644 --- a/tests/test_p0_verification_identity_canaries.py +++ b/tests/test_p0_verification_identity_canaries.py @@ -1,11 +1,16 @@ from __future__ import annotations import json +from datetime import date, timedelta from pathlib import Path import pytest from pydantic import ValidationError +from agents_shipgate.core.evaluation_clock import ( + trust_expiry_date, + use_evaluation_date, +) from agents_shipgate.core.verification_identity import ( build_terminal_receipt, build_unit_result, @@ -152,6 +157,20 @@ def test_worker_ir_cannot_smuggle_a_release_decision() -> None: build_unit_result(plan=_plan(), normalized_ir={"decision": "passed"}) +def test_backdated_evaluation_date_cannot_weaken_trust_expiry_clock() -> None: + """Commit-controlled time can never move hard expiry behind wall time.""" + + wall_clock_today = date.today() + with use_evaluation_date(wall_clock_today - timedelta(days=3650)): + assert trust_expiry_date() >= wall_clock_today + + +def test_future_evaluation_date_only_makes_trust_expiry_more_conservative() -> None: + future = date.today() + timedelta(days=3650) + with use_evaluation_date(future): + assert trust_expiry_date() == future + + def test_engine_distribution_digest_is_part_of_engine_identity() -> None: payload = _plan().engine.model_dump(mode="json") payload["engine_distribution_sha256"] = "sha256:" + "9" * 64 diff --git a/tests/test_severity_override_floor.py b/tests/test_severity_override_floor.py index dc5058e7..c6e8613f 100644 --- a/tests/test_severity_override_floor.py +++ b/tests/test_severity_override_floor.py @@ -27,6 +27,7 @@ import pytest from agents_shipgate.core.errors import ConfigError +from agents_shipgate.core.evaluation_clock import use_evaluation_date from agents_shipgate.core.findings import apply_severity_overrides from agents_shipgate.core.severity_overrides import ( crosses_tier, @@ -339,6 +340,29 @@ def test_ack_expiring_tomorrow_is_accepted() -> None: assert row.expires == (today + timedelta(days=1)).isoformat() +def test_backdated_evaluation_date_cannot_extend_expired_acknowledgement() -> None: + """A forged commit date is provenance, not authority over trust decay.""" + + wall_clock_today = date.today() + with use_evaluation_date(wall_clock_today - timedelta(days=3650)): + with pytest.raises(ConfigError, match=r"expired"): + resolve_severity_overrides( + overrides={ + "SHIP-POLICY-APPROVAL-MISSING": SeverityOverrideEntry( + severity="high" + ), + }, + acknowledgements=[ + OverrideAcknowledgement( + check_id="SHIP-POLICY-APPROVAL-MISSING", + reason="review period ended", + expires=wall_clock_today, + ), + ], + catalog=_catalog(), + ) + + # --- Rich-form override expiry (parallel to ack expiry, hard contract) ----- @@ -400,6 +424,25 @@ def test_rich_form_override_expiring_tomorrow_applies_cleanly() -> None: } +def test_backdated_evaluation_date_cannot_extend_expired_rich_override() -> None: + """The production default clock remains at least the verifier wall clock.""" + + wall_clock_today = date.today() + with use_evaluation_date(wall_clock_today - timedelta(days=3650)): + with pytest.raises(ConfigError, match=r"expired"): + resolve_severity_overrides( + overrides={ + "SHIP-DOC-MISSING-DESCRIPTION": SeverityOverrideEntry( + severity="low", + reason="review period ended", + expires=wall_clock_today, + ), + }, + acknowledgements=[], + catalog=_catalog(), + ) + + # --- Policy-pack rule IDs (no built-in floor) ------------------------------ diff --git a/tests/test_verify_orchestrator.py b/tests/test_verify_orchestrator.py index 85e57baf..c5205b77 100644 --- a/tests/test_verify_orchestrator.py +++ b/tests/test_verify_orchestrator.py @@ -1,20 +1,100 @@ from __future__ import annotations import json +import os import shutil import subprocess +from datetime import date, timedelta from pathlib import Path +import pytest + from agents_shipgate.cli.verification import assemble, worker +from agents_shipgate.cli.verify.git import commit_date from agents_shipgate.cli.verify.orchestrator import run_verify +from agents_shipgate.core.errors import ConfigError from agents_shipgate.core.verification_identity import validate_receipt_artifacts from agents_shipgate.schemas.verification_identity import VerificationReceipt REPO_ROOT = Path(__file__).resolve().parent.parent -def _git(repo: Path, *args: str) -> None: - subprocess.run(["git", *args], cwd=repo, check=True, capture_output=True) +def _git(repo: Path, *args: str, env: dict[str, str] | None = None) -> None: + command_env = os.environ.copy() + command_env.update(env or {}) + subprocess.run( + ["git", *args], + cwd=repo, + check=True, + capture_output=True, + env=command_env, + ) + + +def test_verify_backdated_commit_cannot_extend_expired_override(tmp_path: Path) -> None: + """The real verify path must not use a forgeable commit date for expiry.""" + + repo = tmp_path / "repo" + sample_dst = repo / "samples" / "support_refund_agent" + sample_dst.parent.mkdir(parents=True) + shutil.copytree(REPO_ROOT / "samples" / "support_refund_agent", sample_dst) + + wall_clock_today = date.today() + expired_on = wall_clock_today - timedelta(days=1) + manifest_path = sample_dst / "shipgate.yaml" + manifest = manifest_path.read_text(encoding="utf-8") + manifest_path.write_text( + manifest.replace( + "checks:\n ignore:\n", + "checks:\n" + " severity_overrides:\n" + " SHIP-DOC-MISSING-DESCRIPTION:\n" + " severity: low\n" + " reason: expired integration-test review\n" + f" expires: {expired_on.isoformat()}\n" + " ignore:\n", + ), + encoding="utf-8", + ) + + _git(repo, "init") + _git(repo, "config", "user.email", "test@example.test") + _git(repo, "config", "user.name", "Test User") + _git(repo, "add", ".") + forged_date = wall_clock_today - timedelta(days=3650) + forged_timestamp = f"{forged_date.isoformat()}T12:00:00+0000" + _git( + repo, + "commit", + "-m", + "backdated head", + env={ + "GIT_AUTHOR_DATE": forged_timestamp, + "GIT_COMMITTER_DATE": forged_timestamp, + }, + ) + assert commit_date(repo, "HEAD") == forged_date.isoformat() + + with pytest.raises(ConfigError, match=r"severity_overrides.*expired"): + run_verify( + workspace=repo, + config=Path("samples/support_refund_agent/shipgate.yaml"), + base=None, + head="HEAD", + archive_head=True, + out=repo / "agents-shipgate-reports", + ci_mode="advisory", + fail_on=None, + baseline=None, + baseline_mode="new-findings", + diff_from=None, + policy_packs=None, + plugins_enabled=False, + strict_plugins=False, + suggest_patches=False, + no_heuristics=False, + verbose=False, + ) def test_verify_threads_changed_files_into_head_scan(tmp_path):