Implement §5.3 machine-readably — layer.yaml, checker, conformance tests

The security layer model moved v0.1 -> v0.4 (accepted) after ops-warden's
assent. Both §5 asks from ADR-0010 were adopted: §5.2 now sanctions the conduit
shape on the supplied-authority property, and §5.3 is the declared engine gap
amendment, carrying the four fields verbatim and crediting ops-warden's
delegation machinery as prior art.

Which creates an obligation. §5.3 requires those fields MACHINE-READABLY, and
§11 makes "every direct Tooling client maps to a declared §5.1/§5.2/§5.3 entry"
a mechanical check. ops-warden's declaration was prose in INTENT.md — the repo
that proposed the shape was not implementing it.

layer.yaml is the map: 5 contacts (2 declared gaps, 1 read-only observation,
2 conduits) plus the non-Tooling clients recorded explicitly so the check is
total rather than silently selective.

scripts/check_layer_conformance.py enforces it and found three undeclared
modules on its first run — all false positives (help text, a docstring, and the
doubles library that SIMULATES bao rather than calling it), which is why the
scan now matches invocation shapes instead of the word: an httpx call built
against the configured OpenBao address, or an argv whose first element is the
bao binary.

tests/test_layer_conformance.py adds the §5.2 test the standard says SHOULD
exist: _caller_env() returns the caller's environment unchanged, and proxy.py
is asserted not to reference X-Vault-Token, approle login, or token create — a
conduit that presents its own token is not a conduit.

No assertion on review dates, deliberately: a date-triggered failure breaks the
build on a calendar day with no code change, the same reasoning WP-0033-T05
recorded for blocker staleness.

398 tests pass, ruff clean.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 4014535@bnt-lap001
Assistant-Session: d0036016-73e8-4da1-8e47-563e3ab39a3c
This commit is contained in:
tegwick 2026-08-29 02:45:29 +02:00
parent 57d39ede0f
commit f815bb35ca
6 changed files with 448 additions and 27 deletions

View file

