diff --git a/CONSTITUTION_CONFORMANCE.md b/CONSTITUTION_CONFORMANCE.md index 05ddf05..f3df5c7 100644 --- a/CONSTITUTION_CONFORMANCE.md +++ b/CONSTITUTION_CONFORMANCE.md @@ -1,34 +1,31 @@ -# Constitution conformance record - -## Studio #99 Comic Manifest v1 release preparation — 2026-10-07 +## Studio #99 Codex publication and Platform adoption — 2026-10-09 Owner: @andrewperis. Constitution 1.0.0 at -`a9cc8a503aa30e17820edc62ac95f7cbe10e0564`. Base: merged Platform Issue 98 -`64aba4c770903ef1b597e49ab9bec69828edb6fe`; Codex contract source candidate -is the separate branch commit recorded in the candidate lock. Scope: separate -Comic Manifest v1 contract catalog and additive Platform API/package candidate, -release checks, documentation, and reproducible local artifacts. The historical -SDK and Context Builder locks and published assets remain separate and unchanged. - -The owner's task-specific Epic #6 A3 AI-review exception applies to code-merge -review only. It authorizes AI review within that merge scope; it does not -authorize an A4 release decision, tag, release asset, or publication. No -release/publication exception is provided. No separate human review of final -release artifacts is claimed here; an explicit owner release approval remains -required. The owner's chat acceptance at source head -`97dcd990e87bd0368ddcbecf66d1a6718bc7629e`, recorded -`2026-10-07T19:15:38Z`, is bounded to the Issue 98 golden packet; it is neither a -submitted GitHub review nor approval of this release candidate. - -Evidence and remaining gates are tracked in -`release/comic-manifest-verification.md`. This preparation does not create a -tag, release, upload, npm publication, security/visibility change, or approval. -Codex publication and fresh immutable-download verification, exact Platform -adoption and validator regeneration, final post-merge builds, and owner approval -of exact artifact tuples remain outstanding. Fresh audits of the current -lockfiles report zero vulnerabilities after the Platform fast-uri update; -rerun against the exact post-merge lock before publication. -Status: proposed preparation; no release readiness or publication is asserted. +`a9cc8a503aa30e17820edc62ac95f7cbe10e0564`. Platform base: +`9476aeb27246230d982bd8cfde7cfc0d82939de7`, including merged preparation PR #42. +The Codex Comic Manifest 1.0.0 tag targets +`12e437e30328a3bb9cd2d15e6307a70b4b7e0e2a` and is published with immutable +status. The owner explicitly approved that exact Codex commit, tag and asset +tuples for publication. This A4 decision is not represented as a human technical +review of the release assets. + +The task-specific Epic #6 A3 AI-review exception covered code-merge review only. +It did not authorize this A4 release decision or any Platform release. The owner +acceptance recorded at `2026-10-07T19:15:38Z` for Platform source head +`97dcd990e87bd0368ddcbecf66d1a6718bc7629e` remains bounded to the Issue 98 +golden/catalog/oracle packet; it is neither a GitHub review nor publication +approval. No separate human review of the final Codex assets is claimed. + +This PR adopts the exact immutable Codex release tuples and regenerates the +Platform validator from a fresh public schema download. It preserves the historical +SDK and Context Builder locks/assets and the reviewed Comic Manifest fixture, +golden, catalog, oracle and source bytes. The Platform package remains private and +unpublished; this adoption does not create a Platform tag/release, upload assets, +publish to npm, change security/visibility settings, or close Issue #99/Epic #6. +The owner must separately approve exact Platform artifacts after merge and a +post-merge build. Evidence is tracked in `release/comic-manifest-verification.md`. +Status: Codex publication verified; Platform adoption proposed for independent +review. No final Platform release readiness or publication is asserted. ## Issue #86 verified contract adoption — 2026-09-16 diff --git a/README.md b/README.md index 75f0c78..f0675fe 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ Third-party material remains subject to its own terms. Examples and fixtures must be synthetic, properly licensed, or already public. The [Comic Manifest contract consumer proof](docs/comic-manifest-contracts.md) -checks the unreleased Codex candidate against the released SDK using offline +checks the published Codex contract against the released SDK using offline synthetic fixtures; it does not add a Manifest runtime or production dependency. The unreleased [Comic Manifest validation API](docs/comic-manifest-validation.md) diff --git a/docs/comic-manifest-contracts.md b/docs/comic-manifest-contracts.md index a8a8e68..21b9767 100644 --- a/docs/comic-manifest-contracts.md +++ b/docs/comic-manifest-contracts.md @@ -1,17 +1,21 @@ # Comic Manifest contract consumer proof (Studio #89) -This is a **test-only consumer of an unreleased Codex 1.0.0 candidate**. It adds no -Manifest runtime, orchestration, source retrieval, trust issuer or publication API. -Codex owns the schema and semantics; public creative authority remains in Universe. - -The [fixture lock](../tests/fixtures/comic-manifest-contract-lock.json) pins the -owner-reviewed Codex candidate at commit -`8044643bf888067f5a6e0d212f843d72e8787f2b` by full source paths, sizes and SHA-256 -digests. The schema and scenario are verbatim test artifacts, not local -normative schema forks or production dependencies. The existing Context Builder -fixture is identical to the pinned Codex bytes and is reused unchanged. All apparent -private context, grants, IDs, publication numbers and URLs are synthetic. A fixed -fixture UUID is not a production attestation or evidence of secure issuance. +Codex Comic Manifest 1.0.0 is published at immutable tag +[`contract/comic-manifest/v1.0.0`](https://github.com/DefinitelySecureStudio/codex/releases/tag/contract/comic-manifest/v1.0.0). +This is still a **test-only consumer proof**: it adds no Platform Manifest runtime, +orchestration, source retrieval, trust issuer or publication API. Codex owns the +schema and semantics; public creative authority remains in Universe. + +The [fixture lock](../tests/fixtures/comic-manifest-contract-lock.json) remains +pinned to the owner-reviewed Codex source snapshot at commit +`8044643bf888067f5a6e0d212f843d72e8787f2b`; it records historical test bytes, not +the runtime release pin. The published Codex tag targets +`12e437e30328a3bb9cd2d15e6307a70b4b7e0e2a`. Its schema has the same byte size and +SHA-256 as the reviewed fixture, so this adoption leaves the fixture, golden, +catalog and oracle bytes unchanged. Runtime metadata pins the published release +tuple separately. All apparent private context, grants, IDs, publication numbers +and URLs are synthetic. A fixed fixture UUID is not a production attestation or +evidence of secure issuance. The [Codex specification](https://github.com/DefinitelySecureStudio/codex/blob/8044643bf888067f5a6e0d212f843d72e8787f2b/specs/manifests/comic-manifest-v1.md), RFC 0007 and RFC 0008 define three immutable payloads, exact-version support, @@ -41,12 +45,13 @@ inspect free text, URLs, and output bytes; sentinel checks do not prove safety. ## Adoption and constitutional scope -Merge the coordinated Codex contract PR before this proof. Refresh test pins only -from reviewed exact Codex blobs and verify bytes; do not edit the copies by hand. -Production adoption waits for #99's immutable contract and implementation releases -and full verified artifact tuples. Source commit URLs are not release assets. -Released Prompt SDK/Context Package/Context Builder code, schemas, lockfiles and -artifacts remain unchanged. Rollback removes this test-only consumer. +The Codex contract publication is complete and its exact immutable release tuple +is recorded in the runtime lock. This test proof retains its separately reviewed +historical fixture pins. Production use of the Platform implementation still waits +for its own release, exact verified artifact tuples and separate owner approval. +Source commit URLs are not release assets. Released Prompt SDK/Context Package/ +Context Builder code, schemas, lockfiles and artifacts remain unchanged. Rollback +removes this test-only consumer. Constitution: v1.0.0, `constitution/v1.0.0`, Studio commit `a9cc8a503aa30e17820edc62ac95f7cbe10e0564`; Studio ADR 0018 at diff --git a/docs/comic-manifest-release.md b/docs/comic-manifest-release.md index b86c023..ca7e83d 100644 --- a/docs/comic-manifest-release.md +++ b/docs/comic-manifest-release.md @@ -1,9 +1,10 @@ # Comic Manifest v1 API and release guide -Status: implementation is an unreleased candidate for Studio #99. The proposed -Platform component tag is `comic-manifest/v1.0.0`; the additive Platform npm -package version candidate is `1.2.0`. The package remains private. Neither tag -nor package is published by this change. +Status: Codex Comic Manifest 1.0.0 is published immutably and its exact release +has been independently downloaded and verified. The Platform implementation is +still an unreleased Studio #99 candidate. Its proposed component tag is +`comic-manifest/v1.0.0`; the additive Platform npm package version is `1.2.0` and +the package remains private. The Platform tag and package have not been published. Codex owns normative meanings and the exact contract at `contract/comic-manifest/v1.0.0`. Platform owns the implementation, CLI, @@ -86,12 +87,14 @@ bytes. The owner accepted the proposed Issue 98 golden packet at source head `97dcd990e87bd0368ddcbecf66d1a6718bc7629e` in chat at `2026-10-07T19:15:38Z`; PR #41 records that it was not a submitted GitHub review. -That acceptance applies only to the pinned golden packet. It is not evidence of -human review of the present release candidate or of final release approval. -The current public Constitution exception register lists no active Article 12 -exception. No AI-review exception is claimed or relied on; AI-assisted review -and automation remain advisory and cannot replace the required qualified-human -decisions in Constitution Section 9.4. +That acceptance applies only to the pinned golden packet. The separate owner A4 +decision approved the exact Codex commit, tag and asset tuples for Codex publication; +it did not approve Platform publication and is not represented as a human technical +review of the released artifacts. +The task-specific Epic #6 A3 AI-review exception covered code-merge review only +and did not authorize the A4 release decision. No separate human review of final +release artifacts is claimed. Platform publication still requires its own owner +decision on exact built artifact tuples. ## Compatibility, deprecation, and support @@ -134,17 +137,78 @@ establish Canon, permission, public disclosure, or release approval. ## Release order and Epic #101 handoff -The release sequence is Codex publication and fresh exact-download/immutability -verification; reviewed Platform adoption of those exact published tuples and -validator regeneration; Platform source/package build and owner approval; then -Platform publication and fresh verification. The current Platform lock remains -candidate/unpublished and readiness stays blocked until the first two steps are -complete. GitHub asset transport MIME may be `application/octet-stream`; record -it separately from the declared content media type in the contract manifest. +The Codex publication and fresh immutable-download verification are complete. +This Platform adoption PR pins those exact published tuples and regenerates the +validator from the fresh download. The Platform package remains private and the +API inventory still records `candidate-unpublished`. After independent review, +merge and exact-head CI, build and inspect the Platform source/package artifacts, +then obtain a separate owner approval naming their exact tuples. Only that later +approval can authorize a Platform release and fresh verification. The readiness +check in this repository covers Codex adoption only; it is not final Platform +release readiness. After this PR is independently reviewed and merged, build the +Platform release artifact set once from the exact merge commit into a new +directory outside the checkout: + +```sh +node scripts/build-comic-manifest-release.mjs /path/to/platform-release-candidate \ + --source-commit "$MERGED_COMMIT" --source-tree "$MERGED_TREE" +``` + +The builder records `build_provenance` fields as facts about that build: the +artifact set was built as an approval candidate while owner approval was absent +and Platform publication had not happened. These fields are not live publication +or approval state and are never changed after owner approval. The seven files in +the manifest plus the manifest itself form the eight-file immutable release set. + +Generate the detached approval packet from that set: + +```sh +node scripts/create-comic-manifest-approval-packet.mjs /path/to/platform-release-candidate \ + > /path/to/platform-owner-approval.json +``` + +The packet starts with `status: "awaiting-owner-decision"` and +`owner_approval: null`; it grants no approval. After the owner explicitly reviews +the exact source identity and all eight filename/size/SHA-256 tuples, record the +owner's decision in that separate packet by setting `status` to `approved` and +filling `owner_approval` with `decision: "approve"`, `owner: "@andrewperis"`, +an RFC 3339 `approved_at`, and a non-empty `decision_reference`, then run: + +```sh +node scripts/verify-comic-manifest-approval.mjs /path/to/platform-release-candidate \ + /path/to/platform-owner-approval.json +``` + +This verifier is read-only. It requires the explicit `@andrewperis` approval +record, exact source commit/tree/tag, exactly eight regular files, and all eight +matching filename/size/digest tuples. Missing, changed, extra, or symlinked files +and mismatched approval fields fail closed. `approval_record_matches: true` +means the declared record matches those bytes; `owner_identity_authenticated: +false` and `external_owner_authentication_required: true` mean the verifier does +not authenticate who wrote it or create an approval. Before publication, confirm +the same owner decision through the external authenticated review path and retain +its reference in `decision_reference`. + +After approval, publish those same eight files unchanged; do not rebuild after +approval. To verify a later fresh download, first download the full asset set to a +new empty directory, then run: + +```sh +node scripts/verify-comic-manifest-downloads.mjs \ + /path/to/platform-release-candidate/comic-manifest-v1.0.0.manifest.json \ + /path/to/fresh-downloads +``` + +This compares the fresh bytes to the separately retained approved build set and +rejects missing or extra files. GitHub asset metadata records `application/json` +for the Codex uploads and public downloads returned `application/octet-stream`; +both are recorded separately from each contract-declared media type. + The local builder accepts an external commit identity only when its supplied 40-character tree hash exactly matches the clean local source tree; both values -are recorded in the artifact manifest. Rebuild and rebind those identities after -the preparation PR is merged. +are recorded in the artifact manifest. The adoption PR does not create a Platform +tag, release, upload or npm publication. A pre-merge PR build is review evidence +only; the owner approval packet must be generated from a fresh post-merge build. Epic #101 receives the verified Codex and Platform artifact tuples, API/CLI and adapter limits, synthetic conformance evidence, source/package manifests, and diff --git a/docs/comic-manifest-validation.md b/docs/comic-manifest-validation.md index e818efe..73f80b8 100644 --- a/docs/comic-manifest-validation.md +++ b/docs/comic-manifest-validation.md @@ -1,9 +1,10 @@ # Comic Manifest parsing and local validation (Studio #90) The **unreleased** `@definitely-secure-studio/platform/comic-manifest` entry point -parses and validates explicit raw JSON against the owner-merged Codex candidate. -It has no default source reader, network fetcher, execution engine, production -trust provider or publisher. The explicit host-injected approval boundary is +parses and validates explicit raw JSON against the published Codex Comic Manifest +1.0.0 schema. The Platform package remains private and unreleased. The module has no +default source reader, network fetcher, execution engine, production trust provider +or publisher. The explicit host-injected approval boundary is described in [#95](comic-manifest-approvals.md). It does not mutate the released Prompt SDK or Context Builder. @@ -33,7 +34,7 @@ if (!result.valid) { | `parseComicManifestJson(source)` | Bounded JSON grammar/encoding checks; success `{valid:true,value,diagnostics:[]}` does not establish a Comic Manifest schema | | `validateComicManifest(source)` | Parse, dispatch exact kind/version, validate the generated closed schema, check record-local invariants; success adds canonical `identity` | | `COMIC_MANIFEST_LIMITS` | Frozen contract maximums; callers may reject stricter limits before invoking the API | -| `COMIC_MANIFEST_CONTRACT` | Frozen reviewed source commit, schema ID/size/digest and explicit `unreleased-development` status | +| `COMIC_MANIFEST_CONTRACT` | Frozen published Codex tag/commit, schema ID, release asset URI, declared media type, byte size, digest and `released` status; this grants no execution or publication authority | | `createComicApprovalBoundary(...)` | Host-injected exact current approval evaluation for production review or release publication; no default trust adapter | | `createComicBuildResultBoundary(...)` | Explicit bounded artifact-byte verification, exact result/lineage checks, current disclosure/private-influence verification, final #95 approval check, and separate protected evidence/public proposal | | `diffComicRevision(candidate, previous)` | Deterministic value-free summary for an explicit unchanged/adjacent revision pair | @@ -142,15 +143,20 @@ properties. Normative gaps return to Codex, not local schema edits. Normative source: [Codex specification](https://github.com/DefinitelySecureStudio/codex/blob/8044643bf888067f5a6e0d212f843d72e8787f2b/specs/manifests/comic-manifest-v1.md), RFC 0007 and [RFC 0008](https://github.com/DefinitelySecureStudio/codex/blob/8044643bf888067f5a6e0d212f843d72e8787f2b/rfcs/0008-comic-manifest-rendition-profiles.md), -merged commit `8044643bf888067f5a6e0d212f843d72e8787f2b`. -[Runtime metadata](../src/comic-manifest/contract.js) pins the exact schema bytes; -[development fixture lock](../tests/fixtures/comic-manifest-validation-lock.json) -pins schema, scenario, negative cases and approval-time matrix to that same merged -Codex commit. The -updated 1.0.0 candidate replaces the earlier unreleased #89 profile shape; the -historical consumer proof remains pinned independently. These are candidate source -references, **not immutable production release tuples**. #99 remains the release -gate; no tag, registry publishing or production-ready claim is introduced here. +merged commit `8044643bf888067f5a6e0d212f843d72e8787f2b`. The published immutable +Codex contract tag targets `12e437e30328a3bb9cd2d15e6307a70b4b7e0e2a`; its schema +asset is `comic-manifest-v1.0.0.schema.json` at the locked release URI in +[Runtime metadata](../src/comic-manifest/contract.js). The schema bytes match the +historical [development fixture lock](../tests/fixtures/comic-manifest-validation-lock.json) +exactly; that lock's source commit remains unchanged for the owner-reviewed +scenario, negative cases and approval-time matrix. The published runtime pin and +the historical test fixtures therefore have separate provenance records. + +The Platform implementation remains an unreleased candidate. This PR updates only +the runtime pin, generated validator provenance and release-adoption evidence; it +does not change fixture, golden, catalog or oracle bytes or publish the Platform +package. The readiness check covers the upstream Codex adoption gate only; it does +not claim Platform publication, final artifact approval or release readiness. ```sh npm ci @@ -161,12 +167,13 @@ npm test The generator verifies schema size/digest and the installed/locked Ajv 8.20.0 and ajv-formats 3.0.1 versions before writing standalone validators. It takes explicit -local bytes and never fetches. The committed generated file must reproduce exactly; -it is not hand-edited. This adoption changes only the unreleased Comic Manifest -candidate and its local consumer fixtures. Released Prompt SDK v1, Context Package -v1 and Context Builder v1 bytes and locks remain unchanged. Future normative profile -changes return to Codex for review before an explicit Platform pin update and -regeneration. +local bytes and never fetches. The committed generated file reproduces from the +freshly downloaded published schema; the existing fixture file is byte-identical +to that asset and remains unchanged. This adoption updates only the unpublished +Platform runtime pin, validator provenance and release evidence. Released Prompt SDK +v1, Context Package v1 and Context Builder v1 bytes and locks remain unchanged. +Future normative profile changes return to Codex for review before an explicit +Platform pin update and regeneration. Tests cover reviewed positive/negative fixtures, rendition profile boundaries and cross-record output/episode compatibility, malformed grammar, duplicate and diff --git a/release/README.md b/release/README.md index 14618ea..4f72698 100644 --- a/release/README.md +++ b/release/README.md @@ -6,20 +6,34 @@ The additive component candidate is `comic-manifest/v1.0.0` in package `1.2.0`. Its independent Codex contract lock is `comic-manifest-contract-lock.json`; it must not be appended to the historical five-contract Prompt SDK lock or the separately verified Context Builder lock. -Run `node scripts/check-comic-manifest-release.mjs` to see the current gate. -It remains blocked until Codex publishes the exact contract assets, fresh -downloads and tag immutability are verified, and Platform adopts those exact -published tuples and regenerates its validator. The candidate builder can be -rehearsed locally with `--candidate`, but it never creates tags, releases, -uploads, changes publication status, or grants approval. +Run `node scripts/check-comic-manifest-release.mjs` to verify the exact published +Codex lock, immutable tag target, fresh-download tuple and regenerated runtime pin. +This is only the upstream contract-adoption gate. Platform remains private and +unpublished; a successful check does not approve or publish the Platform package. +After merge, the builder produces an approval-candidate set from the exact merge +commit/tree; it never creates tags, releases, uploads, changes publication status, +or grants approval. Its status fields describe the state when the files were built. `comic-manifest-verification.md` records the exact evidence required for a -post-merge candidate. The API inventory is `comic-manifest-api-v1.json`. -`node scripts/build-comic-manifest-release.mjs /absolute/new/output --candidate` -produces the source archive, package archive, lock/API snapshots, and a -deterministic artifact manifest. See the -[API/release guide](../docs/comic-manifest-release.md) for owner gates, -artifact media-type handling, and the Codex-before-Platform order. +post-merge candidate. The API inventory is `comic-manifest-api-v1.json`. Build +into a new external directory from the exact merge commit and tree: + +```sh +node scripts/build-comic-manifest-release.mjs /absolute/new/output \ + --source-commit "$MERGED_COMMIT" --source-tree "$MERGED_TREE" +``` + +`node scripts/create-comic-manifest-approval-packet.mjs /absolute/new/output` +prints a detached packet with no owner decision. After an external authenticated +owner decision, record it in that packet and run +`node scripts/verify-comic-manifest-approval.mjs /absolute/new/output /path/to/owner-approval.json`. +The verifier's `approval_record_matches: true` result means the record matches +the exact eight files; `owner_identity_authenticated: false` and +`external_owner_authentication_required: true` make clear that it does not +authenticate the owner or authorize publication. Upload the approved files +unchanged without rebuilding. +See the [API/release guide](../docs/comic-manifest-release.md) for the full +workflow, artifact media-type handling, and Codex-before-Platform order. ## Context Builder v1 preparation (#86) diff --git a/release/comic-manifest-api-v1.json b/release/comic-manifest-api-v1.json index 641339b..a1fca14 100644 --- a/release/comic-manifest-api-v1.json +++ b/release/comic-manifest-api-v1.json @@ -35,5 +35,6 @@ "exit_codes": { "success": 0, "input_or_usage": 2, "synthetic_authority": 3, "integrity_or_revision": 4, "protected_output_io": 5 } }, "contract_support": "exact-kind-and-version", + "upstream_contract_status": "published", "publication_status": "candidate-unpublished" } diff --git a/release/comic-manifest-contract-lock.json b/release/comic-manifest-contract-lock.json index 62ab3a0..07df886 100644 --- a/release/comic-manifest-contract-lock.json +++ b/release/comic-manifest-contract-lock.json @@ -1,10 +1,10 @@ { - "status": "candidate-unpublished", + "status": "published", "repository": "DefinitelySecureStudio/codex", "contract": "comic-manifest", "version": "1.0.0", "tag": "contract/comic-manifest/v1.0.0", - "commit": "028d5638e20d9283a2955aacbd38e1bfc6dca259", + "commit": "12e437e30328a3bb9cd2d15e6307a70b4b7e0e2a", "constitution_commit": "a9cc8a503aa30e17820edc62ac95f7cbe10e0564", "schema_id": "urn:definitely-secure:contract:comic-manifest:1.0.0:comic-manifest", "assets": [ @@ -20,19 +20,43 @@ "artifact_uri": "https://github.com/DefinitelySecureStudio/codex/releases/download/contract%2Fcomic-manifest%2Fv1.0.0/comic-manifest-v1.0.0.bundle.json", "media_type": "application/json", "byte_size": 865650, - "sha256": "sha256:661b2c2a0e7c478799551043fa704d936cec238a0e64fea8c4523a0775e67309" + "sha256": "sha256:bb131a7dbb96692172b4e44b56a272d041309e53b136433cc71a24c8d1b934de" }, { "filename": "comic-manifest-v1.0.0.manifest.json", "artifact_uri": "https://github.com/DefinitelySecureStudio/codex/releases/download/contract%2Fcomic-manifest%2Fv1.0.0/comic-manifest-v1.0.0.manifest.json", "media_type": "application/json", "byte_size": 1152, - "sha256": "sha256:8875573aa251776b7133fb5af82e87ce37a6c4a735bffab80a7beed543cf11fc" + "sha256": "sha256:56bba6d990c429384e16d8aa49af99b3a25ca8733efa96b5681be5932dcba878" } ], "publication": { - "status": "pending-owner-approval-and-publication", - "immutable": false, - "release_url": "https://github.com/DefinitelySecureStudio/codex/releases/tag/contract/comic-manifest/v1.0.0" + "status": "published", + "immutable": true, + "tag_target": "12e437e30328a3bb9cd2d15e6307a70b4b7e0e2a", + "verified_at": "2026-10-09T16:02:59Z", + "release_url": "https://github.com/DefinitelySecureStudio/codex/releases/tag/contract/comic-manifest/v1.0.0", + "transport": [ + { + "filename": "comic-manifest-v1.0.0.schema.json", + "github_release_asset_content_type": "application/json", + "public_download_http_status": 200, + "public_download_content_type": "application/octet-stream" + }, + { + "filename": "comic-manifest-v1.0.0.bundle.json", + "github_release_asset_content_type": "application/json", + "public_download_http_status": 200, + "public_download_content_type": "application/octet-stream" + }, + { + "filename": "comic-manifest-v1.0.0.manifest.json", + "github_release_asset_content_type": "application/json", + "public_download_http_status": 200, + "public_download_content_type": "application/octet-stream" + } + ], + "draft": false, + "prerelease": false } } diff --git a/release/comic-manifest-verification.md b/release/comic-manifest-verification.md index bcd6ca1..dc65ba2 100644 --- a/release/comic-manifest-verification.md +++ b/release/comic-manifest-verification.md @@ -1,89 +1,103 @@ # Comic Manifest v1 verification record — Studio #99 -Status: release preparation candidate; no tag, release, or npm package has been -published. Codex PR #16 has merged; Platform PR #42 remains an open preparation draft. This checklist -records what is verified locally and what must be reverified against the exact -post-merge candidate. It does not convert a successful test, AI review, draft -PR, or owner acceptance of the Issue 98 golden packet into release approval. +## Current state — 2026-10-09 -## Preparation evidence — 2026-10-07 +Codex Comic Manifest 1.0.0 is published as an immutable GitHub release. The +Platform #42 preparation PR is merged; this branch prepares the separate Platform +adoption of the exact published Codex tuples. The Platform package stays private, +its API inventory remains `candidate-unpublished`, and no Platform tag/release, +asset upload, npm publication, visibility/security change, or Issue/Epic closure +has occurred. -- Codex merged release source commit `12e437e30328a3bb9cd2d15e6307a70b4b7e0e2a`, tree `cae3cbe3ae5945b53d544ef5be7dd19941e13689`. Its exact post-merge approval candidate was built twice and compared byte-for-byte: schema 30,860 bytes (`7bd3c5392ae0db0c5baba553c233142d3d4427471f5850123eef7b87d4c8eaa4`), source bundle 865,650 bytes (`bb131a7dbb96692172b4e44b56a272d041309e53b136433cc71a24c8d1b934de`), and release manifest 1,152 bytes (`56bba6d990c429384e16d8aa49af99b3a25ca8733efa96b5681be5932dcba878`). All 248 Codex tests pass. The older PR-head artifact tuples are historical and superseded by the exact merged-commit packet. -- Platform prep branch includes current main after the Studio #98 DNS lookup - guard merge, commit `1b5fd00189e6dcc3f3c7f1a76f4229029c9fb8ea`, tree - `ccc876bd1e8184d29e53b4aa4bec3d2d630cfd8e`. The candidate package version - is `1.2.0`; component tag proposal is `comic-manifest/v1.0.0`. The npm package - allowlist contains runtime sources and notices only; tests, examples, and - fixtures are excluded. Its offline smoke installs the local candidate - tarball into an isolated consumer using a separate generated lock and cache, - imports the package export, and invokes the installed `studio-comic` binary - with only the synthetic public release fixture. The external output guard - resolves symlinked ancestors before creating candidate outputs, and the new - download verifier checks exact manifest bytes and the fixed asset allowlist. - Candidate builds are deterministic and readiness is intentionally blocked - by missing Codex publication and runtime-pin adoption. -- The candidate builder accepts a supplied source identity only when its full - commit ID exists locally as a Git commit object, resolves to the supplied - tree, and that tree exactly matches the clean local checkout used to build - the bytes. Source archives and lock/API snapshots are read from the verified - source commit. Candidate mode does not bypass this check. Every builder Git - invocation sets `GIT_NO_REPLACE_OBJECTS=1` for object checks and source - reads, without changing global Git configuration. Isolated replacement-ref - regressions reject a replaced valid commit as a different tree and reject a - wrong commit that a replacement ref points at the local tree. Other - regression coverage rejects a commit-shaped missing object, a tree object - presented as a commit, and a real commit paired with a different tree. The - newest prep head's CI is required to pass before review proceeds. -- The Platform lock retains Ajv 8.20.0, ajv-formats 3.0.1, - fast-deep-equal 3.1.3 (MIT), json-schema-traverse 1.0.0 (MIT), and - require-from-string 2.0.2 (MIT). The fresh audit identified one moderate - advisory in transitive fast-uri 3.1.7, GHSA-hrr3-gc8f-f4qj, patched in 3.1.8. - The lock now pins 3.1.8 within Ajv's existing range; `package.json` is - unchanged and no dependency was added. NOTICE was updated to 3.1.8 - (BSD-3-Clause); project LICENSE/NOTICE are Apache-2.0. -- After the lock update and main integration, fresh `npm audit --json` runs - against both exact lockfiles reported zero vulnerabilities. The Codex lock - SHA-256 is `d655c146d12f82d5583ffe3a3c9740c1716cee4158035e8db5929977e1a48721`; - the Platform lock SHA-256 is - `ef90ff0593d91ed26980629ce896ac08294281ed2521db01d7e19ad89b5d87bd`. - `npm ls --depth=1` confirms the installed Platform tree matches the lock. - Re-run both audits on the exact post-merge release heads before publication. +### Published Codex identity -## Evidence to bind to an exact head +- Repository: `DefinitelySecureStudio/codex` (public). +- Source/tag target: commit `12e437e30328a3bb9cd2d15e6307a70b4b7e0e2a`, tree + `cae3cbe3ae5945b53d544ef5be7dd19941e13689`. +- Tag: `contract/comic-manifest/v1.0.0`; GitHub ref resolves directly to the exact + commit above. +- Release: [Comic Manifest v1.0.0](https://github.com/DefinitelySecureStudio/codex/releases/tag/contract/comic-manifest/v1.0.0), + published `2026-10-09T16:00:19Z`; API verified `draft: false`, + `prerelease: false`, `immutable: true`. +- Repository immutable-release policy was read as `enabled: true`; no security + setting was changed. +- Owner A4 approval covered this exact Codex commit, tag and three asset tuples. + It is distinct from and not inferred from the A3 AI-review exception or the + Issue 98 golden acceptance. -- Codex publication: exact approved source commit and `contract/comic-manifest/v1.0.0` tag target; immutable release status; fresh downloads of schema, source bundle, and release manifest; declared and observed transport media types recorded separately; byte size and SHA-256 for each download. -- Platform candidate: source commit, `comic-manifest/v1.0.0` component tag proposal, package version 1.2.0, source archive/package/lock/API inventory/contract locks, and artifact-manifest identities. -- Verification: Codex 248/248 tests and CI run 19; Platform 700/700 tests, - offline Comic Manifest conformance 325/325 with 29 fixtures unchanged, and - [Platform CI run 80](https://github.com/DefinitelySecureStudio/platform/actions/runs/37689297426) - passed on code head `af408d4bbe083de4f6da160c7f6c12dc28c1a468` before this - documentation-only correction; two clean builds per repository compared - byte-for-byte; isolated local-tarball consumer import and installed - CLI invocation; fresh zero-finding audits of both exact lockfiles; - LICENSE/NOTICE and third-party notice review. -- After a future approved Platform publication, download the complete asset set - into a new directory and run - `node scripts/verify-comic-manifest-downloads.mjs /path/to/trusted-build-manifest.json /path/to/fresh-downloads`. - The trusted manifest must be the separately retained exact local build output. - The verifier checks the downloaded manifest bytes, exact asset allowlist, - regular files, declared media type, size, and SHA-256; it never extracts an - archive. Preserve GitHub's observed transport MIME as separate evidence. -- Approval: owner decision attached to the exact merged Codex and Platform artifact identities, plus any required independent reviewer findings. The Issue 98 acceptance at Platform source head `97dcd990e87bd0368ddcbecf66d1a6718bc7629e`, recorded in chat at `2026-10-07T19:15:38Z`, applies only to its reviewed proposed goldens/catalog/oracle bytes. It is not a GitHub review or release approval. +| Asset | Declared media type | Bytes | SHA-256 | GitHub asset metadata | Public download | +|---|---|---:|---|---|---| +| `comic-manifest-v1.0.0.schema.json` | `application/schema+json` | 30,860 | `7bd3c5392ae0db0c5baba553c233142d3d4427471f5850123eef7b87d4c8eaa4` | `application/json` | HTTP 200, `application/octet-stream` | +| `comic-manifest-v1.0.0.bundle.json` | `application/json` | 865,650 | `bb131a7dbb96692172b4e44b56a272d041309e53b136433cc71a24c8d1b934de` | `application/json` | HTTP 200, `application/octet-stream` | +| `comic-manifest-v1.0.0.manifest.json` | `application/json` | 1,152 | `56bba6d990c429384e16d8aa49af99b3a25ca8733efa96b5681be5932dcba878` | `application/json` | HTTP 200, `application/octet-stream` | -## Candidate status and blocker +All three assets were freshly downloaded from the draft before publication and +then again through both authenticated `gh release download` and unauthenticated +public HTTPS URLs after publication. Each download matched its approved local +counterpart byte-for-byte and by SHA-256. The table keeps contract-declared +content type, GitHub asset metadata content type, and public HTTP download content +type separate; no MIME value was copied between those layers. -The local release builder and lock use `candidate: true` until Codex's tag and -assets have been published and independently verified. The checker requires -both lock and nested publication statuses to say `published`, alongside the -immutable verification evidence. Platform's readiness -checker must return blocked while publication is absent, the runtime schema pin -is still an unreleased source reference, or an exact published tuple differs. -After Codex publication, Platform must adopt the exact downloaded tuple and -regenerate the validator from the downloaded schema before a final Platform -artifact build. Do not mark readiness or publication complete based on draft -assets, generated files, local hashes, or source links. +### Platform adoption scope -No new runtime dependencies are planned. The lockfile-only package version -change must leave Ajv, Ajv formats, and their transitive locked packages -unchanged. Final audit evidence must be fresh and tied to the exact lockfile; -an unavailable audit service is an unresolved gate, not a clean result. +The current Platform main base is `9476aeb27246230d982bd8cfde7cfc0d82939de7` +(tree `683e2dd392c93f1e55502edeb8aeed54c46ba3a3`), containing merged preparation +PR #42. This adoption changes the independent Comic Manifest contract lock, +runtime schema pin, generated validator provenance, readiness checks, tests and +release/constitution documentation. The released SDK and Context Builder locks, +package contents and assets remain unchanged. Reviewed Comic Manifest fixture, +golden, catalog, oracle and source bytes remain unchanged; the published schema +is byte-identical to the already reviewed fixture schema. + +`package.json` remains `1.2.0` and `private: true`; no package is published. The +current readiness check confirms only upstream Codex publication/adoption. Even +when that check passes, the Platform artifact is still a candidate and requires +independent PR review, merge, and exact-head CI. After merge, build once from the +exact merge commit into a new external directory and generate a detached owner +approval packet from that exact eight-file set. Its manifest records candidate, +approval, and publication facts as build-time provenance, not live status. The +owner must separately approve that packet's exact source commit/tree/tag and all +eight filenames, sizes, and hashes. The read-only verifier rejects missing, +changed, extra, or symlinked files and does not rewrite the packet or artifacts. +Its success result means only that the declared decision record matches those +bytes (`approval_record_matches: true`); it marks +`owner_identity_authenticated: false` and +`external_owner_authentication_required: true`. Confirm the decision through the +external authenticated owner-review path before publication. +After approval, publish those exact files unchanged; rebuilding after approval +would create a different set and require a new approval. + +### Historical preparation and checks + +- Codex source was built twice from the exact merged source commit before release; + the two sets matched byte-for-byte. Codex `npm test` passed 248/248, and CI run + [19](https://github.com/DefinitelySecureStudio/codex/actions/runs/37688411471) + passed on the same source tree. +- The Codex source bundle contains 130 tracked public source entries including + `LICENSE` and `NOTICE`; its credential-pattern scan found no matches. +- Platform #42 prepared the additive package/API candidate and passed its exact + head CI. Its previous pre-merge Codex tuples are superseded by the published + tuples above; this adoption updates those locks deliberately. +- Fresh `npm audit --json` on 2026-10-09 reported zero vulnerabilities for the + unchanged Platform lock SHA-256 `ef90ff0593d91ed26980629ce896ac08294281ed2521db01d7e19ad89b5d87bd`. + The raw report is retained at the task-level `prepared-artifacts/audit-evidence/platform-npm-audit-2026-10-09.json` + (SHA-256 `b966435ec31b7d61483d849d853d936f86183d654661604e532f1be934735a79`). +- Platform dependencies remain Ajv 8.20.0, ajv-formats 3.0.1, fast-deep-equal + 3.1.3, fast-uri 3.1.8, json-schema-traverse 1.0.0, and require-from-string + 2.0.2. No dependency is added by this adoption. `NOTICE` attributes fast-uri + 3.1.8 under BSD-3-Clause and the other locked runtime packages under MIT; + project `LICENSE` and `NOTICE` remain Apache-2.0. +- Existing review acceptance for Issue 98 at source head + `97dcd990e87bd0368ddcbecf66d1a6718bc7629e`, recorded + `2026-10-07T19:15:38Z`, applies only to its proposed golden/catalog/oracle + bytes. It is neither a submitted GitHub review nor A4 release approval. + +## Remaining gates + +The parent independently reviews and merges this adoption PR after its exact-head +CI passes. Then build and inspect the Platform source/package, lock and artifact +manifest from the merged source, refresh the locked-dependency audit, and request a +separate owner decision naming the exact Platform artifacts. Platform publication +and Issue #99/Epic #6 closure remain blocked until that later decision and fresh +public-download verification. No human technical review of the Codex release assets +is claimed; the owner approval authorizes only the named Codex publication. diff --git a/scripts/build-comic-manifest-release.mjs b/scripts/build-comic-manifest-release.mjs index 4bf611a..2ef217b 100644 --- a/scripts/build-comic-manifest-release.mjs +++ b/scripts/build-comic-manifest-release.mjs @@ -59,11 +59,11 @@ export async function resolveExternalOutput(destination, checkoutRoot = root) { return output; } -export async function buildComicManifestRelease(destination, { candidate = false, sourceIdentity = {} } = {}) { +export async function buildComicManifestRelease(destination, { sourceIdentity = {} } = {}) { const output = await resolveExternalOutput(destination); if (git('status', '--porcelain').length) throw Error('Build requires a clean committed checkout.'); - const readiness = await comicManifestReleaseReadiness(); - if (!readiness.ready && !candidate) throw Error('Codex contract publication/adoption is not ready.'); + const codexAdoption = await comicManifestReleaseReadiness(); + if (!codexAdoption.ready) throw Error('Codex contract publication/adoption is not ready.'); const localCommit = git('rev-parse', 'HEAD').toString().trim(); const tree = git('rev-parse', localCommit + '^{tree}').toString().trim(); const hasSourceIdentity = verifySourceIdentity(sourceIdentity, tree); @@ -92,7 +92,13 @@ export async function buildComicManifestRelease(destination, { candidate = false ]; const manifest = { repository: 'DefinitelySecureStudio/platform', component: 'comic-manifest', version: '1.0.0', - package_version: version, tag, commit, tree, candidate, readiness, + package_version: version, tag, commit, tree, + build_provenance: { + mode: 'release-approval-candidate', + owner_approval_status_at_build: 'not-approved', + publication_status_at_build: 'not-published' + }, + codex_adoption: codexAdoption, constitution_commit: 'a9cc8a503aa30e17820edc62ac95f7cbe10e0564', codex_contract: JSON.parse(git('show', commit + ':release/comic-manifest-contract-lock.json')), assets: files.map(([filename, bytes, media_type]) => ({ filename, media_type, byte_size: bytes.length, sha256: sha(bytes), @@ -106,13 +112,12 @@ export async function buildComicManifestRelease(destination, { candidate = false } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { - const args = process.argv.slice(3), candidate = args.includes('--candidate'), sourceIdentity = {}; + const args = process.argv.slice(3), sourceIdentity = {}; for (let index = 0; index < args.length; index++) { const key = args[index]; - if (key === '--candidate') continue; const value = args[++index]; if (!['--source-commit', '--source-tree'].includes(key) || !value || value.startsWith('--')) throw Error('Unknown or incomplete argument.'); sourceIdentity[key === '--source-commit' ? 'commit' : 'tree'] = value; } - console.log(JSON.stringify(await buildComicManifestRelease(process.argv[2], { candidate, sourceIdentity }), null, 2)); + console.log(JSON.stringify(await buildComicManifestRelease(process.argv[2], { sourceIdentity }), null, 2)); } diff --git a/scripts/check-comic-manifest-release.mjs b/scripts/check-comic-manifest-release.mjs index 105c445..49fd98a 100644 --- a/scripts/check-comic-manifest-release.mjs +++ b/scripts/check-comic-manifest-release.mjs @@ -3,12 +3,19 @@ import { pathToFileURL } from 'node:url'; import { COMIC_MANIFEST_CONTRACT } from '../src/comic-manifest/contract.js'; const tag = 'comic-manifest/v1.0.0'; -const codexPrefix = 'https://github.com/DefinitelySecureStudio/codex/releases/download/' + encodeURIComponent('contract/comic-manifest/v1.0.0') + '/'; +const codexTag = 'contract/comic-manifest/v1.0.0'; +const codexCommit = '12e437e30328a3bb9cd2d15e6307a70b4b7e0e2a'; +const releaseUrl = 'https://github.com/DefinitelySecureStudio/codex/releases/tag/' + codexTag; +const codexPrefix = 'https://github.com/DefinitelySecureStudio/codex/releases/download/' + encodeURIComponent(codexTag) + '/'; const expectedAssets = [ - ['comic-manifest-v1.0.0.schema.json', 'application/schema+json', codexPrefix], - ['comic-manifest-v1.0.0.bundle.json', 'application/json', codexPrefix], - ['comic-manifest-v1.0.0.manifest.json', 'application/json', codexPrefix] + ['comic-manifest-v1.0.0.schema.json', 'application/schema+json', 30860, 'sha256:7bd3c5392ae0db0c5baba553c233142d3d4427471f5850123eef7b87d4c8eaa4'], + ['comic-manifest-v1.0.0.bundle.json', 'application/json', 865650, 'sha256:bb131a7dbb96692172b4e44b56a272d041309e53b136433cc71a24c8d1b934de'], + ['comic-manifest-v1.0.0.manifest.json', 'application/json', 1152, 'sha256:56bba6d990c429384e16d8aa49af99b3a25ca8733efa96b5681be5932dcba878'] ]; +const expectedTransport = expectedAssets.map(([filename]) => ({ + filename, github_release_asset_content_type: 'application/json', + public_download_http_status: 200, public_download_content_type: 'application/octet-stream' +})); export function checkComicManifestRelease({ pkg, lock, pin = COMIC_MANIFEST_CONTRACT }) { const blockers = []; @@ -20,34 +27,33 @@ export function checkComicManifestRelease({ pkg, lock, pin = COMIC_MANIFEST_CONT const publication = lock?.publication; if (lock?.repository !== 'DefinitelySecureStudio/codex' || lock?.contract !== 'comic-manifest' || - lock?.version !== '1.0.0' || lock?.tag !== 'contract/comic-manifest/v1.0.0' || - !/^[a-f0-9]{40}$/.test(lock?.commit ?? '') || + lock?.version !== '1.0.0' || lock?.tag !== codexTag || lock?.commit !== codexCommit || lock?.constitution_commit !== 'a9cc8a503aa30e17820edc62ac95f7cbe10e0564' || lock?.schema_id !== 'urn:definitely-secure:contract:comic-manifest:1.0.0:comic-manifest') { - blockers.push('Comic Manifest lock lacks the exact contract and source identity.'); + blockers.push('Comic Manifest lock does not match the exact published Codex contract identity.'); } const assets = Array.isArray(lock?.assets) ? lock.assets : []; - if (assets.length !== expectedAssets.length) blockers.push('Comic Manifest lock must contain schema, bundle, and release-manifest assets.'); - for (const [filename, mediaType, prefix] of expectedAssets) { + if (assets.length !== expectedAssets.length) blockers.push('Comic Manifest lock must contain exactly the three published contract assets.'); + for (const [filename, mediaType, byteSize, digest] of expectedAssets) { const matches = assets.filter(asset => asset?.filename === filename), asset = matches[0]; - if (matches.length !== 1 || asset?.media_type !== mediaType || asset?.artifact_uri !== prefix + filename || - !Number.isSafeInteger(asset?.byte_size) || asset.byte_size < 1 || - !/^sha256:[a-f0-9]{64}$/.test(asset?.sha256 ?? '')) { - blockers.push('Invalid or missing Comic Manifest asset identity: ' + filename + '.'); + if (matches.length !== 1 || asset?.artifact_uri !== codexPrefix + filename || asset?.media_type !== mediaType || + asset?.byte_size !== byteSize || asset?.sha256 !== digest) { + blockers.push('Comic Manifest asset tuple differs from the published Codex release: ' + filename + '.'); } } - if (lock?.status !== 'published' || publication?.status !== 'published' || publication?.immutable !== true || + if (lock?.status !== 'published' || publication?.status !== 'published' || publication?.draft !== false || + publication?.prerelease !== false || publication?.immutable !== true || publication?.tag_target !== codexCommit || !Number.isFinite(Date.parse(publication?.verified_at ?? '')) || - publication?.release_url !== 'https://github.com/DefinitelySecureStudio/codex/releases/tag/contract/comic-manifest/v1.0.0') { - blockers.push('Codex contract publication and independent immutable-download verification are not recorded.'); + publication?.release_url !== releaseUrl || JSON.stringify(publication?.transport) !== JSON.stringify(expectedTransport)) { + blockers.push('Codex immutable publication, exact tag target, and separate transport verification are not recorded.'); } const schema = assets.find(asset => asset?.filename === 'comic-manifest-v1.0.0.schema.json'); if (pin?.status !== 'released' || pin?.repository !== 'DefinitelySecureStudio/codex' || - pin?.version !== '1.0.0' || pin?.tag !== 'contract/comic-manifest/v1.0.0' || pin?.commit !== lock?.commit || + pin?.version !== '1.0.0' || pin?.tag !== codexTag || pin?.commit !== codexCommit || pin?.schema_id !== lock?.schema_id || pin?.artifact_uri !== schema?.artifact_uri || pin?.media_type !== schema?.media_type || pin?.byte_size !== schema?.byte_size || pin?.sha256 !== schema?.sha256) { - blockers.push('Runtime validator pin has not adopted the exact published contract tuple.'); + blockers.push('Runtime validator pin has not adopted the exact published schema tuple.'); } return { ready: blockers.length === 0, blockers }; } diff --git a/scripts/comic-manifest-approval.mjs b/scripts/comic-manifest-approval.mjs new file mode 100644 index 0000000..400c1f2 --- /dev/null +++ b/scripts/comic-manifest-approval.mjs @@ -0,0 +1,171 @@ +import { createHash } from 'node:crypto'; +import { lstat, readFile, readdir } from 'node:fs/promises'; +import { join } from 'node:path'; + +export const PLATFORM_RELEASE_APPROVAL_FORMAT = 'DefinitelySecureStudio.PlatformComicManifestReleaseApproval/v1'; +export const PLATFORM_RELEASE_OWNER = '@andrewperis'; + +const prefix = 'comic-manifest-v1.0.0'; +const manifestFilename = prefix + '.manifest.json'; +const assetFilenames = [ + prefix + '.source.tar.gz', + prefix + '.package.tgz', + prefix + '.package-lock.json', + prefix + '.sdk-contract-lock.json', + prefix + '.context-builder-contract-lock.json', + prefix + '.contract-lock.json', + prefix + '.api-v1.json' +]; +const allFilenames = [...assetFilenames, manifestFilename]; +const tag = 'comic-manifest/v1.0.0'; +const codexCommit = '12e437e30328a3bb9cd2d15e6307a70b4b7e0e2a'; +const downloadPrefix = 'https://github.com/DefinitelySecureStudio/platform/releases/download/' + encodeURIComponent(tag) + '/'; +const hash = bytes => 'sha256:' + createHash('sha256').update(bytes).digest('hex'); +const sorted = values => [...values].sort(); +const exactKeys = (value, expected) => value && typeof value === 'object' && !Array.isArray(value) && + JSON.stringify(sorted(Object.keys(value))) === JSON.stringify(sorted(expected)); + +function parseJson(bytes, label) { + try { return JSON.parse(bytes); } + catch { throw Error(label + ' is not valid JSON.'); } +} + +async function readRegularFile(directory, filename) { + const path = join(directory, filename); + let info; + try { info = await lstat(path); } + catch { throw Error('Missing release artifact: ' + filename + '.'); } + if (!info.isFile() || info.isSymbolicLink()) throw Error('Release artifact must be a regular file: ' + filename + '.'); + return readFile(path); +} + +async function inspectApprovalCandidate(directory) { + let rootInfo; + try { rootInfo = await lstat(directory); } + catch { throw Error('Approval artifact directory is missing.'); } + if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink()) throw Error('Approval artifact path must be a regular directory.'); + const names = sorted(await readdir(directory)); + if (JSON.stringify(names) !== JSON.stringify(sorted(allFilenames))) { + throw Error('Approval artifact directory must contain exactly the eight release files.'); + } + + const manifestBytes = await readRegularFile(directory, manifestFilename); + const manifest = parseJson(manifestBytes, 'Release manifest'); + if (manifest?.repository !== 'DefinitelySecureStudio/platform' || manifest?.component !== 'comic-manifest' || + manifest?.version !== '1.0.0' || manifest?.package_version !== '1.2.0' || manifest?.tag !== tag || + !/^[a-f0-9]{40}$/.test(manifest?.commit ?? '') || !/^[a-f0-9]{40}$/.test(manifest?.tree ?? '') || + !exactKeys(manifest?.build_provenance, ['mode', 'owner_approval_status_at_build', 'publication_status_at_build']) || + manifest.build_provenance.mode !== 'release-approval-candidate' || + manifest.build_provenance.owner_approval_status_at_build !== 'not-approved' || + manifest.build_provenance.publication_status_at_build !== 'not-published' || + !exactKeys(manifest?.codex_adoption, ['ready', 'blockers']) || manifest.codex_adoption.ready !== true || + !Array.isArray(manifest.codex_adoption.blockers) || manifest.codex_adoption.blockers.length !== 0 || + manifest?.codex_contract?.status !== 'published' || manifest.codex_contract.repository !== 'DefinitelySecureStudio/codex' || + manifest.codex_contract.contract !== 'comic-manifest' || manifest.codex_contract.version !== '1.0.0' || + manifest.codex_contract.tag !== 'contract/comic-manifest/v1.0.0' || manifest.codex_contract.commit !== codexCommit || + manifest.codex_contract.publication?.immutable !== true || manifest.codex_contract.publication?.tag_target !== codexCommit) { + throw Error('Release manifest does not identify the expected approval-candidate build and source.'); + } + if (!Array.isArray(manifest.assets) || manifest.assets.length !== assetFilenames.length || + JSON.stringify(manifest.assets.map(asset => asset?.filename)) !== JSON.stringify(assetFilenames)) { + throw Error('Release manifest must list the exact seven Platform assets in the fixed order.'); + } + + const artifacts = []; + for (let index = 0; index < assetFilenames.length; index++) { + const filename = assetFilenames[index]; + const asset = manifest.assets[index]; + const mediaType = filename.endsWith('.gz') || filename.endsWith('.tgz') ? 'application/gzip' : 'application/json'; + if (!exactKeys(asset, ['filename', 'media_type', 'byte_size', 'sha256', 'artifact_uri']) || + asset.media_type !== mediaType || asset.artifact_uri !== downloadPrefix + filename || + !Number.isSafeInteger(asset.byte_size) || asset.byte_size < 1 || !/^sha256:[a-f0-9]{64}$/.test(asset.sha256 ?? '')) { + throw Error('Release manifest contains an invalid asset tuple: ' + filename + '.'); + } + const bytes = await readRegularFile(directory, filename); + if (bytes.length !== asset.byte_size || hash(bytes) !== asset.sha256) { + throw Error('Release artifact differs from its manifest tuple: ' + filename + '.'); + } + artifacts.push({ filename, byte_size: bytes.length, sha256: hash(bytes) }); + } + + artifacts.push({ filename: manifestFilename, byte_size: manifestBytes.length, sha256: hash(manifestBytes) }); + return { + manifest, + artifacts, + release: { + repository: manifest.repository, + component: manifest.component, + version: manifest.version, + package_version: manifest.package_version, + tag: manifest.tag, + source_commit: manifest.commit, + source_tree: manifest.tree + } + }; +} + +/** Build a detached, unapproved packet from the exact eight-file set. This function reads only. */ +export async function createComicManifestApprovalPacket(directory) { + const inspected = await inspectApprovalCandidate(directory); + return { + format: PLATFORM_RELEASE_APPROVAL_FORMAT, + status: 'awaiting-owner-decision', + release: inspected.release, + artifacts: inspected.artifacts, + owner_approval: null + }; +} + +/** Verify a detached owner decision against the exact release directory. This function reads only. */ +export async function verifyComicManifestApproval(directory, approvalPacketPath) { + const packetInfo = await lstat(approvalPacketPath).catch(() => null); + if (!packetInfo?.isFile() || packetInfo.isSymbolicLink()) throw Error('Detached approval packet must be a regular file.'); + const packet = parseJson(await readFile(approvalPacketPath), 'Detached approval packet'); + const inspected = await inspectApprovalCandidate(directory); + + if (!exactKeys(packet, ['format', 'status', 'release', 'artifacts', 'owner_approval']) || + packet.format !== PLATFORM_RELEASE_APPROVAL_FORMAT) { + throw Error('Detached approval packet has an unsupported format.'); + } + if (packet.status !== 'approved' || !packet.owner_approval) { + throw Error('Detached approval packet has no explicit owner decision.'); + } + if ( + !exactKeys(packet.release, ['repository', 'component', 'version', 'package_version', 'tag', 'source_commit', 'source_tree']) || + Object.keys(inspected.release).some(key => packet.release[key] !== inspected.release[key]) || !Array.isArray(packet.artifacts) || + packet.artifacts.length !== inspected.artifacts.length) { + throw Error('Detached approval does not bind the exact source commit, tree, tag, and eight release artifacts.'); + } + for (let index = 0; index < inspected.artifacts.length; index++) { + const artifact = packet.artifacts[index]; + const expected = inspected.artifacts[index]; + if (!exactKeys(artifact, ['filename', 'byte_size', 'sha256']) || artifact.filename !== expected.filename || + artifact.byte_size !== expected.byte_size || artifact.sha256 !== expected.sha256) { + throw Error('Detached approval artifact tuple mismatch: ' + expected.filename + '.'); + } + } + + const approval = packet.owner_approval; + if (!exactKeys(approval, ['decision', 'owner', 'approved_at', 'decision_reference']) || + approval.decision !== 'approve' || approval.owner !== PLATFORM_RELEASE_OWNER || + typeof approval.approved_at !== 'string' || + !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(approval.approved_at) || + !Number.isFinite(Date.parse(approval.approved_at)) || typeof approval.decision_reference !== 'string' || + !approval.decision_reference.trim()) { + throw Error('Detached approval must contain an explicit owner decision and reference.'); + } + + return { + approval_record_matches: true, + owner_identity_authenticated: false, + external_owner_authentication_required: true, + repository: inspected.release.repository, + component: inspected.release.component, + version: inspected.release.version, + package_version: inspected.release.package_version, + tag: inspected.release.tag, + commit: inspected.release.source_commit, + tree: inspected.release.source_tree, + artifacts: inspected.artifacts.length + }; +} diff --git a/scripts/create-comic-manifest-approval-packet.mjs b/scripts/create-comic-manifest-approval-packet.mjs new file mode 100644 index 0000000..bcb0725 --- /dev/null +++ b/scripts/create-comic-manifest-approval-packet.mjs @@ -0,0 +1,8 @@ +import { pathToFileURL } from 'node:url'; +import { createComicManifestApprovalPacket } from './comic-manifest-approval.mjs'; + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + if (process.argv.length !== 3) throw Error('Supply the exact external Platform artifact directory.'); + const packet = await createComicManifestApprovalPacket(process.argv[2]); + console.log(JSON.stringify(packet, null, 2)); +} diff --git a/scripts/generate-comic-manifest-validator.mjs b/scripts/generate-comic-manifest-validator.mjs index 82a5376..56c94c8 100644 --- a/scripts/generate-comic-manifest-validator.mjs +++ b/scripts/generate-comic-manifest-validator.mjs @@ -25,6 +25,7 @@ const ajv = new Ajv2020({ strict: true, strictRequired: false, strictTypes: fals addFormats(ajv); ajv.addSchema(schema); const exports = Object.fromEntries(['production', 'result', 'release', 'approval'].map(name => [name, `${schema.$id}#/$defs/${name}`])); -const header = `// Generated from ${pin.repository}@${pin.commit}; unreleased reviewed candidate.\n// Source ${pin.sha256}; ${pin.byte_size} bytes.\n// Rebuild with scripts/generate-comic-manifest-validator.mjs; do not edit.\nimport { createRequire } from 'node:module';\nconst require = createRequire(import.meta.url);\n`; +const sourceStatus = pin.status === 'released' ? `published ${pin.tag}` : 'unreleased reviewed candidate'; +const header = `// Generated from ${pin.repository}@${pin.commit}; ${sourceStatus}.\n// Source ${pin.sha256}; ${pin.byte_size} bytes.\n// Rebuild from fresh verified Codex release bytes with scripts/generate-comic-manifest-validator.mjs; do not edit.\nimport { createRequire } from 'node:module';\nconst require = createRequire(import.meta.url);\n`; const output = process.argv[3] ?? new URL('../src/comic-manifest/generated/schema-v1.js', import.meta.url); await writeFile(output, header + standaloneCode(ajv, exports)); diff --git a/scripts/verify-comic-manifest-approval.mjs b/scripts/verify-comic-manifest-approval.mjs new file mode 100644 index 0000000..d329ff4 --- /dev/null +++ b/scripts/verify-comic-manifest-approval.mjs @@ -0,0 +1,8 @@ +import { pathToFileURL } from 'node:url'; +import { verifyComicManifestApproval } from './comic-manifest-approval.mjs'; + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + if (process.argv.length !== 4) throw Error('Supply the exact artifact directory and detached owner approval JSON.'); + const result = await verifyComicManifestApproval(process.argv[2], process.argv[3]); + console.log(JSON.stringify(result, null, 2)); +} diff --git a/scripts/verify-comic-manifest-downloads.mjs b/scripts/verify-comic-manifest-downloads.mjs index e4115ab..c476e55 100644 --- a/scripts/verify-comic-manifest-downloads.mjs +++ b/scripts/verify-comic-manifest-downloads.mjs @@ -1,5 +1,5 @@ import { createHash } from 'node:crypto'; -import { lstat, readFile } from 'node:fs/promises'; +import { lstat, readFile, readdir } from 'node:fs/promises'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; @@ -22,13 +22,18 @@ export async function verifyComicManifestDownloads(trustedManifestPath, director const manifest = JSON.parse(expectedBytes); const rootStat = await lstat(directory); if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw Error('Download directory must be a regular directory.'); + const expectedFiles = [...expected, prefix + '.manifest.json'].sort(); + const downloadedFiles = (await readdir(directory)).sort(); if (manifest.repository !== 'DefinitelySecureStudio/platform' || manifest.component !== 'comic-manifest' || manifest.version !== '1.0.0' || manifest.package_version !== '1.2.0' || manifest.tag !== tag || !/^[a-f0-9]{40}$/.test(manifest.commit ?? '') || !/^[a-f0-9]{40}$/.test(manifest.tree ?? '') || - manifest.candidate !== false || manifest.readiness?.ready !== true || + manifest.build_provenance?.mode !== 'release-approval-candidate' || + manifest.build_provenance?.owner_approval_status_at_build !== 'not-approved' || + manifest.build_provenance?.publication_status_at_build !== 'not-published' || manifest.codex_adoption?.ready !== true || + JSON.stringify(downloadedFiles) !== JSON.stringify(expectedFiles) || !Array.isArray(manifest.assets) || manifest.assets.length !== expected.length || new Set(manifest.assets.map(asset => asset?.filename)).size !== expected.length) { - throw Error('Not a release-ready trusted manifest.'); + throw Error('Not a complete trusted approval-candidate manifest and asset set.'); } async function bytes(filename) { diff --git a/src/comic-manifest/contract.js b/src/comic-manifest/contract.js index 6a456b9..62ed60f 100644 --- a/src/comic-manifest/contract.js +++ b/src/comic-manifest/contract.js @@ -1,12 +1,13 @@ -/** Reviewed candidate source pin; not a published release or permission grant. */ +/** Published Codex contract identity; this metadata grants no execution or publication authority. */ export const COMIC_MANIFEST_CONTRACT = Object.freeze({ repository: 'DefinitelySecureStudio/codex', version: '1.0.0', - status: 'unreleased-development', - commit: '8044643bf888067f5a6e0d212f843d72e8787f2b', + status: 'released', + tag: 'contract/comic-manifest/v1.0.0', + commit: '12e437e30328a3bb9cd2d15e6307a70b4b7e0e2a', schema_id: 'urn:definitely-secure:contract:comic-manifest:1.0.0:comic-manifest', source_path: 'schemas/json/comic-manifest/v1/comic-manifest.schema.json', - artifact_uri: 'https://github.com/DefinitelySecureStudio/codex/blob/8044643bf888067f5a6e0d212f843d72e8787f2b/schemas/json/comic-manifest/v1/comic-manifest.schema.json', + artifact_uri: 'https://github.com/DefinitelySecureStudio/codex/releases/download/contract%2Fcomic-manifest%2Fv1.0.0/comic-manifest-v1.0.0.schema.json', media_type: 'application/schema+json', byte_size: 30860, sha256: 'sha256:7bd3c5392ae0db0c5baba553c233142d3d4427471f5850123eef7b87d4c8eaa4' diff --git a/src/comic-manifest/generated/schema-v1.js b/src/comic-manifest/generated/schema-v1.js index f998811..697880f 100644 --- a/src/comic-manifest/generated/schema-v1.js +++ b/src/comic-manifest/generated/schema-v1.js @@ -1,6 +1,6 @@ -// Generated from DefinitelySecureStudio/codex@8044643bf888067f5a6e0d212f843d72e8787f2b; unreleased reviewed candidate. +// Generated from DefinitelySecureStudio/codex@12e437e30328a3bb9cd2d15e6307a70b4b7e0e2a; published contract/comic-manifest/v1.0.0. // Source sha256:7bd3c5392ae0db0c5baba553c233142d3d4427471f5850123eef7b87d4c8eaa4; 30860 bytes. -// Rebuild with scripts/generate-comic-manifest-validator.mjs; do not edit. +// Rebuild from fresh verified Codex release bytes with scripts/generate-comic-manifest-validator.mjs; do not edit. import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); "use strict"; diff --git a/tests/comic-manifest/generated.test.js b/tests/comic-manifest/generated.test.js index da163bd..09d5964 100644 --- a/tests/comic-manifest/generated.test.js +++ b/tests/comic-manifest/generated.test.js @@ -9,9 +9,17 @@ import { fileURLToPath } from 'node:url'; import { COMIC_MANIFEST_CONTRACT as pin } from '../../src/comic-manifest/index.js'; const root=fileURLToPath(new URL('../../',import.meta.url)); const hash=b=>'sha256:'+createHash('sha256').update(b).digest('hex'); -test('reviewed Codex artifacts match exact merged source pins',async()=>{ +test('historical fixtures remain pinned while runtime uses identical published Codex schema bytes',async()=>{ const lock=JSON.parse(await readFile(join(root,'tests/fixtures/comic-manifest-validation-lock.json'))); - assert.equal(lock.commit,pin.commit);assert.equal(lock.status,'unreleased-development'); + assert.equal(lock.repository,'DefinitelySecureStudio/codex'); + assert.equal(lock.commit,'8044643bf888067f5a6e0d212f843d72e8787f2b'); + assert.equal(lock.status,'unreleased-development'); + assert.equal(pin.repository,'DefinitelySecureStudio/codex'); + assert.equal(pin.commit,'12e437e30328a3bb9cd2d15e6307a70b4b7e0e2a'); + assert.equal(pin.status,'released'); + assert.equal(pin.tag,'contract/comic-manifest/v1.0.0'); + const fixtureSchema=JSON.parse(await readFile(join(root,'tests/fixtures/comic-manifest-v1.schema.json'),'utf8')); + assert.equal(fixtureSchema.$id,pin.schema_id); for(const a of lock.artifacts){const bytes=await readFile(join(root,a.path));assert.equal(bytes.length,a.byte_size);assert.equal(hash(bytes),a.sha256);assert.equal(a.artifact_uri,`https://github.com/${lock.repository}/blob/${lock.commit}/${a.source_path}`);} assert.equal(lock.artifacts[0].sha256,pin.sha256);assert.equal(lock.artifacts[0].byte_size,pin.byte_size); }); diff --git a/tests/comic-manifest/release.test.js b/tests/comic-manifest/release.test.js index 390adca..e920a47 100644 --- a/tests/comic-manifest/release.test.js +++ b/tests/comic-manifest/release.test.js @@ -9,6 +9,9 @@ import { fileURLToPath } from 'node:url'; import * as comicApi from '../../src/comic-manifest/index.js'; import { buildComicManifestRelease, resolveExternalOutput, verifySourceIdentity } from '../../scripts/build-comic-manifest-release.mjs'; import { checkComicManifestRelease, comicManifestReleaseReadiness } from '../../scripts/check-comic-manifest-release.mjs'; +import { + createComicManifestApprovalPacket, PLATFORM_RELEASE_APPROVAL_FORMAT, verifyComicManifestApproval +} from '../../scripts/comic-manifest-approval.mjs'; import { verifyComicManifestDownloads } from '../../scripts/verify-comic-manifest-downloads.mjs'; const root = resolve(fileURLToPath(new URL('../..', import.meta.url))); @@ -34,36 +37,40 @@ test('Comic Manifest inventory matches the public package API and additive packa assert.equal(packageJson.bin['studio-comic'], './src/comic-manifest/cli.js'); }); -test('candidate lock pins exact Codex bytes while readiness remains blocked until publication and adoption', async () => { - assert.equal(lock.commit, '028d5638e20d9283a2955aacbd38e1bfc6dca259'); - assert.equal(lock.status, 'candidate-unpublished'); - assert.equal(lock.assets.length, 3); - const report = await comicManifestReleaseReadiness(); - assert.equal(report.ready, false); - assert.ok(report.blockers.some(blocker => /publication/.test(blocker))); - assert.ok(report.blockers.some(blocker => /Runtime validator pin/.test(blocker))); +test('published Codex lock adopts exact immutable tuples while Platform remains unpublished', async () => { + assert.equal(lock.commit, '12e437e30328a3bb9cd2d15e6307a70b4b7e0e2a'); + assert.equal(lock.status, 'published'); + assert.equal(lock.tag, 'contract/comic-manifest/v1.0.0'); + assert.equal(lock.publication.status, 'published'); + assert.equal(lock.publication.draft, false); + assert.equal(lock.publication.prerelease, false); + assert.equal(lock.publication.immutable, true); + assert.equal(lock.publication.tag_target, lock.commit); + assert.deepEqual(lock.assets.map(({ filename, media_type, byte_size, sha256 }) => ({ filename, media_type, byte_size, sha256 })), [ + { filename: 'comic-manifest-v1.0.0.schema.json', media_type: 'application/schema+json', byte_size: 30860, sha256: 'sha256:7bd3c5392ae0db0c5baba553c233142d3d4427471f5850123eef7b87d4c8eaa4' }, + { filename: 'comic-manifest-v1.0.0.bundle.json', media_type: 'application/json', byte_size: 865650, sha256: 'sha256:bb131a7dbb96692172b4e44b56a272d041309e53b136433cc71a24c8d1b934de' }, + { filename: 'comic-manifest-v1.0.0.manifest.json', media_type: 'application/json', byte_size: 1152, sha256: 'sha256:56bba6d990c429384e16d8aa49af99b3a25ca8733efa96b5681be5932dcba878' } + ]); + assert.deepEqual(lock.publication.transport.map(({ filename, github_release_asset_content_type, public_download_http_status, public_download_content_type }) => ({ filename, github_release_asset_content_type, public_download_http_status, public_download_content_type })), [ + { filename: 'comic-manifest-v1.0.0.schema.json', github_release_asset_content_type: 'application/json', public_download_http_status: 200, public_download_content_type: 'application/octet-stream' }, + { filename: 'comic-manifest-v1.0.0.bundle.json', github_release_asset_content_type: 'application/json', public_download_http_status: 200, public_download_content_type: 'application/octet-stream' }, + { filename: 'comic-manifest-v1.0.0.manifest.json', github_release_asset_content_type: 'application/json', public_download_http_status: 200, public_download_content_type: 'application/octet-stream' } + ]); + assert.equal(apiInventory.upstream_contract_status, 'published'); + assert.equal(apiInventory.publication_status, 'candidate-unpublished'); + assert.equal(packageJson.private, true); + assert.deepEqual(await comicManifestReleaseReadiness(), { ready: true, blockers: [] }); - const published = structuredClone(lock); - published.status = 'published'; - published.publication = { - status: 'published', immutable: true, verified_at: '2026-10-07T20:00:00Z', - release_url: 'https://github.com/DefinitelySecureStudio/codex/releases/tag/contract/comic-manifest/v1.0.0' - }; - const schema = published.assets.find(asset => asset.filename.endsWith('.schema.json')); - const pin = { - status: 'released', repository: published.repository, version: published.version, - tag: published.tag, commit: published.commit, schema_id: published.schema_id, - artifact_uri: schema.artifact_uri, media_type: schema.media_type, - byte_size: schema.byte_size, sha256: schema.sha256 - }; - assert.deepEqual(checkComicManifestRelease({ pkg: packageJson, lock: published, pin }), { ready: true, blockers: [] }); - published.publication.status = 'pending'; - const contradictoryPublication = checkComicManifestRelease({ pkg: packageJson, lock: published, pin }); - assert.equal(contradictoryPublication.ready, false, 'top-level published status cannot override pending publication evidence'); - assert.ok(contradictoryPublication.blockers.some(blocker => /publication/.test(blocker))); - published.publication.status = 'published'; - published.assets[1].sha256 = 'not-a-digest'; - assert.equal(checkComicManifestRelease({ pkg: packageJson, lock: published, pin }).ready, false); + for (const mutate of [ + candidate => { candidate.commit = '028d5638e20d9283a2955aacbd38e1bfc6dca259'; }, + candidate => { candidate.publication.immutable = false; }, + candidate => { candidate.assets[0].sha256 = 'sha256:' + '0'.repeat(64); }, + candidate => { candidate.publication.transport[0].public_download_content_type = 'application/schema+json'; } + ]) { + const candidate = structuredClone(lock); + mutate(candidate); + assert.equal(checkComicManifestRelease({ pkg: packageJson, lock: candidate }).ready, false); + } }); test('release output path resolution rejects symlinked parents into the checkout and returns physical external paths', async t => { @@ -84,21 +91,21 @@ test('Platform artifact provenance accepts a source commit only for the exact lo const sourceCommit = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim(); const tree = execFileSync('git', ['rev-parse', 'HEAD^{tree}'], { cwd: root, encoding: 'utf8' }).trim(); const manifest = await buildComicManifestRelease(join(temp, 'matching'), { - candidate: true, sourceIdentity: { commit: sourceCommit, tree } + sourceIdentity: { commit: sourceCommit, tree } }); assert.equal(manifest.commit, sourceCommit); assert.equal(manifest.tree, tree); await assert.rejects(buildComicManifestRelease(join(temp, 'missing'), { - candidate: true, sourceIdentity: { commit: 'a'.repeat(40), tree } + sourceIdentity: { commit: 'a'.repeat(40), tree } }), /not present as a verified local Git commit object/); await assert.rejects(buildComicManifestRelease(join(temp, 'non-commit-object'), { - candidate: true, sourceIdentity: { commit: tree, tree } + sourceIdentity: { commit: tree, tree } }), /not present as a verified local Git commit object/); await assert.rejects(buildComicManifestRelease(join(temp, 'mismatch'), { - candidate: true, sourceIdentity: { commit: sourceCommit, tree: 'b'.repeat(40) } + sourceIdentity: { commit: sourceCommit, tree: 'b'.repeat(40) } }), /exact matching local Git tree/); }); @@ -125,15 +132,19 @@ test('Platform source provenance ignores Git replacement refs in an isolated rep 'a replacement ref cannot make a wrong commit appear to contain the checkout tree'); }); -test('candidate artifacts build reproducibly with an exact manifest and declared media types', async t => { +test('approval-candidate artifacts build reproducibly from a clean exact source identity', async t => { const temp = await mkdtemp(join(tmpdir(), 'comic-manifest-release-')); t.after(() => rm(temp, { recursive: true, force: true })); const outputA = join(temp, 'a'), outputB = join(temp, 'b'); - await assert.rejects(buildComicManifestRelease(outputA), /not ready/); - const manifestA = await buildComicManifestRelease(outputA, { candidate: true }); - const manifestB = await buildComicManifestRelease(outputB, { candidate: true }); - assert.equal(manifestA.candidate, true); - assert.deepEqual(manifestA.readiness, { ready: false, blockers: manifestB.readiness.blockers }); + const sourceCommit = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim(); + const sourceTree = execFileSync('git', ['rev-parse', 'HEAD^{tree}'], { cwd: root, encoding: 'utf8' }).trim(); + const sourceIdentity = { commit: sourceCommit, tree: sourceTree }; + const manifestA = await buildComicManifestRelease(outputA, { sourceIdentity }); + const manifestB = await buildComicManifestRelease(outputB, { sourceIdentity }); + assert.deepEqual(manifestA.build_provenance, { + mode: 'release-approval-candidate', owner_approval_status_at_build: 'not-approved', publication_status_at_build: 'not-published' + }); + assert.deepEqual(manifestA.codex_adoption, { ready: true, blockers: manifestB.codex_adoption.blockers }); assert.equal(manifestA.component, 'comic-manifest'); assert.equal(manifestA.version, '1.0.0'); assert.equal(manifestA.package_version, '1.2.0'); @@ -216,23 +227,163 @@ test('candidate artifacts build reproducibly with an exact manifest and declared assert.equal(JSON.parse(await readFile(join(sourceRoot, 'release/comic-manifest-api-v1.json'), 'utf8')).tag, 'comic-manifest/v1.0.0'); }); +test('detached owner approval binds the exact eight-file set and verification never mutates it', async t => { + const temp = await mkdtemp(join(tmpdir(), 'comic-manifest-approval-')); + t.after(() => rm(temp, { recursive: true, force: true })); + const artifacts = join(temp, 'artifacts'), packetPath = join(temp, 'approval.json'); + await buildComicManifestRelease(artifacts); + + const snapshot = async directory => { + const names = (await readdir(directory)).sort(); + return Promise.all(names.map(async name => [name, await readFile(join(directory, name))])); + }; + const beforeGeneration = await snapshot(artifacts); + const packet = await createComicManifestApprovalPacket(artifacts); + assert.equal(packet.format, PLATFORM_RELEASE_APPROVAL_FORMAT); + assert.equal(packet.status, 'awaiting-owner-decision'); + assert.equal(packet.owner_approval, null); + assert.equal(packet.release.tag, 'comic-manifest/v1.0.0'); + assert.match(packet.release.source_commit, /^[a-f0-9]{40}$/); + assert.match(packet.release.source_tree, /^[a-f0-9]{40}$/); + assert.equal(packet.artifacts.length, 8); + assert.equal(packet.artifacts.at(-1).filename, 'comic-manifest-v1.0.0.manifest.json'); + assert.deepEqual(await snapshot(artifacts), beforeGeneration, 'packet generation is read-only'); + const packetFromCli = JSON.parse(execFileSync(process.execPath, [ + join(root, 'scripts/create-comic-manifest-approval-packet.mjs'), artifacts + ], { encoding: 'utf8' })); + assert.deepEqual(packetFromCli, packet); + await writeFile(packetPath, JSON.stringify(packet, null, 2) + '\n'); + const beforeMissingApproval = await snapshot(artifacts); + await assert.rejects(verifyComicManifestApproval(artifacts, packetPath), /explicit owner decision/); + assert.deepEqual(await snapshot(artifacts), beforeMissingApproval, 'rejected verification does not mutate assets'); + + const approved = structuredClone(packet); + approved.status = 'approved'; + approved.owner_approval = { + decision: 'approve', owner: '@andrewperis', approved_at: '2026-10-10T00:00:00Z', + decision_reference: 'synthetic-test-approval' + }; + await writeFile(packetPath, JSON.stringify(approved, null, 2) + '\n'); + const beforeVerification = await snapshot(artifacts), approvalBytes = await readFile(packetPath); + const report = await verifyComicManifestApproval(artifacts, packetPath); + assert.deepEqual(report, { + approval_record_matches: true, owner_identity_authenticated: false, external_owner_authentication_required: true, + repository: 'DefinitelySecureStudio/platform', component: 'comic-manifest', + version: '1.0.0', package_version: '1.2.0', tag: 'comic-manifest/v1.0.0', + commit: packet.release.source_commit, tree: packet.release.source_tree, artifacts: 8 + }); + assert.deepEqual(await snapshot(artifacts), beforeVerification, 'successful verification does not mutate assets'); + assert.deepEqual(await readFile(packetPath), approvalBytes, 'successful verification does not rewrite the approval'); + assert.deepEqual(JSON.parse(execFileSync(process.execPath, [ + join(root, 'scripts/verify-comic-manifest-approval.mjs'), artifacts, packetPath + ], { encoding: 'utf8' })), report); + + const linkedDirectory = join(temp, 'linked-artifacts'); + await symlink(artifacts, linkedDirectory, 'dir'); + const directoryBefore = await snapshot(artifacts); + await assert.rejects(createComicManifestApprovalPacket(linkedDirectory), /regular directory/); + await assert.rejects(verifyComicManifestApproval(linkedDirectory, packetPath), /regular directory/); + assert.equal((await lstat(linkedDirectory)).isSymbolicLink(), true); + assert.deepEqual(await snapshot(artifacts), directoryBefore, 'directory symlink rejection is read-only'); + + const linkedPacket = join(temp, 'approval-symlink.json'); + await symlink(packetPath, linkedPacket); + const packetBeforeSymlinkCheck = await readFile(packetPath), filesBeforePacketSymlinkCheck = await snapshot(artifacts); + await assert.rejects(verifyComicManifestApproval(artifacts, linkedPacket), /regular file/); + assert.equal((await lstat(linkedPacket)).isSymbolicLink(), true); + assert.deepEqual(await readFile(packetPath), packetBeforeSymlinkCheck); + assert.deepEqual(await snapshot(artifacts), filesBeforePacketSymlinkCheck, 'packet symlink rejection is read-only'); + + const symlinkAsset = join(artifacts, packet.artifacts[0].filename); + const symlinkTarget = join(temp, 'external-approval-artifact'); + const originalSymlinkAsset = await readFile(symlinkAsset); + await writeFile(symlinkTarget, originalSymlinkAsset); + await rm(symlinkAsset); + await symlink(symlinkTarget, symlinkAsset); + const filesBeforeArtifactSymlinkCheck = await snapshot(artifacts), packetBeforeArtifactSymlinkCheck = await readFile(packetPath); + await assert.rejects(createComicManifestApprovalPacket(artifacts), /regular file/); + await assert.rejects(verifyComicManifestApproval(artifacts, packetPath), /regular file/); + assert.equal((await lstat(symlinkAsset)).isSymbolicLink(), true); + assert.deepEqual(await snapshot(artifacts), filesBeforeArtifactSymlinkCheck, 'artifact symlink rejection is read-only'); + assert.deepEqual(await readFile(packetPath), packetBeforeArtifactSymlinkCheck); + await rm(symlinkAsset); + await writeFile(symlinkAsset, originalSymlinkAsset); + + const changedPackets = [ + record => { record.release.source_commit = '0'.repeat(40); }, + record => { record.release.source_tree = '0'.repeat(40); }, + record => { record.release.tag = 'comic-manifest/v2.0.0'; }, + record => { record.artifacts[0].byte_size += 1; }, + record => { record.artifacts[0].sha256 = 'sha256:' + '0'.repeat(64); }, + record => { record.artifacts.pop(); }, + record => { record.artifacts.push(structuredClone(record.artifacts[0])); }, + record => { record.owner_approval = null; }, + record => { record.owner_approval.owner = '@someone-else'; }, + record => { record.owner_approval.decision = 'deny'; }, + record => { record.owner_approval.approved_at = 'invalid'; }, + record => { record.owner_approval.decision_reference = ''; } + ]; + for (const mutate of changedPackets) { + const changed = structuredClone(approved); + mutate(changed); + await writeFile(packetPath, JSON.stringify(changed, null, 2) + '\n'); + const filesBefore = await snapshot(artifacts), packetBefore = await readFile(packetPath); + await assert.rejects(verifyComicManifestApproval(artifacts, packetPath)); + assert.deepEqual(await snapshot(artifacts), filesBefore, 'rejected verification does not alter artifacts'); + assert.deepEqual(await readFile(packetPath), packetBefore, 'rejected verification does not alter the packet'); + } + + await writeFile(packetPath, JSON.stringify(approved, null, 2) + '\n'); + const changedAsset = join(artifacts, packet.artifacts[0].filename); + const originalAsset = await readFile(changedAsset); + await writeFile(changedAsset, Buffer.concat([originalAsset, Buffer.from('x')])); + const tamperedBefore = await snapshot(artifacts); + await assert.rejects(verifyComicManifestApproval(artifacts, packetPath), /differs from its manifest tuple/); + assert.deepEqual(await snapshot(artifacts), tamperedBefore, 'tamper rejection leaves the changed file untouched'); + await writeFile(changedAsset, originalAsset); + + const manifestFile = join(artifacts, 'comic-manifest-v1.0.0.manifest.json'); + const originalManifest = await readFile(manifestFile); + await writeFile(manifestFile, Buffer.concat([originalManifest, Buffer.from('\n')])); + const changedManifestBefore = await snapshot(artifacts); + await assert.rejects(verifyComicManifestApproval(artifacts, packetPath), /artifact tuple mismatch/); + assert.deepEqual(await snapshot(artifacts), changedManifestBefore, 'manifest mismatch is rejected without rewriting it'); + await writeFile(manifestFile, originalManifest); + + const extraFile = join(artifacts, 'unexpected.txt'); + await writeFile(extraFile, 'extra'); + const extraBefore = await snapshot(artifacts); + await assert.rejects(verifyComicManifestApproval(artifacts, packetPath), /exactly the eight release files/); + assert.deepEqual(await snapshot(artifacts), extraBefore, 'extra-file rejection is read-only'); + await rm(extraFile); + + const missingFile = join(artifacts, packet.artifacts[0].filename); + await rm(missingFile); + const missingBefore = await snapshot(artifacts); + await assert.rejects(verifyComicManifestApproval(artifacts, packetPath), /exactly the eight release files/); + assert.deepEqual(await snapshot(artifacts), missingBefore, 'missing-file rejection does not restore or rewrite files'); +}); + test('fresh-download verifier checks trusted manifest bytes, exact allowlist, regular files, size and SHA-256', async t => { const temp = await mkdtemp(join(tmpdir(), 'comic-manifest-download-')); t.after(() => rm(temp, { recursive: true, force: true })); const source = join(temp, 'source'), downloads = join(temp, 'downloads'); await mkdir(downloads); - const manifest = await buildComicManifestRelease(source, { candidate: true }); - const trusted = structuredClone(manifest); - trusted.candidate = false; trusted.readiness = { ready: true, blockers: [] }; - const trustedBytes = Buffer.from(JSON.stringify(trusted, null, 2) + '\n'); + const built = await buildComicManifestRelease(source); + const downloadedManifest = 'comic-manifest-v1.0.0.manifest.json'; + const trustedBytes = await readFile(join(source, downloadedManifest)); + const trusted = JSON.parse(trustedBytes); + assert.deepEqual(trusted, built); const trustedPath = join(temp, 'trusted-build-manifest.json'); await writeFile(trustedPath, trustedBytes); for (const asset of trusted.assets) await writeFile(join(downloads, asset.filename), await readFile(join(source, asset.filename))); - const downloadedManifest = 'comic-manifest-v1.0.0.manifest.json'; await writeFile(join(downloads, downloadedManifest), trustedBytes); assert.deepEqual(await verifyComicManifestDownloads(trustedPath, downloads), { verified: true, commit: trusted.commit, assets: trusted.assets.length + 1 }); + await writeFile(join(downloads, 'unexpected.txt'), 'extra'); + await assert.rejects(verifyComicManifestDownloads(trustedPath, downloads), /complete trusted approval-candidate/); + await rm(join(downloads, 'unexpected.txt')); const tampered = join(temp, 'tampered'); await mkdir(tampered); for (const asset of trusted.assets) await writeFile(join(tampered, asset.filename), await readFile(join(source, asset.filename))); diff --git a/tests/comic-manifest/validation.test.js b/tests/comic-manifest/validation.test.js index d41c349..83b387d 100644 --- a/tests/comic-manifest/validation.test.js +++ b/tests/comic-manifest/validation.test.js @@ -60,7 +60,7 @@ test('validation never equates local validity with build success, authorization const blocked=structuredClone(fixture.release);blocked.gates[0].disposition='fail';assert.equal(check(blocked).valid,true); // Structurally coherent historical approvals remain historical, irrespective of today's date. assert.equal(check(fixture.approvals[1]).valid,true); - assert.equal(COMIC_MANIFEST_CONTRACT.status,'unreleased-development'); + assert.equal(COMIC_MANIFEST_CONTRACT.status,'released'); }); test('dates are calendar-validated, preserve fractional ordering and accept UTC leap-second syntax',()=>{ const r=structuredClone(fixture.result);r.execution.started_at='2026-02-30T12:00:00Z';assert.equal(check(r).valid,false);