From acfd93fc863f62db67365d37820d8e31419cd818 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 29 Aug 2026 12:49:24 +0200 Subject: [PATCH] feat: finish ZONE-WP-0003 Engine/PIP freeze and claim mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Declare the layer in layer.yaml, check it against INTENT.md, and fail make check on a new Tooling client or HTTP decision surface. Record the six statute §10 artifacts for the 2026-08-23 cut, name access-engine on the README, and offer a non-schema PIP field mapping to Taxonomy. Assistant: grok Assistant-Session: 01a04ceb-0745-7ae1-9e26-0d10e5d52b8b --- INTENT.md | 3 +- Makefile | 7 +- README.md | 16 +- SCOPE.md | 22 +- WORK-RECORDS.md | 10 +- docs/pip-claim-boundary.md | 72 +++++ history/2026-08-29-layer-change-artifacts.md | 97 ++++++ layer.yaml | 65 ++++ tests/test_layer_conformance.py | 106 +++++++ tools/check_layer_conformance.py | 293 ++++++++++++++++++ ...-WP-0003-security-layer-model-alignment.md | 26 +- 11 files changed, 682 insertions(+), 35 deletions(-) create mode 100644 docs/pip-claim-boundary.md create mode 100644 history/2026-08-29-layer-change-artifacts.md create mode 100644 layer.yaml create mode 100644 tests/test_layer_conformance.py create mode 100644 tools/check_layer_conformance.py diff --git a/INTENT.md b/INTENT.md index 1c6e2f4..62aa122 100644 --- a/INTENT.md +++ b/INTENT.md @@ -34,7 +34,8 @@ companion: net-kingdom/SECURITY-COMPANION.md > surface, or stance compiled into membership would each be a layer change, > and a layer change needs the six artifacts in statute §10, including a > permission freeze. The 2026-08-23 cut already happened; it does not reopen -> by practice. +> by practice. The six statute §10 artifacts are gathered in +> `history/2026-08-29-layer-change-artifacts.md`. > > **`access-engine` is the only PDP.** It is the ruled name for the repository > currently called `flex-auth`. This file's original §5 — *"flex-auth is the diff --git a/Makefile b/Makefile index a739323..99ab92d 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,13 @@ -.PHONY: canon-lineage check exception-fixtures reference-fixtures test +.PHONY: canon-lineage check exception-fixtures layer-conformance reference-fixtures test -check: test reference-fixtures exception-fixtures +check: test reference-fixtures exception-fixtures layer-conformance test: PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -v +layer-conformance: + PYTHONDONTWRITEBYTECODE=1 python3 tools/check_layer_conformance.py + reference-fixtures: PYTHONDONTWRITEBYTECODE=1 python3 tools/resolve_zones.py --manifest fixtures/manifests/reference.yaml --control-profile profiles/netkingdom-build-v0.1.yaml >/dev/null diff --git a/README.md b/README.md index eb51267..3871f96 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ # zone-engine -Offline reference conformance for NetKingdom **security zones**. This repository -validates workload membership and admission, projects only explicit -owner-versioned control profiles, checks time-boxed exception fixtures, and -verifies the lineage of the canonical standard. +Engine-layer **PIP** for NetKingdom **security-zone** identity and membership, +retained as offline reference conformance. This repository validates workload +membership and admission, projects only explicit owner-versioned control +profiles, checks time-boxed exception fixtures, and verifies the lineage of +the canonical standard. -It is not a service or policy decision point. Canon is published by -`net-kingdom`; flex-auth and each enforcement-point owner retain live policy -authority. +It is not a live engine, not a PEP, and not a policy decision point. Canon is +published by `net-kingdom`. `access-engine` (currently `flex-auth`) is the +only PDP; each enforcement-point owner retains live policy and failure-mode +authority. Layer declaration: `INTENT.md` frontmatter and `layer.yaml`. ## Checks diff --git a/SCOPE.md b/SCOPE.md index 1415415..4d6098b 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -28,10 +28,11 @@ Declared in `INTENT.md` frontmatter, in this repository's own voice: | Tooling contacts | none | | Conformance | conforming for Tooling contact; the live Engine API is not a missing capability this repository currently owes | -The machine-readable `layer.yaml` form and a conformance check that fails a -new Tooling client or a new decision surface are not yet evidenced here. -Until they are, the declaration is the `INTENT.md` frontmatter and the prose -in that file. +The machine-readable declaration is `layer.yaml`. `tools/check_layer_conformance.py` +fails if that file disagrees with `INTENT.md` frontmatter, if a Tooling-layer +client appears under `tools/`, or if an HTTP authorization decision surface +appears. The six statute §10 artifacts for the already-completed layer cut +are `history/2026-08-29-layer-change-artifacts.md`. `access-engine` (currently `flex-auth`) is the only PDP. Zone stance and failure mode remain owner policy and PEP configuration. This repository @@ -82,8 +83,8 @@ This repository provides: - an offline exception checker with an explicit evaluation instant, covering grant authority, maximum duration, exclusive expiry, overlap, renewal, wildcard rejection, and durable-authority bounds; and -- reusable manifests, profiles, exception fixtures, evidence, and 29 unit - tests. +- reusable manifests, profiles, exception fixtures, evidence, and unit + tests covering resolver, exceptions, lineage, and layer conformance. The resolver consumes already-located declarations and already-resolved workload-reference projections. It does not discover a fleet, repair an @@ -91,9 +92,10 @@ ambiguous join, read a live clock, or publish a consumer registry. Its profile fixture records owner-approved v0.1 behavior; only the referenced owner policy and PEP configuration can change a live effect. -The resolver output is a reference membership/admission record. It is not yet -a statute §17 request-claim. Taxonomy owns that schema and has not published -it. This repository must not ship a competing dialect. +The resolver output is a reference membership/admission record. It is not a +statute §17 request-claim. Taxonomy owns that schema and has not published +it. The field mapping offered to Taxonomy is `docs/pip-claim-boundary.md`. +This repository must not ship a competing dialect. ## Authority boundary @@ -140,6 +142,8 @@ decision point. - Reproducing or extending v0.1 conformance fixtures. - Checking direct declarations or explicit Repo Manager projections offline. +- Mapping those membership facts onto a future request-claim + (`docs/pip-claim-boundary.md`) without shipping a schema. - Verifying a versioned owner profile is total and fully attributed. - Testing an owner exception implementation against the lifecycle boundaries. - Reviewing a net-kingdom canon lifecycle/content change, including the diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 84b843b..04dbddb 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -10,7 +10,7 @@ | --- | --- | --- | --- | --- | | workplan | ZONE-WP-0001 | finished | — | workplans/ZONE-WP-0001-security-zone-model.md | | workplan | ZONE-WP-0002 | finished | — | workplans/ZONE-WP-0002-reference-contract-hardening-and-retirement.md | -| workplan | ZONE-WP-0003 | ready | — | workplans/ZONE-WP-0003-security-layer-model-alignment.md | +| workplan | ZONE-WP-0003 | finished | — | workplans/ZONE-WP-0003-security-layer-model-alignment.md | | task | ZONE-WP-0001-T01 | done | — | workplans/ZONE-WP-0001-security-zone-model.md | | task | ZONE-WP-0001-T02 | done | — | workplans/ZONE-WP-0001-security-zone-model.md | | task | ZONE-WP-0001-T03 | done | — | workplans/ZONE-WP-0001-security-zone-model.md | @@ -25,9 +25,9 @@ | task | ZONE-WP-0002-T05 | done | — | workplans/ZONE-WP-0002-reference-contract-hardening-and-retirement.md | | task | ZONE-WP-0002-T06 | done | — | workplans/ZONE-WP-0002-reference-contract-hardening-and-retirement.md | | task | ZONE-WP-0002-T07 | done | — | workplans/ZONE-WP-0002-reference-contract-hardening-and-retirement.md | -| task | ZONE-WP-0003-T01 | todo | — | workplans/ZONE-WP-0003-security-layer-model-alignment.md | +| task | ZONE-WP-0003-T01 | done | — | workplans/ZONE-WP-0003-security-layer-model-alignment.md | | task | ZONE-WP-0003-T02 | done | — | workplans/ZONE-WP-0003-security-layer-model-alignment.md | -| task | ZONE-WP-0003-T03 | todo | — | workplans/ZONE-WP-0003-security-layer-model-alignment.md | -| task | ZONE-WP-0003-T04 | todo | — | workplans/ZONE-WP-0003-security-layer-model-alignment.md | -| task | ZONE-WP-0003-T05 | todo | — | workplans/ZONE-WP-0003-security-layer-model-alignment.md | +| task | ZONE-WP-0003-T03 | done | — | workplans/ZONE-WP-0003-security-layer-model-alignment.md | +| task | ZONE-WP-0003-T04 | done | — | workplans/ZONE-WP-0003-security-layer-model-alignment.md | +| task | ZONE-WP-0003-T05 | done | — | workplans/ZONE-WP-0003-security-layer-model-alignment.md | | intake | ZONE-IN-0001 | closed | — | intakes/intakes.md | diff --git a/docs/pip-claim-boundary.md b/docs/pip-claim-boundary.md new file mode 100644 index 0000000..596fa06 --- /dev/null +++ b/docs/pip-claim-boundary.md @@ -0,0 +1,72 @@ +# PIP claim boundary — zone identity and membership + +This note maps the facts this Engine/PIP already emits onto the facts a +future request-claim must carry. It is **not** a schema, not a wire format, +and not estate canon. + +Statute §17 assigns the request-claim schema to Taxonomy and records that +ownership as proposed, not assigned. Statute §18 says zone stance enters a +decision as a **claim on the request or a rule in a versioned +`access-engine` policy package**, never as compiled registry content. +Inventing a competing dialect here is the drift §17 exists to prevent. + +Offered to `gate-house` / `net-kingdom` (Taxonomy publication) and to +`access-engine` (currently `flex-auth`, the consumer of the claim). Assent +is not required to keep the 2026-08-23 freeze. + +## Facts this PIP owns today + +Emitted by `tools/resolve_zones.py` as a reference membership/admission +record, given already-located declarations and already-resolved Repo +Manager projections: + +| Field | Meaning | +| --- | --- | +| `identity_bindings` | Authoritative workload identity bindings | +| `workload_ref` | Explicit workload reference (`rapp_id`, name, deployable, applicability) | +| `declared_zone` | Responsible party's declared membership | +| `effective_zone` | Admitted zone, or `unknown` / none | +| `admission` | `satisfied`, `unsatisfied`, `unknown`, or `not-applicable` | +| `admission_reason` | Why that result, without inference | +| `membership_revision` | Digest bound to identity, membership, workload ref, and source revision | +| `source` | Declaration path or projection source | +| `source_revision` | Caller-supplied source revision; absence yields no source-bound digest | +| `guarantees` | What the record actually promises (non-inference, source binding, …) | + +Optional control-profile projection (`policy_owner`, `pep_owner`, +`policy_ref`, stance, failure mode) is **not** a PIP fact. It is owner data +copied through for evidence, and only the referenced owner policy or PEP +configuration may change a live effect. + +Input `zones.reviewed` / `zones.review_due` are validated and are not +currently copied onto the output record. Freshness the resolver *does* +bind is `source_revision` via `membership_revision`. + +## Mapping onto a future request-claim + +Until Taxonomy publishes the schema, this is the contribution, not a type: + +| Claim slot (statute §17) | This PIP's field | Must not carry | +| --- | --- | --- | +| Issuer | this repository, plus `source` / `source_revision` | a second PDP identity | +| Subject | `identity_bindings` and `workload_ref` | path, repo, reef, actor, or lane as a substitute subject | +| Zone membership | `declared_zone`, `effective_zone`, `admission`, `admission_reason` | stance, failure mode, allow/deny | +| Freshness | `source_revision` and `membership_revision` | an implicit "current" with no revision | +| Effect of the zone | — | **not a PIP field**; `access-engine` policy package or a versioned rule (statute §18) | + +A decision that turned on a zone must name the policy-package version that +read it. Membership may be compiled into the registry `access-engine` +already loads only as a **fact**. Compiled data that determines an outcome +is still deciding (statute §6.1). + +## Forbidden + +- Shipping a zone-engine claim JSON Schema, protobuf, or `/claims` endpoint. +- Encoding stance or failure mode as if they were membership. +- Compiling these facts into registry content that changes allow/deny + without a policy-package revision. +- Treating a missing claim as permission. `unknown` and `not-applicable` + stay distinct; absence is not `z0`. + +The freeze in `history/2026-08-29-layer-change-artifacts.md` holds while +this mapping waits on Taxonomy. diff --git a/history/2026-08-29-layer-change-artifacts.md b/history/2026-08-29-layer-change-artifacts.md new file mode 100644 index 0000000..40a4913 --- /dev/null +++ b/history/2026-08-29-layer-change-artifacts.md @@ -0,0 +1,97 @@ +--- +id: ZONE-LAYER-CHANGE-2026-08-23 +type: layer-change-record +title: "Statute §10 artifacts for the 2026-08-23 zone-engine layer cut" +status: accepted +recorded: "2026-08-29" +standard: net-kingdom/canon/standards/security-layer-model_v0.7.md +--- + +# Statute §10 artifacts — the 2026-08-23 layer cut + +Statute §10 writes the layer-change procedure from this repository's case: +the runtime hypothesis was falsified, and the Engine remained as offline +reference conformance. The six artifacts are gathered here so a reviewer +does not have to reconstruct them from workplan history. + +A later live API, decision surface, or compiled stance would be a **new** +cut and would need its own six artifacts plus assent from every repository +whose boundary moves. This record does not authorize that. + +## 1. Before / after `INTENT.md` + +| | Before (through 2026-08-23) | After (2026-08-29) | +| --- | --- | --- | +| Desired sentence | Not yet sayable | Sayable; standard adopted | +| Runtime | Hypothesis under test | Falsified; cheaper answer taken | +| Layer | Unstated in this repository's voice | Engine, role PIP | +| Surface | Assumed an engine might be needed | Offline reference conformance | +| PDP line | `flex-auth` is the only decision point | Same ruling; ruled name `access-engine` (currently `flex-auth`) | + +Before-state: `history/2026-08-23-retain-reference-decision.md`, +`history/2026-08-23-scope-against-intent.md`, and the 2026-08-23 body of +`INTENT.md` retained as design history. + +After-state: `INTENT.md` frontmatter (`layer: Engine`, `role: PIP`) and the +2026-08-29 declaration prose. Machine-readable twin: `layer.yaml`. + +## 2. Client inventory + +| Contact | Before | After | +| --- | --- | --- | +| Tooling-layer clients (`OpenBao`, `key-cape`, datastore) | none | none | +| HTTP authorization decision surface | none | none | +| Live Engine API | none | none | +| Python tools | offline resolver, exception checker, lineage checker | same | +| Non-Tooling | State Hub work records via CLI / agent sessions | same; recorded in `layer.yaml` | + +There was no Tooling client to retire and none to add. `tools/` imports the +standard library and PyYAML only. + +## 3. Gap inventory + +| Kind | Before | After | +| --- | --- | --- | +| §5.3 declared Tooling contacts | none | none | +| Unowned capabilities this repository currently owes | none | none | +| Live Engine API | not owed; catalog records the offline form | still not owed | + +A missing live API is not a §5.3 gap. Closing it in place would reverse the +cut this record describes. + +## 4. Assent list + +No new boundary moved on 2026-08-29. The cut of 2026-08-23 was already +assented in practice by the two consuming owners: + +| Repository | Evidence | Boundary | +| --- | --- | --- | +| flex-auth (`access-engine`) | `FLEX-WP-0016` adoption; profile owner in `profiles/netkingdom-build-v0.1.yaml`; review of ZONE-WP-0001 | remains the only PDP; stance stays in its policy package | +| ops-warden | retired `policy.enabled`; PEP failure modes; adoption evidence 2026-08-22/23 | consumes membership; owns unreachable-engine stance | +| net-kingdom | published `security-zones_v0.1` (still `proposed`) | canon publication | +| gate-house | §4 catalog row names the offline disposition; `ZONE-IN-0001` requested the own-voice declaration | layer model | + +A future live surface would require fresh assent from at least +`access-engine` and `ops-warden`. + +## 5. State-migration decision + +`history/2026-08-23-retain-reference-decision.md`. There was no live state +to migrate: no API, no store, no consumer registry. Consumers already read +declarations in owning repos and policy in owner packages. Retention is of +fixtures, lineage, and offline tools. + +## 6. Permission freeze + +Until a later cut carries its own six artifacts: + +- no live API, daemon, database, controller, scheduler, or reload path; +- no authorization decision surface and no cached verdict; +- no stance compiled into membership, and no local profile change that + alters a live effect; +- no PEP, and no unreachable-engine stance map published from this + repository; +- no Tooling-layer client. + +`tools/check_layer_conformance.py` is the freeze's check: a new OpenBao +import or an `/authorize` helper fails `make check`. diff --git a/layer.yaml b/layer.yaml new file mode 100644 index 0000000..4be174b --- /dev/null +++ b/layer.yaml @@ -0,0 +1,65 @@ +# zone-engine — NetKingdom security layer declaration +# +# Framework: net-kingdom/canon/standards/security-layer-model_v0.7.md +# Companion: net-kingdom/SECURITY-COMPANION.md v0.2 +# Declaration: INTENT.md (this repository's own voice, §11) +# Validate: python3 tools/check_layer_conformance.py +# +# §11 requires a machine-readable form because prose cannot distinguish a +# declaration from a transcribed review. Shape adapted from kings-guard's +# no-contact declaration, offered estate-wide under §11. +# +# Catalogued as Engine / PIP for zone identity and membership, offline +# reference conformance per the 2026-08-23 disposition. There is no +# Tooling contact, no PEP, and no authorization decision surface. + +schema_version: "0.1" +framework: netkingdom-security-layer-model +standard_version: "0.7" +repository: zone-engine +layer: engine +role: pip +declared_by: INTENT.md +declared_at: "2026-08-29" + +# §4 catalog entry, transcribed so drift is visible. The standard is +# authoritative for the row; this records what we understand ourselves +# to have been assigned. +catalog_entry: + owns: + - zone identity and membership + form: offline reference conformance (2026-08-23 disposition) + pep_shaped: false + +# §5 / §11: every direct contact with a Tooling-layer system (a §4 Tooling +# row), one entry each. Empty is a claim. tools/check_layer_conformance.py +# fails if a Tooling client or a decision surface appears under tools/. +tooling_contacts: [] + +# No pep_stance path. This repository causes no protected side effect. + +# §11 requires non-Tooling clients to be recorded so the check is total. +# There is no library client in this repository. Work-record writes happen +# through the State Hub CLI / agent sessions, not through tools/. +non_tooling_clients: + - id: state-hub-work-records + target: state-hub + layer: not-catalogued + operation: "HTTP via statehub CLI / agent sessions for work records and progress events" + write: true + in_tree: false + note: >- + Outside §5 by the v0.5 scope rule: "Tooling-layer system" means a §4 + Tooling row, and state-hub is not one. Recorded, not policed. Carries no + security authority and no secret payload. No import of a State Hub + client exists under tools/. + +# A live Engine API is not an unowned capability this repository currently +# owes. The catalog records the offline form. Growing one would be a §10 +# layer change, not a gap to close in place. +unowned_capabilities: [] + +declared_shapes: + "5.1": [] + "5.2": [] + "5.3": [] diff --git a/tests/test_layer_conformance.py b/tests/test_layer_conformance.py new file mode 100644 index 0000000..e1cbfbe --- /dev/null +++ b/tests/test_layer_conformance.py @@ -0,0 +1,106 @@ +from pathlib import Path +from tempfile import TemporaryDirectory +import unittest + +import yaml + +from tools import check_layer_conformance as layer + + +INTENT = """--- +layer: Engine +role: PIP +standard: netkingdom-security-layer-model +standard_version: "0.7" +--- + +# INTENT +""" + +DECL = { + "layer": "engine", + "role": "pip", + "repository": "zone-engine", + "standard_version": "0.7", + "tooling_contacts": [], + "unowned_capabilities": [], +} + + +def _write_tree(root: Path, *, decl=None, intent=None, tools=None) -> None: + (root / "layer.yaml").write_text(yaml.safe_dump(decl if decl is not None else DECL)) + (root / "INTENT.md").write_text(intent if intent is not None else INTENT) + tools_dir = root / "tools" + tools_dir.mkdir() + (tools_dir / "noop.py").write_text("import json\n") + if tools: + for name, body in tools.items(): + (tools_dir / name).write_text(body) + + +class LayerDeclarationTest(unittest.TestCase): + def test_declaration_exists_and_matches_intent(self): + decl = yaml.safe_load(layer.DECL.read_text()) + intent = layer.load_intent() + self.assertEqual(decl["repository"], "zone-engine") + self.assertEqual(decl["layer"], "engine") + self.assertEqual(decl["role"], "pip") + self.assertEqual(decl["standard_version"], "0.7") + self.assertEqual(decl["tooling_contacts"], []) + self.assertIsNone(decl.get("pep_stance")) + self.assertEqual(intent["layer"].lower(), "engine") + self.assertEqual(intent["role"].lower(), "pip") + self.assertEqual(layer.check_declaration_agrees(decl, intent), []) + + def test_checker_passes_on_the_real_tree(self): + code, errors, _reports = layer.evaluate() + self.assertEqual(code, 0, errors) + self.assertEqual(errors, []) + + +class LayerCheckerFailureTest(unittest.TestCase): + def test_missing_declaration_is_exit_2(self): + with TemporaryDirectory() as directory: + root = Path(directory) + (root / "INTENT.md").write_text(INTENT) + (root / "tools").mkdir() + code, errors, _ = layer.evaluate(root=root) + self.assertEqual(code, 2) + self.assertTrue(any("no declaration" in item for item in errors)) + + def test_intent_disagreement_is_exit_2(self): + with TemporaryDirectory() as directory: + root = Path(directory) + _write_tree(root, intent=INTENT.replace("Engine", "Staff").replace("PIP", "null")) + code, errors, _ = layer.evaluate(root=root) + self.assertEqual(code, 2) + self.assertTrue(any("disagrees" in item for item in errors)) + + def test_undeclared_tooling_client_is_exit_1(self): + with TemporaryDirectory() as directory: + root = Path(directory) + _write_tree(root, tools={"vault.py": "import hvac\n\nclient = hvac.Client()\n"}) + code, errors, _ = layer.evaluate(root=root) + self.assertEqual(code, 1) + self.assertTrue(any("hvac" in item for item in errors)) + + def test_http_decision_surface_is_exit_1(self): + with TemporaryDirectory() as directory: + root = Path(directory) + _write_tree( + root, + tools={ + "api.py": ( + "import fastapi\n\n" + "app = fastapi.FastAPI()\n\n" + "@app.post('/v1/check')\n" + "def check():\n" + " return {'allow': True}\n" + ) + }, + ) + code, errors, _ = layer.evaluate(root=root) + self.assertEqual(code, 1) + joined = "\n".join(errors) + self.assertIn("fastapi", joined) + self.assertIn("/v1/check", joined) diff --git a/tools/check_layer_conformance.py b/tools/check_layer_conformance.py new file mode 100644 index 0000000..b136d63 --- /dev/null +++ b/tools/check_layer_conformance.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python3 +"""Check zone-engine against the NetKingdom security layer model (§5, §6, §11). + +Read-only. This repository's position is: + + Engine / PIP for zone identity and membership, offline reference + conformance, no Tooling-layer client, no HTTP authorization decision + surface, no PEP. + +§11 requires a machine-readable declaration that agrees with INTENT.md +frontmatter. This script is what makes that claim checkable: it fails if +layer.yaml is missing, disagrees with INTENT.md, or if a Tooling client or +decision surface appears under tools/. + +The failure it exists to catch is a *convenience* — a live lookup, an +OpenBao client, or an /authorize helper "just for this consumer". That is +this repository's original §7 falsifier, now statute §6. + +Review dates are reported, never enforced: a date-triggered failure breaks +the build on a calendar day with no code change. + +Exit 0 clean, 1 undeclared contact or decision surface, 2 declaration +malformed or in disagreement with INTENT.md. +""" +from __future__ import annotations + +import argparse +import ast +import sys +from datetime import date +from pathlib import Path +from typing import Any, Iterable, Mapping + +import yaml + +ROOT = Path(__file__).resolve().parents[1] +TOOLS = ROOT / "tools" +DECL = ROOT / "layer.yaml" +INTENT = ROOT / "INTENT.md" + +TOOLING_IMPORTS = { + "hvac": "OpenBao / Vault client", + "bao": "OpenBao client", + "kubernetes": "cluster client", + "kubernetes_asyncio": "cluster client", + "psycopg": "direct database connection", + "psycopg2": "direct database connection", + "asyncpg": "direct database connection", + "sqlalchemy": "direct database connection", + "pymysql": "direct database connection", + "redis": "direct datastore connection", + "ldap3": "direct LDAP client (key-cape tooling)", + "python_ldap": "direct LDAP client (key-cape tooling)", + "docker": "container runtime client", +} + +HTTP_SURFACE_IMPORTS = { + "flask": "HTTP framework", + "fastapi": "HTTP framework", + "starlette": "HTTP framework", + "aiohttp": "HTTP framework", + "tornado": "HTTP framework", + "bottle": "HTTP framework", + "quart": "HTTP framework", + "sanic": "HTTP framework", + "django": "HTTP framework", + "gunicorn": "HTTP server", + "uvicorn": "HTTP server", + "hypercorn": "HTTP server", + "waitress": "HTTP server", +} + +DECISION_PATHS = ("/authorize", "/v1/check", "/v1/authorize") + + +class ConformanceError(ValueError): + """Declaration is missing, unparseable, or disagrees with INTENT.md.""" + + +def _frontmatter(path: Path) -> dict[str, Any]: + text = path.read_text() + if not text.startswith("---\n") or "\n---\n" not in text[4:]: + raise ConformanceError(f"{path.name} requires YAML frontmatter") + raw = text.split("\n---\n", 1)[0][4:] + value = yaml.safe_load(raw) or {} + if not isinstance(value, Mapping): + raise ConformanceError(f"{path.name} frontmatter must be a mapping") + return dict(value) + + +def load_declaration(path: Path = DECL) -> dict[str, Any]: + if not path.exists(): + raise ConformanceError(f"no declaration at {path.name} (§11)") + try: + data = yaml.safe_load(path.read_text()) + except yaml.YAMLError as exc: + raise ConformanceError(f"{path.name} is not parseable: {exc}") from exc + if not isinstance(data, Mapping): + raise ConformanceError(f"{path.name} must be a mapping") + for key in ("layer", "role", "repository", "tooling_contacts", "standard_version"): + if key not in data: + raise ConformanceError(f"{path.name} missing required key '{key}' (§11)") + return dict(data) + + +def load_intent(path: Path = INTENT) -> dict[str, Any]: + if not path.exists(): + raise ConformanceError(f"no INTENT.md at {path}") + return _frontmatter(path) + + +def _norm(value: Any) -> str: + return str(value or "").strip().lower() + + +def check_declaration_agrees( + decl: Mapping[str, Any], intent: Mapping[str, Any] +) -> list[str]: + errors: list[str] = [] + if _norm(decl.get("layer")) != "engine": + errors.append(f"declared layer is '{decl.get('layer')}', expected 'engine'") + if _norm(decl.get("role")) != "pip": + errors.append(f"declared role is '{decl.get('role')}', expected 'pip'") + if _norm(intent.get("layer")) != _norm(decl.get("layer")): + errors.append( + f"INTENT.md layer '{intent.get('layer')}' disagrees with " + f"layer.yaml '{decl.get('layer')}'" + ) + if _norm(intent.get("role")) != _norm(decl.get("role")): + errors.append( + f"INTENT.md role '{intent.get('role')}' disagrees with " + f"layer.yaml '{decl.get('role')}'" + ) + if decl.get("repository") != "zone-engine": + errors.append(f"declared repository is '{decl.get('repository')}'") + if decl.get("pep_stance"): + errors.append("pep_stance is set; this repository is not PEP-shaped") + contacts = decl.get("tooling_contacts") + if contacts not in ([], None): + errors.append( + "tooling_contacts is not empty; a new contact needs a §10 cut, " + "not a quiet row" + ) + return errors + + +def imported_modules(path: Path) -> set[str]: + try: + tree = ast.parse(path.read_text()) + except SyntaxError: + return set() + found: set[str] = set() + for node in ast.walk(tree): + if isinstance(node, ast.Import): + found.update(alias.name.split(".")[0] for alias in node.names) + elif isinstance(node, ast.ImportFrom): + if node.level == 0 and node.module: + found.add(node.module.split(".")[0]) + return found + + +def string_literals(path: Path) -> list[str]: + try: + tree = ast.parse(path.read_text()) + except SyntaxError: + return [] + found: list[str] = [] + for node in ast.walk(tree): + if isinstance(node, ast.Constant) and isinstance(node.value, str): + found.append(node.value) + return found + + +def scan_tooling(directory: Path) -> list[tuple[Path, str, str]]: + hits: list[tuple[Path, str, str]] = [] + for path in sorted(directory.rglob("*.py")): + for module in sorted(imported_modules(path)): + if module in TOOLING_IMPORTS: + hits.append((path, module, TOOLING_IMPORTS[module])) + return hits + + +def scan_decision_surfaces(directory: Path) -> list[tuple[Path, str, str]]: + hits: list[tuple[Path, str, str]] = [] + for path in sorted(directory.rglob("*.py")): + if path.resolve() == Path(__file__).resolve(): + continue + for module in sorted(imported_modules(path)): + if module in HTTP_SURFACE_IMPORTS: + hits.append((path, module, HTTP_SURFACE_IMPORTS[module])) + if module == "http": + # http.server is the stdlib decision-adjacent live surface. + text = path.read_text() + if "http.server" in text: + hits.append((path, "http.server", "stdlib HTTP server")) + for literal in string_literals(path): + for needle in DECISION_PATHS: + if needle in literal: + hits.append((path, literal, f"authorization path {needle}")) + return hits + + +def overdue_reviews(decl: Mapping[str, Any], today: date | None = None) -> list[str]: + today = today or date.today() + notes: list[str] = [] + for cap in decl.get("unowned_capabilities") or []: + review = cap.get("review") + if not review: + continue + try: + due = date.fromisoformat(str(review)) + except ValueError: + notes.append(f"{cap.get('id')}: unparseable review date {review!r}") + continue + if due < today: + notes.append(f"{cap.get('id')}: review overdue ({review})") + return notes + + +def evaluate( + *, + root: Path | None = None, + tools: Path | None = None, + decl_path: Path | None = None, + intent_path: Path | None = None, +) -> tuple[int, list[str], list[str]]: + """Return (exit_code, errors, reports). Exit 2 for declaration, 1 for tree.""" + root = root or ROOT + tools = tools or (root / "tools") + decl_path = decl_path or (root / "layer.yaml") + intent_path = intent_path or (root / "INTENT.md") + try: + decl = load_declaration(decl_path) + intent = load_intent(intent_path) + except ConformanceError as exc: + return 2, [str(exc)], [] + errors = check_declaration_agrees(decl, intent) + if errors: + return 2, errors, [] + reports = overdue_reviews(decl) + tree_errors: list[str] = [] + for path, module, what in scan_tooling(tools): + rel = path.relative_to(root) if path.is_relative_to(root) else path + tree_errors.append(f"{rel}: imports '{module}' — {what}") + for path, token, what in scan_decision_surfaces(tools): + rel = path.relative_to(root) if path.is_relative_to(root) else path + tree_errors.append(f"{rel}: {what} ({token})") + if tree_errors: + return 1, tree_errors, reports + return 0, [], reports + + +def main(argv: Iterable[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--report", action="store_true", help="print the declaration summary") + args = parser.parse_args(list(argv) if argv is not None else None) + + code, errors, reports = evaluate() + if args.report: + try: + decl = load_declaration() + except ConformanceError as exc: + print(f"FAIL: {exc}", file=sys.stderr) + return 2 + print( + f"zone-engine — layer {decl['layer']}, role {decl['role']}, " + f"standard v{decl['standard_version']}" + ) + print(f" tooling contacts declared: {len(decl.get('tooling_contacts') or [])}") + print(f" pep_stance: {decl.get('pep_stance')!r}") + for note in reports: + print(f" review: {note}") + + if code == 2: + print("FAIL: layer declaration malformed or disagrees with INTENT.md (§11)", file=sys.stderr) + for item in errors: + print(f" {item}", file=sys.stderr) + return 2 + if code == 1: + print("FAIL: undeclared Tooling client or HTTP decision surface (§6, §11)", file=sys.stderr) + for item in errors: + print(f" {item}", file=sys.stderr) + print("", file=sys.stderr) + print(" A live API, Tooling client, or /authorize helper is a layer change.", file=sys.stderr) + print(" Do not declare it to make the check pass.", file=sys.stderr) + return 1 + if not args.report: + print(f"OK: Engine/PIP declaration agrees with INTENT.md; no Tooling client or decision surface in {TOOLS.relative_to(ROOT)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/workplans/ZONE-WP-0003-security-layer-model-alignment.md b/workplans/ZONE-WP-0003-security-layer-model-alignment.md index 44b87ff..fec012f 100644 --- a/workplans/ZONE-WP-0003-security-layer-model-alignment.md +++ b/workplans/ZONE-WP-0003-security-layer-model-alignment.md @@ -4,7 +4,7 @@ type: workplan title: "Declare Engine/PIP and freeze the offline zone-membership surface" domain: infotech repo: zone-engine -status: ready +status: finished owner: zone-engine assignee: grok lane: yellow @@ -12,6 +12,10 @@ topic_slug: netkingdom planning_priority: P1 created: "2026-08-29" updated: "2026-08-29" +quality_dod: DoD-Ok +quality_dod_at: "2026-08-29" +quality_dod_by: grok +quality_dod_note: "layer.yaml agrees with INTENT.md; make check covers the freeze (no Tooling client, no decision surface); §10 artifacts and PIP claim mapping are cited; README uses the ruled PDP name; no live API was added." origin: "history/2026-08-29-security-layer-model-scope-against-intent.md" context_paths: - "GOAL.md" @@ -91,7 +95,7 @@ and the `flex-auth` repository rename are intentionally excluded. ```task id: ZONE-WP-0003-T01 -status: todo +status: done priority: high state_hub_task_id: "ca687f30-2ad5-5243-8e48-efc34c78465c" ``` @@ -149,7 +153,7 @@ counting this repository among the undeclared nine. ```task id: ZONE-WP-0003-T03 -status: todo +status: done priority: medium state_hub_task_id: "828a4c67-9644-5b27-ac97-d79563848dfc" ``` @@ -174,7 +178,7 @@ them from workplan history, and `INTENT.md` or `SCOPE.md` cites the note. ```task id: ZONE-WP-0003-T04 -status: todo +status: done priority: medium state_hub_task_id: "bd4955b6-dfb8-59e5-be20-f4f8ab93ae8e" ``` @@ -194,7 +198,7 @@ SCOPE.md GOAL.md` is the consumer-facing surface. ```task id: ZONE-WP-0003-T05 -status: todo +status: done priority: medium state_hub_task_id: "ee883d61-4d27-5223-826c-c37f66e3bcb4" ``` @@ -221,14 +225,14 @@ and no new wire format has shipped. ## Acceptance criteria -- [ ] `layer.yaml` declares Engine / PIP, no Tooling contacts, no PEP map. -- [ ] `make check` fails a new undeclared Tooling client or decision surface. +- [x] `layer.yaml` declares Engine / PIP, no Tooling contacts, no PEP map. +- [x] `make check` fails a new undeclared Tooling client or decision surface. - [x] `ZONE-IN-0001` is closed on the own-voice declaration. - [x] `gate-house` has been notified. -- [ ] Statute §10 artifacts for the 2026-08-23 cut are gathered in one place. -- [ ] Consumer-facing docs use `access-engine` as the ruled PDP name. -- [ ] PIP facts are mapped for a future claim schema without shipping one. -- [ ] No live API, daemon, decision surface, or compiled stance was added. +- [x] Statute §10 artifacts for the 2026-08-23 cut are gathered in one place. +- [x] Consumer-facing docs use `access-engine` as the ruled PDP name. +- [x] PIP facts are mapped for a future claim schema without shipping one. +- [x] No live API, daemon, decision surface, or compiled stance was added. ## Out of scope