diff --git a/publication.json b/publication.json index 7f43ec2..64312f9 100644 --- a/publication.json +++ b/publication.json @@ -303,7 +303,17 @@ "source_path": "canon/architecture/adr-002-custodian-agent-runtime-design.md", "canonical_path": "adr/custodian-agent-runtime/v1/index.html", "revision_path": "adr/custodian-agent-runtime/v1/revisions/{revision}/index.html", - "review_interval": "6m" + "review_interval": "6m", + "historical_revisions": [ + { + "revision": "accepted-1", + "source_path": "docs/evidence/2026-10-11-adr-currency-review/adr-002-accepted-1.md", + "source_digest": "6aef66cd5cf71a48f5b4e14401dc19a755e2b182445b272d5952df0eb0dea8ec", + "source_revision": "4039c9d1c08c92014ecc0a65dda63cc73ba187bb", + "rendered_page_path": "docs/evidence/2026-10-11-adr-currency-review/adr-002-accepted-1.html", + "rendered_page_digest": "ea4eae05b62d50cd8204a47c1a4f3e3e0beda571880ed1de93ccbb17987d84aa" + } + ] }, { "id": "CUST-ADR-003", @@ -319,7 +329,17 @@ "source_path": "canon/architecture/adr-004-connectivity-first-network-posture.md", "canonical_path": "adr/custodian-connectivity-first/v1/index.html", "revision_path": "adr/custodian-connectivity-first/v1/revisions/{revision}/index.html", - "review_interval": "6m" + "review_interval": "6m", + "historical_revisions": [ + { + "revision": "accepted-1", + "source_path": "docs/evidence/2026-10-11-adr-currency-review/adr-004-accepted-1.md", + "source_digest": "3b68adfa6ab329e73f857cf691dc405136d2d66a0135e2c37c236aabe4659557", + "source_revision": "4039c9d1c08c92014ecc0a65dda63cc73ba187bb", + "rendered_page_path": "docs/evidence/2026-10-11-adr-currency-review/adr-004-accepted-1.html", + "rendered_page_digest": "3db1b0f5c50543ff0fb022ea67f1d85b30f8ff691eaa1a2443220b7474b54de7" + } + ] }, { "id": "CUST-ADR-005", @@ -551,6 +571,16 @@ "source_path": "canon/standards/security-layer-model_v0.7.md", "canonical_path": "standards/security-layer-model/v0.7/index.html", "revision_path": "standards/security-layer-model/v0.7/revisions/{revision}/index.html", + "review_interval": "3m", + "lifecycle": "superseded", + "successor": "/standards/security-layer-model/v0.8/" + }, + { + "id": "netkingdom-security-layer-model-v0.8", + "source_repo": "net-kingdom", + "source_path": "canon/standards/security-layer-model_v0.8.md", + "canonical_path": "standards/security-layer-model/v0.8/index.html", + "revision_path": "standards/security-layer-model/v0.8/revisions/{revision}/index.html", "review_interval": "3m" }, { diff --git a/source-inventory.json b/source-inventory.json index f512a1f..fbf5591 100644 --- a/source-inventory.json +++ b/source-inventory.json @@ -259,6 +259,12 @@ "source_path": "canon/standards/credential-management_v0.2.md", "source_repo": "net-kingdom" }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/emission-cadence-security-profile_v0.1.md", + "source_repo": "net-kingdom" + }, { "disposition": "metadata-pending", "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", @@ -271,18 +277,36 @@ "source_path": "canon/standards/iam-profile_v0.3.md", "source_repo": "net-kingdom" }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/iam-profile_v0.4.md", + "source_repo": "net-kingdom" + }, { "disposition": "metadata-pending", "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", "source_path": "canon/standards/playbook-capability-contract_v0.1.md", "source_repo": "net-kingdom" }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/playbook-capability-contract_v0.2.md", + "source_repo": "net-kingdom" + }, { "disposition": "published", "reason": "Published through an explicit publication.json document entry.", "source_path": "canon/standards/posture-feedback_v0.1.md", "source_repo": "net-kingdom" }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/security-case-types_v0.1.md", + "source_repo": "net-kingdom" + }, { "disposition": "excluded", "reason": "Superseded before publication by security-layer-model v0.2; retained as source history.", @@ -325,6 +349,12 @@ "source_path": "canon/standards/security-layer-model_v0.7.md", "source_repo": "net-kingdom" }, + { + "disposition": "published", + "reason": "Published through an explicit publication.json document entry.", + "source_path": "canon/standards/security-layer-model_v0.8.md", + "source_repo": "net-kingdom" + }, { "disposition": "published", "reason": "Published through an explicit publication.json document entry.", @@ -373,6 +403,12 @@ "source_path": "docs/adr/ADR-0008-object-storage-sts-credential-vending.md", "source_repo": "net-kingdom" }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0009-expanded-mode-keycloak-federation-topology.md", + "source_repo": "net-kingdom" + }, { "disposition": "published", "reason": "Published through an explicit publication.json document entry.", @@ -409,6 +445,24 @@ "source_path": "docs/adr/ADR-0015-netkingdom-railiance-workload-packaging-and-relational-platform.md", "source_repo": "net-kingdom" }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0016-mfa-user-preference-and-workload-step-up.md", + "source_repo": "net-kingdom" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0017-root-grant-custody-and-account-authority.md", + "source_repo": "net-kingdom" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/architecture/expanded-mode-admission-requirements.md", + "source_repo": "net-kingdom" + }, { "disposition": "published", "reason": "Published through an explicit publication.json document entry.", @@ -589,6 +643,12 @@ "source_path": "docs/adr/ADR-0009-netkingdom-security-layer-interaction.md", "source_repo": "railiance-master" }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0010-taxonomy-is-an-enumerated-layer.md", + "source_repo": "railiance-master" + }, { "disposition": "published", "reason": "Published through an explicit publication.json document entry.", @@ -613,6 +673,18 @@ "source_path": "docs/adr/ADR-0003-decisions-live-in-the-repo.md", "source_repo": "railiance-platform" }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0004-service-assurance-disclosure.md", + "source_repo": "railiance-platform" + }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "docs/adr/ADR-0005-demand-gated-stateful-services.md", + "source_repo": "railiance-platform" + }, { "disposition": "excluded", "reason": "Directory index, not an architecture decision record.", @@ -889,6 +961,12 @@ "source_path": "canon/standards/credential-management_v0.1.md", "source_repo": "the-custodian" }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/delegated-owner-review_v0.1.md", + "source_repo": "the-custodian" + }, { "disposition": "metadata-pending", "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", @@ -997,6 +1075,12 @@ "source_path": "canon/standards/schemas/work-records/spine.schema.json", "source_repo": "the-custodian" }, + { + "disposition": "metadata-pending", + "reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.", + "source_path": "canon/standards/task-wait-qualifiers_v0.1.md", + "source_repo": "the-custodian" + }, { "disposition": "unsupported-format", "reason": "Inventoried governing source is not Markdown and has no renderer yet.", diff --git a/tests/test_publication.py b/tests/test_publication.py index 85e9091..2f79ba2 100644 --- a/tests/test_publication.py +++ b/tests/test_publication.py @@ -1,5 +1,6 @@ from __future__ import annotations +import hashlib import importlib.util import json import datetime as dt @@ -275,5 +276,90 @@ class PublicationTest(unittest.TestCase): build_site.build(manifest, root / "site") +class HistoricalRevisionTest(unittest.TestCase): + def prepare(self, root): + manifest = _fixture(root) + data = json.loads(manifest.read_text()) + repo = root / "canon-repo" + source = repo / "canon/standards/example.md" + source.write_text(source.read_text().replace( + 'owner: example-owner', 'owner: example-owner\ndecided_by: Original Founder\ndate: "2026-08-18"')) + with mock.patch.object(build_site, "_source_revision", return_value="a" * 40): + build_site.build(manifest, root / "served", as_of=dt.date(2026, 8, 18)) + original_page = (root / "served/standards/example/v1/revisions/draft-1/index.html").read_bytes() + history = repo / "docs/evidence/original.md" + history.parent.mkdir(parents=True) + history.write_bytes(source.read_bytes()) + page = history.with_suffix(".html") + page.write_bytes(original_page) + data["documents"][0]["historical_revisions"] = [{ + "revision": "draft-1", "source_path": "docs/evidence/original.md", + "source_digest": hashlib.sha256(history.read_bytes()).hexdigest(), + "source_revision": "a" * 40, + "rendered_page_path": "docs/evidence/original.html", + "rendered_page_digest": hashlib.sha256(original_page).hexdigest(), + }] + manifest.write_text(json.dumps(data)) + source.write_text(source.read_text().replace('revision: "draft-1"', 'revision: "draft-2"') + + "\nDated factual annotation; no new authority.\n") + return manifest, source, page, original_page + + def test_clean_build_preserves_exact_served_revision_and_new_annotation(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + manifest, source, _page, original = self.prepare(root) + output = root / "clean" + with mock.patch.object(build_site, "_source_revision", return_value="b" * 40): + build_site.build(manifest, output, as_of=dt.date(2026, 8, 18)) + old = output / "standards/example/v1/revisions/draft-1/index.html" + self.assertEqual(original, old.read_bytes()) + self.assertNotIn(b"Dated factual annotation", old.read_bytes()) + self.assertIn("Dated factual annotation", + (output / "standards/example/v1/index.html").read_text()) + self.assertTrue((output / "standards/example/v1/revisions/draft-2/index.html").is_file()) + meta, _ = build_site.split_frontmatter(source.read_text()) + self.assertEqual("Original Founder", meta["decided_by"]) + self.assertEqual("2026-08-18", meta["date"]) + + def test_changed_historical_page_fails_before_replacing_output(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + manifest, _source, page, _original = self.prepare(root) + page.write_bytes(page.read_bytes() + b"changed") + with self.assertRaisesRegex(RuntimeError, "rendered page digest mismatch"): + build_site.build(manifest, root / "clean") + self.assertFalse((root / "clean").exists()) + + def test_changed_historical_source_and_provenance_are_rejected(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + manifest, _source, page, _original = self.prepare(root) + history = page.with_suffix(".md") + history.write_bytes(history.read_bytes() + b"changed") + with self.assertRaisesRegex(RuntimeError, "source digest mismatch"): + build_site.build(manifest, root / "clean") + + def test_existing_immutable_history_refuses_different_page_bytes(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + manifest, _source, _page, original = self.prepare(root) + old = root / "clean/standards/example/v1/revisions/draft-1/index.html" + old.parent.mkdir(parents=True) + old.write_bytes(original + b"changed") + with self.assertRaisesRegex(RuntimeError, "already has different bytes"): + build_site.build(manifest, root / "clean") + self.assertEqual(original + b"changed", old.read_bytes()) + + def test_historical_source_cannot_escape_owner_repository(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + manifest, _source, _page, _original = self.prepare(root) + data = json.loads(manifest.read_text()) + data["documents"][0]["historical_revisions"][0]["source_path"] = "../publication.json" + manifest.write_text(json.dumps(data)) + with self.assertRaisesRegex(ValueError, "inside source repository"): + build_site.build(manifest, root / "clean") + + if __name__ == "__main__": unittest.main() diff --git a/tools/build_site.py b/tools/build_site.py index 4cded5a..a08900e 100644 --- a/tools/build_site.py +++ b/tools/build_site.py @@ -111,6 +111,8 @@ def load_manifest(path: Path) -> dict[str, Any]: document["canonical_path"], document["revision_path"].replace("{revision}", "revision"), *document.get("legacy_paths", []), + *(document["revision_path"].format(revision=h["revision"]) + for h in document.get("historical_revisions", [])), ): path_value = _safe_path(raw) if path_value in seen: @@ -119,6 +121,44 @@ def load_manifest(path: Path) -> dict[str, Any]: return manifest +def _restore_historical_revisions( + document: dict[str, Any], repo: Path, output: Path, current_revision: str, +) -> None: + """Retain explicitly pinned served revision bytes even in a clean build.""" + for historical in document.get("historical_revisions", []): + revision = historical["revision"] + if revision == current_revision: + raise ValueError("historical revision must differ from current revision") + source = (repo / historical["source_path"]).resolve() + page = (repo / historical["rendered_page_path"]).resolve() + for artifact in (source, page): + if repo not in artifact.parents or not artifact.is_file(): + raise ValueError("historical artifact must be a file inside source repository") + source_bytes, page_bytes = source.read_bytes(), page.read_bytes() + if hashlib.sha256(source_bytes).hexdigest() != historical["source_digest"]: + raise RuntimeError("historical source digest mismatch") + if hashlib.sha256(page_bytes).hexdigest() != historical["rendered_page_digest"]: + raise RuntimeError("historical rendered page digest mismatch") + meta, _ = split_frontmatter(source_bytes.decode("utf-8")) + if meta.get("id") != document["id"] or meta.get("revision") != revision: + raise ValueError("historical source id/revision mismatch") + page_text = page_bytes.decode("utf-8") + source_revision = SOURCE_REVISION.search(page_text) + source_digest = SOURCE_DIGEST.search(page_text) + expected_revision = historical["source_revision"] + if not CLEAN_GIT_REVISION.fullmatch(expected_revision): + raise ValueError("historical source revision must be a clean Git commit") + if (not source_revision or not source_digest + or html.unescape(source_revision.group(1)) != expected_revision + or html.unescape(source_digest.group(1)) != historical["source_digest"]): + raise RuntimeError("historical page/source provenance mismatch") + target = output / _safe_path(document["revision_path"].format(revision=revision)) + if target.exists() and target.read_bytes() != page_bytes: + raise RuntimeError("immutable historical revision already has different bytes") + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(page_bytes) + + def _redirect(target: str, title: str) -> str: escaped = html.escape(target, quote=True) return ( @@ -214,6 +254,7 @@ def build( revision = meta.get("revision") or meta.get("version") if not revision: raise ValueError(f"{source}: revision or version is required") + _restore_historical_revisions(document, repo, temporary, revision) source_revision = _source_revision(repo, source) source_digest = hashlib.sha256(source.read_bytes()).hexdigest() reviewed = meta.get("last_reviewed") or meta.get("updated") diff --git a/workplans/PNEX-WP-0004-security-layer-v08-publication.md b/workplans/PNEX-WP-0004-security-layer-v08-publication.md new file mode 100644 index 0000000..fc538d0 --- /dev/null +++ b/workplans/PNEX-WP-0004-security-layer-v08-publication.md @@ -0,0 +1,64 @@ +--- +id: PNEX-WP-0004 +type: workplan +title: "Publish the accepted security-layer v0.8 guardrails" +domain: infotech +repo: policy-nexus +status: active +owner: codex +topic_slug: custodian +created: "2026-10-11" +updated: "2026-10-11" +flavor: implementation +depends_on: [] +--- + +Native human memo `infd-20261010-security-layer-v08` revision2 accepted exact +proposed source103a635f; confirmed approval8432d0dc. NetKingdom accepted artifact +3d829856 is committed in0e17d956ab45f8c4a0163b70749490c414e54e67. +This is new requested publication demand, not a residual or duplicate checker +migration. GH-WP-0004-T11 is source-complete; hosting is this owner's task. + +## Register accepted source and preserve history + +```task +id: PNEX-WP-0004-T01 +status: done +priority: high +``` + +Register accepted-1 v0.8 and superseded currentv0.7 notice; retain immutable +historicalv0.7 source/page bytes. Full local73document build succeeded. Inventory +refresh adds13 previously unreviewed discovered records as metadata-pending, +changes no existing disposition and publishes none of those13. Exact memo/source +and native acceptance receipts stay with NetKingdom and Custodian evidence. + +## Produce a verified source-bound publisher candidate + +```task +id: PNEX-WP-0004-T02 +status: progress +priority: high +``` + +Retain immutable fetched source revision lock, content/manifest hashes and full +release verification. Local build alone is not a served publication. Current +currency gate honestly reports CUST-ADR-002/004 stale; owner technical review +must establish their truthful status, not blindly bump dates or suppress them. +Publish/verify the candidate through documented owner CI; retain any failed +checks and exact release output before proposing native activation. + +## Serve and verify the accepted guardrails + +```task +id: PNEX-WP-0004-T03 +status: todo +priority: high +``` + +Prepare exact hosting revision/image and rollback, native resource/headroom +preflight and source-bound deployment diff. Obtain required scoped supervisor +disposition if hosting activation is outside the existing source-publication +approval. Execute through owner path; independently verify v0.8 accepted page, +old route supersession notice and immutableoldrevision survival. No permission +or PDP/runtime capability is introduced by publishing static guardrails.