docs: mark the unknown cell, measure the coverage we asked to publish
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

gate-house ruled the v0.8 assent round (GH-DEC-2026-011, net-kingdom@64394e9):
ask 1 declined, ask 2 adopted.

Ask 1's refusal is accepted without reservation and the reason is better than
the ask -- a sanctioned transitional fail_open is indistinguishable at runtime
from the stance the rule forbids, and would make the rule optional at the only
moment it costs anything.

Ask 2 gave §13.1 a Coverage column with this repo's figures as its first
entries. Since we asked for the column, we owe it accuracy:
scripts/report_coverage.py measures both populations from the artifacts the
runtime uses (reusing the workload-join build rather than re-deriving it), and
a test asserts pep-stance.yaml's published block equals what it measures.
A hand-counted number in a register that explicitly does not recompute it
decays silently, and a stale figure beside a marked cell is worse than the
blank the other four rows carry.

pep-stance.yaml marks the unknown cell inline as a declared gap -- assent, the
measured reason for not flipping, the declined ask, WARDEN-WP-0040 as route --
and a second test keeps it marked while it is fail_open, failing when it is
flipped. standard_version stays 0.7 because that is what binds; v0.8 is
proposed, so it gains standard_version_reviewed rather than pre-adopting.

Separately, gate-house corrected GH-DEC-2026-008: the claim/decision digest
comparison it originally required is unimplementable and a fail-closed
consumer obeying it would have denied permanently. We had never copied the
wording, so nothing to unwind -- but everything they have sent about this lane
was living in an inbox thread, a bad home for a correction that only matters
when someone finally wires the consume. Now wiki/ApprovalConsumption.md,
leading with "nothing is wired", carrying the corrected target and the
attribution gap that digest matching does not discharge.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1276224@bnt-lap001
Assistant-Session: 426ec497-e1c4-4dd3-b417-dfce1ca1dbc3
This commit is contained in:
tegwick 2026-09-10 08:02:10 +02:00
parent a942ce805d
commit 5b1a508610
8 changed files with 388 additions and 2 deletions

View file

@ -35,7 +35,7 @@
| workplan | WARDEN-WP-0035 | finished | — | workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md |
| workplan | WARDEN-WP-0036 | finished | — | workplans/WARDEN-WP-0036-attended-login-openbao-output.md |
| workplan | WARDEN-WP-0037 | active | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md |
| workplan | WARDEN-WP-0038 | proposed | — | workplans/WARDEN-WP-0038-plan-mutation-intent.md |
| workplan | WARDEN-WP-0038 | finished | — | workplans/WARDEN-WP-0038-plan-mutation-intent.md |
| workplan | WARDEN-WP-0039 | blocked | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md |
| workplan | WARDEN-WP-0040 | proposed | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md |
| task | WARDEN-WP-ADHOC-2026-06-27-T01 | done | — | workplans/ADHOC-2026-06-27.md |

View file

