diff --git a/docs/automation-threat-model.md b/docs/automation-threat-model.md index 653693ee4..adc1b5d9b 100644 --- a/docs/automation-threat-model.md +++ b/docs/automation-threat-model.md @@ -128,13 +128,13 @@ The security objective is to prevent a lower-trust domain from converting its ou **Threat:** publisher creates a PR but loses the response, then broad cleanup closes/deletes another actor's resource. -**Controls proposed by PR #80:** unique cryptographic publication marker, exact branch/head/base match, numeric PR identity, unique recovery only, conditional branch cleanup. +**Controls implemented on protected `main`:** the non-executing publisher uses a cryptographic publication marker, requires exact proposal head and expected base identity, accepts only a positive numeric pull-request identity, and recovers a lost/malformed create response only when a fully paginated head-scoped search yields exactly one PR whose head, base, and marker all match the current publication. Cleanup re-runs that unique recovery before closing a PR and couples remote-branch cleanup to the exact proposal head. Closed, unmerged PR #80 is historical lineage only; it is not the current implementation owner or evidence authority. ### T-A08 Proposal branch race **Threat:** another actor creates same remote branch between inventory read and push, or advances it before cleanup. -**Controls proposed by PR #80:** expected-absence branch creation lease and exact-created-head deletion lease; no check-then-unguarded-push or unconditional delete. +**Controls implemented on protected `main`:** branch creation uses Git's explicit expected-absence lease (`--force-with-lease=:`), and remote cleanup uses an exact-created-head deletion lease (`--force-with-lease=:`). There is no check-then-unguarded push or unconditional branch deletion. Closed, unmerged PR #80 is retained only as historical provenance. ### T-A09 Queue race after generation @@ -233,4 +233,4 @@ These remain external evidence and must not be closed with documentation-only ch ## 9. Rationale and references -Primary-source rationale and APA 7 references for GitHub OIDC, SLSA source identity, NIST SSDF, Cloudflare capability/state semantics are maintained in `docs/doctoring/architecture-trust-boundaries.md`. Git conditional ref-update and publisher-specific rationale is maintained in the active PR #80 doctoring and should be integrated without duplicating mutable implementation claims after that PR lands. +Primary-source rationale and APA 7 references for GitHub OIDC, SLSA source identity, NIST SSDF, Cloudflare capability/state semantics are maintained in `docs/doctoring/architecture-trust-boundaries.md`. Git conditional ref-update and publisher-specific rationale for the protected implementation are maintained in `docs/doctoring/atomic-product-publisher-lease.md`. Closed, unmerged PR #80 is historical development lineage only and does not define current control status or implementation authority. diff --git a/test/ci-exact-head-contract.test.ts b/test/ci-exact-head-contract.test.ts index 7112b158d..82a00e724 100644 --- a/test/ci-exact-head-contract.test.ts +++ b/test/ci-exact-head-contract.test.ts @@ -6,8 +6,13 @@ const workflowPaths = [ ".github/workflows/reviewer-ci.yml", ] as const; +const requiredVerificationWorkflowPaths = [ + ...workflowPaths, + ".github/workflows/patch-validator-image.yml", +] as const; + /** Read one authoritative pull-request verification workflow as plain text. */ -function readWorkflow(path: (typeof workflowPaths)[number]): string { +function readWorkflow(path: string): string { return readFileSync(path, "utf8"); } @@ -107,4 +112,11 @@ describe("pull-request verification exact-head checkout contract", () => { "- name: install (hash-pinned dependencies)", ); }); + + it("does not suppress required exact-head evidence for documentation-only changes", () => { + for (const path of requiredVerificationWorkflowPaths) { + const workflow = readWorkflow(path); + expect(workflow).not.toContain("paths-ignore:"); + } + }); }); diff --git a/test/documentation-architecture-contract.test.ts b/test/documentation-architecture-contract.test.ts index 4a7222c3d..9baf45415 100644 --- a/test/documentation-architecture-contract.test.ts +++ b/test/documentation-architecture-contract.test.ts @@ -123,6 +123,25 @@ describe("authoritative Noema documentation graph", () => { expect(automationOwnership).not.toContain("stacked target branch does not trigger"); }); + it("keeps protected publisher race controls code-current in the threat model", () => { + const threatModel = document("docs/automation-threat-model.md"); + const publisher = readFileSync( + ".github/workflows/hourly-product-development.yml", + "utf8", + ); + + expect(publisher).toContain( + 'git push --force-with-lease="refs/heads/${branch}:" origin "HEAD:refs/heads/${branch}"', + ); + expect(publisher).toContain( + 'git push --force-with-lease="refs/heads/${branch}:${proposal_head}" origin ":refs/heads/${branch}"', + ); + expect(publisher).toContain("recover_created_pr_number"); + expect(publisher).toContain("publication_marker"); + expect(threatModel).toContain("**Controls implemented on protected `main`:**"); + expect(threatModel).not.toContain("not implemented on protected `main`"); + }); + it("keeps immutable workflow-source trust separate from revision-local canonical-byte hardening", () => { const architecture = document("ARCHITECTURE.md"); const traceability = document("docs/TRACEABILITY.md");