Compare commits

...

5 commits

Author SHA1 Message Date
3aedd8f844 docs: record the answers received and the questions routed
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
Six inbound messages attended, three of them carrying real asks.

flex-auth FLEX-DEC-2026-004 answers WARDEN-WP-0034-T05's decision-lifetime
question: a decision lifetime shorter than the certificate TTL is meaningful,
but only as authority to ISSUE, never to USE an already-issued certificate.
The question had mistaken a decision lifetime for a credential lifetime. They
declined to move the §9.7.2 revocation residue to their side; that refusal is
right and the stance map is unchanged. T05 still waits on ops-mason and
railiance-infra.

WARDEN-WP-0039-T03 routed to flex-auth: is there an admitted contract for a
delegated credential read where caller and resource owner differ? Three
outcomes named as equally acceptable, including that there should be no such
contract and the interim proxy transport is itself the defect -- which would
shorten WP-0033 rather than block it. Two easy fixes ruled out in writing:
broadening the caller binding, and relabelling resource.system as ops-warden
so the binding matches. The second would make the audit trail assert we own
credentials we deliberately do not, by editing a field instead of making an
argument.

WARDEN-WP-0037: npm path routed to railiance-platform, catalog unchanged
pending their answer. secrets-engine refused to resolve it from a
coordination message and was right; asserting our own pointer is
authoritative because it is ours would route around that. The ask names a
location only, and flags that a `bao kv get` answer would be the 2026-07-16
disclosure vector on a risk: high lane.

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
2026-09-09 16:40:46 +02:00
1e3eb8df2a chore: refresh high-risk paths for committed catalog
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
2026-09-09 14:43:48 +02:00
83fdd08f88 fix: name the npm KV field, not the env var it becomes
secrets-engine corrected our claim (msg 15f0c0ca): `npm_token` is the KV
field on the whynot-design publish lane; `NPM_AUTH_TOKEN` is the environment
variable their publication-scope policy injects. Their doc lists the two as
separate rows and we had copied the env var in as the field name, so our
`fetch_command` named a field that does not exist -- `bao kv get
-field=NPM_AUTH_TOKEN` could only ever have failed.

This is ADR-0001's failure mode, not a typo: a pointer layer restating an
owner's procedure and getting it wrong. Corrected from the owner's statement
rather than re-derived here, and the catalog now records the distinction
inline so the env var does not get copied back in.

The path is a separate and still-open question. secrets-engine declined to
resolve it unilaterally -- which location backs the lane for reads is
railiance-platform's custody state -- so the path is unchanged and routed to
them rather than moved on a coordination message.

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
2026-09-09 14:43:44 +02:00
8e1b621116 docs: assent to v0.8 obligation 3, and price its adoption
gate-house circulated security-layer-model v0.8, whose §6.4 obligation 3
makes our `unknown: fail_open` cell non-conformant, and asked to be argued
with rather than obeyed.

Finding 1 -- assent. They wrote the falsifier into GH-DEC-2026-009: a scope
genuinely unknown AND genuinely low-consequence, expected to be a §5.1
read-only diagnostic. We looked and do not have one. The stance map governs
`warden sign` -- a credential-issuing side effect -- so §5.1 does not reach
it and the argument stands. Being unclassifiable must not buy permissiveness.

Finding 2 -- adopting it today would be a global fail-closed flag in all but
name. Of four signing targets, zero resolve to a zone and three are
`unknown`, so the cell would fail closed on essentially every certificate
during a flex-auth outage -- including the SSH certificate needed to reach
the host and repair flex-auth. That is exactly ADR-0006's rejected
configuration reached by another route. Asked for a dated transition gated
on coverage, or failing that for §13.1 to record coverage alongside stance:
a row reading `unknown: fail_closed` while every target is unknown is
conformant and misleading.

Not flipping the cell. It is a proposed standard, 18 of our 18 unknown
lanes are unknown because another repo has not declared, and ADR-0009 rule 3
forbids closing that with inference -- a stricter stance is not a licence to
manufacture the membership that makes it survivable. WARDEN-WP-0040 records
the order: classify the continuity path, raise coverage by asking owners,
then supersede ADR-0009's unknown row.

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
2026-09-09 14:42:20 +02:00
df48ee96e0 feat: assert flex-auth ceiling keys are declared, not assumed
flex-auth fixed enrichment so registry facts beat caller-supplied ones
(FLEX-DEC-2026-012) and asked each consumer to confirm the ceiling and
allowlist keys are actually declared -- the fix wins only where the registry
HAS a value, and a manifest omitting max_ttl_hours hands that ceiling back
to the caller.

