diff --git a/INTENT.md b/INTENT.md index 9b57ca7..b0fcb43 100644 --- a/INTENT.md +++ b/INTENT.md @@ -1,35 +1,45 @@ # INTENT > **Layer: Staff.** *(NetKingdom Security Layer Model — current version -> `net-kingdom/canon/standards/security-layer-model_v0.4.md`, §4 catalog, -> status accepted; ratified by `gate-house/decisions/decisions.md` -> GH-DEC-2026-001; assented here by `decisions/decisions.md` KG-DEC-2026-001 -> on 2026-08-28, against v0.1. This declaration is made in kings-guard's own -> voice, per §11.)* +> `net-kingdom/canon/standards/security-layer-model_v0.6.md`, §4 catalog; +> ratified by `gate-house/decisions/decisions.md` GH-DEC-2026-001; assented here +> by `decisions/decisions.md` KG-DEC-2026-001 on 2026-08-28, against v0.1.)* > -> **Catalog entry (v0.4 §4):** adaptive defence, observation; **containment — -> pending (§9.2)**, until an engine exposes a containment surface. The pending -> mark exists because kings-guard raised the defect that v0.1 catalogued a -> capability §5 forbade discharging; the general rule is now §9.1. +> **The machine-readable declaration required by §11 is `layer.yaml`**, checked +> by `scripts/check_layer_conformance.py` and tested in +> `tests/test_layer_conformance.py`. Prose cannot distinguish a declaration from +> a transcribed review, so the file is authoritative and this note is commentary. > -> kings-guard is **interactive and non-deterministic**: adaptive defence, -> observation, containment. Acting at runtime does not make a repository an -> Engine; being agentic makes it Staff. +> **Catalog entry (v0.6 §4):** adaptive defence and judgment; observation of +> Staff-reachable sources — identity and secret observation **pending**; +> **proposes** containment, which it does not own. +> +> kings-guard is **interactive and non-deterministic**. Acting at runtime does +> not make a repository an Engine; being agentic makes it Staff. > > **The binding rule (§5): Staff never touches Tooling directly. It acts only > through Engine APIs.** kings-guard holds no direct client for a Tooling-layer -> system — no database connection, no OpenBao client, no cluster mutation. It -> may contain a threat only by calling an engine. Where no engine exposes a -> capability kings-guard needs, that is raised as an **engine gap**, never -> solved locally; the open gaps are listed under *System boundary* below. +> system — no database connection, no OpenBao client, no cluster mutation, and +> no §5.1 diagnostic read or §5.2 conduit either. Under §11 that is +> **blocked-clean**: the capabilities needing such a contact sit at zero rather +> than being taken locally, and §11 rules that this MUST NOT rank below +> conforming. +> +> **Containment is not ours (§9.2).** v0.6 moved it off this repository +> entirely: reduce authority, require step-up, and isolate a workload are +> authority-changing operations, so they are rendered by an Engine and enforced +> by a PEP. **kings-guard proposes containment; it never performs it.** The +> actuation surface is unowned and held at zero estate-wide, so no argument +> anywhere may assume containment is automatic. > > **Posture contract.** kings-guard **publishes** posture; `gate-house` defines > its authority meaning; `access-engine` renders it. Posture is not a privilege > source. The asymmetry is absolute: kings-guard may **reduce** authority, > **require step-up**, or **request containment**; it MUST NOT probabilistically -> manufacture additional authority. Every effector request it emits therefore -> carries an explicit authority boundary and is advisory unless the owning -> system has already delegated a narrow, deterministic action lane. +> manufacture additional authority. Every effector request carries an explicit +> authority boundary. Under incomplete observation this asymmetry is what bounds +> the damage: a suppressed event can only cost a tightening that should have +> happened, never manufacture authority through us (§8, §9.6). > This file captures **why this repository exists**, the **direction it is > moving toward**, and the **kind of system it is meant to become**. @@ -151,16 +161,18 @@ rule these are gaps to close in the owning engine, not work to route around. None is a standing licence to reach into Tooling. These are **unowned capabilities**, not §5.3 declared contacts: kings-guard -makes no direct Tooling contact for any of them. Under §11 kings-guard is -**Conforming**, not a tracked non-conformance. The fields follow §5.3's shape so -one register can hold both, but the distinction is load-bearing — see the -assessment sent to gate-house on 2026-08-29. +makes no direct Tooling contact for any of them. v0.5 §11 added the +**blocked-clean** state for exactly this case, on kings-guard's finding, and +ruled that it MUST NOT rank below conforming. + +`layer.yaml` is the authoritative machine-readable form; this table is the +human-readable view of it. | `capability` | Needed for | `intended_owner` | `blocked_on` | `review` | | --- | --- | --- | --- | --- | -| Authentication and assurance evidence (token assurance, attestation outcomes, authentication anomalies) exposed as an engine surface | identity-drift posture | `user-engine` / `access-engine` | no engine surface exists; kings-guard consumes fixtures only | 2026-11-28 | +| Authentication and assurance evidence (token assurance, attestation outcomes, authentication anomalies) exposed as an engine surface | identity-drift posture | identity layer + `audit-core` — **`access-engine` declined** (v0.6 §13); reproposed, not assented | no engine surface exists; kings-guard consumes fixtures only | 2026-11-28 | | Secret-use evidence (lease, revocation, mount and rotation metadata) exposed as an engine surface | secret-abuse posture | `secrets-engine` | no engine surface exists; kings-guard consumes fixtures only | 2026-11-28 | -| Containment surface — reduce authority, require step-up, isolate a workload — callable as a deterministic engine API, available while an incident is in progress | bounded response | `access-engine`, runtime engines | no engine surface exists; ruled pending in v0.4 §9.2, degraded-mode fallback ruled into the engine by §9.3 | 2026-11-28 | +| Actuation surface — reduce authority, require step-up, isolate a workload — as a deterministic engine API carrying a decision record | containment kings-guard **proposes but does not own** | `access-engine` + runtime PEPs; not reviewed (`FLEX-DEC-2026-002`) | ruled an Engine concept held at zero (v0.6 §9.2); recorded here as a dependency, not a kings-guard gap to close | 2026-11-28 | Until a gap closes, the corresponding posture lane stays advisory and fixture-driven. kings-guard MUST NOT open a direct path to the Tooling system diff --git a/Makefile b/Makefile index d0a1ed0..18a5c37 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ PYTHON ?= python3 -.PHONY: install-dev test lint run-demo +.PHONY: install-dev test lint run-demo check-layer install-dev: $(PYTHON) -m pip install -e ".[dev]" @@ -11,5 +11,10 @@ test: lint: $(PYTHON) -m ruff check src tests +# NetKingdom Security Layer Model §11 — makes the no-Tooling-client claim +# checkable rather than asserted. See layer.yaml. +check-layer: + $(PYTHON) scripts/check_layer_conformance.py --report + run-demo: PYTHONPATH=src $(PYTHON) -m kings_guard.main --pilot qonto-assistant diff --git a/SCOPE.md b/SCOPE.md index 95e7273..e0750f9 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -9,7 +9,7 @@ **Staff** — interactive, non-deterministic; adaptive defence, observation, containment. Binding rule: kings-guard never touches Tooling directly; it acts only through Engine APIs. See `INTENT.md` and -`net-kingdom/canon/standards/security-layer-model_v0.4.md`. +`net-kingdom/canon/standards/security-layer-model_v0.6.md`. --- diff --git a/docs/AdjacentSystemBoundary.md b/docs/AdjacentSystemBoundary.md index 1bcff64..d4188b7 100644 --- a/docs/AdjacentSystemBoundary.md +++ b/docs/AdjacentSystemBoundary.md @@ -31,7 +31,7 @@ The following rules apply to every integration below: 4. `kings-guard` must preserve **tenant isolation**: any retained evidence or memory must stay bounded by declared confidentiality rules. 5. `kings-guard` is a **Staff-layer** repository and is bound by §5 of the - NetKingdom Security Layer Model v0.4: it never holds a direct client for a + NetKingdom Security Layer Model v0.6: it never holds a direct client for a Tooling-layer system. Evidence from Tooling (OpenBao, key-cape components) is consumed **through the owning engine**. Where no engine surface exists, the lane stays fixture-driven and the gap is declared in `INTENT.md`. diff --git a/layer.yaml b/layer.yaml new file mode 100644 index 0000000..4452b9a --- /dev/null +++ b/layer.yaml @@ -0,0 +1,108 @@ +# kings-guard — NetKingdom security layer declaration +# +# Framework: net-kingdom/canon/standards/security-layer-model_v0.6.md +# Assent: decisions/decisions.md KG-DEC-2026-001 (kings-guard's own voice, §11) +# Validate: python3 scripts/check_layer_conformance.py +# +# §11 (v0.6) requires a machine-readable declaration: prose cannot distinguish a +# declaration from a transcribed review. Form adapted from ops-warden's +# reference implementation, offered under §11. +# +# kings-guard's position is unusual and this file is shaped to state it exactly: +# there are NO Tooling contacts. Not a narrow one, not a read-only one. The +# capabilities that would need them sit at zero instead. Under §11 that is +# BLOCKED-CLEAN, which MUST NOT rank below conforming. + +schema_version: "0.1" +framework: netkingdom-security-layer-model +standard_version: "0.6" +repository: kings-guard +layer: staff +declared_by: decisions/decisions.md#KG-DEC-2026-001 +declared_at: "2026-08-29" + +# §4 catalog entry, transcribed so drift between the catalog and this file is +# visible. The standard is authoritative for the row; this records what we +# understand ourselves to have been assigned. +catalog_entry: + owns: + - adaptive defence and judgment + - observation of Staff-reachable sources + pending: + - identity observation + - secret observation + proposes_but_does_not_own: + # §9.2 — actuation is an Engine concept held at zero. kings-guard proposes + # containment and never performs it. This is not our gap to close. + - containment + +# §5 / §11: every direct contact with a Tooling-layer system (a §4 Tooling row), +# one entry each. Empty is a claim, and scripts/check_layer_conformance.py is +# what makes it checkable rather than asserted. +tooling_contacts: [] + +# §11 requires non-Tooling clients to be recorded "so the check is total". +non_tooling_clients: + - id: state-hub-work-records + target: state-hub + layer: not-catalogued + operation: "HTTP to the Custodian State Hub for work records and progress events" + write: true + 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. + +# §11 blocked-clean. These are NOT §5.3 declared gaps: there is no contact to +# declare. Fields follow the §5.3 shape so one register can hold both kinds +# (§13 now carries the state column that keeps them distinct — raised by +# kings-guard against v0.4). +unowned_capabilities: + + - id: authentication-assurance-evidence + state: unowned-capability + capability: >- + Token assurance, attestation outcomes and authentication anomalies exposed + as an engine surface, for identity-drift posture. + intended_owner: "identity layer + audit-core" + owner_status: "access-engine declined (v0.6 §13); reproposed, not assented" + blocked_on: >- + No engine exposes authentication evidence. key-cape is Tooling, so §5 + forbids the direct route, and the capability stays at zero rather than + being taken locally. + review: "2026-11-28" + consequence: "identity-drift posture lane stays fixture-driven" + + - id: secret-use-evidence + state: unowned-capability + capability: >- + Lease, revocation, mount and rotation metadata exposed as an engine + surface, for secret-abuse posture. + intended_owner: secrets-engine + owner_status: proposed + blocked_on: >- + No engine exposes secret-use evidence. OpenBao is Tooling; same reasoning + as above. + review: "2026-11-28" + consequence: "secret-abuse posture lane stays fixture-driven" + + - id: actuation-surface + state: unowned-capability + capability: >- + Reduce authority, require step-up, isolate a workload — as a deterministic + engine API carrying a decision record. + intended_owner: "access-engine + runtime PEPs" + owner_status: "proposed; access-engine has not reviewed (FLEX-DEC-2026-002)" + blocked_on: >- + Ruled in v0.6 §9.2 to be an Engine concept, unowned and held at zero. + kings-guard proposes containment and does not own it, so this row is + recorded here as a dependency, not as a kings-guard gap to close. + review: "2026-11-28" + consequence: "no containment is possible anywhere in the estate" + +# §5.1 read-only diagnostic observation of Tooling: none declared, none taken. +# §5.2 conduit: none. kings-guard runs no tool under a caller's identity. +declared_shapes: + "5.1": [] + "5.2": [] + "5.3": [] diff --git a/pyproject.toml b/pyproject.toml index 88daa7b..7d3cf5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,10 @@ dependencies = [] dev = [ "pytest>=8.2,<9.0", "ruff>=0.6,<1.0", + # Reads layer.yaml in scripts/check_layer_conformance.py. Deliberately a + # DEV dependency: `dependencies = []` above is load-bearing for the §5 + # no-Tooling-client claim and must stay empty. + "pyyaml>=6.0,<7.0", ] [project.scripts] diff --git a/scripts/check_layer_conformance.py b/scripts/check_layer_conformance.py new file mode 100644 index 0000000..2b7ffdb --- /dev/null +++ b/scripts/check_layer_conformance.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""Check kings-guard against the NetKingdom security layer model (§5, §11). + +Read-only. kings-guard's whole position under the standard rests on one claim: + + it holds no direct client for any Tooling-layer system, and the + capabilities that would need one sit at zero instead (§11 blocked-clean) + +That claim has been asserted in prose since KG-DEC-2026-001. §11 (v0.6) requires +a machine-readable declaration because prose cannot distinguish a declaration +from a transcribed review. This script is what makes the claim checkable: it +fails if a Tooling client appears in src/ without a matching layer.yaml entry. + +The failure it exists to catch is a *convenience* — someone reaching for an +OpenBao or cluster client during an incident because the engine surface still +does not exist (§9.2). That is precisely the "small convenience" §6 warns about, +and it would arrive as a one-line import. + +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 found, 2 declaration malformed. +""" +from __future__ import annotations + +import argparse +import ast +import sys +from datetime import date +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parents[1] +SRC = ROOT / "src" / "kings_guard" +DECL = ROOT / "layer.yaml" + +# Import roots that would constitute a direct Tooling-layer client under §4. +# Matched against the top-level module of every import in src/. +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", +} + + +def load_declaration() -> dict: + if not DECL.exists(): + print(f"FAIL: no declaration at {DECL.relative_to(ROOT)} (§11)", file=sys.stderr) + raise SystemExit(2) + try: + data = yaml.safe_load(DECL.read_text()) + except yaml.YAMLError as exc: + print(f"FAIL: {DECL.name} is not parseable: {exc}", file=sys.stderr) + raise SystemExit(2) from exc + for key in ("layer", "repository", "tooling_contacts", "standard_version"): + if key not in data: + print(f"FAIL: {DECL.name} missing required key '{key}' (§11)", file=sys.stderr) + raise SystemExit(2) + if data["layer"] != "staff": + print(f"FAIL: declared layer is '{data['layer']}', expected 'staff'", file=sys.stderr) + raise SystemExit(2) + return data + + +def imported_modules(path: Path) -> set[str]: + """Top-level module name of every import in one file.""" + 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 scan() -> list[tuple[Path, str, str]]: + hits: list[tuple[Path, str, str]] = [] + for path in sorted(SRC.rglob("*.py")): + for module in sorted(imported_modules(path)): + if module in TOOLING_IMPORTS: + hits.append((path, module, TOOLING_IMPORTS[module])) + return hits + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--report", action="store_true", help="print the declaration summary") + args = parser.parse_args() + + decl = load_declaration() + declared = {c.get("id") for c in decl.get("tooling_contacts") or []} + hits = scan() + + undeclared = [h for h in hits if h[1] not in declared] + + if args.report: + print(f"kings-guard — layer {decl['layer']}, standard v{decl['standard_version']}") + print(f" tooling contacts declared: {len(declared)}") + print(f" unowned capabilities (§11 blocked-clean): " + f"{len(decl.get('unowned_capabilities') or [])}") + today = date.today() + for cap in decl.get("unowned_capabilities") or []: + review = cap.get("review") + stale = "" + if review and date.fromisoformat(str(review)) < today: + stale = " [REVIEW OVERDUE]" + print(f" - {cap['id']}: {cap.get('owner_status', '?')}{stale}") + + if undeclared: + print("", file=sys.stderr) + print("FAIL: undeclared Tooling-layer client (§11 undeclared violation)", file=sys.stderr) + for path, module, what in undeclared: + rel = path.relative_to(ROOT) + print(f" {rel}: imports '{module}' — {what}", file=sys.stderr) + print("", file=sys.stderr) + print(" A Staff repository may not hold a direct Tooling client (§5).", file=sys.stderr) + print(" Route it through the owning engine, or if none exists, raise an", file=sys.stderr) + print(" engine gap — do not declare this to make the check pass.", file=sys.stderr) + return 1 + + if not args.report: + print(f"OK: no direct Tooling client in {SRC.relative_to(ROOT)} (§5, §11)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/specs/NetKingdomImmuneArchitecture.md b/specs/NetKingdomImmuneArchitecture.md index aa73a29..7d01f0c 100755 --- a/specs/NetKingdomImmuneArchitecture.md +++ b/specs/NetKingdomImmuneArchitecture.md @@ -15,7 +15,7 @@ classification: Public # NetKingdom Immune Architecture > **Layer note (2026-08-28).** kings-guard is a **Staff**-layer repository -> under the NetKingdom Security Layer Model v0.4 (assented in +> under the NetKingdom Security Layer Model v0.6 (assented in > `decisions/decisions.md` KG-DEC-2026-001). Where this document uses "control > plane", read it as naming an **estate-wide, Engine-layer** arrangement of > deterministic authorities — never as a self-description of `kings-guard`, diff --git a/tests/test_layer_conformance.py b/tests/test_layer_conformance.py new file mode 100644 index 0000000..bf71aca --- /dev/null +++ b/tests/test_layer_conformance.py @@ -0,0 +1,88 @@ +"""kings-guard's layer declaration is checkable, not merely asserted (§11). + +The claim under test is the one our whole position rests on: no direct +Tooling-layer client. A test that only ran the checker against a clean tree +would prove nothing — it would pass just as happily if the checker were broken. +So the negative case is exercised too, on a synthetic tree. +""" +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +import pytest + +yaml = pytest.importorskip("yaml") + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "check_layer_conformance.py" +DECL = ROOT / "layer.yaml" + + +def _run(*args: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, str(SCRIPT), *args], + capture_output=True, + text=True, + ) + + +def test_declaration_exists_and_declares_staff(): + """§11: an estate-authored repository declares its layer machine-readably.""" + assert DECL.exists(), "no layer.yaml — §11 requires a machine-readable declaration" + data = yaml.safe_load(DECL.read_text()) + assert data["repository"] == "kings-guard" + assert data["layer"] == "staff" + assert data["framework"] == "netkingdom-security-layer-model" + + +def test_no_tooling_contacts_declared(): + """The blocked-clean position: nothing to declare, because nothing is touched.""" + data = yaml.safe_load(DECL.read_text()) + assert data["tooling_contacts"] == [], ( + "a Tooling contact appeared in the declaration; kings-guard's blocked-clean " + "position under §11 no longer holds and KG-DEC-2026-001 needs revisiting" + ) + for shape, entries in data["declared_shapes"].items(): + assert entries == [], f"§5.{shape} shape declared; see comment above" + + +def test_unowned_capabilities_carry_the_gap_record_fields(): + """§5.3 field shape, reused for §13 unowned-capability rows (§17 gap-record).""" + data = yaml.safe_load(DECL.read_text()) + caps = data["unowned_capabilities"] + assert caps, "the three known gaps should be declared" + for cap in caps: + for field in ("capability", "intended_owner", "blocked_on", "review", "state"): + assert cap.get(field), f"{cap.get('id')} missing '{field}'" + assert cap["state"] == "unowned-capability" + + +def test_checker_passes_on_the_real_tree(): + result = _run() + assert result.returncode == 0, result.stderr + + +def test_checker_catches_an_undeclared_tooling_client(tmp_path, monkeypatch): + """The negative case: a direct OpenBao client must fail the check. + + This is the convenience §6 warns about — it would arrive as one import. + """ + import importlib.util + + spec = importlib.util.spec_from_file_location("check_layer_conformance", SCRIPT) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + fake_src = tmp_path / "src" / "kings_guard" + fake_src.mkdir(parents=True) + (fake_src / "effector.py").write_text( + "import hvac\n\n\ndef contain(actor):\n hvac.Client().revoke(actor)\n" + ) + monkeypatch.setattr(module, "SRC", fake_src) + monkeypatch.setattr(module, "ROOT", tmp_path) + + hits = module.scan() + assert hits, "a direct OpenBao client was not detected — the checker is blind" + assert hits[0][1] == "hvac"