Prepare accepted layer v0.8 publication and preserve immutable ADR history
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
This commit is contained in:
parent
bfef142d4c
commit
5c41564f04
5 changed files with 307 additions and 2 deletions
|
|
@ -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"
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
64
workplans/PNEX-WP-0004-security-layer-v08-publication.md
Normal file
64
workplans/PNEX-WP-0004-security-layer-v08-publication.md
Normal file
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue