diff --git a/README.md b/README.md index 84e12c1..aa34a85 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,29 @@ prompts, credentials, sessions, or user data belong here. can pull them. Public CI certifies their upstream base plus the same profile. - Promotion is explicit. CI may certify a tuple and the watcher may open a PR; neither workflow deploys or changes a consumer. -- Each consumer retains both `current` and `previous` certified tuples so a - rollback restores the gem and server together. +- A passing pull-request head is candidate evidence only. Promotion and + deployed certification use the actual main/merge commit, or an explicit + equal-tree attestation paired with a passing post-merge canary. +- Git-sourced gems use the full peeled commit as the Bundler `ref` and lockfile + revision. A consumer test must also prove the running process loaded that + exact `Bundler::Source::Git`; a release tag or lockfile alone is provenance, + not loaded-source proof. +- Ajent records AIGL, Blackline, and Raven selections together. Every selected + product needs a content digest or local image ID before promotion; a full + commit tag selects a candidate build but does not prove its content. +- In steady state, each consumer retains both `current` and `previous` + certified tuples so a rollback restores the gem and server together. The + explicitly labeled bootstrap state is the temporary degraded exception. - A failing bootstrap baseline is never relabeled as a certified `previous` - tuple. Promotion stays blocked until a distinct rollback consumer commit is - pinned to a passing client/runtime tuple and canaried. + tuple. After a real main/deploy canary, an explicitly acknowledged degraded + bootstrap may certify only `current`; it leaves `previous` null and keeps the + failed baseline as emergency provenance. The next meaningful passing release + becomes the first genuine certified rollback tuple. + +The runtime ledger is schema version 2; its machine-readable shape lives at +[`schemas/runtime-tuples.schema.json`](schemas/runtime-tuples.schema.json). +The current candidates remain deliberately unpromotable PR-head records, and +all `previous` slots remain `null` because no passing rollback tuple exists. ## What is covered diff --git a/docs/certification.md b/docs/certification.md index 21c2c55..9cb20c6 100644 --- a/docs/certification.md +++ b/docs/certification.md @@ -6,6 +6,16 @@ A candidate tuple is the complete set of client version or commit, Rails gem version or commit where applicable, exact OpenCode image, profile, and consumer commit. Changing any member invalidates the certification. +There are two distinct scopes: + +- `pre-merge-pr-head-candidate-only` proves that one reviewed PR head is + compatible. It is never a deployment or promotion authorization. +- `promotion-deployed` is recorded only after a passing post-merge canary and + the consumer coordinate is the actual main/merge commit. If policy + deliberately retains the PR-head commit, the evidence must additionally + record both Git trees as identical and name the actual main/merge commit. + Comparing diffs is not an identical-tree attestation. + The gate requires: 1. repository validation and the shared fixture corpus; @@ -16,6 +26,19 @@ The gate requires: 6. captured evidence including image ID/digest, server version, source commit, gem commits, timestamps, and probe outcome. +For a Git-sourced gem, “gem commit” means the peeled 40-character commit in +both the Gemfile `ref` and lockfile `revision`, plus runtime loaded-source proof: +the loaded version, `Bundler::Source::Git`, observed revision (and observed ref +when the consumer test exposes it), and the consumer test that made those +assertions. Annotated tag objects remain useful release provenance, but they +are not an execution coordinate. + +Ajent's runtime is a three-product selection. The ledger records AIGL, +Blackline, and Raven under one source commit. Candidate evidence may say that a +product artifact has not been built; promotion may not. At promotion time the +set of content-addressed product artifacts must exactly equal the selected +product set. + Health-only probes do not certify a tuple. The shared live probe is intentionally strict: `full_text` must equal the @@ -26,9 +49,18 @@ duplicated `compat-ok\n\ncompat-ok`, fails. ## Promotion Promotion is a reviewed manifest change that moves the old `current` tuple to -`previous` and the passing candidate to `current`. The consumer change is then -merged and deployed separately. Workflows in this repository have no deploy -credentials or deploy steps. +`previous` and the passing candidate to `current`. The consumer change must +already have a main/merge identity (or the equal-tree exception below) before +that manifest change. Production rollout remains a separate consumer-owned +operation; workflows in this repository have no deploy credentials or deploy +steps. + +Before invoking the promoter, replace PR-head-only metadata with either the +actual main/merge consumer ref and `main-commit` promotion provenance, or an +explicit identical-tree attestation containing the main commit/tree. Both +forms require a post-merge canary evidence path. Set `promotion_eligible` only +after that evidence exists. The promoter rejects the current PR-head candidates +even when their compatibility probes pass. Use the repository promotion command; do not hand-edit the three tuple slots. It binds evidence to a canonical SHA-256 of the complete tuple, requires full @@ -89,10 +121,13 @@ being able to observe the underlying model request. Record that distinction explicitly; do not infer a model request count from an application prompt count. The exact-image shared contract supplies the model-request proof. -Likewise, the public image job executes `ruby-rest-sse`; it does not silently -certify Rails persistence, plugin hooks, voice streaming, strict route gates, -or provider hooks. Those appear as `required_consumer_profiles` in the image -matrix and need their own consumer evidence before a full tuple can pass. +Likewise, the public image job executes only `ruby-rest-sse`; it does not +silently certify Rails persistence, plugin hooks, voice streaming, strict route +gates, or provider hooks. Every other profile is labeled +`executable-consumer-attestation` with `shared_ruby_probe_sufficient: false`. +Those profiles appear as `required_consumer_profiles` in the image matrix and +need executable evidence from their owning consumer before a full tuple can +pass. A prose review or a successful shared Ruby probe is not that evidence. The command clears `candidate` after promotion. It sets the repository-wide `migration_state` to `certified` only after every consumer has certified @@ -105,12 +140,39 @@ coordinates for emergency recovery but mark it failed. Do not create passing evidence for it and do not promote it into `previous`. The promoter rejects known-failed baselines even if a document claims `pass`. -The safe bootstrap is two-stage: certify and roll out the new candidate, then -create and canary a distinct consumer rollback commit that preserves the same -known-good client and exact runtime. Only after both immutable consumer commits -have real passing evidence can the manifest honestly contain certified -`current` and `previous` tuples. Until then, `promotion_readiness` remains -blocked and the candidate PR must not be treated as a deploy authorization. +Do not manufacture a no-op second commit merely to fill `previous`. Once the +candidate has been merged, deployed, and canaried under promotion-grade +provenance, a reviewer may use `bootstrap-current` with this exact +acknowledgement: + +```text +accept-degraded-rollback-with-failed-emergency-provenance +``` + +That operation certifies only `current`, moves the failed baseline to +`emergency_provenance` without relabeling it, leaves `previous` null, and sets +`migration_state` to `bootstrap-current-only`. This is an intentionally +degraded rollback state, not full certification. The next meaningful passing +release uses normal promotion; the already-certified current tuple then becomes +the first honest certified `previous` tuple. + +Until main/deploy evidence exists, `promotion_readiness` remains blocked and a +candidate PR must not be treated as deploy authorization. + +After reviewing the degraded rollback consequence, preview the explicit +bootstrap before writing it: + +```sh +ruby scripts/promote_runtime_tuple.rb bootstrap-current \ + --consumer travelwolf \ + --consumer-commit FULL_MAIN_OR_ATTESTED_PR_COMMIT \ + --status pass \ + --certified-at 2026-07-18T12:00:00Z \ + --evidence evidence/travelwolf-post-merge.json \ + --acknowledge-degraded-rollback \ + accept-degraded-rollback-with-failed-emergency-provenance \ + --dry-run +``` ## Rollback diff --git a/evidence/2026-07-18-ajent-rails-alpha7-candidate.json b/evidence/2026-07-18-ajent-rails-alpha7-candidate.json index eaaf2c4..e3d2cb9 100644 --- a/evidence/2026-07-18-ajent-rails-alpha7-candidate.json +++ b/evidence/2026-07-18-ajent-rails-alpha7-candidate.json @@ -2,15 +2,87 @@ "schema_version": 1, "consumer": "ajent-rails", "profile": "rails-persisted-turn", - "consumer_commit": "96552893baccd5ccba1592b2585edfef1299b4f9", + "consumer_commit": "01cdf9da8e1b3f9a205d681e448aefefd9704db2", + "consumer_tree": "9e93834ad16b60fa5edfe1cb3baaba193402be74", + "consumer_ref_kind": "pull-request-head", + "review_url": "https://gitea.ajay.to/ajaynomics/ajent-rails/pulls/946", + "certification_scope": "pre-merge-pr-head-candidate-only", + "promotion_eligible": false, "status": "pass", - "certified_at": "2026-07-18T22:03:14Z", - "tuple_sha256": "sha256:2fc1026cf247b5ad5677bfa4673c1073f7c2478769dca0cc8443a0924cebfca3", + "certified_at": "2026-07-18T23:28:16.435369Z", + "tuple_sha256": "sha256:e7026c294d0b2981f6ec785a45afb69969f4dc2b4d0ed6533f9bda5153304663", "shared_evidence": "evidence/2026-07-18-opencode-ruby-alpha7.json", - "canary": { - "checked_at": "2026-07-18T21:58:00Z", - "docker_image_id": "sha256:239e3dc6cd2958251cda636fb20bd7cd7c46f596bd4c61cb81863ebc49d824e8", + "loaded_sources": { + "opencode-ruby": { + "version": "0.0.1.alpha7", + "requested_ref": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "locked_revision": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "observed_revision": "78b6f9c9e9c7d58b699af1c3c17764acd33de798" + }, + "opencode-rails": { + "version": "0.0.1.alpha7", + "requested_ref": "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb", + "locked_revision": "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb", + "observed_revision": "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb" + }, + "source_class": "Bundler::Source::Git", + "proof_test": "test/lib/opencode/runtime_image_provenance_test.rb", + "status": "pass" + }, + "deterministic_contract": { + "checked_at": "2026-07-18T23:16:13Z", + "docker_image_id": "sha256:075cb49ac7fcb1f8972e051dff96d655fa0d0d911d83a523a6abf03a5e2e2935", + "expected_text": "compat-ok", + "full_text": "compat-ok", + "authoritative_assistant_count": 1, + "model_request_count": 1, + "status": "pass" + }, + "application_canary": { + "checked_at": "2026-07-18T23:28:16.435369Z", + "docker_image_id": "sha256:075cb49ac7fcb1f8972e051dff96d655fa0d0d911d83a523a6abf03a5e2e2935", "persisted_final_text": "compat-ok", - "model_request_count": 1 + "authoritative_final_text": "compat-ok", + "authoritative_assistant_count": 1, + "model_request_count": 1, + "cleanup_status": "pass", + "status": "pass" + }, + "consumer_verification": { + "tests": { + "runs": 4077, + "assertions": 14495, + "failures": 0, + "errors": 0, + "existing_skips": 4 + }, + "rubocop": { + "files": 1365, + "offenses": 0 + }, + "gitea_actions": { + "run_id": 2545, + "status": "success", + "commit": "01cdf9da8e1b3f9a205d681e448aefefd9704db2" + }, + "remotes_exact": [ + "origin", + "github", + "backup" + ] + }, + "product_artifact_coverage": { + "selected_products": [ + "aigl", + "blackline", + "raven" + ], + "content_addressed_artifacts": [ + "blackline" + ], + "missing_before_promotion": [ + "aigl", + "raven" + ] } } diff --git a/evidence/2026-07-18-mushu-alpha7-candidate.json b/evidence/2026-07-18-mushu-alpha7-candidate.json index 50f2651..64c7a5d 100644 --- a/evidence/2026-07-18-mushu-alpha7-candidate.json +++ b/evidence/2026-07-18-mushu-alpha7-candidate.json @@ -2,16 +2,88 @@ "schema_version": 1, "consumer": "mushu", "profile": "ruby-rest-sse", - "consumer_commit": "f26d958b64f214a422aeb629860c8c9a46a4c084", + "consumer_commit": "7cff22788b36ae703f6b1b8fa39b52d6dbafe7e0", + "consumer_tree": "762eb8dcc7042a52cd240ca69acdb9cb45150a84", + "consumer_ref_kind": "pull-request-head", + "review_url": "https://gitea.ajay.to/ajaynomics/mushu/pulls/1", + "certification_scope": "pre-merge-pr-head-candidate-only", + "promotion_eligible": false, "status": "pass", - "certified_at": "2026-07-18T22:03:14Z", - "tuple_sha256": "sha256:6171647c7db6d88d52882695c6106a4391e87d0bb1f9e0c48151b2bc1e95b318", + "certified_at": "2026-07-18T23:26:06.865190Z", + "tuple_sha256": "sha256:648d53b1d800c065e8ecdb95974049ee179617569170cfc169c31c3bfc1e8865", "shared_evidence": "evidence/2026-07-18-opencode-ruby-alpha7.json", - "canary": { - "run_id": "mushu-alpha7-20260718T215300Z-f26d958", - "checked_at": "2026-07-18T21:53:00Z", - "docker_image_id": "sha256:ed6293ca87e6db1dc07870d3ed6da21960743e6e2e904533b3c566d334b58d6a", - "persisted_final_text": "compat-ok", - "model_request_count": 1 + "loaded_source": { + "gem": "opencode-ruby", + "version": "0.0.1.alpha7", + "source_class": "Bundler::Source::Git", + "requested_ref": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "locked_revision": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "observed_ref": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "observed_revision": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "loaded_file_within_spec": true, + "proof_test": "test/services/mushu/runtime/opencode_dependency_provenance_test.rb", + "status": "pass" + }, + "deterministic_contract": { + "checked_at": "2026-07-18T23:23:41Z", + "image": "docker-registry:5000/mushu-opencode@sha256:ed6293ca87e6db1dc07870d3ed6da21960743e6e2e904533b3c566d334b58d6a", + "expected_text": "compat-ok", + "full_text": "compat-ok", + "authoritative_assistant_count": 1, + "model_request_count": 1, + "status": "pass" + }, + "application_canary": { + "checked_at": "2026-07-18T23:26:06.865190Z", + "response_text": "compat-ok", + "authoritative_final_text": "compat-ok", + "message_roles": [ + "user", + "assistant" + ], + "authoritative_assistant_count": 1, + "provider_request_count": 1, + "mapping_and_last_turn_persisted": true, + "active_claim_cleared": true, + "opencode_session_deleted": true, + "synthetic_database_rows_after_cleanup": 0, + "status": "pass" + }, + "consumer_verification": { + "focused_tests": { + "runs": 126, + "assertions": 1413, + "failures": 0, + "errors": 0 + }, + "full_tests": { + "runs": 632, + "assertions": 9013, + "failures": 0, + "errors": 0 + }, + "rubocop": { + "files": 216, + "offenses": 0 + }, + "gitea_actions": { + "run_id": 2544, + "url": "https://gitea.ajay.to/ajaynomics/mushu/actions/runs/2544", + "overall_status": "failure", + "commit": "7cff22788b36ae703f6b1b8fa39b52d6dbafe7e0", + "classification": "inherited-unrelated-baseline-failures", + "jobs": { + "lint": "success", + "scan_js": "success", + "scan_ruby": "inherited-vendor-bundle-source-scan-failure", + "test": "inherited-evidence-report-single-failure", + "system-test": "inherited-missing-test-system-directory" + }, + "baseline_run_id": 2508 + }, + "remotes_exact": [ + "origin", + "backup" + ] } } diff --git a/evidence/2026-07-18-opencode-ruby-alpha2-regression.json b/evidence/2026-07-18-opencode-ruby-alpha2-regression.json index b992b78..eb41785 100644 --- a/evidence/2026-07-18-opencode-ruby-alpha2-regression.json +++ b/evidence/2026-07-18-opencode-ruby-alpha2-regression.json @@ -36,7 +36,9 @@ }, { "target": "ajent-production-artifact", - "docker_image_id": "sha256:6fb2b3bf8e4cdf48e16bf6c3475e953df2f8304e77f5efe816ceb46f905eead8", + "consumer_commit": "8d8550265dca0ac06a41d5427c585ede56bbb808", + "product": "blackline", + "docker_image_id": "sha256:af479243e6ccf1206f57dffd13b8e9d2c028de401cccc954bcc2f00940274c60", "server_version": "1.18.3", "status": "fail" }, @@ -47,5 +49,38 @@ "status": "fail" } ], - "promotion_effect": "The observed alpha2 consumer baselines are not eligible for certified previous tuples. A distinct, tested rollback consumer commit pinned to alpha7 is required before the two-certified-tuples policy can be satisfied." + "observed_ajent_production_runtime": { + "consumer_commit": "8d8550265dca0ac06a41d5427c585ede56bbb808", + "upstream_base_layer_match": "ghcr.io/anomalyco/opencode@sha256:c2d5d6398df72aac85cb1bdc8f900c71a9b75a33fb7c0a76dc1484e4b126e41e", + "shared_accessory": { + "product": "blackline", + "tag_provenance": "docker-registry:5000/opencode-blackline:8d8550265dca0ac06a41d5427c585ede56bbb808", + "docker_image_id": "sha256:af479243e6ccf1206f57dffd13b8e9d2c028de401cccc954bcc2f00940274c60" + }, + "deployed_per_user_product_configuration": { + "strategy": "mutable-latest", + "status": "observed-configuration-drift-failure", + "references": { + "aigl": "docker-registry:5000/opencode-aigl:latest", + "blackline": "docker-registry:5000/opencode-blackline:latest", + "raven": "docker-registry:5000/opencode-raven:latest" + } + }, + "ci_built_sha_artifacts": { + "aigl": { + "tag_provenance": "docker-registry:5000/opencode-aigl:8d8550265dca0ac06a41d5427c585ede56bbb808", + "docker_image_id": "sha256:72c454f39d423618b75439122c9073dbba7c9071421e5f3abd12cf288dc1f898" + }, + "blackline": { + "tag_provenance": "docker-registry:5000/opencode-blackline:8d8550265dca0ac06a41d5427c585ede56bbb808", + "docker_image_id": "sha256:af479243e6ccf1206f57dffd13b8e9d2c028de401cccc954bcc2f00940274c60" + }, + "raven": { + "tag_provenance": "docker-registry:5000/opencode-raven:8d8550265dca0ac06a41d5427c585ede56bbb808", + "docker_image_id": "sha256:6c4da94a8dec6160f0b1b4d0e2cd9ff38a3faa18dbd2a8d6023a71f6416a544c" + } + }, + "scope_note": "The hardened alpha2 contract was rerun against the exact shared Blackline accessory. The three SHA-tagged image IDs are CI artifact provenance, not evidence that the deployed per-user ProductConfig selected them; it selected mutable latest references." + }, + "promotion_effect": "The observed alpha2 consumer baselines are not eligible for certified previous tuples. After a real main/deploy/post-merge canary, an explicitly acknowledged bootstrap may certify current while leaving previous null and retaining alpha2 only as failed emergency provenance; the next meaningful passing release creates the first certified previous tuple." } diff --git a/evidence/2026-07-18-opencode-ruby-alpha7.json b/evidence/2026-07-18-opencode-ruby-alpha7.json index 53fb901..df83a8d 100644 --- a/evidence/2026-07-18-opencode-ruby-alpha7.json +++ b/evidence/2026-07-18-opencode-ruby-alpha7.json @@ -41,7 +41,7 @@ "target": "travelwolf-1.16.1", "image": "ghcr.io/anomalyco/opencode@sha256:e975a0647576016dfdf77d54b979ca30d32b4750472c10263e9894aad6628c2a", "server_version": "1.16.1", - "checked_at": "2026-07-18T22:03:12Z", + "checked_at": "2026-07-18T23:35:57Z", "expected_text": "compat-ok", "full_text": "compat-ok", "llm_request_count": 1, @@ -51,7 +51,7 @@ "target": "opencode-ajent-1.17.18", "image": "ghcr.io/anomalyco/opencode@sha256:bf9d0e84b7cedef436a8f57db4d48767cd5d8fc6604f666335c1cc916b199a97", "server_version": "1.17.18", - "checked_at": "2026-07-18T22:03:13Z", + "checked_at": "2026-07-18T23:35:58Z", "expected_text": "compat-ok", "full_text": "compat-ok", "llm_request_count": 1, @@ -61,7 +61,7 @@ "target": "ajent-base-1.18.3", "image": "ghcr.io/anomalyco/opencode@sha256:c2d5d6398df72aac85cb1bdc8f900c71a9b75a33fb7c0a76dc1484e4b126e41e", "server_version": "1.18.3", - "checked_at": "2026-07-18T22:03:14Z", + "checked_at": "2026-07-18T23:35:58Z", "expected_text": "compat-ok", "full_text": "compat-ok", "llm_request_count": 1, @@ -71,42 +71,55 @@ "consumer_canaries": [ { "consumer": "ajent-rails", - "consumer_commit": "96552893baccd5ccba1592b2585edfef1299b4f9", - "tag_provenance": "ajent-opencode-blackline-canary:96552893", - "docker_image_id": "sha256:239e3dc6cd2958251cda636fb20bd7cd7c46f596bd4c61cb81863ebc49d824e8", - "source_commit": "96552893baccd5ccba1592b2585edfef1299b4f9", + "consumer_commit": "01cdf9da8e1b3f9a205d681e448aefefd9704db2", + "consumer_ref_kind": "pull-request-head", + "certification_scope": "pre-merge-pr-head-candidate-only", + "tag_provenance": "ajent-opencode-blackline-canary:01cdf9da8e1b3f9a205d681e448aefefd9704db2", + "docker_image_id": "sha256:075cb49ac7fcb1f8972e051dff96d655fa0d0d911d83a523a6abf03a5e2e2935", + "source_commit": "01cdf9da8e1b3f9a205d681e448aefefd9704db2", "upstream_base": "ghcr.io/anomalyco/opencode@sha256:c2d5d6398df72aac85cb1bdc8f900c71a9b75a33fb7c0a76dc1484e4b126e41e", "server_version": "1.18.3", - "checked_at": "2026-07-18T21:58:00Z", + "checked_at": "2026-07-18T23:28:16.435369Z", "application_path": "Blackline::GenerateResponseJob", "persisted_final_text": "compat-ok", + "authoritative_final_text": "compat-ok", + "authoritative_assistant_count": 1, "model_request_count": 1, + "cleanup_status": "pass", "status": "pass" }, { "consumer": "mushu", - "consumer_commit": "f26d958b64f214a422aeb629860c8c9a46a4c084", - "run_id": "mushu-alpha7-20260718T215300Z-f26d958", + "consumer_commit": "7cff22788b36ae703f6b1b8fa39b52d6dbafe7e0", + "consumer_ref_kind": "pull-request-head", + "certification_scope": "pre-merge-pr-head-candidate-only", "tag_provenance": "docker-registry:5000/mushu-opencode:sha-99d6328b1", + "image": "docker-registry:5000/mushu-opencode@sha256:ed6293ca87e6db1dc07870d3ed6da21960743e6e2e904533b3c566d334b58d6a", "docker_image_id": "sha256:ed6293ca87e6db1dc07870d3ed6da21960743e6e2e904533b3c566d334b58d6a", "source_commit": "99d6328b18ff8a340a928449a2aa8bc184a063f9", "server_version": "0.0.0-permission-shortcuts-202606300721", - "checked_at": "2026-07-18T21:53:00Z", - "persisted_final_text": "compat-ok", - "model_request_count": 1, + "checked_at": "2026-07-18T23:26:06.865190Z", + "response_text": "compat-ok", + "authoritative_final_text": "compat-ok", + "authoritative_assistant_count": 1, + "provider_request_count": 1, + "cleanup_status": "pass", "status": "pass" }, { "consumer": "travelwolf", - "consumer_commit": "19747b59fa21e12560afd9edd73ed674ab3a644c", - "run_id": "20260718215135-8d8102c0", + "consumer_commit": "b9a2c2dd361a6b2ea547cc67ee817b8eb4ce31e8", + "consumer_ref_kind": "pull-request-head", + "certification_scope": "pre-merge-pr-head-candidate-only", + "run_id": "travelwolf-alpha7-ref-20260718T232211Z-48eadf", "image": "ghcr.io/anomalyco/opencode@sha256:e975a0647576016dfdf77d54b979ca30d32b4750472c10263e9894aad6628c2a", "server_version": "1.16.1", - "started_at": "2026-07-18T21:51:35Z", - "checked_at": "2026-07-18T21:51:56.552Z", - "final_text": "TRAVELWOLF_ALPHA7_SUBSCRIBE_READY", - "application_prompt_count": 1, - "model_request_count_observed": false, + "checked_at": "2026-07-18T23:22:26.216140Z", + "final_text": "TRAVELWOLF_ALPHA7_REF_PIN_READY", + "subscription_ready_before_prompt": true, + "opencode_api_prompt_count": 1, + "provider_request_count_observed": false, + "message_roles": ["user", "assistant"], "cleanup_status": "pass", "status": "pass" } diff --git a/evidence/2026-07-18-travelwolf-alpha7-candidate.json b/evidence/2026-07-18-travelwolf-alpha7-candidate.json index 9f399fa..a68278d 100644 --- a/evidence/2026-07-18-travelwolf-alpha7-candidate.json +++ b/evidence/2026-07-18-travelwolf-alpha7-candidate.json @@ -2,17 +2,86 @@ "schema_version": 1, "consumer": "travelwolf", "profile": "rails-persisted-turn", - "consumer_commit": "19747b59fa21e12560afd9edd73ed674ab3a644c", + "consumer_commit": "b9a2c2dd361a6b2ea547cc67ee817b8eb4ce31e8", + "consumer_tree": "8b3f8a97dc1f1d8b558a49523bf312d2810422df", + "consumer_ref_kind": "pull-request-head", + "review_url": "https://gitea.ajay.to/ajaynomics/travelwolf/pulls/29", + "certification_scope": "pre-merge-pr-head-candidate-only", + "promotion_eligible": false, "status": "pass", - "certified_at": "2026-07-18T22:03:14Z", - "tuple_sha256": "sha256:a7519e661f74d86788928540429c953edcbf20d26321c0dd2b72cd0a1b995b61", + "certified_at": "2026-07-18T23:35:57Z", + "tuple_sha256": "sha256:bed7d6f6a5e6dd9138c3b960b8bf13ac6f5bf402d0aa7e5c01a52a8e8ea1083d", "shared_evidence": "evidence/2026-07-18-opencode-ruby-alpha7.json", - "canary": { - "run_id": "20260718215135-8d8102c0", - "checked_at": "2026-07-18T21:51:56.552Z", - "final_text": "TRAVELWOLF_ALPHA7_SUBSCRIBE_READY", - "application_prompt_count": 1, - "model_request_count_observed": false, - "cleanup_status": "pass" + "loaded_sources": { + "opencode-ruby": { + "version": "0.0.1.alpha7", + "requested_ref": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "locked_revision": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "observed_ref": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "observed_revision": "78b6f9c9e9c7d58b699af1c3c17764acd33de798" + }, + "opencode-rails": { + "version": "0.0.1.alpha7", + "requested_ref": "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb", + "locked_revision": "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb", + "observed_ref": "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb", + "observed_revision": "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb" + }, + "source_class": "Bundler::Source::Git", + "proof_test": "test/services/travelwolf/opencode_env_wiring_test.rb", + "status": "pass" + }, + "deterministic_contract": { + "checked_at": "2026-07-18T23:23:03Z", + "image": "ghcr.io/anomalyco/opencode@sha256:e975a0647576016dfdf77d54b979ca30d32b4750472c10263e9894aad6628c2a", + "opencode_ruby_commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "expected_text": "compat-ok", + "full_text": "compat-ok", + "authoritative_assistant_count": 1, + "model_request_count": 1, + "status": "pass" + }, + "application_canary": { + "run_id": "travelwolf-alpha7-ref-20260718T232211Z-48eadf", + "checked_at": "2026-07-18T23:22:26.216140Z", + "final_text": "TRAVELWOLF_ALPHA7_REF_PIN_READY", + "subscription_ready_before_prompt": true, + "opencode_api_prompt_count": 1, + "provider_request_count_observed": false, + "message_roles": [ + "user", + "assistant" + ], + "cleanup_status": "pass", + "status": "pass" + }, + "consumer_verification": { + "source_guard": { + "runs": 4, + "assertions": 110, + "failures": 0, + "errors": 0 + }, + "focused_tests": { + "runs": 31, + "assertions": 248, + "failures": 0, + "errors": 0 + }, + "bounded_full_tests": { + "runs": 2300, + "assertions": 21679, + "failures": 0, + "errors": 0 + }, + "rubocop": { + "changed_ruby_files": 6, + "offenses": 0 + }, + "remotes_exact": [ + "origin", + "github", + "backup" + ] } } diff --git a/lib/opencode_compat/runtime_tuple_promoter.rb b/lib/opencode_compat/runtime_tuple_promoter.rb index 4fe9529..2985505 100644 --- a/lib/opencode_compat/runtime_tuple_promoter.rb +++ b/lib/opencode_compat/runtime_tuple_promoter.rb @@ -10,11 +10,14 @@ module OpenCodeCompat class PromotionError < StandardError; end class RuntimeTuplePromoter + SCHEMA_VERSION = 2 FULL_COMMIT = /\A[0-9a-f]{40}\z/ DIGEST = /\Asha256:[0-9a-f]{64}\z/ IMMUTABLE_IMAGE = /\A[^@\s]+@sha256:[0-9a-f]{64}\z/ UTC_TIMESTAMP = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\z/ CERTIFICATION_STATUS = "pass" + DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT = + "accept-degraded-rollback-with-failed-emergency-provenance" NON_CERTIFIABLE_TUPLE_STATUSES = %w[observed-production-contract-failed].freeze TUPLE_METADATA_KEYS = %w[ certification @@ -41,7 +44,7 @@ module OpenCodeCompat manifest = read_manifest consumer_entry = fetch_consumer!(manifest, consumer) profile = fetch_profile!(consumer_entry, consumer) - candidate = prepare_candidate!(consumer_entry, consumer_commit) + candidate = prepare_candidate!(consumer_entry, consumer_commit, for_promotion: false) current = consumer_entry.fetch("current") do raise PromotionError, "#{consumer} has no current tuple to preserve as previous" end @@ -82,6 +85,37 @@ module OpenCodeCompat end end + def bootstrap_current(consumer:, consumer_commit:, certification:, acknowledgement:, dry_run: false) + validate_full_commit!(consumer_commit, "consumer commit") + unless acknowledgement == DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT + raise PromotionError, + "bootstrap requires explicit acknowledgement #{DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT.inspect}" + end + + if dry_run + manifest = read_manifest + return bootstrap_current_manifest( + manifest, + consumer: consumer, + consumer_commit: consumer_commit, + certification: certification, + acknowledgement: acknowledgement + ) + end + + with_current_manifest_lock do |manifest| + bootstrapped = bootstrap_current_manifest( + manifest, + consumer: consumer, + consumer_commit: consumer_commit, + certification: certification, + acknowledgement: acknowledgement + ) + atomic_write(bootstrapped) + bootstrapped + end + end + private def read_manifest @@ -93,7 +127,7 @@ module OpenCodeCompat def promote_manifest(manifest, consumer:, consumer_commit:, certification:, previous_certification:) consumer_entry = fetch_consumer!(manifest, consumer) profile = fetch_profile!(consumer_entry, consumer) - candidate = prepare_candidate!(consumer_entry, consumer_commit) + candidate = prepare_candidate!(consumer_entry, consumer_commit, for_promotion: true) current = consumer_entry.fetch("current") do raise PromotionError, "#{consumer} has no current tuple to preserve as previous" end @@ -124,13 +158,60 @@ module OpenCodeCompat consumer_entry["previous"] = certified_previous consumer_entry["current"] = certified_candidate consumer_entry["candidate"] = nil - manifest["migration_state"] = all_consumers_certified?(manifest) ? "certified" : "candidate" + consumer_entry.delete("rollback_state") + manifest["migration_state"] = migration_state_for(manifest) + manifest + end + + def bootstrap_current_manifest(manifest, consumer:, consumer_commit:, certification:, acknowledgement:) + consumer_entry = fetch_consumer!(manifest, consumer) + profile = fetch_profile!(consumer_entry, consumer) + candidate = prepare_candidate!(consumer_entry, consumer_commit, for_promotion: true) + current = consumer_entry.fetch("current") do + raise PromotionError, "#{consumer} has no current tuple to retain as emergency provenance" + end + validate_tuple!(current, "#{consumer} current") + unless NON_CERTIFIABLE_TUPLE_STATUSES.include?(current["status"]) + raise PromotionError, "degraded bootstrap is only for a current tuple known to fail the contract" + end + + candidate_fingerprint = tuple_fingerprint(candidate, consumer: consumer, profile: profile) + current_fingerprint = tuple_fingerprint(current, consumer: consumer, profile: profile) + if candidate_fingerprint == current_fingerprint + raise PromotionError, "#{consumer} candidate is identical to its failed current tuple" + end + + certified_candidate = certify_tuple!( + candidate, + consumer: consumer, + profile: profile, + supplied: certification, + expected_fingerprint: candidate_fingerprint, + label: "bootstrap candidate" + ) + + consumer_entry["emergency_provenance"] = deep_copy(current) + consumer_entry["current"] = certified_candidate + consumer_entry["candidate"] = nil + consumer_entry["previous"] = nil + consumer_entry["rollback_state"] = { + "status" => "degraded-no-certified-previous", + "acknowledgement" => acknowledgement, + "recorded_at" => certification.fetch("certified_at"), + "emergency_provenance_status" => current.fetch("status") + } + manifest["migration_state"] = migration_state_for(manifest) + manifest["promotion_readiness"] = { + "status" => "bootstrap-current-only", + "reason" => "The current tuple is certified, but no independently passing previous tuple exists.", + "required_action" => "Certify the next meaningful release; normal promotion will then retain this current tuple as the first certified previous rollback." + } manifest end def fetch_consumer!(manifest, consumer) - unless manifest.is_a?(Hash) && manifest["schema_version"] == 1 && manifest["consumers"].is_a?(Hash) - raise PromotionError, "runtime tuple manifest must use schema_version 1 and contain consumers" + unless manifest.is_a?(Hash) && manifest["schema_version"] == SCHEMA_VERSION && manifest["consumers"].is_a?(Hash) + raise PromotionError, "runtime tuple manifest must use schema_version #{SCHEMA_VERSION} and contain consumers" end manifest.fetch("consumers").fetch(consumer) do @@ -138,7 +219,7 @@ module OpenCodeCompat end end - def prepare_candidate!(consumer_entry, consumer_commit) + def prepare_candidate!(consumer_entry, consumer_commit, for_promotion:) candidate = consumer_entry["candidate"] unless candidate.is_a?(Hash) raise PromotionError, "consumer has no candidate tuple to promote" @@ -155,9 +236,151 @@ module OpenCodeCompat prepared = deep_copy(candidate) prepared["consumer_commit"] = consumer_commit validate_tuple!(prepared, "candidate") + validate_candidate_consumer_ref!(prepared, consumer_commit) + validate_candidate_client_sources!(prepared) + if for_promotion + validate_promotion_provenance!(prepared, consumer_commit) + validate_promotion_product_artifacts!(prepared.fetch("runtime")) + end prepared end + def validate_candidate_consumer_ref!(candidate, consumer_commit) + reference = candidate["consumer_ref"] + unless reference.is_a?(Hash) + raise PromotionError, "candidate must record its exact consumer ref" + end + + kind = reference["kind"] + unless %w[pull-request-head main-commit].include?(kind) + raise PromotionError, "candidate consumer ref must be a pull-request-head or main-commit" + end + validate_full_commit!(reference["commit"], "candidate consumer ref commit") + validate_full_commit!(reference["tree"], "candidate consumer ref tree") + unless reference["commit"] == consumer_commit + raise PromotionError, "candidate consumer ref does not match the tuple consumer commit" + end + unless reference["repository"].is_a?(String) && !reference["repository"].empty? + raise PromotionError, "candidate consumer ref must name its repository" + end + + return unless kind == "pull-request-head" + + validate_full_commit!(reference["base_commit"], "candidate pull request base commit") + unless reference["review_url"].is_a?(String) && reference["review_url"].start_with?("https://") + raise PromotionError, "candidate pull request ref must include its HTTPS review URL" + end + end + + def validate_candidate_client_sources!(candidate) + %w[opencode_ruby opencode_rails].each do |client_name| + client = candidate[client_name] + next unless client + + validate_client_source!(client, "candidate #{client_name}", required: true) + end + end + + def validate_promotion_provenance!(candidate, consumer_commit) + unless candidate["certification_scope"] == "promotion-deployed" && candidate["promotion_eligible"] == true + raise PromotionError, + "pre-merge pull-request evidence is candidate-only; promotion requires deployed certification scope" + end + + provenance = candidate["promotion_provenance"] + unless provenance.is_a?(Hash) + raise PromotionError, "promotion requires main-commit provenance or an identical-tree attestation" + end + + case provenance["kind"] + when "main-commit" + unless candidate.dig("consumer_ref", "kind") == "main-commit" && + provenance["main_commit"] == consumer_commit + raise PromotionError, "main-commit promotion provenance must match the tuple consumer commit" + end + validate_post_merge_canary!( + provenance["post_merge_canary"], + consumer_commit: provenance["main_commit"] + ) + when "identical-tree-attestation" + validate_identical_tree_attestation!(candidate, provenance, consumer_commit) + else + raise PromotionError, "unknown promotion provenance kind #{provenance['kind'].inspect}" + end + end + + def validate_identical_tree_attestation!(candidate, provenance, consumer_commit) + validate_full_commit!(provenance["pull_request_commit"], "attested pull request commit") + validate_full_commit!(provenance["pull_request_tree"], "attested pull request tree") + validate_full_commit!(provenance["main_commit"], "attested main commit") + validate_full_commit!(provenance["main_tree"], "attested main tree") + validate_timestamp!(provenance["attested_at"], "identical-tree attestation timestamp") + + unless provenance["pull_request_commit"] == consumer_commit && + provenance["pull_request_tree"] == candidate.dig("consumer_ref", "tree") && + provenance["main_tree"] == provenance["pull_request_tree"] + raise PromotionError, "identical-tree attestation does not match the candidate and main trees" + end + + validate_post_merge_canary!( + provenance["post_merge_canary"], + consumer_commit: provenance["main_commit"] + ) + end + + def validate_post_merge_canary!(canary, consumer_commit:) + unless canary.is_a?(Hash) && canary["status"] == "pass" + raise PromotionError, "promotion requires a passing post-merge canary" + end + validate_timestamp!(canary["checked_at"], "post-merge canary timestamp") + evidence = Array(canary["evidence"]) + if evidence.empty? || evidence.any? { |path| !path.is_a?(String) || !path.start_with?("evidence/") } + raise PromotionError, "post-merge canary must reference evidence under evidence/" + end + evidence.each do |reference| + validate_post_merge_canary_evidence!( + reference, + checked_at: canary.fetch("checked_at"), + consumer_commit: consumer_commit + ) + end + end + + def validate_post_merge_canary_evidence!(reference, checked_at:, consumer_commit:) + relative = Pathname.new(reference).cleanpath + if relative.absolute? || relative.each_filename.first != "evidence" + raise PromotionError, "post-merge canary evidence must stay under evidence/" + end + + evidence_root = File.realpath(File.join(@root, "evidence")) + real_path = File.realpath(File.join(@root, relative.to_s)) + unless real_path.start_with?("#{evidence_root}#{File::SEPARATOR}") + raise PromotionError, "post-merge canary evidence resolves outside evidence/" + end + + document = parse_json(File.read(real_path), relative.to_s) + unless document.is_a?(Hash) && document["schema_version"] == 1 && + document["status"] == "pass" && document["checked_at"] == checked_at && + document["consumer_commit"] == consumer_commit + raise PromotionError, + "post-merge canary evidence must match its passing timestamp and main consumer commit" + end + rescue Errno::ENOENT + raise PromotionError, "post-merge canary evidence does not exist: #{reference}" + end + + def validate_promotion_product_artifacts!(runtime) + selection = runtime["product_selection"] + return unless selection + + selected_products = selection.fetch("references").keys.sort + artifact_products = runtime.fetch("product_images", {}).keys.sort + return if selected_products == artifact_products + + raise PromotionError, + "promotion requires exact content IDs or digests for every selected product image" + end + def fetch_profile!(consumer_entry, consumer) profile = consumer_entry["profile"] unless profile.is_a?(String) && profile.match?(/\A[a-z0-9][a-z0-9-]*\z/) @@ -346,6 +569,46 @@ module OpenCodeCompat end validate_full_commit!(client["git_commit"], "#{label} git commit") + validate_client_source!(client, label, required: false) + end + + def validate_client_source!(client, label, required:) + source = client["source"] + if source.nil? + raise PromotionError, "#{label} must include loaded exact-ref source proof" if required + + return + end + unless source.is_a?(Hash) && source["type"] == "git" && source["uri"].is_a?(String) && !source["uri"].empty? + raise PromotionError, "#{label} source must identify its Git URI" + end + + commit = client.fetch("git_commit") + %w[requested_ref locked_revision].each do |key| + unless source[key] == commit + raise PromotionError, "#{label} source #{key} must equal the peeled client commit" + end + end + + proof = source["loaded_source_proof"] + unless proof.is_a?(Hash) && proof["status"] == "pass" && proof["source_class"] == "Bundler::Source::Git" + raise PromotionError, "#{label} must include passing Bundler Git loaded-source proof" + end + expected = { + "loaded_version" => client.fetch("version"), + "observed_revision" => commit + } + expected.each do |key, value| + unless proof[key] == value + raise PromotionError, "#{label} loaded-source proof #{key} must equal #{value.inspect}" + end + end + if proof.key?("observed_ref") && proof["observed_ref"] != commit + raise PromotionError, "#{label} loaded-source proof observed_ref must equal #{commit.inspect}" + end + unless proof["test"].is_a?(String) && !proof["test"].empty? + raise PromotionError, "#{label} loaded-source proof must name the consumer test" + end end def validate_runtime!(runtime, label) @@ -355,6 +618,47 @@ module OpenCodeCompat end exact_execution_coordinate = false + product_selection = runtime["product_selection"] + if product_selection + unless product_selection.is_a?(Hash) && product_selection["strategy"] == "full-consumer-commit-tag" + raise PromotionError, "#{label} product_selection must use the full-consumer-commit-tag strategy" + end + validate_full_commit!(product_selection["source_commit"], "#{label} product selection source commit") + references = product_selection["references"] + unless references.is_a?(Hash) && !references.empty? + raise PromotionError, "#{label} product selection references must be a non-empty object" + end + references.each do |product, reference| + unless product.match?(/\A[a-z0-9][a-z0-9-]*\z/) && + reference.is_a?(String) && reference.end_with?(":#{product_selection['source_commit']}") + raise PromotionError, + "#{label} #{product} selection must end in the full consumer commit tag" + end + end + exact_execution_coordinate = true + end + + product_images = runtime["product_images"] + if product_images + unless product_images.is_a?(Hash) && !product_images.empty? + raise PromotionError, "#{label} product_images must be a non-empty object" + end + product_images.each do |product, coordinate| + unless product.match?(/\A[a-z0-9][a-z0-9-]*\z/) && coordinate.is_a?(Hash) + raise PromotionError, "#{label} product image entries must use named coordinate objects" + end + image_id = coordinate["docker_image_id"] + image = coordinate["image"] + unless (image_id.is_a?(String) && image_id.match?(DIGEST)) || + (image.is_a?(String) && image.match?(IMMUTABLE_IMAGE)) + raise PromotionError, + "#{label} #{product} product image must include an exact sha256 image ID or OCI digest" + end + validate_full_commit!(coordinate["source_commit"], "#{label} #{product} source commit") + exact_execution_coordinate = true + end + end + runtime.each do |key, value| if key == "docker_image_id" || key.end_with?("_image_id") unless value.is_a?(String) && value.match?(DIGEST) @@ -455,6 +759,15 @@ module OpenCodeCompat end end + def migration_state_for(manifest) + return "certified" if all_consumers_certified?(manifest) + + degraded = manifest.fetch("consumers").any? do |_consumer, entry| + entry.dig("rollback_state", "status") == "degraded-no-certified-previous" + end + degraded ? "bootstrap-current-only" : "candidate" + end + def with_current_manifest_lock loop do retry_with_new_inode = false diff --git a/manifests/image-matrix.json b/manifests/image-matrix.json index 04ade4d..5d0e8cf 100644 --- a/manifests/image-matrix.json +++ b/manifests/image-matrix.json @@ -11,7 +11,7 @@ "required_consumer_profiles": ["rails-persisted-turn"], "certification_status": "certified", "certified_client_commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", - "certified_at": "2026-07-18T22:03:12Z", + "certified_at": "2026-07-18T23:35:57Z", "expected_text": "compat-ok", "full_text": "compat-ok", "llm_request_count": 1 @@ -26,7 +26,7 @@ "required_consumer_profiles": ["plugin-ledger"], "certification_status": "certified", "certified_client_commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", - "certified_at": "2026-07-18T22:03:13Z", + "certified_at": "2026-07-18T23:35:58Z", "expected_text": "compat-ok", "full_text": "compat-ok", "llm_request_count": 1 @@ -41,7 +41,7 @@ "required_consumer_profiles": ["rails-persisted-turn", "plugin-ledger"], "certification_status": "certified", "certified_client_commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", - "certified_at": "2026-07-18T22:03:14Z", + "certified_at": "2026-07-18T23:35:58Z", "expected_text": "compat-ok", "full_text": "compat-ok", "llm_request_count": 1 @@ -51,35 +51,43 @@ { "id": "ajent-blackline-alpha7-canary", "consumer": "ajent-rails", - "consumer_commit": "96552893baccd5ccba1592b2585edfef1299b4f9", - "tag_provenance": "ajent-opencode-blackline-canary:96552893", - "docker_image_id": "sha256:239e3dc6cd2958251cda636fb20bd7cd7c46f596bd4c61cb81863ebc49d824e8", - "source_commit": "96552893baccd5ccba1592b2585edfef1299b4f9", + "consumer_commit": "01cdf9da8e1b3f9a205d681e448aefefd9704db2", + "consumer_ref_kind": "pull-request-head", + "certification_scope": "pre-merge-pr-head-candidate-only", + "promotion_eligible": false, + "tag_provenance": "ajent-opencode-blackline-canary:01cdf9da8e1b3f9a205d681e448aefefd9704db2", + "docker_image_id": "sha256:075cb49ac7fcb1f8972e051dff96d655fa0d0d911d83a523a6abf03a5e2e2935", + "source_commit": "01cdf9da8e1b3f9a205d681e448aefefd9704db2", "reported_version": "1.18.3", "upstream_base": "ghcr.io/anomalyco/opencode@sha256:c2d5d6398df72aac85cb1bdc8f900c71a9b75a33fb7c0a76dc1484e4b126e41e", "profiles": ["rails-persisted-turn"], "certification_status": "candidate-certified", "certified_client_commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", "certified_rails_commit": "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb", - "certified_at": "2026-07-18T21:58:00Z", + "certified_at": "2026-07-18T23:28:16.435369Z", "persisted_final_text": "compat-ok", - "model_request_count": 1 + "authoritative_final_text": "compat-ok", + "model_request_count": 1, + "product_artifacts_missing_before_promotion": ["aigl", "raven"] }, { "id": "mushu-permission-shortcuts-alpha7-canary", "consumer": "mushu", - "consumer_commit": "f26d958b64f214a422aeb629860c8c9a46a4c084", - "run_id": "mushu-alpha7-20260718T215300Z-f26d958", + "consumer_commit": "7cff22788b36ae703f6b1b8fa39b52d6dbafe7e0", + "consumer_ref_kind": "pull-request-head", + "certification_scope": "pre-merge-pr-head-candidate-only", + "promotion_eligible": false, "tag_provenance": "docker-registry:5000/mushu-opencode:sha-99d6328b1", + "image": "docker-registry:5000/mushu-opencode@sha256:ed6293ca87e6db1dc07870d3ed6da21960743e6e2e904533b3c566d334b58d6a", "docker_image_id": "sha256:ed6293ca87e6db1dc07870d3ed6da21960743e6e2e904533b3c566d334b58d6a", "source_commit": "99d6328b18ff8a340a928449a2aa8bc184a063f9", "reported_version": "0.0.0-permission-shortcuts-202606300721", "profiles": ["ruby-rest-sse"], "certification_status": "candidate-certified", "certified_client_commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", - "certified_at": "2026-07-18T21:53:00Z", - "persisted_final_text": "compat-ok", - "model_request_count": 1 + "certified_at": "2026-07-18T23:26:06.865190Z", + "authoritative_final_text": "compat-ok", + "provider_request_count": 1 } ] } diff --git a/manifests/runtime-tuples.json b/manifests/runtime-tuples.json index 98cf2b8..62c0add 100644 --- a/manifests/runtime-tuples.json +++ b/manifests/runtime-tuples.json @@ -1,18 +1,19 @@ { - "schema_version": 1, + "$schema": "../schemas/runtime-tuples.schema.json", + "schema_version": 2, "migration_state": "candidate", "promotion_readiness": { "status": "blocked", - "reason": "Every observed alpha2 baseline fails the hardened exact-final-text contract, so none can be certified as the previous rollback tuple.", + "reason": "Every observed alpha2 baseline fails the hardened exact-final-text contract, so none can be certified as the previous rollback tuple; the passing evidence is also bound to unmerged pull-request heads.", "evidence": "evidence/2026-07-18-opencode-ruby-alpha2-regression.json", - "required_action": "Create and canary a distinct rollback consumer commit that pins alpha7 and the exact runtime before promoting any candidate." + "required_action": "Merge, deploy, and canary the exact main/merge tuple (or record an identical-tree attestation plus post-merge canary), then explicitly acknowledge bootstrap-current-only; previous remains null until the next meaningful independently passing release." }, "consumers": { "ajent-rails": { "profile": "rails-persisted-turn", "current": { "status": "observed-production-contract-failed", - "consumer_commit": "83eed3c247a352842b08fc5cf339c4d0acdaacba", + "consumer_commit": "8d8550265dca0ac06a41d5427c585ede56bbb808", "opencode_ruby": { "version": "0.0.1.alpha2", "git_commit": "889d38332f98f5f7b76d16952b7204d8a5b9a662" @@ -22,21 +23,83 @@ "git_commit": "9b0c4cd3cd31bdfebbb89567daca012d68a356ea" }, "runtime": { - "tag_provenance": "docker-registry:5000/opencode-blackline:83eed3c247a352842b08fc5cf339c4d0acdaacba", - "docker_image_id": "sha256:6fb2b3bf8e4cdf48e16bf6c3475e953df2f8304e77f5efe816ceb46f905eead8", + "tag_provenance": "docker-registry:5000/opencode-blackline:8d8550265dca0ac06a41d5427c585ede56bbb808", + "docker_image_id": "sha256:af479243e6ccf1206f57dffd13b8e9d2c028de401cccc954bcc2f00940274c60", + "source_commit": "8d8550265dca0ac06a41d5427c585ede56bbb808", "reported_version": "1.18.3", - "upstream_base": "ghcr.io/anomalyco/opencode@sha256:c2d5d6398df72aac85cb1bdc8f900c71a9b75a33fb7c0a76dc1484e4b126e41e" + "upstream_base": "ghcr.io/anomalyco/opencode@sha256:c2d5d6398df72aac85cb1bdc8f900c71a9b75a33fb7c0a76dc1484e4b126e41e", + "upstream_base_proof": { + "method": "rootfs-prefix-layer-match", + "matched_layer_count": 4, + "status": "pass", + "note": "The deployed images match the exact 1.18.3 base layers even though this main commit selected the mutable upstream tag at build time." + }, + "deployed_product_selection": { + "strategy": "mutable-latest", + "status": "observed-configuration-drift-failure", + "references": { + "aigl": "docker-registry:5000/opencode-aigl:latest", + "blackline": "docker-registry:5000/opencode-blackline:latest", + "raven": "docker-registry:5000/opencode-raven:latest" + }, + "note": "The shared Blackline accessory used the SHA tag above, but Rails ProductConfig resolved per-user AIGL, Blackline, and Raven containers through mutable latest references." + }, + "ci_built_product_images": { + "aigl": { + "tag_provenance": "docker-registry:5000/opencode-aigl:8d8550265dca0ac06a41d5427c585ede56bbb808", + "docker_image_id": "sha256:72c454f39d423618b75439122c9073dbba7c9071421e5f3abd12cf288dc1f898", + "source_commit": "8d8550265dca0ac06a41d5427c585ede56bbb808" + }, + "blackline": { + "tag_provenance": "docker-registry:5000/opencode-blackline:8d8550265dca0ac06a41d5427c585ede56bbb808", + "docker_image_id": "sha256:af479243e6ccf1206f57dffd13b8e9d2c028de401cccc954bcc2f00940274c60", + "source_commit": "8d8550265dca0ac06a41d5427c585ede56bbb808" + }, + "raven": { + "tag_provenance": "docker-registry:5000/opencode-raven:8d8550265dca0ac06a41d5427c585ede56bbb808", + "docker_image_id": "sha256:6c4da94a8dec6160f0b1b4d0e2cd9ff38a3faa18dbd2a8d6023a71f6416a544c", + "source_commit": "8d8550265dca0ac06a41d5427c585ede56bbb808" + } + } } }, "candidate": { "status": "compatibility-certified", - "consumer_commit": "96552893baccd5ccba1592b2585edfef1299b4f9", + "certification_scope": "pre-merge-pr-head-candidate-only", + "promotion_eligible": false, + "promotion_blockers": [ + "consumer-ref-is-an-unmerged-pull-request-head", + "aigl-and-raven-candidate-content-artifacts-have-not-been-built-and-canaried", + "no-certified-previous-tuple" + ], + "consumer_commit": "01cdf9da8e1b3f9a205d681e448aefefd9704db2", + "consumer_ref": { + "kind": "pull-request-head", + "repository": "ajaynomics/ajent-rails", + "commit": "01cdf9da8e1b3f9a205d681e448aefefd9704db2", + "tree": "9e93834ad16b60fa5edfe1cb3baaba193402be74", + "base_commit": "8d8550265dca0ac06a41d5427c585ede56bbb808", + "review_url": "https://gitea.ajay.to/ajaynomics/ajent-rails/pulls/946" + }, "opencode_ruby": { "version": "0.0.1.alpha7", "git_commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", "tag_provenance": { "tag": "v0.0.1.alpha7", "annotated_tag_object": "abfce3ec8cb1ee468107d0845521769502251645" + }, + "source": { + "type": "git", + "uri": "https://gitea.krishnan.ca/ajaynomics/opencode-ruby.git", + "requested_ref": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "locked_revision": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "loaded_source_proof": { + "status": "pass", + "source_class": "Bundler::Source::Git", + "loaded_version": "0.0.1.alpha7", + "observed_revision": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "test": "test/lib/opencode/runtime_image_provenance_test.rb" + } } }, "opencode_rails": { @@ -45,23 +108,61 @@ "tag_provenance": { "tag": "v0.0.1.alpha7", "annotated_tag_object": "61e128084b2b665c0ba20b332987e41794948d7c" + }, + "source": { + "type": "git", + "uri": "https://gitea.krishnan.ca/ajaynomics/opencode-rails.git", + "requested_ref": "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb", + "locked_revision": "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb", + "loaded_source_proof": { + "status": "pass", + "source_class": "Bundler::Source::Git", + "loaded_version": "0.0.1.alpha7", + "observed_revision": "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb", + "test": "test/lib/opencode/runtime_image_provenance_test.rb" + } } }, "runtime": { - "tag_provenance": "ajent-opencode-blackline-canary:96552893", - "docker_image_id": "sha256:239e3dc6cd2958251cda636fb20bd7cd7c46f596bd4c61cb81863ebc49d824e8", - "source_commit": "96552893baccd5ccba1592b2585edfef1299b4f9", "reported_version": "1.18.3", - "upstream_base": "ghcr.io/anomalyco/opencode@sha256:c2d5d6398df72aac85cb1bdc8f900c71a9b75a33fb7c0a76dc1484e4b126e41e" + "upstream_base": "ghcr.io/anomalyco/opencode@sha256:c2d5d6398df72aac85cb1bdc8f900c71a9b75a33fb7c0a76dc1484e4b126e41e", + "product_selection": { + "strategy": "full-consumer-commit-tag", + "source_commit": "01cdf9da8e1b3f9a205d681e448aefefd9704db2", + "references": { + "aigl": "docker-registry:5000/opencode-aigl:01cdf9da8e1b3f9a205d681e448aefefd9704db2", + "blackline": "docker-registry:5000/opencode-blackline:01cdf9da8e1b3f9a205d681e448aefefd9704db2", + "raven": "docker-registry:5000/opencode-raven:01cdf9da8e1b3f9a205d681e448aefefd9704db2" + } + }, + "product_images": { + "blackline": { + "tag_provenance": "ajent-opencode-blackline-canary:01cdf9da8e1b3f9a205d681e448aefefd9704db2", + "docker_image_id": "sha256:075cb49ac7fcb1f8972e051dff96d655fa0d0d911d83a523a6abf03a5e2e2935", + "source_commit": "01cdf9da8e1b3f9a205d681e448aefefd9704db2" + } + } }, - "application_canary": { - "checked_at": "2026-07-18T21:58:00Z", - "path": "Blackline::GenerateResponseJob", - "persisted_final_text": "compat-ok", + "deterministic_contract": { + "checked_at": "2026-07-18T23:16:13Z", + "docker_image_id": "sha256:075cb49ac7fcb1f8972e051dff96d655fa0d0d911d83a523a6abf03a5e2e2935", + "expected_text": "compat-ok", + "full_text": "compat-ok", + "authoritative_assistant_count": 1, "model_request_count": 1, "status": "pass" }, - "certified_at": "2026-07-18T22:03:14Z" + "application_canary": { + "checked_at": "2026-07-18T23:28:16.435369Z", + "path": "Blackline::GenerateResponseJob", + "persisted_final_text": "compat-ok", + "authoritative_final_text": "compat-ok", + "authoritative_assistant_count": 1, + "model_request_count": 1, + "cleanup_status": "pass", + "status": "pass" + }, + "certified_at": "2026-07-18T23:28:16.435369Z" }, "previous": null }, @@ -85,13 +186,41 @@ }, "candidate": { "status": "compatibility-certified", - "consumer_commit": "19747b59fa21e12560afd9edd73ed674ab3a644c", + "certification_scope": "pre-merge-pr-head-candidate-only", + "promotion_eligible": false, + "promotion_blockers": [ + "consumer-ref-is-an-unmerged-pull-request-head", + "no-certified-previous-tuple" + ], + "consumer_commit": "b9a2c2dd361a6b2ea547cc67ee817b8eb4ce31e8", + "consumer_ref": { + "kind": "pull-request-head", + "repository": "ajaynomics/travelwolf", + "commit": "b9a2c2dd361a6b2ea547cc67ee817b8eb4ce31e8", + "tree": "8b3f8a97dc1f1d8b558a49523bf312d2810422df", + "base_commit": "75c92ed5248996e5278ac9a3351cc4eaf03df820", + "review_url": "https://gitea.ajay.to/ajaynomics/travelwolf/pulls/29" + }, "opencode_ruby": { "version": "0.0.1.alpha7", "git_commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", "tag_provenance": { "tag": "v0.0.1.alpha7", "annotated_tag_object": "abfce3ec8cb1ee468107d0845521769502251645" + }, + "source": { + "type": "git", + "uri": "https://gitea.krishnan.ca/ajaynomics/opencode-ruby.git", + "requested_ref": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "locked_revision": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "loaded_source_proof": { + "status": "pass", + "source_class": "Bundler::Source::Git", + "loaded_version": "0.0.1.alpha7", + "observed_ref": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "observed_revision": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "test": "test/services/travelwolf/opencode_env_wiring_test.rb" + } } }, "opencode_rails": { @@ -100,22 +229,52 @@ "tag_provenance": { "tag": "v0.0.1.alpha7", "annotated_tag_object": "61e128084b2b665c0ba20b332987e41794948d7c" + }, + "source": { + "type": "git", + "uri": "https://gitea.krishnan.ca/ajaynomics/opencode-rails.git", + "requested_ref": "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb", + "locked_revision": "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb", + "loaded_source_proof": { + "status": "pass", + "source_class": "Bundler::Source::Git", + "loaded_version": "0.0.1.alpha7", + "observed_ref": "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb", + "observed_revision": "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb", + "test": "test/services/travelwolf/opencode_env_wiring_test.rb" + } } }, "runtime": { "image": "ghcr.io/anomalyco/opencode@sha256:e975a0647576016dfdf77d54b979ca30d32b4750472c10263e9894aad6628c2a", "reported_version": "1.16.1" }, + "deterministic_contract": { + "evidence": "evidence/2026-07-18-opencode-ruby-alpha7.json", + "checked_at": "2026-07-18T23:23:03Z", + "image": "ghcr.io/anomalyco/opencode@sha256:e975a0647576016dfdf77d54b979ca30d32b4750472c10263e9894aad6628c2a", + "opencode_ruby_commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "expected_text": "compat-ok", + "full_text": "compat-ok", + "authoritative_assistant_count": 1, + "model_request_count": 1, + "status": "pass" + }, "application_canary": { - "run_id": "20260718215135-8d8102c0", - "checked_at": "2026-07-18T21:51:56.552Z", - "final_text": "TRAVELWOLF_ALPHA7_SUBSCRIBE_READY", - "application_prompt_count": 1, - "model_request_count_observed": false, + "run_id": "travelwolf-alpha7-ref-20260718T232211Z-48eadf", + "checked_at": "2026-07-18T23:22:26.216140Z", + "final_text": "TRAVELWOLF_ALPHA7_REF_PIN_READY", + "subscription_ready_before_prompt": true, + "opencode_api_prompt_count": 1, + "provider_request_count_observed": false, + "message_roles": [ + "user", + "assistant" + ], "cleanup_status": "pass", "status": "pass" }, - "certified_at": "2026-07-18T22:03:14Z" + "certified_at": "2026-07-18T23:35:57Z" }, "previous": null }, @@ -130,36 +289,87 @@ }, "runtime": { "tag_provenance": "docker-registry:5000/mushu-opencode:sha-99d6328b1", + "image": "docker-registry:5000/mushu-opencode@sha256:ed6293ca87e6db1dc07870d3ed6da21960743e6e2e904533b3c566d334b58d6a", "docker_image_id": "sha256:ed6293ca87e6db1dc07870d3ed6da21960743e6e2e904533b3c566d334b58d6a", "source_commit": "99d6328b18ff8a340a928449a2aa8bc184a063f9", + "base_image": "docker-registry:5000/mushu-opencode-base@sha256:de8a6adeefb30ce1c474cde23fe173dfe059434a13df0a54d14c7473edb33e16", "reported_version": "0.0.0-permission-shortcuts-202606300721" } }, "candidate": { "status": "compatibility-certified", - "consumer_commit": "f26d958b64f214a422aeb629860c8c9a46a4c084", + "certification_scope": "pre-merge-pr-head-candidate-only", + "promotion_eligible": false, + "promotion_blockers": [ + "consumer-ref-is-an-unmerged-pull-request-head", + "no-certified-previous-tuple" + ], + "consumer_commit": "7cff22788b36ae703f6b1b8fa39b52d6dbafe7e0", + "consumer_ref": { + "kind": "pull-request-head", + "repository": "ajaynomics/mushu", + "commit": "7cff22788b36ae703f6b1b8fa39b52d6dbafe7e0", + "tree": "762eb8dcc7042a52cd240ca69acdb9cb45150a84", + "base_commit": "fe16e5bab028e3c92cd5508ef2cebd9bfa23386f", + "review_url": "https://gitea.ajay.to/ajaynomics/mushu/pulls/1" + }, "opencode_ruby": { "version": "0.0.1.alpha7", "git_commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", "tag_provenance": { "tag": "v0.0.1.alpha7", "annotated_tag_object": "abfce3ec8cb1ee468107d0845521769502251645" + }, + "source": { + "type": "git", + "uri": "https://gitea.krishnan.ca/ajaynomics/opencode-ruby.git", + "requested_ref": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "locked_revision": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "loaded_source_proof": { + "status": "pass", + "source_class": "Bundler::Source::Git", + "loaded_version": "0.0.1.alpha7", + "observed_ref": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "observed_revision": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "loaded_file_within_spec": true, + "test": "test/services/mushu/runtime/opencode_dependency_provenance_test.rb" + } } }, "runtime": { "tag_provenance": "docker-registry:5000/mushu-opencode:sha-99d6328b1", + "image": "docker-registry:5000/mushu-opencode@sha256:ed6293ca87e6db1dc07870d3ed6da21960743e6e2e904533b3c566d334b58d6a", "docker_image_id": "sha256:ed6293ca87e6db1dc07870d3ed6da21960743e6e2e904533b3c566d334b58d6a", "source_commit": "99d6328b18ff8a340a928449a2aa8bc184a063f9", + "base_image": "docker-registry:5000/mushu-opencode-base@sha256:de8a6adeefb30ce1c474cde23fe173dfe059434a13df0a54d14c7473edb33e16", "reported_version": "0.0.0-permission-shortcuts-202606300721" }, - "application_canary": { - "run_id": "mushu-alpha7-20260718T215300Z-f26d958", - "checked_at": "2026-07-18T21:53:00Z", - "persisted_final_text": "compat-ok", + "deterministic_contract": { + "checked_at": "2026-07-18T23:23:41Z", + "image": "docker-registry:5000/mushu-opencode@sha256:ed6293ca87e6db1dc07870d3ed6da21960743e6e2e904533b3c566d334b58d6a", + "expected_text": "compat-ok", + "full_text": "compat-ok", + "authoritative_assistant_count": 1, "model_request_count": 1, "status": "pass" }, - "certified_at": "2026-07-18T22:03:14Z" + "application_canary": { + "checked_at": "2026-07-18T23:26:06.865190Z", + "response_text": "compat-ok", + "authoritative_final_text": "compat-ok", + "message_roles": [ + "user", + "assistant" + ], + "authoritative_assistant_count": 1, + "provider_request_count": 1, + "mapping_and_last_turn_persisted": true, + "active_claim_cleared": true, + "opencode_session_deleted": true, + "synthetic_database_rows_after_cleanup": 0, + "status": "pass" + }, + "certified_at": "2026-07-18T23:26:06.865190Z" }, "previous": null } diff --git a/profiles/plugin-ledger.json b/profiles/plugin-ledger.json index 2fde50e..0b67192 100644 --- a/profiles/plugin-ledger.json +++ b/profiles/plugin-ledger.json @@ -1,6 +1,8 @@ { "id": "plugin-ledger", "owner": "opencode-ajent", + "certification_mode": "executable-consumer-attestation", + "shared_ruby_probe_sufficient": false, "required_behaviors": [ "exact-upstream-image", "exact-plugin-version", diff --git a/profiles/provider-hooks.json b/profiles/provider-hooks.json index 99cfacc..0a12f27 100644 --- a/profiles/provider-hooks.json +++ b/profiles/provider-hooks.json @@ -1,6 +1,8 @@ { "id": "provider-hooks", "owner": "inference", + "certification_mode": "executable-consumer-attestation", + "shared_ruby_probe_sufficient": false, "required_behaviors": [ "provider-schema", "model-routing", diff --git a/profiles/rails-persisted-turn.json b/profiles/rails-persisted-turn.json index f29f755..397d69b 100644 --- a/profiles/rails-persisted-turn.json +++ b/profiles/rails-persisted-turn.json @@ -1,6 +1,8 @@ { "id": "rails-persisted-turn", "owner": "opencode-rails", + "certification_mode": "executable-consumer-attestation", + "shared_ruby_probe_sufficient": false, "extends": ["ruby-rest-sse"], "required_behaviors": [ "session-recovery", diff --git a/profiles/ruby-rest-sse.json b/profiles/ruby-rest-sse.json index 33e67d3..fbb8318 100644 --- a/profiles/ruby-rest-sse.json +++ b/profiles/ruby-rest-sse.json @@ -1,6 +1,8 @@ { "id": "ruby-rest-sse", "owner": "opencode-ruby", + "certification_mode": "shared-ruby-fixture-and-live-probe", + "shared_ruby_probe_sufficient": true, "required_fixtures": [ "legacy-idle", "current-status-idle", diff --git a/profiles/strict-v2.json b/profiles/strict-v2.json index 9f5a5ef..0f55dd2 100644 --- a/profiles/strict-v2.json +++ b/profiles/strict-v2.json @@ -1,6 +1,8 @@ { "id": "strict-v2", "owner": "leela", + "certification_mode": "executable-consumer-attestation", + "shared_ruby_probe_sufficient": false, "required_behaviors": [ "exact-upstream-version-and-commit", "bun-toolchain-provenance", diff --git a/profiles/voice-stream.json b/profiles/voice-stream.json index 334e8f6..4a94ce3 100644 --- a/profiles/voice-stream.json +++ b/profiles/voice-stream.json @@ -1,6 +1,8 @@ { "id": "voice-stream", "owner": "greenroom", + "certification_mode": "executable-consumer-attestation", + "shared_ruby_probe_sufficient": false, "extends": ["ruby-rest-sse"], "required_behaviors": [ "legacy-and-current-terminal-events", diff --git a/schemas/runtime-tuples.schema.json b/schemas/runtime-tuples.schema.json new file mode 100644 index 0000000..7a229ce --- /dev/null +++ b/schemas/runtime-tuples.schema.json @@ -0,0 +1,401 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/ajaynomics/opencode-compat/schemas/runtime-tuples.schema.json", + "title": "OpenCode runtime tuple ledger", + "type": "object", + "required": [ + "schema_version", + "migration_state", + "promotion_readiness", + "consumers" + ], + "properties": { + "schema_version": { + "const": 2 + }, + "migration_state": { + "enum": [ + "candidate", + "bootstrap-current-only", + "certified" + ] + }, + "consumers": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": [ + "profile", + "current", + "candidate", + "previous" + ], + "properties": { + "rollback_state": { + "type": "object", + "required": [ + "status", + "acknowledgement", + "recorded_at", + "emergency_provenance_status" + ], + "properties": { + "status": { + "const": "degraded-no-certified-previous" + }, + "acknowledgement": { + "const": "accept-degraded-rollback-with-failed-emergency-provenance" + }, + "emergency_provenance_status": { + "const": "observed-production-contract-failed" + } + } + }, + "candidate": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/candidate" + } + ] + } + } + } + } + }, + "$defs": { + "fullCommit": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + }, + "loadedGitSource": { + "type": "object", + "required": [ + "type", + "uri", + "requested_ref", + "locked_revision", + "loaded_source_proof" + ], + "properties": { + "type": { + "const": "git" + }, + "requested_ref": { + "$ref": "#/$defs/fullCommit" + }, + "locked_revision": { + "$ref": "#/$defs/fullCommit" + }, + "loaded_source_proof": { + "type": "object", + "required": [ + "status", + "source_class", + "loaded_version", + "observed_revision", + "test" + ], + "properties": { + "status": { + "const": "pass" + }, + "source_class": { + "const": "Bundler::Source::Git" + }, + "observed_ref": { + "$ref": "#/$defs/fullCommit" + }, + "observed_revision": { + "$ref": "#/$defs/fullCommit" + } + } + } + } + }, + "client": { + "type": "object", + "required": [ + "version", + "git_commit", + "source" + ], + "properties": { + "git_commit": { + "$ref": "#/$defs/fullCommit" + }, + "source": { + "$ref": "#/$defs/loadedGitSource" + } + } + }, + "consumerRef": { + "type": "object", + "required": [ + "kind", + "repository", + "commit", + "tree" + ], + "properties": { + "kind": { + "enum": [ + "pull-request-head", + "main-commit" + ] + }, + "commit": { + "$ref": "#/$defs/fullCommit" + }, + "tree": { + "$ref": "#/$defs/fullCommit" + }, + "base_commit": { + "$ref": "#/$defs/fullCommit" + } + }, + "allOf": [ + { + "if": { + "properties": { + "kind": { + "const": "pull-request-head" + } + } + }, + "then": { + "required": [ + "base_commit", + "review_url" + ] + } + } + ] + }, + "promotionProvenance": { + "oneOf": [ + { + "type": "object", + "required": [ + "kind", + "main_commit", + "post_merge_canary" + ], + "properties": { + "kind": { + "const": "main-commit" + }, + "main_commit": { + "$ref": "#/$defs/fullCommit" + }, + "post_merge_canary": { + "type": "object", + "required": [ + "status", + "checked_at", + "evidence" + ], + "properties": { + "status": { + "const": "pass" + }, + "evidence": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "pattern": "^evidence/" + } + } + } + } + } + }, + { + "type": "object", + "required": [ + "kind", + "pull_request_commit", + "pull_request_tree", + "main_commit", + "main_tree", + "attested_at", + "post_merge_canary" + ], + "properties": { + "kind": { + "const": "identical-tree-attestation" + }, + "pull_request_commit": { + "$ref": "#/$defs/fullCommit" + }, + "pull_request_tree": { + "$ref": "#/$defs/fullCommit" + }, + "main_commit": { + "$ref": "#/$defs/fullCommit" + }, + "main_tree": { + "$ref": "#/$defs/fullCommit" + }, + "post_merge_canary": { + "type": "object", + "required": [ + "status", + "checked_at", + "evidence" + ], + "properties": { + "status": { + "const": "pass" + }, + "evidence": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "pattern": "^evidence/" + } + } + } + } + } + } + ] + }, + "runtime": { + "type": "object", + "required": [ + "reported_version" + ], + "properties": { + "product_selection": { + "type": "object", + "required": [ + "strategy", + "source_commit", + "references" + ], + "properties": { + "strategy": { + "const": "full-consumer-commit-tag" + }, + "source_commit": { + "$ref": "#/$defs/fullCommit" + }, + "references": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "type": "string", + "pattern": ":[0-9a-f]{40}$" + } + } + } + }, + "product_images": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "type": "object", + "required": [ + "source_commit" + ], + "properties": { + "source_commit": { + "$ref": "#/$defs/fullCommit" + }, + "docker_image_id": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + }, + "image": { + "type": "string", + "pattern": "@sha256:[0-9a-f]{64}$" + } + }, + "anyOf": [ + { + "required": [ + "docker_image_id" + ] + }, + { + "required": [ + "image" + ] + } + ] + } + } + } + }, + "candidate": { + "type": "object", + "required": [ + "status", + "certification_scope", + "promotion_eligible", + "consumer_commit", + "consumer_ref", + "opencode_ruby", + "runtime" + ], + "properties": { + "status": { + "const": "compatibility-certified" + }, + "certification_scope": { + "enum": [ + "pre-merge-pr-head-candidate-only", + "promotion-deployed" + ] + }, + "promotion_eligible": { + "type": "boolean" + }, + "consumer_commit": { + "$ref": "#/$defs/fullCommit" + }, + "consumer_ref": { + "$ref": "#/$defs/consumerRef" + }, + "promotion_provenance": { + "$ref": "#/$defs/promotionProvenance" + }, + "opencode_ruby": { + "$ref": "#/$defs/client" + }, + "opencode_rails": { + "$ref": "#/$defs/client" + }, + "runtime": { + "$ref": "#/$defs/runtime" + } + }, + "allOf": [ + { + "if": { + "properties": { + "promotion_eligible": { + "const": true + } + } + }, + "then": { + "required": [ + "promotion_provenance" + ], + "properties": { + "certification_scope": { + "const": "promotion-deployed" + } + } + } + } + ] + } + } +} diff --git a/scripts/promote_runtime_tuple.rb b/scripts/promote_runtime_tuple.rb index a0974e3..06d75c6 100755 --- a/scripts/promote_runtime_tuple.rb +++ b/scripts/promote_runtime_tuple.rb @@ -20,6 +20,10 @@ parser = OptionParser.new do |opts| --status pass --certified-at TIMESTAMP --evidence evidence/FILE.json \\ [--previous-status pass --previous-certified-at TIMESTAMP \\ --previous-evidence evidence/FILE.json] [--dry-run] + ruby scripts/promote_runtime_tuple.rb bootstrap-current --consumer NAME --consumer-commit SHA \\ + --status pass --certified-at TIMESTAMP --evidence evidence/FILE.json \\ + --acknowledge-degraded-rollback \\ + accept-degraded-rollback-with-failed-emergency-provenance [--dry-run] USAGE opts.on("--consumer NAME") { |value| options["consumer"] = value } @@ -30,6 +34,9 @@ parser = OptionParser.new do |opts| opts.on("--previous-status STATUS") { |value| options["previous_status"] = value } opts.on("--previous-certified-at TIMESTAMP") { |value| options["previous_certified_at"] = value } opts.on("--previous-evidence PATH") { |value| options["previous_evidence"] << value } + opts.on("--acknowledge-degraded-rollback VALUE") do |value| + options["degraded_rollback_acknowledgement"] = value + end opts.on("--dry-run") { options["dry_run"] = true } end @@ -94,8 +101,42 @@ begin "migration_state" => promoted.fetch("migration_state") ) end + when "bootstrap-current" + required!( + options, + "consumer", + "consumer_commit", + "status", + "certified_at", + "evidence", + "degraded_rollback_acknowledgement" + ) + bootstrapped = promoter.bootstrap_current( + consumer: options.fetch("consumer"), + consumer_commit: options.fetch("consumer_commit"), + certification: { + "status" => options.fetch("status"), + "certified_at" => options.fetch("certified_at"), + "evidence" => options.fetch("evidence") + }, + acknowledgement: options.fetch("degraded_rollback_acknowledgement"), + dry_run: options.fetch("dry_run", false) + ) + if options.fetch("dry_run", false) + puts JSON.pretty_generate(bootstrapped) + else + consumer = bootstrapped.fetch("consumers").fetch(options.fetch("consumer")) + puts JSON.pretty_generate( + "consumer" => options.fetch("consumer"), + "current_consumer_commit" => consumer.dig("current", "consumer_commit"), + "current_tuple_sha256" => consumer.dig("current", "certification", "tuple_sha256"), + "previous" => consumer.fetch("previous"), + "rollback_state" => consumer.fetch("rollback_state"), + "migration_state" => bootstrapped.fetch("migration_state") + ) + end else - raise OptionParser::InvalidArgument, "command must be fingerprint or promote" + raise OptionParser::InvalidArgument, "command must be fingerprint, promote, or bootstrap-current" end rescue OpenCodeCompat::PromotionError, OptionParser::ParseError => e warn "error: #{e.message}" diff --git a/test/repository_test.rb b/test/repository_test.rb index 8611293..aa51885 100644 --- a/test/repository_test.rb +++ b/test/repository_test.rb @@ -12,7 +12,7 @@ class RepositoryTest < Minitest::Test end def test_every_json_document_parses - paths = Dir.glob(File.join(ROOT, "{evidence,fixtures,manifests,profiles}/**/*.json")) + paths = Dir.glob(File.join(ROOT, "{evidence,fixtures,manifests,profiles,schemas}/**/*.json")) refute_empty paths paths.each { |path| JSON.parse(File.read(path)) } end @@ -36,6 +36,19 @@ class RepositoryTest < Minitest::Test end end + def test_only_ruby_rest_sse_can_be_certified_by_the_shared_ruby_probe + Dir.glob(File.join(ROOT, "profiles/*.json")).each do |path| + profile = JSON.parse(File.read(path)) + if profile.fetch("id") == "ruby-rest-sse" + assert_equal "shared-ruby-fixture-and-live-probe", profile.fetch("certification_mode") + assert_equal true, profile.fetch("shared_ruby_probe_sufficient") + else + assert_equal "executable-consumer-attestation", profile.fetch("certification_mode") + assert_equal false, profile.fetch("shared_ruby_probe_sufficient") + end + end + end + def test_public_matrix_uses_only_immutable_oci_digests targets = json("manifests/image-matrix.json").fetch("public_ci") refute_empty targets @@ -78,6 +91,10 @@ class RepositoryTest < Minitest::Test def test_runtime_tuple_profiles_exist tuples = json("manifests/runtime-tuples.json") + schema = json("schemas/runtime-tuples.schema.json") + + assert_equal 2, tuples.fetch("schema_version") + assert_equal 2, schema.dig("properties", "schema_version", "const") tuples.fetch("consumers").each_value do |consumer| profile = consumer.fetch("profile") assert_path_exists File.join(ROOT, "profiles", "#{profile}.json") @@ -120,6 +137,96 @@ class RepositoryTest < Minitest::Test end end + def test_pull_request_heads_are_explicitly_candidate_only + tuples = json("manifests/runtime-tuples.json") + + tuples.fetch("consumers").each do |name, consumer| + candidate = consumer.fetch("candidate") + reference = candidate.fetch("consumer_ref") + + assert_equal "pre-merge-pr-head-candidate-only", candidate.fetch("certification_scope"), name + assert_equal false, candidate.fetch("promotion_eligible"), name + assert_equal "pull-request-head", reference.fetch("kind"), name + assert_equal candidate.fetch("consumer_commit"), reference.fetch("commit"), name + assert_match(/\A[0-9a-f]{40}\z/, reference.fetch("tree"), name) + assert_match(%r{\Ahttps://.+/pulls/\d+\z}, reference.fetch("review_url"), name) + end + end + + def test_candidate_gems_use_peeled_refs_and_loaded_source_proof + tuples = json("manifests/runtime-tuples.json") + + tuples.fetch("consumers").each do |consumer_name, consumer| + candidate = consumer.fetch("candidate") + %w[opencode_ruby opencode_rails].each do |client_name| + client = candidate[client_name] + next unless client + + commit = client.fetch("git_commit") + source = client.fetch("source") + proof = source.fetch("loaded_source_proof") + + assert_equal "git", source.fetch("type"), "#{consumer_name} #{client_name}" + assert_equal commit, source.fetch("requested_ref"), "#{consumer_name} #{client_name}" + assert_equal commit, source.fetch("locked_revision"), "#{consumer_name} #{client_name}" + assert_equal "pass", proof.fetch("status"), "#{consumer_name} #{client_name}" + assert_equal "Bundler::Source::Git", proof.fetch("source_class"), "#{consumer_name} #{client_name}" + assert_equal client.fetch("version"), proof.fetch("loaded_version"), "#{consumer_name} #{client_name}" + assert_equal commit, proof.fetch("observed_ref"), "#{consumer_name} #{client_name}" if proof.key?("observed_ref") + assert_equal commit, proof.fetch("observed_revision"), "#{consumer_name} #{client_name}" + refute_empty proof.fetch("test"), "#{consumer_name} #{client_name}" + end + end + end + + def test_ajent_records_current_selection_drift_and_exact_candidate_selection + ajent = json("manifests/runtime-tuples.json").fetch("consumers").fetch("ajent-rails") + expected_products = %w[aigl blackline raven] + + current_runtime = ajent.fetch("current").fetch("runtime") + assert_match(/\Asha256:[0-9a-f]{64}\z/, current_runtime.fetch("docker_image_id")) + deployed = current_runtime.fetch("deployed_product_selection") + assert_equal "mutable-latest", deployed.fetch("strategy") + assert_equal "observed-configuration-drift-failure", deployed.fetch("status") + assert_equal expected_products, deployed.fetch("references").keys.sort + deployed.fetch("references").each_value { |reference| assert reference.end_with?(":latest") } + + built = current_runtime.fetch("ci_built_product_images") + assert_equal expected_products, built.keys.sort + built.each_value do |coordinate| + assert_match(/\Asha256:[0-9a-f]{64}\z/, coordinate.fetch("docker_image_id")) + assert_equal ajent.dig("current", "consumer_commit"), coordinate.fetch("source_commit") + end + + candidate_runtime = ajent.fetch("candidate").fetch("runtime") + selected = candidate_runtime.fetch("product_selection") + assert_equal expected_products, selected.fetch("references").keys.sort + selected.fetch("references").each_value do |reference| + assert reference.end_with?(":#{ajent.dig('candidate', 'consumer_commit')}") + refute reference.end_with?(":latest") + end + assert_equal ["blackline"], candidate_runtime.fetch("product_images").keys + assert_includes ajent.dig("candidate", "promotion_blockers"), + "aigl-and-raven-candidate-content-artifacts-have-not-been-built-and-canaried" + end + + def test_manifest_candidates_cannot_be_promoted_from_pre_merge_evidence + promoter = OpenCodeCompat::RuntimeTuplePromoter.new(root: ROOT) + tuples = json("manifests/runtime-tuples.json") + + tuples.fetch("consumers").each do |name, consumer| + error = assert_raises(OpenCodeCompat::PromotionError) do + promoter.promote( + consumer: name, + consumer_commit: consumer.dig("candidate", "consumer_commit"), + certification: {}, + dry_run: true + ) + end + assert_match(/pre-merge pull-request evidence is candidate-only/, error.message) + end + end + def test_candidate_evidence_is_bound_to_complete_tuple_fingerprints promoter = OpenCodeCompat::RuntimeTuplePromoter.new(root: ROOT) evidence_paths = { @@ -138,6 +245,8 @@ class RepositoryTest < Minitest::Test assert_equal consumer, evidence.fetch("consumer") assert_equal fingerprints.fetch("profile"), evidence.fetch("profile") assert_equal fingerprints.fetch("candidate_tuple_sha256"), evidence.fetch("tuple_sha256") + assert_equal "pre-merge-pr-head-candidate-only", evidence.fetch("certification_scope") + assert_equal false, evidence.fetch("promotion_eligible") assert_equal "pass", evidence.fetch("status") end end diff --git a/test/runtime_tuple_promoter_test.rb b/test/runtime_tuple_promoter_test.rb index c22d474..53c7973 100644 --- a/test/runtime_tuple_promoter_test.rb +++ b/test/runtime_tuple_promoter_test.rb @@ -18,6 +18,7 @@ class RuntimeTuplePromoterTest < Minitest::Test CANDIDATE_IMAGE = "ghcr.io/anomalyco/opencode@sha256:#{'b' * 64}" CURRENT_TIME = "2026-07-17T12:00:00Z" CANDIDATE_TIME = "2026-07-18T12:00:00Z" + CANDIDATE_TREE = "7" * 40 def setup @root = Dir.mktmpdir("opencode-compat-promotion") @@ -25,6 +26,7 @@ class RuntimeTuplePromoterTest < Minitest::Test FileUtils.mkdir_p(File.join(@root, "evidence")) FileUtils.mkdir_p(File.join(@root, "profiles")) File.write(File.join(@root, "profiles", "rails-persisted-turn.json"), "{}\n") + write_post_merge_canary_evidence("post-merge-canary.json", CANDIDATE_COMMIT) write_manifest(valid_manifest) @promoter = OpenCodeCompat::RuntimeTuplePromoter.new(root: @root) end @@ -175,6 +177,154 @@ class RuntimeTuplePromoterTest < Minitest::Test assert_equal before, File.binread(manifest_path) end + def test_rejects_pre_merge_pull_request_evidence_as_candidate_only + manifest = valid_manifest + candidate = manifest.dig("consumers", CONSUMER, "candidate") + candidate["certification_scope"] = "pre-merge-pr-head-candidate-only" + candidate["promotion_eligible"] = false + candidate["consumer_ref"] = { + "kind" => "pull-request-head", + "repository" => "example/consumer", + "commit" => CANDIDATE_COMMIT, + "tree" => CANDIDATE_TREE, + "base_commit" => CURRENT_COMMIT, + "review_url" => "https://example.test/pulls/1" + } + candidate.delete("promotion_provenance") + write_manifest(manifest) + candidate_evidence, previous_evidence = write_matching_evidence + + error = assert_raises(OpenCodeCompat::PromotionError) do + @promoter.promote( + consumer: CONSUMER, + consumer_commit: CANDIDATE_COMMIT, + certification: certification(CANDIDATE_TIME, candidate_evidence), + previous_certification: certification(CURRENT_TIME, previous_evidence), + dry_run: true + ) + end + + assert_match(/pre-merge pull-request evidence is candidate-only/, error.message) + end + + def test_accepts_explicit_identical_tree_attestation_with_post_merge_canary + manifest = valid_manifest + candidate = manifest.dig("consumers", CONSUMER, "candidate") + candidate["consumer_ref"] = { + "kind" => "pull-request-head", + "repository" => "example/consumer", + "commit" => CANDIDATE_COMMIT, + "tree" => CANDIDATE_TREE, + "base_commit" => CURRENT_COMMIT, + "review_url" => "https://example.test/pulls/1" + } + main_commit = "8" * 40 + write_post_merge_canary_evidence("post-merge-attested-canary.json", main_commit) + candidate["promotion_provenance"] = { + "kind" => "identical-tree-attestation", + "pull_request_commit" => CANDIDATE_COMMIT, + "pull_request_tree" => CANDIDATE_TREE, + "main_commit" => main_commit, + "main_tree" => CANDIDATE_TREE, + "attested_at" => CANDIDATE_TIME, + "post_merge_canary" => { + "status" => "pass", + "checked_at" => CANDIDATE_TIME, + "evidence" => ["evidence/post-merge-attested-canary.json"] + } + } + write_manifest(manifest) + candidate_evidence, previous_evidence = write_matching_evidence + + promoted = @promoter.promote( + consumer: CONSUMER, + consumer_commit: CANDIDATE_COMMIT, + certification: certification(CANDIDATE_TIME, candidate_evidence), + previous_certification: certification(CURRENT_TIME, previous_evidence), + dry_run: true + ) + + assert_equal "certified", promoted.dig("consumers", CONSUMER, "current", "status") + end + + def test_main_commit_promotion_still_requires_a_post_merge_canary + manifest = valid_manifest + manifest.dig("consumers", CONSUMER, "candidate", "promotion_provenance").delete("post_merge_canary") + write_manifest(manifest) + + error = assert_raises(OpenCodeCompat::PromotionError) do + @promoter.promote( + consumer: CONSUMER, + consumer_commit: CANDIDATE_COMMIT, + certification: {}, + dry_run: true + ) + end + + assert_match(/passing post-merge canary/, error.message) + end + + def test_rejects_candidate_without_loaded_exact_ref_source_proof + manifest = valid_manifest + manifest.dig("consumers", CONSUMER, "candidate", "opencode_ruby").delete("source") + write_manifest(manifest) + + error = assert_raises(OpenCodeCompat::PromotionError) do + @promoter.fingerprints(consumer: CONSUMER, consumer_commit: CANDIDATE_COMMIT) + end + + assert_match(/loaded exact-ref source proof/, error.message) + end + + def test_explicit_degraded_bootstrap_certifies_current_without_faking_previous + manifest = valid_manifest + failed = manifest.dig("consumers", CONSUMER, "current") + failed["status"] = "observed-production-contract-failed" + write_manifest(manifest) + fingerprint = @promoter.fingerprints( + consumer: CONSUMER, + consumer_commit: CANDIDATE_COMMIT + ).fetch("candidate_tuple_sha256") + evidence = write_evidence( + "bootstrap-candidate.json", + commit: CANDIDATE_COMMIT, + timestamp: CANDIDATE_TIME, + fingerprint: fingerprint + ) + + bootstrapped = @promoter.bootstrap_current( + consumer: CONSUMER, + consumer_commit: CANDIDATE_COMMIT, + certification: certification(CANDIDATE_TIME, evidence), + acknowledgement: OpenCodeCompat::RuntimeTuplePromoter::DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT, + dry_run: true + ) + + consumer = bootstrapped.dig("consumers", CONSUMER) + assert_equal "certified", consumer.dig("current", "status") + assert_equal CANDIDATE_COMMIT, consumer.dig("current", "consumer_commit") + assert_nil consumer["candidate"] + assert_nil consumer["previous"] + assert_equal "observed-production-contract-failed", consumer.dig("emergency_provenance", "status") + assert_nil consumer.dig("emergency_provenance", "certification") + assert_equal "degraded-no-certified-previous", consumer.dig("rollback_state", "status") + assert_equal "bootstrap-current-only", bootstrapped.fetch("migration_state") + end + + def test_degraded_bootstrap_requires_exact_explicit_acknowledgement + error = assert_raises(OpenCodeCompat::PromotionError) do + @promoter.bootstrap_current( + consumer: CONSUMER, + consumer_commit: CANDIDATE_COMMIT, + certification: {}, + acknowledgement: "yes", + dry_run: true + ) + end + + assert_match(/explicit acknowledgement/, error.message) + end + def test_rejects_evidence_that_does_not_match_the_complete_tuple candidate, previous = write_matching_evidence manifest = JSON.parse(File.read(manifest_path)) @@ -254,7 +404,7 @@ class RuntimeTuplePromoterTest < Minitest::Test def valid_manifest { - "schema_version" => 1, + "schema_version" => 2, "migration_state" => "candidate", "consumers" => { CONSUMER => { @@ -269,8 +419,25 @@ class RuntimeTuplePromoterTest < Minitest::Test "candidate" => { "status" => "compatibility-certified", "certified_at" => "2026-07-18T10:00:00Z", - "opencode_ruby" => {"version" => "0.0.1.alpha7", "git_commit" => RUBY_CANDIDATE}, - "opencode_rails" => {"version" => "0.0.1.alpha7", "git_commit" => RAILS_CANDIDATE}, + "certification_scope" => "promotion-deployed", + "promotion_eligible" => true, + "consumer_ref" => { + "kind" => "main-commit", + "repository" => "example/consumer", + "commit" => CANDIDATE_COMMIT, + "tree" => CANDIDATE_TREE + }, + "promotion_provenance" => { + "kind" => "main-commit", + "main_commit" => CANDIDATE_COMMIT, + "post_merge_canary" => { + "status" => "pass", + "checked_at" => CANDIDATE_TIME, + "evidence" => ["evidence/post-merge-canary.json"] + } + }, + "opencode_ruby" => client("0.0.1.alpha7", RUBY_CANDIDATE), + "opencode_rails" => client("0.0.1.alpha7", RAILS_CANDIDATE), "runtime" => {"image" => CANDIDATE_IMAGE, "reported_version" => "1.18.3"} }, "previous" => nil @@ -279,6 +446,27 @@ class RuntimeTuplePromoterTest < Minitest::Test } end + def client(version, commit) + { + "version" => version, + "git_commit" => commit, + "source" => { + "type" => "git", + "uri" => "https://example.test/client.git", + "requested_ref" => commit, + "locked_revision" => commit, + "loaded_source_proof" => { + "status" => "pass", + "source_class" => "Bundler::Source::Git", + "loaded_version" => version, + "observed_ref" => commit, + "observed_revision" => commit, + "test" => "test/dependency_provenance_test.rb" + } + } + } + end + def write_matching_evidence fingerprints = @promoter.fingerprints(consumer: CONSUMER, consumer_commit: CANDIDATE_COMMIT) candidate = write_evidence( @@ -311,6 +499,16 @@ class RuntimeTuplePromoterTest < Minitest::Test {"path" => path, "tuple_sha256" => fingerprint} end + def write_post_merge_canary_evidence(name, commit) + document = { + "schema_version" => 1, + "status" => "pass", + "checked_at" => CANDIDATE_TIME, + "consumer_commit" => commit + } + File.write(File.join(@root, "evidence", name), JSON.pretty_generate(document) + "\n") + end + def certification(timestamp, evidence) { "status" => "pass",