@ -117,3 +117,59 @@ catalog stage is an accurate statement about their control, and making them
assert a zone they cannot know would trade a real fact for a conformant fiction.
The unavailability of cross-axis aggregation is better recorded than papered
over, and §13.1 now says so.
---
## Ruling — GH-DEC-2026-011, 2026-09-09
gate-house closed the assent round the same day: **ask 1 declined, ask 2
adopted**, standard amended at `net-kingdom@64394e9`, dispositioned in
`gate-house/docs/conformance/2026-09-06-v08-assent-round.md`.
**Ask 1 (a dated transitional `unknown: fail_open`) — declined**, and the reason
is better than the ask. A sanctioned transitional `fail_open` is
*indistinguishable at runtime* from the stance the rule forbids, and it would make
the rule optional at the moment of adoption — the only moment it costs anything.
Dating an intention to stop being permissive does not make `unknown` any more
expensive for an attacker to induce while the transition runs. Accepted without
reservation; the argument defeats the ask on its own terms.
**Ask 2 (coverage published beside stance) — adopted.** §13.1 now carries a
Coverage column and this repo's figures are its first entries: *signing targets
0/3 resolved; routing lanes 3/21 resolved (2026-09-09, self-measured)*. Three
guards came with it, all of which we would have wanted: coverage does not soften
the stance, does not gate it, and never makes a non-conformant cell conformant; a
blank means "not reported" and never "complete"; and the decision carries an
explicit reversal condition — if a row is ever argued to be conformant *because*
its coverage is low, the column is doing harm and comes out.
That reversal condition is the right shape and it binds us first, since the column
exists because we asked. Our row reads non-conformant **and** low-coverage; those
are two facts, and the second is not a defence of the first.
**The deadlock was recorded rather than routed around.** §6.4 obligation 3 now
states that a stricter stance is not a licence to manufacture the membership that
makes it survivable — where a scope is unknown because another repository has
published no workload-identity declaration, the consumer MUST NOT infer one. That
is `ADR-0009` rule 3 promoted into the standard at exactly the point where the
pressure lands, which is more than we asked for.
### What we did in response
- `pep-stance.yaml` marks the `unknown` cell as a **declared gap** inline, with
the assent, the measured reason for not flipping, the declined ask, and
`WARDEN-WP-0040` as its route. The stance value is unchanged — v0.8 is still
`proposed` and v0.7 remains in force, so `standard_version` stays `0.7` with a
new `standard_version_reviewed: 0.8`.
- The coverage figures are published in `pep-stance.yaml` and, because we asked
for the column, **measured rather than hand-counted**:
`scripts/report_coverage.py` derives both populations from the artifacts the
runtime uses, reusing `report_workload_join.build` rather than re-deriving the
join. `tests/test_layer_conformance.py` asserts the published block equals the
measured one, and asserts the cell stays marked while it is `fail_open`.
A hand-counted figure in a register that explicitly does not recompute it would
decay silently, and a stale number beside a marked cell is worse than the blank
the other four rows carry — a blank at least reads as "not reported".
- `WARDEN-WP-0040` T04 is closed; T01T03 stand unchanged. gate-house confirmed
they are not asking for the cell to be flipped before coverage exists.

View file

@ -18,7 +18,13 @@
schema_version: "0.1"
framework: netkingdom-security-layer-model
# v0.7 is the accepted standard and the one in force. v0.8 is `proposed`: its
# assent round closed 2026-09-09 (GH-DEC-2026-011) with nine corrections applied,
# but the version is not yet accepted, so this declaration stays pinned to what
# binds. The one cell v0.8 would change is marked inline below rather than
# silently pre-adopted.
standard_version: "0.7"
standard_version_reviewed: "0.8" # reviewed and assented; see history/2026-09-09-layer-model-v08-review.md
repository: ops-warden
pep_shape: true
declared_by: docs/adr/ADR-0009
@ -36,7 +42,26 @@ stance:
z2-protected: fail_open
z2-continuity: fail_open
z3-critical: fail_closed
unknown: fail_open # versioned build profile (ADR-0009); explicit, never inferred
# DECLARED GAP under security-layer-model v0.8 §6.4 obligation 3 (GH-DEC-2026-011,
# net-kingdom@64394e9): unknown is not a zone and MUST resolve to fail_closed.
# ops-warden ASSENTED to that rule — we went looking for the §5.1 read-only
# diagnostic its reversal clause predicts and do not have one, because this map
# governs `warden sign`, a credential-issuing side effect.
#
# The cell has not been flipped, and the reason is measured rather than argued:
# 0 of 3 signing targets resolve to a zone, so converting today would fail closed
# on essentially every certificate whenever the engine is unreachable — including
# the certificate an operator needs to reach the host and repair it. That is
# ADR-0006's rejected configuration reached from another direction.
#
# We asked for a coverage-gated transitional fail_open and were DECLINED: a
# sanctioned transitional fail_open is indistinguishable at runtime from the
# stance the rule forbids. Our second preference was adopted instead — §13.1 now
# carries a Coverage column, and this repo's figures are its first entries.
#
# So this is tracked non-conformance with a route, not an exemption.
# Route: WARDEN-WP-0040. Register row: §13.1, marked.
unknown: fail_open # non-conformant at v0.8; see above
not-applicable: fail_closed
# What happens when the stance is applied. §6.4 obligation 1 requires a decision
@ -57,6 +82,23 @@ on_apply:
# §6.4 obligation 2 — the verdict is never cached. Input claims (zone membership,
# compiled from the flex-auth registry snapshot) are cached under their own
# freshness rules; the answer is not.
# Classification coverage, published beside the stance because v0.8 §6.4
# obligation 3 now requires it and because ops-warden asked for the column.
# Coverage is DISCLOSURE, never a transitional licence: it does not soften this
# map's stance, does not gate it, and never makes the marked cell conformant.
# Self-measured; regenerate with `python scripts/report_coverage.py`.
classification_coverage:
measured: "2026-09-09"
attribution: self-measured
signing_targets:
resolved: 0
unknown: 3
not_applicable: 1
routing_lanes:
resolved: 3
unknown: 18
not_applicable: 12
verdict_caching: none
input_claim_caching: "registry/flex-auth/production_registry_snapshot.json, rebuilt by scripts/build_flex_auth_registry.py"