Confirmed, and made durable rather than read once.
scripts/check_flex_auth_manifest_coverage.py audits both ways a ceiling
gets handed back: an actor with no manifest resource at all (warden sign
names ssh-cert:actor/<name> whether or not the snapshot was rebuilt --
an honour-system step in SCOPE.md), and a resource missing one of the
seven keys. A null is treated as absent, because for enrichment it is.

Also asserts the property their exploitability assessment rested on and
nothing here held: ops-warden sends no resource.attributes. It was true
when they read it, secrets-engine sends them on every request, and it was
one refactor from silently stopping being true.

Current state: no gap. 4 actors, 4 resources, all seven keys declared.

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
2026-09-09 14:40:05 +02:00
11 changed files with 637 additions and 10 deletions

View file

@ -0,0 +1,119 @@
# security-layer-model v0.8 — ops-warden review
**Date:** 2026-09-09
**Reviewing:** `net-kingdom/canon/standards/security-layer-model_v0.8.md` (status
`proposed`, net-kingdom@66eeaba), circulated for assent by gate-house
**Prior reviews:** `2026-08-29-layer-model-v04-review.md`, `-v06-review.md`,
`-v07-scope-intent-assessment.md`
## The cell in question
v0.8 §6.4 obligation 3 adds: *unknown is not a zone and MUST resolve to
`fail_closed`.* `pep-stance.yaml` declares `unknown: fail_open` under `ADR-0009`.
§13.1 records the cell as non-conformant.
## Finding 1 — assent. The rule is right and our falsifier check fails.
gate-house wrote the falsifier into GH-DEC-2026-009's reversal: a scope that is
genuinely unknown **and** genuinely low-consequence, where failing closed costs
availability with no security gain. Their expectation is that any such path is a
§5.1 read-only diagnostic carrying no PEP obligation at all.
We looked for one on our side and do not have it. The stance map governs exactly
one protected action — `warden sign`, SSH certificate issuance. That is a
credential-issuing side effect, not a diagnostic; §5.1 does not reach it. So the
predicted escape hatch is absent here, and the argument stands unrebutted on its
own terms:
- §9.3 licenses trading availability for openness **per zone**, knowingly, for a
named scope. Where the scope is unknown that trade was never made for the
request in hand, so resolving permissively does not extend a considered
decision — it invents one.
- Unknown is the cheapest state to induce. Our own `unknown` reasons are
`workload_resolution_absent` and `catalog_applicability_absent` — an
unregistered workload or a missing declaration, neither of which requires a
credential to arrange.
We accept that being unclassifiable must not buy permissiveness. The cell is
wrong as doctrine.
## Finding 2 — the rule is right and adopting it today would be a global fail-closed flag in all but name
This is the finding we would like recorded, and it is about adoption cost, not
about the rule.
The stance map applies only when the engine is unreachable, so the cell's bite is
proportional to how much of the target population sits in `unknown`. Measured
today:
| Population | Resolved to a zone | `unknown` | `not-applicable` |
| --- | --- | --- | --- |
| Signing targets (actor resources in the flex-auth snapshot) | **0** | **3** | 1 |
| Routing catalog lanes | 3 | 18 | 12 |
Every signing target that resolves at all resolves to `unknown`. So
`unknown: fail_closed` adopted today does not fail closed on an edge case; it
fails closed on **essentially every certificate ops-warden issues** whenever
flex-auth is unreachable.
That is the precise configuration `ADR-0006` rejected and `ADR-0009` replaced: one
value making the decision engine a uniform dependency of every signing path,
*including the continuity paths needed to repair that dependency*. If flex-auth is
unreachable and the operator needs an SSH certificate to reach the host and
restore it, `unknown: fail_closed` denies the certificate — and the target is
`unknown` precisely because nobody has classified the repair path.
The zone model already answers this: classify continuity paths `z2-continuity`,
whose stance is `fail_open`, and the deadlock does not arise. We accept that as
the right answer. The point is that it is *work not yet done*, and the reason it
is not done is not ours to fix — 18 of our 18 unknown lanes are unknown because
another repo has not published an authoritative workload identity declaration.
`ADR-0009` rule 3 forbids us from closing that gap with inference, correctly.
So as written, v0.8 §6.4 obligation 3 transfers an availability cost onto the
consumer for a classification gap the consumer cannot close alone, and it does so
at the moment of assent rather than at the moment the gap closes.
**What we ask for**, in preference order:
1. **§6.4 obligation 3 names a transition.** A consumer may declare
`unknown: fail_open` as a dated, published transitional state with a
classification-coverage figure attached, converting to `fail_closed` on
coverage rather than on calendar. This keeps the doctrine and prices the
adoption honestly.
2. Failing that, **§13.1 records classification coverage alongside stance.** A
register row reading `unknown: fail_closed` while 100% of that consumer's
targets are unknown is conformant and materially misleading; a reader cannot
tell a strict consumer from an unclassified one. This is the same defect
§11's *published map equals shipped behaviour* rule exists to prevent, one
level up: the map is now accurate about itself and inaccurate about its
effect.
## Finding 3 — §12's derived-artifact rule, small and in agreement
Six instances in one week matches our experience. `registry/generated/high-risk-data-paths.yaml`
carries `catalog_revision` and a `catalog_dirty` flag for exactly the reason §12
now generalises: a derived artifact whose revision does not describe the file it
came from is worse than one with no revision, because the stale one is trusted.
No change requested; recording the agreement since the rule is being generalised
from other repos' incidents.
## What ops-warden is doing about it
Not flipping the cell on a `proposed` standard, and not flipping it before
coverage exists — that ordering is `ADR-0006`'s whole holding. `WARDEN-WP-0040`
records the adoption sequence: raise signing-target classification coverage
first, then convert the cell, with the deadlock case classified before rather
than after. If v0.8 is accepted with obligation 3 unchanged, ops-warden's cell
remains a **declared gap** under §11 — tracked non-conformance with a named
owner and a route, which is the mark §9.1 exists for and which we argued for in
the v0.6 round.
## Axis declaration (second change touching us)
`pep-stance.yaml` already carries `scope: security-zone`. No change needed. We
agree with rejecting a forced migration onto zones — secrets-engine scoping by
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.

