Declare layer machine-readably (§11); adopt v0.6 corrections

The standard moved v0.4 -> v0.6. All four findings from our v0.4 review
were adopted in v0.5, and v0.6 went further on two of them.

§11 now requires a machine-readable declaration — prose cannot
distinguish a declaration from a transcribed review. We had none.
Added layer.yaml (form adapted from ops-warden's reference
implementation), scripts/check_layer_conformance.py, and
tests/test_layer_conformance.py.

The check makes our central claim mechanical rather than asserted: no
direct Tooling client in src/. The test exercises the negative case on a
synthetic tree, so it fails if the checker goes blind. pyyaml is added as
a DEV dependency only — `dependencies = []` is load-bearing for the §5
claim and stays empty.

Adopted from v0.6:
- containment is no longer ours (§9.2). Actuation is an Engine concept,
  unowned and held at zero; kings-guard proposes containment and never
  performs it. The register row is now a dependency, not our gap.
- observation is scoped to Staff-reachable sources, with identity and
  secret observation pending — our finding 1, adopted near-verbatim.
- access-engine DECLINED the authentication-evidence gap; owner is now
  the identity layer plus audit-core, reproposed and unassented.
- §11 blocked-clean recorded, with the rule that it must not rank below
  conforming — our finding 2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UEtvmYUBP2fDtirJGWn5MW

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 4014379@bnt-lap001
Assistant-Session: 4af9e20f-1768-4afc-951b-b507784e382b
This commit is contained in:
tegwick 2026-08-29 10:20:39 +02:00
parent d99f395aa1
commit 72c2a42d67
9 changed files with 389 additions and 29 deletions

View file

@ -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

View file

@ -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

View file

@ -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`.
---

View file

@ -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`.

108
layer.yaml Normal file
View file

@ -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": []

View file

@ -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]

View file

@ -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())

View file

@ -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`,

View file

@ -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"