129
scripts/report_coverage.py Normal file
View file

@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""Measure the classification coverage published in `pep-stance.yaml`.
Read-only. No network, no OpenBao, no secret material.
Why this exists. security-layer-model v0.8 §6.4 obligation 3 requires a dated
classification-coverage figure beside each stance, and §13.1 carries a Coverage
column whose first entries are ops-warden's — because ops-warden asked for the
column after measuring that 0 of 3 signing targets resolve to a zone.
Having asked for it, we own the figure's accuracy. A hand-counted number in a
published register decays silently: the register explicitly does not compute
anyone's coverage, and a stale figure beside a marked cell is worse than a blank,
which at least reads as "not reported". So the figure is measured from the same
two artifacts the runtime uses, and `tests/test_layer_conformance.py` asserts the
published block equals what this reports the same property that makes the stance
map worth publishing (`pep-stance.yaml` equals `PolicyConfig.failure_modes` by
test), applied one level up.
Two populations, deliberately not summed. They answer different questions and
share no denominator:
signing targets actor resources in the flex-auth registry snapshot. This is
the population the stance map actually governs: `warden sign` resolves a zone
per actor resource, and an unresolved one takes the `unknown` cell.
routing lanes catalog entries with an explicit workload reference. Wider than
the stance map's reach, and the figure that shows *why* coverage is low: an
unknown lane is almost always another repository's undeclared workload
identity, which `ADR-0009` rule 3 forbids closing by inference here.
Usage:
python scripts/report_coverage.py [--json]
Exit: 0 always. This reports; it does not gate. Coverage is disclosure, never a
transitional licence, and a script that failed on low coverage would be arguing
the case v0.8 declined.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
SNAPSHOT = REPO / "registry" / "flex-auth" / "production_registry_snapshot.json"
NOT_APPLICABLE = "not-applicable"
def signing_target_coverage(snapshot_path: Path = SNAPSHOT) -> dict[str, int]:
"""Zone resolution across the actor resources `warden sign` can name."""
registry = json.loads(snapshot_path.read_text())
resolved = unknown = not_applicable = 0
for manifest in registry.get("resource_manifests") or []:
for resource in manifest.get("resources") or []:
if str(resource.get("type")) != "ssh-certificate":
continue
attributes = resource.get("attributes") or {}
admission = str(attributes.get("security_zone_admission") or "unknown")
zone = str(attributes.get("security_zone") or "unknown")
if admission == NOT_APPLICABLE:
not_applicable += 1
elif zone == "unknown":
unknown += 1
else:
resolved += 1
return {"resolved": resolved, "unknown": unknown, "not_applicable": not_applicable}
def routing_lane_coverage(estate_root: Path | None = None) -> dict[str, int]:
"""Workload resolution across catalog lanes.
Delegates to `report_workload_join.build` rather than re-deriving the join: two
implementations of "is this lane resolved" would drift, and the published figure
should be the one the join report shows.
"""
import importlib.util
spec = importlib.util.spec_from_file_location(
"report_workload_join", Path(__file__).resolve().parent / "report_workload_join.py"
)
join = importlib.util.module_from_spec(spec)
spec.loader.exec_module(join)
report = join.build(
REPO / "registry" / "routing" / "catalog.yaml",
estate_root if estate_root is not None else Path.home(),
)
return {
"resolved": len(report["resolved"]),
"unknown": len(report["unknown"]),
"not_applicable": len(report["not_applicable"]),
}
def measure(estate_root: Path | None = None) -> dict[str, Any]:
return {
"signing_targets": signing_target_coverage(),
"routing_lanes": routing_lane_coverage(estate_root),
}
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--json", action="store_true", dest="as_json")
args = parser.parse_args(argv)
report = measure()
if args.as_json:
print(json.dumps(report, indent=2))
return 0
for name, counts in report.items():
total = sum(counts.values())
scoped = counts["resolved"] + counts["unknown"]
print(f"{name.replace('_', ' ')}: {counts['resolved']}/{scoped} resolved "
f"({counts['unknown']} unknown, {counts['not_applicable']} not-applicable, "
f"{total} total)")
print()
print("Coverage is disclosure, not a transitional licence (v0.8 §6.4 obligation 3).")
print("Publish in pep-stance.yaml; §13.1 does not compute it for you.")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -150,3 +150,52 @@ class TestPepStanceMap:
assert cadence["observed_window"]["signature_records"] == 3
assert cadence["observed_window"]["active_signature_days"] == 2
assert cadence["reason"]
# --- classification coverage (v0.8 §6.4 obligation 3) -------------------------
def test_published_coverage_equals_measured_coverage():
"""The published figure must equal what the repo actually measures.
ops-warden asked gate-house for §13.1's Coverage column and its figures are
that column's first entries, so their accuracy is ours to hold. The register
explicitly does not compute anyone's coverage, and a stale number beside a
marked cell is worse than a blank -- a blank at least reads as "not reported".
This is the same property that makes the stance map worth publishing (the map
equals PolicyConfig.failure_modes by test), applied one level up.
"""
import importlib.util
repo = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location(
"report_coverage", repo / "scripts" / "report_coverage.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
published = yaml.safe_load((repo / "pep-stance.yaml").read_text())[
"classification_coverage"
]
measured = module.measure()
for population in ("signing_targets", "routing_lanes"):
assert published[population] == measured[population], population
def test_the_unknown_cell_is_marked_as_a_declared_gap():
"""A non-conformant cell must say so where it is declared, not only in a review.
§11's marking obligation, which ops-warden argued for in the v0.6 round and
then acquired a marked cell under. If the cell is ever flipped to fail_closed
this test fails, which is the correct time to remove the marking.
"""
repo = Path(__file__).resolve().parents[1]
text = (repo / "pep-stance.yaml").read_text()
stance = yaml.safe_load(text)["stance"]
if stance["unknown"] == "fail_open":
assert "DECLARED GAP" in text
assert "WARDEN-WP-0040" in text
else:
assert stance["unknown"] == "fail_closed"

View file

@ -0,0 +1,77 @@
# Approval consumption — notes for the signing lane's future consume
**Status:** notes only. **Nothing is wired.** ops-warden's pre-sign gate consults
flex-auth (`wiki/PolicyGatedSigning.md`); it does not yet consume an approval
artifact. This page exists so the corrections we have already been given survive
until it does, rather than sitting in an inbox thread.
Owner of the contract: `gate-house`, `docs/contracts/approval-consumption.md`.
ops-warden is one of two PEP-shaped consumers named there (the SSH signing lane).
## Validate across two artifacts, never one
`GH-DEC-2026-005` settles the step-1 artifact: `GET /v1/approvals/{id}/claim`
serves the **approval-claim**. flex-auth's proposed `ActionAuthorization` is not
required there and will not be served from that endpoint.
| Artifact | Carries |
| --- | --- |
| approval-claim (step 1) | the approval fact — binding digest, validity window, consumption state, freshness, issuer |
| `DecisionEnvelope` (step 2) | exact `CheckRequest` match, policy pin |
v0.8 §6.4 obligation 5 hardened this: each artifact **must** be validated against
the layer that owns its data, and a PIP **must not** republish the PDP's decision.
Neither artifact may be taken from the other.
## Three things that are easy to get wrong
**1. Do not require `provenance.authority == 'state-hub'`.**
State Hub is a read model with no runtime approval authority. This is easy to get
wrong precisely *because* the hub is where everything else about a workplan lives,
so a consumer reaches for it as the authority on approval too.
Worth holding as a shape rather than an instance: a PEP requiring the hub to have
asserted an approval is the same boundary violated from the **consuming** side
that "never register workplans or tasks by hand" forbids from the **writing**
side. Same rule, two directions.
**2. The digest comparison target was corrected. Use `binding.approval_binding_digest`.**
`GH-DEC-2026-008` as originally issued required the claim's `pdp_digest` to equal
the decision's `request_digest`. That is unimplementable — a claim travelling
inside a hashed request cannot name the digest of the request containing it — and
a fail-closed consumer obeying it would have denied permanently. Corrected by
`FLEX-DEC-2026-007` to the PDP's published **exclusion-scoped** digest,
`binding.approval_binding_digest`.
ops-warden never copied the superseded wording anywhere (checked 2026-09-09), so
there was nothing to unwind. Recorded here so it cannot be reintroduced from an
older reading of the contract.
**3. Digest comparison does not discharge attribution, and attribution is not
available today.**
`GH-DEC-2026-010` rules that a PEP must be able to **attribute** a decision to
access-engine, and that matching digests does not achieve that. flex-auth's
envelope is unsigned and its pins serve plain HTTP, so that half is a declared
§13 gap owned by access-engine until `FLEX-WP-0024` lands.
Nothing for ops-warden to build. But when this lane is wired, its validation is
**complete-minus-attribution** and must be described that way: fail-closed
protects against a decision point that is absent, not against one that lies.
## Open dependency
`WARDEN-WP-0039-T03` — whether an admitted flex-auth contract exists for a
delegated credential read where caller and resource owner differ. If the answer
changes the caller shape it may reach how this lane's consume is built, and
`gate-house` has asked to hear that before rather than after: a contract admitted
for that shape would reach §6.4 obligation 5, and whether such a delegation
attenuates or widens is doctrine they own.
## See also
- `wiki/PolicyGatedSigning.md` — the pre-sign gate as it actually is today
- `pep-stance.yaml` — unreachable-engine stance, and the marked `unknown` cell
- `history/2026-09-09-layer-model-v08-review.md` — the v0.8 assent round

View file

@ -192,6 +192,14 @@ Rollback is a versioned profile or registry rollback. Do not reintroduce
`policy.enabled: false`: that would erase per-zone evidence and recreate the
global control ADR-0009 supersedes.
## Approval consumption (not wired)
This lane does not yet consume an approval artifact. The corrections already
received about how it must validate when it does — two artifacts never taken from
each other, the corrected `binding.approval_binding_digest` comparison target, and
the attribution gap that digest matching does not discharge — are held in
`wiki/ApprovalConsumption.md` so they are not lost between now and then.
## See also
- `tenancy.yaml`

View file

@ -125,3 +125,28 @@ records classification coverage alongside stance, so a register row cannot read
If both are declined, that is an answer and this workplan proceeds unchanged —
the cell stays a **declared gap** under §11 with this workplan as its route,
which is what the `declared-gap` mark (our own v0.6 finding) exists to express.
**Answered 2026-09-09 — GH-DEC-2026-011.** Ask 1 declined, ask 2 adopted;
standard amended at `net-kingdom@64394e9`.
Ask 1's refusal is correct and we are not appealing it: a sanctioned transitional
`fail_open` is indistinguishable at runtime from the stance the rule forbids, and
would make the rule optional at the only moment it costs anything. Ask 2 gave us
what we actually needed — §13.1 carries a Coverage column with this repo's
figures as its first entries, guarded so that coverage never softens a stance,
never gates one, and never makes a non-conformant cell conformant.
The reversal condition on that column binds ops-warden first, since the column
exists because we asked: if a row is ever argued conformant *because* its coverage
is low, the column comes out. Our row reads non-conformant and low-coverage, and
the second is not a defence of the first.
gate-house confirmed explicitly that they are **not** asking for the cell to be
flipped before coverage exists, and that this workplan's order is right. The cell
stays a declared gap under §11 with WP-0040 recorded as its route — the outcome
this task named as acceptable if both asks were declined, reached with one of them
adopted.
T01T03 are unchanged and still gate the conversion. Coverage is now measured
rather than asserted (`scripts/report_coverage.py`), so T02's reporting obligation
has a tool behind it and the published figure cannot drift from the register's.