@ -1,10 +1,10 @@
{
"schema": "repo_manager.index.v1",
"slug": "layer-model-assent",
"slug": "layer-model-v03-review",
"repo_root": "/home/worsch/ops-warden",
"head_sha": "467635e84b99757336ee49d7f0dbf107607d0560",
"observed_at": "2026-08-28T19:30:29.828983Z",
"source_fingerprint": "7dfae4f3d33f6a9503a2c7326b925efb378613e772b39c4dd9b0640a35b85654",
"head_sha": "f289465b90fcdffe2c8bf4a51cc71607747126f9",
"observed_at": "2026-08-28T20:40:26.143458Z",
"source_fingerprint": "a9ae0d530804e039aae34b4902d8609723e9bba4385a501b9a4027911c3b6302",
"source_files": [
".repo-classification.yaml",
"INTENT.md",
@ -55,7 +55,7 @@
"status": "finished",
"title": "Ad Hoc Tasks \u2014 2026-06-27",
"source_path": "workplans/ADHOC-2026-06-27.md",
"uuid": null,
"uuid": "a222c91f-3bb5-58a4-b6b2-f0fb18cdd5c3",
"parent_id": null,
"extra": {}
},
@ -75,7 +75,7 @@
"status": "finished",
"title": "Ad Hoc Tasks \u2014 2026-06-29",
"source_path": "workplans/ADHOC-2026-06-29.md",
"uuid": null,
"uuid": "13fa845f-852e-55ec-a2a5-2296996e0216",
"parent_id": null,
"extra": {}
},
@ -95,7 +95,7 @@
"status": "finished",
"title": "Ad Hoc Tasks \u2014 2026-08-11",
"source_path": "workplans/ADHOC-2026-08-11.md",
"uuid": null,
"uuid": "9f99cc64-4682-5f20-b13e-89af2b6f7c70",
"parent_id": null,
"extra": {}
},
@ -135,7 +135,7 @@
"status": "finished",
"title": "Ad Hoc Tasks \u2014 2026-08-17",
"source_path": "workplans/ADHOC-2026-08-17.md",
"uuid": null,
"uuid": "5c6c2bbb-b944-5afd-b89c-20d865518849",
"parent_id": null,
"extra": {}
},
@ -1192,7 +1192,7 @@
{
"kind": "workplan",
"id": "WARDEN-WP-0033",
"status": "active",
"status": "finished",
"title": "Native lane handoff \u2014 review secrets-engine's catalog admission, and fix what it exposed",
"source_path": "workplans/WARDEN-WP-0033-native-lane-handoff.md",
"uuid": "4627d89b-4b00-562a-81e9-76e96f90fa7e",
@ -1232,7 +1232,7 @@
{
"kind": "task",
"id": "WARDEN-WP-0033-T04",
"status": "wait",
"status": "done",
"title": "Tasks",
"source_path": "workplans/WARDEN-WP-0033-native-lane-handoff.md",
"uuid": "5acac140-a586-5db3-b231-bbf236710786",
@ -2155,7 +2155,7 @@
"status": "finished",
"title": "Ad Hoc Tasks \u2014 2026-07-07",
"source_path": "workplans/archived/260707-ADHOC-2026-07-07.md",
"uuid": null,
"uuid": "90568b1e-8395-5c67-9c69-851ed08ff3d3",
"parent_id": null,
"extra": {}
},
@ -2172,17 +2172,18 @@
{
"kind": "intake",
"id": "WARDEN-IN-0001",
"status": "open",
"status": "closed",
"title": "Assent requested: Staff layer, doctrine vs runbook, and the access lane/rule demarcation",
"source_path": "intakes/intakes.md",
"uuid": null,
"uuid": "01a049ed-bbbc-7520-bc7c-6b0912ca534a",
"parent_id": null,
"extra": {
"record": {
"id": "WARDEN-IN-0001",
"kind": "intake",
"title": "Assent requested: Staff layer, doctrine vs runbook, and the access lane/rule demarcation",
"status": "open",
"status": "closed",
"outcome": "assented",
"origin": "cross-repo",
"origin_ref": "gate-house GH-DEC-2026-001",
"priority": "medium",
@ -2190,8 +2191,35 @@
"requested_by": "gate-house",
"standard": "net-kingdom/canon/standards/security-layer-model_v0.1.md",
"description": "gate-house asks ops-warden to assent to three boundary items. (1) ops-warden is Staff, bound by the rule that Staff acts only through Engine APIs and never touches Tooling directly (standard section 5). (2) Doctrine versus runbook: the NetKingdom Security Literacy section in ops-warden INTENT is evidence the security curriculum had no owner; it now has one in gate-house. Proposal is that doctrine and curriculum move to gate-house and that section becomes lane-specific runbooks referencing gate-house doctrine rather than restating it. ops-warden keeps the lanes it stewards and everything operational about them. (3) The access lane/rule demarcation, normative in standard section 8: ops-warden and ops-mason own access lanes \u2014 how a worker reaches a host; access-engine owns access rules \u2014 whether they may. This demarcation is the condition attached to renaming flex-auth to access-engine, so ops-warden effectively holds a veto on that name. Also requested: add gate-house to the Security Literacy and routing tables \u2014 currently every plane is listed and gate-house appears nowhere \u2014 routing doctrine and authority-model questions there while continuing to route policy decisions to access-engine. If moving the curriculum out leaves ops-warden unable to instruct its own workers, say so; the boundary is wrong if it does.",
"notes": "Assented to all three items in ADR-0010, with reasoning in history/2026-08-28-security-layer-model-assent.md. (1) Staff accepted; the section 5 binding rule exposed a real non-conformance \u2014 src/warden/vault.py is a direct OpenBao client performing a write, as is warden desk's bao kv put. Declared in INTENT.md as an engine gap with intended owner secrets-engine and blocker \"no engine exposes an SSH-CA surface\", not negotiated as an exemption; taint.py declared under the read-only allowance; warden access proxies run under the caller's identity. An amendment is offered back to gate-house: a second sanctioned shape in section 5 for a declared engine gap carrying intended owner, blocker and review date, machine-readable so section 10 can tell a tracked gap from an undeclared violation. (2) Doctrine versus runbook accepted; the literacy section is now a lane routing runbook referencing gate-house doctrine. Answering gate-house's test question: it does not leave ops-warden unable to instruct its workers, because what instructs them is warden plan / warden route and .claude/rules/credential-routing.md, which stays inline by design. (3) The lane/rule demarcation assented unconditionally and the access-engine veto not exercised \u2014 ops-warden already consumes decisions and renders none. One request on sequencing only: a deprecation window in which both names resolve (598 references across 82 files here). gate-house added to the routing tables in INTENT.md and SCOPE.md.",
"created": "2026-08-28T19:30:28.087109Z",
"updated": "2026-08-28T19:30:28.087109Z"
"updated": "2026-08-28T21:05:00Z",
"state_hub_intake_id": "01a049ed-bbbc-7520-bc7c-6b0912ca534a"
}
}
},
{
"kind": "intake",
"id": "WARDEN-IN-0002",
"status": "open",
"title": "Review requested: security layer model v0.3 \u2014 and does maturity-engine absorb warden route gaps?",
"source_path": "intakes/intakes.md",
"uuid": null,
"parent_id": null,
"extra": {
"record": {
"id": "WARDEN-IN-0002",
"kind": "intake",
"title": "Review requested: security layer model v0.3 \u2014 and does maturity-engine absorb warden route gaps?",
"status": "open",
"origin": "cross-repo",
"origin_ref": "net-kingdom security-layer-model_v0.3",
"priority": "medium",
"owner": "ops-warden",
"requested_by": "gate-house",
"description": "v0.3 is proposed and changes sections 4, 9 and 13 only; the v0.2 assent record stands. Two new engines: approval-engine (section 9.4) and maturity-engine (section 9.5). THE QUESTION FOR YOU concerns section 5.3, which exists because you offered the amendment. v0.3 gives declared gaps an owner: maturity-engine takes the gap register with intended_owner, blocked_on and review dates, and section 13 now says the register in the standard is interim and should not outlive that engine. You offered warden route gaps and the 27 delegation catalog entries as reusable prior art. So the question is whether that machinery should MOVE, be MIRRORED, or STAY. Our tentative reading, which we want tested rather than accepted: routing is yours and stays yours \u2014 warden route find answers where a credential need goes, and that is lane knowledge, not maturity. What might move is the readiness half: whether a declared gap is still within its review date, and whether an intended owner has an engine surface yet. If splitting those creates two sources for one fact, that is worse than either option and we would rather hear it now. Your SSH-CA signing write would be tracked in maturity-engine as a declared gap with intended owner secrets-engine and a review date \u2014 that is reporting your own non-conformance to an engine, so we would rather you assent to it than discover it. Also note approval-engine (section 9.4): it owns the approval object, not the approval workflow, so ops-warden lanes needing approval consume a claim rather than implementing one. Assent, revision, or rejection acceptable.",
"created": "2026-08-28T20:40:24.957468Z",
"updated": "2026-08-28T20:40:24.957468Z"
}
}
}
@ -2201,15 +2229,15 @@
"type": "repo.command.applied",
"command": "repo.work.create_intake",
"operation": "create",
"correlation_id": "65d40cdd-5894-440e-9c95-c6bcfe259b66",
"correlation_id": "676e92a9-dc2e-4101-a31c-a584962c25df",
"kind": "intake",
"id": "WARDEN-IN-0001",
"git_sha": "467635e84b99757336ee49d7f0dbf107607d0560",
"id": "WARDEN-IN-0002",
"git_sha": "f289465b90fcdffe2c8bf4a51cc71607747126f9",
"files_touched": [
"intakes/intakes.md"
],
"source": "repo-manager",
"emitted_at": "2026-08-28T19:30:29.829125Z"
"emitted_at": "2026-08-28T20:40:26.143617Z"
}
]
}

View file

@ -3,8 +3,9 @@
> **Layer: Staff.** ops-warden is a Staff repository under the NetKingdom
> IT-security layer model (Taxonomy -> Tooling -> Engines -> Staff, layered by
> determinism and by the kind of artifact each layer produces):
> `net-kingdom/canon/standards/security-layer-model_v0.1.md`, owned by gate-house,
> ratified as `gate-house/decisions/decisions.md` GH-DEC-2026-001.
> `net-kingdom/canon/standards/security-layer-model_v0.4.md` (**accepted**), owned by
> gate-house, ratified as `gate-house/decisions/decisions.md` GH-DEC-2026-001.
> ops-warden's assent is recorded there as `ADR-0010`.
>
> **The binding rule: Staff never touches Tooling directly. It acts only through
> Engine APIs.** ops-warden holds no state another layer depends on at runtime and
@ -19,8 +20,15 @@
> runs the owner's tool under **the caller's** identity and supplies no authority of
> its own (`ADR-0002`).
>
> Assent, reasoning, and the amendment offered back to gate-house:
> `history/2026-08-28-security-layer-model-assent.md` and `ADR-0010`.
> **The declaration is `layer.yaml`, not this note.** v0.4 §5.3 requires a gap to
> carry `capability`, `intended_owner`, `blocked_on` and `review` *machine-readably*,
> and §11 makes "every direct Tooling client maps to a declared §5.1/§5.2/§5.3 entry"
> a mechanical check. `layer.yaml` is that map; `scripts/check_layer_conformance.py`
> and `tests/test_layer_conformance.py` enforce it, so a new undeclared OpenBao
> client fails on the commit that introduces it.
>
> Assent, reasoning, and the amendment offered back to gate-house — adopted as
> v0.4 §5.3: `history/2026-08-28-security-layer-model-assent.md` and `ADR-0010`.
> This file captures **why this repository exists**, the **direction it is
> moving toward**, and the **kind of system it is meant to become**.

