Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 37 additions & 23 deletions docs/npm-release-agent-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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": "<fixed independent reviewer agent>"
"agent": "/root/<fixed-independent-codewith-reviewer>"
},
"publisher": {
"type": "coding-agent",
Expand All @@ -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/<task-name>` (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
Expand All @@ -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=<task-evidence-directory>
reviewer_lineage=/root/<fixed-independent-codewith-reviewer>
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 \
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions scripts/issue-npm-release-agent-review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { spawnSync } from "node:child_process";
import {
NPM_RELEASE_AGENT_REVIEW_SCHEMA,
deriveNpmReleaseAgentReviewKeyId,
isNativeCodewithSubagentLineage,
issueSignedNpmReleaseAgentReviewReceipt,
parsePublisherAgentTrailer,
type NpmReleaseAgentReviewPayload,
Expand Down Expand Up @@ -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");
Expand Down
8 changes: 7 additions & 1 deletion scripts/verify-npm-release-agent-review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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");

Expand Down
25 changes: 24 additions & 1 deletion src/lib/npm-release-agent-review.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"],
Expand Down
23 changes: 22 additions & 1 deletion src/lib/npm-release-agent-review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading