Publish canon-derived build-mode guardrail source and evaluator
Some checks failed
Build and publish policy-nexus image / build-and-push (push) Failing after 1m21s
Some checks failed
Build and publish policy-nexus image / build-and-push (push) Failing after 1m21s
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
This commit is contained in:
parent
444b67e9ae
commit
f784f017b0
11 changed files with 359 additions and 4 deletions
2
Makefile
2
Makefile
|
|
@ -10,7 +10,7 @@ build:
|
||||||
|
|
||||||
check:
|
check:
|
||||||
python3 -m unittest discover -s tests -p 'test_*.py'
|
python3 -m unittest discover -s tests -p 'test_*.py'
|
||||||
python3 -m py_compile tools/render.py tools/build_site.py tools/check_currency.py tools/verify_release.py tools/source_inventory.py tools/fetch_sources.py tools/adr_review_ledger.py
|
python3 -m py_compile tools/render.py tools/build_site.py tools/check_currency.py tools/verify_release.py tools/source_inventory.py tools/fetch_sources.py tools/adr_review_ledger.py tools/guardrails.py
|
||||||
$(MAKE) source-audit
|
$(MAKE) source-audit
|
||||||
git diff --check
|
git diff --check
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"schema_version": "policy-nexus-guardrail-validation/v1",
|
||||||
|
"validated_at": "2026-10-11T19:08:06.581967+00:00",
|
||||||
|
"workplan": "PNEX-WP-0006",
|
||||||
|
"native_publication": false,
|
||||||
|
"task_closures": [],
|
||||||
|
"tests": {
|
||||||
|
"command": "python3 -m unittest discover -s tests -p 'test_*.py'",
|
||||||
|
"passed": 60,
|
||||||
|
"failed": 0
|
||||||
|
},
|
||||||
|
"compile": "passed",
|
||||||
|
"diff_whitespace": "passed",
|
||||||
|
"local_build": {
|
||||||
|
"document_count": 74,
|
||||||
|
"artifact_equals_canon_block": true,
|
||||||
|
"expiry_visible_in_header": true,
|
||||||
|
"source_sha256": "7135ad8e36041f6dc9633c029109b4751be275f912cec44fc462c9f55befaab8",
|
||||||
|
"artifact_sha256": "a86462a57bb1df5804d3088881af22569082d43ff68bcab2b67cbe56b6fc46cd"
|
||||||
|
},
|
||||||
|
"source_audit": {
|
||||||
|
"status": "failed_closed",
|
||||||
|
"unreviewed_source": "net-kingdom/canon/standards/security-layer-model_v0.9.md",
|
||||||
|
"unrelated_inventory_refreshed": false
|
||||||
|
},
|
||||||
|
"publication_lane": "red",
|
||||||
|
"pending_daily_batch": "PNEX-WP-0006: public page and canon-derived JSON artifact"
|
||||||
|
}
|
||||||
|
|
@ -139,3 +139,39 @@ matching source; publication stays one explicit entry per document.
|
||||||
| `net-kingdom-architecture` | `net-kingdom` | `net-kingdom` | `docs/architecture/net-kingdom_v0.1.md` |
|
| `net-kingdom-architecture` | `net-kingdom` | `net-kingdom` | `docs/architecture/net-kingdom_v0.1.md` |
|
||||||
| `state-hub-architecture` | `state-hub` | `state-hub` | `docs/architecture/state-hub_v0.1.md` |
|
| `state-hub-architecture` | `state-hub` | `state-hub` | `docs/architecture/state-hub_v0.1.md` |
|
||||||
| `policy-nexus-architecture` | `policy-nexus` | `policy-nexus` | `docs/architecture/policy-nexus_v0.1.md` |
|
| `policy-nexus-architecture` | `policy-nexus` | `policy-nexus` | `docs/architecture/policy-nexus_v0.1.md` |
|
||||||
|
|
||||||
|
## Machine-readable build-mode authorization
|
||||||
|
|
||||||
|
The build-mode standard's publication entry declares
|
||||||
|
`guardrail_path: guardrails/build-mode-change-authorization.json`. The builder
|
||||||
|
extracts its single machine-checkable YAML block from the canonical Markdown
|
||||||
|
and emits JSON at that stable path. No second policy copy is maintained. The
|
||||||
|
release manifest binds the artifact digest to the document's source provenance;
|
||||||
|
release verification rejects missing, changed or unsupported rule artifacts.
|
||||||
|
`expires_on` is shown in the document header.
|
||||||
|
|
||||||
|
A consumer can evaluate a change record with the stdlib-only checker:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 tools/guardrails.py build/guardrails/build-mode-change-authorization.json change.json --build-mode
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"categories": [],
|
||||||
|
"diff_ref": "reviewed.patch",
|
||||||
|
"rollback": "Restore the previous deployment image and source revision",
|
||||||
|
"abort_owner": "owning-repo",
|
||||||
|
"receipt_path": "execution-receipt.json"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Category declarations are mandatory, even when empty. The checker returns
|
||||||
|
`blue` only with all four nonempty preparation references and confirmed build
|
||||||
|
mode. Missing references, unknown categories or unconfirmed/expired build mode
|
||||||
|
return `incomplete`. Red overrides orange when both are declared. Only `blue`
|
||||||
|
returns exit code zero. Classification does not verify the referenced review or
|
||||||
|
grant credentials, autopilot, public publication approval or runtime authority.
|
||||||
|
Public publication goes in the founder's daily red-lane batch; orange identity
|
||||||
|
acts go in the attended-window checklist. Source generation and local checks can
|
||||||
|
proceed before publication approval.
|
||||||
|
|
|
||||||
|
|
@ -666,6 +666,15 @@
|
||||||
"canonical_path": "methods/risk-dependencies/v1/index.html",
|
"canonical_path": "methods/risk-dependencies/v1/index.html",
|
||||||
"revision_path": "methods/risk-dependencies/v1/revisions/{revision}/index.html",
|
"revision_path": "methods/risk-dependencies/v1/revisions/{revision}/index.html",
|
||||||
"review_interval": "6m"
|
"review_interval": "6m"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "canon-build-mode-change-authorization",
|
||||||
|
"source_repo": "the-custodian",
|
||||||
|
"source_path": "canon/standards/build-mode-change-authorization_v0.1.md",
|
||||||
|
"canonical_path": "standards/build-mode-change-authorization/v0.1/index.html",
|
||||||
|
"revision_path": "standards/build-mode-change-authorization/v0.1/revisions/{revision}/index.html",
|
||||||
|
"review_interval": "3m",
|
||||||
|
"guardrail_path": "guardrails/build-mode-change-authorization.json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"published_history": [
|
"published_history": [
|
||||||
|
|
|
||||||
|
|
@ -913,6 +913,12 @@
|
||||||
"source_path": "canon/standards/autonomy-lanes_v0.1.md",
|
"source_path": "canon/standards/autonomy-lanes_v0.1.md",
|
||||||
"source_repo": "the-custodian"
|
"source_repo": "the-custodian"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"source_repo": "the-custodian",
|
||||||
|
"source_path": "canon/standards/build-mode-change-authorization_v0.1.md",
|
||||||
|
"disposition": "published",
|
||||||
|
"reason": "Published through an explicit publication.json document entry."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"disposition": "metadata-pending",
|
"disposition": "metadata-pending",
|
||||||
"reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.",
|
"reason": "In scope; awaits explicit publication addressing and owner/revision/review metadata.",
|
||||||
|
|
|
||||||
120
tests/test_guardrails.py
Normal file
120
tests/test_guardrails.py
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT / 'tools'))
|
||||||
|
from guardrails import extract_rule, evaluate
|
||||||
|
from build_site import build, load_manifest
|
||||||
|
from test_publication import _fixture
|
||||||
|
from test_release import _release
|
||||||
|
from verify_release import verify
|
||||||
|
|
||||||
|
CANON_FIXTURE = '---\nid: canon-build-mode-change-authorization\ntitle: Build-Mode Change Authorization\nstatus: accepted\nrevision: accepted-1\nowner: the-custodian\nlast_reviewed: 2026-10-11\nreview_interval: 3m\nexpires_on: production-close-down\n---\n\n# Build-Mode Change Authorization\n\n```yaml\nrule: build-mode-change-authorization\nversion: 0.1\napplies_while: build-mode\nblue_lane_requires: # every field must be present on the change record\n - diff_ref # path or commit of reviewed diff / dry-run output\n - rollback # command or procedure reference\n - abort_owner # repo or agent name\n - receipt_path # where the execution receipt is written\nred_lane_categories:\n - destructive-deletion\n - access-scope-expansion\n - secret-scope-expansion\n - spend-ceiling\n - public-publication\norange_lane_categories:\n - founder-identity-act\n - platform-admin-login\n```\n'
|
||||||
|
|
||||||
|
|
||||||
|
class GuardrailTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.source = CANON_FIXTURE
|
||||||
|
self.rule = extract_rule(self.source)
|
||||||
|
self.record = {'categories': [], 'diff_ref': 'reviewed.patch', 'rollback': 'revert source/image',
|
||||||
|
'abort_owner': 'policy-nexus', 'receipt_path': 'receipt.json'}
|
||||||
|
|
||||||
|
def test_canon_block_contract(self):
|
||||||
|
self.assertEqual(self.rule, {
|
||||||
|
'rule': 'build-mode-change-authorization', 'version': 0.1,
|
||||||
|
'applies_while': 'build-mode',
|
||||||
|
'blue_lane_requires': ['diff_ref', 'rollback', 'abort_owner', 'receipt_path'],
|
||||||
|
'red_lane_categories': ['destructive-deletion', 'access-scope-expansion',
|
||||||
|
'secret-scope-expansion', 'spend-ceiling', 'public-publication'],
|
||||||
|
'orange_lane_categories': ['founder-identity-act', 'platform-admin-login']})
|
||||||
|
|
||||||
|
def test_blue_requires_complete_record_and_confirmed_build_mode(self):
|
||||||
|
self.assertEqual(evaluate(self.rule, self.record, build_mode=True), {'lane': 'blue'})
|
||||||
|
self.assertEqual(evaluate(self.rule, self.record, build_mode=False)['lane'], 'incomplete')
|
||||||
|
for key in self.rule['blue_lane_requires']:
|
||||||
|
for invalid in ['', ' ', None, True, [], {}]:
|
||||||
|
with self.subTest(key=key, value=invalid):
|
||||||
|
result = evaluate(self.rule, self.record | {key: invalid}, build_mode=True)
|
||||||
|
self.assertEqual(result['lane'], 'incomplete')
|
||||||
|
self.assertIn(key, result['missing'])
|
||||||
|
|
||||||
|
def test_all_red_and_orange_categories_and_red_precedence(self):
|
||||||
|
for lane in ('red', 'orange'):
|
||||||
|
for category in self.rule[f'{lane}_lane_categories']:
|
||||||
|
self.assertEqual(evaluate(self.rule, {'categories': [category]}, build_mode=True)['lane'], lane)
|
||||||
|
self.assertEqual(evaluate(self.rule, {'categories': ['public-publication', 'platform-admin-login']}, build_mode=True)['lane'], 'red')
|
||||||
|
|
||||||
|
def test_unknown_or_undeclared_categories_cannot_grant_blue(self):
|
||||||
|
for categories in [None, 'public-publication', [False], ['typo']]:
|
||||||
|
self.assertEqual(evaluate(self.rule, self.record | {'categories': categories}, build_mode=True)['lane'], 'incomplete')
|
||||||
|
|
||||||
|
def test_unsupported_yaml_duplicate_fields_and_duplicate_blocks_refused(self):
|
||||||
|
block = self.source[self.source.index('```yaml'):self.source.index('```', self.source.index('```yaml') + 7) + 3]
|
||||||
|
for source in [self.source + '\n' + block + '\n', self.source.replace('version: 0.1', 'version: 0.1\nversion: 0.1'),
|
||||||
|
self.source.replace(' - rollback', ' - rollback: &alias'),
|
||||||
|
self.source.replace('version: 0.1', 'version: 0.2')]:
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
extract_rule(source)
|
||||||
|
|
||||||
|
def test_build_published_artifact_equals_canon_and_expiry_is_prominent(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
manifest = _fixture(root)
|
||||||
|
data = json.loads(manifest.read_text())
|
||||||
|
document = data['documents'][0]
|
||||||
|
source = root / 'canon-repo' / document['source_path']
|
||||||
|
source.write_text(self.source)
|
||||||
|
document.update(id='canon-build-mode-change-authorization', guardrail_path='guardrails/build-mode-change-authorization.json')
|
||||||
|
manifest.write_text(json.dumps(data))
|
||||||
|
records = build(manifest, root / 'build')
|
||||||
|
artifact = root / 'build' / document['guardrail_path']
|
||||||
|
self.assertEqual(json.loads(artifact.read_text()), extract_rule(source.read_text()))
|
||||||
|
page = (root / 'build' / document['canonical_path']).read_text()
|
||||||
|
self.assertIn('Expires on: production-close-down', page[:page.index('</header>')])
|
||||||
|
self.assertIn('blue_lane_requires:', page)
|
||||||
|
self.assertIn('guardrail_digest', records[0])
|
||||||
|
self.assertIn('Build-Mode Change Authorization', (root / 'build/index.html').read_text())
|
||||||
|
|
||||||
|
def test_release_verifier_rejects_missing_tampered_or_invalid_guardrail(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
output = _release(root)
|
||||||
|
manifest_path = output / 'publication-manifest.json'
|
||||||
|
manifest = json.loads(manifest_path.read_text())
|
||||||
|
artifact = output / 'guardrails/rule.json'
|
||||||
|
artifact.parent.mkdir()
|
||||||
|
good = (json.dumps(self.rule) + '\n').encode()
|
||||||
|
artifact.write_bytes(good)
|
||||||
|
document = manifest['documents'][0]
|
||||||
|
document.update(guardrail_path='guardrails/rule.json', guardrail_digest=hashlib.sha256(good).hexdigest())
|
||||||
|
manifest_path.write_text(json.dumps(manifest))
|
||||||
|
verify(output)
|
||||||
|
artifact.write_bytes(good + b' ')
|
||||||
|
with self.assertRaisesRegex(ValueError, 'guardrail artifact digest'):
|
||||||
|
verify(output)
|
||||||
|
artifact.unlink()
|
||||||
|
with self.assertRaises(OSError):
|
||||||
|
verify(output)
|
||||||
|
invalid = json.dumps(self.rule | {'version': 99}).encode()
|
||||||
|
artifact.write_bytes(invalid)
|
||||||
|
document['guardrail_digest'] = hashlib.sha256(invalid).hexdigest()
|
||||||
|
manifest_path.write_text(json.dumps(manifest))
|
||||||
|
with self.assertRaisesRegex(ValueError, 'unsupported machine rule'):
|
||||||
|
verify(output)
|
||||||
|
|
||||||
|
def test_guardrail_path_collision_and_traversal_refused(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
manifest = _fixture(Path(directory))
|
||||||
|
original = json.loads(manifest.read_text())
|
||||||
|
for path in ['../outside.json', '/outside.json', 'example.html']:
|
||||||
|
data = json.loads(json.dumps(original))
|
||||||
|
data['documents'][0]['guardrail_path'] = path
|
||||||
|
manifest.write_text(json.dumps(data))
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
load_manifest(manifest)
|
||||||
|
|
@ -17,6 +17,7 @@ import tempfile
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from render import STYLE, render_page, split_frontmatter
|
from render import STYLE, render_page, split_frontmatter
|
||||||
|
from guardrails import extract_rule
|
||||||
|
|
||||||
|
|
||||||
SOURCE_REVISION = re.compile(
|
SOURCE_REVISION = re.compile(
|
||||||
|
|
@ -111,6 +112,7 @@ def load_manifest(path: Path) -> dict[str, Any]:
|
||||||
document["canonical_path"],
|
document["canonical_path"],
|
||||||
document["revision_path"].replace("{revision}", "revision"),
|
document["revision_path"].replace("{revision}", "revision"),
|
||||||
*document.get("legacy_paths", []),
|
*document.get("legacy_paths", []),
|
||||||
|
*([document["guardrail_path"]] if document.get("guardrail_path") else []),
|
||||||
*(document["revision_path"].format(revision=h["revision"])
|
*(document["revision_path"].format(revision=h["revision"])
|
||||||
for h in document.get("historical_revisions", [])),
|
for h in document.get("historical_revisions", [])),
|
||||||
):
|
):
|
||||||
|
|
@ -397,6 +399,22 @@ def build(
|
||||||
target.parent.mkdir(parents=True, exist_ok=True)
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
canonical_url = "/" + canonical.as_posix()
|
canonical_url = "/" + canonical.as_posix()
|
||||||
target.write_text(_redirect(canonical_url, meta["title"]), encoding="utf-8")
|
target.write_text(_redirect(canonical_url, meta["title"]), encoding="utf-8")
|
||||||
|
guardrail_metadata = {}
|
||||||
|
if document.get("guardrail_path"):
|
||||||
|
rule = extract_rule(source.read_text(encoding="utf-8"))
|
||||||
|
artifact_path = _safe_path(document["guardrail_path"])
|
||||||
|
if artifact_path.suffix != ".json":
|
||||||
|
raise ValueError("guardrail artifact must use a JSON path")
|
||||||
|
if artifact_path in archived_paths:
|
||||||
|
raise ValueError("guardrail conflicts with archived immutable path")
|
||||||
|
artifact_bytes = (json.dumps(rule, indent=2, sort_keys=True) + "\n").encode()
|
||||||
|
target = temporary / artifact_path
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
target.write_bytes(artifact_bytes)
|
||||||
|
guardrail_metadata = {
|
||||||
|
"guardrail_path": artifact_path.as_posix(),
|
||||||
|
"guardrail_digest": hashlib.sha256(artifact_bytes).hexdigest(),
|
||||||
|
}
|
||||||
records.append(
|
records.append(
|
||||||
{
|
{
|
||||||
"id": document["id"],
|
"id": document["id"],
|
||||||
|
|
@ -414,6 +432,7 @@ def build(
|
||||||
"source_path": document["source_path"],
|
"source_path": document["source_path"],
|
||||||
"source_revision": source_revision,
|
"source_revision": source_revision,
|
||||||
"source_digest": source_digest,
|
"source_digest": source_digest,
|
||||||
|
**guardrail_metadata,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
98
tools/guardrails.py
Normal file
98
tools/guardrails.py
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Publish and evaluate the restricted machine-checkable canon rule, fail closed.
|
||||||
|
|
||||||
|
This deliberately accepts only scalar mappings and block lists used by the canon
|
||||||
|
contract. Unsupported YAML, unknown categories and malformed records never grant
|
||||||
|
execution. Lane classification is not a credential or runtime approval token.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
from render import split_frontmatter
|
||||||
|
|
||||||
|
|
||||||
|
def extract_rule(source: str) -> dict:
|
||||||
|
_meta, body = split_frontmatter(source)
|
||||||
|
blocks = re.findall(r'^```yaml\n(.*?)^```\s*$', body, re.M | re.S)
|
||||||
|
candidates = [b for b in blocks if re.search(r'^rule: build-mode-change-authorization\s*$', b, re.M)]
|
||||||
|
if len(candidates) != 1:
|
||||||
|
raise ValueError('expected exactly one build-mode authorization YAML block')
|
||||||
|
rule = {}
|
||||||
|
current = None
|
||||||
|
for line in candidates[0].splitlines():
|
||||||
|
text = line.split('#', 1)[0].rstrip()
|
||||||
|
if not text.strip():
|
||||||
|
continue
|
||||||
|
item = re.fullmatch(r' - ([a-z][a-z0-9_-]*)', text)
|
||||||
|
if item and current and isinstance(rule[current], list):
|
||||||
|
rule[current].append(item[1])
|
||||||
|
continue
|
||||||
|
field = re.fullmatch(r'([a-z][a-z0-9_]*):(?: ([a-z0-9_.-]+))?', text)
|
||||||
|
if not field or field[1] in rule:
|
||||||
|
raise ValueError('unsupported or duplicate machine rule YAML field')
|
||||||
|
current = field[1]
|
||||||
|
value = field[2]
|
||||||
|
rule[current] = [] if value is None else float(value) if re.fullmatch(r'\d+\.\d+', value) else value
|
||||||
|
validate_rule(rule)
|
||||||
|
return rule
|
||||||
|
|
||||||
|
|
||||||
|
def validate_rule(rule: dict) -> None:
|
||||||
|
fields = {'rule', 'version', 'applies_while', 'blue_lane_requires', 'red_lane_categories', 'orange_lane_categories'}
|
||||||
|
if not isinstance(rule, dict) or set(rule) != fields:
|
||||||
|
raise ValueError('machine rule fields differ from supported contract')
|
||||||
|
if (rule['rule'] != 'build-mode-change-authorization' or rule['version'] != 0.1
|
||||||
|
or rule['applies_while'] != 'build-mode'):
|
||||||
|
raise ValueError('unsupported machine rule identity, version or lifetime')
|
||||||
|
for name in ('blue_lane_requires', 'red_lane_categories', 'orange_lane_categories'):
|
||||||
|
values = rule[name]
|
||||||
|
if (not isinstance(values, list) or not values
|
||||||
|
or any(not isinstance(v, str) or not re.fullmatch(r'[a-z][a-z0-9_-]*', v) for v in values)
|
||||||
|
or len(values) != len(set(values))):
|
||||||
|
raise ValueError(f'invalid {name}')
|
||||||
|
if set(rule['red_lane_categories']) & set(rule['orange_lane_categories']):
|
||||||
|
raise ValueError('lane categories overlap')
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate(rule: dict, record: dict, *, build_mode: bool) -> dict:
|
||||||
|
validate_rule(rule)
|
||||||
|
if not isinstance(record, dict):
|
||||||
|
raise ValueError('change record must be an object')
|
||||||
|
if not build_mode:
|
||||||
|
return {'lane': 'incomplete', 'reason': 'standing grant expired or build mode unconfirmed'}
|
||||||
|
categories = record.get('categories')
|
||||||
|
if not isinstance(categories, list) or any(not isinstance(c, str) for c in categories):
|
||||||
|
return {'lane': 'incomplete', 'reason': 'categories must be an explicit list'}
|
||||||
|
known = set(rule['red_lane_categories']) | set(rule['orange_lane_categories'])
|
||||||
|
unknown = sorted(set(categories) - known)
|
||||||
|
if unknown:
|
||||||
|
return {'lane': 'incomplete', 'unknown_categories': unknown}
|
||||||
|
for lane in ('red', 'orange'):
|
||||||
|
declared = sorted(set(categories) & set(rule[f'{lane}_lane_categories']))
|
||||||
|
if declared:
|
||||||
|
return {'lane': lane, 'categories': declared}
|
||||||
|
missing = [key for key in rule['blue_lane_requires']
|
||||||
|
if not isinstance(record.get(key), str) or not record[key].strip()]
|
||||||
|
return {'lane': 'incomplete', 'missing': missing} if missing else {'lane': 'blue'}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument('rule', type=Path, help='published JSON rule artifact')
|
||||||
|
parser.add_argument('record', type=Path, help='JSON change record')
|
||||||
|
parser.add_argument('--build-mode', action='store_true', help='confirm build mode has not closed down')
|
||||||
|
args = parser.parse_args()
|
||||||
|
try:
|
||||||
|
result = evaluate(json.loads(args.rule.read_text()), json.loads(args.record.read_text()), build_mode=args.build_mode)
|
||||||
|
except (ValueError, OSError, TypeError) as exc:
|
||||||
|
parser.error(str(exc))
|
||||||
|
print(json.dumps(result, sort_keys=True))
|
||||||
|
return 0 if result['lane'] == 'blue' else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -379,6 +379,11 @@ def render_page(
|
||||||
if review_due
|
if review_due
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
|
expiry_line = (
|
||||||
|
'<p class="rule-quote"><strong>Expires on: '
|
||||||
|
f'{html.escape(meta["expires_on"])}</strong></p>'
|
||||||
|
if meta.get("expires_on") else ""
|
||||||
|
)
|
||||||
lifecycle = publication.get("lifecycle", "active")
|
lifecycle = publication.get("lifecycle", "active")
|
||||||
successor = publication.get("successor", "")
|
successor = publication.get("successor", "")
|
||||||
lifecycle_notice = ""
|
lifecycle_notice = ""
|
||||||
|
|
@ -428,6 +433,7 @@ def render_page(
|
||||||
+ (f'<p class="sub">{html.escape(subtitle)}</p>' if subtitle else "")
|
+ (f'<p class="sub">{html.escape(subtitle)}</p>' if subtitle else "")
|
||||||
+ source_line
|
+ source_line
|
||||||
+ review_line
|
+ review_line
|
||||||
|
+ expiry_line
|
||||||
+ '</header><div class="layout">'
|
+ '</header><div class="layout">'
|
||||||
f'<nav class="rail" aria-label="Sections"><ol>{rail_html}</ol></nav>'
|
f'<nav class="rail" aria-label="Sections"><ol>{rail_html}</ol></nav>'
|
||||||
f"<main>{lifecycle_notice}{currency_notice}{body}"
|
f"<main>{lifecycle_notice}{currency_notice}{body}"
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,12 @@ def verify(build: Path) -> dict[str, Any]:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"{document_id}: immutable revision page was not built from a clean Git commit"
|
f"{document_id}: immutable revision page was not built from a clean Git commit"
|
||||||
)
|
)
|
||||||
|
if document.get("guardrail_path"):
|
||||||
|
artifact = build / _safe_relative(document["guardrail_path"])
|
||||||
|
if hashlib.sha256(artifact.read_bytes()).hexdigest() != document.get("guardrail_digest"):
|
||||||
|
raise ValueError(f"{document_id}: guardrail artifact digest differs from manifest")
|
||||||
|
from guardrails import validate_rule
|
||||||
|
validate_rule(json.loads(artifact.read_text(encoding="utf-8")))
|
||||||
verified.append(document_id)
|
verified.append(document_id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ type: workplan
|
||||||
title: "Publish the build-mode change authorization guardrail"
|
title: "Publish the build-mode change authorization guardrail"
|
||||||
domain: infotech
|
domain: infotech
|
||||||
repo: policy-nexus
|
repo: policy-nexus
|
||||||
status: ready
|
status: blocked
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: custodian
|
topic_slug: custodian
|
||||||
created: "2026-10-11"
|
created: "2026-10-11"
|
||||||
|
|
@ -23,8 +23,10 @@ repo's intent: publication only, the content stays in canon.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: PNEX-WP-0006-T01
|
id: PNEX-WP-0006-T01
|
||||||
status: todo
|
status: wait
|
||||||
priority: high
|
priority: high
|
||||||
|
needs_human: true
|
||||||
|
intervention_note: "Public publication is red-lane; add PNEX-WP-0006 to the single daily founder batch. Local code and tests are prepared; no per-item approval request."
|
||||||
state_hub_task_id: "ee3c6f88-91b5-5dac-94e7-d1b05b0276bc"
|
state_hub_task_id: "ee3c6f88-91b5-5dac-94e7-d1b05b0276bc"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -35,8 +37,10 @@ machine-checkable YAML block intact. Show `expires_on: production-close-down` pr
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: PNEX-WP-0006-T02
|
id: PNEX-WP-0006-T02
|
||||||
status: todo
|
status: wait
|
||||||
priority: high
|
priority: high
|
||||||
|
needs_human: true
|
||||||
|
intervention_note: "Public publication is red-lane; add PNEX-WP-0006 to the single daily founder batch. Local code and tests are prepared; no per-item approval request."
|
||||||
state_hub_task_id: "92247888-82ad-52ca-962f-1a578adeb9f7"
|
state_hub_task_id: "92247888-82ad-52ca-962f-1a578adeb9f7"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -44,3 +48,26 @@ Serve the YAML block as a stable JSON/YAML artifact (for example
|
||||||
`/guardrails/build-mode-change-authorization.json`). CI checks in other repos and access-engine
|
`/guardrails/build-mode-change-authorization.json`). CI checks in other repos and access-engine
|
||||||
can then fetch and evaluate it deterministically. Add a test that the published artifact equals
|
can then fetch and evaluate it deterministically. Add a test that the published artifact equals
|
||||||
the canon block.
|
the canon block.
|
||||||
|
|
||||||
|
## Implementation and publication boundary (2026-10-11)
|
||||||
|
|
||||||
|
T01/T02 source implementation is prepared: canonical document inventory/index
|
||||||
|
entry, visible expiry, canon YAML-derived JSON artifact, digest-bound release
|
||||||
|
verification, and fail-closed local evaluator. Local checks pass; the native
|
||||||
|
public page/artifact have not been published and neither task is closed.
|
||||||
|
Validation receipt: `docs/evidence/2026-10-11-build-mode-guardrail/validation.json`.
|
||||||
|
|
||||||
|
Daily red-lane package: publish only the new build-mode standard and its derived
|
||||||
|
JSON artifact after founder batch disposition and clean source provenance.
|
||||||
|
Expected addresses: `/standards/build-mode-change-authorization/v0.1/` and
|
||||||
|
`/guardrails/build-mode-change-authorization.json`. Abort owner: policy-nexus.
|
||||||
|
Rollback: restore the previously deployed image/source revision; retain all
|
||||||
|
previous immutable publication paths. Verification: canonical page expiry/YAML,
|
||||||
|
JSON equality with the approved canon block, artifact digest and installed
|
||||||
|
source revision. No native action, access-scope change or new spend is authorized
|
||||||
|
by local preparation.
|
||||||
|
|
||||||
|
Full source audit also detects the unrelated unreviewed
|
||||||
|
`net-kingdom/canon/standards/security-layer-model_v0.9.md`. Its owning source
|
||||||
|
publication disposition must be resolved before the release gate passes; this
|
||||||
|
workplan does not silently admit it or edit another owner's standard.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue