diff --git a/docs/adr/0361-immutable-release-supply-chain-boundary.md b/docs/adr/0361-immutable-release-supply-chain-boundary.md new file mode 100644 index 000000000..11956fa5e --- /dev/null +++ b/docs/adr/0361-immutable-release-supply-chain-boundary.md @@ -0,0 +1,319 @@ +# ADR 0361 — Immutable release supply-chain boundary + +**Decision status:** Proposed +**Date:** 2026-09-03 +**Related:** ContextualWisdomLab/.github#1782, ContextualWisdomLab/.github#1791, LineageWeave #911 + +## Context + +LineageWeave does not currently publish an immutable GitHub release from a +product-local release workflow. The protected product line is versioned as a +Python package, but release evidence must identify one exact protected source +commit, the exact wheel and source distribution built from that commit, their +software bills of materials, and the immutable publication that buyers can +verify later. + +The organization already owns the reusable trust boundary for exact-artifact +SBOM attestation in `ContextualWisdomLab/.github`. LineageWeave must consume +that boundary rather than copy signing, OIDC, attestation-verification, or +provider policy into this repository. + +A prior integration attempt exposed the circular handoff defect tracked by +`ContextualWisdomLab/.github#1782`: the reusable required the post-upload GitHub +Actions artifact digest inside `source-identity.json`, even though that file was +part of the same upload whose bytes determine the digest. The canonical owner +repaired that model in merged `.github#1791`. Protected-owner commit +`bd866a21cca2a7e709f0b7a88150c310a9d98239` is the immutable consumer pin for +this decision. At that commit the inner source identity binds repository, +source SHA, predicate/schema and exact distribution/SBOM filenames and digests, +but not the outer GitHub artifact digest. The reusable independently verifies +the returned artifact ID/name/digest before downloading the sealed evidence and +again inside the credentialed signer boundary. This resolves the owner-side +cryptographic cycle without weakening transport-receipt verification. + +LineageWeave still cannot publish a commercial release from protected `main` +while the reachable synchronous PostgreSQL tooling path contains the LGPL-family +`psycopg2-binary` dependency. PR #911 owns its replacement and the reproducible +lockfile migration. Release work must consume that merged, license-clean +protected result; it must not waive or suppress the inventory. + +GitHub's immutable-release setting is a separate repository/organization +control from artifact attestation. GitHub documents that a published immutable +release locks the associated tag and assets and automatically creates a release +attestation. The repository API returns both `enabled` and `enforced_by_owner` +for immutable-release admission. For this high-assurance commercial release +path, repository-local enablement is not enough: the final publication boundary +requires owner-enforced immutability so a repository administrator cannot turn +the control off between admission and publish. The trusted release writer uses +only the administrative read capability needed to prove that state. + +The API does not expose a consumer-supplied compare-and-publish precondition +that atomically couples an earlier settings/tag read to Release publication. +The release design therefore cannot treat two REST reads as atomic. It closes +the controllable races with mandatory configuration locks: owner-enforced +release immutability, a reviewed protected ruleset for the candidate tag +namespace, and one exclusively serialized trusted release writer for all +candidate tag/ref and Release mutations. If those controls cannot be proved, +publication remains RED. + +GitHub's Git data model distinguishes an annotated tag reference from the +commit that the tag names. `refs/tags/` points to a Git tag object; +that tag object separately names its target object and target type. The final +publish decision must therefore re-read the exact tag object, peel it to a +`commit`, and compare that target with the exact protected source SHA while the +protected/serialized candidate namespace prevents an admitted writer from +retargeting the ref. + +The immutable protections begin at publication, not at draft creation. GitHub +recommends creating a draft, attaching all assets, and only then publishing it. +That creates a pre-publication interval in which the candidate tag, draft and +assets are already real identities. The final admission must revalidate the +exact Release ID, `draft: true`, `tag_name`, `prerelease: false`, and the full +asset name/digest set. Abort cleanup must likewise bind deletion to the exact +candidate under the same writer/ruleset serialization; otherwise cleanup is +quarantined rather than risking deletion of a retargeted ref. + +## Decision + +1. LineageWeave owns the product-local release caller: release readiness, + package build, exact artifact preparation, release notes, tag creation, + immutable GitHub Release publication, reproducibility checks, and rollback + instructions. +2. `ContextualWisdomLab/.github` owns the reusable credentialed SBOM + attestation and verification boundary. The LineageWeave caller must invoke + `ContextualWisdomLab/.github/.github/workflows/exact-artifact-sbom-attestation.yml@bd866a21cca2a7e709f0b7a88150c310a9d98239`. + It must not vendor or fork the trusted verifier, and a later mutable + `.github/main` SHA does not replace this reviewed pin. +3. Release initiation is allowed only from the exact protected LineageWeave + `main` commit being released. A version is valid only when package metadata, + changelog/release notes, tag, distribution metadata, source identity and + GitHub Release all name the same canonical `MAJOR.MINOR.PATCH` version and + source SHA. +4. The unprivileged build stage receives `contents: read` only. It repeats the + repository's complete release-relevant test, documentation, lockfile, + security-contract and package checks before building a wheel and source + distribution. Pull-request-controlled source never receives OIDC, + attestation, release or package-publication credentials. +5. The build stage prepares one sealed evidence handoff containing exactly the + wheel, source distribution, one CycloneDX 1.7 SBOM bound to each exact + distribution, `source-identity.json`, and `checksums.sha256`. The inner + source identity binds repository, exact source SHA, predicate/schema, + distribution filenames and distribution/SBOM SHA-256 values. It deliberately + excludes the post-upload GitHub Actions artifact digest; that value exists + only in the outer transport receipt returned after upload. +6. GitHub's uploaded artifact ID/name/digest is an outer immutable transport + receipt. The caller passes that returned receipt and the exact inner + identities to the pinned canonical reusable. The reusable independently + revalidates same-run artifact ID/name/digest and the inert handoff before any + OIDC token or attestation permission becomes available, and repeats the + outer-receipt verification inside the credentialed signer boundary. +7. Before any release tag or draft is created, enter a single **trusted release + writer** serialized for the candidate version/tag namespace. Call + `GET /repos/{owner}/{repo}/immutable-releases` and require both + `enabled: true` and `enforced_by_owner: true`. Require a reviewed tag + ruleset/protection receipt that prevents actors outside the trusted release + writer from retargeting or deleting the candidate release-tag namespace. + Any missing administrative-read capability, non-confirming settings result, + absent protection receipt, or inability to prove writer serialization must + **fail closed** before tag creation. These privileged controls are not + available to pull-request or unprivileged build jobs. +8. After that admission, create an annotated tag object whose target type is + `commit` and whose target SHA is the exact protected source SHA, then create + `refs/tags/` pointing to that object. Record the pre-create absence + proof, exact tag-object SHA/ref and active protection receipt. Create a draft + Release, retain its **exact Release ID**, require the creation receipt to be + `draft: true` and `prerelease: false` with the exact admitted `tag_name`, and + attach the complete verified asset set while recording every expected asset + name and digest. **Immediately before publish**, while the same trusted + release writer still owns the serialized namespace, recheck + `GET /repos/{owner}/{repo}/immutable-releases` and again require + `enabled: true` and `enforced_by_owner: true`; re-read the exact Release ID + and require `draft: true`, exact `tag_name`, `prerelease: false`, and exact + asset names/digests; then re-resolve the annotated tag, require the ref still + names the recorded **tag object**, fetch it, **peel** its target, require + **type `commit`**, and compare the peeled target with the **exact protected + source SHA**. The owner-enforced setting, protected tag namespace and + exclusively serialized trusted release writer are mandatory configuration + locks for the interval that GitHub does not expose as one atomic API call. + Any state drift, missing lock, mismatch, lookup failure or malformed response + must **fail closed** with the draft unpublished. Only after all of those + exact predicates remain true may the trusted writer publish the draft. +9. A failure after candidate tag/draft creation but before publication is a + **pre-publication abort**. Same-version retry is admissible only after the + trusted release writer proves from recorded receipts that the exact Release + ID is still `draft: true`, unpublished, `prerelease: false`, names the exact + candidate `tag_name`, and retains the exact candidate asset/digest set; the + candidate ref still points to the recorded tag object; that tag object still + peels to the admitted protected source commit; and no published release + resolves for the tag. Delete only the exact draft ID after those proofs and + re-resolve it as absent. Candidate tag deletion is a **compare-and-delete** + under the same trusted release writer and protected namespace: immediately + before deletion compare the live ref with the recorded tag-object SHA, and + proceed only while serialization/protection guarantees that another admitted + writer cannot change it before deletion. If compare-and-delete semantics or + exclusive serialization cannot be guaranteed, do not delete or retarget the + ref; quarantine the version. After an allowed deletion, re-resolve both draft + and ref as absent before same-version retry. Never reuse a tag name that has + been associated with a published immutable release. +10. Reproducibility is tested by rebuilding the wheel and source distribution + from the same protected source under the reviewed toolchain and comparing + the release contract's declared deterministic subjects. Any known + nondeterministic field must be removed or normalized by source/tooling + repair; it is not excluded from comparison merely to obtain GREEN. +11. Rollback restores a previously reviewed workflow revision and produces new + artifacts from a new protected commit/version. It does not move an existing + release tag or reuse an old attestation for different bytes. Once publication + may have happened, remediation is forward-only; a tag name associated with a + published immutable release is never reused even if that release is later + deleted. +12. Package-registry publication is not inferred from a GitHub Release. If a + registry such as PyPI is adopted, its protected environment, trusted + publishing identity, independent review policy and immutable-version + behavior require a separate accepted decision before credentials or + publishing steps are added. + +## RED / GREEN acceptance + +The current RED is product-local: LineageWeave has no release workflow and +protected `main` is not yet license-clean. The former canonical handoff blocker +`.github#1782` is resolved by `.github#1791` and the reviewed owner pin +`bd866a21cca2a7e709f0b7a88150c310a9d98239`. + +GREEN requires all of the following on one unchanged protected source SHA: + +- #911 or a verified successor has removed the reachable disallowed dependency + and committed a reproducible lock that passes the frozen dependency gate; +- the product-local workflow consumes + `exact-artifact-sbom-attestation.yml@bd866a21cca2a7e709f0b7a88150c310a9d98239` + rather than mutable `.github/main` or a copied verifier; +- a product-local release workflow builds wheel/sdist plus the exact six-file + evidence handoff without credentialed execution of pull-request source; +- the canonical reusable verifies and attests the exact returned artifact + receipt and exact wheel/sdist subjects; +- one trusted release writer has exclusive candidate-version serialization and + an active protected tag namespace; the immutable-release preflight and final + publish-boundary check both require `enabled: true` and + `enforced_by_owner: true`; +- immediately before publish, the exact Release ID still has `draft: true`, the + exact `tag_name`, `prerelease: false`, and the exact sealed asset name/digest + set; the annotated tag ref still names the recorded tag object, which peels to + type `commit` and the exact protected source SHA; +- a clean rebuild proves the declared reproducibility contract; +- post-publication verification observes the same release/tag/assets and + `immutable: true`; +- synthetic release fixtures exercise pre-publication abort, conditional + compare-and-delete, exact draft/tag ownership, quarantine when serialization + cannot be proved, re-resolve-as-absent before same-version retry, and never + reuse a tag associated with a published immutable release; +- release notes, version, protected source SHA, tag, distributions, SBOMs, + attestations and immutable GitHub Release are mutually consistent; and +- rollback/incident instructions are exercised without deleting or rewriting + valid published evidence. + +Until every condition is evidenced, this ADR remains Proposed and no +LineageWeave release-readiness claim may cite this design as delivered. + +## Alternatives considered + +### Copy the central attestation workflow into LineageWeave + +Rejected. It would create a second signing-policy authority, duplicate security +fixes and let a product repository bypass the canonical owner. + +### Drop the GitHub artifact digest from verification locally + +Rejected. The outer receipt protects the exact same-run transport handoff. The +former circularity was an owner-contract modeling defect; the canonical repair +moved the digest out of the pre-upload inner identity while retaining +independent outer-receipt verification. + +### Accept repository-local immutable-release enablement + +Rejected for the high-assurance commercial path. GitHub exposes +`enforced_by_owner`; requiring that owner-level control materially narrows the +settings race that a repository administrator could otherwise create between +admission and publication. If owner enforcement is unavailable, publication is +RED rather than silently downgraded to a mutable-release risk. + +### Treat a final settings/tag read as an atomic publish precondition + +Rejected because the documented Release REST API does not expose a consumer +compare-and-publish condition that atomically binds those earlier reads. The +contract therefore requires owner-enforced immutability plus protected tag +namespace and trusted-writer serialization, and verifies `immutable: true` +after publication instead of claiming a nonexistent REST atomicity primitive. + +### Compare an annotated tag ref SHA directly with the source commit SHA + +Rejected. For an annotated tag, the ref names a Git tag object rather than the +source commit. The trusted boundary must peel the tag object, require target +type `commit`, and compare that target SHA with the admitted source SHA. + +### Delete a candidate tag after a standalone read + +Rejected. A read-then-delete sequence can delete a ref that was retargeted after +the read. Candidate deletion is allowed only as compare-and-delete under the +exclusive trusted writer/protected namespace; without that serialization proof, +the version is quarantined and the ref is left untouched. + +### Publish a GitHub Release first and attach evidence later + +Rejected. Buyers would observe a release identity before its exact artifact, +SBOM and provenance evidence was complete. GitHub recommends attaching assets +to a draft and publishing only after the asset set is complete. + +## Risks and follow-up + +- The canonical reusable can evolve after the reviewed owner repair. This ADR + pins `bd866a21cca2a7e709f0b7a88150c310a9d98239`; changing that dependency + requires normal review and fresh exact evidence. +- `enforced_by_owner: true` depends on organization release-immutability policy. + If the organization cannot provide that configuration lock, this Proposed ADR + does not authorize release publication. +- The trusted release writer and candidate-tag ruleset must be implemented and + tested before Accepted status. A workflow-level concurrency group without a + tag ruleset is not sufficient against out-of-band tag mutation. +- The GitHub API does not provide a consumer-side atomic publish precondition + for all checked configuration. Post-publication `immutable: true` verification + is therefore mandatory and a failure is a release incident, not evidence that + the earlier checks were atomic. +- An unpublished draft/tag can survive a failed final admission. Ambiguous + ownership, missing serialization, or a changed ref prevents cleanup and + quarantines the version. +- Reproducible Python distributions may expose timestamps, archive ordering or + backend metadata that require causal build-system repair. A mismatch remains + RED until explained and removed at the source. +- Actions queue saturation can delay evidence, but queue latency is not a reason + to bypass release gates or transfer predecessor-head results. + +## References + +Bray, T. (2017). *The JavaScript Object Notation (JSON) data interchange +format* (RFC 8259). Internet Engineering Task Force. +https://doi.org/10.17487/RFC8259 + +CycloneDX Core Working Group. (2025). *CycloneDX specification 1.7*. +OWASP Foundation. https://cyclonedx.org/specification/overview/ + +GitHub. (2026). *Immutable releases*. GitHub Docs. +https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases + +GitHub. (2026). *REST API endpoints for releases*. GitHub Docs. +https://docs.github.com/en/rest/releases/releases + +GitHub. (2026). *REST API endpoints for repositories: Check if immutable +releases are enabled for a repository*. GitHub Docs. +https://docs.github.com/en/rest/repos/repos + +GitHub. (2026). *REST API endpoints for Git references*. GitHub Docs. +https://docs.github.com/en/rest/git/refs + +GitHub. (2026). *REST API endpoints for Git tags*. GitHub Docs. +https://docs.github.com/en/rest/git/tags + +GitHub. (2026). *Using artifact attestations to establish provenance for +builds*. GitHub Docs. +https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/use-artifact-attestations + +Open Source Security Foundation. (2025). *SLSA specification version 1.2*. +https://slsa.dev/spec/v1.2/ diff --git a/docs/adr/README.md b/docs/adr/README.md index 8979111c1..cfb80d99b 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -11,9 +11,10 @@ decision from them. |---|---| | [`product-requirements.md`](../product-requirements.md) | Product requirements projection across the ADR set; ADRs remain normative, including [0252](0252-temporal-primary-voice-history.md) | | [`product-technical-gap-baseline.md`](../product-technical-gap-baseline.md) | Product/technical traceability projection across the ADR set; ADRs remain normative | +| [`release.md`](../release.md) | [0361](0361-immutable-release-supply-chain-boundary.md) | +| [`RELEASE_SUPPLY_CHAIN_REFERENCES.md`](../doctoring/RELEASE_SUPPLY_CHAIN_REFERENCES.md) | [0361](0361-immutable-release-supply-chain-boundary.md) | | [`lineage-bi-research-notes.md`](../lineage-bi-research-notes.md) | [0084](0084-lineage-research-grounding.md), [0062](0062-semantic-unit-embedding.md), [0064](0064-lineage-evidence-and-tree-assembly.md), [0024](0024-rankweave-fusion-fail-closed.md), [0165](0165-quantity-script-display.md), [0167](0167-rankweave-ranking-channel-evidence.md), [0169](0169-ask-batched-lineage-graph.md), [0172](0172-event-lineage-channel-evidence.md), [0202](0202-ask-event-time-filter.md), [0223](0223-explicit-semantic-content-unit-kinds.md), [0238](0238-source-conversation-turn-import-contract.md) | | [`voice-combination-technical-requirements.md`](../voice-combination-technical-requirements.md) | [0246](0246-expanded-voice-of-x-post-taxonomy.md), [0251](0256-evidence-bearing-voice-combinations.md), [0252](0252-temporal-primary-voice-history.md) | -| [`lineage-bi-research-notes.md`](../lineage-bi-research-notes.md) | [0084](0084-lineage-research-grounding.md), [0062](0062-semantic-unit-embedding.md), [0064](0064-lineage-evidence-and-tree-assembly.md), [0024](0024-rankweave-fusion-fail-closed.md), [0165](0165-quantity-script-display.md), [0167](0167-rankweave-ranking-channel-evidence.md), [0169](0169-ask-batched-lineage-graph.md), [0172](0172-event-lineage-channel-evidence.md), [0202](0202-ask-event-time-filter.md), [0223](0223-explicit-semantic-content-unit-kinds.md) | | [`PROV_O_IMPLEMENTATION.md`](../PROV_O_IMPLEMENTATION.md) | [0065](0065-prov-o-provenance-boundary.md) | | [`PROV_O_IMPLEMENTATION_MATRIX.md`](../PROV_O_IMPLEMENTATION_MATRIX.md) | [0065](0065-prov-o-provenance-boundary.md) | | [`ONTOLOGY_NAMESPACE_INVENTORY.md`](../doctoring/ONTOLOGY_NAMESPACE_INVENTORY.md) | [0207](0207-repository-case-ontology-namespace-canonical.md), [0157](0157-public-ontology-namespace-identity.md) | @@ -53,4 +54,4 @@ decision from them. Files under `docs/doctoring/` remain non-normative supporting evidence even when this map links them to an ADR. Runtime-evidence files record observed -results for already-decided behavior. +results for already-decided behavior. \ No newline at end of file diff --git a/docs/doctoring/RELEASE_SUPPLY_CHAIN_REFERENCES.md b/docs/doctoring/RELEASE_SUPPLY_CHAIN_REFERENCES.md new file mode 100644 index 000000000..67049b20a --- /dev/null +++ b/docs/doctoring/RELEASE_SUPPLY_CHAIN_REFERENCES.md @@ -0,0 +1,199 @@ +# Release supply-chain references + +**Supporting evidence for:** ADR 0361 +**Reviewed:** 2026-09-04 +**Status:** Non-normative doctoring evidence. ADR 0361 remains the decision authority. + +This note records the authoritative external standards, platform contracts and +canonical-owner implementation evidence used while defining LineageWeave's +immutable release boundary. It does not promote an unimplemented workflow or a +queued check to release evidence. + +## Current authoritative baseline + +### CycloneDX 1.7 + +The CycloneDX specification overview identifies **1.7** as the current +specification version and gives its release date as 2025-10-21. ADR 0361 uses +CycloneDX 1.7 only for the per-distribution SBOM representation; LineageWeave +does not claim that a generic repository-directory SBOM is an attestation of +one exact wheel or source distribution. + +CycloneDX Core Working Group. (2025). *CycloneDX specification 1.7*. OWASP +Foundation. https://cyclonedx.org/specification/overview/ + +### SLSA 1.2 + +The SLSA project announced Version **1.2** as the approved release on +2025-11-24. ADR 0361 uses SLSA as supply-chain threat/provenance grounding; it +does not claim a SLSA level merely because a workflow uses provenance or a +reusable workflow. + +Open Source Security Foundation. (2025, November 24). *Announcing SLSA v1.2*. +https://slsa.dev/blog/2025/11/announce-slsa-v1.2 + +### GitHub artifact attestations + +GitHub's current documentation requires explicit attestation/OIDC permissions +for credentialed provenance generation and documents verification of artifact +attestations. ADR 0361 therefore keeps pull-request-controlled build work in an +unprivileged job and delegates credentialed attestation to the canonical +organization reusable only after inert exact-artifact verification. + +GitHub. (n.d.). *Using artifact attestations to establish provenance for +builds*. GitHub Docs. Retrieved September 3, 2026, from +https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/use-artifact-attestations + +GitHub. (n.d.). *Using artifact attestations*. GitHub Docs. Retrieved September +3, 2026, from +https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations + +### Canonical exact-artifact handoff + +The circular transport-receipt defect originally tracked by +`ContextualWisdomLab/.github#1782` is resolved by merged `.github#1791`. The +reviewed immutable owner commit for LineageWeave is +`bd866a21cca2a7e709f0b7a88150c310a9d98239`, and the consumer surface is: + +`ContextualWisdomLab/.github/.github/workflows/exact-artifact-sbom-attestation.yml@bd866a21cca2a7e709f0b7a88150c310a9d98239` + +At that commit, `source-identity.json` contains the inner source/artifact +identity only: schema version, source repository/SHA, evidence artifact name, +predicate/schema and exact wheel/sdist plus SBOM filenames and SHA-256 values. +It does not contain the post-upload GitHub artifact digest. The reusable keeps +`evidence_artifact_id`, `evidence_artifact_name` and +`evidence_artifact_digest` as outer receipt inputs, independently queries the +same-run GitHub Actions artifact metadata, requires exact ID/name/digest/run and +non-expired state before download, and repeats that receipt verification inside +the credentialed signer boundary. The six-file cardinality, strict JSON, +checksums and exact-subject CycloneDX validation remain fail closed. + +The current protected `.github/main` descends from the repair commit, but ADR +0361 pins the reviewed repair commit itself rather than a moving default branch. +A later owner revision is a new dependency change requiring normal review and +fresh exact evidence. + +ContextualWisdomLab. (2026). *fix(release): make exact artifact handoff acyclic* +(PR #1791). GitHub. + +### GitHub immutable releases + +GitHub documents immutable releases as a repository/organization control that +locks a published release's associated tag and assets. Publication also creates +a release attestation. GitHub recommends creating a draft release, attaching +all assets, and publishing the populated draft so immutability does not leave a +partially populated release. + +The repository REST API exposes `GET /repos/{owner}/{repo}/immutable-releases` +and returns an object containing `enabled` and `enforced_by_owner`. For ADR 0361 +both must be true: `enabled: true` proves the feature is active and +`enforced_by_owner: true` proves repository administrators cannot locally turn +it off during the release interval. The administrative read capability used to +inspect that state belongs only to the trusted release boundary. + +The Release REST representation exposes the exact Release ID, `tag_name`, +`draft`, `prerelease`, asset list and asset digests, and published releases +report whether they are `immutable`. ADR 0361 therefore treats a final settings +read alone as insufficient. Immediately before publish it re-reads the exact +Release ID, requires `draft: true`, the admitted `tag_name`, +`prerelease: false`, and the exact sealed asset name/digest set, then repeats the +annotated-tag object/commit check. After publication it requires +`immutable: true` and verifies the same tag/assets again. + +GitHub documents no consumer-supplied compare-and-publish precondition that +atomically binds those prior REST reads to publication. The architecture does +not invent one. The controllable race is narrowed with three mandatory +configuration/serialization controls: owner-enforced release immutability, a +reviewed ruleset protecting the candidate release-tag namespace, and one +exclusively serialized **trusted release writer** for candidate tag/ref and +Release mutations. If those controls are absent or cannot be proved, release +publication stays RED. + +That same ownership rule applies to abort cleanup. Verifying a candidate ref +and deleting it in separate unsynchronized calls leaves a TOCTOU window. ADR +0361 therefore defines candidate ref removal as **compare-and-delete** under the +same trusted release writer and protected tag namespace: compare the live ref +with the recorded tag-object SHA immediately before deletion and allow deletion +only when writer/ruleset serialization prevents another admitted writer from +retargeting it before the delete. If the platform configuration cannot provide +that guarantee, the ref is not deleted; the version is quarantined. Exact draft +identity, unpublished state, asset set and post-delete absent-state checks are +still required. + +GitHub also states that once a tag has been associated with a published +immutable release, the tag name cannot be reused even after that release is +deleted. Possible publication therefore changes recovery to a new version +rather than cleanup/reuse. + +GitHub. (n.d.). *Immutable releases*. GitHub Docs. Retrieved September 4, 2026, +from +https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases + +GitHub. (n.d.). *Preventing changes to your releases*. GitHub Docs. Retrieved +September 4, 2026, from +https://docs.github.com/en/code-security/how-tos/secure-your-supply-chain/establish-provenance-and-integrity/prevent-release-changes + +GitHub. (n.d.). *REST API endpoints for repositories*. GitHub Docs. Retrieved +September 4, 2026, from https://docs.github.com/en/rest/repos/repos + +GitHub. (n.d.). *REST API endpoints for releases*. GitHub Docs. Retrieved +September 4, 2026, from https://docs.github.com/en/rest/releases/releases + +### GitHub annotated tag identity + +GitHub's Git data APIs distinguish the tag reference from an annotated tag +object. Creating an annotated tag requires creating a Git tag object and then a +`refs/tags/` reference that points to that object. The tag object separately +records its target object type and target SHA; GitHub documents `commit`, +`tree`, and `blob` as possible target types for tag-object creation. Therefore a +trusted release verifier cannot compare the tag ref's object SHA directly with +the protected source commit SHA. For ADR 0361 the release tag is valid only when +the ref resolves to the expected annotated tag object, that object has target +type `commit`, and the peeled target SHA equals the exact protected source SHA. +Missing refs/tag objects, non-commit targets, and mismatched target SHAs fail +closed before publication and again during post-publication verification. + +GitHub. (n.d.). *REST API endpoints for Git references*. GitHub Docs. Retrieved +September 4, 2026, from https://docs.github.com/en/rest/git/refs + +GitHub. (n.d.). *REST API endpoints for Git tags*. GitHub Docs. Retrieved +September 4, 2026, from https://docs.github.com/en/rest/git/tags + +### JSON strictness + +The canonical `.github` verifier rejects duplicate JSON properties, non-finite +numbers, invalid UTF-8 and unexpected evidence members before trusting the +handoff. RFC 8259 is the interoperability baseline for the JSON representation; +repository-specific stricter validation remains a security profile rather than +a claim that RFC 8259 itself mandates every fail-closed rule used by the +verifier. + +Bray, T. (2017). *The JavaScript Object Notation (JSON) data interchange +format* (RFC 8259). Internet Engineering Task Force. +https://doi.org/10.17487/RFC8259 + +## Traceability to ADR 0361 + +- exact wheel/sdist SBOM representation → CycloneDX 1.7; +- source/build provenance threat model → SLSA 1.2; +- credential separation and artifact-attestation verification → GitHub artifact + attestation documentation; +- acyclic inner identity plus immutable outer GitHub Actions artifact receipt → + `ContextualWisdomLab/.github#1791` at + `bd866a21cca2a7e709f0b7a88150c310a9d98239`; +- owner-enforced immutable-release admission, exact draft/asset verification, + protected tag namespace, trusted-writer serialization and post-publication + `immutable: true` verification → GitHub immutable release, repository and + Release REST documentation; +- conditional candidate-ref compare-and-delete and quarantine on missing + serialization → the CWE-367 recovery boundary derived from those GitHub API + semantics; no undocumented atomic REST primitive is assumed; +- annotated-tag ref/object separation and exact source-commit peeling → GitHub + Git references and Git tags REST documentation; +- strict machine-readable evidence intake → RFC 8259 plus the stricter + canonical `.github` verifier contract. + +External standards do not override the current canonical owner implementation. +LineageWeave consumes only the reviewed immutable owner SHA above; moving to a +later `.github` revision requires a normal dependency review and regenerated +exact-head evidence. diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 000000000..160572a63 --- /dev/null +++ b/docs/release.md @@ -0,0 +1,190 @@ +# LineageWeave release contract + +This document is the operator projection of Proposed ADR 0361. It describes +what must be true before LineageWeave publishes an immutable release; it is not +evidence that a release already exists. + +## Current delivery state + +As of 2026-09-04, protected `main` has no product-local release workflow and no +GitHub Release has been published. One product prerequisite remains outside +this document's implementation scope: + +- LineageWeave PR #911 owns removal of the reachable `psycopg2-binary` + commercial-license intake finding and the corresponding reproducible + `uv.lock` migration. + +The former canonical-owner blocker `ContextualWisdomLab/.github#1782` is +resolved by merged `.github#1791`. LineageWeave consumes the repaired acyclic +exact-artifact reusable at immutable protected-owner commit +`bd866a21cca2a7e709f0b7a88150c310a9d98239`; it does not copy or weaken that +trust boundary locally. At that owner commit the sealed inner +`source-identity.json` is constructible before upload and excludes the +post-upload artifact digest, while GitHub's artifact ID/name/digest remains an +outer transport receipt that is independently rechecked before and inside the +credentialed signer boundary. + +A queued workflow, a predecessor-head success, a locally built wheel, a tag +without exact evidence, a mutable GitHub release, or an unpublished draft +release does not satisfy this contract. + +## Release sequence + +1. Resolve the candidate from protected `main` and record its full 40-character + source SHA. Refuse another ref, detached historical branch, or a source SHA + that moves during the release decision. +2. Validate one canonical three-part version across `pyproject.toml`, package + metadata and release notes. Refuse a tag/version that already exists. +3. Reproduce the committed dependency lock and install from the reviewed frozen + inputs. The release candidate must be commercial-license clean under current + organization policy; do not suppress an inventory finding to continue. +4. Repeat the release-relevant repository contract on that exact SHA: complete + backend/frontend tests, documentation-as-contract checks, package checks and + the then-required security/governance gates. Required asynchronous GitHub + checks must be terminal-success on the unchanged candidate before publish. +5. Build a wheel and source distribution in an unprivileged job. The build job + receives `contents: read` only and does not receive OIDC, attestation, + release, package-publication or repository-write credentials. +6. Rebuild from the same exact source/toolchain and compare the deterministic + release subjects. Any unexplained byte difference is RED; do not omit the + differing subject merely to make the comparison pass. +7. Generate one CycloneDX 1.7 SBOM for each exact distribution and bind its root + component to the distribution filename and SHA-256 digest. Prepare the + six-file handoff defined by ADR 0361: wheel, wheel SBOM, source distribution, + source-distribution SBOM, `source-identity.json`, and + `checksums.sha256`. +8. Upload that handoff once and retain GitHub's returned artifact ID, name and + digest as the immutable outer transport receipt. +9. Invoke + `ContextualWisdomLab/.github/.github/workflows/exact-artifact-sbom-attestation.yml@bd866a21cca2a7e709f0b7a88150c310a9d98239`. + The canonical verifier must independently bind the same run, source SHA, + outer artifact ID/name/digest receipt, inner checksums and exact wheel/sdist + subjects before its credentialed attestation job runs. A later mutable + `.github/main` commit is not a substitute for this reviewed pin. +10. Before tag creation or Release publication, enter one **trusted release writer** + that is exclusively serialized for the candidate release/tag namespace and + call `GET /repos/{owner}/{repo}/immutable-releases` with the minimum GitHub + Administration (read) capability. Continue only when the authenticated + response confirms both `enabled: true` and `enforced_by_owner: true`. + Repository-level enablement without owner enforcement is insufficient for + this high-assurance path because a repository administrator could disable + the setting between admission and publication. The release-tag namespace + must also be protected by a reviewed ruleset that rejects retarget/delete by + actors outside this trusted release writer. If owner enforcement, writer + serialization, or tag protection cannot be proved, fail closed before tag + creation. This credential and writer capability are not exposed to + pull-request or unprivileged build execution. +11. After trusted verification and the step-10 admission both succeed, create an + annotated release-specific tag object with type `commit` and target SHA + equal to the exact protected source SHA from step 1, then create + `refs/tags/` pointing to that tag object. Record the newly created + tag-object SHA/ref, the active protection/ruleset receipt, and the pre-create + proof that the ref was absent. Refuse lightweight tags, tree/blob targets, + an existing ref, or any tag object whose target differs from the admitted + source. Create a draft GitHub Release, retain its exact Release ID and + `draft: true` creation receipt, and attach the complete verified + distributions, SBOM/provenance evidence, checksum material and release + notes. Record the exact expected asset name/digest set from the sealed + evidence. Do not publish an incomplete asset set. +12. **Immediately before publish**, while the same trusted release writer still + owns the exclusive candidate namespace, recheck + `GET /repos/{owner}/{repo}/immutable-releases` and require `enabled: true` + plus `enforced_by_owner: true`; re-read the exact Release ID and require + `draft: true`, the exact admitted `tag_name`, `prerelease: false`, and an + asset set whose names and digests exactly equal the sealed evidence. Then + re-resolve the annotated tag ref, require it still points to the recorded + tag object, fetch that tag object, peel its target, require type `commit`, + and compare the peeled commit with the **exact protected source SHA** from + step 1. The protected tag ruleset plus the exclusively serialized trusted + release writer are the lease that prevents another admitted writer from + retargeting the ref between this validation and publication. GitHub exposes + no consumer-supplied compare-and-publish precondition for repository + immutability, so this contract does not pretend that two independent REST + calls are atomic: owner-enforced immutability and the protected/serialized + tag namespace are mandatory configuration locks. Any drift, missing lock, + Release ID/state/tag/prerelease mismatch, asset/digest mismatch, lookup + failure, malformed response, tag-object mismatch, unexpected object type, + or peeled commit/source mismatch must **fail closed** with the draft still + unpublished. If that happens after step 11, execute the pre-publication + abort procedure below before any same-version retry. +13. Publish that fully populated draft as the non-prerelease immutable GitHub + Release. Never move an existing release tag or overwrite published bytes + under an existing version. +14. Fetch the published release back through GitHub's API, require + `immutable: true`, verify the exact Release ID/tag and asset digests again, + and repeat the annotated-tag peel to prove the release tag still reaches + the exact protected source commit. Retain this post-publication receipt as + release evidence. A non-immutable result is a release incident even if the + preceding admission checks succeeded. + +## Failure and rollback + +A failure before step 11 creates no candidate release identity. Preserve the +run ID, exact source SHA, logs and any sealed evidence needed for RCA, fix the +source/configuration through a normal protected PR, and start again from a +fresh exact candidate. + +A failure after step 11 but before publication is a **pre-publication abort**, +not proof that no identity exists. The run may already own an unpublished draft +release and a candidate tag ref. Before retrying the same version, the trusted +release writer must use the recorded creation receipts to prove all of the +following: the exact Release ID created by this run still resolves as +`draft: true` and unpublished; its `tag_name` is the admitted version and +`prerelease: false`; its asset set/digests still match the recorded candidate; +the candidate tag ref still points to the exact annotated tag object created by +this run; that tag object still peels to the admitted protected source commit; +and no published release resolves for the candidate tag. If any lookup, +identity, ownership, publication-state, ruleset, or exclusive-writer proof is +missing or ambiguous, do not delete or retarget anything. Quarantine that +version and require a new version/source candidate. + +Only after those unpublished-only proofs succeed may the abort cleanup delete +the exact draft Release ID created by this run. Re-resolve that Release ID as +absent, then recheck that no published release resolves for the candidate tag +and that the ref still points to the recorded candidate tag object. Candidate +ref removal is a **compare-and-delete** operation under the same exclusively +serialized trusted release writer and protected tag namespace: immediately +before deletion, compare the live ref with the recorded tag-object SHA and +proceed only if the writer/ruleset guarantee prevents another admitted writer +from changing it before delete. If the platform/ruleset cannot provide that +serialization guarantee, do not delete the ref; quarantine the version. After +a permitted deletion, re-resolve both the draft release identity and candidate +tag ref as absent before a same-version retry is admissible. A cleanup failure +remains RED and quarantines the version; it is not permission to force, +retarget, or reuse the identity. + +Never reuse a tag name that has been associated with a published immutable release. +GitHub's immutable-release contract locks the associated tag after publication +and reserves that tag name even if the immutable release is later deleted. Once +publication may have occurred, rollback is forward-only: preserve the forensic +evidence, identify affected subjects, correct source/workflow through normal +governance, and publish replacement artifacts under a new version and protected +source SHA. + +If an already-published artifact or attestation is found invalid, preserve the +forensic evidence and identify affected subjects before any revocation or +removal. Correct the source or workflow through normal governance, publish new +artifacts under a new version/source SHA, and tell consumers which subjects are +invalid and which replacements they should verify. Rollback never means moving +a tag, replacing an asset under the same name/version, or reusing an old +attestation for new bytes. + +## Evidence that does not transfer + +Checks, reviews, package builds, SBOMs, attestations, browser evidence and +release receipts belong to the exact head/artifact they evaluated. A source +commit, dependency lock, release workflow, reusable-workflow pin, repository +immutability setting, tag-protection ruleset or artifact byte change invalidates +predecessor evidence and requires fresh verification. + +## Owner boundaries + +LineageWeave owns release orchestration for its own package and buyer-visible +release receipt. `ContextualWisdomLab/.github` owns the credentialed reusable +attestation policy. The trusted release-admission step may receive only the +administrative read capability needed to prove repository release immutability; +that capability is not a build, pull-request, attestation-policy or provider +credential. Provider/model execution remains owned by `contextual-orchestrator`; +statistical/psychometric engines and their release truth remain with their +canonical owners. No release step copies those owners' source or treats a +mutable sibling branch as a production dependency. diff --git a/tests/test_adr_supporting_document_map.py b/tests/test_adr_supporting_document_map.py new file mode 100644 index 000000000..4ff2280cc --- /dev/null +++ b/tests/test_adr_supporting_document_map.py @@ -0,0 +1,29 @@ +"""Regression tests for the ADR supporting-document authority map.""" + +from collections import Counter +from pathlib import Path +import re + + +_REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +_ADR_INDEX = _REPOSITORY_ROOT / "docs" / "adr" / "README.md" +_SUPPORTING_DOCUMENT_ROW = re.compile( + r"^\| \[`[^`]+`\]\((?P[^)]+)\) \|" +) + + +def test_supporting_document_map_has_unique_document_targets() -> None: + """Keep one normative ADR mapping row per supporting document target.""" + targets = [ + match.group("target") + for line in _ADR_INDEX.read_text(encoding="utf-8").splitlines() + if (match := _SUPPORTING_DOCUMENT_ROW.match(line)) is not None + ] + duplicate_targets = sorted( + target for target, count in Counter(targets).items() if count > 1 + ) + + assert duplicate_targets == [], ( + "docs/adr/README.md must map each supporting document exactly once; " + f"duplicate targets: {duplicate_targets}" + ) diff --git a/tests/test_release_supply_chain_docs.py b/tests/test_release_supply_chain_docs.py new file mode 100644 index 000000000..b85627d8e --- /dev/null +++ b/tests/test_release_supply_chain_docs.py @@ -0,0 +1,133 @@ +"""Contract tests for release supply-chain documentation.""" + +from pathlib import Path + + +_REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +_ADR = _REPOSITORY_ROOT / "docs" / "adr" / "0361-immutable-release-supply-chain-boundary.md" +_RELEASE_GUIDE = _REPOSITORY_ROOT / "docs" / "release.md" +_DOCTORING = _REPOSITORY_ROOT / "docs" / "doctoring" / "RELEASE_SUPPLY_CHAIN_REFERENCES.md" +_IMMUTABILITY_ENDPOINT = "GET /repos/{owner}/{repo}/immutable-releases" +_EXACT_ARTIFACT_OWNER_SHA = "bd866a21cca2a7e709f0b7a88150c310a9d98239" + + +def _numbered_step(text: str, number: int, following: int) -> str: + """Return one numbered release/decision step so ordering assertions stay local.""" + start = text.index(f"\n{number}. ") + end = text.index(f"\n{following}. ", start) + return " ".join(text[start:end].lower().split()) + + +def _between(text: str, start_marker: str, end_marker: str) -> str: + """Return one named procedure slice instead of accepting document-wide keywords.""" + start = text.lower().index(start_marker.lower()) + end = text.lower().index(end_marker.lower(), start) + return text[start:end].lower() + + +def test_release_publication_requires_owner_enforced_github_release_immutability() -> None: + """Keep the immutability admission predicate in one ordered preflight step.""" + for path in (_ADR, _RELEASE_GUIDE): + text = path.read_text(encoding="utf-8") + preflight = _numbered_step( + text, + 10 if path == _RELEASE_GUIDE else 7, + 11 if path == _RELEASE_GUIDE else 8, + ) + assert _IMMUTABILITY_ENDPOINT.lower() in preflight, path + assert "enabled: true" in preflight, path + assert "enforced_by_owner: true" in preflight, path + assert "fail closed" in preflight, path + + +def test_release_publication_rechecks_exact_draft_tag_assets_and_immutability_at_boundary() -> None: + """Bind the final publish decision to one exact draft, tag, asset set and immutable policy.""" + for path in (_ADR, _RELEASE_GUIDE): + text = path.read_text(encoding="utf-8") + publish_step = _numbered_step( + text, + 12 if path == _RELEASE_GUIDE else 8, + 13 if path == _RELEASE_GUIDE else 9, + ) + for required in ( + "immediately before publish", + "exact release id", + "draft: true", + "tag_name", + "prerelease: false", + "asset", + "digest", + "annotated tag", + "tag object", + "peel", + "type `commit`", + "exact protected source sha", + ): + assert required in publish_step, (path, required) + assert "enforced_by_owner: true" in publish_step, path + assert "trusted release writer" in publish_step, path + assert "fail closed" in publish_step, path + + +def test_prepublication_abort_orders_identity_proof_before_conditional_tag_cleanup() -> None: + """Exact candidate ownership must be proved before compare-and-delete or quarantine.""" + adr_text = _ADR.read_text(encoding="utf-8") + release_text = _RELEASE_GUIDE.read_text(encoding="utf-8") + abort_procedures = ( + (_ADR, _numbered_step(adr_text, 9, 10), "protected namespace"), + ( + _RELEASE_GUIDE, + _between( + release_text, + "A failure after step 11 but before publication is a **pre-publication abort**", + "Never reuse a tag name that has been associated with a published immutable release", + ), + "protected tag namespace", + ), + ) + + for path, procedure, protected_marker in abort_procedures: + proof_terms = ( + "exact release id", + "draft: true", + "prerelease: false", + "tag_name", + "asset", + "digest", + "recorded tag object" if path == _ADR else "recorded candidate tag object", + ) + proof_end = max(procedure.index(term) for term in proof_terms) + compare_delete = procedure.index("compare-and-delete") + trusted_writer = procedure.index("trusted release writer", compare_delete) + protected_tag = procedure.index(protected_marker, trusted_writer) + recorded_tag_object = procedure.index("recorded tag-object sha", protected_tag) + serialization = procedure.index("serialization", recorded_tag_object) + quarantine = procedure.index("quarantine", serialization) + + assert ( + proof_end + < compare_delete + < trusted_writer + < protected_tag + < recorded_tag_object + < serialization + < quarantine + ), path + assert "do not delete" in procedure[serialization:quarantine], path + assert "re-resolve" in procedure[compare_delete:], path + assert "absent" in procedure[compare_delete:], path + + +def test_release_contract_pins_the_repaired_canonical_attestation_owner() -> None: + """Consume the merged acyclic handoff by immutable owner SHA, never mutable main or the old blocker.""" + for path in (_ADR, _RELEASE_GUIDE, _DOCTORING): + text = path.read_text(encoding="utf-8") + assert _EXACT_ARTIFACT_OWNER_SHA in text, path + assert ".github#1791" in text, path + assert ".github#1782 remains open" not in text, path + + release_text = _RELEASE_GUIDE.read_text(encoding="utf-8") + assert ( + "ContextualWisdomLab/.github/.github/workflows/" + "exact-artifact-sbom-attestation.yml@" + _EXACT_ARTIFACT_OWNER_SHA + ) in release_text