View file

@ -10,10 +10,10 @@
# declares it, and is null where the field set has not been established --
# null means unknown, never 'one field'.
generated_at: "2026-09-08T12:55:03Z"
generated_at: "2026-09-09T12:43:48Z"
source: ops-warden/registry/routing/catalog.yaml
catalog_revision: "c1330049d9535f701676aa955d56f2138bb70d19"
catalog_revision_date: "2026-09-08T14:55:02+02:00"
catalog_revision: "83fdd08f881ce6a5bd9c3fbcb4dc79d1ab62b4d5"
catalog_revision_date: "2026-09-09T14:43:44+02:00"
catalog_dirty: false
high_risk_lane_count: 24
concrete_path_count: 15

View file

@ -241,7 +241,15 @@ entries:
# form is superseded; do not reintroduce it.
auth_method: "bao login -method=oidc -path=netkingdom role=whynot-design-workload-kv-read"
path_template: "platform/workloads/coulomb/whynot-design/npm-publish"
fetch_command: "bao kv get -field=NPM_AUTH_TOKEN platform/workloads/coulomb/whynot-design/npm-publish"
# Field corrected 2026-09-09 on the owner's statement (secrets-engine msg
# 15f0c0ca): `npm_token` is the KV FIELD; `NPM_AUTH_TOKEN` is the environment
# variable the publication-scope policy injects, and ops-warden had copied the
# env var in as the field name. That is ADR-0001's failure mode exactly — a
# pointer layer restating an owner's procedure and getting it wrong — so it is
# corrected from the owner's doc (secrets-engine docs/whynot-design-real-publish-closeout.md),
# never re-derived here. The path is a separate question, open with
# railiance-platform; do not change it on this correction.
fetch_command: "bao kv get -field=npm_token platform/workloads/coulomb/whynot-design/npm-publish"
policy_ref: "flex-auth check secret.read:whynot-design"
exec_capable: true
lane: secret
@ -257,7 +265,7 @@ entries:
automatable: false
steps:
- "In the coulomb Forgejo registry, revoke the current @whynot/design publish token and generate a new one (scope: package read/write) for the whynot-design publish identity."
- "Write it back: `bao kv put platform/workloads/coulomb/whynot-design/npm-publish NPM_AUTH_TOKEN=@file` (value from a mode-0600 file)."
- "Write it back: `bao kv put platform/workloads/coulomb/whynot-design/npm-publish npm_token=@file` (value from a mode-0600 file). The field is `npm_token`; `NPM_AUTH_TOKEN` is the injected env var, not a KV key."
- "Verify capabilities-safe, then publish a fresh version and confirm it with Forgejo-supported `npm view <package>@<version>` through the governed execution lane (value used, not printed)."
- id: policy-nexus-forgejo-source-read

View file

