diff --git a/README.md b/README.md index 310cdaf..f7806f6 100644 --- a/README.md +++ b/README.md @@ -93,10 +93,12 @@ and must not be described as review evidence. A person must review a passing GitHub artifact and commit the durable certification document under `evidence/`; no workflow commits or promotes its own result. -`manifests/image-matrix.json` binds the active pending matrix to both candidate -commits. Its `previous_certification` entries preserve the last alpha7 result; -they do not certify alpha8. Ajent and Mushu custom-image rows remain pending -until their host canaries run against this exact pair. +`manifests/image-matrix.json` binds the certified shared-client matrix to both +exact, unpublished alpha8 commits. Each public row is explicitly scoped to the +shared Ruby/SSE contract; its separate `required_consumer_profiles` are not +implied by that result. The host rows bind the Ajent, Travelwolf, and Mushu +application canaries to their exact current alpha8 and previous alpha7 consumer +commits and durable evidence documents. The checked-in alpha8 local exact-image result is explicitly marked `not-certified` and `shared-client-contract-only`. It proves the Ruby/SSE probe @@ -122,7 +124,9 @@ durable certification documents and Git history are the archive. This keeps a fast-moving upstream from making every future run retest every old release. See [docs/certification.md](docs/certification.md) for promotion, canary, and -rollback evidence requirements. +rollback evidence requirements, and [docs/maintenance.md](docs/maintenance.md) +for consumer ownership, release order, runner boundaries, and expected failure +modes. ## License diff --git a/docs/certification.md b/docs/certification.md index 6d6c571..a0930f7 100644 --- a/docs/certification.md +++ b/docs/certification.md @@ -85,6 +85,13 @@ must explicitly contain values matching the promotion: } ``` +The promotion command stores each accepted document as a hash-bound reference, +`{"path":"evidence/...json","sha256":"sha256:..."}`. Later transitions +re-read the bytes, verify that digest, and cross-check any detailed consumer +tree, client, and runtime coordinates against the tuple. Editing an evidence +document therefore invalidates the certification until it is reviewed and +promoted again; a copied `tuple_sha256` alone is not sufficient. + Preview the exact manifest transition with `--dry-run`, then repeat without it to write the manifest: @@ -153,16 +160,34 @@ 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. +non-certified (`bootstrap-current-only` or `candidate`) and the candidate PR +must not be treated as deploy authorization. -The schema-v1 promotion command deliberately cannot perform the first degraded -bootstrap transition when `current` is a known-failing baseline. Do not bypass -that guard by hand-editing the manifest or relabeling alpha2 evidence. A -separate reviewed state-machine change must first add an explicit -`bootstrap-current-only` state that preserves the failed baseline as -uncertified emergency provenance and leaves `previous` null. Itemized rollback -certification is complete only after a later, materially distinct passing tuple -can move the first certified current into `previous`. +Use the explicit degraded-bootstrap transition for the first passing rollback +tuple. It requires passing evidence bound to the complete candidate fingerprint +and a literal acknowledgement; it preserves the failed baseline as uncertified +`emergency_provenance`, leaves `previous` null, and records the temporary +`bootstrap-current-only` state: + +```sh +ruby scripts/promote_runtime_tuple.rb bootstrap-current \ + --consumer travelwolf \ + --consumer-commit FULL_40_CHARACTER_ROLLBACK_COMMIT \ + --status pass \ + --certified-at 2026-07-20T12:00:00Z \ + --evidence evidence/travelwolf-rollback.json \ + --acknowledge-degraded-rollback \ + accept-degraded-rollback-with-failed-emergency-provenance \ + --dry-run +``` + +Review the dry run, repeat without `--dry-run`, and then treat that passing +tuple as the base for a normal promotion. Normal promotion moves it into +`previous`, removes the degraded rollback marker, and keeps the original failed +baseline only as historical emergency provenance. The repository becomes +`certified` only when every consumer has exact passing `current` and `previous` +tuples. Never use bootstrap to replace an existing previous tuple or to certify +a baseline already known to fail. ## Rollback diff --git a/docs/maintenance.md b/docs/maintenance.md new file mode 100644 index 0000000..1ebad26 --- /dev/null +++ b/docs/maintenance.md @@ -0,0 +1,158 @@ +# Maintaining OpenCode consumers + +OpenCode compatibility is an executable tuple, not a gem version. The tuple is +the exact client commit, Rails adapter commit where used, OpenCode image digest, +consumer commit, compatibility profile, and passing evidence. A change to any +coordinate creates a new candidate. + +## Dependency ownership + +The adjacent projects intentionally do not all use the same gems: + +| Consumer | OpenCode boundary | Owned dependency | +| --- | --- | --- | +| Ajent Rails | Ruby REST/SSE plus persisted Rails turns | `opencode-ruby` and `opencode-rails` | +| Travelwolf | Ruby REST/SSE plus persisted Rails turns and Sprite lifecycle | `opencode-ruby` and `opencode-rails` | +| Mushu | Ruby REST/SSE with application-owned conversation, claim, recovery, Telegram scope, and idempotency | `opencode-ruby` only | +| Greenroom | Direct voice-stream worker | No Ruby adapter gem | +| Leela | Custom strict-v2 server, security, and toolchain lane | No Ruby adapter gem | +| opencode-ajent | Native CLI and plugin hook/event lane | No Ruby adapter gem | +| inference | Provider configuration, hooks, routing, and migration lane | No Ruby adapter gem | +| Context Kit | MCP and OpenCode configuration producer | No Ruby adapter gem | + +Do not add `opencode-rails` to an application that owns different persistence +semantics, and do not route plugin, provider, voice, or strict-v2 behavior +through the Ruby REST/SSE adapter merely to make versions look uniform. Share +fixtures, provenance, and promotion policy across those lanes instead. + +Generated release snapshots, detached operational copies, editor locks, caches, +and linked task worktrees are evidence or tooling. They are not additional +consumers and must not be bulk-upgraded. + +Direct consumers still need immutable inputs even though they do not use the +Ruby gems. A system service must execute a clean certified checkout or image, +not whichever files happen to be in a mutable development worktree. Node and +Bun plugins must be pinned through their package lock and tested against the +same OpenCode digest; a globally cached, unversioned plugin is not release +provenance. Generated OpenCode or MCP configuration needs both schema validation +and a candidate-runtime smoke test. Keep OpenCode auto-update disabled in every +certified lane so a process restart cannot silently change the server half of a +previously passing tuple. + +## Supported window + +Support the exact current and previous certified runtime tuples. Do not claim +compatibility with arbitrary future OpenCode versions. Additive fields and +unknown events should remain tolerant, while terminal text, request count, +ownership, persistence, and cleanup invariants remain strict. + +The Ruby/SSE profile covers the endpoints and events the client actually uses, +including session creation and deletion, asynchronous prompts, event +subscription, status, terminal idle/status events, part deltas and updates, +authoritative assistant messages, questions, and permissions. A passing shared +profile does not certify Rails persistence, voice streaming, plugin hooks, +provider migrations, or generated MCP configuration. + +## Release and promotion order + +1. The watcher records a new upstream release tag and resolved OCI digest in a + PR. It never merges or deploys. +2. Update fixtures for any observed protocol change before changing the client. +3. Build `opencode-ruby` and `opencode-rails` as one release train. Rails must + resolve the exact Ruby version and commit being tested. +4. Run the shared fixture corpus, Ruby 3.2 through 4.0 lockstep matrix, and every + exact public image still used by a current, previous, or candidate tuple. +5. Run only the application-owned profiles for each consumer, in its own PR and + isolated environment. Health checks alone do not certify a tuple. +6. Promote and deploy one consumer at a time. Record the exact production + commit, image/base digest, loaded client commits, and live result. +7. Commit reviewed evidence and move the old passing current tuple to previous. +8. Publish annotated gem tags only after the exact commit candidate is green and + the trusted publisher is configured. Publication never implies deployment. + +Commit pins are valid for an unpublished candidate, but the durable published +state must record the annotated tag object and peeled commit. Tags or `latest` +may be kept as human-readable provenance only; execution coordinates use full +Git commits and `image@sha256:...` references. + +## Rollback + +Rollback restores the whole certified `previous` tuple. Do not roll back only +the gem, only the consumer, or only the runtime image: the wire contract is the +unit of compatibility. + +The deployment platform's immediately preceding application image is not +automatically the certified OpenCode rollback. Keep it as emergency service +provenance, but if it contains a client/runtime tuple known to fail the contract +it must not occupy `previous`. In that case rollback means deploying the exact +consumer commit and runtime coordinates recorded in the certified `previous` +tuple, even if that is different from the platform's one-click rollback target. + +## Custom images + +Record each provenance layer separately: + +- consumer commit; +- exact output registry digest or Docker image ID; +- exact base image digest; +- custom OpenCode source commit, when the base is a fork; +- build-source commit when it differs from the deployment commit. + +The tuple fingerprint binds these values. On the next rebuild of an older +unlabelled private image, add OCI labels for the custom OpenCode source, reported +version, consumer build revision, and base digest, then make preflight compare +the labels. Do not relabel an already certified image: that changes its digest. + +## Runner and forge contract + +Runner upgrades are a separate compatibility surface from OpenCode upgrades. +Workflows must install their required Ruby/toolchain explicitly, declare Bash +for scripts that use Bash syntax, and avoid relying on ambient runner packages. +Browser suites must provision the reviewed Chrome/Selenium path explicitly; +an absent browser is a runner failure, not product evidence. Keep a system-test +suite serial when its harness shares an ephemeral server port instead of +mistaking parallel `EADDRINUSE` failures for OpenCode regressions. + +GitHub and Gitea do not implement every Actions feature identically. Keep the +same tests on both forges, but use forge-specific execution where necessary: +GitHub retains review artifacts and parallel dynamic image jobs; Gitea runs the +same manifest image set sequentially and makes no artifact-retention claim. +Neither path may mutate a runner or deploy a consumer. + +A runner-only workflow repair needs exact-head CI, not an application canary. +A client, runtime image, event, persistence, or toolchain change needs the +profile and consumer canaries described above. + +For a frozen rollback snapshot that is no longer a merge candidate, provide a +default-branch audit workflow that accepts and checks out an explicit full SHA. +That lets current runner plumbing test the immutable old application tree +without adding a CI-only commit to the rollback coordinate or pretending a +known runner-workflow failure is an application incompatibility. + +## Expected breakages + +- Prompt submission before event subscription can miss a terminal event and + hang a turn. +- An SSE parser that recognizes only `\n\n` can stall on CRLF, bare-CR, + byte-order-mark, comment, or multiline-data framing that is valid on the + wire. +- Changes to terminal or message-part events can duplicate or lose final text. +- Usage events can undercount multi-step requests if totals are overwritten. +- Reconnect logic can replay a prompt and create duplicate model requests. +- A Bun/Node server close can wait forever on idle keep-alive sockets unless + idle connections are explicitly reaped after admissions stop; the resulting + orphan can exhaust a service cgroup's PID budget and block a safe upgrade. +- Asset builds can fail when runtime configuration is evaluated without the + image variables available only at deploy time. +- A workflow can pass locally but fail under `sh` when it uses Bash arrays or + `mapfile`. +- Plugin hook names, provider schema, config/MCP schema, or CLI flags can break + direct consumers even when the Ruby profile stays green. +- `Opencode::Turn` is still an alpha-stage Rails composition seam; constructor, + observer, persistence, or finalization changes can break hosts even when the + lower-level Ruby wire client remains compatible. +- A custom fork can silently lose its required ordering or permission patch if + only an opaque output digest is retained. + +When one of these changes, update the owning profile and consumer evidence. Do +not weaken a strict invariant to make a new upstream release pass. diff --git a/evidence/2026-07-20-ajent-rails-alpha7-rollback.json b/evidence/2026-07-20-ajent-rails-alpha7-rollback.json new file mode 100644 index 0000000..2d24fed --- /dev/null +++ b/evidence/2026-07-20-ajent-rails-alpha7-rollback.json @@ -0,0 +1,76 @@ +{ + "schema_version": 1, + "consumer": "ajent-rails", + "profile": "rails-persisted-turn", + "consumer_commit": "6caa0132e4b172777126ed537de99cb7e9758e0b", + "consumer_tree": "29e6cd35313a06cef6f9c1f40a36e3ee582fd035", + "status": "pass", + "certified_at": "2026-07-20T08:14:53Z", + "tuple_sha256": "sha256:ae48333ba453f442d14263a916418a384f576db34d09a503111fc28c340eb408", + "clients": { + "opencode_ruby": { + "version": "0.0.1.alpha7", + "commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798" + }, + "opencode_rails": { + "version": "0.0.1.alpha7", + "commit": "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb" + } + }, + "runtime": { + "application_image": "docker-registry:5000/ajent_rails@sha256:fe6fdc2325d304ddb276df7d35807c183e7e574876a0d76a6c6de6014619c76e", + "aigl_image": "docker-registry:5000/opencode-aigl@sha256:f4064cb8b7a0f680f076dba9c72ba8e63b1457227c9fad5aa1407f0e0aac12e7", + "blackline_image": "docker-registry:5000/opencode-blackline@sha256:7b4ea4d70ff5fd35e3b17c571865dd6bb489217c7c00942e8569132fe2f749c8", + "raven_image": "docker-registry:5000/opencode-raven@sha256:9fe37ee88d8b10165e17f5f2a5c47b2396256464e732b4ef49138fb92a1234e3", + "reported_version": "1.18.3", + "upstream_base": "ghcr.io/anomalyco/opencode@sha256:c2d5d6398df72aac85cb1bdc8f900c71a9b75a33fb7c0a76dc1484e4b126e41e" + }, + "consumer_verification": { + "test_runs": 4247, + "assertions": 15740, + "failures": 0, + "errors": 0, + "skips": 4, + "rubocop_files": 1430, + "status": "pass" + }, + "application_canary": { + "products": [ + { + "id": "aigl", + "agent": "travel-agent", + "model": "openai-gpt-5.5", + "final_text": "CANARY_OK", + "status": "pass" + }, + { + "id": "blackline", + "agent": "employment-lawyer", + "model": "moonshot-kimi-k2.5", + "final_text": "CANARY_OK", + "status": "pass" + }, + { + "id": "raven", + "agent": "raven-legal", + "model": "moonshot-kimi-k2.5", + "final_text": "CANARY_OK", + "status": "pass" + } + ], + "session_cleanup_status": "pass", + "status": "pass" + }, + "source_availability": { + "origin_ref": "refs/heads/opencode/opencode-alpha7-rollback-certified-20260720", + "backup_ref": "refs/heads/opencode/opencode-alpha7-rollback-certified-20260720", + "github_ref": "refs/heads/opencode/opencode-alpha7-rollback-certified-20260720", + "status": "pass" + }, + "rollback_operability": { + "source_archived_on_all_remotes": true, + "prebuilt_application_image_retained": true, + "application_rebuild_required": false, + "deployed": false + } +} diff --git a/evidence/2026-07-20-ajent-rails-alpha8-production.json b/evidence/2026-07-20-ajent-rails-alpha8-production.json new file mode 100644 index 0000000..16a8318 --- /dev/null +++ b/evidence/2026-07-20-ajent-rails-alpha8-production.json @@ -0,0 +1,80 @@ +{ + "schema_version": 1, + "consumer": "ajent-rails", + "profile": "rails-persisted-turn", + "consumer_commit": "2ec8b879c5a34bb683fa5aaf26b998041d0e1264", + "consumer_tree": "a1e044f3a4f5c5ac0510a1d998c4afb55286da5a", + "status": "pass", + "certified_at": "2026-07-20T08:15:17Z", + "tuple_sha256": "sha256:326a87cdb67061bdd36dfcee989ae149511a0c627e857488d59e0f2b3c796720", + "clients": { + "publication_state": "unpublished", + "opencode_ruby": { + "version": "0.0.1.alpha8", + "commit": "9277646a4bb2cf25a8384ffc140b154f49ea5766" + }, + "opencode_rails": { + "version": "0.0.1.alpha8", + "commit": "a9add2a7c1dd3eb978aa8b4ebf9ef7e111d1057f" + } + }, + "runtime": { + "application_image": "docker-registry:5000/ajent_rails@sha256:c7b2c6f3c3ffb6af62069c3142a2d76e606b0794077b81c5464add2266b7c350", + "aigl_image": "docker-registry:5000/opencode-aigl@sha256:f4064cb8b7a0f680f076dba9c72ba8e63b1457227c9fad5aa1407f0e0aac12e7", + "blackline_image": "docker-registry:5000/opencode-blackline@sha256:7b4ea4d70ff5fd35e3b17c571865dd6bb489217c7c00942e8569132fe2f749c8", + "raven_image": "docker-registry:5000/opencode-raven@sha256:9fe37ee88d8b10165e17f5f2a5c47b2396256464e732b4ef49138fb92a1234e3", + "reported_version": "1.18.3", + "upstream_base": "ghcr.io/anomalyco/opencode@sha256:c2d5d6398df72aac85cb1bdc8f900c71a9b75a33fb7c0a76dc1484e4b126e41e" + }, + "forge_ci": { + "pr_run_id": "2718", + "main_build_run_id": "2719", + "protected_main_run_id": "2720", + "deploy_job_id": "12065", + "completed_at": "2026-07-20T08:06:22Z", + "status": "pass" + }, + "application_canary": { + "checked_at": "2026-07-20T08:15:17Z", + "products": [ + { + "id": "aigl", + "agent": "travel-agent", + "model": "openai-gpt-5.5", + "final_text": "CANARY_OK", + "status": "pass" + }, + { + "id": "blackline", + "agent": "employment-lawyer", + "model": "moonshot-kimi-k2.5", + "final_text": "CANARY_OK", + "status": "pass" + }, + { + "id": "raven", + "agent": "raven-legal", + "model": "moonshot-kimi-k2.5", + "final_text": "CANARY_OK", + "status": "pass" + } + ], + "session_cleanup_status": "pass", + "status": "pass" + }, + "production": { + "checked_at": "2026-07-20T08:15:17Z", + "application_health_status": 200, + "blackline_health_status": 200, + "ajent_public_status": 308, + "voice_contract_status": "pass", + "scotus_contract_status": "pass", + "status": "pass" + }, + "source_availability": { + "origin_main": "2ec8b879c5a34bb683fa5aaf26b998041d0e1264", + "backup_main": "2ec8b879c5a34bb683fa5aaf26b998041d0e1264", + "github_main": "2ec8b879c5a34bb683fa5aaf26b998041d0e1264", + "status": "pass" + } +} diff --git a/evidence/2026-07-20-mushu-alpha7-rollback.json b/evidence/2026-07-20-mushu-alpha7-rollback.json new file mode 100644 index 0000000..dfc876e --- /dev/null +++ b/evidence/2026-07-20-mushu-alpha7-rollback.json @@ -0,0 +1,68 @@ +{ + "schema_version": 1, + "consumer": "mushu", + "profile": "ruby-rest-sse", + "consumer_commit": "af2e9b0ce7ccacf49500450595a67c765a9d617a", + "consumer_tree": "f43f48972ab444310508e9067831dfc6977f1a24", + "status": "pass", + "certified_at": "2026-07-20T06:39:46.921805Z", + "tuple_sha256": "sha256:8d7d3cb2d3e2d920c900a9c1ada365026872904588cff0b166903d1d6d32183f", + "clients": { + "opencode_ruby": { + "version": "0.0.1.alpha7", + "commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798" + }, + "opencode_rails": null + }, + "runtime": { + "registry_ref": "docker-registry:5000/mushu-opencode@sha256:ed6293ca87e6db1dc07870d3ed6da21960743e6e2e904533b3c566d334b58d6a", + "base_image": "docker-registry:5000/mushu-opencode-base@sha256:de8a6adeefb30ce1c474cde23fe173dfe059434a13df0a54d14c7473edb33e16", + "custom_opencode_source_commit": "99d6328b18ff8a340a928449a2aa8bc184a063f9", + "build_source_commit": null, + "reported_version": "0.0.0-permission-shortcuts-202606300721", + "provenance_strength": "one-time-tag-source-attestation" + }, + "source_availability": { + "origin_ref": "refs/heads/review/mushu-opencode-alpha7-consumer-final-20260719", + "backup_ref": "refs/heads/review/mushu-opencode-alpha7-consumer-final-20260719", + "status": "archived" + }, + "shared_contract": { + "review_input": "opencode-compat-mushu-final-head.json", + "review_input_path_at_capture": "/tmp/opencode-compat-mushu-final-head.json", + "review_input_sha256": "sha256:57bf4b4f3c65159de4476b530aa0bc9b6077f1eacc69f7ea404f02314cb9760c", + "status": "pass" + }, + "application_canary": { + "started_at": "2026-07-20T06:37:55Z", + "completed_at": "2026-07-20T06:39:46.921805Z", + "successful_turn_count": 13, + "turn_count": 13, + "successful_conversation_count": 6, + "conversation_count": 6, + "successful_mcp_context_count": 7, + "mcp_context_count": 7, + "database_sha256": "sha256:a567d2ff9a906c9b78831bb6fa43153af539f1e76ee63bf35961472f55651d74", + "log_sha256": "sha256:a35a83b003a1f082b8a721b3faad8b35cb71dc9d1ceaedfc3cec3b26916e85b9", + "clean_worktree": true, + "status": "pass" + }, + "raw_artifact_retention": "raw clean-worktree database and log artifacts are ephemeral; durable counts, hashes, and sanitized results are retained in this committed evidence document", + "forge_ci": { + "status": "not_run", + "run_ids": [], + "reason": "The frozen snapshot predates a runner-only empty-system-test repair. Earlier runs 2588 and 2590 tested different commits and are deliberately not attributed to this tuple." + }, + "rollback_operability": { + "source_archived_on_all_remotes": true, + "prebuilt_application_image_retained": false, + "application_rebuild_required": true, + "immediate_platform_rollback_is_certified_tuple": false, + "immediate_platform_rollback_commit": "82ca30b18e1f38b55e2bed91cbd46f1eaf4a5922", + "reason": "The retained one-click application rollback uses alpha2 and the old runtime; the certified compatibility rollback is this alpha7 source tuple." + }, + "limitations": [ + "The old runtime image metadata does not recover an exact Mushu overlay-build commit.", + "No exact-head forge run is claimed; the clean-worktree full application canary is the application evidence." + ] +} diff --git a/evidence/2026-07-20-mushu-alpha8-production.json b/evidence/2026-07-20-mushu-alpha8-production.json new file mode 100644 index 0000000..5eb5bec --- /dev/null +++ b/evidence/2026-07-20-mushu-alpha8-production.json @@ -0,0 +1,87 @@ +{ + "schema_version": 1, + "consumer": "mushu", + "profile": "ruby-rest-sse", + "consumer_commit": "3f6754caab3f66537a5834f44ee63c1f05ecb047", + "consumer_tree": "726c3878e71f7a4cf8c0b8a3fa8fc15382bbab45", + "status": "pass", + "certified_at": "2026-07-20T07:13:51Z", + "tuple_sha256": "sha256:804410a63cf2b6e4056d6dd9ce289ac72464f441c9785b6fc8dd50016263a1a8", + "clients": { + "publication_state": "unpublished", + "opencode_ruby": { + "version": "0.0.1.alpha8", + "commit": "9277646a4bb2cf25a8384ffc140b154f49ea5766" + }, + "opencode_rails": null + }, + "runtime": { + "registry_ref": "docker-registry:5000/mushu-opencode@sha256:a16bf0c4cd17698eba02244f6ba0e429b2238b6455e1415ff69fb08965c863a7", + "base_image": "docker-registry:5000/mushu-opencode-base@sha256:de8a6adeefb30ce1c474cde23fe173dfe059434a13df0a54d14c7473edb33e16", + "custom_opencode_source_commit": "99d6328b18ff8a340a928449a2aa8bc184a063f9", + "build_source_commit": "521185613a864a16a76a21c7a60597ab3973bae9", + "reported_version": "0.0.0-permission-shortcuts-202606300721" + }, + "forge_ci": { + "pr": { + "run_id": "2700", + "job_ids": ["11903", "11904", "11905", "11906", "11907"], + "status": "pass", + "started_at": "2026-07-20T06:29:41Z", + "completed_at": "2026-07-20T06:41:14Z" + }, + "main": { + "run_id": "2705", + "job_ids": ["11934", "11935", "11936", "11937", "11938"], + "status": "pass", + "started_at": "2026-07-20T06:43:03Z", + "completed_at": "2026-07-20T06:54:58Z" + } + }, + "shared_contract": { + "review_input_path_at_capture": "/tmp/opencode-alpha8-mushu-permission-shortcuts.json", + "review_input_sha256": "sha256:6be88f0929815fa80839687e357473b232830aca4ccd1686f2a38ac9381088de", + "durable_ci_evidence": "evidence/2026-07-20-opencode-alpha8-shared-client-ci.json", + "status": "pass" + }, + "preproduction_canary": { + "successful_turn_count": 13, + "turn_count": 13, + "successful_conversation_count": 6, + "conversation_count": 6, + "successful_mcp_context_count": 2, + "mcp_context_count": 2, + "database_sha256": "sha256:7fbe32ff9621196a2504aeba5eca78cb0c9c670edd7a1168b1f81f667872e2b6", + "log_path_at_capture": "tmp/alpha8-canary-rails.log", + "log_sha256": "sha256:51b698fcc8b7bce68e79cbae10997e19eb47a88985cbe24871d10740dda26225", + "status": "pass" + }, + "raw_artifact_retention": "raw clean-worktree database and log artifacts are ephemeral; durable counts, hashes, and sanitized results are retained in this committed evidence document", + "application_canary": { + "started_at": "2026-07-20T07:03:17Z", + "completed_at": "2026-07-20T07:04:51.376351Z", + "successful_turn_count": 13, + "turn_count": 13, + "successful_conversation_count": 6, + "conversation_count": 6, + "successful_mcp_context_count": 1, + "mcp_context_count": 1, + "status": "pass" + }, + "production": { + "checked_at": "2026-07-20T07:13:20Z", + "application_tag_provenance": "docker-registry:5000/mushu:3f6754caab3f66537a5834f44ee63c1f05ecb047", + "application_image": "docker-registry:5000/mushu@sha256:30f204184520d504c7c384c7f6e503fa3547217aaaef2f1d0d65884533bdeacd", + "web_health_status": 200, + "status_health_status": 200, + "runtime_readiness": "ready", + "containers": ["web", "calendar", "telegram", "opencode"], + "deployment_mode": "manual", + "status": "pass" + }, + "rollback_operability": { + "certified_previous_consumer_commit": "af2e9b0ce7ccacf49500450595a67c765a9d617a", + "immediate_platform_rollback_is_certified_tuple": false, + "immediate_platform_rollback_commit": "82ca30b18e1f38b55e2bed91cbd46f1eaf4a5922" + } +} diff --git a/evidence/2026-07-20-opencode-alpha8-shared-client-ci.json b/evidence/2026-07-20-opencode-alpha8-shared-client-ci.json new file mode 100644 index 0000000..baeb9f6 --- /dev/null +++ b/evidence/2026-07-20-opencode-alpha8-shared-client-ci.json @@ -0,0 +1,107 @@ +{ + "schema_version": 1, + "kind": "shared-client-and-lockstep-certification", + "status": "pass", + "certification_scope": "shared-ruby-rest-sse-and-rails-lockstep", + "publication_state": "unpublished", + "clients": { + "opencode_ruby": { + "version": "0.0.1.alpha8", + "commit": "9277646a4bb2cf25a8384ffc140b154f49ea5766" + }, + "opencode_rails": { + "version": "0.0.1.alpha8", + "commit": "a9add2a7c1dd3eb978aa8b4ebf9ef7e111d1057f", + "runtime_opencode_ruby_commit": "9277646a4bb2cf25a8384ffc140b154f49ea5766" + } + }, + "github_workflow": { + "run_id": "29725021192", + "run_attempt": "1", + "head_sha": "328fa39c557d598ad09b270ad1ceab19901031cd", + "repository": "ajaynomics/opencode-compat", + "run_url": "https://github.com/ajaynomics/opencode-compat/actions/runs/29725021192", + "completed_at": "2026-07-20T07:34:59Z", + "status": "pass", + "artifact_retention_days": 30 + }, + "gitea_workflow": { + "run_id": "2717", + "head_sha": "328fa39c557d598ad09b270ad1ceab19901031cd", + "repository": "ajaynomics/opencode-compat", + "run_url": "https://gitea.ajay.to/ajaynomics/opencode-compat/actions/runs/2717", + "completed_at": "2026-07-20T07:36:06Z", + "status": "pass", + "artifact_evidence_claimed": false + }, + "fixture_contract": { + "checked_at": "2026-07-20T07:34:34Z", + "fixture_count": 7, + "status": "pass" + }, + "rails_lockstep": [ + { + "ruby_runtime_version": "3.2.11", + "status": "pass" + }, + { + "ruby_runtime_version": "3.3.12", + "status": "pass" + }, + { + "ruby_runtime_version": "3.4.10", + "status": "pass" + }, + { + "ruby_runtime_version": "4.0.6", + "status": "pass" + } + ], + "exact_image_contracts": [ + { + "id": "upstream-1.16.1", + "checked_at": "2026-07-20T07:34:50Z", + "image": "ghcr.io/anomalyco/opencode@sha256:e975a0647576016dfdf77d54b979ca30d32b4750472c10263e9894aad6628c2a", + "reported_version": "1.16.1", + "status": "pass", + "expected_text": "compat-ok", + "full_text": "compat-ok", + "authoritative_assistant_message_count": 1, + "llm_request_count": 1, + "executed_profiles": ["ruby-rest-sse"], + "required_consumer_profiles": ["rails-persisted-turn"] + }, + { + "id": "upstream-1.17.18", + "checked_at": "2026-07-20T07:34:55Z", + "image": "ghcr.io/anomalyco/opencode@sha256:bf9d0e84b7cedef436a8f57db4d48767cd5d8fc6604f666335c1cc916b199a97", + "reported_version": "1.17.18", + "status": "pass", + "expected_text": "compat-ok", + "full_text": "compat-ok", + "authoritative_assistant_message_count": 1, + "llm_request_count": 1, + "executed_profiles": ["ruby-rest-sse"], + "required_consumer_profiles": ["plugin-ledger"] + }, + { + "id": "upstream-1.18.3", + "checked_at": "2026-07-20T07:34:48Z", + "image": "ghcr.io/anomalyco/opencode@sha256:c2d5d6398df72aac85cb1bdc8f900c71a9b75a33fb7c0a76dc1484e4b126e41e", + "reported_version": "1.18.3", + "status": "pass", + "expected_text": "compat-ok", + "full_text": "compat-ok", + "authoritative_assistant_message_count": 1, + "llm_request_count": 1, + "executed_profiles": ["ruby-rest-sse"], + "required_consumer_profiles": ["rails-persisted-turn", "plugin-ledger"] + } + ], + "certified_at": "2026-07-20T07:36:06Z", + "limitations": [ + "The exact-image contracts execute opencode-ruby, not opencode-rails.", + "Rails persistence and each consumer application profile require separate passing evidence.", + "The exact client commits are certified candidates but remain unpublished until RubyGems trusted publishers are configured and registry publication is verified." + ] +} diff --git a/evidence/2026-07-20-travelwolf-alpha7-rollback.json b/evidence/2026-07-20-travelwolf-alpha7-rollback.json new file mode 100644 index 0000000..dc75e7d --- /dev/null +++ b/evidence/2026-07-20-travelwolf-alpha7-rollback.json @@ -0,0 +1,73 @@ +{ + "schema_version": 1, + "consumer": "travelwolf", + "profile": "rails-persisted-turn", + "consumer_commit": "531011defe4acc45a15ffe44a8e9da9cb610aaac", + "consumer_tree": "d0e47b2d90ace87c6cc9c4b94b2f22987441c1f6", + "status": "pass", + "certified_at": "2026-07-20T06:31:42Z", + "tuple_sha256": "sha256:20ecec3d2c3743efcd8575e71ec46dd7b86c2865e472511d16a96d57ddb8b40c", + "clients": { + "opencode_ruby": { + "version": "0.0.1.alpha7", + "commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798" + }, + "opencode_rails": { + "version": "0.0.1.alpha7", + "commit": "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb" + } + }, + "runtime": { + "image": "ghcr.io/anomalyco/opencode@sha256:e975a0647576016dfdf77d54b979ca30d32b4750472c10263e9894aad6628c2a", + "reported_version": "1.16.1" + }, + "forge_ci": { + "pr": { + "run_id": "2696", + "job_id": "11898", + "status": "pass", + "started_at": "2026-07-20T06:20:57Z", + "completed_at": "2026-07-20T06:23:32Z" + }, + "main": { + "run_id": "2698", + "job_id": "11900", + "status": "pass", + "started_at": "2026-07-20T06:23:58Z", + "completed_at": "2026-07-20T06:25:49Z" + } + }, + "shared_contract": { + "artifact": "opencode-alpha7-shared-contract-531011de.json", + "source_path_at_capture": "/data/projects/travelwolf/.worktrees/opencode-alpha8-candidate-20260720/tmp/opencode-alpha7-shared-contract-531011de.json", + "artifact_sha256": "sha256:12cef4d8b713f32289dc98ce3a0a3ef2094a54205a160d3a5b4bcfc81f2805c9", + "expected_text": "compat-ok", + "full_text": "compat-ok", + "authoritative_assistant_message_count": 1, + "llm_request_count": 1, + "status": "pass" + }, + "application_canary": { + "artifact": "opencode-alpha7-application-531011de.json", + "source_path_at_capture": "/data/projects/travelwolf/.worktrees/opencode-alpha8-candidate-20260720/tmp/opencode-alpha7-application-531011de.json", + "artifact_sha256": "sha256:5f5693bba27a16bc703c3211827a09171c77d5f9ef1acf6def1917df43a6cebc", + "checked_at": "2026-07-20T06:22:36.958Z", + "final_text": "TRAVELWOLF_ALPHA7_CANARY_OK", + "application_prompt_count": 1, + "user_turn_count": 1, + "authoritative_assistant_turn_count": 1, + "child_turn_count": 0, + "model_request_count_observed": false, + "cleanup_status": "pass", + "status": "pass" + }, + "raw_artifact_retention": "ephemeral clean-worktree files; durable sanitized results and hashes are retained in this committed evidence document", + "rollback_operability": { + "source_archived_on_all_remotes": true, + "prebuilt_application_image_retained": false, + "application_rebuild_required": true, + "immediate_platform_rollback_is_certified_tuple": false, + "immediate_platform_rollback_commit": "8494c52d11bf2995c378398e977d82d06a09e046", + "reason": "The retained one-click application rollback uses alpha6 clients; the certified compatibility rollback is this alpha7 source tuple." + } +} diff --git a/evidence/2026-07-20-travelwolf-alpha8-production.json b/evidence/2026-07-20-travelwolf-alpha8-production.json new file mode 100644 index 0000000..246e076 --- /dev/null +++ b/evidence/2026-07-20-travelwolf-alpha8-production.json @@ -0,0 +1,84 @@ +{ + "schema_version": 1, + "consumer": "travelwolf", + "profile": "rails-persisted-turn", + "consumer_commit": "02aed70eb4de201c6510e3be583699f8eb984ce7", + "consumer_tree": "f436a7248d90e5ae9e84c45f264acd913a4162aa", + "status": "pass", + "certified_at": "2026-07-20T06:51:48Z", + "tuple_sha256": "sha256:0321c4246bed6ab62587924a83204a9e2ca7517b1dec5e4aef35dcac400fcc8e", + "clients": { + "publication_state": "unpublished", + "opencode_ruby": { + "version": "0.0.1.alpha8", + "commit": "9277646a4bb2cf25a8384ffc140b154f49ea5766" + }, + "opencode_rails": { + "version": "0.0.1.alpha8", + "commit": "a9add2a7c1dd3eb978aa8b4ebf9ef7e111d1057f" + } + }, + "runtime": { + "image": "ghcr.io/anomalyco/opencode@sha256:e975a0647576016dfdf77d54b979ca30d32b4750472c10263e9894aad6628c2a", + "reported_version": "1.16.1" + }, + "forge_ci": { + "pr": { + "run_id": "2702", + "job_id": "11920", + "status": "pass", + "started_at": "2026-07-20T06:33:58Z", + "completed_at": "2026-07-20T06:35:29Z" + }, + "main": { + "run_id": "2704", + "job_id": "11933", + "status": "pass", + "started_at": "2026-07-20T06:36:57Z", + "completed_at": "2026-07-20T06:38:11Z" + } + }, + "shared_contract": { + "review_input_path_at_capture": "/data/projects/travelwolf/.worktrees/opencode-alpha8-candidate-20260720/tmp/opencode-alpha8-shared-contract-02aed70e.json", + "review_input_sha256": "sha256:f63edbafba8fb7646d1c88e33472636af56770110d920006211b4ff3f115b977", + "durable_ci_evidence": "evidence/2026-07-20-opencode-alpha8-shared-client-ci.json", + "expected_text": "compat-ok", + "full_text": "compat-ok", + "authoritative_assistant_message_count": 1, + "llm_request_count": 1, + "status": "pass" + }, + "application_canary": { + "review_input_path_at_capture": "/data/projects/travelwolf/.worktrees/opencode-alpha8-candidate-20260720/tmp/opencode-alpha8-application-02aed70e.json", + "review_input_sha256": "sha256:6f4dfe14a74bb84c13cf566302222f737603d2fbafa7231c7bb332a2567bca24", + "offline_input_path_at_capture": "/data/projects/travelwolf/.worktrees/opencode-alpha8-candidate-20260720/tmp/opencode-alpha8-offline-contract-02aed70e.json", + "offline_input_sha256": "sha256:4e8157fbb44d6fd0346bea79aee1271e400cd7fde57bc8a45afa77250134029c", + "checked_at": "2026-07-20T06:31:13.506Z", + "final_text": "TRAVELWOLF_ALPHA8_CANARY_OK", + "application_prompt_count": 1, + "user_turn_count": 1, + "authoritative_assistant_turn_count": 1, + "child_turn_count": 0, + "model_request_count_observed": false, + "cleanup_status": "pass", + "status": "pass" + }, + "raw_artifact_retention": "ephemeral clean-worktree files; durable sanitized results and hashes are retained in this committed evidence document", + "production": { + "checked_at": "2026-07-20T06:51:48Z", + "application_tag_provenance": "127.0.0.1:5000/travelwolf:02aed70eb4de201c6510e3be583699f8eb984ce7", + "application_image_id": "sha256:031371e20f34e1014712ddc668b4e182483664bb70f116b683c0ec83d6668338", + "application_image_created_at": "2026-07-20T06:42:55.929825054Z", + "internal_health_status": 200, + "authenticated_public_health_status": 200, + "container_status": "running", + "deployment_mode": "manual-isolated-kamal", + "status": "pass" + }, + "rollback_operability": { + "certified_previous_consumer_commit": "531011defe4acc45a15ffe44a8e9da9cb610aaac", + "immediate_platform_rollback_is_certified_tuple": false, + "immediate_platform_rollback_commit": "8494c52d11bf2995c378398e977d82d06a09e046", + "immediate_platform_rollback_image_id": "sha256:4291b256b37eeb694435093655db17154c837e53335b8d0eb8f1b07a744e856e" + } +} diff --git a/lib/opencode_compat/runtime_tuple_promoter.rb b/lib/opencode_compat/runtime_tuple_promoter.rb index 4fe9529..d5ae164 100644 --- a/lib/opencode_compat/runtime_tuple_promoter.rb +++ b/lib/opencode_compat/runtime_tuple_promoter.rb @@ -15,6 +15,8 @@ module OpenCodeCompat 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 @@ -30,6 +32,26 @@ module OpenCodeCompat source_image upstream_base ].freeze + EVIDENCE_REFERENCE_KEYS = %w[path sha256].freeze + NULLABLE_COMMIT_KEYS = %w[build_source_commit].freeze + EVIDENCE_RUNTIME_KEY_MAP = { + "custom_opencode_source_commit" => "source_commit" + }.freeze + EVIDENCE_RUNTIME_COORDINATE_KEYS = %w[ + application_image + application_image_id + base_image + build_source_commit + docker_image_id + image + provenance_strength + registry_ref + reported_version + source_commit + source_image + tag_provenance + upstream_base + ].freeze def initialize(root:, manifest_path: File.join(root, "manifests/runtime-tuples.json")) @root = File.realpath(root) @@ -82,6 +104,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 @@ -124,7 +177,55 @@ 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") + refresh_promotion_state!(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) + 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 + unless consumer_entry["previous"].nil? + raise PromotionError, "degraded bootstrap cannot replace an existing previous tuple" + end + if consumer_entry.key?("emergency_provenance") || consumer_entry.key?("rollback_state") + raise PromotionError, "degraded bootstrap has already been recorded for this consumer" + 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") + } + refresh_promotion_state!(manifest) manifest end @@ -236,21 +337,23 @@ module OpenCodeCompat end validate_timestamp!(certified_at, "#{label} certification timestamp") raise PromotionError, "#{label} certification requires at least one evidence file" if evidence.empty? - raise PromotionError, "#{label} certification evidence files must be unique" unless evidence.uniq == evidence - consumer_commit = tuple.fetch("consumer_commit") normalized_evidence = evidence.map do |reference| validate_evidence!( reference, consumer: consumer, profile: profile, - consumer_commit: consumer_commit, + tuple: tuple, status: status, certified_at: certified_at, tuple_fingerprint: expected_fingerprint, label: label ) end + evidence_paths = normalized_evidence.map { |reference| reference.fetch("path") } + unless evidence_paths.uniq == evidence_paths + raise PromotionError, "#{label} certification evidence files must be unique" + end { "status" => status, @@ -266,7 +369,13 @@ module OpenCodeCompat raise PromotionError, "#{label} tuple has invalid or stale certification metadata" end - validate_supplied_certification!( + references = Array(certification["evidence"]) + unless references.all? { |reference| structured_evidence_reference?(reference) } + raise PromotionError, + "#{label} recorded certification evidence must use hash-bound {path, sha256} references" + end + + validated = validate_supplied_certification!( certification, consumer: consumer, profile: profile, @@ -274,23 +383,24 @@ module OpenCodeCompat expected_fingerprint: expected_fingerprint, label: label ) + unless certification["evidence"] == validated["evidence"] + raise PromotionError, "#{label} recorded certification evidence references are not canonical" + end end def validate_evidence!( reference, consumer:, profile:, - consumer_commit:, + tuple:, status:, certified_at:, tuple_fingerprint:, label: ) - unless reference.is_a?(String) && !reference.empty? - raise PromotionError, "#{label} evidence references must be non-empty strings" - end + reference_path, expected_sha256 = evidence_reference_parts!(reference, label) - relative = Pathname.new(reference).cleanpath + relative = Pathname.new(reference_path).cleanpath if relative.absolute? || relative.each_filename.first != "evidence" raise PromotionError, "#{label} evidence must be a repository-relative path under evidence/" end @@ -302,14 +412,23 @@ module OpenCodeCompat raise PromotionError, "#{label} evidence resolves outside evidence/" end - document = parse_json(File.read(real_path), relative.to_s) + contents = File.binread(real_path) + actual_sha256 = "sha256:#{Digest::SHA256.hexdigest(contents)}" + if expected_sha256 && expected_sha256 != actual_sha256 + raise PromotionError, + "#{label} evidence #{relative} has sha256=#{actual_sha256.inspect}; " \ + "expected #{expected_sha256.inspect}" + end + + document = parse_json(contents, relative.to_s) unless document.is_a?(Hash) && document["schema_version"] == 1 raise PromotionError, "#{label} evidence #{relative} must use schema_version 1" end + validate_immutable_fields!(document, "#{label} evidence #{relative}") expected = { "consumer" => consumer, "profile" => profile, - "consumer_commit" => consumer_commit, + "consumer_commit" => tuple.fetch("consumer_commit"), "status" => status, "certified_at" => certified_at, "tuple_sha256" => tuple_fingerprint @@ -321,14 +440,20 @@ module OpenCodeCompat "#{label} evidence #{relative} has #{key}=#{document[key].inspect}; expected #{value.inspect}" end - relative.to_s + validate_evidence_tuple_details!(document, tuple, "#{label} evidence #{relative}") + + { + "path" => relative.to_s, + "sha256" => actual_sha256 + } rescue Errno::ENOENT - raise PromotionError, "#{label} evidence does not exist: #{reference}" + raise PromotionError, "#{label} evidence does not exist: #{reference_path || reference}" end def validate_tuple!(tuple, label) raise PromotionError, "#{label} tuple must be an object" unless tuple.is_a?(Hash) + validate_immutable_fields!(tuple, label) validate_full_commit!(tuple["consumer_commit"], "#{label} consumer commit") validate_client!(tuple["opencode_ruby"], "#{label} opencode_ruby", required: true) validate_client!(tuple["opencode_rails"], "#{label} opencode_rails", required: false) @@ -366,8 +491,10 @@ module OpenCodeCompat raise PromotionError, "#{label} #{key} must be an immutable image@sha256 digest, not a tag" end exact_execution_coordinate = true if %w[image registry_ref].include?(key) - elsif key == "source_commit" - validate_full_commit!(value, "#{label} source commit") + elsif key.end_with?("_commit") + next if value.nil? && NULLABLE_COMMIT_KEYS.include?(key) + + validate_full_commit!(value, "#{label} #{key.tr('_', ' ')}") end end @@ -382,6 +509,154 @@ module OpenCodeCompat (key.end_with?("_ref") && key != "tag_provenance") end + def structured_evidence_reference?(reference) + reference.is_a?(Hash) && reference.keys.sort == EVIDENCE_REFERENCE_KEYS + end + + def evidence_reference_parts!(reference, label) + if reference.is_a?(String) + unless !reference.empty? + raise PromotionError, "#{label} evidence references must contain a non-empty path" + end + + return [reference, nil] + end + + unless structured_evidence_reference?(reference) + raise PromotionError, + "#{label} evidence references must be a path string or an object containing only path and sha256" + end + + path = reference["path"] + sha256 = reference["sha256"] + unless path.is_a?(String) && !path.empty? + raise PromotionError, "#{label} evidence references must contain a non-empty path" + end + unless sha256.is_a?(String) && sha256.match?(DIGEST) + raise PromotionError, "#{label} evidence reference sha256 must be an exact sha256 digest" + end + + [path, sha256] + end + + def validate_evidence_tuple_details!(document, tuple, label) + if document.key?("consumer_tree") + expected_tree = tuple["consumer_tree"] + unless expected_tree && document["consumer_tree"] == expected_tree + raise PromotionError, + "#{label} has consumer_tree=#{document['consumer_tree'].inspect}; " \ + "expected #{expected_tree.inspect}" + end + end + + validate_evidence_clients!(document["clients"], tuple, label) if document.key?("clients") + validate_evidence_runtime!(document["runtime"], tuple.fetch("runtime"), label) if document.key?("runtime") + end + + def validate_evidence_clients!(clients, tuple, label) + unless clients.is_a?(Hash) + raise PromotionError, "#{label} clients must be an object" + end + + %w[opencode_ruby opencode_rails].each do |client_name| + next unless clients.key?(client_name) + + evidence_client = clients[client_name] + tuple_client = tuple[client_name] + if evidence_client.nil? + unless tuple_client.nil? + raise PromotionError, "#{label} #{client_name} is null but the tuple declares a client" + end + next + end + + unless evidence_client.is_a?(Hash) && tuple_client.is_a?(Hash) + raise PromotionError, "#{label} #{client_name} must match the tuple client" + end + unless evidence_client["version"].is_a?(String) && !evidence_client["version"].empty? + raise PromotionError, "#{label} #{client_name} must include a non-empty version" + end + + commits = %w[commit git_commit].filter_map do |key| + evidence_client[key] if evidence_client.key?(key) + end + if commits.empty? + raise PromotionError, "#{label} #{client_name} must include commit or git_commit" + end + + compare_evidence_coordinate!( + evidence_client["version"], + tuple_client["version"], + "#{label} #{client_name}.version" + ) + commits.each do |commit| + compare_evidence_coordinate!(commit, tuple_client["git_commit"], "#{label} #{client_name}.commit") + end + end + end + + def validate_evidence_runtime!(runtime, tuple_runtime, label) + unless runtime.is_a?(Hash) + raise PromotionError, "#{label} runtime must be an object" + end + + runtime.each do |key, value| + tuple_key = EVIDENCE_RUNTIME_KEY_MAP.fetch(key, key) + next unless EVIDENCE_RUNTIME_COORDINATE_KEYS.include?(tuple_key) + + unless tuple_runtime.key?(tuple_key) + raise PromotionError, "#{label} runtime.#{key} is not present in the certified tuple" + end + + compare_evidence_coordinate!(value, tuple_runtime[tuple_key], "#{label} runtime.#{key}") + end + end + + def compare_evidence_coordinate!(actual, expected, label) + return if actual == expected + + raise PromotionError, "#{label}=#{actual.inspect}; expected #{expected.inspect}" + end + + def validate_immutable_fields!(value, label, path = []) + case value + when Hash + value.each do |key, child| + field_path = path + [key] + field_label = "#{label} #{field_path.join('.')}" + if key == "commit" || key.end_with?("_commit") || key == "head_sha" || key.end_with?("_head_sha") + unless child.nil? && NULLABLE_COMMIT_KEYS.include?(key) + validate_full_commit!(child, field_label) + end + elsif key == "tree" || key.end_with?("_tree") + validate_full_oid!(child, field_label) + elsif key == "annotated_tag_object" || key.end_with?("_tag_object") + validate_full_oid!(child, field_label) + elsif digest_field?(key) + unless child.is_a?(String) && child.match?(DIGEST) + raise PromotionError, "#{field_label} must be an exact sha256 digest" + end + end + + validate_immutable_fields!(child, label, field_path) + end + when Array + value.each_with_index { |child, index| validate_immutable_fields!(child, label, path + [index]) } + end + end + + def digest_field?(key) + key == "sha256" || key.end_with?("_sha256") || + key == "digest" || key.end_with?("_digest") || + key == "docker_image_id" || key.end_with?("_image_id") + end + + def validate_full_oid!(value, label) + return if value.is_a?(String) && value.match?(FULL_COMMIT) + + raise PromotionError, "#{label} must be a full 40-character lowercase Git object ID" + end + def validate_full_commit!(value, label) return if value.is_a?(String) && value.match?(FULL_COMMIT) @@ -455,6 +730,61 @@ module OpenCodeCompat end end + def all_consumers_bootstrapped?(manifest) + manifest.fetch("consumers").all? do |consumer, entry| + next false unless entry["candidate"].nil? && entry["previous"].nil? + next false unless entry.dig("rollback_state", "status") == "degraded-no-certified-previous" + next false unless NON_CERTIFIABLE_TUPLE_STATUSES.include?(entry.dig("emergency_provenance", "status")) + + profile = fetch_profile!(entry, consumer) + tuple = entry["current"] + next false unless tuple.is_a?(Hash) && tuple["status"] == "certified" + + validate_tuple!(tuple, "#{consumer} current") + validate_recorded_certification!( + tuple, + consumer: consumer, + profile: profile, + expected_fingerprint: tuple_fingerprint(tuple, consumer: consumer, profile: profile), + label: "#{consumer} current" + ) + true + rescue PromotionError + false + end + end + + def migration_state_for(manifest) + return "certified" if all_consumers_certified?(manifest) + return "bootstrap-current-only" if all_consumers_bootstrapped?(manifest) + + "candidate" + end + + def refresh_promotion_state!(manifest) + manifest["migration_state"] = migration_state_for(manifest) + manifest["promotion_readiness"] = case manifest.fetch("migration_state") + when "certified" + { + "status" => "certified", + "reason" => "Every consumer has exact current and previous passing tuples.", + "required_action" => "Promote only a newly certified, materially changed tuple." + } + when "bootstrap-current-only" + { + "status" => "bootstrap-current-only", + "reason" => "Every current tuple is certified, but no independently passing previous tuple exists yet.", + "required_action" => "Certify the next meaningful release so normal promotion retains the current tuple as previous." + } + else + { + "status" => "candidate", + "reason" => "At least one consumer transition remains incomplete.", + "required_action" => "Finish exact tuple certification without treating failed emergency provenance as rollback evidence." + } + end + 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 df35f80..087c357 100644 --- a/manifests/image-matrix.json +++ b/manifests/image-matrix.json @@ -5,7 +5,9 @@ "publication_state": "unpublished", "opencode_ruby_commit": "9277646a4bb2cf25a8384ffc140b154f49ea5766", "opencode_rails_commit": "a9add2a7c1dd3eb978aa8b4ebf9ef7e111d1057f", - "certification_status": "pending" + "certification_status": "certified", + "certified_at": "2026-07-20T07:36:06Z", + "evidence": "evidence/2026-07-20-opencode-alpha8-shared-client-ci.json" }, "public_ci": [ { @@ -16,7 +18,20 @@ "consumers": ["travelwolf"], "profiles": ["ruby-rest-sse"], "required_consumer_profiles": ["rails-persisted-turn"], - "certification_status": "pending", + "certification_scope": "shared-client-contract-only", + "certification_status": "certified", + "current_certification": { + "status": "certified", + "client_commit": "9277646a4bb2cf25a8384ffc140b154f49ea5766", + "rails_commit": "a9add2a7c1dd3eb978aa8b4ebf9ef7e111d1057f", + "checked_at": "2026-07-20T07:34:50Z", + "expected_text": "compat-ok", + "full_text": "compat-ok", + "llm_request_count": 1, + "workflow_run_id": "29725021192", + "workflow_head_sha": "328fa39c557d598ad09b270ad1ceab19901031cd", + "evidence": "evidence/2026-07-20-opencode-alpha8-shared-client-ci.json" + }, "previous_certification": { "status": "certified", "client_commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", @@ -34,7 +49,20 @@ "consumers": ["opencode-ajent"], "profiles": ["ruby-rest-sse"], "required_consumer_profiles": ["plugin-ledger"], - "certification_status": "pending", + "certification_scope": "shared-client-contract-only", + "certification_status": "certified", + "current_certification": { + "status": "certified", + "client_commit": "9277646a4bb2cf25a8384ffc140b154f49ea5766", + "rails_commit": "a9add2a7c1dd3eb978aa8b4ebf9ef7e111d1057f", + "checked_at": "2026-07-20T07:34:55Z", + "expected_text": "compat-ok", + "full_text": "compat-ok", + "llm_request_count": 1, + "workflow_run_id": "29725021192", + "workflow_head_sha": "328fa39c557d598ad09b270ad1ceab19901031cd", + "evidence": "evidence/2026-07-20-opencode-alpha8-shared-client-ci.json" + }, "previous_certification": { "status": "certified", "client_commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", @@ -52,7 +80,20 @@ "consumers": ["ajent-rails"], "profiles": ["ruby-rest-sse"], "required_consumer_profiles": ["rails-persisted-turn", "plugin-ledger"], - "certification_status": "pending", + "certification_scope": "shared-client-contract-only", + "certification_status": "certified", + "current_certification": { + "status": "certified", + "client_commit": "9277646a4bb2cf25a8384ffc140b154f49ea5766", + "rails_commit": "a9add2a7c1dd3eb978aa8b4ebf9ef7e111d1057f", + "checked_at": "2026-07-20T07:34:48Z", + "expected_text": "compat-ok", + "full_text": "compat-ok", + "llm_request_count": 1, + "workflow_run_id": "29725021192", + "workflow_head_sha": "328fa39c557d598ad09b270ad1ceab19901031cd", + "evidence": "evidence/2026-07-20-opencode-alpha8-shared-client-ci.json" + }, "previous_certification": { "status": "certified", "client_commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", @@ -65,42 +106,87 @@ ], "host_canary": [ { - "id": "ajent-blackline-alpha7-canary", + "id": "ajent-rails-alpha8-production", "consumer": "ajent-rails", - "consumer_commit": "96552893baccd5ccba1592b2585edfef1299b4f9", - "tag_provenance": "ajent-opencode-blackline-canary:96552893", - "docker_image_id": "sha256:239e3dc6cd2958251cda636fb20bd7cd7c46f596bd4c61cb81863ebc49d824e8", - "source_commit": "96552893baccd5ccba1592b2585edfef1299b4f9", + "consumer_commit": "2ec8b879c5a34bb683fa5aaf26b998041d0e1264", + "application_image": "docker-registry:5000/ajent_rails@sha256:c7b2c6f3c3ffb6af62069c3142a2d76e606b0794077b81c5464add2266b7c350", + "product_images": { + "aigl": "docker-registry:5000/opencode-aigl@sha256:f4064cb8b7a0f680f076dba9c72ba8e63b1457227c9fad5aa1407f0e0aac12e7", + "blackline": "docker-registry:5000/opencode-blackline@sha256:7b4ea4d70ff5fd35e3b17c571865dd6bb489217c7c00942e8569132fe2f749c8", + "raven": "docker-registry:5000/opencode-raven@sha256:9fe37ee88d8b10165e17f5f2a5c47b2396256464e732b4ef49138fb92a1234e3" + }, "reported_version": "1.18.3", "upstream_base": "ghcr.io/anomalyco/opencode@sha256:c2d5d6398df72aac85cb1bdc8f900c71a9b75a33fb7c0a76dc1484e4b126e41e", - "profiles": ["rails-persisted-turn"], - "certification_status": "pending", + "profiles": ["rails-persisted-turn", "plugin-ledger"], + "certification_status": "certified", + "current_certification": { + "status": "certified", + "client_commit": "9277646a4bb2cf25a8384ffc140b154f49ea5766", + "rails_commit": "a9add2a7c1dd3eb978aa8b4ebf9ef7e111d1057f", + "consumer_commit": "2ec8b879c5a34bb683fa5aaf26b998041d0e1264", + "certified_at": "2026-07-20T08:15:17Z", + "evidence": "evidence/2026-07-20-ajent-rails-alpha8-production.json" + }, "previous_certification": { - "status": "candidate-certified", + "status": "certified", "client_commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", "rails_commit": "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb", - "certified_at": "2026-07-18T21:58:00Z", - "persisted_final_text": "compat-ok", - "model_request_count": 1 + "consumer_commit": "6caa0132e4b172777126ed537de99cb7e9758e0b", + "certified_at": "2026-07-20T08:14:53Z", + "evidence": "evidence/2026-07-20-ajent-rails-alpha7-rollback.json" } }, { - "id": "mushu-permission-shortcuts-alpha7-canary", + "id": "travelwolf-alpha8-production", + "consumer": "travelwolf", + "consumer_commit": "02aed70eb4de201c6510e3be583699f8eb984ce7", + "image": "ghcr.io/anomalyco/opencode@sha256:e975a0647576016dfdf77d54b979ca30d32b4750472c10263e9894aad6628c2a", + "application_image_id": "sha256:031371e20f34e1014712ddc668b4e182483664bb70f116b683c0ec83d6668338", + "reported_version": "1.16.1", + "profiles": ["rails-persisted-turn"], + "certification_status": "certified", + "current_certification": { + "status": "certified", + "client_commit": "9277646a4bb2cf25a8384ffc140b154f49ea5766", + "rails_commit": "a9add2a7c1dd3eb978aa8b4ebf9ef7e111d1057f", + "consumer_commit": "02aed70eb4de201c6510e3be583699f8eb984ce7", + "certified_at": "2026-07-20T06:51:48Z", + "evidence": "evidence/2026-07-20-travelwolf-alpha8-production.json" + }, + "previous_certification": { + "status": "certified", + "client_commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "rails_commit": "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb", + "consumer_commit": "531011defe4acc45a15ffe44a8e9da9cb610aaac", + "certified_at": "2026-07-20T06:31:42Z", + "evidence": "evidence/2026-07-20-travelwolf-alpha7-rollback.json" + } + }, + { + "id": "mushu-permission-shortcuts-alpha8-production", "consumer": "mushu", - "consumer_commit": "f26d958b64f214a422aeb629860c8c9a46a4c084", - "run_id": "mushu-alpha7-20260718T215300Z-f26d958", - "tag_provenance": "docker-registry:5000/mushu-opencode:sha-99d6328b1", - "docker_image_id": "sha256:ed6293ca87e6db1dc07870d3ed6da21960743e6e2e904533b3c566d334b58d6a", + "consumer_commit": "3f6754caab3f66537a5834f44ee63c1f05ecb047", + "registry_ref": "docker-registry:5000/mushu-opencode@sha256:a16bf0c4cd17698eba02244f6ba0e429b2238b6455e1415ff69fb08965c863a7", + "application_image": "docker-registry:5000/mushu@sha256:30f204184520d504c7c384c7f6e503fa3547217aaaef2f1d0d65884533bdeacd", + "base_image": "docker-registry:5000/mushu-opencode-base@sha256:de8a6adeefb30ce1c474cde23fe173dfe059434a13df0a54d14c7473edb33e16", "source_commit": "99d6328b18ff8a340a928449a2aa8bc184a063f9", + "build_source_commit": "521185613a864a16a76a21c7a60597ab3973bae9", "reported_version": "0.0.0-permission-shortcuts-202606300721", "profiles": ["ruby-rest-sse"], - "certification_status": "pending", + "certification_status": "certified", + "current_certification": { + "status": "certified", + "client_commit": "9277646a4bb2cf25a8384ffc140b154f49ea5766", + "consumer_commit": "3f6754caab3f66537a5834f44ee63c1f05ecb047", + "certified_at": "2026-07-20T07:13:51Z", + "evidence": "evidence/2026-07-20-mushu-alpha8-production.json" + }, "previous_certification": { - "status": "candidate-certified", + "status": "certified", "client_commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", - "certified_at": "2026-07-18T21:53:00Z", - "persisted_final_text": "compat-ok", - "model_request_count": 1 + "consumer_commit": "af2e9b0ce7ccacf49500450595a67c765a9d617a", + "certified_at": "2026-07-20T06:39:46.921805Z", + "evidence": "evidence/2026-07-20-mushu-alpha7-rollback.json" } } ] diff --git a/manifests/runtime-tuples.json b/manifests/runtime-tuples.json index 98cf2b8..a30fdae 100644 --- a/manifests/runtime-tuples.json +++ b/manifests/runtime-tuples.json @@ -1,16 +1,200 @@ { "schema_version": 1, - "migration_state": "candidate", + "migration_state": "certified", "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.", - "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." + "status": "certified", + "reason": "Every consumer has exact current and previous passing tuples.", + "required_action": "Promote only a newly certified, materially changed tuple." }, "consumers": { "ajent-rails": { "profile": "rails-persisted-turn", "current": { + "status": "certified", + "consumer_commit": "2ec8b879c5a34bb683fa5aaf26b998041d0e1264", + "consumer_tree": "a1e044f3a4f5c5ac0510a1d998c4afb55286da5a", + "opencode_ruby": { + "version": "0.0.1.alpha8", + "git_commit": "9277646a4bb2cf25a8384ffc140b154f49ea5766", + "source_provenance": "unpublished-exact-commit" + }, + "opencode_rails": { + "version": "0.0.1.alpha8", + "git_commit": "a9add2a7c1dd3eb978aa8b4ebf9ef7e111d1057f", + "source_provenance": "unpublished-exact-commit" + }, + "runtime": { + "image": "docker-registry:5000/ajent_rails@sha256:c7b2c6f3c3ffb6af62069c3142a2d76e606b0794077b81c5464add2266b7c350", + "application_image": "docker-registry:5000/ajent_rails@sha256:c7b2c6f3c3ffb6af62069c3142a2d76e606b0794077b81c5464add2266b7c350", + "aigl_image": "docker-registry:5000/opencode-aigl@sha256:f4064cb8b7a0f680f076dba9c72ba8e63b1457227c9fad5aa1407f0e0aac12e7", + "blackline_image": "docker-registry:5000/opencode-blackline@sha256:7b4ea4d70ff5fd35e3b17c571865dd6bb489217c7c00942e8569132fe2f749c8", + "raven_image": "docker-registry:5000/opencode-raven@sha256:9fe37ee88d8b10165e17f5f2a5c47b2396256464e732b4ef49138fb92a1234e3", + "build_source_commit": "2ec8b879c5a34bb683fa5aaf26b998041d0e1264", + "reported_version": "1.18.3", + "upstream_base": "ghcr.io/anomalyco/opencode@sha256:c2d5d6398df72aac85cb1bdc8f900c71a9b75a33fb7c0a76dc1484e4b126e41e" + }, + "application_canary": { + "checked_at": "2026-07-20T08:15:17Z", + "products": [ + { + "id": "aigl", + "agent": "travel-agent", + "model": "openai-gpt-5.5", + "final_text": "CANARY_OK", + "reported_version": "1.18.3", + "status": "pass" + }, + { + "id": "blackline", + "agent": "employment-lawyer", + "model": "moonshot-kimi-k2.5", + "final_text": "CANARY_OK", + "reported_version": "1.18.3", + "status": "pass" + }, + { + "id": "raven", + "agent": "raven-legal", + "model": "moonshot-kimi-k2.5", + "final_text": "CANARY_OK", + "reported_version": "1.18.3", + "status": "pass" + } + ], + "session_cleanup_status": "pass", + "status": "pass" + }, + "forge_ci": { + "pr_run_id": "2718", + "main_build_run_id": "2719", + "protected_main_run_id": "2720", + "deploy_job_id": "12065", + "status": "pass" + }, + "production": { + "checked_at": "2026-07-20T08:15:17Z", + "application_health_status": 200, + "blackline_health_status": 200, + "ajent_public_status": 308, + "voice_contract_status": "pass", + "scotus_contract_status": "pass", + "status": "pass" + }, + "source_availability": { + "origin_main": "2ec8b879c5a34bb683fa5aaf26b998041d0e1264", + "backup_main": "2ec8b879c5a34bb683fa5aaf26b998041d0e1264", + "github_main": "2ec8b879c5a34bb683fa5aaf26b998041d0e1264", + "status": "pass" + }, + "compatibility_certified_at": "2026-07-20T08:15:17Z", + "certification": { + "status": "pass", + "certified_at": "2026-07-20T08:15:17Z", + "tuple_sha256": "sha256:326a87cdb67061bdd36dfcee989ae149511a0c627e857488d59e0f2b3c796720", + "evidence": [ + { + "path": "evidence/2026-07-20-ajent-rails-alpha8-production.json", + "sha256": "sha256:1db57a6df214d03a3466ce8668c773723e733f00d08d072dfa55206c3e8b09a1" + } + ] + } + }, + "candidate": null, + "previous": { + "status": "certified", + "consumer_commit": "6caa0132e4b172777126ed537de99cb7e9758e0b", + "consumer_tree": "29e6cd35313a06cef6f9c1f40a36e3ee582fd035", + "opencode_ruby": { + "version": "0.0.1.alpha7", + "git_commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", + "tag_provenance": { + "tag": "v0.0.1.alpha7", + "annotated_tag_object": "abfce3ec8cb1ee468107d0845521769502251645" + } + }, + "opencode_rails": { + "version": "0.0.1.alpha7", + "git_commit": "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb", + "tag_provenance": { + "tag": "v0.0.1.alpha7", + "annotated_tag_object": "61e128084b2b665c0ba20b332987e41794948d7c" + } + }, + "runtime": { + "image": "docker-registry:5000/ajent_rails@sha256:fe6fdc2325d304ddb276df7d35807c183e7e574876a0d76a6c6de6014619c76e", + "application_image": "docker-registry:5000/ajent_rails@sha256:fe6fdc2325d304ddb276df7d35807c183e7e574876a0d76a6c6de6014619c76e", + "aigl_image": "docker-registry:5000/opencode-aigl@sha256:f4064cb8b7a0f680f076dba9c72ba8e63b1457227c9fad5aa1407f0e0aac12e7", + "blackline_image": "docker-registry:5000/opencode-blackline@sha256:7b4ea4d70ff5fd35e3b17c571865dd6bb489217c7c00942e8569132fe2f749c8", + "raven_image": "docker-registry:5000/opencode-raven@sha256:9fe37ee88d8b10165e17f5f2a5c47b2396256464e732b4ef49138fb92a1234e3", + "build_source_commit": "6caa0132e4b172777126ed537de99cb7e9758e0b", + "reported_version": "1.18.3", + "upstream_base": "ghcr.io/anomalyco/opencode@sha256:c2d5d6398df72aac85cb1bdc8f900c71a9b75a33fb7c0a76dc1484e4b126e41e" + }, + "application_canary": { + "checked_at": "2026-07-20T08:14:53Z", + "products": [ + { + "id": "aigl", + "agent": "travel-agent", + "model": "openai-gpt-5.5", + "final_text": "CANARY_OK", + "reported_version": "1.18.3", + "status": "pass" + }, + { + "id": "blackline", + "agent": "employment-lawyer", + "model": "moonshot-kimi-k2.5", + "final_text": "CANARY_OK", + "reported_version": "1.18.3", + "status": "pass" + }, + { + "id": "raven", + "agent": "raven-legal", + "model": "moonshot-kimi-k2.5", + "final_text": "CANARY_OK", + "reported_version": "1.18.3", + "status": "pass" + } + ], + "session_cleanup_status": "pass", + "status": "pass" + }, + "consumer_verification": { + "test_runs": 4247, + "assertions": 15740, + "failures": 0, + "errors": 0, + "skips": 4, + "rubocop_files": 1430, + "status": "pass" + }, + "source_availability": { + "origin_ref": "refs/heads/opencode/opencode-alpha7-rollback-certified-20260720", + "backup_ref": "refs/heads/opencode/opencode-alpha7-rollback-certified-20260720", + "github_ref": "refs/heads/opencode/opencode-alpha7-rollback-certified-20260720" + }, + "rollback_operability": { + "source_archived_on_all_remotes": true, + "prebuilt_application_image_retained": true, + "application_rebuild_required": false, + "deployed": false + }, + "compatibility_certified_at": "2026-07-20T08:14:53Z", + "certification": { + "status": "pass", + "certified_at": "2026-07-20T08:14:53Z", + "tuple_sha256": "sha256:ae48333ba453f442d14263a916418a384f576db34d09a503111fc28c340eb408", + "evidence": [ + { + "path": "evidence/2026-07-20-ajent-rails-alpha7-rollback.json", + "sha256": "sha256:c3554a346a6585c7f98fde2aee2ad4f06ef4db7b621c98160183274e9e6ddc5c" + } + ] + } + }, + "emergency_provenance": { "status": "observed-production-contract-failed", "consumer_commit": "83eed3c247a352842b08fc5cf339c4d0acdaacba", "opencode_ruby": { @@ -27,10 +211,95 @@ "reported_version": "1.18.3", "upstream_base": "ghcr.io/anomalyco/opencode@sha256:c2d5d6398df72aac85cb1bdc8f900c71a9b75a33fb7c0a76dc1484e4b126e41e" } + } + }, + "travelwolf": { + "profile": "rails-persisted-turn", + "current": { + "status": "certified", + "consumer_commit": "02aed70eb4de201c6510e3be583699f8eb984ce7", + "consumer_tree": "f436a7248d90e5ae9e84c45f264acd913a4162aa", + "opencode_ruby": { + "version": "0.0.1.alpha8", + "git_commit": "9277646a4bb2cf25a8384ffc140b154f49ea5766", + "source_provenance": "unpublished-exact-commit" + }, + "opencode_rails": { + "version": "0.0.1.alpha8", + "git_commit": "a9add2a7c1dd3eb978aa8b4ebf9ef7e111d1057f", + "source_provenance": "unpublished-exact-commit" + }, + "runtime": { + "image": "ghcr.io/anomalyco/opencode@sha256:e975a0647576016dfdf77d54b979ca30d32b4750472c10263e9894aad6628c2a", + "application_image_id": "sha256:031371e20f34e1014712ddc668b4e182483664bb70f116b683c0ec83d6668338", + "reported_version": "1.16.1" + }, + "application_canary": { + "checked_at": "2026-07-20T06:31:13.506Z", + "final_text": "TRAVELWOLF_ALPHA8_CANARY_OK", + "application_prompt_count": 1, + "user_turn_count": 1, + "authoritative_assistant_turn_count": 1, + "child_turn_count": 0, + "model_request_count_observed": false, + "cleanup_status": "pass", + "status": "pass" + }, + "forge_ci": { + "status": "pass", + "pr_run_id": "2702", + "main_run_id": "2704" + }, + "production": { + "checked_at": "2026-07-20T06:51:48Z", + "application_image_created_at": "2026-07-20T06:42:55.929825054Z", + "internal_health_status": 200, + "authenticated_public_health_status": 200, + "container_status": "running", + "deployment_mode": "manual-isolated-kamal", + "status": "pass" + }, + "rollback_operability": { + "certified_previous_consumer_commit": "531011defe4acc45a15ffe44a8e9da9cb610aaac", + "immediate_platform_rollback_is_certified_tuple": false, + "immediate_platform_rollback_commit": "8494c52d11bf2995c378398e977d82d06a09e046" + }, + "compatibility_certified_at": "2026-07-20T06:51:48Z", + "certification": { + "status": "pass", + "certified_at": "2026-07-20T06:51:48Z", + "tuple_sha256": "sha256:0321c4246bed6ab62587924a83204a9e2ca7517b1dec5e4aef35dcac400fcc8e", + "evidence": [ + { + "path": "evidence/2026-07-20-travelwolf-alpha8-production.json", + "sha256": "sha256:ab8d8de768d415b3a68090507fb6ec40bdfad439fcaf0471beea4f52450e9d2f" + } + ] + } }, - "candidate": { - "status": "compatibility-certified", - "consumer_commit": "96552893baccd5ccba1592b2585edfef1299b4f9", + "immediate_platform_rollback": { + "status": "uncertified-known-contract-mismatch", + "consumer_commit": "8494c52d11bf2995c378398e977d82d06a09e046", + "opencode_ruby": { + "version": "0.0.1.alpha6", + "git_commit": "9356268fb339269cd53763435af7ce88df1eeec7" + }, + "opencode_rails": { + "version": "0.0.1.alpha6", + "git_commit": "8d1eb7701fe9e24baa64c16d6af03c6ed1f423eb" + }, + "runtime": { + "image": "ghcr.io/anomalyco/opencode@sha256:e975a0647576016dfdf77d54b979ca30d32b4750472c10263e9894aad6628c2a", + "application_image_id": "sha256:4291b256b37eeb694435093655db17154c837e53335b8d0eb8f1b07a744e856e", + "reported_version": "1.16.1" + }, + "reason": "This retained application image is the platform's one-click target, but its alpha6 clients are not the certified alpha7 previous tuple." + }, + "candidate": null, + "previous": { + "status": "certified", + "consumer_commit": "531011defe4acc45a15ffe44a8e9da9cb610aaac", + "consumer_tree": "d0e47b2d90ace87c6cc9c4b94b2f22987441c1f6", "opencode_ruby": { "version": "0.0.1.alpha7", "git_commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", @@ -48,26 +317,44 @@ } }, "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" + "image": "ghcr.io/anomalyco/opencode@sha256:e975a0647576016dfdf77d54b979ca30d32b4750472c10263e9894aad6628c2a", + "reported_version": "1.16.1" }, "application_canary": { - "checked_at": "2026-07-18T21:58:00Z", - "path": "Blackline::GenerateResponseJob", - "persisted_final_text": "compat-ok", - "model_request_count": 1, + "checked_at": "2026-07-20T06:22:36.958Z", + "final_text": "TRAVELWOLF_ALPHA7_CANARY_OK", + "application_prompt_count": 1, + "user_turn_count": 1, + "authoritative_assistant_turn_count": 1, + "child_turn_count": 0, + "model_request_count_observed": false, + "cleanup_status": "pass", "status": "pass" }, - "certified_at": "2026-07-18T22:03:14Z" + "forge_ci": { + "status": "pass", + "pr_run_id": "2696", + "main_run_id": "2698" + }, + "rollback_operability": { + "source_archived_on_all_remotes": true, + "prebuilt_application_image_retained": false, + "application_rebuild_required": true + }, + "compatibility_certified_at": "2026-07-20T06:31:42Z", + "certification": { + "status": "pass", + "certified_at": "2026-07-20T06:31:42Z", + "tuple_sha256": "sha256:20ecec3d2c3743efcd8575e71ec46dd7b86c2865e472511d16a96d57ddb8b40c", + "evidence": [ + { + "path": "evidence/2026-07-20-travelwolf-alpha7-rollback.json", + "sha256": "sha256:d5e36ffc40f44ff28cbd7eee7bf209fa595ce57619081a76f8d049499b5d30a4" + } + ] + } }, - "previous": null - }, - "travelwolf": { - "profile": "rails-persisted-turn", - "current": { + "emergency_provenance": { "status": "observed-production-contract-failed", "consumer_commit": "1ca756d64f058c9e0379f69fc4a3d53330cfcd15", "opencode_ruby": { @@ -82,10 +369,110 @@ "image": "ghcr.io/anomalyco/opencode@sha256:e975a0647576016dfdf77d54b979ca30d32b4750472c10263e9894aad6628c2a", "reported_version": "1.16.1" } + } + }, + "mushu": { + "profile": "ruby-rest-sse", + "current": { + "status": "certified", + "consumer_commit": "3f6754caab3f66537a5834f44ee63c1f05ecb047", + "consumer_tree": "726c3878e71f7a4cf8c0b8a3fa8fc15382bbab45", + "opencode_ruby": { + "version": "0.0.1.alpha8", + "git_commit": "9277646a4bb2cf25a8384ffc140b154f49ea5766", + "source_provenance": "unpublished-exact-commit" + }, + "runtime": { + "tag_provenance": "docker-registry:5000/mushu-opencode:sha-521185613a864a16a76a21c7a60597ab3973bae9", + "registry_ref": "docker-registry:5000/mushu-opencode@sha256:a16bf0c4cd17698eba02244f6ba0e429b2238b6455e1415ff69fb08965c863a7", + "application_image": "docker-registry:5000/mushu@sha256:30f204184520d504c7c384c7f6e503fa3547217aaaef2f1d0d65884533bdeacd", + "base_image": "docker-registry:5000/mushu-opencode-base@sha256:de8a6adeefb30ce1c474cde23fe173dfe059434a13df0a54d14c7473edb33e16", + "source_commit": "99d6328b18ff8a340a928449a2aa8bc184a063f9", + "build_source_commit": "521185613a864a16a76a21c7a60597ab3973bae9", + "reported_version": "0.0.0-permission-shortcuts-202606300721" + }, + "preproduction_canary": { + "successful_turn_count": 13, + "turn_count": 13, + "successful_conversation_count": 6, + "conversation_count": 6, + "successful_mcp_context_count": 2, + "mcp_context_count": 2, + "database_sha256": "sha256:7fbe32ff9621196a2504aeba5eca78cb0c9c670edd7a1168b1f81f667872e2b6", + "log_sha256": "sha256:51b698fcc8b7bce68e79cbae10997e19eb47a88985cbe24871d10740dda26225", + "status": "pass" + }, + "application_canary": { + "started_at": "2026-07-20T07:03:17Z", + "checked_at": "2026-07-20T07:04:51.376351Z", + "successful_turn_count": 13, + "turn_count": 13, + "successful_conversation_count": 6, + "conversation_count": 6, + "successful_mcp_context_count": 1, + "mcp_context_count": 1, + "status": "pass" + }, + "forge_ci": { + "status": "pass", + "pr_run_id": "2700", + "main_run_id": "2705" + }, + "production": { + "checked_at": "2026-07-20T07:13:20Z", + "web_health_status": 200, + "status_health_status": 200, + "runtime_readiness": "ready", + "containers": [ + "web", + "calendar", + "telegram", + "opencode" + ], + "deployment_mode": "manual", + "status": "pass" + }, + "rollback_operability": { + "certified_previous_consumer_commit": "af2e9b0ce7ccacf49500450595a67c765a9d617a", + "immediate_platform_rollback_is_certified_tuple": false, + "immediate_platform_rollback_commit": "82ca30b18e1f38b55e2bed91cbd46f1eaf4a5922" + }, + "compatibility_certified_at": "2026-07-20T07:13:51Z", + "certification": { + "status": "pass", + "certified_at": "2026-07-20T07:13:51Z", + "tuple_sha256": "sha256:804410a63cf2b6e4056d6dd9ce289ac72464f441c9785b6fc8dd50016263a1a8", + "evidence": [ + { + "path": "evidence/2026-07-20-mushu-alpha8-production.json", + "sha256": "sha256:786bf92a02588b951b5b7f7a21ed915b7ac1120538c5f894af027c190379170f" + } + ] + } }, - "candidate": { - "status": "compatibility-certified", - "consumer_commit": "19747b59fa21e12560afd9edd73ed674ab3a644c", + "immediate_platform_rollback": { + "status": "uncertified-known-contract-failure", + "consumer_commit": "82ca30b18e1f38b55e2bed91cbd46f1eaf4a5922", + "opencode_ruby": { + "version": "0.0.1.alpha2", + "git_commit": "889d38332f98f5f7b76d16952b7204d8a5b9a662" + }, + "opencode_rails": null, + "runtime": { + "registry_ref": "docker-registry:5000/mushu-opencode@sha256:ed6293ca87e6db1dc07870d3ed6da21960743e6e2e904533b3c566d334b58d6a", + "application_image": null, + "application_image_provenance": "not-retained", + "base_image": "docker-registry:5000/mushu-opencode-base@sha256:de8a6adeefb30ce1c474cde23fe173dfe059434a13df0a54d14c7473edb33e16", + "source_commit": "99d6328b18ff8a340a928449a2aa8bc184a063f9", + "reported_version": "0.0.0-permission-shortcuts-202606300721" + }, + "reason": "The one-click application image is not retained and the alpha2 client failed the contract; use the certified alpha7 previous source tuple instead." + }, + "candidate": null, + "previous": { + "status": "certified", + "consumer_commit": "af2e9b0ce7ccacf49500450595a67c765a9d617a", + "consumer_tree": "f43f48972ab444310508e9067831dfc6977f1a24", "opencode_ruby": { "version": "0.0.1.alpha7", "git_commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", @@ -94,34 +481,56 @@ "annotated_tag_object": "abfce3ec8cb1ee468107d0845521769502251645" } }, - "opencode_rails": { - "version": "0.0.1.alpha7", - "git_commit": "2a391ccad1d098e4bb51eb19b6ca52adaa79e5cb", - "tag_provenance": { - "tag": "v0.0.1.alpha7", - "annotated_tag_object": "61e128084b2b665c0ba20b332987e41794948d7c" - } - }, "runtime": { - "image": "ghcr.io/anomalyco/opencode@sha256:e975a0647576016dfdf77d54b979ca30d32b4750472c10263e9894aad6628c2a", - "reported_version": "1.16.1" + "tag_provenance": "docker-registry:5000/mushu-opencode:sha-99d6328b1", + "registry_ref": "docker-registry:5000/mushu-opencode@sha256:ed6293ca87e6db1dc07870d3ed6da21960743e6e2e904533b3c566d334b58d6a", + "base_image": "docker-registry:5000/mushu-opencode-base@sha256:de8a6adeefb30ce1c474cde23fe173dfe059434a13df0a54d14c7473edb33e16", + "source_commit": "99d6328b18ff8a340a928449a2aa8bc184a063f9", + "build_source_commit": null, + "provenance_strength": "one-time-tag-source-attestation", + "reported_version": "0.0.0-permission-shortcuts-202606300721" }, "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, - "cleanup_status": "pass", + "started_at": "2026-07-20T06:37:55Z", + "checked_at": "2026-07-20T06:39:46.921805Z", + "successful_turn_count": 13, + "turn_count": 13, + "successful_conversation_count": 6, + "conversation_count": 6, + "successful_mcp_context_count": 7, + "mcp_context_count": 7, + "database_sha256": "sha256:a567d2ff9a906c9b78831bb6fa43153af539f1e76ee63bf35961472f55651d74", + "log_sha256": "sha256:a35a83b003a1f082b8a721b3faad8b35cb71dc9d1ceaedfc3cec3b26916e85b9", "status": "pass" }, - "certified_at": "2026-07-18T22:03:14Z" + "forge_ci": { + "status": "not_run", + "run_ids": [], + "reason": "Frozen exact head predates the runner-only system-test workflow repair; clean-worktree application canary is authoritative." + }, + "source_availability": { + "origin_ref": "refs/heads/review/mushu-opencode-alpha7-consumer-final-20260719", + "backup_ref": "refs/heads/review/mushu-opencode-alpha7-consumer-final-20260719" + }, + "rollback_operability": { + "source_archived_on_all_remotes": true, + "prebuilt_application_image_retained": false, + "application_rebuild_required": true + }, + "compatibility_certified_at": "2026-07-20T06:39:46.921805Z", + "certification": { + "status": "pass", + "certified_at": "2026-07-20T06:39:46.921805Z", + "tuple_sha256": "sha256:8d7d3cb2d3e2d920c900a9c1ada365026872904588cff0b166903d1d6d32183f", + "evidence": [ + { + "path": "evidence/2026-07-20-mushu-alpha7-rollback.json", + "sha256": "sha256:143deb6b400a05305991c956eaf80f07fdee76a9cce98af96f8eee395ed9847d" + } + ] + } }, - "previous": null - }, - "mushu": { - "profile": "ruby-rest-sse", - "current": { + "emergency_provenance": { "status": "observed-production-contract-failed", "consumer_commit": "fe16e5bab028e3c92cd5508ef2cebd9bfa23386f", "opencode_ruby": { @@ -134,34 +543,7 @@ "source_commit": "99d6328b18ff8a340a928449a2aa8bc184a063f9", "reported_version": "0.0.0-permission-shortcuts-202606300721" } - }, - "candidate": { - "status": "compatibility-certified", - "consumer_commit": "f26d958b64f214a422aeb629860c8c9a46a4c084", - "opencode_ruby": { - "version": "0.0.1.alpha7", - "git_commit": "78b6f9c9e9c7d58b699af1c3c17764acd33de798", - "tag_provenance": { - "tag": "v0.0.1.alpha7", - "annotated_tag_object": "abfce3ec8cb1ee468107d0845521769502251645" - } - }, - "runtime": { - "tag_provenance": "docker-registry:5000/mushu-opencode:sha-99d6328b1", - "docker_image_id": "sha256:ed6293ca87e6db1dc07870d3ed6da21960743e6e2e904533b3c566d334b58d6a", - "source_commit": "99d6328b18ff8a340a928449a2aa8bc184a063f9", - "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", - "model_request_count": 1, - "status": "pass" - }, - "certified_at": "2026-07-18T22:03:14Z" - }, - "previous": null + } } } } 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 7064d37..7abb438 100644 --- a/test/repository_test.rb +++ b/test/repository_test.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "json" +require "digest" require "minitest/autorun" require_relative "../lib/opencode_compat/client_candidate" require_relative "../lib/opencode_compat/runtime_tuple_promoter" @@ -48,7 +49,14 @@ class RepositoryTest < Minitest::Test assert_equal ["ruby-rest-sse"], target.fetch("profiles") assert_equal "shared-client-contract-only", target.fetch("certification_scope", "shared-client-contract-only") - assert_equal "pending", target.fetch("certification_status") + assert_equal "certified", target.fetch("certification_status") + current = target.fetch("current_certification") + assert_equal "certified", current.fetch("status") + assert_match(/\A[0-9a-f]{40}\z/, current.fetch("client_commit")) + assert_match(/\A[0-9a-f]{40}\z/, current.fetch("rails_commit")) + assert_equal current.fetch("expected_text"), current.fetch("full_text") + assert_equal 1, current.fetch("llm_request_count") + assert_path_exists File.join(ROOT, current.fetch("evidence")) previous = target["previous_certification"] next unless previous @@ -88,22 +96,29 @@ class RepositoryTest < Minitest::Test assert_equal clients.fetch("opencode-ruby").fetch("ref"), dependency.fetch("ref") end - def test_image_matrix_is_pending_and_bound_to_the_exact_client_candidate + def test_image_matrix_is_certified_and_bound_to_the_exact_unpublished_client_candidate candidate = json("manifests/client-candidate.json") clients = candidate.fetch("clients") matrix_candidate = json("manifests/image-matrix.json").fetch("client_candidate") - assert_equal "pending", matrix_candidate.fetch("certification_status") + assert_equal "certified", matrix_candidate.fetch("certification_status") + assert_equal "unpublished", matrix_candidate.fetch("publication_state") + assert_path_exists File.join(ROOT, matrix_candidate.fetch("evidence")) assert_equal candidate.fetch("release_train"), matrix_candidate.fetch("release_train") assert_equal candidate.fetch("publication_state"), matrix_candidate.fetch("publication_state") assert_equal clients.dig("opencode-ruby", "ref"), matrix_candidate.fetch("opencode_ruby_commit") assert_equal clients.dig("opencode-rails", "ref"), matrix_candidate.fetch("opencode_rails_commit") json("manifests/image-matrix.json").fetch("host_canary").each do |target| - assert_equal "pending", target.fetch("certification_status") + assert_equal "certified", target.fetch("certification_status") + current = target.fetch("current_certification") + assert_equal "certified", current.fetch("status") + assert_match(/\A[0-9a-f]{40}\z/, current.fetch("client_commit")) + assert_path_exists File.join(ROOT, current.fetch("evidence")) previous = target.fetch("previous_certification") - assert_includes %w[candidate-certified certified], previous.fetch("status") + assert_equal "certified", previous.fetch("status") assert_match(/\A[0-9a-f]{40}\z/, previous.fetch("client_commit")) + assert_path_exists File.join(ROOT, previous.fetch("evidence")) end end @@ -129,6 +144,32 @@ class RepositoryTest < Minitest::Test assert evidence.fetch("limitations").any? { |entry| entry.include?("remain unverified") } end + def test_alpha8_ci_evidence_certifies_shared_and_lockstep_scopes_without_claiming_apps + evidence = json("evidence/2026-07-20-opencode-alpha8-shared-client-ci.json") + candidate = json("manifests/client-candidate.json").fetch("clients") + + assert_equal "pass", evidence.fetch("status") + assert_equal "unpublished", evidence.fetch("publication_state") + assert_equal candidate.dig("opencode-ruby", "ref"), evidence.dig("clients", "opencode_ruby", "commit") + assert_equal candidate.dig("opencode-rails", "ref"), evidence.dig("clients", "opencode_rails", "commit") + assert_equal "29725021192", evidence.dig("github_workflow", "run_id") + assert_equal "2717", evidence.dig("gitea_workflow", "run_id") + assert_equal false, evidence.dig("gitea_workflow", "artifact_evidence_claimed") + assert_operator evidence.fetch("certified_at"), :>=, evidence.dig("github_workflow", "completed_at") + assert_operator evidence.fetch("certified_at"), :>=, evidence.dig("gitea_workflow", "completed_at") + assert_equal %w[3.2.11 3.3.12 3.4.10 4.0.6], + evidence.fetch("rails_lockstep").map { |entry| entry.fetch("ruby_runtime_version") } + assert evidence.fetch("rails_lockstep").all? { |entry| entry.fetch("status") == "pass" } + assert_equal 3, evidence.fetch("exact_image_contracts").length + evidence.fetch("exact_image_contracts").each do |contract| + assert_equal "pass", contract.fetch("status") + assert_equal contract.fetch("expected_text"), contract.fetch("full_text") + assert_equal 1, contract.fetch("authoritative_assistant_message_count") + assert_equal 1, contract.fetch("llm_request_count") + end + assert evidence.fetch("limitations").any? { |entry| entry.include?("application profile") } + end + def test_certified_migration_keeps_previous_tuple tuples = json("manifests/runtime-tuples.json") return unless tuples.fetch("migration_state") == "certified" @@ -155,7 +196,7 @@ class RepositoryTest < Minitest::Test def test_runtime_tuples_use_full_commits_and_no_mutable_registry_coordinate tuples = json("manifests/runtime-tuples.json") tuples.fetch("consumers").each_value do |consumer| - %w[current candidate previous].each do |slot| + %w[current candidate previous emergency_provenance].each do |slot| tuple = consumer[slot] next unless tuple @@ -176,37 +217,83 @@ class RepositoryTest < Minitest::Test end end - def test_failed_alpha2_baselines_block_promotion_instead_of_becoming_previous + def test_failed_alpha2_baselines_remain_uncertified_emergency_provenance tuples = json("manifests/runtime-tuples.json") readiness = tuples.fetch("promotion_readiness") - assert_equal "blocked", readiness.fetch("status") - assert_path_exists File.join(ROOT, readiness.fetch("evidence")) + assert_equal "certified", tuples.fetch("migration_state") + assert_equal "certified", readiness.fetch("status") tuples.fetch("consumers").each_value do |consumer| - assert_equal "observed-production-contract-failed", consumer.dig("current", "status") - assert_nil consumer.fetch("previous") + emergency = consumer.fetch("emergency_provenance") + assert_equal "observed-production-contract-failed", emergency.fetch("status") + refute emergency.key?("certification") + assert_equal "certified", consumer.dig("current", "status") + assert_equal "0.0.1.alpha8", consumer.dig("current", "opencode_ruby", "version") + assert_equal "certified", consumer.dig("previous", "status") + assert_equal "0.0.1.alpha7", consumer.dig("previous", "opencode_ruby", "version") + refute consumer.key?("rollback_state") end end - def test_candidate_evidence_is_bound_to_complete_tuple_fingerprints + def test_platform_one_click_targets_are_explicitly_uncertified_and_separate + consumers = json("manifests/runtime-tuples.json").fetch("consumers") + + %w[travelwolf mushu].each do |name| + rollback = consumers.fetch(name).fetch("immediate_platform_rollback") + assert_match(/\Auncertified-/, rollback.fetch("status")) + assert_match(/\A[0-9a-f]{40}\z/, rollback.fetch("consumer_commit")) + assert_match(/\A[0-9a-f]{40}\z/, rollback.dig("opencode_ruby", "git_commit")) + refute rollback.key?("certification") + refute_equal consumers.dig(name, "previous", "consumer_commit"), rollback.fetch("consumer_commit") + end + + travelwolf_runtime = consumers.dig("travelwolf", "immediate_platform_rollback", "runtime") + assert_match(/@sha256:[0-9a-f]{64}\z/, travelwolf_runtime.fetch("image")) + assert_match(/\Asha256:[0-9a-f]{64}\z/, travelwolf_runtime.fetch("application_image_id")) + + mushu_runtime = consumers.dig("mushu", "immediate_platform_rollback", "runtime") + assert_match(/@sha256:[0-9a-f]{64}\z/, mushu_runtime.fetch("registry_ref")) + assert_nil mushu_runtime.fetch("application_image") + assert_equal "not-retained", mushu_runtime.fetch("application_image_provenance") + end + + def test_certified_tuple_evidence_is_bound_to_complete_tuple_fingerprints promoter = OpenCodeCompat::RuntimeTuplePromoter.new(root: ROOT) - evidence_paths = { - "ajent-rails" => "evidence/2026-07-18-ajent-rails-alpha7-candidate.json", - "travelwolf" => "evidence/2026-07-18-travelwolf-alpha7-candidate.json", - "mushu" => "evidence/2026-07-18-mushu-alpha7-candidate.json" - } - evidence_paths.each do |consumer, path| - evidence = json(path) - fingerprints = promoter.fingerprints( - consumer: consumer, - consumer_commit: evidence.fetch("consumer_commit") - ) + json("manifests/runtime-tuples.json").fetch("consumers").each do |consumer, entry| + profile = entry.fetch("profile") + %w[current previous].each do |slot| + tuple = entry.fetch(slot) + fingerprint = promoter.send(:tuple_fingerprint, tuple, consumer: consumer, profile: profile) + certification = tuple.fetch("certification") + promoter.send( + :validate_recorded_certification!, + tuple, + consumer: consumer, + profile: profile, + expected_fingerprint: fingerprint, + label: "#{consumer} #{slot}" + ) - 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 "pass", evidence.fetch("status") + assert_equal "certified", tuple.fetch("status") + assert_equal "pass", certification.fetch("status") + assert_equal fingerprint, certification.fetch("tuple_sha256") + refute_empty certification.fetch("evidence") + + certification.fetch("evidence").each do |reference| + assert_equal %w[path sha256], reference.keys.sort + path = reference.fetch("path") + bytes = File.binread(File.join(ROOT, path)) + assert_equal "sha256:#{Digest::SHA256.hexdigest(bytes)}", reference.fetch("sha256") + evidence = json(path) + assert_equal consumer, evidence.fetch("consumer") + assert_equal profile, evidence.fetch("profile") + assert_equal tuple.fetch("consumer_commit"), evidence.fetch("consumer_commit") + assert_equal certification.fetch("certified_at"), evidence.fetch("certified_at") + assert_equal fingerprint, evidence.fetch("tuple_sha256") + assert_equal "pass", evidence.fetch("status") + end + end end end diff --git a/test/runtime_tuple_promoter_test.rb b/test/runtime_tuple_promoter_test.rb index c22d474..f5fdd15 100644 --- a/test/runtime_tuple_promoter_test.rb +++ b/test/runtime_tuple_promoter_test.rb @@ -10,14 +10,22 @@ class RuntimeTuplePromoterTest < Minitest::Test CONSUMER = "example" CURRENT_COMMIT = "1" * 40 CANDIDATE_COMMIT = "2" * 40 + NEXT_COMMIT = "7" * 40 RUBY_CURRENT = "3" * 40 RUBY_CANDIDATE = "4" * 40 + RUBY_NEXT = "8" * 40 RAILS_CURRENT = "5" * 40 RAILS_CANDIDATE = "6" * 40 + RAILS_NEXT = "9" * 40 + CURRENT_TREE = "a" * 40 + CANDIDATE_TREE = "b" * 40 + NEXT_TREE = "c" * 40 CURRENT_IMAGE = "ghcr.io/anomalyco/opencode@sha256:#{'a' * 64}" CANDIDATE_IMAGE = "ghcr.io/anomalyco/opencode@sha256:#{'b' * 64}" + NEXT_IMAGE = "ghcr.io/anomalyco/opencode@sha256:#{'c' * 64}" CURRENT_TIME = "2026-07-17T12:00:00Z" CANDIDATE_TIME = "2026-07-18T12:00:00Z" + NEXT_TIME = "2026-07-19T12:00:00Z" def setup @root = Dir.mktmpdir("opencode-compat-promotion") @@ -49,10 +57,12 @@ class RuntimeTuplePromoterTest < Minitest::Test assert_equal RUBY_CANDIDATE, consumer.dig("current", "opencode_ruby", "git_commit") assert_equal "certified", consumer.dig("current", "status") assert_equal candidate.fetch("tuple_sha256"), consumer.dig("current", "certification", "tuple_sha256") + assert_equal evidence_reference(candidate), consumer.dig("current", "certification", "evidence", 0) assert_equal CURRENT_COMMIT, consumer.dig("previous", "consumer_commit") assert_equal RUBY_CURRENT, consumer.dig("previous", "opencode_ruby", "git_commit") assert_equal "certified", consumer.dig("previous", "status") assert_equal previous.fetch("tuple_sha256"), consumer.dig("previous", "certification", "tuple_sha256") + assert_equal evidence_reference(previous), consumer.dig("previous", "certification", "evidence", 0) assert_equal "certified", promoted.fetch("migration_state") assert_equal promoted, JSON.parse(File.read(manifest_path)) end @@ -121,6 +131,41 @@ class RuntimeTuplePromoterTest < Minitest::Test assert_match(/full 40-character/, error.message) end + def test_rejects_malformed_immutable_tuple_coordinates_but_allows_unknown_build_source + mutations = { + "consumer tree" => lambda { |candidate| candidate["consumer_tree"] = "abc123" }, + "build source commit" => lambda { |candidate| candidate.fetch("runtime")["build_source_commit"] = "abc123" }, + "rollback commit" => lambda do |candidate| + candidate["rollback_operability"] = {"immediate_platform_rollback_commit" => "abc123"} + end, + "artifact digest" => lambda do |candidate| + candidate["application_canary"] = {"database_sha256" => "sha256:abc123"} + end + } + + mutations.each do |label, mutate| + manifest = valid_manifest + mutate.call(manifest.dig("consumers", CONSUMER, "candidate")) + write_manifest(manifest) + + error = assert_raises(OpenCodeCompat::PromotionError, label) do + @promoter.fingerprints(consumer: CONSUMER, consumer_commit: CANDIDATE_COMMIT) + end + assert_match(/full 40-character|exact sha256 digest/, error.message, label) + end + + manifest = valid_manifest + manifest.dig("consumers", CONSUMER, "candidate", "runtime")["build_source_commit"] = nil + write_manifest(manifest) + assert_match( + /\Asha256:[0-9a-f]{64}\z/, + @promoter.fingerprints( + consumer: CONSUMER, + consumer_commit: CANDIDATE_COMMIT + ).fetch("candidate_tuple_sha256") + ) + end + def test_rejects_missing_server_version_or_unknown_profile manifest = valid_manifest manifest.dig("consumers", CONSUMER, "candidate", "runtime").delete("reported_version") @@ -175,6 +220,163 @@ class RuntimeTuplePromoterTest < Minitest::Test assert_equal before, File.binread(manifest_path) end + def test_explicit_degraded_bootstrap_preserves_failed_emergency_provenance + manifest = valid_manifest + manifest.dig("consumers", CONSUMER, "current")["status"] = "observed-production-contract-failed" + write_manifest(manifest) + candidate_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: candidate_fingerprint + ) + before = File.binread(manifest_path) + + preview = @promoter.bootstrap_current( + consumer: CONSUMER, + consumer_commit: CANDIDATE_COMMIT, + certification: certification(CANDIDATE_TIME, evidence), + acknowledgement: OpenCodeCompat::RuntimeTuplePromoter::DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT, + dry_run: true + ) + + assert_equal before, File.binread(manifest_path) + assert_equal "bootstrap-current-only", preview.fetch("migration_state") + + bootstrapped = @promoter.bootstrap_current( + consumer: CONSUMER, + consumer_commit: CANDIDATE_COMMIT, + certification: certification(CANDIDATE_TIME, evidence), + acknowledgement: OpenCodeCompat::RuntimeTuplePromoter::DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT + ) + 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.dig("promotion_readiness", "status") + assert_equal bootstrapped, JSON.parse(File.read(manifest_path)) + end + + def test_bootstrap_then_normal_promotion_retains_the_first_passing_tuple_as_previous + manifest = valid_manifest + manifest.dig("consumers", CONSUMER, "current")["status"] = "observed-production-contract-failed" + write_manifest(manifest) + bootstrap_fingerprint = @promoter.fingerprints( + consumer: CONSUMER, + consumer_commit: CANDIDATE_COMMIT + ).fetch("candidate_tuple_sha256") + bootstrap_evidence = write_evidence( + "bootstrap-rollback.json", + commit: CANDIDATE_COMMIT, + timestamp: CANDIDATE_TIME, + fingerprint: bootstrap_fingerprint + ) + @promoter.bootstrap_current( + consumer: CONSUMER, + consumer_commit: CANDIDATE_COMMIT, + certification: certification(CANDIDATE_TIME, bootstrap_evidence), + acknowledgement: OpenCodeCompat::RuntimeTuplePromoter::DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT + ) + + manifest = JSON.parse(File.read(manifest_path)) + manifest.dig("consumers", CONSUMER)["candidate"] = { + "status" => "compatibility-certified", + "certified_at" => NEXT_TIME, + "consumer_commit" => NEXT_COMMIT, + "opencode_ruby" => {"version" => "0.0.1.alpha8", "git_commit" => RUBY_NEXT}, + "opencode_rails" => {"version" => "0.0.1.alpha8", "git_commit" => RAILS_NEXT}, + "runtime" => {"image" => NEXT_IMAGE, "reported_version" => "1.19.0"} + } + write_manifest(manifest) + next_fingerprint = @promoter.fingerprints( + consumer: CONSUMER, + consumer_commit: NEXT_COMMIT + ).fetch("candidate_tuple_sha256") + next_evidence = write_evidence( + "next-current.json", + commit: NEXT_COMMIT, + timestamp: NEXT_TIME, + fingerprint: next_fingerprint + ) + + promoted = @promoter.promote( + consumer: CONSUMER, + consumer_commit: NEXT_COMMIT, + certification: certification(NEXT_TIME, next_evidence) + ) + consumer = promoted.dig("consumers", CONSUMER) + + assert_equal NEXT_COMMIT, consumer.dig("current", "consumer_commit") + assert_equal CANDIDATE_COMMIT, consumer.dig("previous", "consumer_commit") + assert_equal "certified", consumer.dig("previous", "status") + assert_equal "observed-production-contract-failed", consumer.dig("emergency_provenance", "status") + refute consumer.key?("rollback_state") + assert_equal "certified", promoted.fetch("migration_state") + assert_equal "certified", promoted.dig("promotion_readiness", "status") + end + + def test_degraded_bootstrap_requires_exact_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_degraded_bootstrap_rejects_a_passing_baseline_or_existing_previous_tuple + candidate, = write_matching_evidence + error = assert_raises(OpenCodeCompat::PromotionError) do + @promoter.bootstrap_current( + consumer: CONSUMER, + consumer_commit: CANDIDATE_COMMIT, + certification: certification(CANDIDATE_TIME, candidate), + acknowledgement: OpenCodeCompat::RuntimeTuplePromoter::DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT, + dry_run: true + ) + end + assert_match(/only for a current tuple known to fail/, error.message) + + manifest = valid_manifest + manifest.dig("consumers", CONSUMER, "current")["status"] = "observed-production-contract-failed" + manifest.dig("consumers", CONSUMER)["previous"] = manifest.dig("consumers", CONSUMER, "current").dup + write_manifest(manifest) + candidate_fingerprint = @promoter.fingerprints( + consumer: CONSUMER, + consumer_commit: CANDIDATE_COMMIT + ).fetch("candidate_tuple_sha256") + evidence = write_evidence( + "bootstrap-existing-previous.json", + commit: CANDIDATE_COMMIT, + timestamp: CANDIDATE_TIME, + fingerprint: candidate_fingerprint + ) + error = assert_raises(OpenCodeCompat::PromotionError) do + @promoter.bootstrap_current( + consumer: CONSUMER, + consumer_commit: CANDIDATE_COMMIT, + certification: certification(CANDIDATE_TIME, evidence), + acknowledgement: OpenCodeCompat::RuntimeTuplePromoter::DEGRADED_BOOTSTRAP_ACKNOWLEDGEMENT, + dry_run: true + ) + end + assert_match(/cannot replace an existing previous/, 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)) @@ -196,6 +398,114 @@ class RuntimeTuplePromoterTest < Minitest::Test assert_equal before, File.binread(manifest_path) end + def test_hash_binds_evidence_and_accepts_a_structured_reference + candidate, previous = write_matching_evidence + structured_candidate = evidence_reference(candidate) + structured_previous = evidence_reference(previous) + + promoted = @promoter.promote( + consumer: CONSUMER, + consumer_commit: CANDIDATE_COMMIT, + certification: certification(CANDIDATE_TIME, candidate).merge("evidence" => [structured_candidate]), + previous_certification: certification(CURRENT_TIME, previous).merge("evidence" => [structured_previous]), + dry_run: true + ) + + assert_equal structured_candidate, promoted.dig("consumers", CONSUMER, "current", "certification", "evidence", 0) + + error = assert_raises(OpenCodeCompat::PromotionError) do + @promoter.promote( + consumer: CONSUMER, + consumer_commit: CANDIDATE_COMMIT, + certification: certification(CANDIDATE_TIME, candidate).merge( + "evidence" => [structured_candidate.merge("sha256" => "sha256:#{'0' * 64}")] + ), + previous_certification: certification(CURRENT_TIME, previous).merge("evidence" => [structured_previous]), + dry_run: true + ) + end + assert_match(/has sha256=.*expected/, error.message) + end + + def test_rejects_evidence_coordinates_that_disagree_with_the_tuple + mutations = { + "consumer_tree" => lambda { |document| document["consumer_tree"] = "f" * 40 }, + "opencode_ruby.commit" => lambda do |document| + document["clients"] = { + "opencode_ruby" => {"version" => "0.0.1.alpha7", "commit" => "f" * 40} + } + end, + "runtime.image" => lambda do |document| + document["runtime"] = { + "image" => "ghcr.io/anomalyco/opencode@sha256:#{'f' * 64}", + "reported_version" => "1.18.3" + } + end + } + + mutations.each do |field, mutate| + write_manifest(valid_manifest) + candidate, previous = write_matching_evidence + path = File.join(@root, candidate.fetch("path")) + document = JSON.parse(File.read(path)) + mutate.call(document) + File.write(path, JSON.pretty_generate(document) + "\n") + + error = assert_raises(OpenCodeCompat::PromotionError, field) do + @promoter.promote( + consumer: CONSUMER, + consumer_commit: CANDIDATE_COMMIT, + certification: certification(CANDIDATE_TIME, candidate), + previous_certification: certification(CURRENT_TIME, previous), + dry_run: true + ) + end + assert_match(/#{Regexp.escape(field)}/, error.message, field) + end + end + + def test_recorded_certifications_require_hash_bound_evidence_references + candidate, previous = write_matching_evidence + promoted = @promoter.promote( + consumer: CONSUMER, + consumer_commit: CANDIDATE_COMMIT, + certification: certification(CANDIDATE_TIME, candidate), + previous_certification: certification(CURRENT_TIME, previous) + ) + consumer = promoted.dig("consumers", CONSUMER) + consumer.dig("current", "certification")["evidence"] = [candidate.fetch("path")] + consumer["candidate"] = { + "status" => "compatibility-certified", + "certified_at" => NEXT_TIME, + "consumer_commit" => NEXT_COMMIT, + "consumer_tree" => NEXT_TREE, + "opencode_ruby" => {"version" => "0.0.1.alpha8", "git_commit" => RUBY_NEXT}, + "opencode_rails" => {"version" => "0.0.1.alpha8", "git_commit" => RAILS_NEXT}, + "runtime" => {"image" => NEXT_IMAGE, "reported_version" => "1.19.0"} + } + write_manifest(promoted) + fingerprint = @promoter.fingerprints( + consumer: CONSUMER, + consumer_commit: NEXT_COMMIT + ).fetch("candidate_tuple_sha256") + next_evidence = write_evidence( + "next-after-legacy-reference.json", + commit: NEXT_COMMIT, + timestamp: NEXT_TIME, + fingerprint: fingerprint + ) + + error = assert_raises(OpenCodeCompat::PromotionError) do + @promoter.promote( + consumer: CONSUMER, + consumer_commit: NEXT_COMMIT, + certification: certification(NEXT_TIME, next_evidence), + dry_run: true + ) + end + assert_match(/hash-bound \{path, sha256\}/, error.message) + end + def test_rejects_non_passing_or_implicit_certification_metadata candidate, previous = write_matching_evidence @@ -262,6 +572,7 @@ class RuntimeTuplePromoterTest < Minitest::Test "current" => { "status" => "observed-production", "consumer_commit" => CURRENT_COMMIT, + "consumer_tree" => CURRENT_TREE, "opencode_ruby" => {"version" => "0.0.1.alpha2", "git_commit" => RUBY_CURRENT}, "opencode_rails" => {"version" => "0.0.1.alpha2", "git_commit" => RAILS_CURRENT}, "runtime" => {"image" => CURRENT_IMAGE, "reported_version" => "1.16.1"} @@ -269,6 +580,7 @@ class RuntimeTuplePromoterTest < Minitest::Test "candidate" => { "status" => "compatibility-certified", "certified_at" => "2026-07-18T10:00:00Z", + "consumer_tree" => CANDIDATE_TREE, "opencode_ruby" => {"version" => "0.0.1.alpha7", "git_commit" => RUBY_CANDIDATE}, "opencode_rails" => {"version" => "0.0.1.alpha7", "git_commit" => RAILS_CANDIDATE}, "runtime" => {"image" => CANDIDATE_IMAGE, "reported_version" => "1.18.3"} @@ -319,6 +631,14 @@ class RuntimeTuplePromoterTest < Minitest::Test } end + def evidence_reference(evidence) + path = evidence.fetch("path") + { + "path" => path, + "sha256" => "sha256:#{Digest::SHA256.file(File.join(@root, path)).hexdigest}" + } + end + def manifest_path File.join(@root, "manifests", "runtime-tuples.json") end