123
layer.yaml Normal file
View file

@ -0,0 +1,123 @@
# ops-warden — NetKingdom security layer declaration
#
# Framework: net-kingdom/canon/standards/security-layer-model_v0.4.md
# Assent: docs/adr/ADR-0010 (ops-warden's own voice, per §11 "who must declare")
# Validate: python3 scripts/check_layer_conformance.py
#
# §11 makes one check mechanical: "every direct Tooling client in a Staff
# repository maps to a declared §5.1, §5.2, or §5.3 entry". This file is that
# map. It is machine-readable because §5.3 requires it to be — ops-warden
# proposed that shape and is implementing it rather than declaring in prose.
#
# Conformance rule inherited from tenancy.yaml: accuracy, not altitude. A
# declared gap is TRACKED NON-CONFORMANCE (§11), never a claim of conformance.
schema_version: "0.1"
framework: netkingdom-security-layer-model
standard_version: "0.4"
repository: ops-warden
layer: staff
declared_by: docs/adr/ADR-0010
declared_at: "2026-08-29"
# Every direct contact with a Tooling-layer system (§4), one entry each.
tooling_contacts:
- id: ssh-ca-signing-write
shape: "5.3" # declared engine gap
module: src/warden/vault.py
symbol: VaultCA.sign
tooling: OpenBao
operation: "HTTP POST <addr>/v1/<mount>/sign/<role> with X-Vault-Token"
write: true
capability: "Sign a short-lived SSH certificate for an adm/agt/atm actor"
intended_owner: secrets-engine
blocked_on: >-
No engine exposes an SSH certificate signing surface. secrets-engine owns
credential abstraction, custody and lifecycle, which is the layer this
belongs in, but fronts no SSH-CA API today.
review: "2026-11-28"
note: >-
Production-verified and in daily use. This is the one lane ops-warden owns
permanently (§4). Signing continues while the gap is open: refusing would
remove production host access to close a documentation gap.
- id: desk-paste-once-provision
shape: "5.3"
module: src/warden/desk.py
symbol: _provision_to_openbao
tooling: OpenBao
operation: "bao kv put <path> <field>=- (value on stdin, never argv)"
write: true
capability: "Founder paste-once provisioning of a secret straight into OpenBao"
intended_owner: secrets-engine
blocked_on: >-
No engine exposes an attended provisioning surface for a value the founder
holds and no automated path can produce.
review: "2026-11-28"
note: >-
Attended and founder-operated (WP-0029). The value reaches OpenBao without
passing through a terminal, an argv, or the audit log.
- id: taint-metadata-read
shape: "5.1" # read-only diagnostic observation
module: src/warden/taint.py
symbol: fetch_taint_status
tooling: OpenBao
operation: "bao kv metadata get -format=json <path>"
write: false
capability: "Report EXPOSED taint (custom_metadata) without reading secret data"
intended_owner: secrets-engine
blocked_on: >-
No engine exposes a disclosure-taint query. Metadata-only by construction —
reading the data would be the 2026-07-16 vector this exists to avoid.
review: "2026-11-28"
- id: access-proxy-conduit
shape: "5.2" # conduit
module: src/warden/proxy.py
symbol: proxy_fetch, proxy_attended_login_exec
tooling: OpenBao, key-cape
operation: "Runs the catalog-declared owner fetch_command as a child process"
write: false
capability: "warden access --fetch/--exec/--out/--wrap for exec_capable lanes"
supplied_authority: none
evidence:
no_own_credential: src/warden/proxy.py::_caller_env
test: tests/test_proxy.py::test_conduit_supplies_no_authority_of_its_own
audit: "audit.jsonl records the caller, the lane, and the outcome; never a value"
note: >-
The §5.2 test is the supplied-authority property: ops-warden presents no
credential of its own, cannot widen what the caller could already do, and
the action reconstructs as the caller's. Governed by ADR-0002.
- id: caller-identity-token
shape: "5.2"
module: src/warden/caller_identity.py
symbol: resolve_caller_token
tooling: OpenBao
operation: "Runs the operator-configured caller_auth command, or reads token env"
write: false
capability: "Establish the caller's own identity for the pre-sign policy gate"
supplied_authority: none
detection: voluntary # runs an operator-configured command, so no fixed
# argv shape to scan for; declared rather than omitted
note: >-
Obtains the CALLER's credential by the operator's configured means; adds no
authority. Never mints, and never persists what it resolves.
# Contacts that are deliberately NOT Tooling contacts, recorded so the check is
# total rather than silently selective.
non_tooling_clients:
- module: src/warden/policy.py
target: access-engine (flex-auth)
rationale: "Engine API — §5 permits it; this is the shape §5 prescribes."
- module: src/warden/worker.py
target: state-hub, llm-connect
rationale: >-
Not catalogued in §4. The layer catalog scopes the security estate, and
neither the State Hub nor llm-connect appears in it, so no §5 shape applies
on the standard's own terms. Raised with gate-house 2026-08-29 as a scope
question rather than resolved unilaterally — see the assessment note.

