From 09d8f9bec7e4b939b8db0686120bae5df8421be8 Mon Sep 17 00:00:00 2001 From: Andrei Hasna Date: Mon, 10 Aug 2026 16:11:44 +0300 Subject: [PATCH] fix(release): bind review receipts to Codewith lineage Agent: cossus --- docs/npm-release-agent-review.md | 60 +++++++++++++--------- scripts/issue-npm-release-agent-review.ts | 5 +- scripts/verify-npm-release-agent-review.ts | 8 ++- src/lib/npm-release-agent-review.test.ts | 25 ++++++++- src/lib/npm-release-agent-review.ts | 23 ++++++++- 5 files changed, 93 insertions(+), 28 deletions(-) diff --git a/docs/npm-release-agent-review.md b/docs/npm-release-agent-review.md index 250e724..15fea37 100644 --- a/docs/npm-release-agent-review.md +++ b/docs/npm-release-agent-review.md @@ -2,16 +2,19 @@ The `@hasna/todos` release workflow will not publish from a tag unless the `npm-release` environment contains a valid Ed25519-signed receipt using schema -`hasna.npm-release-agent-review.v1`. The matching private key belongs only to -the one fixed independent coding-agent reviewer. It never enters this -repository or the release workflow. +`hasna.npm-release-agent-review.v1`. The matching private key is the fixed +package release-review authority and is supplied only to the one independent +native Codewith sub-agent lineage fixed before reviewing that candidate. It +never enters this repository or the release workflow, and it is not attributed +to a standing Fable persona. This is not a human approval gate. The `npm-release` environment has no required reviewers or wait timer. Its non-secret variables provide the fixed reviewer identity, deterministic key id, Ed25519 public key, and signed receipt consumed by the package-owned verifier: -- `RELEASE_REVIEWER_AGENT`; +- `RELEASE_REVIEWER_AGENT`, set to the candidate's exact native Codewith + sub-agent lineage such as `/root/review_todos_release_01522`; - `RELEASE_REVIEW_KEY_ID`; - `RELEASE_REVIEW_PUBLIC_KEY`; and - `NPM_RELEASE_AGENT_REVIEW_RECEIPT`. @@ -56,7 +59,7 @@ The decoded, signed payload is also exact and has no additional fields: "registry": "https://registry.npmjs.org", "reviewer": { "type": "coding-agent", - "agent": "" + "agent": "/root/" }, "publisher": { "type": "coding-agent", @@ -70,12 +73,15 @@ The decoded, signed payload is also exact and has no additional fields: } ``` -The publisher identity must differ from the fixed reviewer and must match the -annotated tag's single final `Agent:` trailer. Missing, unsigned, malformed, -`NO_GO`, non-zero P0/P1, self-reviewed, stale, mismatched, tampered, wrong-key, -or replayed receipts fail before publication. A valid receipt from an earlier -commit or version is still a replay and fails the live commit, package, tag, and -workflow-revision comparisons. +The publisher identity must differ from the reviewer lineage and must match the +annotated tag's single final `Agent:` trailer. The reviewer must be the +canonical native Codewith lineage `/root/` (nested sub-agent +lineages remain slash-separated). Persona names such as `Anscombe`, Fable +reviewers, root coordinators, and arbitrary coding-agent labels are rejected. +Missing, unsigned, malformed, `NO_GO`, non-zero P0/P1, self-reviewed, stale, +mismatched, tampered, wrong-key, or replayed receipts fail before publication. +A valid receipt from an earlier commit or version is still a replay and fails +the live commit, package, tag, and workflow-revision comparisons. The existing tag/version, protected-main, immutable-version, typecheck, no-cloud, full-test, build, clean-tree, OIDC, provenance, and registry-readback @@ -85,28 +91,36 @@ receives the package. ## Exact issuance procedure for the later reviewer -Fix the reviewer and publisher identities before review begins. The reviewer -checks out the exact clean release commit already on protected main, records the -durable review-run id on the release task, and independently evaluates that -candidate. The same reviewer may perform at most two focused remediation checks -for concrete reachable in-scope P0/P1 defects. It issues `GO` only when both -blocking counts are zero. - -The fixed reviewer uses its registered vault item containing a canonical base64 -PKCS8 DER Ed25519 private key. Replace the four bracketed values below with the -actual vault item, exact release SHA, intended publisher agent, and task evidence -directory. No private-key value is printed or stored in the repository. +Fix the native Codewith reviewer lineage and publisher identity before review +begins. The reviewer checks out the exact clean release commit already on +protected main, records its canonical `/root/...` lineage and durable review-run +id on the release task, and independently evaluates that candidate. Fable may +support a decision or adjudication but cannot perform this review or issue this +receipt. The same Codewith reviewer may perform at most two focused remediation +checks for concrete reachable in-scope P0/P1 defects. It issues `GO` only when +both blocking counts are zero. + +The fixed reviewer lineage uses the package release-review vault item containing +a canonical base64 PKCS8 DER Ed25519 private key. That key is delivered only to +the same native Codewith sub-agent process after its review verdict is final. +Replace the five bracketed values below with the actual reviewer lineage, vault +item, exact release SHA, intended publisher agent, and task evidence directory. +No private-key value is printed or stored in the repository. ```bash evidence_dir= +reviewer_lineage=/root/ reviewer_agent_file="$(mktemp)" reviewer_key_id_file="$(mktemp)" reviewer_public_key_file="$(mktemp)" receipt_file="$(mktemp)" receipt_readback_file="$(mktemp)" +printf '%s\n' "${reviewer_lineage}" | gh variable set RELEASE_REVIEWER_AGENT \ + --repo hasna/todos --env npm-release gh variable get RELEASE_REVIEWER_AGENT \ --repo hasna/todos --env npm-release > "${reviewer_agent_file}" +test "$(sed -n '1p' "${reviewer_agent_file}")" = "${reviewer_lineage}" gh variable get RELEASE_REVIEW_KEY_ID \ --repo hasna/todos --env npm-release > "${reviewer_key_id_file}" gh variable get RELEASE_REVIEW_PUBLIC_KEY \ @@ -138,7 +152,7 @@ registry from the exact release commit rather than accepting those values as free-form arguments. The reviewer records the command result, receipt digest, readback result, exact -release SHA, fixed reviewer identity, review-run id, verdict, and blocker counts +release SHA, native Codewith lineage, review-run id, verdict, and blocker counts on the release task. It does not create or push the tag and does not publish. The later publisher creates one annotated tag on the receipt's exact commit. diff --git a/scripts/issue-npm-release-agent-review.ts b/scripts/issue-npm-release-agent-review.ts index 6769994..c0f62ce 100644 --- a/scripts/issue-npm-release-agent-review.ts +++ b/scripts/issue-npm-release-agent-review.ts @@ -4,6 +4,7 @@ import { spawnSync } from "node:child_process"; import { NPM_RELEASE_AGENT_REVIEW_SCHEMA, deriveNpmReleaseAgentReviewKeyId, + isNativeCodewithSubagentLineage, issueSignedNpmReleaseAgentReviewReceipt, parsePublisherAgentTrailer, type NpmReleaseAgentReviewPayload, @@ -48,8 +49,8 @@ function main(): void { if (options.verdict === "NO_GO" && options.openP0 === 0 && options.openP1 === 0) { fail("a NO_GO receipt must name at least one open P0 or P1 blocker"); } - if (parsePublisherAgentTrailer(`Agent: ${reviewerAgent}`).failures.length > 0) { - fail("RELEASE_REVIEWER_AGENT must be a registered agent identifier"); + if (!isNativeCodewithSubagentLineage(reviewerAgent)) { + fail("RELEASE_REVIEWER_AGENT must name the exact native Codewith sub-agent lineage fixed for this release candidate"); } if (parsePublisherAgentTrailer(`Agent: ${options.publisherAgent}`).failures.length > 0) { fail("--publisher-agent must be a registered agent identifier"); diff --git a/scripts/verify-npm-release-agent-review.ts b/scripts/verify-npm-release-agent-review.ts index 0ce5a7e..533a8f8 100644 --- a/scripts/verify-npm-release-agent-review.ts +++ b/scripts/verify-npm-release-agent-review.ts @@ -3,6 +3,7 @@ import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { spawnSync } from "node:child_process"; import { + isNativeCodewithSubagentLineage, parsePublisherAgentTrailer, validateNpmReleaseAgentReviewReceipt, type ExpectedNpmReleaseAgentReview, @@ -38,7 +39,12 @@ function main(): void { addContextFailure(failures, packageJson.name !== "@hasna/todos", "release-agent-review-package", "package.json must declare @hasna/todos"); addContextFailure(failures, !packageJson.version, "release-agent-review-version", "package.json must declare a release version"); addContextFailure(failures, packageJson.publishConfig?.registry !== "https://registry.npmjs.org", "release-agent-review-registry", "package.json must target the public npm registry"); - addContextFailure(failures, !reviewerAgentId, "release-agent-review-reviewer-config", "RELEASE_REVIEWER_AGENT must name the one fixed independent reviewer"); + addContextFailure( + failures, + !isNativeCodewithSubagentLineage(reviewerAgentId), + "release-agent-review-reviewer-config", + "RELEASE_REVIEWER_AGENT must name the exact native Codewith sub-agent lineage fixed for this release candidate", + ); addContextFailure(failures, !reviewerKeyId, "release-agent-review-key-id-config", "RELEASE_REVIEW_KEY_ID must identify the fixed reviewer public key"); addContextFailure(failures, !reviewerPublicKey, "release-agent-review-public-key", "RELEASE_REVIEW_PUBLIC_KEY must contain the fixed reviewer public key"); diff --git a/src/lib/npm-release-agent-review.test.ts b/src/lib/npm-release-agent-review.test.ts index 013a825..41825dc 100644 --- a/src/lib/npm-release-agent-review.test.ts +++ b/src/lib/npm-release-agent-review.test.ts @@ -26,7 +26,7 @@ const expected: ExpectedNpmReleaseAgentReview = { workflowPath: ".github/workflows/release.yml", workflowRevision: "2".repeat(40), registry: "https://registry.npmjs.org", - reviewerAgentId: "independent-reviewer", + reviewerAgentId: "/root/review_todos_release_01522", reviewerKeyId, reviewerPublicKey, publisherAgentId: "nausicaa", @@ -179,6 +179,29 @@ describe("npm release independent-agent review receipt", () => { })).failures.map((failure) => failure.check)).toContain("release-agent-review-publisher"); }); + test("rejects Fable-attributed and malformed reviewer identities even when configuration matches", () => { + for (const reviewerAgentId of [ + "Anscombe", + "independent-reviewer", + "/root", + "/root/Review_todos_release_01522", + "/root/review.todos.release", + "/other/review_todos_release_01522", + ]) { + const payload = { + ...acceptedPayload, + reviewer: { type: "coding-agent" as const, agent: reviewerAgentId }, + }; + const result = validateNpmReleaseAgentReviewReceipt( + JSON.stringify(signedReceipt(payload)), + { ...expected, reviewerAgentId }, + ); + expect(result.failures.map((failure) => failure.check)).toContain( + "release-agent-review-reviewer-runtime", + ); + } + }); + test("rejects every exact release binding mismatch", () => { const mismatches: Array<[unknown, string]> = [ [{ ...acceptedPayload, repository: "hasna/accounts" }, "release-agent-review-repository"], diff --git a/src/lib/npm-release-agent-review.ts b/src/lib/npm-release-agent-review.ts index a71ead0..9a3cff6 100644 --- a/src/lib/npm-release-agent-review.ts +++ b/src/lib/npm-release-agent-review.ts @@ -88,6 +88,15 @@ const PAYLOAD_KEYS = [ "openReachableInScopeBlockers", ]; +export function isNativeCodewithSubagentLineage(value: string): boolean { + if (value.length > 1024) return false; + const segments = value.split("/"); + return segments.length >= 3 + && segments[0] === "" + && segments[1] === "root" + && segments.slice(2).every((segment) => /^[a-z0-9][a-z0-9_]{0,127}$/.test(segment)); +} + export function validateNpmReleaseAgentReviewReceipt( rawReceipt: string | undefined, expected: ExpectedNpmReleaseAgentReview, @@ -230,11 +239,23 @@ export function validateNpmReleaseAgentReviewReceipt( "release-agent-review-agent-types", "reviewer and publisher must both be coding agents rather than human approvers", ); + addIf( + failures, + !isNativeCodewithSubagentLineage(expected.reviewerAgentId), + "release-agent-review-reviewer-config", + "RELEASE_REVIEWER_AGENT must name the exact native Codewith sub-agent lineage fixed for this release candidate", + ); + addIf( + failures, + !isNativeCodewithSubagentLineage(payload.reviewer.agent), + "release-agent-review-reviewer-runtime", + "reviewer.agent must name the native Codewith sub-agent lineage that performed the review", + ); addIf( failures, payload.reviewer.agent.trim().toLowerCase() === payload.publisher.agent.trim().toLowerCase(), "release-agent-review-independence", - "the fixed reviewer agent must differ from the publisher agent", + "the native Codewith reviewer lineage must differ from the publisher agent", ); addIf( failures,