@ -0,0 +1,161 @@
#!/usr/bin/env python3
"""Assert every ssh-certificate resource declares its ceilings in the manifest.
Read-only. Reads `inventory.yaml` and the flex-auth registry snapshot; touches
no network, no OpenBao, and no secret material.
Why this exists (FLEX-DEC-2026-012, 2026-09-07). flex-auth's enrichment used to
overlay registry facts additive-if-absent, so a caller-supplied value for a key
won and the registry's ceiling never applied. That is fixed on their side:
registry facts now win. But they win only where the registry HAS a value —
"A resource whose manifest omits max_ttl_hours hands that ceiling back to
the caller, and registering the resource is not sufficient; the specific
key must be present."
So there are two ways to hand a ceiling back, and this check covers both:
1. An actor with no manifest resource at all. `warden sign` names
`ssh-cert:actor/<name>` for any actor in inventory, whether or not the
snapshot was regenerated. Adding an actor and forgetting to rebuild is an
honour-system step in `SCOPE.md`, and honour-system steps are what
`ADR-0004`'s WARDEN_AGENT_ID marker taught us to stop relying on.
2. A manifest resource missing one of the ceiling or allowlist keys. The
builder emits all of them today; this asserts it stays true, including for
resources added by hand or by a future code path.
ops-warden sends no `resource.attributes` on a CheckRequest (`src/warden/policy.py`),
which is why FLEX-DEC-2026-012 was defence-in-depth rather than a live hole for
this repo. That property is asserted separately in `tests/test_policy.py`; this
check covers the half that survives it — if anything ever does reach a request
field, the ceiling it would have to beat must actually exist.
Usage:
python scripts/check_flex_auth_manifest_coverage.py \
--inventory examples/inventory.seed.yaml \
--registry registry/flex-auth/production_registry_snapshot.json [--json]
Exit: 0 covered, 2 coverage gap, 1 usage/IO error.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
import yaml
#: Every key ops-warden's shipped policy package branches on as a ceiling or an
#: allowlist. Named by flex-auth in FLEX-DEC-2026-012; kept here rather than
#: derived from the package so a package edit that drops a branch does not
#: silently shrink what this check requires.
REQUIRED_CEILING_KEYS = (
"actor_id",
"actor_type",
"allowed_principals",
"allowed_subjects",
"max_ttl_hours",
"security_zone",
"security_zone_admission",
)
RESOURCE_TYPE = "ssh-certificate"
def _resources(registry: dict[str, Any]) -> list[dict[str, Any]]:
out: list[dict[str, Any]] = []
for manifest in registry.get("resource_manifests") or []:
out.extend(manifest.get("resources") or [])
return out
def audit(inventory: dict[str, Any], registry: dict[str, Any]) -> dict[str, Any]:
actors = sorted((inventory.get("actors") or {}).keys())
resources = _resources(registry)
by_id = {str(r.get("id")): r for r in resources}
unregistered: list[str] = []
missing_keys: list[dict[str, Any]] = []
for name in actors:
if f"ssh-cert:actor/{name}" not in by_id:
unregistered.append(name)
# Check every ssh-certificate resource in the manifest, not only the ones an
# inventory actor maps to: a resource flex-auth can be asked about is one it
# holds, regardless of where it came from.
for resource in resources:
if str(resource.get("type")) != RESOURCE_TYPE:
continue
attributes = resource.get("attributes") or {}
absent = [
key for key in REQUIRED_CEILING_KEYS
# A null is not a declaration: flex-auth overlays a registry value
# only where one exists, so `key: null` hands the ceiling back
# exactly as an absent key does.
if attributes.get(key) is None
]
if absent:
missing_keys.append({"resource": str(resource.get("id")), "missing": absent})
# Not a security defect — a stale resource has ceilings, it just has no
# actor. Reported so drift is visible rather than accumulating silently.
orphaned = sorted(
rid for rid, r in by_id.items()
if str(r.get("type")) == RESOURCE_TYPE
and rid.removeprefix("ssh-cert:actor/") not in actors
)
return {
"actors": len(actors),
"resources": sum(1 for r in resources if str(r.get("type")) == RESOURCE_TYPE),
"required_keys": list(REQUIRED_CEILING_KEYS),
"unregistered_actors": unregistered,
"resources_missing_keys": missing_keys,
"orphaned_resources": orphaned,
"covered": not unregistered and not missing_keys,
}
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--inventory", default="examples/inventory.seed.yaml", type=Path)
parser.add_argument(
"--registry",
default="registry/flex-auth/production_registry_snapshot.json",
type=Path,
)
parser.add_argument("--json", action="store_true", dest="as_json")
args = parser.parse_args(argv)
try:
inventory = yaml.safe_load(args.inventory.read_text()) or {}
registry = json.loads(args.registry.read_text())
except (OSError, ValueError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
report = audit(inventory, registry)
report["inventory"] = str(args.inventory)
report["registry"] = str(args.registry)
if args.as_json:
print(json.dumps(report, indent=2))
else:
print(f"inventory : {args.inventory} ({report['actors']} actors)")
print(f"registry : {args.registry} ({report['resources']} ssh-certificate resources)")
for name in report["unregistered_actors"]:
print(f" MISSING {name} — no manifest resource; every ceiling is caller-supplied")
for row in report["resources_missing_keys"]:
print(f" UNDECLARED {row['resource']} — {', '.join(row['missing'])}")
for rid in report["orphaned_resources"]:
print(f" orphaned {rid} — manifest resource with no inventory actor")
print("covered" if report["covered"] else "NOT COVERED")
return 0 if report["covered"] else 2
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -81,3 +81,86 @@ def test_compiler_joins_explicit_workload_reference_to_resolved_zone(tmp_path):
assert attrs["security_zone"] == "z2-continuity"
assert attrs["security_zone_admission"] == "satisfied"
assert attrs["security_zone_revision"] == "sha256:zone-revision"
# --- manifest ceiling coverage (FLEX-DEC-2026-012) ----------------------------
#
# flex-auth fixed enrichment so registry facts beat caller-supplied ones, then
# asked every consumer to confirm the ceiling keys are actually declared: the
# fix wins only where the registry HAS a value. These assert the two ways a
# ceiling gets handed back to the caller.
import importlib.util # noqa: E402
import pytest # noqa: E402
_REPO = ROOT
_spec = importlib.util.spec_from_file_location(
"check_flex_auth_manifest_coverage",
_REPO / "scripts" / "check_flex_auth_manifest_coverage.py",
)
_coverage = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_coverage)
REQUIRED_CEILING_KEYS = _coverage.REQUIRED_CEILING_KEYS
audit = _coverage.audit
_build_spec = importlib.util.spec_from_file_location("build_flex_auth_registry", SCRIPT)
_builder = importlib.util.module_from_spec(_build_spec)
_build_spec.loader.exec_module(_builder)
build_registry = _builder.build_registry
def _repo_inventory_and_registry():
inventory = yaml.safe_load((_REPO / "examples" / "inventory.seed.yaml").read_text())
registry = json.loads(
(_REPO / "registry" / "flex-auth" / "production_registry_snapshot.json").read_text()
)
return inventory, registry
def test_shipped_snapshot_declares_every_ceiling_key():
"""The confirmation flex-auth asked for, as a test rather than a reading."""
report = audit(*_repo_inventory_and_registry())
assert report["unregistered_actors"] == []
assert report["resources_missing_keys"] == []
assert report["covered"] is True
def test_actor_with_no_manifest_resource_is_a_coverage_gap():
"""`warden sign` names ssh-cert:actor/<name> whether or not the snapshot was rebuilt."""
inventory, registry = _repo_inventory_and_registry()
inventory["actors"]["adm-added-after-the-last-build"] = {
"type": "adm", "principals": ["adm-full"], "ttl_hours": 48,
}
report = audit(inventory, registry)
assert report["unregistered_actors"] == ["adm-added-after-the-last-build"]
assert report["covered"] is False
@pytest.mark.parametrize("key", REQUIRED_CEILING_KEYS)
def test_dropping_any_single_ceiling_key_is_a_coverage_gap(key):
inventory, registry = _repo_inventory_and_registry()
resource = registry["resource_manifests"][0]["resources"][0]
resource["attributes"].pop(key)
report = audit(inventory, registry)
assert report["resources_missing_keys"] == [
{"resource": resource["id"], "missing": [key]}
]
assert report["covered"] is False
@pytest.mark.parametrize("key", REQUIRED_CEILING_KEYS)
def test_a_null_ceiling_is_not_a_declaration(key):
"""flex-auth overlays a registry value only where one exists."""
inventory, registry = _repo_inventory_and_registry()
resource = registry["resource_manifests"][0]["resources"][0]
resource["attributes"][key] = None
report = audit(inventory, registry)
assert report["covered"] is False
def test_builder_output_is_covered_by_construction(tmp_path):
"""Every resource the builder emits carries the full ceiling set."""
inventory = yaml.safe_load((_REPO / "examples" / "inventory.seed.yaml").read_text())
report = audit(inventory, build_registry(inventory))
assert report["covered"] is True

View file

@ -307,3 +307,47 @@ def test_advisory_decision_is_recorded_and_does_not_block(tmp_path):
assert check_sign_policy(cfg, spec) == "decision:advisory"
assert spec.policy_zone == "unknown"
assert spec.policy_outcome == "audit_only"
def test_check_request_asserts_no_resource_attributes(tmp_path, monkeypatch):
"""The property FLEX-DEC-2026-012 turned on, asserted rather than assumed.
flex-auth's enrichment used to overlay registry facts additive-if-absent, so
a caller-supplied `resource.attributes` value won and the registry ceiling
never applied. Their exploitability assessment rested on ops-warden sending
no `resource.attributes` at all -- true when they read it, and nothing here
held it true. secrets-engine sends them on every request, so this is a
property of this code rather than of the protocol.
Ceilings must arrive from the registry. If a future change ever needs to send
an attribute, this test is the place that argument gets made.
"""
from warden import policy as policy_mod
pubkey = tmp_path / "id.pub"
pubkey.write_text("ssh-ed25519 AAAA test\n")
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
seen = {}
class _Response:
status_code = 200
def raise_for_status(self):
return None
def json(self):
return {"effect": "allow", "id": "decision:49350f1064f674d7"}
def fake_post(url, json=None, headers=None, timeout=None):
seen["body"] = json
return _Response()
monkeypatch.setattr(policy_mod.httpx, "post", fake_post)
policy_mod.check_sign_policy(cfg, _spec(pubkey))
assert "attributes" not in seen["body"]["resource"]
# The requested TTL is a policy input (ttl_out_of_bounds is denied against
# the registry ceiling), so it must travel as context, never as a resource
# attribute that would be compared against itself.
assert seen["body"]["context"]["ttl_hours"] == 24
assert set(seen["body"]["resource"]) == {"id", "type", "system", "tenant"}

View file

@ -10,7 +10,8 @@ Owner: `railiance-platform` (OpenBao) · provisioning CCR-2026-0001 (commit 8f61
> plan-authorized Forgejo admin recovery lane. Treat this dedicated lane as
> unverified for writes until its package token is rotated and re-proven.
The `NPM_AUTH_TOKEN` that publishes `@whynot/design` to the coulomb Forgejo npm registry
The npm publish token for `@whynot/design` on the coulomb Forgejo npm registry
(KV field `npm_token`; reaching the publish command as `NPM_AUTH_TOKEN`)
(`https://forgejo.coulomb.social/api/packages/coulomb/npm/`). ops-warden **does not hold
this token** — it is the access front door: `warden access` proxies the read from OpenBao
**as the caller** and never persists, caches, or logs the value.
@ -22,7 +23,8 @@ this token** — it is the access front door: `warden access` proxies the read f
| Field | Value |
| --- | --- |
| OpenBao path | `platform/workloads/coulomb/whynot-design/npm-publish` |
| Field | `NPM_AUTH_TOKEN` |
| KV field | `npm_token` |
| Injected env var | `NPM_AUTH_TOKEN` (set by the publication-scope policy; not a KV key) |
| KV mount | `platform` |
| Read policy | `workload-kv-read-whynot-design-npm-publish` |
| OIDC login | `bao login -method=oidc -path=netkingdom role=whynot-design-workload-kv-read` |
@ -59,12 +61,12 @@ this token** — it is the access front door: `warden access` proxies the read f
token fragment at `forgejo.coulomb.social`:
```bash
# --exec needs the env-var name. The zone-aware policy gate always runs first.
warden access whynot-design-npm-publish --field NPM_AUTH_TOKEN \
warden access whynot-design-npm-publish --field npm_token \
--exec -- npm view @whynot/design@<version> version \
--registry=https://forgejo.coulomb.social/api/packages/coulomb/npm/
warden access whynot-design-npm-publish --field NPM_AUTH_TOKEN \
warden access whynot-design-npm-publish --field npm_token \
--exec -- npm publish
warden access whynot-design-npm-publish --field NPM_AUTH_TOKEN --fetch
warden access whynot-design-npm-publish --field npm_token --fetch
```
On either path the value transits to you (or the child env) and never enters
ops-warden's memory, disk, or audit log.

View file

@ -198,6 +198,36 @@ current access-engine implementation, received the decision-lifetime semantics
question (message `02c99a74`). No implementation or policy decision was assigned
to another owner by these messages.
**One of three answered, 2026-09-09.** `flex-auth` answered the decision-lifetime
question as `FLEX-DEC-2026-004` (flex-auth commit `6a6464f`), and answered it
rather than declining it: a decision lifetime shorter than the certificate TTL is
meaningful, but only as authority to **issue**, never as authority to **use** an
already-issued certificate. The action is `sign` and it completes at issuance, so
the decision is consumed once the certificate exists and no field in the decision
record reaches the artifact. 15m against a 48h `adm` certificate was never an
inconsistency to reconcile — the question mistook a decision lifetime for a
credential lifetime.
Two obligations land on this side and are accepted: no replay of an allow past
`expires_at`, and a fresh Check per sign. The second is where the value is, and it
is real rather than nominal because claims are not cached PDP-side (deadline 0 for
the claim class), so a revoked principal stops obtaining *new* certificates
immediately.
`pep-stance.yaml` §9.7.2 is unchanged and stays here: flex-auth explicitly declined
to move the residue to their side, on the ground that a PDP recalling an issued
credential would be "the same shape of lie as fail-open". Declining it was the
right call even though absorbing it would have made their record look tidier.
The TTL lever they named — capping requested TTL per actor class or zone, already a
policy input since `ttl_out_of_bounds` is denied before OpenBao is reached — is
noted and deliberately not requested. Shortening `adm` from 48h has an operator
cost, and issuance on this lane is too sparse (3 signature records across 79 days,
`pep-stance.yaml` `emission_cadence`) to price it from intuition.
T05 stays `wait` on the remaining two: `ops-mason` (no stance map published) and
`railiance-infra` (KRL distribution versus TTL expiry). Neither has replied.
## Related
Review 2026-09-05: reviewed the pending implementation and fixed the identity

View file

@ -79,3 +79,33 @@ authenticated exact-version lookup proved that Forgejo's remote integrity
matches the local dry-run. T03 waits only on rotating the dedicated package
token and proving the next fresh version through `whynot-design-npm-publish`;
the package migration itself is complete.
**2026-09-09 — field claim corrected, path routed.** `secrets-engine` (msg
`15f0c0ca`) corrected two things and declined a third, all correctly.
The KV field is `npm_token`. `NPM_AUTH_TOKEN` is the environment variable their
publication-scope policy injects, and ops-warden had copied the env var in as the
field name — so the catalog's `fetch_command` named a field that does not exist
and could only ever have failed. Corrected in `registry/routing/catalog.yaml` and
`wiki/playbooks/whynot-design-npm-publish.md` from the owner's statement rather
than re-derived here. This was `ADR-0001`'s failure mode rather than a typo: a
pointer layer restating an owner's procedure and getting a detail wrong.
The endpoint claim already agreed; `delivery_config.npm.registry` has been the
Forgejo URL throughout.
The **path** is routed to `railiance-platform` and the catalog is unchanged pending
their answer. Which location backs the lane for reads is custody state they own;
`secrets-engine` has no lane read authority to confirm it and refused to rewrite a
production lane pointer from a coordination message (`SECRETS-WP-0006-T06`). Their
reasoning is right, and asserting our own pointer is authoritative because it is
ours would route around it. The ask names a location only and flags that a
`bao kv get` answer would be the 2026-07-16 disclosure vector on a `risk: high`
lane.
`ready: false` is not being read as path evidence: a source checkout with no
production authority reports not-ready regardless of which path the catalog names.
T03 still waits on the human rotation of the dedicated Forgejo package token; the
path question does not block that, it determines whether the pointer is correct
once it rotates.

View file

@ -74,3 +74,31 @@ authorized by this workplan alone. HFACT-WP-0001-T03 consumes this return.
Validation: 429 tests passed (4 integration tests deselected by the repository default); Ruff passed for changed Python files. Full tests used the declared phase-memory source and an isolated temporary memory store. The focused policy/proxy suite passed 69 tests. The existing authenticated SSH policy probe still returns HTTP 200/ALLOW, decision:f3f7c88f9585582a; the credential-owner request returns 403. No CA issue or credential read was performed by these probes.
Installed verification: source guard present after refreshed owner installation (`19bb75c`). `warden access forgejo-admin-api-token --exec --field API_TOKEN -- true` exits 4 with explicit HTTP 403 before credential transport or child execution. The previous fail-open warning is absent. The probe requested no output value; no credential was fetched. T01/T02 are complete; T03 owns the remaining exact policy-binding admission.
**T03 routed 2026-09-09.** Asked `flex-auth` whether an admitted contract exists
for a delegated credential read where the caller (ops-warden) and the resource
owner differ, since that is exactly the shape drawing the live 403. Three outcomes
were named as equally acceptable answers, with no preference stated between the
first two: an existing contract to conform to; no contract yet, making this a
design question they should raise rather than one ops-warden invents a shape for;
or that there should be **no** such contract — the interim proxy transport is
itself the defect and the answer is to finish the native handoff
(`WARDEN-WP-0033` / `SECRETS-WP-0006`).
The third is a real possibility and was not argued against. Eleven catalog lanes
are ops-warden proxies with a named intended owner precisely because no owner
front door exists; if delegated reads should not be made policy-admissible, that
shortens WP-0033 rather than blocking it.
Two fixes were explicitly ruled out in the message rather than left unmentioned:
broadening the ops-warden caller binding so it may read other systems' resources,
and relabelling `resource.system` as ops-warden so the existing binding matches.
The second is worse — it would make the audit trail assert ops-warden owns
credentials it deliberately does not, which is the claim `ADR-0002` exists to
prevent, achieved by editing a field instead of making an argument.
The refusal stays in place until an answer yields positive evidence plus
wrong-caller, wrong-owner and wrong-tenant negatives. No grant was requested.
`secrets-engine` was told this bears on SECRETS-WP-0007-T04 and on how many lanes
stay proxied; `gate-house` was told it may reach how the signing lane's
approval-consume is built (GH-DEC-2026-005).

View file

@ -0,0 +1,122 @@
---
id: WARDEN-WP-0040
type: workplan
title: "Adopt unknown -> fail_closed behind signing-target classification coverage"
domain: infotech
repo: ops-warden
status: proposed
owner: ops-warden
topic_slug: netkingdom
planning_priority: P1
depends_on_workplans:
- WARDEN-WP-0032
- WARDEN-WP-0034
created: "2026-09-09"
updated: "2026-09-09"
---
# WARDEN-WP-0040 — `unknown` → `fail_closed`, in the right order
security-layer-model v0.8 §6.4 obligation 3 (proposed, 2026-09-06) rules that
`unknown` is not a zone and must resolve to `fail_closed`. ops-warden's
`pep-stance.yaml` declares `unknown: fail_open` under `ADR-0009`.
**We agree with the rule.** `history/2026-09-09-layer-model-v08-review.md` records
the assent and why our falsifier check failed: the cell governs certificate
issuance, not a §5.1 read-only diagnostic, so nothing rescues it.
**We disagree with adopting it first.** Measured 2026-09-09: of four signing
targets, zero resolve to a zone, three are `unknown`, one is `not-applicable`.
Flipping the cell today makes flex-auth a hard dependency of essentially every
certificate — `ADR-0006`'s rejected configuration, reached by a different route,
including the continuity path needed to repair flex-auth itself.
So: coverage first, then the cell. That ordering is the whole holding of
`ADR-0006` and the reason `ADR-0009` replaced a global switch with a zone map.
## Tasks
```task
id: WARDEN-WP-0040-T01
status: todo
priority: high
```
**Classify the continuity path before anything else changes.**
The deadlock is specific and worth naming before it is designed around: flex-auth
unreachable → operator needs an SSH certificate to reach the host → target is
`unknown` because nobody classified the repair path → `fail_closed` denies it.
`z2-continuity` exists for this and its stance is `fail_open`, so the fix is
classification, not an exception. Determine which actor(s) constitute the
repair path, establish whether the declaration is ops-warden's to make (our own
`tenancy.yaml` covers ops-warden as a workload; the actors' target workloads may
not be ours), and route what is not.
Blocks T03. Converting the cell with the repair path unclassified is the one
outcome this workplan exists to prevent.
```task
id: WARDEN-WP-0040-T02
status: todo
priority: high
```
**Raise signing-target coverage, by asking owners — never by inferring.**
Three actor resources report `workload_resolution_absent`:
`agt-codex-interhub-bootstrap`, `agt-state-hub-bridge`, `atm-backup-daily`.
Their owners (ops-bridge and the backup execution unit among them) have not
published an authoritative workload identity declaration.
`ADR-0009` rule 3 forbids closing this with path or repository-name inference,
and that prohibition holds under pressure from this workplan specifically —
adopting a stricter stance is not a licence to manufacture the membership that
makes it survivable.
Route to each owner with the concrete consequence: while the declaration is
absent their actor cannot be issued a certificate during a flex-auth outage once
the cell converts. That is a better ask than a generic request to publish a
declaration, and it is true.
Report coverage with `scripts/report_workload_join.py`; this task is done when
coverage is stated, not when it reaches a threshold — the threshold is T03's
gate, and owners may legitimately decline.
```task
id: WARDEN-WP-0040-T03
status: wait
priority: high
```
**Convert the cell and supersede `ADR-0009`'s unknown row.**
Gated on T01, and on v0.8 (or its successor) reaching `accepted`. Do not convert
against a `proposed` standard.
`ADR-0009` is `owner: ops-warden`, so changing it means a superseding ADR, never
an in-place edit (`.claude/rules/architecture.md`). The superseding record should
carry the v0.8 argument rather than restate it: unknown is not a zone, so a §9.3
per-zone trade was never made for that request.
`pep-stance.yaml` is asserted equal to `PolicyConfig.failure_modes` by
`tests/test_layer_conformance.py`, so the map and the code convert in one commit
or the test fails — which is the property that makes the map worth publishing.
```task
id: WARDEN-WP-0040-T04
status: todo
priority: medium
```
**Return findings to gate-house and track the transition ask.**
Sent 2026-09-09 with the review. Two asks, in preference order: obligation 3
names a dated transitional state gated on coverage; or failing that, §13.1
records classification coverage alongside stance, so a register row cannot read
`unknown: fail_closed` while every target is unknown.
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.