View file

@ -10,11 +10,11 @@
# declares it, and is null where the field set has not been established --
# null means unknown, never 'one field'.
generated_at: "2026-08-28T19:58:57Z"
generated_at: "2026-08-29T00:44:23Z"
source: ops-warden/registry/routing/catalog.yaml
catalog_revision: "c374d41a4925720ae7706dfda020537b60a07eb9"
catalog_revision_date: "2026-08-23T21:43:12+02:00"
catalog_dirty: true
catalog_revision: "ee94c189385271194f22a53998b2c6e12e961ea9"
catalog_revision_date: "2026-08-28T22:00:09+02:00"
catalog_dirty: false
high_risk_lane_count: 23
concrete_path_count: 14

View file

@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""Check ops-warden against the NetKingdom security layer model (§5, §11).
Read-only. Makes §11's second mechanical check real:
every direct Tooling client in a Staff repository maps to a declared
§5.1, §5.2, or §5.3 entry
The failure this catches is a *new* direct OpenBao contact appearing in
src/warden/ without an entry in layer.yaml an undeclared violation (§11),
which is a finding rather than a tracked gap. It deliberately does NOT check
the review dates: a date-triggered failure breaks the build on a calendar day
with no code change (the reasoning recorded in WARDEN-WP-0033-T05), so
staleness is reported and left to `--report`, never to CI.
Exit 0 clean, 1 undeclared contact found, 2 declaration malformed.
"""
from __future__ import annotations
import argparse
import re
import sys
from datetime import date
from pathlib import Path
import yaml
ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "src" / "warden"
DECL = ROOT / "layer.yaml"
VALID_SHAPES = {"5.1", "5.2", "5.3"}
# A direct Tooling contact is an *invocation*, not a mention. Matching the word
# "bao" caught help text, a docstring, and the dev-tier doubles library that
# simulates bao rather than calling it — three false positives on first run.
# So match the two shapes that actually execute:
# 1. an HTTP request built against the OpenBao address
# 2. an argv list whose first element is the bao binary
TOOLING_PATTERNS = (
# httpx call whose URL is built from the configured OpenBao/Vault address
re.compile(r"""\bhttpx\.\w+\(|url\s*=\s*f?["'].*\{self\._cfg\.addr\}"""),
# argv construction: [bao_bin, ...] / ["bao", ...] / [bao_binary, ...]
re.compile(r"""\[\s*(?:["']bao["']|bao_bin\b|bao_binary\b)\s*,"""),
)
# httpx alone is not a Tooling contact — policy.py calls an Engine and worker.py
# calls the State Hub. A module matching only the httpx pattern counts as a
# contact only if it also references the OpenBao address configuration.
ADDR_HINT = re.compile(r"""_cfg\.addr|VAULT_ADDR|BAO_ADDR""")
# Modules that talk to an Engine or to something outside the §4 catalog. Listed
# in layer.yaml under non_tooling_clients and excluded from the scan with it.
def _excluded(decl: dict) -> set[str]:
return {e["module"].split("/")[-1] for e in decl.get("non_tooling_clients", [])}
def load_declaration() -> dict:
if not DECL.exists():
print(f"MISSING: {DECL} — ops-warden must declare in its own voice (§11)")
raise SystemExit(2)
decl = yaml.safe_load(DECL.read_text())
for key in ("layer", "repository", "standard_version", "tooling_contacts"):
if key not in decl:
print(f"MALFORMED: layer.yaml has no {key!r}")
raise SystemExit(2)
for c in decl["tooling_contacts"]:
if c.get("shape") not in VALID_SHAPES:
print(f"MALFORMED: {c.get('id')} has shape {c.get('shape')!r}, not one of {sorted(VALID_SHAPES)}")
raise SystemExit(2)
# §5.3 carries four fields, machine-readably. That is the whole point of
# the shape; a gap missing them is prose wearing a schema.
if c["shape"] == "5.3":
for field in ("capability", "intended_owner", "blocked_on", "review"):
if not c.get(field):
print(f"MALFORMED: §5.3 entry {c['id']!r} is missing {field!r}")
raise SystemExit(2)
# §5.2's test is the supplied-authority property.
if c["shape"] == "5.2" and c.get("supplied_authority") != "none":
print(f"MALFORMED: §5.2 conduit {c['id']!r} must declare supplied_authority: none")
raise SystemExit(2)
return decl
def scan_modules() -> dict[str, list[int]]:
"""Return {module_name: [line numbers]} for direct Tooling contacts."""
found: dict[str, list[int]] = {}
for path in sorted(SRC.rglob("*.py")):
if path.name.startswith("test_"):
continue
text = path.read_text()
hits: list[int] = []
for n, line in enumerate(text.splitlines(), 1):
stripped = line.strip()
if stripped.startswith("#") or stripped.startswith('"'):
continue
if any(p.search(line) for p in TOOLING_PATTERNS):
hits.append(n)
if hits:
# An httpx-only match needs the OpenBao address to be a Tooling
# contact; otherwise it is an Engine or non-catalogued call.
argv_shape = any(TOOLING_PATTERNS[1].search(ln) for ln in text.splitlines())
if argv_shape or ADDR_HINT.search(text):
found[path.name] = hits
return found
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--report", action="store_true", help="also print the declaration and gap review dates")
args = ap.parse_args()
decl = load_declaration()
declared = {c["module"].split("/")[-1] for c in decl["tooling_contacts"]}
excluded = _excluded(decl)
found = scan_modules()
undeclared = {m: lines for m, lines in found.items() if m not in declared and m not in excluded}
# A voluntary declaration has no fixed argv shape to detect (an
# operator-configured command). Over-declaring is safe; not reporting it as
# stale keeps the signal meaningful.
voluntary = {
c["module"].split("/")[-1]
for c in decl["tooling_contacts"]
if c.get("detection") == "voluntary"
}
stale_decls = declared - set(found) - voluntary
if args.report:
print(f"{decl['repository']} — layer: {decl['layer']} (model v{decl['standard_version']})")
print(f"declared by {decl['declared_by']}\n")
for c in decl["tooling_contacts"]:
line = f" §{c['shape']} {c['id']:<28} {c['module']}"
if c["shape"] == "5.3":
overdue = str(c["review"]) < date.today().isoformat()
line += f" -> {c['intended_owner']} review {c['review']}"
if overdue:
line += " [REVIEW OVERDUE]"
print(line)
gaps = [c for c in decl["tooling_contacts"] if c["shape"] == "5.3"]
print(f"\n{len(gaps)} declared gap(s) — tracked non-conformance, not conformance (§11).")
ok = True
if undeclared:
ok = False
print("\nUNDECLARED TOOLING CONTACT — a finding under §11, not a tracked gap:")
for m, lines in sorted(undeclared.items()):
print(f" src/warden/{m}: line(s) {', '.join(map(str, lines[:6]))}")
print("\nAdd a §5.1/§5.2/§5.3 entry to layer.yaml, or route it through an engine.")
if stale_decls:
print("\nNote: declared but no contact found (module removed or refactored?):")
for m in sorted(stale_decls):
print(f" {m}")
if ok and not args.report:
print(f"PASS — {len(found)} module(s) with Tooling contact, all declared.")
elif ok:
print("\nPASS — every direct Tooling contact maps to a declared shape.")
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,98 @@
"""Layer-model conformance (security-layer-model_v0.4 §5, §11).
Two things are checked here. §11 makes one of them mechanical: every direct
Tooling client maps to a declared shape. §5.2 asks for the other: the conduit's
supplied-authority property covered by a test.
Deliberately absent: any assertion on a §5.3 review date. A date-triggered
failure breaks the build on a calendar day with no code change, punishing
whoever commits next rather than whoever owns the gap the same reasoning
recorded in WARDEN-WP-0033-T05 for blocker staleness.
"""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
import yaml
ROOT = Path(__file__).resolve().parents[1]
def _decl() -> dict:
return yaml.safe_load((ROOT / "layer.yaml").read_text())
class TestDeclaration:
def test_declares_staff_layer_in_its_own_voice(self):
d = _decl()
assert d["repository"] == "ops-warden"
assert d["layer"] == "staff"
# §11: "only the repository's own file, in its own voice, conforms."
assert d["declared_by"] == "docs/adr/ADR-0010"
def test_every_tooling_contact_maps_to_a_declared_shape(self):
"""§11 mechanical check — the guard against a new undeclared client."""
result = subprocess.run(
[sys.executable, str(ROOT / "scripts" / "check_layer_conformance.py")],
capture_output=True,
text=True,
)
assert result.returncode == 0, (
f"undeclared Tooling contact — a finding under §11, not a tracked gap:\n"
f"{result.stdout}{result.stderr}"
)
def test_declared_gaps_carry_all_four_fields(self):
"""§5.3 is machine-readable or it is prose wearing a schema."""
for c in _decl()["tooling_contacts"]:
if c["shape"] == "5.3":
for field in ("capability", "intended_owner", "blocked_on", "review"):
assert c.get(field), f"{c['id']} missing {field}"
def test_gaps_are_not_counted_as_conformance(self):
"""§11: a declared gap is tracked non-conformance. Keep that visible."""
text = (ROOT / "layer.yaml").read_text()
assert "TRACKED NON-CONFORMANCE" in text.upper()
class TestConduitSuppliesNoAuthority:
"""§5.2: 'MUST NOT present its own credential, MUST NOT widen what the
caller could already do.' The standard says this SHOULD be covered by a
test; this is that test."""
def test_conduit_supplies_no_authority_of_its_own(self, monkeypatch):
from warden import proxy
monkeypatch.setenv("VAULT_TOKEN", "caller-own-token")
monkeypatch.setenv("HOME", "/home/nobody")
before = dict(os.environ)
env = proxy._caller_env()
# The child environment IS the caller's environment — nothing added,
# nothing removed, no ops-warden credential injected.
assert env == before, (
"conduit altered the caller's environment; §5.2 requires it to "
"supply no authority of its own"
)
assert env["VAULT_TOKEN"] == "caller-own-token"
def test_conduit_declares_supplied_authority_none(self):
conduits = [c for c in _decl()["tooling_contacts"] if c["shape"] == "5.2"]
assert conduits, "no §5.2 conduit declared — proxy.py is one"
for c in conduits:
assert c["supplied_authority"] == "none"
def test_proxy_holds_no_credential_constant(self):
"""A conduit that presents its own token is not a conduit (§5.2)."""
src = (ROOT / "src" / "warden" / "proxy.py").read_text()
# It may name token ENV VARS to detect caller auth; it must not carry a
# token value or mint one.
for forbidden in ("X-Vault-Token", "auth/approle/login", "token create"):
assert forbidden not in src, (
f"proxy.py references {forbidden!r} — that is presenting or "
f"minting authority, not conducting the caller's"
)