Compare commits

...

178 commits
v0.1.1 ... main

Author SHA1 Message Date
8afcc9c321 chore: refresh Warden work-record indexes
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a058f3-8ba0-7692-a042-9a870fc3d663
2026-09-01 01:27:55 +02:00
repo-manager
529feeac49 repo.work.assign_missing_identifiers
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
source: repo-manager
reason: deterministic projection registration

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a058f3-8ba0-7692-a042-9a870fc3d663
2026-09-01 00:51:59 +02:00
9770d6ad66 docs: close Warden credential workplans
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a058f3-8ba0-7692-a042-9a870fc3d663
2026-09-01 00:51:52 +02:00
custodian-sync
31694ab64d chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Updated by fix-consistency on 2026-09-01:
  - update .custodian-brief.md for ops-warden

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a058f3-8ba0-7692-a042-9a870fc3d663
2026-09-01 00:50:08 +02:00
eddb5d426b chore: refresh high-risk credential paths
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a058f3-8ba0-7692-a042-9a870fc3d663
2026-09-01 00:47:25 +02:00
4fee839b11 feat: route Policy Nexus source credential
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a058f3-8ba0-7692-a042-9a870fc3d663
2026-09-01 00:46:28 +02:00
8f01eefb1e Preserve Warden config in attended child
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a058f3-8ba0-7692-a042-9a870fc3d663
2026-09-01 00:24:18 +02:00
b4c1d3900a Fix contained OpenBao login handoff
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a058f3-8ba0-7692-a042-9a870fc3d663
2026-09-01 00:13:26 +02:00
repo-manager
4e267179db chore(registrar): assign State Hub identifiers
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 4014535@bnt-lap001
Assistant-Session: d0036016-73e8-4da1-8e47-563e3ab39a3c
2026-08-29 14:56:21 +02:00
custodian-sync
d18d18e313 chore(consistency): sync task status from DB [auto]
Some checks failed
CI Smoke / host-smoke (push) Has been cancelled
CI Smoke / container-smoke (push) Has been cancelled
Updated by fix-consistency on 2026-08-29:
  - update .custodian-brief.md for ops-warden

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 4014535@bnt-lap001
Assistant-Session: d0036016-73e8-4da1-8e47-563e3ab39a3c
2026-08-29 14:56:20 +02:00
2e3ff772f4 Refresh work-record index for WARDEN-WP-0034
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
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
2026-08-29 14:55:36 +02:00
fd08950231 Align INTENT and SCOPE to layer model v0.7; assess gaps; open WARDEN-WP-0034
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
The standard is accepted at v0.7, with SECURITY-COMPANION.md v0.2 as its
operative form. Four ops-warden findings were adopted between v0.4 and v0.7 —
§9.1's two marks, §5's Tooling scope rule, §6.4 obligation 1's second limb, and
§13.1's existence — and both ops-warden declaration artifacts are now cited in
the text as the estate's reference forms.

INTENT.md gains frontmatter (layer: Staff, pep_shaped: true) because §11 requires
a machine-readable declaration and prose cannot distinguish a declaration from a
transcribed review. The note now covers the agent principal (§3.4), the PEP
shape, the attributive evidence position, and the role the companion assigns:
the estate is told to ask ops-warden which lane, which credential, which route.

SCOPE.md records what is actually shipped against v0.7 and the honest conformance
state — declared gap, which is tracked non-conformance, not conformance.

The assessment checked every obligation against shipped code rather than intent.
Three gaps survive:

- §9.7.2 requires a PEP to state one revocation visibility deadline. Ours is
  unstated, and the honest value is uncomfortable: the cert TTL, up to 48h. A
  cert outlives revocation of the decision that authorized it — no CRL, no KRL
  distribution. That is a design property never written down, which is exactly
  what §9.7.2 exists to force into the open.
- §3.4 rule 1 forbids standing credentials and requires issued, attributable
  authority. ADR-0004's boundary keys on WARDEN_AGENT_ID, which an agent sets
  about itself. key-cape now issues a real coding-agent identity, so the
  ops-warden half can stop being advisory.
- §9.6 cadence remains undeclared. Attributive, so SHOULD not MUST, but silence
  through two reviews is the one outcome that is not defensible.

WARDEN-WP-0034 addresses all three, plus the discoverability gap the companion
creates and two items to route rather than absorb.

402 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
2026-08-29 14:50:55 +02:00
94f32bd160 Review layer model v0.6; publish the PEP stance map §6.4 requires
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
All three v0.4 findings were acted on — §9.1 split into pending/declared-gap and
§5's scope rule adopted as recommended and credited, and §9.6 ruled via the
load-bearing/attributive distinction with ops-warden's `# audit must not block
signing` named as the estate's live example.

Checked the favourable ruling rather than accepting it. §9.6's test is "no
control branches on its presence": the only consumer of audit.jsonl is `warden
activity`, which displays. Nothing gates on a signing record, so the lane is
genuinely attributive. AuditTrail.md now records the ruling instead of the open
question, and states that the trade must be revisited if a control ever gates on
the trail.

CONFORMANCE ACTION. §6.4 obligation 3 requires a stance map "published rather
than held in code", and requires every PEP-shaped consumer to publish one so the
maps can be inventoried — naming ADR-0009 as the reference shape. ops-warden was
not doing it: the map lived in PolicyConfig.failure_modes, a dataclass default.
Not a code comment, but not published either.

pep-stance.yaml publishes it, and the test asserts the published map EQUALS the
shipped default. A published map that may drift from the code is worse than no
map, because it invites reliance it cannot support.

Two findings sent to gate-house, in history/2026-08-29-layer-model-v06-review.md:
§6.4 obligation 1 (no side effect without a decision record) contradicts
obligation 3 and §9.3, with ops-warden's blessed fail-open stance as the
instance; and §6.4 mandates a stance-map inventory in §13 that §13 does not
implement — where ops-warden is currently the only PEP to have published one.

402 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
2026-08-29 10:20:49 +02:00
ec625873fb Review layer model v0.4; correct an unsound audit claim it exposes
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assessment in history/2026-08-29-layer-model-v04-review.md. No objection to the
ruling; both ops-warden amendments were adopted (5.2 conduit, 5.3 declared gap).
Three findings, one against us.

The one against us is real. 9.6 requires emission atomic with the state change
for load-bearing evidence. ops-warden ca.py carries `pass  # audit must not block
signing` and AuditTrail.md advertises that the trail never blocks the primary
action, so a failed append loses the event while the cert still issues -- a
suppressed event leaving the chain intact, which is exactly what 9.6 describes.

Whether to make it atomic is gate-house doctrine, not ops-wardens call: it would
give the estates operational access lane a new dependency on its own evidence
store. But one half of the fix is ours regardless -- the trail must not be read
as complete. AuditTrail.md now says absence of a record is not evidence of
absence, which it did not.

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
2026-08-29 02:46:50 +02:00
f815bb35ca 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
2026-08-29 02:45:29 +02:00
57d39ede0f Refresh work-record index
Regenerated by fix-consistency; adds the inbound v0.3 review intake.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2564823@bnt-lap001
Assistant-Session: 2a7ed827-4928-4b9f-8613-9135c9cadfe9
2026-08-28 22:43:06 +02:00
repo-manager
f289465b90 repo.work.create_intake WARDEN-IN-0002
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
correlation_id: 676e92a9-dc2e-4101-a31c-a584962c25df
reason: Propose layer model v0.3 for review
source: repo-manager

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2564823@bnt-lap001
Assistant-Session: 2a7ed827-4928-4b9f-8613-9135c9cadfe9
2026-08-28 22:40:24 +02:00
custodian-sync
a45280f30d chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Updated by fix-consistency on 2026-08-28:
  - update .custodian-brief.md for ops-warden

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 4014535@bnt-lap001
Assistant-Session: d0036016-73e8-4da1-8e47-563e3ab39a3c
2026-08-28 22:02:58 +02:00
61c992923c WARDEN-WP-0027-T02: the owner gate closed five days ago
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
railiance-platform accepted ops-warden revision 0fae0904 on 2026-08-23, in
RPF-WP-0017 (status: finished), together with railiance-infra approval at
186b030 and all five acceptance criteria met. T02 has been sitting `progress`
on a gate that was already open.

Verified rather than trusted: the receipt at bc1966da hashes to
d2ba444ed16989590325697e69d25283dc75a9432c29a72e627e80bf9fd987e4, matching
their record exactly.

One reason it went unnoticed is an identifier mismatch — T02 cites the
remediation interface as RAILIANCE-WP-0026-T01, but it is RPF-WP-0017-T01 in
the owner repo, and the cited id resolves to an unrelated workplan there.

Their acceptance is source acceptance only and authorizes no live drill, so
T02 stays progress: what unblocks is preparing a NEW scenario, which needs a
fresh human GO and is the platform owner s to execute. Surfaced, not taken.

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
2026-08-28 22:01:47 +02:00
ee94c18938 WARDEN-WP-0033 finished — key-cape accepted the issuance question five days ago
T04 was the last open task, waiting on key-cape to accept or refuse ownership of
the coding-agent OpenBao issuance identity. They accepted, in KEY-WP-0009-T03,
on 2026-08-23: codex-railiance-platform is published in their
config/service-clients.example.yaml with subject service:codex:railiance-platform,
role coding-agent, scope openbao:login, 15m lifetime, and the service-auth
semantics in docs/openbao-service-auth-contract.md. The split is the one we
routed for — KeyCape issues, railiance-platform binds the OpenBao role, OpenBao
enforces, no secret value in either repo.

We found it by reading their repository. KEY-WP-0009-T04 records replying to
ops-warden; the inbox has zero messages from key-cape, read or unread. The task
sat `wait` on an answer that already existed.

That is T05's own lesson arriving on T04: a blocker is a claim about the world at
a date. So the same pass re-verified the two lanes pointing at key-cape against
their source instead of bumping dates:

- rapp-qonto-keycape-client -> verified: source-read. KEY-WP-0009-T02 did add
  bounded service-auth, but that is client_credentials JWT issuance for OpenBao
  machine login and does not front this client_secret_basic exchange or its
  rotation. Blocker stands, now with evidence rather than memory.
- key-cape-oidc-login -> asked of key-cape today, which the entry had recorded
  as still outstanding since 2026-08-21.

Also cleared the inbox that hid this: 9 stale unread, all superseded by shipped
work, with late closes sent to secrets-engine and llm-connect on the two threads
that had asked ops-warden something and never got an answer.

391 tests pass, ruff clean, boundary coverage 0 uncovered.

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
2026-08-28 22:00:09 +02:00
d7f4ebcfe0 Re-emit the high-risk path artifact after the NetKingdom SSO lanes
c374d41 added net-kingdom-lldap-bind-credential and
net-kingdom-privacyidea-admin-token as `risk: high` and did not re-run the
emitter, so registry/generated/high-risk-data-paths.yaml still described the
catalog at 0fae090. railiance-platform consumes that file instead of
hand-maintaining its deny list, and it has been reading a census two lanes short
since 2026-08-23.

This is precisely the drift WARDEN-WP-0033-T03 built the guard for — a lane
graded high after the last emit silently failing to reach the consumer. The
guard fired; nothing had acted on it.

The deny list itself does not move: both lanes are blocked on their OpenBao path
being published, so they land in `no_concrete_path` and concrete_path_count
stays 14. What changes is the count the consumer sees — 23 high-risk lanes, two
of which have no address yet. That is the honest signal and the reason the
bucket is listed rather than omitted.

check_agent_read_boundary.py still reports 0 uncovered.

The workload-join census moves 9 -> 11 not-applicable: both lanes are
provider/control-plane credentials rather than workload delivery lanes.

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
2026-08-28 21:55:33 +02:00
repo-manager
f766400563 chore(registrar): assign State Hub identifiers
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 4014535@bnt-lap001
Assistant-Session: d0036016-73e8-4da1-8e47-563e3ab39a3c
2026-08-28 21:52:03 +02:00
70d8b503f0 Assent to the NetKingdom security layer model (WARDEN-IN-0001)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
gate-house asked ops-warden to assent to three boundary items ratified in
GH-DEC-2026-001. All three are assented in ADR-0010.

Staff: accepted. Grepping section 5 as it invites turned up a real
non-conformance — src/warden/vault.py is a direct OpenBao client performing a
write, and so is `warden desk`'s `bao kv put`. Section 5's only escape hatch is
read-only diagnostics, which does not cover a signing write, so both are
declared in INTENT.md as an engine gap with intended owner secrets-engine and
the blocker "no engine exposes an SSH-CA surface" — ADR-0003 turned inward
rather than an exemption argued for. taint.py is metadata-only and declared
under the read-only allowance; `warden access` proxies run under the caller's
identity and supply no authority of their own.

Doctrine versus runbook: accepted. NetKingdom Security Literacy becomes a lane
routing runbook that references gate-house doctrine instead of restating it. It
had also become a prose second source for registry/routing/catalog.yaml, which
ADR-0001 already rules against.

Lane versus rule: assented unconditionally, and the access-engine veto is not
exercised. One request on sequencing only — a window where both names resolve.

gate-house added to the routing tables in INTENT.md and SCOPE.md.

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
2026-08-28 21:47:44 +02:00
85d3078bae Refresh work-record index
Regenerated by fix-consistency; adds the inbound assent intake.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2564823@bnt-lap001
Assistant-Session: 2a7ed827-4928-4b9f-8613-9135c9cadfe9
2026-08-28 21:35:57 +02:00
repo-manager
467635e84b repo.work.create_intake WARDEN-IN-0001
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
correlation_id: 65d40cdd-5894-440e-9c95-c6bcfe259b66
reason: Request assent for GH-DEC-2026-001 boundaries
source: repo-manager

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2564823@bnt-lap001
Assistant-Session: 2a7ed827-4928-4b9f-8613-9135c9cadfe9
2026-08-28 21:30:28 +02:00
15730bb650 Point layering note at the published standard
The layer model is now published as
net-kingdom/canon/standards/security-layer-model_v0.1.md (proposed) and
ratified by gate-house GH-DEC-2026-001. The note previously said the
standard was not yet written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2564823@bnt-lap001
Assistant-Session: 2a7ed827-4928-4b9f-8613-9135c9cadfe9
2026-08-28 21:21:08 +02:00
347e47ce8e Note NetKingdom layering review in INTENT
Records this repository's layer in the NetKingdom IT-security layer model
(Taxonomy / Tooling / Engines / Staff) and what should change in this INTENT
as a result. Links to the review that established the model:
gate-house/history/2026-08-28-security-layer-model-and-gate-house-recut.md

The note flags pending adaptation only; the body is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2564823@bnt-lap001
Assistant-Session: 2a7ed827-4928-4b9f-8613-9135c9cadfe9
2026-08-28 20:33:43 +02:00
37c387bd34 fix(workplans): qualify archived ad-hoc identifiers
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
The live-file pass missed these: archived ad-hocs carry a YYMMDD- filename
prefix, so the ADHOC-* glob did not match them. They still derive from the
forge, so they are live records rather than dead files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
2026-08-28 00:34:02 +02:00
030362085a fix(workplans): qualify ad-hoc identifiers with the repository prefix
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
`ADHOC-YYYY-MM-DD` is unique per date but not per repository, so any two repos
opening an ad-hoc on the same day collide. The 2026-08-26 fleet projection
reset refused 9 records for exactly this reason.

Canon (work-record-types_v0.1, CUST-WP-0066) settled the form as
`{PREFIX}-WP-ADHOC-YYYY-MM-DD`, filename unchanged, and grandfathered existing
ids on the condition they are never *silently* re-derived. This is the explicit
migration that clause allows for.

The hub id is derived from the record id, so a changed id is a different
record: stale state_hub_*_id fields are dropped and fix-consistency re-derives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
2026-08-28 00:28:23 +02:00
custodian-sync
0fc581ffb9 chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Updated by fix-consistency on 2026-08-25:
  - update .custodian-brief.md for ops-warden

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
2026-08-25 20:17:24 +02:00
186fa99f58 fix(workplans): adopt ADR-007 derived identifiers for unregistered records
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
These workplans exist only in the retired local hub. Their random pre-ADR-007
identifiers are refused by C-06 as stale references, so they cannot be
registered. Deriving from the canonical record id takes no identity from
anything: central does not hold them and the old ids die with the cache.

Records central already holds were deliberately left untouched.

Refs CUST-WP-0068-T06

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
2026-08-25 20:16:12 +02:00
custodian-sync
76e545ecf0 chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Updated by fix-consistency on 2026-08-25:
  - update .custodian-brief.md for ops-warden

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
2026-08-25 19:56:00 +02:00
51e1434cdb fix(workplan): point workplan at its authoritative projection id
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
The file carried an identifier minted by the retired local hub while central
held a different record for the same file — ADR-010's same-filename,
different-UUID duplicate-registration class.

Central is authoritative (ADR-010 decision 1) and decision 6 says to mitigate
by changing the repository file rather than editing central, so the file adopts
central's id. The cache-side id dies with the cache.

Refs CUST-WP-0068

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
2026-08-25 17:37:51 +02:00
9087edf673 docs(agents): repoint remote State Hub URL to the in-cluster address
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
The remote row pointed at 127.0.0.1:18000, a reverse tunnel back to the
workstation. On railiance01 the State Hub runs in the cluster on that same
machine, so the request left the box and came back to reach a local service.

Refs CUST-WP-0067-T07

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
2026-08-25 00:21:31 +02:00
c374d41a49 Add NetKingdom SSO credential routing lanes
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0217e-8c4c-7383-be6b-f50a6e485306
2026-08-23 21:43:12 +02:00
c8fa02adf0 docs: advance remaining owner gates
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0290b-3241-74c3-b868-6049545af836
2026-08-23 11:25:03 +02:00
bc1966da82 docs: record attended login containment evidence
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0290b-3241-74c3-b868-6049545af836
2026-08-23 01:32:59 +02:00
0fae0904ce fix: contain attended OpenBao login output
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0290b-3241-74c3-b868-6049545af836
2026-08-23 01:31:05 +02:00
461f580813 docs: project remaining WP0027 owner gates
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0290b-3241-74c3-b868-6049545af836
2026-08-22 23:50:46 +02:00
cdb44a8f23 docs: project WP0027 infra review contract
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0290b-3241-74c3-b868-6049545af836
2026-08-22 22:38:00 +02:00
7fa6985f72 docs: record recovery preparation boundaries
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0290b-3241-74c3-b868-6049545af836
2026-08-22 22:12:52 +02:00
5ac47520b1 docs: establish attended recovery drill scenario
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0290b-3241-74c3-b868-6049545af836
2026-08-22 22:00:35 +02:00
2c1fc25e43 test: account for recovery ceremony lane
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0290b-3241-74c3-b868-6049545af836
2026-08-22 20:58:50 +02:00
8cd10eae17 chore: refresh high-risk route artifact
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0290b-3241-74c3-b868-6049545af836
2026-08-22 20:55:08 +02:00
e3b9b1620c fix: route OpenBao recovery ceremonies safely
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0290b-3241-74c3-b868-6049545af836
2026-08-22 20:54:45 +02:00
fff76ef089 docs: activate credential break-glass planning
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0290b-3241-74c3-b868-6049545af836
2026-08-22 20:36:49 +02:00
custodian-sync
5c2368ad42 chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Updated by fix-consistency on 2026-08-22:
  - update .custodian-brief.md for ops-warden

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0290b-3241-74c3-b868-6049545af836
2026-08-22 20:24:17 +02:00
repo-manager
72a3c0b6e2 chore(registrar): assign State Hub identifiers
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0290b-3241-74c3-b868-6049545af836
2026-08-22 19:09:26 +02:00
custodian-sync
145e31034e chore(consistency): sync task status from DB [auto]
Some checks failed
CI Smoke / host-smoke (push) Has been cancelled
CI Smoke / container-smoke (push) Has been cancelled
Updated by fix-consistency on 2026-08-22:
  - update .custodian-brief.md for ops-warden

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0290b-3241-74c3-b868-6049545af836
2026-08-22 19:09:25 +02:00
8280c0b7b7 fix: route OpenBao platform administration login
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0290b-3241-74c3-b868-6049545af836
2026-08-22 19:08:21 +02:00
e24d2d5bd0 docs: record live zone config migration
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0291a-1e87-7151-9934-fcbfe3f65eb1
2026-08-22 15:50:42 +02:00
bebcdf929c docs: assess security zone workplan DoD
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0291a-1e87-7151-9934-fcbfe3f65eb1
2026-08-22 15:42:23 +02:00
59d426b87b chore(consistency): sync completed zone task
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0291a-1e87-7151-9934-fcbfe3f65eb1
2026-08-22 15:40:41 +02:00
custodian-sync
4143d1d8da chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Updated by fix-consistency on 2026-08-22:
  - update .custodian-brief.md for ops-warden

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0291a-1e87-7151-9934-fcbfe3f65eb1
2026-08-22 15:40:33 +02:00
6604ace982 chore: refresh generated security inputs
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0291a-1e87-7151-9934-fcbfe3f65eb1
2026-08-22 15:37:33 +02:00
7ce58ae638 feat: adopt security zones and explicit workload refs
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0291a-1e87-7151-9934-fcbfe3f65eb1
2026-08-22 15:36:37 +02:00
custodian-sync
12c637cbf2 chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Updated by fix-consistency on 2026-08-22:
  - update .custodian-brief.md for ops-warden

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0290b-3241-74c3-b868-6049545af836
2026-08-22 12:42:42 +02:00
e18894ee5b Scale the blocker window by lane risk, converging with risk-nexus
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
risk-nexus accepted the offer to match their convention rather than grow a second
one, and published it: 14d critical/high, 30d medium, 60d low, nothing auto-closing
on staleness alone. Their preference — point warden route gaps at those windows and
the two registers agree without a shared mechanism — is better than a joint tool.

blocker_stale_days() now maps lane risk onto those windows. A flat 14 would have
been wrong in both directions: too aggressive for a low-risk pointer, and it treated
an admin PAT lane the same as one.

ungraded takes the shortest window, not the longest. ADR-0007 makes an absent grade
a defect and ADR-0008 makes a grade cover the whole path, so a lane nobody graded is
the one whose blocker deserves least trust. Encoding that as 60 days would have been
the fail-open default this repo already fixed once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:29:29 +02:00
55f0f47a02 WARDEN-WP-0033-T05: split the stale cadences, and record how a blocker was verified
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
The 90-day --stale-days default on `warden route gaps` was not a loose threshold,
it was an inert one: the delegation register was created 2026-08-15, so it could
not have fired before November. It was inherited from the catalog pointer cadence
and applied to a claim with a completely different half-life.

Two changes. DEFAULT_BLOCKER_STALE_DAYS = 14 now governs interim blockers, while
DEFAULT_STALE_DAYS = 90 keeps governing pointer freshness -- "is this the right
owner and page" is quarterly, "has the owner answered" is not. 14 is calibrated
on blockers that actually cost something: ten days for the secrets-engine lanes,
one for RISK-F-0001, roughly fifty for FLEX-WP-0007.

The second change matters more. `reviewed` records when someone touched an entry,
which is indistinguishable from re-checking it -- six lanes read as freshly
reviewed today because I typed in them. `verified:` now says how the claim was
established, and asked-and-waiting explicitly does NOT count: that is the state
the secrets-engine blocker sat in for ten days while looking current. A lane in
that state is stale at zero days old, and key-cape-oidc-login proves it works.

8 of 14 interim lanes are honestly marked unverified rather than given a fresh
date they did not earn.

--fail-on-stale exits 3 for a cron or gate. No CI test on age: a date-triggered
failure breaks the build for whoever commits next instead of whoever owns the
blocker. The CI test is structural -- every interim lane must record how it was
verified -- so it fails on the commit that introduces the omission.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:26:10 +02:00
a565e62b2f Regenerate the artifact; correct the test that classified openbao-api-key as a gap
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
test_catalog_gaps_lists_only_interim asserted openbao-api-key was an interim
cover. It encoded the same classification the entry did, so it defended the
wrong answer rather than catching it -- the second time this session a test has
held a judgement still (see ADR-0008).

The staleness test added in T03 did its job on the first real change: it failed
the moment the catalog moved without the generated artifact being re-emitted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 09:06:08 +02:00
675e04e8e6 Correct the delegation register after secrets-engine's SECRETS-WP-0006 answer
Three fixes, the first of which is my own botched edit from an hour ago.

1. The "six delivery modes" claim was never actually removed. I ran a str.replace
   with no assertion on the result, it matched nothing, and the print said "ok".
   The wrong claim sat in all seven lanes while I told secrets-engine it was
   fixed. This edit asserts 5/1/1 and fails loudly otherwise.

2. Five lanes ACCEPTED (SECRETS-WP-0006, decision ae676382). The blocker now
   records that secrets-engine holds the entry and that the interim proxy retires
   on approved native verification in T05 -- not on acceptance. Retiring early
   would drop the cover before the replacement is proven.

3. Two lanes REFUSED, and both refusals are right:
   - key-cape-oidc-login: intended_owner corrected secrets-engine -> key-cape.
     Login and identity-token issuance were never secrets-engine's. Not yet asked
     of key-cape, and the record says so.
   - openbao-api-key: not a delegable lane at all -- a <domain>/<workload>/<bundle>
     routing template, not one secret lane, so there is no front door to own.
     interim -> native with railiance-platform named: OpenBao is itself the front
     door and this entry is a pointer to it, never an interim cover. It should
     not have been inflating the interim count.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 09:04:54 +02:00
b30626edcf chore(consistency): regenerate WORK-RECORDS.md
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:41:22 +02:00
6cd969dda9 WARDEN-WP-0033-T04: route the coding-agent issuance identity to key-cape
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
Routed, not absorbed. It is an identity and issuance question about a principal
authenticating to OpenBao, which is key-cape's. secrets-engine drew the same
boundary at us an hour ago over key-cape-oidc-login and we agreed with them.

Named zone-engine as interested and user-engine as explicitly not involved, so
neither is concluded by inference. Asked for accept or refuse; a refusal with a
pointer is an equally good answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:39:10 +02:00
6e1d5201aa WARDEN-WP-0033-T03: emit the high-risk data-path artifact
railiance-platform asked for a generated list to consume instead of hand-
maintaining agent-high-risk-boundary. Hand-maintaining it is what let the two
lists drift for four lanes in RISK-F-0009.

19 high-risk lanes, 14 concrete data paths, 5 without a single KV address listed
separately so absence does not read as omission. Carries catalog_revision and a
dirty flag. fields is null where unestablished, never a one-element guess.

The header states plainly that this is an input and not a policy: railiance-
platform owns the deny set and may deny more, less, or dispute a grade. ADR-0002
survives the handoff.

Two CI tests guard staleness, because a consumer applies this to a live control.
Note the immediate consequence of T02: 2 uncovered against a policy they closed
to 0 yesterday.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:38:13 +02:00
c357ce5908 WARDEN-WP-0033 T01/T02: two under-graded lanes, and ADR-0008
secrets-engine reviewed our catalog metadata while drafting their five entries
and graded issue-core-ingestion-api-key and reuse-surface-hub-write-token high.
We had both as standard, and had deliberately regraded them DOWN on 2026-08-19.

They are right. Both paths carry a second credential our grade never looked at --
GITEA_BACKEND_TOKEN (CCR-2026-0002, a deliberate field-set decision) and a
dual-consumer webhook HMAC (CCR-2026-0005). Neither is recovered by rotating the
credential the lane is named after.

The defect is structural: we graded the lane by its headline field, but a read
returns every field at the path. Worse, the evidence was already in the CCRs we
cite as authoritative -- not missing, unread -- and a test asserted the wrong
answer, so a correct first-pass grade got overruled by it.

ADR-0008 records the rule: a grade covers every field its path discloses.
ADR-0007 is unchanged and still governs; this says what the grade is of.

Six of the remaining standard lanes have no KV path. Two have paths and no field
evidence; per ADR-0008 they are stated as unknown rather than assumed, and left
for operator-sanctioned grading.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:36:42 +02:00
9b7e2e9bd2 Open WARDEN-WP-0033 — native lane handoff
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
secrets-engine accepted five interim lanes and drafted the entries itself rather
than wait for our contribution. Reviewing them turned up a defect in our own
grading model, which is now T02 and the most consequential item in the plan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:30:19 +02:00
06d0a1e690 Reframe the secrets-engine blocker on seven interim lanes
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
The blocker asked whether `secrets-engine exec --catalog` generalizes over
arbitrary OpenBao lanes. Asked 2026-08-11, chased 08-15, never answered. Rather
than chase a third time, read their code.

It generalizes by construction: catalog.py takes mount and path as plain fields,
`kv` is a general kind, and six delivery modes are supported. What exists is two
catalog entries -- warden-sign and whynot-design-npm-publish -- which are exactly
the two lanes this register already marks native.

So the blocker was misframed for ten days. Not "can the engine do this" but "who
authors the entries and who operates them", which is smaller and had never been
put to them. Register now says that, and ops-warden has offered to author all
seven entries against their schema for them to accept or reject.

Applying the rule this repo already had and missed twice this week: re-read a
blocker before trusting it. A blocker is a claim about the world at a date.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 01:26:07 +02:00
0dafb53e84 WARDEN-WP-0032-T06: record the live verification, and that the blocker was stale
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
The token was never expired and the verification script was never written. Both
were recorded here as fact for a day. Noting it as an instance of the re-read-
your-blockers rule rather than quietly correcting it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 00:50:45 +02:00
edb587851c Verify the OpenBao read-boundary live; ship the invariant
The operator token was not expired after all -- `bao policy read` succeeded, so
the deployed policy is now compared directly instead of the file. Three
corrections to RISK-F-0009, which was filed static:

1. Six high-risk lanes are uncovered, not eight. The finding counted
   openbao-api-key (a path pattern) and ops-warden-warden-sign-token (a broker
   grant, not KV) among the concrete uncovered paths, while its own prose said
   the first was a pattern. Five lanes have no address for a policy to deny.
2. Coverage holds at 6 of 17 against the live policy.
3. The deployed policy has drifted from the file: the file denies
   platform/workloads/core-hub/runtime, the server does not. No ops-warden lane
   maps there so our numbers are unchanged, but it proves the file was never a
   safe proxy for the server -- which is what the finding flagged as unconfirmed.

scripts/check_agent_read_boundary.py is the invariant RISK-F-0009 asked for
rather than a one-off audit: it fails when a high-risk lane has no corresponding
deny. Capabilities-only by construction -- it reads the policy document, never a
secret value, and never mints a token.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 00:50:00 +02:00
801bbe7b35 chore(consistency): regenerate WORK-RECORDS.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 00:45:49 +02:00
custodian-sync
1cd71e74d7 chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Updated by fix-consistency on 2026-08-21:
  - update .custodian-brief.md for ops-warden
2026-08-21 00:45:34 +02:00
9ed8b452a1 Refresh the activity-core issue-sink lane; close WP-0032-T01
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
issue-core asked us to drop a CoulombCore/port-18765 bridge topology from
activity-core-issue-sink. That topology was never in this repo — the only address
the playbook carried was a local-dev 127.0.0.1:8765 example. What was actually
stale was step 5, which still told workers to coordinate with railiance-platform
"when the canonical path ships"; it shipped 2026-07-02 and the lane is active.

So: point at issue-core's SCOPE.md for the production address rather than
restating it here (ADR-0001), state plainly that no bridge or forwarded port is
involved so the next reader does not re-derive the retired topology, and route
step 5 through warden route / warden rotate-guide instead of a read.

WP-0032-T01 closes as done. What was owed was inputs, not a design, and
ZONE-WP-0001-T02 is done carrying all of them — including the two that were
corrections to ops-warden's own claims (organization_posture, refused by
net-kingdom; and the workload join key, which this repo wrongly said did not
exist anywhere).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 00:43:42 +02:00
72f7dc2c5d Add scripts/report_workload_join.py — measure the lane-to-workload join
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Computes the join rather than asserting it, and offers both plausible workload
segments per path instead of a single positional guess, because the path
convention is inconsistent.

Measured result: 1 of 27 lanes matches a declared workload. The workload
declaration surface exists, but it covers almost none of the credential estate.

Read-only; touches no secret value and no live system.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 07:22:07 +02:00
50e185ed7b Check the OpenBao half of the read-boundary; route RISK-F-0004
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
agent-high-risk-boundary denies 5 data paths, covering 6 of ops-warden's 17
high-risk lanes. Eight high-risk lanes with concrete KV paths are not denied,
four of which were already graded high before the 2026-08-19 regrade — the
divergence is pre-existing and the regrade only made the lists comparable.

This is the layer that matters most: warden access exits 7 for all 17, but that
protects only the ops-warden path. The policy protects a direct bao kv get,
which is the 2026-07-16 vector.

Routed to risk-nexus as RISK-F-0004 with fix_owner railiance-platform, since the
policy is theirs. Live confirmation outstanding — ops-warden's OpenBao token is
expired, so this is a static file-vs-catalog comparison and the finding says so
rather than implying it was verified against the server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 07:11:03 +02:00
custodian-sync
9fc5a735c6 chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Updated by fix-consistency on 2026-08-20:
  - update .custodian-brief.md for ops-warden
2026-08-20 01:14:31 +02:00
d0d4f9d8fc Make the risk grade fail safe, and gate CI on absence
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
The mechanism behind RISK-F-0003 was sharper than the finding described.
is_high_risk was risk == "high", but risk was never absent at the model layer:
RouteEntry.risk carried a dataclass default of "standard". An omitted grade was
not unhandled, it was actively resolved to the permissive value — fail-open by
construction, which is why nothing ever warned.

The default is now "ungraded" and is_high_risk returns true for anything outside
an explicit low-risk vocabulary (standard / low / accepted). An omitted grade and
an unrecognised grade from a newer catalog both resolve to high, so the boundary
fails safe in both directions rather than reading an unknown value as permission.

test_every_repo_catalog_lane_is_explicitly_graded is the CI gate that stops an
ungraded lane being committed, per ADR-0007: absence is not a grade.

"accepted" is in the low-risk vocabulary deliberately, ready for the
maturity-derived default — an experimental-context lane may be explicitly
accepted, which is a graded decision rather than an omission.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 01:13:35 +02:00
ac85259c20 Grade every catalog lane; record ADR-0007
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Closes the RISK-F-0003 exposure. All 14 ungraded lanes now carry an explicit
risk grade with its justification in the entry: 17 high, 10 standard, 0
ungraded. The agent read-boundary now fires (exit 7) on lanes that were silently
outside it.

Graded on merit rather than defensively. A first pass marked two ordinary
internal workload secrets high; test_high_risk_lanes_classified asserted the
opposite and was right, so both were regraded down. high means disclosure into a
logged context is damaging beyond what rotation recovers. inter-hub-bootstrap-ssh
is high conservatively, with the reason in the entry so it is regraded with
evidence rather than assumed down.

ADR-0007 records the rule the grading rests on: build-stage permissiveness
applies to controls that gate work, not to controls that prevent credential
disclosure. The test is friction, not severity — the read-boundary blocks nobody,
since --out/--exec/--wrap remain available, so relaxing it buys nothing. A
blocked operation is recovered by retrying; a credential in a logged transcript
is not recovered by rotation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 23:44:58 +02:00
7b8dd3467b WARDEN-WP-0032: grade the exposed lanes now, structural fix after the model
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Splits RISK-F-0003's response in two. T05 grades the five exec_capable ungraded
lanes explicitly — a live gap in a shipped ADR should not wait months for the
zone model. T06 makes absence impossible once the model says what absence means,
per the operator's maturity-derived default.

Grading is flagged as judgement requiring operator sanction, not backfill.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 23:24:34 +02:00
f528c1df31 Declare ops-warden's tenancy posture
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
net-kingdom ruled that zone membership rides tenancy.yaml under a reserved
zones: key, and ops-warden had no such file — so declaring zones later implies
declaring the six-axis vector first. This is that file.

Current: I1 A1 E0 P n/a R n/a V0, validated against the canon schema.

Graded honestly against the standard's own worked examples rather than
flatteringly. A1 not A2: there is a single choke point on the signing path, but
it binds an actor to principals, not a request to the tenants it may act for,
and tenant context is a constant. E0 is accurate and is not a defect to
remediate — ops-warden holds no tenant-keyed data because ADR-0002 forbids it
custody, and claiming E1 on the strength of OpenBao's enforcement would be
someone else's control counted as ours. V0 because the code path for a local-CA
fallback exists but has never been exercised, and §13 does not accept "the code
path exists" as evidence.

implemented: A3 is the one place ops-warden is ahead of its current level —
flex-auth delegation is built and was verified live against the enforcing pin
(decision:f3f7c88f9585582a). It is not current because ADR-0006 defers it.

The zones: key is deliberately absent with the reason written in: no zone
vocabulary exists yet, so any value would be fabricated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:20:44 +02:00
9008ded1b0 WARDEN-WP-0032: net-kingdom amendment — carrier file settled, organization_posture answered
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Amended by net-kingdom as canon owner of tenancy-posture_v0.1, answering
ZONE-WP-0001-T01.

T01: organization_posture does not fold into zones and does not belong in a
per-repo declaration — it is a fleet-wide time-varying scalar. Hand it over as an
input.

T03: zone membership is declared in tenancy.yaml under a reserved zones: key
(canon Decision 5.6). ops-warden has no tenancy.yaml, so this now implies
declaring the six-axis posture vector too. Also records why this declaration may
not set stance: a declarer that sets its own stance makes exempt conformant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:06:39 +02:00
b1070be644 WARDEN-WP-0032: flex-auth amendments to T02 and T03
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
flex-auth reviewed ZONE-WP-0001 as the consuming PDP and two findings
land on the ops-warden consumer side:

T02 - fail_closed is not expressible by a PDP. Fail-open describes what
warden sign does when flex-auth is unreachable, so no decision exists to
carry it. The replacement for policy.enabled is two things: stance,
which arrives in the decision, and failure mode, which stays local and
is declared per zone.

T02 - build_flex_auth_registry.py:77 already emits a hardcoded
trust_zone: platform that no policy reads. Resolve that before
compiling zone membership rather than adding security_zone beside it.

T03 - the declaration must say whether a zone rides the actor (subject)
or the lane (resource); in the compiled registry an actor is both.

No flex-auth registry schema change is needed - metadata, labels and
attributes already flatten into the rego input.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:00:13 +02:00
5e2f7dc939 chore(consistency): sync WP-0032 hub ids [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 21:22:59 +02:00
8c58f8bfa1 Hand the zone model to zone-engine; keep WP-0032 as the consumer side
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
zone-engine is seeded and owns the security zone model as ZONE-WP-0001. Under
ADR-0005 ops-warden implements one lane narrowly and routes the rest, and an
estate-wide enforcement model is not a lane to absorb — it was ops-warden's
deferred flip that exposed the gap, not ops-warden's model to define.

WARDEN-WP-0032 is rewritten as the consumer side: hand the estate inputs to
ZONE-WP-0001-T02 (27 catalog lanes, the actor inventory, the three posture axes,
the three controls the model must express, and the compiled-registry path),
then replace policy.enabled with a zone-aware control and amend ADR-0006 to say
ops-warden follows the model rather than owning it.

ADR-0006 and SCOPE updated to point at zone-engine, which joins the related
repositories table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 21:20:34 +02:00
b845f4b51e Draft WARDEN-WP-0032 properly — security zones, ownership question first
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Expands the placeholder into a real plan, grounded in what the estate already
has rather than first principles:

- tenancy-posture_v0.1 is the structural model to follow (canon standard +
  per-repo declaration + "accuracy, not altitude"), and its §14 adoption stance
  is "structure, not tooling" — a precedent about sequencing.
- Reefs are substrate placement zones with real risk statements. A security
  zone is not a reef; repo-manager's own "topology is not readiness" applies
  here as "placement is not posture".
- flex-auth is latency-critical and already consumes a compiled registry
  snapshot, so zone membership should reach the PDP by compilation rather than
  a synchronous lookup in the decision path.
- warden plan already returns verdicts with reasons; zone-aware enforcement
  extends that machinery instead of growing a parallel one.

T01 settles ownership before any modelling, because it decides which repo the
rest lives in: canon standard, a zone-engine runtime authority, or flex-auth
policy data. The recommendation is canon-first with zone-engine named as an
explicit promotion — but the time-boxed refactor exception is the case that
could justify an engine immediately, since an expiring grant is state and state
wants an owner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 20:38:25 +02:00
custodian-sync
3d968bb993 chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Updated by fix-consistency on 2026-08-19:
  - update .custodian-brief.md for ops-warden
2026-08-19 20:32:22 +02:00
cbf6828061 ADR-0006: enforcement is zone-scoped; defer the policy.enabled flip
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
flex-auth enforced its ops-warden pin (FLEX-WP-0016 T03) and the gate verified
clean against it: readiness exits 0, decision:f3f7c88f9585582a, anonymous
/v1/check now 401. Everything needed to set policy.enabled: true was in place.

It stays false, by decision. policy.enabled is a single repo-wide boolean, and
with fail_closed: true it makes flex-auth a hard dependency of every warden
sign — including the certs the ops-bridge tunnels depend on, one of which
carries the policy call itself. Uniform enforcement across an estate being
actively rebuilt hardens the access needed to perform the rebuild.

The repo already refuses one-dimensional posture: WP-0015 shipped environment
and maturity axes, WP-0029 added organization_posture. A global flag ignores all
three. ADR-0006 records that enforcement belongs to a zone, and binds future
work — a zone-blind enforcement flag is out of order, not merely unwise.

WARDEN-WP-0032 drafts the zone model, leading with the ownership question:
whether this is ops-warden's to own or NetKingdom canon to consume (ADR-0005).
WP-0031 is finished with T05 cancelled and resuming as WP-0032-T05.

Also replaces the hand-run kubectl port-forward with a managed ops-bridge
tunnel, flex-auth-ops-warden-railiance01 (-L 19090:10.43.1.165:8080).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 20:31:28 +02:00
661176e37a Retire CoulombCore references; correct the 16443 diagnosis
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
CoulombCore is being retired, so the docs stop using it as the reference host:
state-hub-coulombcore examples become state-hub-railiance01, and the reuse-surface
playbook no longer attributes bao.coulomb.social to it — that resolves to
92.205.62.239, which is railiance01. The openrouter lane keeps its factual note
about ESO on the CoulombCore cluster, with a retirement flag for its owner.

Also corrects this session's own error. The WP-0031 evidence blamed the
Unauthorized on a local-port collision between k3s-api-coulombcore and
k3s-api-haskelseed. That was wrong: the haskelseed tunnel is a reverse forward,
where local_port is a destination rather than a listener, so they never competed.
16443 was simply CoulombCore's k3s — a different cluster whose client CA does not
know that cert. The wrong reason had already gone to flex-auth, so it is
corrected in the file rather than quietly dropped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 19:31:41 +02:00
custodian-sync
e22f9b3434 chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Updated by fix-consistency on 2026-08-19:
  - update .custodian-brief.md for ops-warden
2026-08-19 19:07:27 +02:00
654c05dece WARDEN-WP-0031 T04: prove ops-warden's caller identity against the live pin
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
flex-auth's binding names system:serviceaccount:ops-warden:ops-warden, and that
ServiceAccount did not exist. deploy/kubernetes/caller-identity.yaml creates it
plus its namespace — no RBAC, automount off; it is never used to call the
Kubernetes API, only to be TokenReviewed. Applied to the railiance01 cluster.

Operator warden.yaml now uses caller_auth mode: command (kubectl create token,
audience flex-auth, 10m). Gate exits 0 live against a port-forward of the pin:
HTTP 200, effect=allow, decision:f3f7c88f9585582a.

The evidence is not that allow — warn allows anonymous callers too. It is that
the pin's "caller authentication warning" count held at 4 across two
authenticated runs. That is the ADHOC-2026-08-17-T01 condition.

Also gives the readiness probe a structurally complete context, so a deny means
the policy said no rather than the probe being malformed.

policy.enabled stays false. T05 waits on flex-auth setting callerAuth.mode:
enforce (their FLEX-WP-0016 T03).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 19:06:04 +02:00
custodian-sync
9b9bfe565a chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Updated by fix-consistency on 2026-08-19:
  - update .custodian-brief.md for ops-warden
2026-08-19 15:10:03 +02:00
0a331413a2 Send a caller identity to flex-auth so policy.enabled can flip
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
flex-auth's flex-auth-ops-warden pin (FLEX-WP-0016) TokenReviews the caller and
binds resource.system: ops-warden to system:serviceaccount:ops-warden:ops-warden.
policy.py posted /v1/check with no Authorization header, so the pin logs
"caller authentication warning" and can only run callerAuth.mode: warn — which,
under ADHOC-2026-08-17-T01, is exactly what blocks policy.enabled: true.

- policy.caller_auth (none | file | env | command) + src/warden/caller_identity.py:
  token resolved per call, never cached, written, or logged (ADR-0002)
- both check_sign_policy and check_fetch_policy attach the bearer header; an
  unobtainable token fails closed rather than retrying anonymously
- scripts/check_policy_caller_identity.py: read-only gate, prints length and a
  truncated fingerprint only, distinguishes 401 (audience/binding) from 403
- example config: caller_auth block, and flex_auth_url corrected — it pointed at
  flex-auth.flex-auth.svc, a Service that does not exist
- WARDEN-WP-0031, PolicyGatedSigning caller-identity section and flip sequence

Default stays mode: none, so behaviour is unchanged until an operator opts in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:08:34 +02:00
35aff380a3 Lift ops-warden's binding rules into owned ADRs
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Five rules that governed this repo lived in wiki prose, a workplan, and a
comment at the top of catalog.yaml. All were followed; none was addressable.
A reader outside ops-warden could not cite one, could not tell whether it was
current, and — the point of this change — could not tell whether it was ours
to change or someone else's that we merely obey.

  ADR-0001  The routing catalog is a pointer layer, never a second copy
  ADR-0002  ops-warden is a transparent conduit, never a secret broker
  ADR-0003  Cover gaps, but never silently own them
  ADR-0004  High-risk lanes refuse raw value streaming to agent sessions
  ADR-0005  Implement one lane narrowly, route everything else

Each carries owner: ops-warden, which is the load-bearing field. It says we
follow the rule AND we are responsible for changing it — by superseding ADR,
never an in-place edit. The failure this prevents runs both ways: a rule we own
mistaken for inherited canon never gets fixed, because we wait for an owner who
does not exist; inherited canon mistaken for ours gets quietly bent, and the
drift is invisible until it breaks a repo that trusted the canonical version.

Rules we follow but do not own — NetKingdom canon, the IAM profile, the
credential-management standard, the-custodian's ADR-001 workplan convention —
are cited, never copied into docs/adr/. Copying them would recreate exactly the
second-source-of-truth failure ADR-0001 exists to prevent. architecture.md also
now flags the three-digit/four-digit ADR-001 vs ADR-0001 collision, which is
itself an ours-versus-inherited confusion waiting to happen.

Publication verified rather than assumed: all five render through policy-nexus
tools/render.py, and owner reaches the reader in three places — the page eyebrow
(render.py:346), the index Owner column (build_site.py:123,137), and the
publication manifest. build_site.py:179 makes title/status/owner required, so
ownership cannot be dropped on the way out. policy-nexus publishes and never
writes back; docs/adr/ stays the source of truth.

Documentation adapted: SCOPE.md gains a Governing rules section and an orientation
entry; .claude/rules/architecture.md replaced its stub with the ADR index, the
owned-versus-inherited rule, and ADR-over-wiki precedence; finding-routing.md's
ADR-gap section closed; catalog.yaml's no-double-source header now cites ADR-0001
rather than the originating workplan.

uv run pytest -q → 338 passed, 4 deselected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 13:35:13 +02:00
faa4f2c23e chore(consistency): register ADHOC-2026-08-17, refresh WORK-RECORDS [auto]
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 13:25:31 +02:00
683f22d986 Adopt risk-nexus finding routing; record the ADR gap policy-nexus exposes
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Two estate repos now own things ops-warden had been handling in-repo by
default. New rule .claude/rules/finding-routing.md, wired into CLAUDE.md.

The correction it encodes: on 2026-08-17 flex-auth reported a live
authorization bypass directly to ops-warden — in the service our own pre-sign
gate consults. We answered the design question and wrote the recommendation
into wiki/NetKingdomSecurityMap.md, and did not route the finding.
rapp-postgres filed it, which is why RISK-F-0001 reads
reported_via: rapp-postgres despite ops-warden being a first-hand recipient
and the affected PEP. That is the exact failure risk-nexus/INTENT names:
findings landing in whichever document was open. A wiki section answers a
question; it carries no severity, owner, date, or review that fires when
nobody looks. Answering and routing are not alternatives.

Also recorded: the delegation register is explicitly NOT a findings feed
(risk-nexus wants a register small enough to read); severity/disclosure/
escalation stay unset when we route, because the reporter says what is true
and that repo says how bad it is; and a blocker is a claim about the world at
a date — RISK-F-0001 invalidated one of ours in a day.

Offers warden plan (WP-0029) to risk-nexus for its unwritten escalation duty:
a shipped classifier for what must reach the operator personally, decided by
properties rather than instinct, carrying reasons and a typed act.

Flags but does not close the policy-nexus gap: ops-warden has no ADRs, yet
carries rules that govern other repos (no-double-source, conduit-not-broker,
interim-by-default, agent read-boundary). They sit in wiki prose and are
therefore unpublishable and uncitable. Structural call, left to the operator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 13:04:50 +02:00
3447687bbc Answer flex-auth caller-auth question; refresh two delegation lanes
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Inbox triage (ADHOC-2026-08-17), three messages, all answered and marked read.

flex-auth (FLEX-WP-0015 T02) reported that /v1/check and /v1/batch_check
authenticate no caller — any workload with cluster reach can assert any subject
and get an authoritative allow. It lands here because ops-warden owns workload
identity in this estate and its pre-sign gate is a flex-auth PEP.

Recommendation recorded in wiki/NetKingdomSecurityMap.md: Kubernetes
ServiceAccount TokenReview with a projected, audience-scoped token. Rejected the
shared-secret header — it manufactures a risk: high lane with a rotation owner
per caller on the authorization path, the exact interim-proxy debt WP-0030
exists to stop growing. Deferred mTLS pending an owner for a workload X.509 CA.
Bind `system` to the authenticated SA at auth time, but keep the resource-type
allowlist in the policy package rather than the admission layer. Warn-only
rollout; policy.enabled must not flip while /v1/check answers unauthenticated
callers. Recorded as a pattern, not a catalog entry.

Catalog delegation refreshes:
- coulomb-social-runtime-env: USER_ENGINE_PROXY_SECRET ownership settled
  (decision 8fe22037, USER-WP-0021) — intended_owner stays railiance-apps,
  answered clause dropped from blocked_on, consumers: [user-engine] recorded.
- rapp-qonto-keycape-client: blocked_on no longer implies an in-flight
  KEY-WP-0008 dependency; the key-cape-native exchange command is unopened work.

uv run pytest -q → 338 passed, 4 deselected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:55:33 +02:00
263b919c17 Refresh WORK-RECORDS for finished WARDEN-WP-0030
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
2026-08-15 20:56:51 +02:00
c93e3c9b43 Ship WARDEN-WP-0030: delegation register for every catalog lane
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Every execution position is now explicit. Catalog entries carry
delegation.mode (permanent / native / interim) with intended owner and
blocker. warden route gaps lists the interim set. Promotion requires
the ownership question. Doctrine lives in AccessRouting.md; the
register was published to the named owner repos.
2026-08-15 20:54:58 +02:00
8d3706fa06 Refresh WORK-RECORDS for finished ADHOC-2026-08-11
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Generated index now matches T03 done and the workplan finished.
2026-08-15 20:37:34 +02:00
custodian-sync
49b8618ff1 chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Updated by fix-consistency on 2026-08-15:
  - update .custodian-brief.md for ops-warden
2026-08-15 20:36:20 +02:00
bfa0eda327 Close ADHOC-2026-08-11: hand warden-sign AppRole to WP-0027
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
T03 stays parked (neither apply nor withdraw). Neither un-park trigger has
fired, so the ad-hoc is finished and WP-0027 T02 owns the question.
2026-08-15 20:35:12 +02:00
817af8bc6e catalog: draft scaleway-bootstrap lane
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Pointer to the mason plan and playbook. Founder provision only;
warden does not execute the fetch.
2026-08-14 17:36:17 +02:00
56876ee456 Add draft routing entry audit-core-senders
Pointer-only. Database leases stay on database-dynamic-credentials.
Promote after the Mason AppRole build is verified.
2026-08-13 10:27:13 +02:00
064a0736be Route email-connect transactional SMTP and ingest token.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Catalog id email-connect-transactional (active) with OpenBao pointer playbook
for CCR-2026-0010 / EMAIL-WP-0004 custody.
2026-08-12 13:32:11 +02:00
8d5188c24f Classify 5 proxy lanes interim; hold 6 pending secrets-engine
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Revised the classification axis after inspecting the entries: sorting by
subsystem was wrong. Nine of eleven lanes are a generic KV read with the
caller's own token, duplicating no owner procedure. Test is now
procedure-or-lifecycle vs generic read.

Interim now: rapp-qonto-keycape-client (key-cape), binky-company-email-imap
and binky-qonto-api (tenant-engine), railiance-backup-offsite-lane and
agent-harness-forgejo-deploy (re-establish procedures).

Held: the six thin-wrapper lanes are permanent only if secrets-engine exec
stays per-lane; asked whether it generalizes (msg 7d55d332).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 01:33:29 +02:00
cd074caaaa Park the warden-sign AppRole with explicit un-park triggers
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Founder decision 2026-08-11: neither withdraw nor proceed. The broker covers
the workstation, but unattended signing on a remote tunnel host is unresolved,
and there the AppRole is narrower than placing the broker's issuer token.

Recorded where each trigger will be hit: WP-0027 break-glass task (trust-root
design), and the ops-bridge cutover playbook (token source must be resolved
before going live). secrets-engine told to stop holding apply readiness.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 14:32:04 +02:00
9d42dd5abd Add WP-0030 delegation register; refresh INTENT and SCOPE
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Founder directive: ops-warden works with, never replaces or duplicates,
secrets-engine / tenant-engine / user-engine. Covering an unfilled gap is
acceptable only as a tracked interim with a named intended owner.

- INTENT §9 "Cover gaps, but never silently own them"; success criterion 7;
  tenant-engine and user-engine added to the literacy table; non-goal on
  permanently owning another component's lane
- WP-0030 (proposed): delegation: metadata, backfill, warden route gaps,
  promotion gate, publish the register to owner repos
- history/2026-08-11-delegation-surface-assessment.md: 2 of 24 lanes carry
  exec_owner; 11 proxies record no intended owner
- SCOPE refreshed to 2026-08-11 (was 6 workplans behind); completeness C5 -> C4

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:11:01 +02:00
custodian-sync
95ecfe51e2 chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Updated by fix-consistency on 2026-08-11:
  - update .custodian-brief.md for ops-warden
2026-08-11 10:40:49 +02:00
custodian-sync
538f08c050 chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Updated by fix-consistency on 2026-08-11:
  - update .custodian-brief.md for ops-warden
2026-08-11 10:39:53 +02:00
custodian-sync
b346e63a07 chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Updated by fix-consistency on 2026-08-11:
  - update .custodian-brief.md for ops-warden
2026-08-11 10:38:53 +02:00
cd3eaac97e Triage the stale ops-warden inbox (11 unread)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Answers owed were sent and all 11 marked read: railiance-platform front-door
thread confirmed closed (both lanes active, selector is openrouter-llm-connect),
railiance01/activity-core and llm-connect requests superseded, secrets-engine
warden-sign referred back to the operator.

Leaves T03 open: whether to keep or withdraw the warden-sign AppRole now that
the credential broker serves the VAULT_TOKEN lane.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 10:38:05 +02:00
456fdc4b4f Repair stale rapp-qonto-keycape-client wiki anchor
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
The catalog pointed at wiki/CredentialRouting.md#credential-routing-catalog,
which does not exist; the live heading is "Routing catalog index". Restores
tests/test_routing.py to 61/61.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 10:27:12 +02:00
a9086ad6b6 Route dynamic database credentials to rapp-postgres
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
2026-08-10 19:37:05 +02:00
350f66472c Note apps-pg live and coulomb-social DB connectivity in playbook
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Cluster healthy; role/database present; env Secret keys established.
2026-08-09 02:18:17 +02:00
d8d3d5b1a0 Route coulomb-social runtime env credentials via ops-warden catalog
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Add coulomb-social-runtime-env lane and playbook. Points operators at
railiance-apps env-secret assembly; ops-warden never holds values.
2026-08-09 02:11:34 +02:00
979c6f68b4 Adopt Target Revenue Source License V1C1 (org-wide preliminary rollout)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Maintainer decision, 2026-07-29: adopts TRSL V1C1 as this repo's
preliminary governing license, per target-revenue's
workplans/TREV-WP-0008-governance-and-pilot-rollout.md T05. Full
specialist legal review is deferred until out of beta (target-revenue
SCOPE.md §1). No Phase is yet declared for this repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 00:42:48 +02:00
2e862bbff0 Route rapp-qonto workload identity
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
2026-07-27 20:37:09 +02:00
d961da1ef2 Promote rein-openweights-openrouter-approle: draft -> active
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Founder completed paste-once-provision; glas-harness/GLAS-WP-0002-T02's
live verification succeeded for real (AppRole login, KV v2 read, real
OpenRouter call, real commit, OPENROUTER_API_KEY unset throughout).

Two real fixes recorded along the way: platform-admin's policy needed
a new reins/* entry (every other KV mount was already listed there),
and the consumer policy itself needed the KV v2 data/+metadata/ path
shape instead of the bare KV v1 path it was originally written against.
Full account in ops-mason/plans/rein-openweights-openrouter-approle.md
section 7.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 01:51:57 +02:00
c0a50bc1bf Catalog: rein-openweights AppRole for non-interactive OpenRouter key read
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Proposed by ops-mason (MASON-WP-0001-T05), built and approved 2026-07-27
(Bernd Worsch). New pointer-only entry mirroring
agent-harness-binky-mail-approle's shape: AppRole login, no operator
present, scoped to exactly one KV path (reins/rein-openweights/openrouter,
new reins/ KV v2 mount -- no existing mount fit without widening scope
beyond what was approved). status: draft until the founder completes
paste-once-provision and glas-harness/GLAS-WP-0002-T02's live
verification succeeds.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 01:22:59 +02:00
cb6e9a73f4 catalog: promote binky-qonto-api to active (CCR-2026-0008)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Point at live tenants/binky/qonto-api fields API_KEY/API_USER; playbook and
CredentialRouting index updated after first BINKY-WP-0005 read-only pull.
2026-07-21 21:42:10 +02:00
5d30220cc5 catalog: draft binky-qonto-api lane + playbook (CCR-2026-0008)
Tenant Qonto bank API front door for binky-control read-only MCP; stays
draft until policy apply, founder provision, and capabilities-safe verify.
2026-07-21 21:26:25 +02:00
custodian-sync
507bbef6ea chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 20s
Updated by fix-consistency on 2026-07-18:
  - update .custodian-brief.md for ops-warden
2026-07-18 17:00:26 +02:00
5149946a4c WARDEN-WP-0029: implement plan front door, org posture, desk, freshness
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
Ship posture-aware access planning: organization_posture=build (axis C),
catalog freshness warnings, warden plan verdicts, localhost founder desk,
and playbook/agent guidance that retire /tmp file-drop patterns.

Compose route catalog + handoff rather than a second routing layer.
2026-07-18 16:59:37 +02:00
5c6b71b83b WARDEN-WP-0029: optimize workplan sequence, constraints, reuse map
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 3s
Reorder for delivery (T02/T05 → T01 → T04 → T03), raise T05 priority for
catalog-staleness root cause, add compose/third-axis/desk non-goals, and
document reuse.coulomb.social building blocks.
2026-07-18 16:50:23 +02:00
cb762e06cb WARDEN-WP-0029: hub workstream/task id write-back
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 16:40:16 +02:00
custodian-sync
7a08171dab chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Updated by fix-consistency on 2026-07-18:
  - update .custodian-brief.md for ops-warden
2026-07-18 14:26:51 +02:00
29645c9303 INTENT §7/§8 + WARDEN-WP-0029: policy front door, build-phase posture, founder surface
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Founder directive 2026-07-18: agents ask ops-warden what a credential
need requires; the founder is escalated to only for policy-required
decisions/identity acts, preferably via a local web interaction surface
— never tasked with raw mechanics (UI clicks, /tmp file drops).
Organization posture 'build' becomes declared configuration. Workplan:
warden plan front door, posture declaration, warden desk, file-drop
retirement, catalog freshness + agent guidance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 14:26:15 +02:00
5219104809 feat(catalog): agent-harness forgejo deploy + binky mail AppRole lanes
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Register agent-harness-forgejo-deploy and agent-harness-binky-mail-approle
with playbook pointers; provisioned 2026-07-17 (metadata only).
2026-07-17 23:57:55 +02:00
custodian-sync
ffc3b22fb0 chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Updated by fix-consistency on 2026-07-17:
  - update .custodian-brief.md for ops-warden
2026-07-17 00:46:37 +02:00
0cec8eef76 WARDEN-WP-0027: backlog with cancelled deferred tasks
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
C-23 forces active when tasks are wait/progress; C-15 preferred wait over
todo. Park Strand B as backlog and cancel T01–T03 until an activation gate
fires (then re-open as todo).
2026-07-17 00:45:51 +02:00
custodian-sync
54fd31aa5c chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Updated by fix-consistency on 2026-07-17:
  - WARDEN-WP-0027-T03: todo → wait
2026-07-17 00:45:35 +02:00
custodian-sync
6fe5034a65 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-07-17:
  - WARDEN-WP-0027-T02: todo → wait
2026-07-17 00:45:35 +02:00
custodian-sync
ffff2eff4f chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-07-17:
  - WARDEN-WP-0027-T01: todo → wait
2026-07-17 00:45:35 +02:00
custodian-sync
885e362daa chore(consistency): renormalize lifecycle state [auto]
Updated by fix-consistency on 2026-07-17:
  - workplan status: backlog → active
2026-07-17 00:45:35 +02:00
25a691d49a WARDEN-WP-0027: park Strand B as backlog (C-23-safe)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Set workplan status backlog and tasks todo (not wait) so fix-consistency
does not re-promote to active. Activate only when a gate fires.
2026-07-17 00:45:03 +02:00
custodian-sync
0433481e94 chore(consistency): renormalize lifecycle state [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Updated by fix-consistency on 2026-07-17:
  - workplan status: backlog → active
2026-07-17 00:44:16 +02:00
6bfbf64108 WARDEN-WP-0027: move Strand B to backlog
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
No activation gate met after WP-0026/0028 closeout. Keep capture only;
promote to ready only when mass-rotate or policy-reconcile is justified.
2026-07-17 00:43:42 +02:00
custodian-sync
59f0277f20 chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Updated by fix-consistency on 2026-07-17:
  - update .custodian-brief.md for ops-warden
2026-07-17 00:34:39 +02:00
c5ec9bdaa6 WARDEN-WP-0028: mark workplan finished after T05
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 8s
2026-07-17 00:34:00 +02:00
custodian-sync
b6861e4b62 chore(consistency): sync task status from DB [auto]
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Has been cancelled
Updated by fix-consistency on 2026-07-17:
  - update .custodian-brief.md for ops-warden
2026-07-17 00:33:55 +02:00
053a1d7cee WARDEN-WP-0028: promote binky-company-email-imap active
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Founder provisioned IMAP on tenants/ (KV v2); capabilities-safe verify
pass. Catalog resolvable; workplan finished.
2026-07-17 00:33:20 +02:00
6b5432229f playbook: IONOS IMAP endpoints for binky-company-email-imap
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Document non-secret provider host/port from founder; point at binky-control
mailbox config. Password custody unchanged (OpenBao tenants/).
2026-07-17 00:15:27 +02:00
custodian-sync
80ce5eb2df chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Updated by fix-consistency on 2026-07-17:
  - update .custodian-brief.md for ops-warden
2026-07-17 00:10:25 +02:00
98a2339b81 WARDEN-WP-0028: tenant secrets on mount tenants/ (first lane draft)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 3s
Adopt tenants/<tenant>/… custody (not platform/workloads). Document
onboarding, add draft binky-company-email-imap catalog entry, and mark
T01–T04/T06–T07 done. Founder Red provision remains T05.
2026-07-17 00:09:28 +02:00
b971403dad WARDEN-WP-0026 finish Strand A (T04/T05/T07)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Promote railiance-backup-offsite-lane to active/resolvable after
capabilities-safe re-verify. Add catalog risk=high, agent read-boundary
(exit 7 + OpenBao policy companion), EXPOSED taint via warden taint, and
close WP-0026.
2026-07-16 23:26:26 +02:00
custodian-sync
7d0c7c7684 chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Updated by fix-consistency on 2026-07-16:
  - update .custodian-brief.md for ops-warden
2026-07-16 23:22:05 +02:00
custodian-sync
0eb2126311 chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 0s
Updated by fix-consistency on 2026-07-16:
  - update .custodian-brief.md for ops-warden
2026-07-16 14:55:24 +02:00
fc0f18aa5c WARDEN-WP-0026 T03: masking display filter (defense-in-depth)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
- warden/mask.py: fingerprint()/mask_value() — presence, length, 8-char sha256
  prefix; never the value.
- proxy.proxy_fetch_fingerprint + `warden access --fingerprint`: masked status view
  (presence/length/hash) that emits no value, so it bypasses the T02 stdout guard.
  Lets two parties compare sha256 prefixes to confirm a shared value without seeing
  it (e.g. rotation landed).
- documented as defense-in-depth (raw bao bypasses it) in OperatorAccessAssist.md
  and the module docstring.
- tests: tests/test_mask.py + CLI fingerprint test. 299 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 14:54:55 +02:00
custodian-sync
04c8b2ab1d chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 0s
Updated by fix-consistency on 2026-07-16:
  - update .custodian-brief.md for ops-warden
2026-07-16 14:52:28 +02:00
359ca1bd0e WARDEN-WP-0026 T02: safe access transports (no secret values on stdout)
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
- proxy.py: proxy_fetch_to_file (mode-0600 file), build_wrapped_fetch +
  proxy_fetch_wrapped (single-use OpenBao response-wrapping token), _capture_value
  helper, is_bao_kv_fetch.
- warden access: --out FILE, --wrap [--wrap-ttl], --unsafe-stdout. Raw --fetch to a
  non-TTY stdout is refused (exit 6) — captured/piped output is the disclosure risk;
  sanctioned transports are --out / --exec / --wrap.
- canon: anti-pattern (secret value onto captured stdout) + transport table in
  .claude/rules/credential-routing.md; OperatorAccessAssist.md examples + G2 updated.
- tests: file/wrap/build + stdout-guard in tests/test_proxy.py. 293 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 14:51:56 +02:00
custodian-sync
c749561b75 chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Updated by fix-consistency on 2026-07-16:
  - update .custodian-brief.md for ops-warden
2026-07-16 14:41:02 +02:00
c3eb59ea04 WARDEN-WP-0026 T06: rotation guidance registry + warden rotate-guide
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
- routing model: RotationGuide (method rotate|re-establish, steps, owner,
  automatable), RouteEntry.rotation + has_rotation + vends_secret.
- catalog parser: validate rotation block; secret-material screen gains a
  prose-safe mode (high-entropy detector only) so authored steps aren't tripped
  by substrings like "s."/"exists.".
- CLI: `warden rotate-guide <id>` (human + --json); route show --json now
  carries has_rotation + rotation.
- scorecard: catalog_rotation_coverage — every active secret-vending lane must
  carry a rotation block (SSH/login/pointer lanes exempt). Promotion checklist
  criterion 9.
- data: rotation blocks for all 7 active vending lanes + the draft
  railiance-backup lane (re-establish: age keypair regen + re-encrypt).
- fix pre-existing collision: bare `npm` keyword on forgejo-admin -> forgejo-npm
  so "npm token" routes to the generic lane (restores test_access expectations).
- tests: rotation parse/coverage/prose-screen/CLI in tests/test_routing.py;
  scorecard count 6 -> 7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 14:40:30 +02:00
custodian-sync
ac09f21ad3 chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Updated by fix-consistency on 2026-07-16:
  - update .custodian-brief.md for ops-warden
2026-07-16 14:26:39 +02:00
custodian-sync
fb4251bab6 chore(consistency): renormalize lifecycle state [auto]
Updated by fix-consistency on 2026-07-16:
  - workplan status: backlog → active
2026-07-16 14:26:36 +02:00
custodian-sync
03ffa27b08 chore(consistency): renormalize lifecycle state [auto]
Updated by fix-consistency on 2026-07-16:
  - workplan status: ready → active
2026-07-16 14:26:35 +02:00
ea98d6bf39 WARDEN-WP-0026 T01: capabilities-safe lane verification + incident note
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
T01 (done): canonical capabilities-based verify pattern in the fleet promotion
checklist (catalog-lane-promotion.md) and applied to the railiance-backup and
forgejo-admin lane playbooks. Verification proves allow/deny via
`bao token capabilities` against the KV v2 data path, never `bao kv get`; a denied
default-policy token-create is a pass, not a privileged-fallback trigger.

T07 (progress): lessons-learned note for the 2026-07-16 CCR-2026-0004 disclosure
(three root causes). Live re-verify + rotation block remain (depend on T06).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 14:26:05 +02:00
2ad8a53781 Add WARDEN-WP-0027: Strand B credential governance/lockdown (backlog)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 28s
Captures the heavyweight governance work deliberately deferred from WP-0026
(Strand A): executable mass rotation, graded lockdown/break-glass with a
designed trust-root, and tamper-evident policy governance + reconcile.
Status backlog with an explicit activation gate — captured, not scheduled;
implemented only when the gate is met and promoted to ready.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 14:22:38 +02:00
167e29de99 chore(consistency): write state-hub IDs into WARDEN-WP-0026 [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 16s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 01:43:19 +02:00
custodian-sync
5615b94649 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-07-16:
  - update .custodian-brief.md for ops-warden
2026-07-16 01:42:29 +02:00
7e0789ab0d WARDEN-WP-0026: credential disclosure hygiene + rotation guidance (Strand A)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 3s
Follow-up to the 2026-07-16 CCR-2026-0004 verify disclosure incident. Strand A:
capabilities-based verification, safe access transport, masking (defense-in-depth),
agent read-boundary, EXPOSED taint convention, and a structured-but-advisory
rotation/re-establishment guidance registry surfaced via warden. Strand B deferred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 01:41:39 +02:00
custodian-sync
a8adb9c2c5 chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
Updated by fix-consistency on 2026-07-13:
  - update .custodian-brief.md for ops-warden
2026-07-13 01:52:13 +02:00
19cd215d0b WARDEN-WP-0025 complete: T05 downstream notify sent, workplan finished
Some checks failed
CI Smoke / host-smoke (push) Successful in 4s
CI Smoke / container-smoke (push) Has been cancelled
Notified the-custodian (949e8ed1) + railiance-platform (5be8e500) /
activity-core (9ed1af98) / railiance-apps (2e47b6e5) that the
forgejo-admin-api-token OpenBao lane is active. No secret values shared.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 01:49:44 +02:00
custodian-sync
ce469c93b3 chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Updated by fix-consistency on 2026-07-13:
  - update .custodian-brief.md for ops-warden
2026-07-13 01:48:42 +02:00
171efa83fe Promote forgejo-admin-api-token lane to active (WARDEN-WP-0025 T04)
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Has been cancelled
PAT attended-minted and stored at platform/workloads/forgejo/forgejo-admin
under field API_TOKEN (re-stored from initial Token field to match
CCR/catalog/playbook fetch_command). Positive fetch verified: PAT valid
against forgejo.coulomb.social (/api/v1/user -> login=tegwick, is_admin=true).

- catalog: draft -> active, resolvable: true, verification evidence, reviewed 2026-07-13
- playbook: header active/resolvable, drop post-promotion caveat
- workplan: T04 done

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 01:47:52 +02:00
custodian-sync
a5f1d2aad7 chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Updated by fix-consistency on 2026-07-12:
  - update .custodian-brief.md for ops-warden
2026-07-12 16:41:59 +02:00
ddeac8bf9c Update WARDEN-WP-0025 after CCR-2026-0006 metadata apply
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Mark T03 done, T04 in progress; document forgejo-admin-pat-provision.sh
in the worker playbook.
2026-07-12 16:07:32 +02:00
custodian-sync
caaa40ce7e chore(consistency): sync task status from DB [auto]
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Updated by fix-consistency on 2026-07-12:
  - update .custodian-brief.md for ops-warden
2026-07-12 16:02:42 +02:00
fd231fac0d Add forgejo-admin-api-token catalog lane (CCR-2026-0006)
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 18s
Draft routing entry and worker playbook for Forgejo site-admin PAT custody
in OpenBao. Workplan WARDEN-WP-0025 tracks approval, apply, and verification.
2026-07-12 16:01:53 +02:00
48b21ab85c CUST-WP-0055 T07: add archive workplan terminology grandfather note
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 8s
2026-07-08 20:26:36 +02:00
8f3ce50e7b docs: workplan-first agent guidance prose (CUST-WP-0055 T04 batch 2)
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
2026-07-08 16:41:15 +02:00
07b564309e Regenerate agent instructions from state-hub templates (CUST-WP-0055 T01)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Sync AGENTS.md, CLAUDE.md, and .claude/rules from updated project_rules
templates: workplan-first session protocol, legacy terminology footnote,
and GET /workplans/ examples.
2026-07-08 14:50:32 +02:00
7906a731fc Adhoc finished
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
2026-07-08 14:47:36 +02:00
7fbfae0fe1 Add Forgejo CI smoke workflow (enablement template)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
2026-07-08 12:35:30 +02:00
4b0f771cbd Link reuse-surface playbook to rotation runbook (T04)
Point lifecycle and rotation procedures at railiance-platform docs for
CCR-2026-0005.
2026-07-08 00:01:22 +02:00
1e6c4eedf1 Migrate reuse-surface hub token lane to OpenBao handoff
RAILIANCE-WP-0011-T03: point reuse-surface-hub-write-token catalog,
playbook, and tests at bao kv get on platform/workloads/reuse/reuse-surface/runtime-secrets;
kubectl documented as break-glass only.
2026-07-07 22:38:45 +02:00
3ddccaf701 Document reuse-surface webhook secret in hub token playbook
Note the sibling REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET key, Forgejo webhook
rollout command, and the RAILIANCE-WP-0011 OpenBao migration backlog item.
2026-07-07 21:28:46 +02:00
d12fdb6112 Add draft catalog lane for railiance backup offsite credentials.
Points at CCR-2026-0004 OpenBao path; playbook documents bao login and
fetch shapes for railiance-backup and forgejo-backup tooling.
2026-07-07 17:16:30 +02:00
07c5cf18ea Release v0.1.2.
Bump version; sync uv.lock; README upgrade examples point at v0.1.2.
2026-07-07 16:59:22 +02:00
4976002f9c Document Forgejo clone, install, and upgrade steps in README.
Canonical source is forgejo.coulomb.social; includes SSH remote config,
make install-all, release upgrade path, and stale-wheel recovery.
2026-07-07 16:57:39 +02:00
162 changed files with 18698 additions and 755 deletions

View file

@ -1,7 +1,59 @@
## Architecture
<!-- TODO: Describe the key design decisions and component structure.
Key modules, data flows, external integrations, state machines, etc. -->
### Our rules are ADRs — `docs/adr/`
The decisions that govern this repo live in `docs/adr/` as addressable records,
not in wiki prose. Read `docs/adr/README.md` first; it explains the one
distinction that matters here.
| ADR | Rule |
| --- | --- |
| `ADR-0001` | The routing catalog is a pointer layer, never a second copy of an owner's procedure |
| `ADR-0002` | ops-warden is a transparent conduit, never a secret broker |
| `ADR-0003` | Cover gaps, but never silently own them |
| `ADR-0004` | High-risk lanes refuse raw value streaming to agent sessions |
| `ADR-0005` | Implement one lane narrowly, route everything else |
| `ADR-0006` | Enforcement is zone-scoped, never a global flag |
| `ADR-0007` | Build-stage permissiveness stops at credential disclosure; every lane carries an explicit `risk` grade |
| `ADR-0008` | A lane's risk grade covers every field its path discloses, not just the field it is named after |
| `ADR-0009` | Adopt security-zones v0.1 as a consumer; membership is compiled, never inferred |
| `ADR-0010` | ops-warden is Staff: it owns access lanes, never access rules; doctrine belongs to gate-house |
### Owned versus inherited — check `owner:` before changing anything
Every ADR carries `owner:` in its frontmatter, and it decides what you are allowed
to do with the rule:
- **`owner: ops-warden`** — ours. We are bound by it *and* we may change it. Changing
one means writing a superseding ADR, not editing the decision in place.
- **any other owner** — inherited. We follow it; we do not own it. Dispute it through
that owner's process; never amend it here.
Everything in `docs/adr/` today is `owner: ops-warden`. Rules we merely follow —
NetKingdom canon, the IAM profile, the credential-management standard — are cited,
never copied in. Copying them would recreate the second-source-of-truth failure
`ADR-0001` exists to prevent.
**Naming collision, worth knowing.** `ADR-001` (three digits) in
`workplan-convention.md` and `session-protocol.md` is **the-custodian's** ADR
establishing the workplan convention across the whole estate. It is inherited and
not ours to change. Our records are four-digit — `ADR-0001``ADR-0005` — and live
in this repo. When writing, say "the-custodian's ADR-001" if that is what you mean.
### Precedence
If a wiki page, playbook, or `.claude/rules/` file disagrees with an ADR, **the ADR
is right and the other file is a defect** — fix it rather than working around it.
The rule files are agent-facing operational instructions derived from these
decisions; they should cite an ADR rather than restate its reasoning.
### Publication
These ADRs are publishable through `policy-nexus` at `policy.coulomb.social`, which
requires `title`, `status` and `owner`, renders owner in the page header and in the
index, and records source repo, path and revision digest in its manifest. Ownership
survives the repo boundary. `policy-nexus` publishes and never writes back: the file
here is the source of truth.
## Quick Reference

View file

@ -4,62 +4,82 @@
for inference. Run this check **before** requesting secrets, API keys, SSH access,
login tokens, or database passwords — in any repo, not only `ops-warden`.
ops-warden **issues SSH certificates** (`warden sign`, `cert_command`) **and is the
operator access front door** for every other credential need. For `exec_capable` lanes
(OpenBao reads, key-cape login) `warden access <need> --fetch/--exec` **proxies the fetch
as you** — it runs the owner's tool with your identity and streams the value to you;
ops-warden holds, caches, and logs nothing. For non-exec lanes it points you at the owner.
**Do not** `POST /messages/` to `ops-warden` expecting a secret *value* — a State Hub
reply is always a pointer. The **value comes from the CLI front door** (`warden access`),
run with **your** identity, never from the inbox.
ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Every
other credential need belongs to another subsystem. **Do not** message
`ops-warden` on State Hub expecting a secret value; the reply is a pointer, not a key.
### Lookup (do this first)
**Always plan before drafting any founder credential step** (WARDEN-WP-0029):
```bash
warden route find "<describe your need>" --json # who owns it (pointer)
warden access "<describe your need>" --json # how to get it (handoff)
warden plan "<describe your need>" --json
# verdict: autonomous | founder_required | unroutable
```
`warden access` is the operator front door (WARDEN-WP-0014): it renders the owner,
auth method, path template, command skeleton, and policy-gate status for any need.
For `exec_capable` lanes it can **proxy the fetch as you** (`--fetch`/`--exec`) — it
runs the owner's tool with **your** identity and streams the value to you; ops-warden
never holds, caches, or logs the value. See `wiki/OperatorAccessAssist.md`.
```bash
warden route find "<describe your need>" --json
warden route show <catalog-id> --json
```
Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run warden`).
If a known lane is missing, reinstall from checkout (stale bundled catalog).
| Agent runtime | How to orient |
| --- | --- |
| **Codex / Grok** (shell, HTTP State Hub) | `warden route` commands above; inbox `to_agent=ops-warden` is for coordination, not secret vending |
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workstreams; **still** use `warden route` for credential ownership |
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden route` |
| **Codex / Grok** (shell, HTTP State Hub) | `warden plan` first; inbox `to_agent=ops-warden` is for coordination, not secret vending |
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workplans; **still** use `warden plan` / `warden route` for credential ownership |
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody via `warden plan` |
### Quick routing table
| I need… | Owner | ops-warden role |
| I need… | Owner | ops-warden executes? |
| --- | --- | --- |
| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Issue**`warden sign` |
| Provisioned secret-exec lane (e.g. npm publish) | **secrets-engine** | **Route** — primary is `secrets-engine exec --catalog <id> -- <cmd>`; `warden access <id> --exec` is the transparent fallback |
| Generic API key / DB password / provider token | OpenBao (`railiance-platform`) | **Assist**`warden access <need> --fetch/--exec` proxies as you; OpenBao keeps custody |
| Login / OIDC / MFA | key-cape / Keycloak | **Assist**`warden access <need> --fetch` runs the login as you |
| Authorization decision | flex-auth | Route only |
| activity-core → issue-core emission | activity-core + issue-core | Route — `warden route show activity-core-issue-sink` |
| SSH tunnel | ops-bridge (+ `cert_command` from warden) | Route only |
For an owned lane, `warden route find <need> --json` / `warden access <id>` surface
`exec_owner`, the `secrets-engine exec` command, and the `resolvable` flag. Run the
secrets-engine command; ops-warden routes to it and requests/holds no token.
| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Yes**`warden sign` |
| API key, DB password, provider token | OpenBao (`railiance-platform`) | No — route only |
| Login / OIDC / MFA | key-cape / Keycloak | No — route only |
| Authorization decision | flex-auth | No — route only |
| activity-core → issue-core emission | activity-core + issue-core | No — `warden route show activity-core-issue-sink` |
| SSH tunnel | ops-bridge (+ `cert_command` from warden) | No — route only |
### Anti-patterns (do not do these)
- Drafting founder steps ("paste PAT to `/tmp`", "click admin UI") without `warden plan`
- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc.
- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist
- Pasting secrets into Git, State Hub, workplans, logs, or chat
- Treating `warden access --fetch` as a *secret store*. It is a transparent conduit
using **your** identity — it holds nothing. ops-warden as a **standing broker**
(its own secret-read token, a cache of fetched values) is forbidden; runtime secret
custody stays in OpenBao, authorization in flex-auth.
- Steady-state **file drops** of credentials under `/tmp` (use `warden desk` paste-once
or `warden access --out/--exec/--wrap`)
- **Reading a secret value onto a captured stdout.** `bao kv get <path>` (full table)
or `bao kv get -field=X` piped/redirected/run in an agent session dumps the value
into a logged context — the 2026-07-16 disclosure. To *verify* a lane use
`bao token capabilities` (allow/deny), never a read (WP-0026 T01).
### Safe fetch transports (WP-0026 T02)
When a value must actually move, use a sanctioned transport that keeps it off
stdout. `warden access <need> --fetch` refuses to stream a value to a non-terminal
stdout unless you pass `--unsafe-stdout` (interactive human sessions only):
| Transport | Command | Result |
| --- | --- | --- |
| **File** | `warden access <need> --out FILE` | value written to a mode-0600 file, never shown |
| **Env (exec)** | `warden access <need> --exec -- <cmd>` | value injected into the child process env only |
| **Wrapping token** | `warden access <need> --wrap` | a single-use, short-TTL OpenBao wrapping token to `bao unwrap` in your own context |
### Agent read-boundary on high-risk lanes (WP-0026 T04)
When `WARDEN_AGENT_ID` is set and the catalog lane is `risk: high`, raw value
streaming is refused (exit 7). Use `--out` / `--exec` / `--wrap` / `--fingerprint`
only. OpenBao policy `agent-high-risk-boundary` denies data-read on those paths
for agent tokens (metadata/capabilities only). See
`wiki/playbooks/agent-read-boundary.md`.
### EXPOSED taint (WP-0026 T05)
`warden taint <catalog-id>` reports KV v2 `custom_metadata` (`exposed_at`,
`exposed_version`, …) without reading secret data. Convention:
`wiki/playbooks/exposed-taint.md`.
### Other capabilities (reuse-surface)

View file

@ -0,0 +1,104 @@
# Finding routing (risk-nexus) and policy publication (policy-nexus)
Two estate repos exist that did not when most of ops-warden's practices were
written. Both are owned by `the-custodian` and both are **downstream by
construction** — the same rule ops-warden's own catalog lives under.
| Repo | Owns | ops-warden's relationship |
| --- | --- | --- |
| `risk-nexus` | Findings, severity, disclosure timing, escalation, regulatory intake. Serves `risk.coulomb.social` | **Route findings to it.** It does not fix; ops-warden fixes what ops-warden owns |
| `policy-nexus` | Publication of canon and ADRs at permanent addresses. Serves `policy.coulomb.social` | Source repo. It publishes; it never writes back |
## When a session discovers or receives a defect, route it
**A design question is not a finding. A defect is.** The distinction matters
because ops-warden receives both through the same channel — the State Hub inbox.
This was gotten wrong on 2026-08-17. `flex-auth` reported directly to ops-warden
that `/v1/check` authenticates no caller — a live authorization bypass in the
service ops-warden's own pre-sign gate consults. ops-warden answered the design
question well and wrote the recommendation into
`wiki/NetKingdomSecurityMap.md`. It did not route the finding. `rapp-postgres`
filed it as `RISK-F-0001`, which is why that record reads
`reported_via: rapp-postgres` and not `ops-warden`, despite ops-warden being a
first-hand recipient and the affected PEP.
The failure mode is exactly the one `risk-nexus/INTENT.md` names: *"findings
landed in whichever document was open."* A wiki section answers the question; it
does not carry a severity, an owner, a date, or a review that fires when nobody
looks.
**So: when an inbound message or a session turns up a defect — in any repo —
answer it *and* route it.** They are not alternatives.
### How to route
Write the finding file into `~/risk-nexus/findings/` following
`RISK-F-0001`/`RISK-F-0002`, and commit it there. This is the established
pattern: a repo routes a finding by writing the record.
Leave `severity`, `disclosure`, and `escalation` **unset**. They are
`risk-nexus`'s to set, not the reporter's. The reporter says what is true; that
repo says how bad it is and who hears about it. State exposure only as far as
you can support it — do not infer a mitigating control (a NetworkPolicy, a
deployment flag) on a system you do not own; say it should be verified.
### What does not go there
`risk-nexus/INTENT.md` is explicit that a register nobody can read is worse than
none: *"if a finding would not change anyone's decision, it is a note, not a
risk."*
The **delegation register is not a findings feed.** `warden route gaps` lists
interim lanes with an intended owner, a blocker and a review date — that is
already tracked, already legible, and already ops-warden's. Do not bulk-file it.
What goes to `risk-nexus` from ops-warden is a defect or an exposure, not a
known gap that is being worked under a workplan.
Also re-read a *blocker* before trusting it. A blocker is a claim about the
world at a date; `RISK-F-0001` invalidated one of ops-warden's in a day and
nothing would have re-checked it.
## Escalation: ops-warden already solved this shape
`risk-nexus` carries an unwritten escalation duty — deciding what reaches the
operator personally rather than sitting in a register — and says the rule
*"must be written down rather than exercised by instinct"*.
ops-warden shipped that classifier for the credential domain in WP-0029.
`warden plan "<need>"` returns `autonomous` / `founder_required` / `unroutable`,
and when it escalates it returns a **typed act** (`approve`, `login`,
`provision`) plus the `reasons` that produced the verdict, with `warden desk` as
the surface that actually executes the act. The transferable design properties:
- escalation is decided by **properties of the thing** (lane type, status,
request signals), not by the assessor's judgement in the moment
- every verdict carries its `reasons`, so the rule is auditable after the fact
- there is a typed act, so "needs the operator" says *what the operator does*
- there is a real surface for the act, so escalation is not just a flag
Offer this rather than let a second, incompatible escalation vocabulary grow.
Do not implement it for them — routing work is theirs to own.
## Policy publication (closed 2026-08-18)
This section previously recorded that ops-warden had no ADRs and that its binding
rules — the no-double-source catalog rule, conduit-not-broker, interim-by-default,
the agent read-boundary — sat in wiki prose, unaddressable and unpublishable.
**That is now resolved.** They live in `docs/adr/` as `ADR-0001``ADR-0005`, each
carrying `owner: ops-warden`, and each verified to render through `policy-nexus`'s
own `tools/render.py`. See `.claude/rules/architecture.md` for the owned-versus-
inherited rule and the three-digit/four-digit ADR naming collision.
What matters when routing something to `policy-nexus`: it requires `title`,
`status` and `owner` on every published document (`tools/build_site.py:179`),
renders owner in both the page eyebrow and the index Owner column, and records
source repo, path, revision and content digest in its manifest. Ownership survives
publication — a reader landing on the URL can tell the rule is ours.
`policy-nexus` publishes and never writes back. The file in `docs/adr/` is the
source of truth; if the site disagrees, the site is the defect.
**When you record a new binding rule, write the ADR.** Not a wiki section — that is
the habit this whole rule file exists to correct, in the other direction.

View file

@ -1,6 +1,6 @@
## First Session Protocol
Triggered when `get_domain_summary("infotech")` shows **no workstreams**.
Triggered when `get_domain_summary("infotech")` shows **no workplans**.
The project is registered but work has not yet been structured.
**Step 1 — Read, don't write**
@ -11,27 +11,31 @@ The project is registered but work has not yet been structured.
**Step 2 — Survey in-progress work**
Look for TODOs, open branches, half-finished files. Note done vs. started but incomplete.
**Step 3 — Propose workstreams to Bernd**
Propose 13 workstreams — each a coherent strand, weeks to months, anchored to a
**Step 3 — Propose workplans to Bernd**
Propose 13 workplans — each a coherent strand, weeks to months, anchored to a
roadmap phase. **Wait for approval before creating.**
**Step 4 — Create workplan file first, then DB record (ADR-001)**
**Step 4 — Write the workplan file; fix-consistency registers it (ADR-001)**
```
workplans/WARDEN-WP-NNNN-<slug>.md ← write this first
workplans/WARDEN-WP-NNNN-<slug>.md ← write this, commit it
```
Then register in the hub:
```
create_workstream(topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a", title="...", owner="...", description="...")
create_task(workstream_id="<id>", title="...", priority="high|medium|low")
Then register by running the consistency check — do **not** call
`create_workplan`/`create_task` yourself; manual registration duplicates what
C-06 creates from the file:
```bash
statehub fix-consistency --repo ops-warden
```
C-06 creates the hub workplan + tasks and writes `state_hub_workstream_id`
(legacy frontmatter name — holds the workplan UUID) and `state_hub_task_id`
back into the file.
**Step 5 — Record the setup**
```
add_progress_event(
summary="First session: structured infotech into N workstreams, M tasks",
summary="First session: structured infotech into N workplans, M tasks",
event_type="milestone",
topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a",
detail={"workstreams": [...], "tasks_created": M}
detail={"workplans": [...], "tasks_created": M}
)
```

View file

@ -44,7 +44,7 @@ For each file with `status: ready`, `active`, or `blocked`, note pending
**Step 4 — Present brief**
1. **Active workstreams** for `infotech` — title, task counts, blocking decisions
1. **Active workplans** for `infotech` — title, task counts, blocking decisions
2. **Pending tasks** from `workplans/` + any `[repo:ops-warden]` hub tasks
3. **Goal guidance** — if `goal_guidance` in summary:
- `needs_workplan`: surface as top action — *"Repo goal '{title}' has no workplan yet"*
@ -52,33 +52,40 @@ For each file with `status: ready`, `active`, or `blocked`, note pending
4. **Suggested next action** — highest-priority open item
5. **SBOM status** — flag if `last_sbom_at` is unset for this repo
If no workstreams: follow First Session Protocol (`first-session.md`).
If no workplans: follow First Session Protocol (`first-session.md`).
**During work:** `record_decision()` · `add_progress_event()` · `resolve_decision()`
> State Hub is a *read model*. Bootstrap tools (`create_workstream`, `create_task`)
> are First Session Protocol only. Work structure belongs in repo files (ADR-001).
> State Hub is a *read model*. **Never register workplans or tasks by hand**
> (`create_workplan`, `create_task`) — write the workplan file in `workplans/`
> and run `fix-consistency`; C-06 registers the workplan and tasks and writes
> IDs back into the file. Manual registration creates duplicates when
> fix-consistency runs. Work structure belongs in repo files (ADR-001).
>
> Legacy: `create_workstream` and `/workstreams/` remain as metered aliases —
> see `workplan-convention.md` (compatibility footnote).
**Session close:**
With MCP tools:
```
add_progress_event(summary="...", topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a", workstream_id="<uuid>")
add_progress_event(summary="...", topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a", workplan_id="<uuid>")
```
Without MCP tools:
```bash
curl -s -X POST http://127.0.0.1:8000/progress/ \
-H "Content-Type: application/json" \
-d '{"topic_id":"cee7bedf-2b48-46ef-8601-006474f2ad7a","workstream_id":"<uuid>","event_type":"note","summary":"what changed","author":"codex"}'
-d '{"topic_id":"cee7bedf-2b48-46ef-8601-006474f2ad7a","workplan_id":"<uuid>","event_type":"note","summary":"what changed","author":"codex"}'
```
If workplan files were modified, ensure the local copy is up to date first:
If workplan files were modified, ensure the local copy is up to date first,
then sync from the repo checkout:
```bash
git -C <repo_path> pull --ff-only
cd ~/state-hub && make fix-consistency REPO=ops-warden
git pull --ff-only
statehub fix-consistency
```
For repos where implementation runs on a remote machine (e.g. CoulombCore),
use the combined target which pulls before fixing:
For repos where implementation runs on a remote machine (e.g. railiance01),
use the pull-before-fix mode from any shell with the State Hub CLI:
```bash
cd ~/state-hub && make fix-consistency-remote REPO=ops-warden
statehub fix-consistency --repo ops-warden --remote
```
**C-15** (DB task ahead of file) is normal in multi-machine workflows — writeback
will sync the file to match DB. **C-16** (repo behind remote) blocks all writes

View file

@ -5,7 +5,7 @@ ID prefix: `WARDEN-WP-`
Work items originate as files in this repo **before** being registered in the hub.
Canonical workplan/workstream frontmatter statuses are:
Canonical workplan frontmatter statuses are:
`proposed`, `ready`, `active`, `blocked`, `backlog`, `finished`, `archived`.
Use `proposed` for a newly drafted plan, `ready` after review against current
repo state, and `finished` when implementation is complete. `stalled` and
@ -16,14 +16,15 @@ prefix: `YYMMDD-WARDEN-WP-NNNN-<slug>.md`. The frontmatter id remains
unchanged; the prefix is only for quick visual reference.
Small opportunistic tasks discovered during another session use **Ad Hoc Tasks**:
`workplans/ADHOC-YYYY-MM-DD.md`, workstream slug `adhoc-YYYY-MM-DD`, and task ids
`workplans/ADHOC-YYYY-MM-DD.md`, workplan slug `adhoc-YYYY-MM-DD`, and task ids
`ADHOC-YYYY-MM-DD-T01`, `T02`, etc. Use adhocs only for low-risk work completed
directly. Promote anything requiring analysis, design, approval, dependencies, or
multiple planned phases into a normal workplan.
Ecosystem todos from other agents arrive as `[repo:ops-warden]` hub tasks —
visible at session start. Pick one up by creating the workplan file, then registering
the workstream.
visible at session start. Pick one up by creating the workplan file, committing,
and running `statehub fix-consistency` — C-06 registers the workplan in the hub.
Never register by hand with `create_workplan` (legacy MCP alias: `create_workstream`).
Task blocks use this shape:
@ -37,4 +38,18 @@ state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
Status progression is `todo``progress``done`; use `wait` for waiting or
blocked work and `cancel` for stopped work.
Workplan frontmatter carries `state_hub_workstream_id` — a legacy field name
kept for compatibility; it holds the hub workplan UUID and is written by
fix-consistency. Do not edit or rename it.
### Legacy terminology (compatibility footnote)
**Workplan** is the fleet term — see
`the-custodian/canon/standards/workplan-terminology-fleet_v0.1.md`.
**Workplan** is legacy only: some API routes (`/workstreams/`), params
(`workstream_id`), MCP aliases (`create_workstream`), and the frontmatter field
above remain until `STATE-WP-0069` retires them via legacy-meter. Treat those
identifiers as workplan IDs. Prefer `GET /workplans/` and `workplan_id` in new
examples and scripts.
<!-- Ralph Loop rules and HEUREKA sequence: ~/.claude/CLAUDE.md — do not duplicate here -->

View file

@ -2,12 +2,31 @@
# Custodian Brief — ops-warden
**Domain:** infotech
**Last synced:** 2026-07-01 21:35 UTC
**Last synced:** 2026-08-31 22:49 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
*(none — repo may need first-session setup)*
### Layer model v0.7 conformance — state the deadline, bind the agent boundary, steward the estate's newest rule
Progress: 0/5 done | workplan_id: `ae3ff76f-883d-5e2f-b6aa-144d61e8fdef`
**Open tasks:**
- · Tasks `8b3bdb9f`
- · Tasks `3318ee1a`
- · Tasks `a891b32c`
- · Tasks `94e73daa`
- · Tasks `7d1b3c82`
### Tamper-resistant credential governance + mass rotation/lockdown (Strand B)
Progress: 2/3 done | workplan_id: `21528e8d-a049-523d-9ae1-da7a27cb8bbf`
**Open tasks:**
- ► Task: Graded lockdown / break-glass with explicit trust-root `cae498ee`
## Inbox Hygiene
**Stale unread:** 1 message(s) older than 3 day(s) — triage at session start.
**Missing thread_id:** 6 unread message(s) lack supersession chains.
---
## MCP Orientation (when available)

View file

@ -0,0 +1,29 @@
# Canonical CI smoke template (tier 1 routing drill).
# Copy to: .forgejo/workflows/ci-smoke.yaml in consumer repos.
name: CI Smoke
on:
push:
branches:
- main
workflow_dispatch:
jobs:
host-smoke:
runs-on: self-hosted
steps:
- name: Routing probe (host runner)
run: |
set -eu
echo "repository=${GITHUB_REPOSITORY:-unknown}"
echo "sha=${GITHUB_SHA:-unknown}"
echo "runner=${RUNNER_NAME:-unknown}"
uname -a
container-smoke:
runs-on: ubuntu-latest
steps:
- name: Routing probe (container label)
run: |
set -eu
echo "container-smoke ok for ${GITHUB_REPOSITORY:-unknown}"

2360
.repo-manager/index.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -19,7 +19,13 @@ there is no MCP server for Codex agents.
| Context | URL |
|---------|-----|
| Local workstation | `http://127.0.0.1:8000` |
| Remote via tunnel | `http://127.0.0.1:18000` |
| Remote (railiance01, in-cluster) | `http://10.43.68.154:8000` |
| Optional local edge relay | http://127.0.0.1:18080 |
When an operator has enabled the edge relay, set API_BASE to the relay URL.
Queueable writes return an explicit queued receipt if the central hub is
unreachable. Treat that as pending local evidence, then ask the operator to run
statehub outbox status/replay after connectivity returns.
### Orient at session start
@ -27,8 +33,8 @@ there is no MCP server for Codex agents.
# Offline brief — works without hub connection
cat .custodian-brief.md
# Active workstreams for this domain
curl -s "http://127.0.0.1:8000/workstreams/?topic_id=cee7bedf-2b48-46ef-8601-006474f2ad7a&status=active" \
# Active workplans for this domain
curl -s "http://127.0.0.1:8000/workplans/?topic_id=cee7bedf-2b48-46ef-8601-006474f2ad7a&status=active" \
| python3 -m json.tool
# Check inbox
@ -51,12 +57,12 @@ curl -s -X POST http://127.0.0.1:8000/progress/ \
"summary": "what was done",
"event_type": "note",
"author": "codex",
"workstream_id": "<uuid>",
"workplan_id": "<uuid>",
"task_id": "<uuid>"
}'
```
Omit `workstream_id` / `task_id` when not applicable.
Omit `workplan_id` / `task_id` when not applicable.
### Update task status
@ -80,7 +86,7 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
## Session Protocol
**Start:**
1. `cat .custodian-brief.md` — domain goal and open workstreams (offline-safe)
1. `cat .custodian-brief.md` — domain goal and open workplans (offline-safe)
2. Check inbox: `GET /messages/?to_agent=ops-warden&unread_only=true`; mark read
3. Scan workplans: `ls workplans/` — note `status: ready`, `active`, or `blocked` files and open tasks
4. Check human-needed tasks: `GET /tasks/?needs_human=true`
@ -92,12 +98,12 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
**Close:**
1. Update workplan file task statuses to reflect progress
2. Log: `POST /progress/` with a summary of what changed
3. Note for the custodian operator: after workplan file changes, run from
`~/state-hub`:
3. After workplan file changes, run:
```bash
make fix-consistency REPO=ops-warden
statehub fix-consistency
```
This syncs task status from files into the hub DB.
Coding agents should run this directly; ask the operator only if the CLI or
State Hub API is unavailable. This syncs task status from files into the hub DB.
---
@ -113,18 +119,33 @@ other credential need belongs to another subsystem. **Do not** message
### Lookup (do this first)
**Always plan before drafting any founder credential step** (WARDEN-WP-0029):
```bash
warden plan "<describe your need>" --json
# verdict: autonomous | founder_required | unroutable
# autonomous → run the commands (usually warden access --exec/--out/--wrap)
# founder_required → escalate exactly one act via warden desk (not /tmp file drops)
# unroutable → propose a CCR / catalog lane; do not improvise
```
Supporting lookups:
```bash
warden route find "<describe your need>" --json
warden route show <catalog-id> --json
warden route list # human output includes catalog source/hash freshness
```
Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run warden`).
If `warden plan` misses a known lane, the installed catalog may be stale (bundled
fallback) — reinstall from checkout and re-run plan.
| Agent runtime | How to orient |
| --- | --- |
| **Codex / Grok** (shell, HTTP State Hub) | `warden route` commands above; inbox `to_agent=ops-warden` is for coordination, not secret vending |
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workstreams; **still** use `warden route` for credential ownership |
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden route` |
| **Codex / Grok** (shell, HTTP State Hub) | `warden plan` first; inbox `to_agent=ops-warden` is for coordination, not secret vending |
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workplans; **still** use `warden plan` / `warden route` for credential ownership |
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden plan` |
### Quick routing table
@ -139,9 +160,22 @@ Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run wa
### Anti-patterns (do not do these)
- Drafting founder steps ("paste PAT to `/tmp`", "click Forgejo admin UI") **without**
`warden plan` first (WP-0029)
- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc.
- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist
- Pasting secrets into Git, State Hub, workplans, logs, or chat
- **Reading a secret value onto a captured stdout.** Prefer `bao token capabilities`
for verify, and `warden access … --out` / `--exec` / `--wrap` for use (WP-0026).
- Steady-state credential **file drops** (`/tmp/…-token`); use desk paste-once or
sanctioned transports instead
### Agent read-boundary + EXPOSED taint (WP-0026 T04/T05)
- High-risk lanes (`risk: high` in catalog): with `WARDEN_AGENT_ID` set, raw value
streaming is refused. Use sanctioned transports only.
- `warden taint <id>` reports EXPOSED metadata without reading secret data.
- Playbooks: `wiki/playbooks/agent-read-boundary.md`, `wiki/playbooks/exposed-taint.md`.
### Other capabilities (reuse-surface)

View file

@ -9,4 +9,5 @@
@.claude/rules/architecture.md
@.claude/rules/repo-boundary.md
@.claude/rules/credential-routing.md
@.claude/rules/finding-routing.md
@.claude/rules/agents.md

180
INTENT.md
View file

@ -1,5 +1,68 @@
---
layer: Staff
role: null # Engines only: PDP | PIP | Evidence | Lifecycle
pep_shaped: true # §6.4 — issuing a certificate is a protected side effect
standard: net-kingdom/canon/standards/security-layer-model_v0.7.md
standard_version: "0.7"
companion: net-kingdom/SECURITY-COMPANION.md
declaration: layer.yaml
pep_stance: pep-stance.yaml
assent: docs/adr/ADR-0010
---
# INTENT
> **ops-warden is Staff, and PEP-shaped.** Declared here in its own voice, per
> `security-layer-model_v0.7` §11 — a layer stated *about* a repository by another
> repository is not a declaration. The standard is **accepted**; its operative form
> is `net-kingdom/SECURITY-COMPANION.md`. ops-warden's assent is `ADR-0010`, and its
> reviews of v0.4, v0.6 and v0.7 are in `history/`.
>
> **Staff** because ops-warden's core function is judgement, not computation: it
> decides which lane a need belongs to and stewards the paths through the estate's
> rules. Its artifacts are runbooks, routing decisions, workplans. **PEP-shaped**
> because issuing a certificate is a protected side effect — a shape, not a layer
> (§6.4). ops-warden renders no authorization decision and never will; it consumes
> them from `access-engine`.
>
> **The estate's front door for paths.** The companion routes the whole estate here:
> *"For how to get something done — which lane, which credential, which route — ask
> `ops-warden`. This document says what the rules are; ops-warden stewards the paths
> through them."* That is a standing obligation, not a compliment: every rule
> gate-house writes needs a path someone can actually walk, and ops-warden owes the
> estate that path.
>
> **The declarations are files, not this note** — prose cannot distinguish a
> declaration from a transcribed review (§11):
>
> | File | Declares | Enforced by |
> | --- | --- | --- |
> | `layer.yaml` | every Tooling contact, mapped to a §5.1/§5.2/§5.3 shape, plus the non-Tooling clients so the check is total | `scripts/check_layer_conformance.py`, `tests/test_layer_conformance.py` |
> | `pep-stance.yaml` | the unreachable-engine stance map (§6.4 obl. 3), total per zone | test asserts the published map **equals** the shipped default |
> | `tenancy.yaml` | tenancy posture, and `z1-operational` zone membership | `ADR-0009` |
>
> Both are cited in the standard as the estate's reference forms (§11, §6.4, §13.1).
>
> **Two declared engine gaps (§5.3), tracked non-conformance and not conformance.**
> `VaultCA` signs over a direct OpenBao client and `warden desk` shells `bao kv put`.
> Intended owner **secrets-engine**; blocked on no engine exposing an SSH-CA or
> attended-provisioning surface; reviewed quarterly; registered in statute §13.
> ops-warden keeps signing while the gap is open — refusing would remove production
> host access to close a documentation gap.
>
> **The agent principal (§3.4).** ops-warden is operated by agents as well as people,
> and they share the layer but not the blast radius. No standing credential; tool use
> is a conduit or an engine API and there is no third route; **tool availability is
> not permission**, which is exactly what `ADR-0004`'s read-boundary enforces; agent
> memory is not a state plane. Session semantics belong to `glas-harness`, not here.
>
> **Evidence (§9.6).** ops-warden's audit trail is **attributive**, not load-bearing:
> no control branches on the presence of a signing record. Emission is deliberately
> non-atomic so an audit-store failure cannot remove production host access — a trade
> the standard sanctions, declared in `wiki/AuditTrail.md`, registered in §13. If any
> future control ever gates on this trail, that trade must be revisited before it
> ships.
> This file captures **why this repository exists**, the **direction it is
> moving toward**, and the **kind of system it is meant to become**.
> It is intentionally **aspirational and stable**, not a description of
@ -77,28 +140,52 @@ owns one lane and points at the rest:
---
## NetKingdom Security Literacy
## Lane routing — who owns which need
ops-warden should be fluent in the platform architecture documented in
`net-kingdom` — especially:
**This is a runbook, not doctrine.** Security doctrine, the authority model, and the
security curriculum are **gate-house's** (`security-layer-model_v0.7` §8).
ops-warden references them and does not restate them. What follows is lane
stewardship: which subsystem owns which need, and what ops-warden does about it.
| Plane / component | Role in access | ops-warden relationship |
| --- | --- | --- |
| **key-cape / Keycloak** | Identity — who is the actor, MFA, IAM Profile claims | Instruct identity path; do not re-implement OIDC |
| **flex-auth + Topaz** | Authorization — may this actor perform this action | Caller-side policy gate shipped (opt-in); production flip is flex-auth's |
| **OpenBao** | Runtime secrets — API keys, dynamic creds, leases, audit | Instruct custody paths; SSH engine is signing backend only; proxy reads as caller when `exec_capable` |
| **secrets-engine** | Owner-native secret-exec (`secrets-engine exec`) | Route provisioned exec lanes (e.g. npm publish); ops-warden does not hold tokens |
| **railiance-platform** (credential broker) | Scoped lease grants (`credential exec`) | Route `warden-sign` token needs; ops-warden does not mint OpenBao tokens |
| **ops-warden** | Operational SSH certificates — short-lived host access | **Own and issue** this lane |
| **ops-bridge** | Tunnel transport — consumes certs via `cert_command` | Primary consumer; document integration |
| **railiance-infra** | Host principals, force-command, SSH hardening | Instruct host-side deployment; do not own Ansible |
| **railiance-platform** (deploy) | OpenBao/K8s/platform service deployment | Instruct production endpoints; do not deploy clusters |
The machine-readable form is `registry/routing/catalog.yaml`, and the executable form
is `warden plan "<need>"` / `warden route find`. Prefer either over this table — it is
orientation, and the catalog is the source of truth (`ADR-0001`).
| Component | Layer | Owns | ops-warden relationship |
| --- | --- | --- | --- |
| **gate-house** | Staff | Security doctrine, invariants, authority ceilings, authority context, conformance review, curriculum | **Route doctrine and authority-model questions here.** Not policy decisions — those go to access-engine |
| **access-engine** (`flex-auth`) | Engine | **The policy decision** — whether an actor may act. The only decision point in NetKingdom | Consume decisions; caller-side pre-sign gate. ops-warden never renders or caches one |
| **key-cape / Keycloak** | Tooling | Identity — who the actor is, MFA, IAM Profile claims | Instruct the identity path; do not re-implement OIDC |
| **OpenBao** | Tooling | Runtime secrets — API keys, dynamic creds, leases, audit | Instruct custody paths; proxy reads as the caller when `exec_capable`. Direct client use is the declared exception above |
| **secrets-engine** | Engine | Credential abstraction, custody, lifecycle; owner-native exec | Route provisioned exec lanes (e.g. npm publish). **Intended owner of the SSH-CA surface** |
| **tenant-engine** | Engine | Tenant/client secret custody and front door | Route tenant lanes once fronted; current tenant proxies are interim (section 9) |
| **user-engine** | Engine | Users, accounts, memberships | No ops-warden lane today; route rather than absorb |
| **zone-engine** | Engine | Zone identity and membership | Consume compiled membership; ops-warden declares `z1-operational` (`ADR-0009`) |
| **railiance-platform** (broker) | — | Scoped lease grants (`credential exec`) | Route `warden-sign` token needs; ops-warden does not mint OpenBao tokens |
| **ops-mason** | Staff | Building and tearing down access routes and perimeters | Peer lane owner; same lane/rule demarcation applies |
| **ops-warden** | Staff | **Operational access lanes** — short-lived SSH certificates, routing, stewardship, runbooks | **Own and issue** the SSH lane |
| **ops-bridge** | Staff | Tunnel transport — consumes certs via `cert_command` | Primary consumer; document integration |
| **railiance-infra** | — | Host principals, force-command, SSH hardening | Instruct host-side deployment; do not own Ansible |
| **kings-guard** | Staff | Adaptive defence, observation, containment; publishes posture | Posture may reduce authority, never manufacture it |
### Access lane versus access rule
Normative, per `security-layer-model_v0.7` §8 and assented to in `ADR-0010`:
- **access lane** — ops-warden and ops-mason. *How* a worker reaches a host.
- **access rule** — access-engine. *Whether* they may.
ops-warden owns the route and never the decision. A question about whether an actor
may do something is not an ops-warden question, however it arrives.
Canonical references:
- `net-kingdom/SECURITY-COMPANION.md` — the operative form; start here
- `net-kingdom/canon/standards/security-layer-model_v0.7.md` (accepted; §5 shapes, §6.4 PEP, §8 vocabulary)
- `net-kingdom/docs/platform-identity-security-architecture.md`
- `net-kingdom/docs/responsibility-map.md`
- `wiki/AccessManagementDirective.md` (ops SSH actor model)
- `.claude/rules/credential-routing.md` (agent-facing runbook — stays inline by design)
---
@ -121,7 +208,8 @@ Canonical references:
| Need | Route to |
| --- | --- |
| OIDC login, MFA, human identity claims | key-cape / Keycloak (NetKingdom IAM Profile) |
| Policy decision — may actor X access resource Y | flex-auth |
| Security doctrine, invariants, authority model | gate-house |
| Policy decision — may actor X access resource Y | access-engine (`flex-auth`) |
| API keys, provider secrets, DB creds, object-storage STS | OpenBao (+ flex-auth policy where required) |
| Inter-Hub operator keys, LLM provider credentials | OpenBao or approved operator secret store |
| Tunnel lifecycle, port forwarding | ops-bridge |
@ -170,6 +258,62 @@ Every ops-warden action appends metadata-only audit events; `warden activity`
answers *what happened recently* in one command. Compliance checks (scorecard) make
cert-side policy violations visible before they become incidents.
### 7. The founder is escalated to, never tasked with mechanics
*(added 2026-07-18, founder directive — see WARDEN-WP-0029)*
Workers and agents ask **ops-warden** what a credential need requires — never
the founder directly. ops-warden answers three questions, in policy terms:
1. Can this be done autonomously under current policy and posture? → do it /
route it, unattended.
2. Does policy require a founder *decision or identity act* (OIDC login,
Red-lane approval)? → escalate exactly that act, nothing more.
3. Is the need unroutable? → name the missing lane and propose it (CCR),
instead of improvising file drops or UI instructions.
Raw mechanics — "paste this PAT into /tmp", "click through the forgejo admin
UI" — are **anti-patterns**: they leak credentials into CLI history and file
artefacts and burn founder attention on work a lane should do. When founder
interaction *is* required, prefer a purpose-built interaction surface (local
web approval page rendering the exact action) over CLI/file handoffs.
### 8. Posture-aware: the organization is in build phase
Policy answers depend on lifecycle posture. The organization currently runs in
**build phase**: one founder-operator, pre-revenue, velocity prioritized —
pragmatic provisioning (workstation OIDC, per-repo deploy keys, advisory
policy gates) is deliberately acceptable where audit and custody invariants
hold (values only in OpenBao/process env; metadata-only trails). ops-warden
must know the current posture, state it in its answers, and tighten defaults
when the posture graduates (first customer data, first non-founder operator,
production tier). Posture is declared configuration, not tribal knowledge.
### 9. Cover gaps, but never silently own them
*(added 2026-08-11, founder directive — see WARDEN-WP-0030)*
ops-warden **works with, and never replaces or duplicates**, secrets-engine,
tenant-engine, user-engine and the other NetKingdom security components.
It may nonetheless *cover* a need that no component systematically provides yet —
that is a legitimate service, and the `warden access` proxy makes it cheap. The
danger is precisely that cheapness: an absorbed need looks permanent, stops
registering as a missing capability, and quietly turns a routing layer into a
second secrets broker.
So every execution position other than SSH issuance is **interim by default**:
- record the component that *should* own the front door, and what is missing
- treat the cover as a tracked gap, not as ownership
- delegate the moment that component ships its front door, keeping the proxy
only as a fallback (`exec_owner` / `exec_command` — the WP-0019 pattern)
A gap ops-warden covers silently is worse than a gap it refuses, because the
refusal is visible and the cover is not. Filling the gap properly — with the
owner's governance, custody, and policy — is the goal; ops-warden holding the
lane is the temporary means.
---
## Credential flow (target mental model)
@ -236,6 +380,9 @@ ops-warden is succeeding when:
5. Non-SSH secrets remain **out of ops-warden storage** — only documented paths.
6. Security blockers can be classified by environment posture, workload maturity,
owner route, and non-secret evidence instead of by vague credential risk.
7. Every ops-warden execution position is explicitly **permanent** (SSH issuance) or
explicitly **interim** with a named intended owner and blocker — so gaps ops-warden
covers stay visible as gaps and can be handed back.
---
@ -248,6 +395,9 @@ ops-warden is succeeding when:
- Host-side SSH configuration deployment
- **Duplicating or restating another subsystem's procedure** — routing material
points at the owner's docs; it does not fork them
- **Permanently owning a lane that belongs to another component** — covering an
unfilled gap is acceptable and expected; keeping it after the owner can front it,
or holding it without recording that it is interim, is not (§9)
- SSO / Teleport at scale (trigger per Access Management Directive §6.2)
---

161
LICENSE
View file

@ -1,16 +1,151 @@
MIT No Attribution
# Target Revenue Source License
Copyright <YEAR> <COPYRIGHT HOLDER>
**Version 1.0, Candidate 1 (V1C1)**
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so.
---
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
> **PRELIMINARY CANDIDATE — SUBJECT TO CHANGE — NOT FINAL**
>
> This repository is governed by the Target Revenue Source License
> (TRSL), Version 1, Candidate 1. This is the framework's first working
> candidate, adopted as the org's preliminary operating license across
> all repos (maintainer decision, 2026-07-29) during the build/alpha
> stage — see `SCOPE.md` §1 and Appendix A of the canonical text (link
> below) for the alpha/beta risk-acceptance decision this reflects and
> what it does and does not mean. Full specialist legal review is
> explicitly deferred until the framework moves out of beta.
>
> **Canonical source, full candidate-status banner, and Appendix A
> (non-normative candidate notes tracking every open item):** the
> `coulomb` org's `target-revenue` repository,
> `specs/TargetRevenueSourceLicense-V1C1.md` — this file is the operative
> legal text (Preamble through Section 11) only; the canonical document
> is authoritative if this copy and that document ever diverge.
---
## Preamble
This Target Revenue Source License ("**License**") governs the Software identified in the applicable Phase Manifest. It implements the Target Revenue Framework: a defined development Phase accumulates Development Credit and Remission Credit against an immutable Initial Target until the Milestone Release automatically and irrevocably converts to a declared permissive Future License.
Commercial beneficiaries fund the creation and early availability of a software improvement; once the declared target is satisfied, the governed release becomes permissively open source.
## 1. Definitions
Capitalized terms used in this License have the meanings given below. Where a term is also defined in the Phase Manifest or Target Ledger for a specific Phase, the Phase Manifest and Target Ledger govern the *values* (amounts, dates, identifiers) and this License governs the *legal effect* of those values — the two must not be read as conflicting definitions of the same concept.
**"Commercial Entitlement"** means a right, purchased or otherwise granted under a Commercial Use Agreement, to make Commercial Use of the Software during a Phase.
**"Commercial Use"** means billing, invoicing, or otherwise charging any customer a fee, subscription, license fee, or other consideration for or in connection with use of the Software, at any time before the Conversion Event for the applicable Phase, regardless of whether the person or organization billed would otherwise qualify for Noncommercial Use. Commercial Use occurs by virtue of such billing alone, whether or not the resulting payment is registered with the Trust Service; in particular, billing a customer for pre-conversion use of the Software without recording the corresponding payment in the applicable Phase's Target Ledger is Commercial Use without a valid Commercial Entitlement — a violation of Section 3, addressed under Section 7 and, where applicable, the Enforcement Network described in the canonical repository's `specs/EnforcementNetworkConcept.md`.
**"Commercial Use Agreement"** means the separate agreement, referenced by the applicable Phase Manifest, under which a Commercial Entitlement is purchased or granted. This License does not itself set pricing, metering, or payment terms — those are governed by the Commercial Use Agreement.
**"Conversion Event"** means the moment the Outstanding Target for a Phase reaches zero, as computed from the Phase Manifest and Target Ledger per the Target Ledger Specification. The Conversion Event occurs automatically and is not conditioned on any declaration, attestation, or other act by the Licensor or any Trust Service.
**"Development Credit"** means the portion of a collected and settled payment explicitly allocated toward satisfying the Initial Target of a specific Phase, as recorded in that Phase's Target Ledger.
**"Future License"** means the permissive license identified in the applicable Phase Manifest, being either the MIT License or the Apache License, Version 2.0, which applies to the Milestone Release upon the Conversion Event.
**"Initial Target"** means the immutable monetary target declared for a Phase in its Phase Manifest.
**"Licensor"** means **Binky Hedgehog GmbH**, the party that publishes the Phase Manifest and holds the rights necessary to grant this License and the Future License for the Milestone Release.
**"Milestone Release"** means the precisely identified software release designated in the applicable Phase Manifest, identified by an immutable source revision, release artifact, or cryptographic digest.
**"Noncommercial Use"** means use of the Software for personal purposes, private study, hobby or amateur projects; use by any charitable organization, educational institution, public research organization, or government institution acting in a non-revenue-generating capacity; or other use of a materially similar character.
**"Outstanding Target"** means, at any time, `max(0, Initial Target cumulative Development Credit cumulative Remission Credit)` for a Phase, as computed from that Phase's Target Ledger.
**"Phase"** means a bounded development undertaking governed by one Initial Target, one Milestone Release, one degeneration policy, and one Future License declaration, as declared in a Phase Manifest.
**"Phase Manifest"** means the published, immutable declaration identifying a Phase, its Milestone Release, Initial Target, Future License, degeneration policy, and Target Ledger location, as specified in the Phase Manifest Specification.
**"Remission Credit"** means a transparent, non-revenue reduction of a Phase's Outstanding Target, generated under that Phase's published degeneration policy and recorded in the Target Ledger.
**"Settled Payment"** means a payment that has cleared through its payment processor and is no longer subject to reversal in the ordinary course (chargeback, dispute, or equivalent), as further specified by the applicable Commercial Use Agreement or monetization extension.
**"Software"** means the source code, object code, and associated documentation of the Milestone Release identified in the applicable Phase Manifest.
**"Target Ledger"** means the append-only record of Development Credit, Remission Credit, and correction entries for a Phase, as specified in the Target Ledger Specification.
**"You"** or **"Licensee"** means the individual or entity exercising rights under this License.
## 2. Grant of Rights for Noncommercial Use
Subject to the terms of this License, the Licensor grants You a worldwide, royalty-free, non-exclusive license, during the applicable Phase, to:
(a) use, reproduce, and study the Software for any Noncommercial Use;
(b) modify the Software and create derivative works of it for any Noncommercial Use; and
(c) redistribute the Software and Your modifications, in source or object form, for any Noncommercial Use, provided that You include this License, unmodified, with any such redistribution, and that You do not remove or alter any copyright, patent, trademark, or attribution notices contained in the Software.
This grant does not extend to Commercial Use. Commercial Use requires a Commercial Entitlement under Section 3.
## 3. Commercial Use
You may not make Commercial Use of the Software during the applicable Phase unless You hold a valid, current Commercial Entitlement under a Commercial Use Agreement with the Licensor covering the applicable Phase. A Commercial Entitlement granted under one Phase's Commercial Use Agreement does not extend to a later Phase's Milestone Release unless the Commercial Use Agreement expressly says so.
This Section 3 states the existence and boundary of the commercial-use restriction. It does not itself set pricing, invoicing, metering, audit rights, or payment terms — those are governed exclusively by the applicable Commercial Use Agreement.
## 4. Patent License
Subject to the terms of this License, each contributor to the Software grants You, during the applicable Phase and solely to the extent of rights granted under Sections 2 and 3, a perpetual (subject to the termination below), worldwide, non-exclusive, no-charge, royalty-free patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Software, limited to those patent claims licensable by that contributor that are necessarily infringed by their contribution(s) alone or by combination of their contribution(s) with the Software.
If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Software or a contribution incorporated within it constitutes direct or contributory patent infringement, then any patent licenses granted to You under this Section 4 for the Software shall terminate as of the date such litigation is filed.
## 5. Automatic Conversion to the Future License
**5.1 Automatic effect.** Upon the Conversion Event for a Phase, the rights and restrictions in Sections 3 (Commercial Use) of this License, as they apply to that Phase's Milestone Release, terminate automatically. In their place, the Milestone Release is licensed under the Future License identified in that Phase's Phase Manifest, effective as of the Conversion Event, without any further act, declaration, or attestation required by the Licensor, any Trust Service, or any other party.
**5.2 Irrevocability.** Once a valid Conversion Event has occurred for a Phase, no subsequent refund, chargeback, accounting correction, dispute, or termination of this License for an unrelated breach shall revoke, suspend, or otherwise impair the Future License grant for that Phase's Milestone Release. Any shortfall or dispute arising after a Conversion Event is a commercial or accounting matter between the relevant parties and does not reinstate a commercial-use restriction over already-converted Software.
**5.3 Prior freedom preserved.** A later Phase covering subsequent improvements to the Software does not restrict, withdraw, or otherwise affect the rights granted under the Future License for an earlier Phase's already-converted Milestone Release.
**5.4 Evidence, not cause.** A Trust Service may publish a Conversion Attestation documenting a Conversion Event. Such an attestation is evidence that the Conversion Event occurred; it is not a condition of, and its absence or delay does not postpone, the automatic effect described in Section 5.1. Any person may independently verify whether a Conversion Event has occurred directly from the Phase Manifest and Target Ledger.
## 6. Successive Phases
The Licensor may declare a new Phase covering subsequent improvements to the Software following a Milestone Release's Conversion Event. Each Phase is independently governed by its own Phase Manifest, Initial Target, degeneration policy, and Target Ledger. Nothing in a later Phase's Phase Manifest may be construed to reduce or withdraw rights already granted under Section 5 for an earlier Phase's Milestone Release.
## 7. Term and Termination
**7.1 Term.** This License applies to the Software for the duration of the applicable Phase, and, for the Milestone Release, indefinitely following that Phase's Conversion Event under the Future License.
**7.2 Termination for breach.** If You breach Section 3 (Commercial Use) or Section 2(c) (redistribution notice requirement), the Licensor may terminate this License as to You. Before such termination becomes effective, the Licensor shall provide You written notice of the breach; if You cure the breach within thirty (30) days of that notice, this License continues in effect. A second breach of the same provision within twelve (12) months may be terminated immediately without a further cure opportunity.
**7.3 Effect of termination.** Termination under this Section 7 affects only Your rights under Sections 2 and 3 for the Phase in which the breach occurred. It does not affect any rights already vested under Section 5 (Automatic Conversion) for a Milestone Release whose Conversion Event has already occurred, per Section 5.2.
**7.4 Public record of breach and resolution.** The Licensor shall cause the Trust Service to publish, as part of the public record for the affected Phase, notice of: (a) any breach notice issued under Section 7.2, stating the general nature of the breach and the date of notice; (b) whether the breach was cured within the applicable cure period, and the date of cure; and (c) any termination determination made under this Section 7, including its effective date and scope. This public record exists to give the ecosystem a transparent, verifiable conformity signal for the Phase, distinct from and in addition to the Development Credit and Remission Credit facts already published under Section 5.4 and the Target Ledger Specification.
A breach that You dispute, and that has not been finally determined, shall be recorded as **alleged**; it shall be recorded as **determined** only once the cure period has run without cure, or the dispute has been resolved against You under the applicable Commercial Use Agreement's dispute process, if any. The Trust Service shall update the record promptly upon resolution in either direction. Recording an alleged or determined breach under this Section 7.4 is a ministerial act of publishing the Licensor's determination (or a dispute process's outcome); it does not give the Trust Service discretionary authority to decide whether a breach occurred, consistent with Section 5.4's evidence-not-cause principle.
Whether, and under what conditions, the public record identifies a Commercial Entitlement holder by name is governed exclusively by the applicable Commercial Use Agreement, which the Licensor and that Commercial Entitlement holder negotiate and agree to directly. This License does not itself set a naming default. Where no Commercial Use Agreement addresses the question, or where the affected party has no Commercial Use Agreement at all (for example, a Section 2(c) breach by a Noncommercial Use licensee), the public record states the Phase and breach category only, without naming the party.
## 8. Disclaimer of Warranty
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. THE LICENSOR DOES NOT WARRANT THAT THE SOFTWARE WILL BE ERROR-FREE OR THAT ANY PHASE WILL REACH ITS CONVERSION EVENT.
## 9. Limitation of Liability
IN NO EVENT SHALL THE LICENSOR OR ANY CONTRIBUTOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE, EXCEPT TO THE EXTENT SUCH LIMITATION IS PROHIBITED BY APPLICABLE LAW.
## 10. Trademarks
This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary attribution.
## 11. General Provisions
**11.1 Governing law and venue.** Adopted for alpha/beta 2026-07-29: disputes arising under this License shall be resolved by binding arbitration, seated at a neutral, arbitration-mature venue (Singapore or London are the two candidates concretely supported by current research), rather than by litigation in a national court. The specific arbitral institution and substantive governing law remain a per-deployment blank pending final selection; they are not fixed by this candidate. See the canonical repository's `history/260729-TRSL-Jurisdiction-Synthesis.md` §2.
**11.2 Severability.** If any provision of this License is held unenforceable, the remaining provisions remain in full force, and the unenforceable provision shall be reformed to the minimum extent necessary to make it enforceable.
**11.3 No waiver.** Failure to enforce any provision of this License is not a waiver of future enforcement of that or any other provision.
**11.4 Entire agreement (as to licensing).** This License, together with the applicable Phase Manifest and, where applicable, the Commercial Use Agreement, constitutes the entire agreement between You and the Licensor regarding the Software's licensing terms. Operations, service, and consulting arrangements are governed by separate agreements, if any, and are not part of this License.
**11.5 Definitions control.** Marketing materials, documentation, or other non-normative communications about the Software must not describe pre-Conversion-Event Software as "Open Source," "free software," or "open core." Pre-conversion Noncommercial Use is **source-available**; pre-conversion Commercial Use requires a **Commercial Entitlement**; only post-conversion Software may be described as Open Source, under the Future License.
---
**No Phase is currently declared for this repository under this License.** Until a Phase Manifest is published and registered with the Trust Service for a Milestone Release in this repository, Sections 27 above have no operative subject matter here — this License establishes the governing framework in advance of that declaration, consistent with the org-wide rollout decision recorded in `target-revenue`'s `workplans/TREV-WP-0008-governance-and-pilot-rollout.md`.

View file

@ -9,8 +9,41 @@ See `INTENT.md` for direction, `SCOPE.md` for current implementation, and
and routes every other credential need to its owner — see `wiki/AccessRouting.md`.
Latest gap analysis: `history/2026-06-17-post-wp0007-reassessment.md`.
## Get the source (Forgejo)
Canonical repo: `https://forgejo.coulomb.social/coulomb/ops-warden`
Releases: `https://forgejo.coulomb.social/coulomb/ops-warden/releases`
**HTTPS clone:**
```bash
git clone https://forgejo.coulomb.social/coulomb/ops-warden.git ~/ops-warden
cd ~/ops-warden
```
**SSH clone** (recommended for push/pull; add to `~/.ssh/config` if missing):
```sshconfig
Host forgejo-remote
HostName 92.205.62.239
Port 30022
User git
IdentityFile ~/.ssh/id_gitea
StrictHostKeyChecking accept-new
```
```bash
git clone forgejo-remote:coulomb/ops-warden.git ~/ops-warden
cd ~/ops-warden
```
Legacy Gitea remotes (`gitea-remote`, `gitea.coulomb.social`) still work during
migration; new checkouts should use Forgejo.
## Install
From a Forgejo checkout:
**Recommended** (warden + experiential memory for route/worker/agent sessions):
```bash
@ -41,6 +74,34 @@ phase-memory must be a sibling checkout at `../phase-memory` by default, or set
`PHASE_MEMORY_REPO` when running make. Opt out of memory at runtime with
`WARDEN_MEMORY=0`.
### Upgrade after a release
When a new tag is published on Forgejo (e.g. `v0.1.2`):
```bash
cd ~/ops-warden
git fetch --tags origin
git pull --ff-only
make install-all
warden route list # sanity check the installed CLI
```
If `warden` still behaves like an older build (same version string but missing
recent subcommands or fixes), clear the cached wheel and reinstall:
```bash
uv cache clean ops-warden
uv tool install . --with-editable ../phase-memory --reinstall --force
```
Check out a specific release:
```bash
git fetch --tags origin
git checkout v0.1.2
make install-all
```
## Quick start (local backend)
```bash

186
SCOPE.md
View file

@ -17,7 +17,7 @@ access guidance aligned with NetKingdom canon.
---
## Where we are (2026-07-01)
## Where we are (2026-08-22)
ops-warden **issues short-lived SSH certificates and routes every other credential
need to the subsystem that owns it.** SSH signing is **production-verified** on
@ -48,10 +48,13 @@ the read-only conformance checker `scripts/check_secret_posture_conformance.py`
and the dev-tier contract-double library `warden.doubles` (T4). Canon landing in
net-kingdom / info-tech-canon is owner-driven (tracked via coordination messages, T5).
**Policy gate** is shipped on the caller side (WP-0007) with production registry
and smoke evidence (WP-0009 archived). flex-auth published the `ssh-certificate`
policy package (FLEX-WP-0006). `policy.enabled` remains **false** in production
until flex-auth is deployed to a reachable URL (flex-auth FLEX-WP-0007).
**The policy gate is zone-aware.** The caller-identity path is production proven
and the flex-auth pin enforces caller authentication. WP-0032 adopted
`security-zones_v0.1`: the repo-wide `policy.enabled` and `policy.fail_closed`
settings are retired, target workload membership compiles into flex-auth resource
attributes, and ops-warden selects dependency failure behavior from the target
zone. Unknown membership is explicit and uses the versioned build profile.
Ops-warden itself declares `z1-operational` in `tenancy.yaml`.
**ops-bridge cert_command pilot** is shipped to pilot-ready (WP-0016): a read-only
readiness gate (`scripts/check_tunnel_cert_readiness.py`) plus an opt-in offline
@ -59,12 +62,50 @@ contract smoke (`--sign-smoke`); the playbook leads with the gate and the pilot
(`agt-state-hub-bridge`) is handed to ops-bridge. The live tunnel cutover is
ops-bridge's to execute.
**INTENT alignment:** SSH issuance mission met in production. ops-warden workplans
through WP-0021 are finished; WP-0022 (audit) and WP-0023 (INTENTSCOPE closeout)
ship in July 2026. Remaining distance is in other repos' lanes: ops-bridge running
the cert_command pilot cutover, flex-auth runtime deployment (FLEX-WP-0007, unblocks
`policy.enabled: true`), and the owner-driven WP-0015 canon landing — plus ongoing
operator hygiene.
**Credential hygiene and the policy front door** shipped through July 2026:
disclosure hygiene and rotation guidance (WP-0026 — `warden taint`,
`warden rotate-guide`, agent read-boundary on high-risk lanes), the tenant secret
custody pattern (WP-0028, first lane binky company email IMAP), experiential memory
across worker/agent sessions (WP-0024), the Forgejo admin PAT lane (WP-0025), and the
posture-aware policy front door (WP-0029 — `warden plan`, `warden desk`, declared
`organization_posture: build` as a third axis). WP-0027 (tamper-resistant governance,
mass rotation/lockdown) is drafted and sits in `backlog`.
**Delegation register** is the open question (WP-0030, proposed). ops-warden fronts
11 catalog lanes as a caller-identity proxy with no record of which component *should*
own that front door. The primitive to delegate exists and is proven
(`exec_owner`/`exec_command` — secrets-engine for npm publish, the credential broker
for warden-sign) but is used by 2 of 24 lanes. See
`history/2026-08-11-delegation-surface-assessment.md`.
**INTENT alignment:** SSH issuance mission met in production. All ops-warden workplans
through WP-0029 are finished except WP-0027 (`backlog`) and WP-0030 (`proposed`).
Remaining distance is in other repos' lanes: ops-bridge running the cert_command pilot
cutover, flex-auth publishing the zone-aware pre-sign stance package,
the owner-driven WP-0015 canon landing, and — newly named — the missing owner front
doors that keep ops-warden holding interim lanes (secrets-engine, tenant-engine).
### Layer-model conformance (v0.7, accepted)
ops-warden declares **Staff**, **PEP-shaped**, in `INTENT.md` frontmatter and in its
own voice — `security-layer-model_v0.7` §11. Shipped declaration artifacts, both
cited in the standard as the estate's reference forms:
| Artifact | Declares | Status |
| --- | --- | --- |
| `layer.yaml` | 5 Tooling contacts mapped to §5.1/§5.2/§5.3 shapes + non-Tooling clients so the check is total | shipped; named reference form (§11) |
| `pep-stance.yaml` | unreachable-engine stance map, total per zone | shipped; registered in statute §13.1 (§6.4 obl. 3) |
| `scripts/check_layer_conformance.py` | every direct Tooling client maps to a declared shape | shipped; CI-enforced |
| `tests/test_layer_conformance.py` | the §5.2 no-authority property, and published stance map **equals** shipped default | shipped, 11 tests |
Conformance state under §11: **declared gap** — tracked non-conformance, not
conformance. Two §5.3 contacts (`VaultCA` signing write, `warden desk` `bao kv put`),
intended owner `secrets-engine`, registered in statute §13.
Four ops-warden findings have been adopted into the standard: §9.1's two marks
(`pending` vs `declared-gap`), §5's Tooling scope rule, §6.4 obligation 1's second
limb, and §13.1's existence. Reviews: `history/2026-08-29-layer-model-v04-review.md`,
`-v06-review.md`, `-v07-scope-intent-assessment.md`.
### Issue vs route
@ -84,6 +125,12 @@ ops-warden executes exactly one lane with its own authority and routes/assists t
Full role and boundary: `wiki/AccessRouting.md`. The catalog is a **pointer layer**
it never restates an owner's procedure (authored `steps` exist only for the SSH lane).
**Interim by default.** SSH issuance is the only lane ops-warden owns permanently.
Where it proxies or assists, it is covering a need no component fronts yet — a
legitimate service, but a *tracked gap*, retired to the owner once their front door
exists (INTENT §9). Recording that intent per lane is WP-0030; today only
`whynot-design-npm-publish` and `ops-warden-warden-sign-token` carry it.
Gap analysis: `history/2026-07-01-intent-scope-gap-analysis.md` (current);
`history/2026-06-24-intent-scope-gap-analysis.md` (prior);
`history/2026-06-18-post-wp0008-intent-scope-reassessment.md` (SSH lane);
@ -101,6 +148,7 @@ Gap analysis: `history/2026-07-01-intent-scope-gap-analysis.md` (current);
| NetKingdom evolution reflected in docs | Met |
| Non-SSH secrets stay out of ops-warden | Met |
| Workload posture / maturity model for secret-flow blockers | Met — two-axis standard + descriptors + conformance checker + dev doubles (WP-0015) |
| Every execution position explicitly permanent or interim with a named owner | **Met** — every catalog entry carries `delegation:`; `warden route gaps` lists the interim set (WP-0030) |
**Maturity vector:** `D5 / A5 / C5 / R4` (Discovery / Availability / Completeness / Reliability)
@ -108,11 +156,37 @@ Gap analysis: `history/2026-07-01-intent-scope-gap-analysis.md` (current);
| --- | --- | --- |
| D5 | Discovery | Routing wiki + security map + pointer catalog + NK canon cross-links |
| A5 | Availability | CLI + `warden route` + `warden access` advisory & proxy front door + `warden policy` + opt-in policy gate + agent `--json` |
| C5 | Completeness | All ops-warden lanes shipped — SSH (prod), routing, access assist, posture conformance, cert_command pilot gate, two owner-native exec routes documented (secrets-engine npm, credential broker warden-sign). Open items are external: flex-auth prod flip + ops-bridge live cutover |
| C5 | Completeness | All ops-warden lanes shipped — SSH (prod), routing, access assist, posture conformance, cert_command pilot gate, disclosure hygiene, tenant custody, policy front door, delegation register (WP-0030) |
| R4 | Reliability | Live OpenBao sign + credential-broker policy-gate smoke evidence on Railiance (2026-07-01) |
---
## Governing rules (ours)
The decisions that bind this repo are ADRs in `docs/adr/`, each `owner: ops-warden`
meaning we follow them *and* we are the ones who may change them. Changing one is a
superseding ADR, never an in-place edit.
| ADR | Rule |
| --- | --- |
| `ADR-0001` | The routing catalog is a pointer layer, never a second copy of an owner's procedure (CI-enforced) |
| `ADR-0002` | ops-warden is a transparent conduit, never a secret broker |
| `ADR-0003` | Cover gaps, but never silently own them |
| `ADR-0004` | High-risk lanes refuse raw value streaming to agent sessions |
| `ADR-0005` | Implement one lane narrowly, route everything else |
| `ADR-0006` | Superseded: enforcement is zone-scoped, never a global flag |
| `ADR-0007` | Build-stage permissiveness stops at credential disclosure; every lane carries an explicit `risk` grade |
| `ADR-0008` | A lane's risk grade covers every field its path discloses, not just the field it is named after |
| `ADR-0009` | Adopt security-zones v0.1 and compile explicit workload membership; PEP failure mode is per zone |
| `ADR-0010` | ops-warden is Staff and PEP-shaped — it owns access lanes, never access rules; the direct OpenBao client is a declared engine gap, not an exemption |
Rules we follow but do not own — NetKingdom canon, the IAM profile, the
credential-management standard, the-custodian's ADR-001 workplan convention — are
cited, never copied here. Publishable through `policy-nexus`, which carries `owner`
into the published page and index.
---
## Core Idea
**Today:** implements the SSH certificate lane from `wiki/AccessManagementDirective.md`
@ -137,7 +211,8 @@ for the rest.
- `cert_command`: `warden sign <actor> --pubkey <path>` → cert on stdout
- TTL enforcement per `ActorType` (`adm` 48 h, `agt` 24 h, `atm` 8 h)
- `warden status`, cleanup, scorecard, signatures log
- Opt-in flex-auth policy gate (`policy.enabled`, `policy_decision_id` in log)
- Zone-aware flex-auth policy gate (`policy_decision_id`, zone, failure mode, and
outcome in the signing audit; no repo-wide enable switch)
- Production flex-auth registry builder (`scripts/build_flex_auth_registry.py`,
`registry/flex-auth/production_registry_snapshot.json`)
- Policy gate smoke runner (`scripts/policy_gate_production_smoke.sh`)
@ -165,6 +240,19 @@ for the rest.
- **Unified audit trail** (WP-0022): append-only `audit.jsonl`, secret-material guard,
instrumentation on sign/access/worker paths, `warden activity` CLI merging legacy
logs + optional State Hub notes (`wiki/AuditTrail.md`)
- **Experiential memory** (WP-0024, `src/warden/memory.py`) — recorded outcomes feed
routing and coordination; no secret values, guardrail allowlist unchanged
- **Disclosure hygiene** (WP-0026): `warden taint <catalog-id>` (KV `custom_metadata`,
no data read), `warden rotate-guide`, safe fetch transports (`--out` / `--exec` /
`--wrap`) with refusal to stream to non-terminal stdout, and the agent read-boundary
on `risk: high` lanes (exit 7 when `WARDEN_AGENT_ID` is set)
- **Tenant secret custody** (WP-0028): tenant vs `platform/workloads/...` path
convention, policy/CCR/catalog ownership, first lane `binky-company-email-imap`
- **Policy front door** (WP-0029): `warden plan "<need>" [--json]` returning
`autonomous` / `founder_required` (typed act) / `unroutable` (CCR stub);
`warden desk` loopback founder surface (approve, OIDC login, paste-once provision
straight into OpenBao); `organization_posture: build` as posture axis C; catalog
freshness reporting on `warden route list` and in plan JSON
### Stewardship (documentation and alignment)
@ -183,7 +271,7 @@ for the rest.
| --- | --- |
| WP-00010005 | Initial CLI, quality, hygiene, OpenBao docs, hub sync |
| WP-0006 | Credential routing, security map, inventory patterns, OpenBao checklist |
| WP-0007 | Opt-in flex-auth policy gate (`policy.enabled`) |
| WP-0007 | Original opt-in flex-auth policy gate (global switch retired by WP-0032) |
| WP-0008 | Production sign verification, stewardship closeout, archive hygiene |
| WP-0009 | flex-auth registry + policy smoke; pickup brief for FLEX-WP-0007 |
| WP-0010 | Access routing charter + pointer catalog |
@ -198,8 +286,26 @@ for the rest.
| WP | Focus |
| --- | --- |
| WP-0017 | Access front-door discoverability |
| WP-0018 | `whynot-design-npm-publish` — first concrete secret lane (production-exercised) |
| WP-0019 | Route provisioned secret-exec lanes to secrets-engine (`exec_owner` pattern) |
| WP-0020 | Coordination worker (`warden worker`) |
| WP-0021 | Scheduled worker tick (systemd --user timer, kill switch) |
| WP-0022 | Unified audit trail + `warden activity` |
| WP-0023 | INTENTSCOPE alignment closeout |
| WP-0024 | Experiential memory across worker/agent sessions (`src/warden/memory.py`) |
| WP-0025 | Forgejo admin PAT OpenBao lane (CCR-2026-0006) |
| WP-0026 | Credential disclosure hygiene — `warden taint`, `warden rotate-guide`, agent read-boundary, safe fetch transports |
| WP-0028 | Tenant secret custody pattern — tenant vs platform paths; first lane binky company email IMAP |
| WP-0029 | Policy front door — `warden plan`, `warden desk`, `organization_posture: build` third axis |
### Open ops-warden work
| WP | Status | Focus |
| --- | --- | --- |
| WP-0027 | `active` | Break-glass design/rehearsal activated narrowly on T02; mass rotation and policy-manifest reconcile remain deferred |
| WP-0032 | `finished` | Security zones adopted — global switch retired, explicit workload references compiled, and owner policy live |
| WP-0030 | `proposed` | Delegation register — record intended owner + blocker on every interim lane, `warden route gaps`, promotion gate |
Remaining production distance is also in other repos' lanes (see Known gaps).
@ -207,11 +313,12 @@ Remaining production distance is also in other repos' lanes (see Known gaps).
| Gap | Owner | Notes |
| --- | --- | --- |
| flex-auth production runtime + registry deploy | flex-auth | **FLEX-WP-0007** — unblocks `policy.enabled: true` |
| ops-bridge `cert_command` on live tunnels | ops-bridge | Playbook + readiness gate shipped (WP-0016); pilot cutover handed off, awaiting ops-bridge |
| Principals sync warden ↔ railiance-infra | ops-warden + infra | `scripts/check_principals_drift.py` — operator runs periodically |
| NK-WP-0009 joint SSH tutorial | net-kingdom | Parallel coordination track |
| WP-0015 canon landing (generic `WorkloadMaturityLevel` + M0-M3 requirements) | net-kingdom + info-tech-canon | ops-warden drafted + offered (coordination msgs); owner-driven landing |
| Owner front doors for workload secret lanes | secrets-engine | 6 lanes proxied by ops-warden that `secrets-engine exec` could front, as WP-0019 did for npm publish |
| Owner front door for tenant secret lanes | tenant-engine | WP-0028 defined the custody pattern; 3 tenant lanes still fronted by ops-warden proxy |
---
@ -231,6 +338,9 @@ Remaining production distance is also in other repos' lanes (see Known gaps).
- OpenBao / Vault cluster deployment → `railiance-platform`
- Human admin SSH key generation (self-service `ssh-keygen`)
- Session recording, SIEM, SSO / Teleport at scale
- **Permanently owning another component's lane.** Covering an unfilled gap is in
scope and expected; keeping it once secrets-engine / tenant-engine / user-engine
can front it — or holding it without recording that it is interim — is not (INTENT §9)
---
@ -268,7 +378,12 @@ Remaining production distance is also in other repos' lanes (see Known gaps).
- **Production sign:** verified 2026-06-18 (`history/2026-06-17-openbao-production-verify.md`)
- **Access routing:** WP-0010 + WP-0011 shipped (`warden route`, pointer catalog)
- **Policy gate:** caller shipped (WP-0007); registry + smoke complete (WP-0009 archived).
`policy.enabled: false` until flex-auth reachable (`FLEX-WP-0007`)
WP-0031 shipped the calling identity and flex-auth's pin now runs
`callerAuth.mode: enforce` (FLEX-WP-0016) — the gate is **ready and verified**
(`decision:f3f7c88f9585582a`, anonymous `/v1/check` -> 401). WP-0032 and
`ADR-0009` retired the global switch: the compiled target workload selects the
zone, flex-auth owns stance, and ops-warden applies the zone's PEP failure mode.
Re-check caller identity with `scripts/check_policy_caller_identity.py`.
- **Workload posture:** WP-0015 shipped (standard, descriptors, `warden policy`,
conformance checker, dev doubles); canon landing owner-driven
- **ops-bridge cert_command:** WP-0016 shipped to pilot-ready (readiness gate +
@ -284,12 +399,24 @@ Remaining production distance is also in other repos' lanes (see Known gaps).
- **Audit + activity:** WP-0022 shipped — `warden activity`, `wiki/AuditTrail.md`
- **INTENT closeout:** WP-0023 shipped — INTENT refresh, production flip/cutover
checklists, catalog promotion cadence, broker hint on missing `VAULT_TOKEN`
- **Active work:** none open in ops-warden after WP-0022/0023; remaining distance is
other repos' lanes
- **Disclosure hygiene:** WP-0026 shipped — `warden taint`, `warden rotate-guide`,
safe fetch transports (`--out`/`--exec`/`--wrap`), agent read-boundary on `risk: high`
lanes (`wiki/playbooks/agent-read-boundary.md`)
- **Tenant custody:** WP-0028 shipped — tenant vs platform path convention; first lane
`binky-company-email-imap`. Front door is still an ops-warden proxy (tenant-engine gap)
- **Policy front door:** WP-0029 shipped — `warden plan "<need>"` (autonomous /
founder_required / unroutable), `warden desk` founder interaction surface, declared
`organization_posture: build` as a third posture axis, catalog freshness reporting
- **Delegation:** 27 catalog lanes carry `delegation:` (WP-0030). SSH is
`permanent`; owner-fronted lanes are `native`; interim proxies name
`intended_owner` + `blocked_on`. Query: `warden route gaps`.
- **Active work:** WP-0027 (`backlog`); remaining production distance is other
repos' lanes (and retiring interim covers as those owners ship front doors)
- **Integration docs:** cert_command migration, token hygiene (broker-first), principals
drift (`wiki/playbooks/`)
- **Latest assessment:** `history/2026-07-01-intent-scope-gap-analysis.md`
- **Latest workplans:** WP-0022 (audit), WP-0023 (INTENTSCOPE closeout) — shipped July 2026
- **Latest assessment:** `history/2026-08-11-delegation-surface-assessment.md`
- **Latest workplans:** WP-0029 (policy front door) shipped July 2026; WP-0030
(delegation register) shipped August 2026
---
@ -329,14 +456,18 @@ Downstream: `ops-bridge` (primary), kaizen agents, CI automations, human operato
| Repo | Relationship |
| --- | --- |
| `gate-house` | Owns the security layer model, doctrine, invariants, authority context, and conformance review. ops-warden routes doctrine questions there, and the companion routes the estate's *path* questions back to ops-warden (`ADR-0010`) |
| `net-kingdom` | Canonical security architecture; ops-warden aligns to it |
| `ops-bridge` | Primary cert_command consumer |
| `railiance-infra` | Host-side SSH principals and hardening |
| `railiance-platform` | OpenBao deployment and platform secrets |
| `flex-auth` | Authorization; policy package shipped (FLEX-WP-0006); runtime deploy FLEX-WP-0007 |
| `flex-auth` | Authorization — ruled name `access-engine`; the only policy decision point. Policy package shipped (FLEX-WP-0006); runtime deploy FLEX-WP-0007 |
| `key-cape` | Identity / IAM Profile lightweight mode |
| `secrets-engine` | Owner-native secret-exec front door (`secrets-engine exec/route`); ops-warden routes provisioned secret lanes to it (WP-0019) |
| `state-hub` | Workstream registry |
| `secrets-engine` | Owner-native secret-exec front door (`secrets-engine exec/route`); ops-warden routes provisioned secret lanes to it (WP-0019) and holds 6 more as interim proxies pending its front doors |
| `tenant-engine` | Intended owner of tenant/client secret front doors; ops-warden holds 3 tenant lanes as interim proxies (WP-0028 pattern, WP-0030 register) |
| `user-engine` | End-user identity/account lifecycle; no ops-warden lane today — route rather than absorb |
| `zone-engine` | Owns the security zone model and exception lifecycle (`ADR-0006`); ops-warden is its first consumer |
| `state-hub` | Workplan registry |
---
@ -373,11 +504,16 @@ keywords: [access, credential, secret, npm, token, api-key, openbao, key-cape, l
| --- | --- |
| `INTENT.md` | Why ops-warden exists and where it is going |
| `SCOPE.md` | What is implemented today (this file) |
| `docs/adr/README.md` | **The rules ops-warden owns** — and how to tell ours from inherited canon |
| `wiki/AccessRouting.md` | What ops-warden issues vs routes vs assists (role and boundary) |
| `wiki/OperatorAccessAssist.md` | `warden access` front door + conduit-vs-broker boundary + guardrails |
| `wiki/CredentialRouting.md` | Which subsystem for each credential need |
| `wiki/WorkloadSecurityPosture.md` | Secret-store posture, workload maturity, and blocker triage |
| `registry/routing/catalog.yaml` | Machine-readable routing pointer catalog |
| `net-kingdom/SECURITY-COMPANION.md` | **The estate's operative security rules — start here** |
| `layer.yaml` | Layer declaration: every Tooling contact and its §5 shape |
| `pep-stance.yaml` | Unreachable-engine stance map (§6.4); equals shipped behaviour by test |
| `tenancy.yaml` | Declared tenancy posture (`I1 A1 E0 P n/a R n/a V0`) and why each axis sits where it does |
| `wiki/NetKingdomSecurityMap.md` | Platform security component map |
| `examples/warden.production.example.yaml` | Production warden.yaml template |
| `wiki/PolicyGatedSigning.md` | flex-auth opt-in gate + registry rollout |
@ -388,7 +524,9 @@ keywords: [access, credential, secret, npm, token, api-key, openbao, key-cape, l
| `wiki/AuditTrail.md` | Unified metadata-only audit + `warden activity` |
| `wiki/playbooks/catalog-lane-promotion.md` | draft → active catalog promotion checklist |
| `wiki/CertCommandInterface.md` | cert_command contract |
| `history/2026-07-01-intent-scope-gap-analysis.md` | Current INTENT↔SCOPE gap analysis |
| `history/2026-08-11-delegation-surface-assessment.md` | Current assessment — where ops-warden covers gaps and who should own them |
| `workplans/WARDEN-WP-0030-delegation-register.md` | Delegation register plan (proposed) |
| `history/2026-07-01-intent-scope-gap-analysis.md` | Prior INTENT↔SCOPE gap analysis |
| `workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md` | Alignment closeout plan |
| `history/2026-06-24-intent-scope-gap-analysis.md` | Prior gap analysis |
| `history/2026-06-27-workload-security-posture-charter.md` | WP-0015 posture/conformance charter |

145
WORK-RECORDS.md Normal file
View file

@ -0,0 +1,145 @@
# Work Records — ops-warden
> Generated by `statehub fix-consistency` (CUST-WP-0061-T04, work-record
> stage 3). Do not edit by hand — edit the source file/block listed for
> each record and re-run fix-consistency to refresh this index. Archived
> workplans are omitted; closed decisions/intakes/engagements stay listed
> so recently-resolved work is still visible. [auto]
| Kind | ID | Status | Lane | Source |
| --- | --- | --- | --- | --- |
| workplan | WARDEN-WP-ADHOC-2026-06-27 | finished | — | workplans/ADHOC-2026-06-27.md |
| workplan | WARDEN-WP-ADHOC-2026-06-29 | finished | — | workplans/ADHOC-2026-06-29.md |
| workplan | WARDEN-WP-ADHOC-2026-08-11 | finished | — | workplans/ADHOC-2026-08-11.md |
| workplan | WARDEN-WP-ADHOC-2026-08-17 | finished | — | workplans/ADHOC-2026-08-17.md |
| workplan | WARDEN-WP-0016 | finished | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md |
| workplan | WARDEN-WP-0017 | finished | — | workplans/WARDEN-WP-0017-access-front-door-discoverability.md |
| workplan | WARDEN-WP-0018 | finished | — | workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md |
| workplan | WARDEN-WP-0019 | finished | — | workplans/WARDEN-WP-0019-route-to-secrets-engine.md |
| workplan | WARDEN-WP-0020 | finished | — | workplans/WARDEN-WP-0020-ops-warden-worker.md |
| workplan | WARDEN-WP-0021 | finished | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md |
| workplan | WARDEN-WP-0022 | finished | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md |
| workplan | WARDEN-WP-0023 | finished | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md |
| workplan | WARDEN-WP-0024 | finished | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md |
| workplan | WARDEN-WP-0025 | finished | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md |
| workplan | WARDEN-WP-0026 | finished | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md |
| workplan | WARDEN-WP-0027 | active | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md |
| workplan | WARDEN-WP-0028 | finished | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md |
| workplan | WARDEN-WP-0029 | finished | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md |
| workplan | WARDEN-WP-0030 | finished | — | workplans/WARDEN-WP-0030-delegation-register.md |
| workplan | WARDEN-WP-0031 | finished | — | workplans/WARDEN-WP-0031-policy-caller-identity.md |
| workplan | WARDEN-WP-0032 | finished | — | workplans/WARDEN-WP-0032-security-zones.md |
| workplan | WARDEN-WP-0033 | finished | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
| workplan | WARDEN-WP-0034 | ready | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md |
| 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 |
| task | WARDEN-WP-ADHOC-2026-06-27-T01 | done | — | workplans/ADHOC-2026-06-27.md |
| task | WARDEN-WP-ADHOC-2026-06-29-T01 | done | — | workplans/ADHOC-2026-06-29.md |
| task | WARDEN-WP-ADHOC-2026-08-11-T01 | done | — | workplans/ADHOC-2026-08-11.md |
| task | WARDEN-WP-ADHOC-2026-08-11-T02 | done | — | workplans/ADHOC-2026-08-11.md |
| task | WARDEN-WP-ADHOC-2026-08-11-T03 | done | — | workplans/ADHOC-2026-08-11.md |
| task | WARDEN-WP-ADHOC-2026-08-17-T01 | done | — | workplans/ADHOC-2026-08-17.md |
| task | WARDEN-WP-ADHOC-2026-08-17-T02 | done | — | workplans/ADHOC-2026-08-17.md |
| task | WARDEN-WP-ADHOC-2026-08-17-T03 | done | — | workplans/ADHOC-2026-08-17.md |
| task | WARDEN-WP-ADHOC-2026-08-17-T04 | done | — | workplans/ADHOC-2026-08-17.md |
| task | WARDEN-WP-0016-T01 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md |
| task | WARDEN-WP-0016-T02 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md |
| task | WARDEN-WP-0016-T03 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md |
| task | WARDEN-WP-0016-T04 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md |
| task | WARDEN-WP-0017-T01 | done | — | workplans/WARDEN-WP-0017-access-front-door-discoverability.md |
| task | WARDEN-WP-0017-T02 | done | — | workplans/WARDEN-WP-0017-access-front-door-discoverability.md |
| task | WARDEN-WP-0017-T03 | done | — | workplans/WARDEN-WP-0017-access-front-door-discoverability.md |
| task | WARDEN-WP-0018-T01 | done | — | workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md |
| task | WARDEN-WP-0018-T02 | done | — | workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md |
| task | WARDEN-WP-0018-T03 | done | — | workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md |
| task | WARDEN-WP-0019-T01 | done | — | workplans/WARDEN-WP-0019-route-to-secrets-engine.md |
| task | WARDEN-WP-0019-T02 | done | — | workplans/WARDEN-WP-0019-route-to-secrets-engine.md |
| task | WARDEN-WP-0020-T01 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md |
| task | WARDEN-WP-0020-T02 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md |
| task | WARDEN-WP-0020-T03 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md |
| task | WARDEN-WP-0020-T04 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md |
| task | WARDEN-WP-0020-T05 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md |
| task | WARDEN-WP-0021-T01 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md |
| task | WARDEN-WP-0021-T02 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md |
| task | WARDEN-WP-0021-T03 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md |
| task | WARDEN-WP-0021-T04 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md |
| task | WARDEN-WP-0021-T05 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md |
| task | WARDEN-WP-0022-T01 | done | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md |
| task | WARDEN-WP-0022-T02 | done | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md |
| task | WARDEN-WP-0022-T03 | done | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md |
| task | WARDEN-WP-0022-T04 | done | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md |
| task | WARDEN-WP-0023-T01 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md |
| task | WARDEN-WP-0023-T02 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md |
| task | WARDEN-WP-0023-T03 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md |
| task | WARDEN-WP-0023-T04 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md |
| task | WARDEN-WP-0023-T05 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md |
| task | WARDEN-WP-0023-T06 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md |
| task | WARDEN-WP-0023-T07 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md |
| task | WARDEN-WP-0024-T01 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md |
| task | WARDEN-WP-0024-T02 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md |
| task | WARDEN-WP-0024-T03 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md |
| task | WARDEN-WP-0024-T04 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md |
| task | WARDEN-WP-0024-T05 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md |
| task | WARDEN-WP-0024-T06 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md |
| task | WARDEN-WP-0024-T07 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md |
| task | WARDEN-WP-0025-T01 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md |
| task | WARDEN-WP-0025-T02 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md |
| task | WARDEN-WP-0025-T03 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md |
| task | WARDEN-WP-0025-T04 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md |
| task | WARDEN-WP-0025-T05 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md |
| task | WARDEN-WP-0026-T01 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md |
| task | WARDEN-WP-0026-T02 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md |
| task | WARDEN-WP-0026-T03 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md |
| task | WARDEN-WP-0026-T04 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md |
| task | WARDEN-WP-0026-T05 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md |
| task | WARDEN-WP-0026-T06 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md |
| task | WARDEN-WP-0026-T07 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md |
| task | WARDEN-WP-0027-T01 | cancel | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md |
| task | WARDEN-WP-0027-T02 | progress | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md |
| task | WARDEN-WP-0027-T03 | cancel | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md |
| task | WARDEN-WP-0028-T01 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md |
| task | WARDEN-WP-0028-T02 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md |
| task | WARDEN-WP-0028-T03 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md |
| task | WARDEN-WP-0028-T04 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md |
| task | WARDEN-WP-0028-T05 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md |
| task | WARDEN-WP-0028-T06 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md |
| task | WARDEN-WP-0028-T07 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md |
| task | WARDEN-WP-0029-T01 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md |
| task | WARDEN-WP-0029-T02 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md |
| task | WARDEN-WP-0029-T03 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md |
| task | WARDEN-WP-0029-T04 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md |
| task | WARDEN-WP-0029-T05 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md |
| task | WARDEN-WP-0030-T01 | done | — | workplans/WARDEN-WP-0030-delegation-register.md |
| task | WARDEN-WP-0030-T02 | done | — | workplans/WARDEN-WP-0030-delegation-register.md |
| task | WARDEN-WP-0030-T03 | done | — | workplans/WARDEN-WP-0030-delegation-register.md |
| task | WARDEN-WP-0030-T04 | done | — | workplans/WARDEN-WP-0030-delegation-register.md |
| task | WARDEN-WP-0030-T05 | done | — | workplans/WARDEN-WP-0030-delegation-register.md |
| task | WARDEN-WP-0031-T01 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md |
| task | WARDEN-WP-0031-T02 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md |
| task | WARDEN-WP-0031-T03 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md |
| task | WARDEN-WP-0031-T04 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md |
| task | WARDEN-WP-0031-T05 | cancel | — | workplans/WARDEN-WP-0031-policy-caller-identity.md |
| task | WARDEN-WP-0032-T01 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0032-T02 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0032-T03 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0032-T04 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0032-T05 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0032-T06 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0032-T07 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
| task | WARDEN-WP-0033-T01 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
| task | WARDEN-WP-0033-T02 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
| task | WARDEN-WP-0033-T03 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
| task | WARDEN-WP-0033-T04 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
| task | WARDEN-WP-0033-T05 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
| task | WARDEN-WP-0033-T06 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
| task | WARDEN-WP-0034-T01 | todo | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md |
| task | WARDEN-WP-0034-T02 | todo | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md |
| task | WARDEN-WP-0034-T03 | todo | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md |
| task | WARDEN-WP-0034-T04 | todo | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md |
| task | WARDEN-WP-0034-T05 | todo | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md |
| task | WARDEN-WP-0035-T01 | done | — | workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md |
| task | WARDEN-WP-0035-T02 | done | — | workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md |
| task | WARDEN-WP-0036-T01 | done | — | workplans/WARDEN-WP-0036-attended-login-openbao-output.md |
| task | WARDEN-WP-0036-T02 | done | — | workplans/WARDEN-WP-0036-attended-login-openbao-output.md |
| intake | WARDEN-IN-0001 | closed | — | intakes/intakes.md |
| intake | WARDEN-IN-0002 | open | — | intakes/intakes.md |

View file

@ -0,0 +1,39 @@
# ops-warden's calling identity for flex-auth (WARDEN-WP-0031 T04).
#
# flex-auth's `flex-auth-ops-warden` pin binds `resource.system: ops-warden` to
# the principal `system:serviceaccount:ops-warden:ops-warden` and TokenReviews
# the caller's bearer token with audience `flex-auth` (FLEX-WP-0016).
#
# This ServiceAccount is the subject of that binding. It holds no RBAC at all —
# it is never used to talk to the Kubernetes API, only to be *reviewed* by it.
# A workstation `warden sign` mints a short-lived bound token against it:
#
# kubectl create token ops-warden -n ops-warden \
# --audience flex-auth --duration 10m
#
# Boundary note: cluster resources are railiance-platform's to own. This
# manifest lives here because the identity is ops-warden's and flex-auth's
# binding names it; railiance-platform should adopt it into the cluster's own
# manifests, at which point this file becomes the record of what was applied
# rather than the source of truth (ADR-0003 — cover the gap, name the owner).
apiVersion: v1
kind: Namespace
metadata:
name: ops-warden
labels:
app.kubernetes.io/managed-by: ops-warden
netkingdom.coulomb.social/purpose: caller-identity
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: ops-warden
namespace: ops-warden
labels:
app.kubernetes.io/managed-by: ops-warden
annotations:
netkingdom.coulomb.social/bound-by: >-
flex-auth-ops-warden callerAuth binding
ops-warden=system:serviceaccount:ops-warden:ops-warden (FLEX-WP-0016)
netkingdom.coulomb.social/workplan: WARDEN-WP-0031
automountServiceAccountToken: false

View file

@ -0,0 +1,90 @@
---
id: ops-warden-adr-0001
type: adr
title: "ADR-0001 — The routing catalog is a pointer layer, never a second copy"
domain: infotech
repo: ops-warden
status: accepted
version: "1.0"
revision: "1"
owner: ops-warden
binds: "ops-warden; every repo contributing a catalog entry"
created: "2026-06-20"
updated: "2026-08-18"
last_reviewed: "2026-08-18"
review_interval: 6m
enforced_by: tests/test_routing.py
supersedes: ""
successor: ""
---
# ADR-0001 — The routing catalog is a pointer layer, never a second copy
## Status
Accepted. Decided during WARDEN-WP-0010 (access routing charter), enforced in code
since WARDEN-WP-0011. Restated here because it binds repos other than ops-warden
and had, until now, no address they could cite.
## Context
`registry/routing/catalog.yaml` tells a worker which subsystem owns a credential
need and where the authoritative procedure lives. The obvious temptation, every
time someone uses it, is to add the procedure itself: the reader is already here,
the steps are short, and one more copy seems cheaper than a second lookup.
That temptation is the failure mode. A copied procedure is correct on the day it
is written and silently wrong afterwards, because the owner changes theirs without
knowing ours exists. The estate has already paid for this once, between an ADR and
its published page — fixed by generating the page from the markdown rather than
maintaining both.
The catalog is consulted precisely when someone is about to touch a credential.
Being confidently wrong there is worse than being absent.
## Decision
**For any subsystem ops-warden does not own, a catalog entry carries identifiers
and pointers only** — `owner_repo`, `subsystem`, `wiki_ref`, `canon_ref`,
`need_keywords`, and the secret-free handoff metadata `warden access` needs.
**Authored procedure is permitted only where `warden_executes: true`.** A `steps:`
block and a `cert_command:` may exist on the SSH certificate lane and nowhere else,
because that is the one lane ops-warden actually owns. Rotation `steps:` are the
narrow exception and describe what the *owner* does, recorded because rotation
guidance had no other home; they are still pointers in spirit and must not grow
into a runnable substitute for the owner's tooling.
**No secret material in this file, ever.**
This is enforced, not merely documented. `tests/test_routing.py` fails any non-SSH
entry carrying a `steps` block, and checks that every `wiki_ref` anchor resolves to
a real section. A rule that is only written down is a rule that erodes.
## Consequences
**Accepted cost.** Two lookups instead of one. A worker who wants the procedure
follows the pointer. We consider a correct second hop cheaper than a stale first
one.
**Anchors must resolve.** Because the entry is only a pointer, a broken pointer is
a total failure rather than a cosmetic one. Hence the anchor test — which has
already caught a real break (`ADHOC-2026-08-11-T01`, a stale
`rapp-qonto-keycape-client` anchor).
**Other repos are bound by this.** When another repo asks us to add or rename a
lane, we add the pointer and decline to absorb the procedure. That has been
exercised: on 2026-08-11 railiance-platform asked ops-warden to rename an active
lane, and the answer was to cross-reference the id from their CCR rather than have
this repo carry a second identity for the same thing.
**It constrains what this repo may usefully become.** ops-warden cannot grow into
a documentation site for other people's credential procedures, however often that
is asked for. The value of the catalog is that a reader knows it points at truth
rather than at a copy of truth.
## Related
- `registry/routing/catalog.yaml` — the file this governs, header comment
- `wiki/AccessRouting.md` — the issue-vs-route role and boundary
- `ADR-0005` — the narrower charter this follows from

View file

@ -0,0 +1,90 @@
---
id: ops-warden-adr-0002
type: adr
title: "ADR-0002 — ops-warden is a transparent conduit, never a secret broker"
domain: infotech
repo: ops-warden
status: accepted
version: "1.0"
revision: "1"
owner: ops-warden
binds: "ops-warden"
created: "2026-06-26"
updated: "2026-08-18"
last_reviewed: "2026-08-18"
review_interval: 6m
enforced_by: "src/warden/access.py; wiki/OperatorAccessAssist.md"
supersedes: ""
successor: ""
---
# ADR-0002 — ops-warden is a transparent conduit, never a secret broker
## Status
Accepted. Decided during WARDEN-WP-0014 (operator access assist), tightened by
WARDEN-WP-0026 (disclosure hygiene).
## Context
`warden access` is the operator front door for every credential need in the estate.
For lanes marked `exec_capable` it does more than advise: it runs the owner's tool
and returns the value. Anything that fetches secrets on request looks like a broker,
and the gravity toward becoming one is strong — a broker is more convenient at every
individual call site.
The distinction is not stylistic. A broker holds authority; a conduit borrows the
caller's. Only one of those creates a new thing worth attacking.
## Decision
**ops-warden runs the owner's tool with the caller's own identity, and takes no
custody of the value.** The caller's credentials do the work. ops-warden holds
nothing after the command returns, stores nothing, and caches nothing.
**Forbidden: a standing broker.** ops-warden must not hold its own long-lived
secret-read credential in order to serve values to callers who could not have
fetched them themselves. If the caller lacks authority, the correct outcome is a
denial from the owner's system — not a fetch performed on their behalf by a more
privileged intermediary.
The test is a question: *could the caller have run this themselves?* If yes, we are
a conduit and may proxy. If no, proxying is privilege laundering and is refused.
**Owner-native front doors outrank the proxy.** Where an owner has shipped their own
exec surface — `secrets-engine exec`, the railiance-platform credential broker — we
route there and do not proxy. The proxy is a fallback for lanes nobody fronts yet,
not a preferred path. This is why `whynot-design-npm-publish` and
`ops-warden-warden-sign-token` are `native` rather than `interim`.
**The value must not land somewhere it will be logged.** Sanctioned transports are
`--out` (mode-0600 file), `--exec` (child process env), and `--wrap` (a single-use
OpenBao wrapping token). Streaming to a non-terminal stdout is refused without an
explicit `--unsafe-stdout`, which exists for interactive humans only.
## Consequences
**ops-warden never becomes a credential store, and gains no value by being
compromised beyond the SSH CA it already holds.** This is the whole point. An
attacker who owns ops-warden gets the SSH signing lane — serious, bounded, and
already the thing this repo is hardened around — not a key to every secret in the
estate.
**Some requests cannot be served, and that is the correct answer.** When a caller
lacks authority, ops-warden routes and explains rather than fetching. This reads as
unhelpfulness at the moment it happens; it is the property that makes the front door
safe to point every agent at.
**Every proxied fetch is auditable and attributable to the caller**, because it ran
as them. `audit.jsonl` records metadata only — never values, guarded in code.
**The `--unsafe-stdout` escape hatch is a known liability.** It exists because
humans in terminals legitimately need to see values. It is also exactly the shape of
the 2026-07-16 disclosure, where a value reached a captured stdout. `ADR-0004`
constrains it further for agent sessions.
## Related
- `wiki/OperatorAccessAssist.md#the-conduit-vs-broker-boundary-the-security-model`
- `ADR-0004` — the agent-session read boundary built on top of this
- `ADR-0003` — why proxied lanes are tracked as interim rather than owned

View file

@ -0,0 +1,95 @@
---
id: ops-warden-adr-0003
type: adr
title: "ADR-0003 — Cover gaps, but never silently own them"
domain: infotech
repo: ops-warden
status: accepted
version: "1.0"
revision: "1"
owner: ops-warden
binds: "ops-warden"
created: "2026-07-01"
updated: "2026-08-18"
last_reviewed: "2026-08-18"
review_interval: 6m
enforced_by: "registry/routing/catalog.yaml delegation:; warden route gaps"
supersedes: ""
successor: ""
---
# ADR-0003 — Cover gaps, but never silently own them
## Status
Accepted. Stated as INTENT §9, made structural by WARDEN-WP-0030 (delegation
register).
## Context
ops-warden owns exactly one lane: SSH certificate issuance. It nonetheless fronts
around eleven credential lanes as a caller-identity proxy, because no other
component fronts them yet and a worker blocked on a credential is a worker blocked.
Covering a gap is legitimate and this repo intends to keep doing it. The failure is
subtler: **a cover that is never recorded as a cover becomes ownership by default.**
Nobody decides to permanently own another component's lane. It happens because the
interim arrangement worked, nobody wrote down that it was interim, and the intended
owner never learned they were expected to build a front door.
By August 2026 the primitive to hand a lane back existed and was proven — `exec_owner`
/ `exec_command`, used by secrets-engine for npm publish and by the railiance-platform
credential broker for warden-sign — and was used by 2 of 24 lanes. The other
twenty-two had no record of who *should* own them.
## Decision
**Every catalog entry carries a `delegation:` block**, with a `mode:` of:
| `mode` | Meaning |
| --- | --- |
| `permanent` | Ours forever. SSH certificate issuance, and nothing else |
| `native` | The owner has a front door; we route to it and execute nothing |
| `interim` | We are covering a gap. Requires `intended_owner:` and `blocked_on:` |
**`interim` without an `intended_owner` is not permitted.** If we cannot name who
should own it, we have not understood the lane well enough to be fronting it.
**`blocked_on:` must name a specific condition, not a mood.** "No front door yet" is
not a blocker; "secrets-engine has not confirmed whether `exec --catalog` generalizes
over arbitrary OpenBao lanes (asked 2026-08-11, msg 7d55d332)" is. A blocker with a
question and a date can be chased. A blocker without one is an excuse with a
timestamp.
**The interim set is queryable**: `warden route gaps` lists it with review dates and
staleness. A cover that nobody can enumerate is a cover nobody will retire.
**A blocker is a claim about the world at a date, and expires.** `reviewed:` is
bumped only on a real re-check, never inherited. This was learned the hard way:
`RISK-F-0001` invalidated one of our blockers within a day and nothing would have
re-checked it.
## Consequences
**Retiring a cover is a normal, expected event rather than a renegotiation.** When
an owner ships their front door the lane flips `interim``native`. This has
happened twice and both were routine.
**Other repos can see what we are holding for them.** The register is why key-cape
and user-engine were able to confirm or decline lanes in August 2026 — the question
was answerable because it had been written down. One of those answers was "not ours",
which is a legitimate and useful outcome.
**We accept looking worse than we are.** `warden route gaps` publishes a list of
things this repo is doing that it would rather not be doing. That is the intent: the
alternative is a repo that looks clean because nobody counted.
**This register is not a risk register.** Interim lanes are tracked work with an
owner and a date, not defects. They do not get bulk-filed into `risk-nexus`, which
needs to stay small enough to read. Defects go there; gaps stay here.
## Related
- `INTENT.md` §9 — the principle this formalizes
- `history/2026-08-11-delegation-surface-assessment.md` — the assessment that forced it
- `.claude/rules/finding-routing.md` — the register-versus-findings boundary

View file

@ -0,0 +1,85 @@
---
id: ops-warden-adr-0004
type: adr
title: "ADR-0004 — High-risk lanes refuse raw value streaming to agent sessions"
domain: infotech
repo: ops-warden
status: accepted
version: "1.0"
revision: "1"
owner: ops-warden
binds: "ops-warden; any agent runtime calling warden access"
created: "2026-07-20"
updated: "2026-08-18"
last_reviewed: "2026-08-18"
review_interval: 6m
enforced_by: "src/warden/access.py (exit 7); OpenBao policy agent-high-risk-boundary"
supersedes: ""
successor: ""
---
# ADR-0004 — High-risk lanes refuse raw value streaming to agent sessions
## Status
Accepted. Decided during WARDEN-WP-0026 (credential disclosure hygiene), in
response to a real disclosure on 2026-07-16.
## Context
On 2026-07-16 a secret value reached a captured stdout. The mechanism was ordinary:
`bao kv get -field=X` in an agent session. Nothing was misconfigured and nobody
misused a tool. The value was read correctly, by an authorized caller, using the
documented command — and an agent session records its stdout, so the value landed in
a transcript that outlives the shell.
This is a structural mismatch, not a mistake to train away. Agent sessions are
logged by design; that is what makes them reviewable. A human at a terminal sees a
value and it scrolls away. An agent "seeing" a value writes it into a durable
context that may be stored, replayed, or sent to an inference provider.
Guidance alone will not fix it. The command is correct, it is in every runbook, and
the next agent that needs the value will reach for it.
## Decision
**When `WARDEN_AGENT_ID` is set and the catalog lane is `risk: high`, ops-warden
refuses to stream the raw value and exits 7.** The agent is not blocked from doing
its work — `--out`, `--exec`, `--wrap` and `--fingerprint` all remain available.
It is blocked from doing its work *in a way that writes the secret into a transcript*.
**The boundary is enforced at the credential store as well as at the CLI.** The
OpenBao policy `agent-high-risk-boundary` denies data-read on those paths for agent
tokens, allowing metadata and capabilities only. A control that lives solely in our
own CLI is a control that ends the moment someone calls `bao` directly.
**Verification must not require a read.** To check a lane, use
`bao token capabilities` — allow/deny — never a read of the value. This is the
specific habit the disclosure taught us to break.
**Exposure is reportable without reading.** `warden taint <catalog-id>` reports KV v2
`custom_metadata` (`exposed_at`, `exposed_version`) and touches no secret data.
## Consequences
**Agents can still do everything they could before, by a different route.** `--exec`
covers nearly every real case: the child process gets the value in its environment,
the agent never sees it. The friction is deliberate and small.
**Exit 7 is a contract other runtimes depend on.** It is a distinguishable code, not
a generic failure, so a caller can tell "refused by boundary" from "lane broken" and
retry correctly. Changing it is a breaking change to every agent runtime.
**`risk: high` becomes a load-bearing catalog field** rather than documentation.
Classifying a lane now changes runtime behaviour, so it must be set deliberately.
**We accept that `--unsafe-stdout` still exists for humans.** The boundary keys on
`WARDEN_AGENT_ID`, so an agent that does not set it is not caught. That is a known
limit: this ADR raises the floor for cooperating runtimes and hardens the store
behind them; it does not claim to stop a determined caller.
## Related
- `wiki/playbooks/agent-read-boundary.md`
- `wiki/playbooks/exposed-taint.md`
- `ADR-0002` — the conduit rule this narrows for agent callers

View file

@ -0,0 +1,79 @@
---
id: ops-warden-adr-0005
type: adr
title: "ADR-0005 — Implement one lane narrowly, route everything else"
domain: infotech
repo: ops-warden
status: accepted
version: "1.0"
revision: "1"
owner: ops-warden
binds: "ops-warden"
created: "2026-06-18"
updated: "2026-08-18"
last_reviewed: "2026-08-18"
review_interval: 6m
enforced_by: "SCOPE.md; registry/routing/catalog.yaml warden_executes"
supersedes: ""
successor: ""
---
# ADR-0005 — Implement one lane narrowly, route everything else
## Status
Accepted. The founding charter decision, taken 2026-06-18
(`history/2026-06-18-access-routing-intent-shift-assessment.md`).
## Context
ops-warden began as an SSH certificate manager. It then became the place workers
asked when they did not know where a credential came from — which is a real need,
and the obvious way to serve it is to start fetching credentials.
Down that path is a component that issues SSH certificates, vends API keys, brokers
tokens, and holds authority over all of them: a single point whose compromise is
total. NetKingdom's architecture deliberately separates identity (key-cape),
authorization (flex-auth), and secrets (OpenBao). A helpful front door that absorbed
all three would quietly undo that separation, one convenience at a time.
## Decision
**ops-warden executes exactly one lane with its own authority: SSH certificate
issuance for `adm`/`agt`/`atm` actors.** `warden_executes: true` appears on one
catalog entry and is expected to stay that way.
**For every other need it routes, and where the lane is `exec_capable` it may assist
by proxying as the caller** under `ADR-0002`. Routing is not a lesser service — it is
the service. Knowing which subsystem owns a need, and being right about it, is what
this repo sells.
**Scope growth is tested by ownership, not by usefulness.** "Would this be handy in
ops-warden?" is the wrong question and almost always answers yes. The right question
is "does ops-warden have the authority to own this, permanently?" If the answer is no,
the correct outcome is a pointer, or an `interim` cover recorded under `ADR-0003`.
## Consequences
**The blast radius stays bounded and known.** Compromising ops-warden yields the SSH
signing lane. That is worth defending well precisely because it is the only thing here.
**We say no to requests that would be easy to say yes to.** `warden secret`,
`warden login`, `warden bao`, `warden tunnel` do not exist and must not be invented;
the agent instructions name them as anti-patterns because agents keep reaching for
them. Each would be a day's work and a permanent widening.
**Being useful therefore depends on the pointers being right**, which is the whole
weight behind `ADR-0001`'s anchor enforcement and the catalog's review dates. A router
that routes wrongly is worse than no router.
**It leaves real gaps visible rather than filled.** Six workload lanes and three
tenant lanes are covered interim because secrets-engine and tenant-engine have not
shipped front doors. Under this ADR that is the correct state, tracked under
`ADR-0003`, and not a signal that ops-warden should absorb them.
## Related
- `SCOPE.md` — the issue-vs-route table
- `wiki/AccessRouting.md` — role and boundary
- `ADR-0001`, `ADR-0002`, `ADR-0003` — the three rules that follow from this one

View file

@ -0,0 +1,103 @@
---
id: ops-warden-adr-0006
type: adr
title: "ADR-0006 — Enforcement is zone-scoped, never a global flag"
domain: infotech
repo: ops-warden
status: superseded
version: "1.0"
revision: "1"
owner: ops-warden
binds: "ops-warden"
created: "2026-08-19"
updated: "2026-08-22"
last_reviewed: "2026-08-19"
review_interval: 6m
enforced_by: "warden.yaml policy.enabled; scripts/check_policy_caller_identity.py; zone-engine ZONE-WP-0001; WARDEN-WP-0032"
supersedes: ""
successor: "ops-warden-adr-0009"
---
# ADR-0006 — Enforcement is zone-scoped, never a global flag
## Status
Accepted 2026-08-19, at the moment `policy.enabled: true` was ready to be set
and deliberately was not.
## Context
WARDEN-WP-0031 finished the calling side of the flex-auth pre-sign gate. The
flex-auth pin `flex-auth-ops-warden` runs `callerAuth.mode: enforce`; ops-warden
presents a bound ServiceAccount token; the readiness gate exits 0 against the
enforcing pin and an anonymous `/v1/check` is 401. Everything needed to set
`policy.enabled: true` was in place.
`policy.enabled` is a **single boolean over the whole repo**. Combined with
`fail_closed: true` it makes flex-auth a hard dependency of *every* `warden
sign` — including the certificates the ops-bridge tunnels depend on, one of
which is the tunnel carrying the policy call itself. A dead tunnel or a
recreated Service does not degrade signing; it stops it.
That cost might be acceptable for a settled production lane. It is not
acceptable uniformly, because ops-warden signs across an estate that is being
actively rebuilt. During deep refactors — CoulombCore's decommission, the
issue-core move, cluster rebuilds — the same flag would harden exactly the
access needed to *perform* the refactor. Security that stops the work stops
being security and starts being an outage with good intentions.
The repo already refuses to treat posture as one-dimensional. WP-0015 shipped
environment posture (`dev` / `test` / `prod`) and workload maturity (`M0``M3`);
WP-0029 added `organization_posture: build` as a third axis precisely because
the *organization's* state changes what is reasonable to demand. A global
`policy.enabled` contradicts all of that: it is a fourth control that ignores
the three axes already declared.
## Decision
**Enforcement posture is a property of a zone, not of the repo.** ops-warden
does not enable a fail-closed authorization gate globally. Before
`policy.enabled: true` is set anywhere, the zones must exist: named bands of
differing rigidity, each declaring what is enforced, what is advisory, and what
is exempt — and the gate must be scoped to them.
Concretely, until `zone-engine`'s `ZONE-WP-0001` defines the zone model:
- `policy.enabled` stays `false`. Its readiness is evidence, not a mandate.
- A gate that is *ready* is recorded as ready. Readiness is not a reason to
enable; deferral with a stated reason is a legitimate terminal state for a
task, not an unfinished one.
- Any future enforcement control ships zone-aware or does not ship. A second
global boolean is the defect this record exists to prevent.
## Consequences
**We accept** that the pre-sign gate remains unexercised in production longer,
and that the WP-0031 evidence ages. Re-running
`scripts/check_policy_caller_identity.py` re-establishes it cheaply, and the
readiness gate exists precisely so this is a re-check rather than a re-do.
**We accept** that flex-auth's `flex-auth-ops-warden` pin sits enforcing with no
enforcing consumer. That is not waste: it makes the anonymous path 401 rather
than a decision, which was the ADHOC-2026-08-17-T01 condition regardless of
whether ops-warden calls it.
**We reject** the framing that a ready control should be turned on because it is
ready. The question is not "does it work" but "which zone is this, and does this
zone want this failure mode."
**This binds future work.** A zone-blind enforcement flag proposed in any
ops-warden workplan is out of order under this ADR, and should be sent back to
the zone model rather than merged with a caveat in its description.
## Related
- `WARDEN-WP-0031` — the calling side that made the flip possible (T05 deferred
under this ADR)
- `zone-engine` `ZONE-WP-0001` — the zone model this record defers to, seeded
2026-08-19 as the owning repo
- `WARDEN-WP-0032` — ops-warden's consumer-side adoption
- `wiki/WorkloadSecurityPosture.md` — the two axes already shipped (WP-0015)
- `wiki/PolicyGatedSigning.md` — the gate itself
- `history/2026-08-19-flex-auth-caller-identity-evidence.md` — readiness evidence
- flex-auth `FLEX-WP-0016` — the enforcing pin

View file

@ -0,0 +1,100 @@
---
id: ops-warden-adr-0007
type: adr
title: "ADR-0007 — Build-stage permissiveness stops at credential disclosure"
domain: infotech
repo: ops-warden
status: accepted
version: "1.0"
revision: "1"
owner: ops-warden
binds: "ops-warden"
created: "2026-08-19"
updated: "2026-08-19"
last_reviewed: "2026-08-19"
review_interval: 6m
enforced_by: "registry/routing/catalog.yaml risk grades; src/warden/cli.py agent read-boundary; WARDEN-WP-0032-T06"
supersedes: ""
successor: ""
---
# ADR-0007 — Build-stage permissiveness stops at credential disclosure
## Status
Accepted 2026-08-19, alongside grading the last 14 ungraded catalog lanes.
## Context
`ADR-0006` deferred a global fail-closed authorization gate because uniform
enforcement across an estate under deep refactor hardens the access needed to
perform the refactor. The organization's declared posture is `build`
(WP-0029), and the operator has confirmed the estate need not be tight yet.
That is correct, and it is also the kind of principle that quietly generalises
past its warrant. Read loosely, "we are in build stage" argues for relaxing
every control, including the ones that stop a credential landing in a logged
agent transcript. Those are not the same class of control, and the difference
is not severity — it is **cost**.
`RISK-F-0003` made the distinction concrete. `ADR-0004` reads as a categorical
rule: high-risk lanes refuse raw value streaming to agent sessions. The
implementation was `risk == "high"` against an **optional** field, so 14 of 27
lanes never reached the control at all — five of them `exec_capable`. The
control had not been relaxed by anyone's decision. It had simply never been
reached, which is worse, because nothing announced it.
## Decision
**Build-stage permissiveness applies to controls that gate work. It does not
apply to controls that prevent credential disclosure.**
The test is friction, not severity:
- A control that can **block a legitimate operation** — a fail-closed
authorization gate, an enforcement stance — is a candidate for relaxation
while the organization is in `build`, and `ADR-0006` scopes that relaxation
to zones.
- A control that **redirects how a value moves without preventing the work**
the agent read-boundary, which refuses raw stdout but leaves `--out`,
`--exec`, `--wrap` and `--fingerprint` fully available — is not relaxed by
build posture, because relaxing it buys nothing. Nobody is unblocked by it.
The asymmetry that settles it: a blocked operation is recovered by retrying.
A credential written into a logged transcript is not recovered by rotation —
rotation limits the damage, it does not unwrite the log. The 2026-07-16
disclosure is the case in point.
**Therefore, regardless of `organization_posture`:**
1. Every catalog lane carries an explicit `risk` grade. **Absence is not a
grade**, and a lane that omits it is a defect, not a default.
2. Grading is done on merit, per lane. This decision is not licence to grade
everything `high` — an over-broad grade is its own inaccuracy, and
`tenancy-posture` §6's *accuracy, not altitude* applies to this field too.
3. Minimum credential-handling standards — the read-boundary, the safe fetch
transports, the no-secret audit guard — hold in every posture.
## Consequences
**We accept** the grading cost, now and on every new lane. That is the point:
`WARDEN-WP-0032-T06` makes an ungraded lane impossible rather than merely
discouraged, because a rule enforced by remembering is not enforced.
**We reject** "build stage" as a general argument in credential-handling
discussions. It is a real and useful argument about *gating*, and citing it
against a disclosure control is a category error this record exists to name.
**We note what this decision is not.** It does not set severity for
`RISK-F-0003` — that is `risk-nexus`'s. It does not make ops-warden the judge of
other repos' controls. And it does not survive contact with a zone model that
says otherwise: when `zone-engine` defines admission standards, a zone may
legitimately require *more* than this floor. It may not require less.
## Related
- `ADR-0004` — high-risk lanes refuse raw value streaming to agent sessions
- `ADR-0006` — enforcement is zone-scoped, never a global flag
- `RISK-F-0003` — the read-boundary blind spot that prompted this
- `WARDEN-WP-0032-T05` / `T06` — the grading, and making absence impossible
- `zone-engine` `ZONE-WP-0001` — where admission standards will be defined

View file

@ -0,0 +1,94 @@
---
id: ops-warden-adr-0008
type: adr
title: "ADR-0008 — A lane's risk grade covers every field its path discloses"
domain: infotech
repo: ops-warden
status: accepted
version: "1.0"
revision: "1"
owner: ops-warden
binds: "ops-warden"
created: "2026-08-21"
updated: "2026-08-21"
last_reviewed: "2026-08-21"
review_interval: 6m
enforced_by: "registry/routing/catalog.yaml fields + risk; tests/test_routing.py::test_high_risk_lanes_classified"
supersedes: ""
successor: ""
---
# ADR-0008 — A lane's risk grade covers every field its path discloses
## Status
Accepted 2026-08-21, after `secrets-engine` found two under-graded lanes while
reviewing ops-warden's own catalog metadata.
## Context
`ADR-0007` requires every catalog lane to carry an explicit `risk` grade. It does
not say what the grade is *of*, and the omission turned out to matter.
The catalog describes a lane by a single `fetch_command` naming a single field —
`bao kv get -field=ISSUE_CORE_API_KEY <path>`. Grading followed that description.
But the unit of disclosure is not the field, it is the **path**: `bao kv get`
without `-field` returns every key stored there, and an agent session that
discloses one field has disclosed all of them.
On 2026-08-19, grading all 27 lanes, ops-warden graded
`issue-core-ingestion-api-key` and `reuse-surface-hub-write-token` as `standard`
— "ordinary internal workload secrets". Both grades read only the headline field.
`CCR-2026-0002` records a deliberate decision to keep `GITEA_BACKEND_TOKEN` at the
first path; `CCR-2026-0005` declares a dual-consumer webhook HMAC at the second.
Neither is recovered by rotating the credential the lane is named after.
Three details make this worth a record rather than a fix:
- **The evidence was already ours.** The field sets were in the CCRs the catalog
already cites as authoritative. This was not missing data; it was unread data.
- **A test held the error still.** `test_high_risk_lanes_classified` asserted
`issue-core-ingestion-api-key` was *not* high. A first grading pass had marked
it high, the test contradicted it, and the test was believed. A test that
encodes a judgement defends that judgement from correction.
- **Another repo found it.** `secrets-engine` graded both `high` independently
while drafting catalog entries whose schema records `fields`. A schema that
names the field set makes the right grade obvious; ours did not have one.
## Decision
**A lane's `risk` grade is a property of its path, and must cover the union of
everything a read of that path would disclose.**
1. Where the field set is known, the catalog records it as `fields`, with the
authority it came from.
2. The grade is argued against the most damaging field, not the named one.
3. Where the field set is unknown, that is stated — never assumed to be one
field. An unverified field set is a reason to grade conservatively, matching
the `inter-hub-bootstrap-ssh` precedent under `ADR-0007`.
4. Establishing a field set must not be done by reading the secret. Use the
owning CCR, the owner's catalog, or `bao kv metadata`. `bao kv get` on a
high-risk path is the 2026-07-16 vector and is forbidden by
`ADR-0004` for agent sessions regardless of intent.
## Consequences
`ADR-0007` is unchanged and still governs: every lane carries an explicit grade,
and absence fails safe. This record says what that grade must account for.
Grading gets more expensive: it now requires knowing what is at a path, not just
what the lane is called. That cost is the point — the cheap version produced two
wrong answers in one pass and is the reason this exists.
A test that asserts a grade is asserting a judgement. When a grade is disputed,
re-argue it from evidence before trusting the test that encodes it.
## Related
- `ADR-0007` — every lane carries an explicit grade; build-stage permissiveness
stops at credential disclosure
- `ADR-0004` — high-risk lanes refuse raw value streaming to agent sessions
- `ADR-0001` — the catalog is a pointer layer; `fields` records the owner's
declared field set with its source, and does not restate their procedure
- `WARDEN-WP-0033-T02`; `secrets-engine` `SECRETS-WP-0006`
- `history/2026-07-16-credential-disclosure-lessons.md`

View file

@ -0,0 +1,101 @@
---
id: ops-warden-adr-0009
type: adr
title: "ADR-0009 — Adopt security-zones v0.1 as a consumer"
domain: infotech
repo: ops-warden
status: accepted
version: "1.0"
revision: "1"
owner: ops-warden
binds: "ops-warden"
created: "2026-08-22"
updated: "2026-08-22"
last_reviewed: "2026-08-22"
review_interval: 3m
enforced_by: "tenancy.yaml; registry/routing/catalog.yaml workload_ref; scripts/build_flex_auth_registry.py; src/warden/policy.py; src/warden/config.py"
supersedes: "ops-warden-adr-0006"
successor: ""
---
# ADR-0009 — Adopt security-zones v0.1 as a consumer
## Status
Accepted 2026-08-22 after zone-engine completed `ZONE-WP-0001-T03/T05` and
published the declaration, compilation, stance, and failure-mode contract in
canon revision `337484a`; zone-engine's reference compiler is revision
`9b6ada7`.
## Context
ADR-0006 rejected a repo-wide `policy.enabled` switch because one boolean plus
one `fail_closed` value made flex-auth a uniform dependency of every signing
path, including continuity paths needed to repair that dependency. It deferred
the replacement to zone-engine rather than designing an estate model here.
The owning model now exists. A zone is an evidenced workload-admission fact;
control stance remains with the control owner, and dependency failure behavior
remains with the PEP. Membership resolves only through an authoritative
workload identity. Missing identity, membership, admission evidence, or a
required floor is `unknown`, never an inferred permissive zone.
## Decision
Ops-warden adopts `security-zones_v0.1` and accepts its initial build-stage rows
for the controls ops-warden owns:
- the pre-sign PEP fails open for `z0-experimental`, `z1-operational`,
`z2-protected`, `z2-continuity`, and build-profile `unknown`; it fails closed
for `z3-critical`;
- the agent high-risk read boundary remains enforced and fail-closed in every
zone and for `unknown`;
- `warden plan` never derives `autonomous` authority from unknown zone evidence.
The implementation follows four rules:
1. `policy.enabled` and the global `policy.fail_closed` setting are retired and
rejected by configuration loading. The PEP chooses failure behavior from a
total per-zone map.
2. The existing compiled flex-auth registry is the resource-membership carrier.
Actor resources receive `workload_id`, `security_zone`,
`security_zone_admission`, and `security_zone_revision`. The dormant
`trust_zone: platform` constant is removed; it is not repurposed.
3. Workload joins are explicit. Managed deployables use Repo Manager's exact
`(rapp_id, workload_identity.name, deployable?)` tuple. Independent
operational workloads use their owner-reviewed `tenancy.yaml`. Catalog
owners distinguish `not-applicable` from applicable-but-`unknown`; no path or
repository-name inference is allowed.
4. A fail-open signing result is metadata, not silence. Signature and unified
audit records carry the selected zone, failure mode, outcome, and decision id
when one exists.
Ops-warden itself declares `z1-operational`. That is an accuracy decision: the
workload has M1 evidence and does not yet have the SLO history, on-call rotation,
or exercised recovery evidence needed for z2 admission.
## Consequences
The global flip and its failure cycle no longer exist. An unknown target remains
observable and follows the versioned build profile without manufacturing
membership. A future organization-posture graduation changes the versioned
control profile, not each workload declaration.
The flex-auth policy package still owns pre-sign stance. Ops-warden can compile
and send the membership attributes, handle `allow`/`audit_only`/deny, and apply
the correct PEP failure mode; it does not write flex-auth's Rego rows.
Catalog coverage is intentionally honest at adoption: exact references resolve
where authoritative declarations exist, applicable lanes without one report
`unknown` with a reason, and generic actions/patterns are explicitly
`not-applicable`. Resolution coverage improves by adding owner declarations,
never by adding heuristics here.
## Related
- `security-zones_v0.1` (net-kingdom canon revision `337484a`; zone-engine
compiler revision `9b6ada7`)
- Repo Manager `helixforge.workloads.ops-warden-reference.v1` revision `890f3b0`
- NetKingdom tenancy-posture Decisions 5.6.1/5.6.2
- `WARDEN-WP-0032`
- `ADR-0004`, `ADR-0007`, and `ADR-0008`

View file

@ -0,0 +1,109 @@
---
id: ops-warden-adr-0010
type: adr
title: "ADR-0010 — ops-warden is Staff: lanes, not rules, and one declared engine gap"
domain: infotech
repo: ops-warden
status: accepted
version: "1.0"
revision: "1"
owner: ops-warden
binds: "ops-warden"
created: "2026-08-28"
updated: "2026-08-28"
last_reviewed: "2026-08-28"
review_interval: 3m
enforced_by: "INTENT.md layer declaration; docs/adr/ADR-0002; docs/adr/ADR-0003; docs/adr/ADR-0005; registry/routing/catalog.yaml delegation fields"
supersedes: ""
successor: ""
---
# ADR-0010 — ops-warden is Staff: lanes, not rules, and one declared engine gap
## Status
Accepted 2026-08-28, answering intake `WARDEN-IN-0001` from gate-house, which
carries decision `GH-DEC-2026-001`. The standard being adopted —
`net-kingdom/canon/standards/security-layer-model_v0.1.md` — is `proposed`, and was
proposed pending assent from flex-auth, kings-guard, and ops-warden. This ADR is
ops-warden's half of that assent.
## Context
The estate acquired overlapping claims to the same responsibility, most visibly two
repositories describing themselves as the authorization control plane. The layer
model resolves the overlap by layering repositories on determinism — Taxonomy,
Tooling, Engines, Staff — and by two rules: Staff never touches Tooling directly
(§5), and `access-engine` is the only policy decision point (§6).
ops-warden is assigned Staff. Two demarcations follow that touch this repository:
the security curriculum it had been carrying belongs to gate-house, and the words
*access lane* and *access rule* are bound to different owners.
Full reasoning: `history/2026-08-28-security-layer-model-assent.md`.
## Decision
**1. ops-warden is Staff and declares it.** `INTENT.md` carries the layer label and
the §5 invariant. ops-warden holds no state another layer depends on at runtime and
renders no authorization decision — it consumes them.
**2. Lanes, not rules.** ops-warden owns *how* a worker reaches a host: SSH
certificate issuance, the routing catalog, `warden access`, `warden plan`,
`cert_command`. It never owns *whether* a worker may — that is `access-engine`
(today `flex-auth`), and ops-warden neither renders nor caches that decision. This
restates what `ADR-0002` and `ADR-0005` already bind; it is recorded here because
the demarcation is now normative estate-wide and other repositories rely on
ops-warden holding to it. The ruled rename `flex-auth``access-engine` is assented
to; ops-warden asks only for a window in which both names resolve.
**3. Doctrine goes to gate-house; runbooks stay here.** ops-warden does not restate
security doctrine, the authority model, or the curriculum. It references
gate-house's. It keeps everything operational about the lanes it stewards: which
subsystem owns which need, how to obtain a credential lane by lane, and conformance
evidence for its own lanes. `.claude/rules/credential-routing.md` is runbook, not
curriculum, and stays inlined in this and every other repository.
**4. One declared engine gap, not an exemption.** `src/warden/vault.py` (`VaultCA`)
is a direct OpenBao client performing a write from a Staff repository. It is a §5
non-conformance. ops-warden declares it rather than arguing it away:
- **intended owner:** `secrets-engine` (credential abstraction, custody, lifecycle)
- **blocked on:** no engine exposes an SSH certificate signing surface
- **review:** with this ADR, every 3 months
Until that surface exists, ops-warden continues to sign — refusing to would remove
production host access to close a documentation gap — and reports the position as
open. `warden desk`'s `bao kv put` is declared on the same terms. `taint.py` is
metadata-only observation, declared under §5's read-only allowance. `proxy.py`
supplies no authority of its own: it runs the owner's tool under the caller's
identity and is governed by `ADR-0002`.
This is `ADR-0003` turned inward. ops-warden has required an intended owner and a
blocker on 27 catalog lanes it holds for other repositories; it holds itself to the
same record.
## Consequences
ops-warden's conformance under §10 is *declared non-conformant with a tracked
closure path*, not clean. That is the accurate state and it is the state that gets
fixed, because it names an owner who can fix it.
An amendment to §5 has been offered to gate-house — a second sanctioned shape
alongside read-only diagnostics: a declared engine gap carrying intended owner,
blocker, and review date, machine-readable so §10 can tell a tracked gap from an
undeclared violation. It is offered, not assumed; §5 stays gate-house's to write. If
gate-house declines it, ops-warden's position is a plain non-conformance and is
reported as one.
The `NetKingdom Security Literacy` section stops being a prose second source for
`registry/routing/catalog.yaml`, which `ADR-0001` had already ruled against for
catalog procedure.
## Related
- `net-kingdom/canon/standards/security-layer-model_v0.1.md` (proposed, gate-house)
- `gate-house/decisions/decisions.md``GH-DEC-2026-001`
- `history/2026-08-28-security-layer-model-assent.md`
- `ADR-0001`, `ADR-0002`, `ADR-0003`, `ADR-0005`, `ADR-0009`
- `WARDEN-IN-0001`

74
docs/adr/README.md Normal file
View file

@ -0,0 +1,74 @@
# ops-warden architecture decision records
This directory holds the rules **ops-warden owns** — the decisions this repo made,
is bound by, and is responsible for changing.
## Why these exist as ADRs rather than wiki prose
Until 2026-08-18 every rule in this list lived in wiki prose, a workplan, or a
comment at the top of `registry/routing/catalog.yaml`. All of them were being
followed. None of them was *addressable*: a reader outside ops-warden could not
cite one, could not tell whether it was current, and could not tell whether it
was ours to change or someone else's that we merely obey.
That distinction is the point of this directory. It matters in both directions:
- **A rule we own, mistaken for inherited canon, never gets fixed.** We wait for
an owner who does not exist.
- **Inherited canon, mistaken for ours, gets quietly bent.** We change something
we had no authority over, and the drift is invisible until it breaks a repo
that trusted the canonical version.
## Owned versus inherited
Every ADR here carries `owner:` in its frontmatter. It is the load-bearing field.
| `owner:` | Meaning | How it changes |
| --- | --- | --- |
| `ops-warden` | **Ours.** We decided it, we are bound by it, and we may change it | A new ADR that supersedes this one. Never an edit-in-place that rewrites a decision |
| anything else | **Inherited.** We follow it; we do not own it | Through that owner's process. We may dispute it — we may not amend it |
Everything currently in this directory is `owner: ops-warden`. Rules we follow but
do not own — NetKingdom canon, the IAM profile, the credential-management standard
— are *not* copied here. They are cited. Copying inherited canon into our own ADR
directory would recreate exactly the second-source-of-truth failure that
`ADR-0001` exists to prevent.
## Superseding one of these
A decision here changed the behaviour of other repos, so retracting it silently is
not available. Write a new ADR, set the old one's `status: superseded` and
`successor:`, and leave it in place. Superseded is a lifecycle state; deletion is
not. `policy-nexus` publishes the history, and a reader asking "what did this say
when we made that decision" must be able to find out.
## Relationship to `.claude/rules/`
`.claude/rules/*.md` are **agent-facing operational instructions**. They tell an
agent what to do in a session. They are derived from these ADRs and should cite
them rather than restate the reasoning. If the two disagree, the ADR is right and
the rule file is a defect.
## Publication
These are publishable through `policy-nexus` at `policy.coulomb.social`, which
requires `title`, `status` and `owner` on every document and renders Owner as a
column in its index. The ownership knowledge therefore survives publication
rather than being a local convention that evaporates at the repo boundary.
`policy-nexus` publishes; it never writes back. The file in this directory is the
source of truth. If the site and this directory disagree, this directory is right
and the publication is a defect.
| ADR | Rule | Binds |
| --- | --- | --- |
| `ADR-0001` | The routing catalog is a pointer layer, never a second copy of an owner's procedure | ops-warden, and every repo contributing a catalog entry |
| `ADR-0002` | ops-warden is a transparent conduit, never a secret broker | ops-warden |
| `ADR-0003` | Cover gaps, but never silently own them | ops-warden |
| `ADR-0004` | High-risk lanes refuse raw value streaming to agent sessions | ops-warden, and any agent runtime calling `warden access` |
| `ADR-0005` | Implement one lane narrowly, route everything else | ops-warden |
| `ADR-0006` | Enforcement is zone-scoped, never a global flag (**superseded by ADR-0009**) | ops-warden |
| `ADR-0007` | Build-stage permissiveness stops at credential disclosure | ops-warden |
| `ADR-0008` | A lane's risk grade covers every field its path discloses | ops-warden |
| `ADR-0009` | Adopt security-zones v0.1; compile explicit membership and select PEP failure mode per zone | ops-warden |
| `ADR-0010` | ops-warden is Staff: it owns access lanes, never access rules; the direct OpenBao client is a declared engine gap | ops-warden, and gate-house as the standard's owner |

View file

@ -0,0 +1,122 @@
# Credential governance break-glass contract
Status: active design contract for WARDEN-WP-0027-T02. This document does not
authorize a seal, re-key, token mint, policy write, workload restart, or host
reboot.
## Decision boundary
ops-warden consumes and verifies credential-control evidence; it does not own
the OpenBao cluster, unseal shares, recovery snapshots, policies, or root token.
`railiance-platform` is accountable for the OpenBao operation. The share
custodians, platform driver, abort operator, provider-console operator, and
affected workload owners participate through an attended approval window.
The current production trust-root is the rotated Shamir barrier restored to
railiance01 in RMASTER-WP-0020: three separately custodied shares with a
threshold of two. Share-holder identities and share material remain in the
approved out-of-band custody system, never Git, State Hub, shell history, logs,
or chat. A readiness receipt names participating *roles* and attests that two
distinct custodians are present; it never contains a share or recovery value.
Root is offline bootstrap/break-glass material only. It is not a substitute for
`platform-admin` OIDC and must not be used through the browser UI. An ordinary
unseal uses threshold shares and does not require a root token.
## Graded response
| Grade | Trigger | Action owner | Exit evidence |
| --- | --- | --- | --- |
| Observe | Suspicion without confirmed credential disclosure | lane owner + risk-nexus | Metadata-only taint, audit, policy, and capability review |
| Soft lockdown | Coding-agent access must stop while human recovery remains available | railiance-platform | `agent-high-risk-boundary` source/live equality; all concrete high-risk data paths denied; metadata readable |
| Lane containment | One or more concrete credentials are exposed | each credential owner | Front door disabled, provider/OpenBao rotation through the owner-native procedure, consumer cutover, taint cleared only after verification |
| Hard lockdown | OpenBao trust or control-plane integrity is in doubt | platform driver under attended approval | Fresh encrypted snapshot receipt, intentional seal, sealed-state proof, 2-of-3 unseal, post-unseal and consumer verification |
| Re-key | A Shamir share or the barrier custody model is compromised | platform owner + threshold custodians | Separately approved OpenBao re-key ceremony, new threshold custody attestation, old-share retirement, recovery drill |
Hard lockdown is not an agent command. The irreversible hold point is the
platform owner's attended seal action. All prerequisites below must be true
before that hold point; repository access alone grants no authority to cross it.
Route planning must select `openbao-shamir-recovery-ceremony` and return one
`founder_required` approval act. A result that selects `openbao-api-key`, asks
for paste-once provisioning, or offers any raw-value transport is a routing
failure and must not be executed.
## Pre-seal hold point
- An approved, bounded window names the accountable platform driver and a
distinct abort operator.
- Two distinct Shamir custodians attest availability through the approved
out-of-band channel.
- Independent provider-console access is verified by its owner.
- A current Raft snapshot is encrypted and stored off-host; the non-secret
receipt binds cluster id, Raft index, plaintext/encrypted hashes, location,
age, and verification without including protected material.
- Current OpenBao health, seal state, Raft peers, audit device, auth methods,
policy fingerprints, and consumer readiness are captured as metadata.
- A rollback/re-entry order and stop conditions are acknowledged by affected
workload owners.
Any missing or stale item aborts before sealing. A live incident may require
immediate network isolation, but that does not authorize improvising share or
root-token handling.
## Re-entry sequence
The platform owner performs the exact commands from
`railiance-platform/docs/openbao.md`; ops-warden does not copy a second
execution procedure here.
1. Confirm the instance is intentionally sealed and the approved window is
still open.
2. Two custodians supply shares through hidden, non-logged prompts. No agent
observes the values.
3. Prove `initialized=true` and `sealed=false`, then run the owner post-unseal
verification.
4. Verify Raft, persistent audit output, OIDC metadata, SSH roles, and policy
fingerprints before restoring normal access.
5. Reconcile ExternalSecret stores and bounded consumers in dependency order.
6. Run value-safe capability probes, including the coding-agent deny-wins check
and an ops-warden signing smoke that records only backend and decision ids.
7. Revoke temporary operator tokens and close the window with non-secret
timing, status, and abort/rollback evidence.
The August 3 migration already proved two attended restart/unseal cycles under
the same 2-of-3 barrier. T02 still requires one current emergency-seal drill
against the authoritative runtime because restore rehearsal and intentional
production lockdown are different claims.
## Warden signing recovery
Do not apply the parked standalone `warden-sign` AppRole as a T02 break-glass
path.
- Normal operation uses the railiance-platform credential broker and short-lived
`warden-sign` child token.
- Broker/issuer recovery uses an attended OIDC platform operation.
- A sealed or integrity-compromised OpenBao is recovered by the Shamir
trust-root; another AppRole cannot bypass the seal.
- Placing standing AppRole material outside the broker expands credential
custody without improving root recovery.
The AppRole dry-run in SECRETS-WP-0004 remains useful negative evidence: its
capability set is narrow. It is not apply authorization. A separately approved
ops-bridge unattended-signing design may re-evaluate it under its own workplan;
that service-access question is outside T02 and does not create recovery
authority.
## Evidence and owner surfaces
- Current barrier/re-entry evidence: `railiance-master` RMASTER-WP-0020 and
`docs/evidence/openbao-isolated-restore-2026-08-03.json`
- Authoritative execution and recovery runbook:
`railiance-platform/docs/openbao.md`
- Current coordinated recovery gates:
`railiance-platform/docs/railiance01-coordinated-reboot.md`
- Emergency evidence contract and validator:
`railiance-platform/docs/openbao-emergency-drill-evidence.example.json` and
`make openbao-validate-emergency-evidence`
- Soft-lockdown policy and proof: railiance-platform RAILIANCE-WP-0022 and
ops-warden `scripts/check_agent_read_boundary.py`
- Warden-sign recovery input: secrets-engine SECRETS-WP-0004 and
`workplans/ADHOC-2026-08-11.md` T03

View file

@ -0,0 +1,37 @@
{
"interface": "railiance.attended-login-containment-receipt",
"version": 1,
"task_id": "RAILIANCE-WP-0026-T01",
"owner": "ops-warden",
"source_repo": "ops-warden",
"source_revision": "0fae0904ce8d8694338dd53a8a79abec5fec788d",
"created_at": "2026-08-22T23:31:17Z",
"disposition": "ready_for_owner_review",
"focused_test": {
"command": "uv run pytest -q tests/test_proxy.py tests/test_plan.py",
"passed": true,
"passed_count": 42,
"failed_count": 0
},
"repository_verification": {
"command": "uv run pytest -q",
"passed": true,
"passed_count": 390,
"deselected_count": 4,
"lint_command": "uv run ruff check .",
"lint_passed": true
},
"acceptance_outcomes": {
"private_helper_preflight_before_auth": true,
"read_only_home_refused_before_oidc": true,
"login_child_and_revocation_stdio_contained": true,
"unexpected_stdout_and_stderr_fail_closed": true,
"possible_issuance_triggers_contained_self_revocation": true,
"helper_cleanup_is_deterministic": true,
"persistent_login_only_handoff_refused": true
},
"live_oidc_performed": false,
"live_drill_authorized": false,
"secret_values_observed": false,
"sensitive_material_recorded": false
}

View file

@ -0,0 +1,73 @@
# WARDEN-WP-0027-T02 attended drill preparation checklist
Status: `preparing``authorizes_execution: false`.
## Immutable scenario basis
- Scenario: `WARDEN-WP-0027-T02-DRILL-20260822-01`
- Scenario artifact:
`docs/evidence/WARDEN-WP-0027-T02-drill-scenario-2026-08-22.md`
- Scenario SHA-256:
`ffa69764ad391db633f57ac70444e5781eee698bf9e362dfef31614fa43152dc`
- Preparation approval decision:
`9da57559-712a-4521-b46e-a4c69729f9d2`
- Preparation approved at: `2026-08-22T21:43:11Z`
- Scenario expires: `2026-08-23T20:00:00Z`
- Maximum live duration after a later exact GO: 45 minutes
The scenario artifact is intentionally unchanged after railiance-infra approved
its pinned digest. This checklist records later preparation evidence without
invalidating that receipt.
## Exact prepared live scope
The only prepared live sequence is one intentional OpenBao seal followed by the
existing attended 2-of-3 Shamir unseal ceremony and value-safe post-unseal
verification.
Preparation and owner receipts authorize no live action. The scope excludes a
host reboot, re-key, snapshot restore, policy change, PVC mutation, credential
disclosure, general workload restart, and every action not named above.
## Owner review gates
| Gate | Contract | State |
| --- | --- | --- |
| Independent provider console and distinct abort authority | `WARDEN-WP-0027-T02-DRILL-20260822-01-INFRA` | satisfied; receipt `01a02b4b-7295-7836-b288-f29407008524` |
| Fresh encrypted, verified, off-host Raft snapshot and platform driver acceptance | `WARDEN-WP-0027-T02-DRILL-20260822-01-PLATFORM` | pending |
| Two distinct custodians available for the current 2-of-3 barrier | `WARDEN-WP-0027-T02-DRILL-20260822-01-QUORUM` | pending |
Every receipt is metadata-only. No receipt may include a provider credential,
OpenBao token, unseal share, recovery value, decrypted snapshot, custody
location, custodian identity, or value-derived fingerprint.
## Final read-only preflight
After both pending contracts are satisfied, ops-warden runs the platform-owned
`scripts/audit-core-recovery-preflight.py node-reboot` interface with:
- approved window id `WARDEN-WP-0027-T02-DRILL-20260822-01`;
- the platform owner's current snapshot evidence file;
- the railiance-master quorum attestation;
- the accepted railiance-infra provider-console and abort role;
- the existing RAILIANCE-WP-0024 procedure-owner acknowledgements.
The interface name reflects its superset recovery checklist; it does not add a
reboot to this scenario. Its result must report all of:
- `preflight_only: true`;
- `automated_checks_passed: true`;
- `ready_for_live_execution: true`;
- `secret_values_observed: false`.
Any changed cluster identity, invalid/stale snapshot receipt, missing owner
receipt, overlapping mutation, failed automated check, observed secret value,
or expired scenario is a NO-GO.
## Final human hold point
Only after the green preflight may ops-warden ask:
`GO WARDEN-WP-0027-T02-DRILL-20260822-01?`
No prior approval or conversational “go” crosses this hold point.

View file

@ -0,0 +1,86 @@
# WARDEN-WP-0027-T02 attended drill scenario
Status: `preparing` — live execution is prohibited.
## Window
- Scenario/window id: `WARDEN-WP-0027-T02-DRILL-20260822-01`
- Preparation approval: Warden Desk `approve` recorded at
`2026-08-22T19:22:28Z` (metadata only)
- Approval expires: `2026-08-23T20:00:00Z`
- Live window opens only when the operator gives the exact final go/no-go for
this scenario after the owner preflight reports
`ready_for_live_execution: true`
- Maximum live-window duration after GO: 45 minutes
- A NO-GO, missing gate, changed cluster identity, or expired approval closes
this scenario without mutation
## Assigned roles
| Responsibility | Assigned owner | Acceptance |
| --- | --- | --- |
| Preparation coordinator and hold-point enforcement | `ops-warden` | accepted |
| OpenBao snapshot, seal/unseal driver, post-unseal verification | `railiance-platform` | pending owner receipt |
| Independent provider console and distinct abort authority | `railiance-infra` | pending owner receipt |
| Two distinct 2-of-3 share custodians available out of band | `railiance-master` custody authority | pending quorum receipt |
| Final live GO or NO-GO | human operator | deliberately not requested yet |
Owner procedure approval is already complete for `audit-core`,
`rapp-postgres`, `railiance-platform`, `railiance-cluster`, and
`railiance-infra` under the RAILIANCE-WP-0024 contract. Those receipts approve
the procedure, not this live window.
## Current value-safe baseline
The `railiance-platform` node-reboot preflight at `2026-08-22T19:23:55Z`
reported:
- `automated_checks_passed: true`
- one Ready node with active k3s
- `platform-pg` healthy 1/1 with continuous archiving and a successful backup
17.15 hours old
- OpenBao initialized and unsealed, Shamir `shares=3`, `threshold=2`
- required ExternalSecret stores Valid and projections SecretSynced
- audit-core at the reviewed digest, 1/1 Ready, zero restarts
- `secret_values_observed: false`
This baseline is not reusable as the final hold-point result. The platform
owner must rerun it against the current state and fresh snapshot receipt.
## Fail-closed preparation probes
- The OpenBao pod token helper and the current workstation caller token both
receive `403 permission denied` for a capabilities check on
`sys/storage/raft/snapshot`. No snapshot command was attempted after that
denial. The platform driver must use its attended owner identity; the agent
will not widen a workload token or substitute root.
- Warden has no autonomous provider-console catalog lane. Independent console
access therefore remains an explicit `railiance-infra` owner attestation,
not an inferred result from SSH reachability.
- The live barrier reports three shares and threshold two, but state metadata
cannot prove two custodians are currently present. Availability must arrive
through the out-of-band custody authority without identities or values.
## Pending receipts
- [ ] `railiance-platform`: fresh encrypted, verified, off-host OpenBao Raft
snapshot receipt matching the live cluster id and possible applied index;
acceptance of the driver role
- [ ] `railiance-infra`: independent provider-console access verified;
acceptance of the distinct abort role
- [ ] `railiance-master`: two distinct share custodians explicitly available
through the approved out-of-band custody paths; no identities or share values
in the receipt
- [ ] Fully parameterized read-only owner preflight returns
`ready_for_live_execution: true` and `secret_values_observed: false`
## Final hold point
Once all pending receipts validate, `ops-warden` presents only the scenario id,
bounded duration, owner roles, current preflight result, and stop conditions to
the human operator. The live step requires an explicit `GO` for this exact
scenario. Any other response is NO-GO.
No unseal share, token, provider credential, Secret data, decrypted snapshot,
or value-derived fingerprint belongs in Git, State Hub, shell history, logs, or
chat.

View file

@ -0,0 +1,37 @@
# Ops-warden security-zone admission evidence — 2026-08-22
This record supports the `z1-operational` membership declared in
`tenancy.yaml`. It does not claim the M2 gates that ops-warden has not met.
## Identity and scope
- Workload id: `ops-warden`.
- Runtime binding: Kubernetes ServiceAccount
`system:serviceaccount:ops-warden:ops-warden`, issued by railiance01 and
verified against the enforcing flex-auth pin on 2026-08-19.
- Responsible party: `team:platform-security` in this repository.
- Scope: attended issuance of short-lived SSH certificates plus a pointer-only
credential catalog. Secret values are not stored in the catalog or audit.
## M1 evidence
- Owned front door: `warden sign` is the sole certificate-issuance interface;
actor inventory, principal allow-list, and TTL ceilings are enforced before
the CA backend.
- Basic service objective: production signing is bounded by the actor TTL
policy (`adm` 48h, `agt` 24h, `atm` 8h); `warden status` and the production
verification records expose backend readiness.
- Data handling: `ADR-0002` makes ops-warden a transparent conduit and
`ADR-0004`/`ADR-0007` prevent raw agent reads and fail safe on ungraded lanes.
- Policy path: `history/2026-08-19-flex-auth-caller-identity-evidence.md` proves
the authenticated caller path and anonymous rejection. At adoption, the
migrated real operator config reran the check successfully through the
existing tunnel: HTTP 200, effect `allow`, decision
`decision:f3f7c88f9585582a`.
## Why not z2
Ops-warden has security review artifacts, but not the complete M2 promotion
set: there is no SLO history, on-call rotation, or exercised signing-path
incident/recovery runbook. Its tenancy posture therefore remains V0 and its
accurate zone membership remains `z1-operational`.

View file

@ -10,6 +10,9 @@ actors:
- agt-task-bridge
ttl_hours: 24
description: "ops-bridge tunnel agent for state-hub"
zone_subject:
applicability: applicable
workload_id: ops-bridge-tunnel
agt-codex-interhub-bootstrap:
type: agt
@ -17,6 +20,9 @@ actors:
- agt-interhub-bootstrap
ttl_hours: 2
description: "Short-lived agent access for attended Inter-Hub bootstrap"
zone_subject:
applicability: applicable
workload_id: codex-interhub-bootstrap
adm-example:
type: adm
@ -24,6 +30,9 @@ actors:
- adm-full
ttl_hours: 48
description: "Example human operator — replace with per-person adm-* actors"
zone_subject:
applicability: not-applicable
reason: human operator retains native actor identity
atm-backup-daily:
type: atm
@ -31,6 +40,9 @@ actors:
- atm-backup-daily
ttl_hours: 8
description: "Example nightly automation actor"
zone_subject:
applicability: applicable
workload_id: backup-daily
hosts:
example-host:

View file

@ -0,0 +1,20 @@
environments:
prod:
backend: openbao-sealed-shamir
real_values: generated-fresh-no-reuse
unseal: shamir-3-of-5-break-glass
workloads:
- id: rapp-qonto
env_posture: prod
maturity: M3
secret_requests:
- secret: binky-qonto-api
to_workload: rapp-qonto
required_maturity: M3
dataclass: restricted
- secret: rapp-qonto-keycape-client
to_workload: rapp-qonto
required_maturity: M3
dataclass: confidential

View file

@ -15,13 +15,39 @@ vault:
inventory_path: ~/.config/warden/inventory.yaml
state_dir: ~/.local/state/warden
# Opt-in flex-auth gate — enable only when flex-auth is reachable at flex_auth_url.
# Zone-aware flex-auth gate. Missing target membership is the explicit unknown
# profile; there is no repo-wide enable switch.
# Registry: registry/flex-auth/production_registry_snapshot.json (build from inventory).
# See wiki/PolicyGatedSigning.md (operator checklist) and wiki/playbooks/operator-openbao-token-hygiene.md
policy:
enabled: false
flex_auth_url: http://flex-auth.flex-auth.svc.cluster.local:8080
fail_closed: true
# The in-cluster pin for ops-warden's signing policy (FLEX-WP-0016). A bare
# flex-auth.flex-auth.svc Service does not exist. From a workstation, reach it
# through a port-forward or tunnel and point this at that local address.
flex_auth_url: http://flex-auth-ops-warden.flex-auth.svc.cluster.local:8080
zone_registry_path: registry/flex-auth/production_registry_snapshot.json
failure_modes:
z0-experimental: fail_open
z1-operational: fail_open
z2-protected: fail_open
z2-continuity: fail_open
z3-critical: fail_closed
unknown: fail_open
not-applicable: fail_closed
tenant: tenant:platform
subject_env: WARDEN_POLICY_SUBJECT
system: ops-warden
# How ops-warden proves it is ops-warden. flex-auth TokenReviews this bearer
# token and requires the principal system:serviceaccount:ops-warden:ops-warden
# for resource.system: ops-warden. Mode none sends no header, which is what
# holds the pin in warn.
caller_auth:
mode: none # none | file | env | command
# In-cluster PEP — projected ServiceAccount token, audience-bound:
# mode: file
# token_path: /var/run/secrets/flex-auth/token
# Workstation — mint a short-lived bound token per call:
# mode: command
# command: kubectl create token ops-warden -n ops-warden
# --audience flex-auth --duration 10m
token_env: WARDEN_POLICY_CALLER_TOKEN
audience: flex-auth

View file

@ -0,0 +1,57 @@
# Credential disclosure lessons — 2026-07-16
**Context:** buildup mode. Exposure was accepted; the value here is the learnings,
not blame. Rotation of the exposed values is the operator's optional call, not a
blocker (see WP-0026 T07).
## What happened
While verifying `CCR-2026-0004` (railiance offsite backup lane), a negative policy
test was run as:
```bash
BAO_TOKEN=$(bao token create -policy=default -field=token) bao kv get <path>
```
The `bao token create` was **denied** (the workload role lacks it), so `BAO_TOKEN`
was left unset and `bao kv get` fell back to the caller's **privileged login
token**. The read succeeded and printed all three field values —
`NC_WEBDAV_TOKEN`, `NC_WEBDAV_URL`, `AGE_PRIVATE_KEY` — into an agent session
transcript (a logged context).
## Root causes
1. **The deny-test read the secret data path at all.** A negative test should
prove *deny*, and proving deny never requires reading the value.
2. **Silent privileged-token fallback.** When the scoped token creation failed,
the command quietly used the caller's privileged token instead of failing.
3. **The read landed in a logged context.** An agent session transcript is not a
safe sink for secret material.
## Corrections (WARDEN-WP-0026, Strand A)
- **Verification never reads secret data.** Prove allow/deny with
`bao token capabilities`, not `bao kv get`. If `bao token create -policy=default`
is itself denied, that is a *pass* for the deny direction — never fall back to a
privileged token. Canonical pattern:
`wiki/playbooks/catalog-lane-promotion.md#capabilities-safe-lane-verification`
(WP-0026 T01, applied to the forgejo and railiance-backup lane playbooks).
- **Safe transport** for values that must move: env var, file, or response-wrapping
token (`-wrap-ttl`) — never a stdout table (WP-0026 T02).
- **Masking** as defense-in-depth in the warden wrapper (WP-0026 T03).
- **Agent read-boundary + EXPOSED taint** on high-risk lanes, and per-lane
**rotation guidance** (WP-0026 T04T06).
## Deferred (Strand B — WARDEN-WP-0027)
Executable one-command mass rotation, graded lockdown / break-glass with a designed
trust-root, and tamper-evident policy governance + reconcile are captured in
`WARDEN-WP-0027` (backlog, gated on an activation trigger).
## References
- `WARDEN-WP-0026` — disclosure hygiene (Strand A)
- `WARDEN-WP-0027` — governance/lockdown (Strand B, deferred)
- `CCR-2026-0004-railiance-backup-offsite-lane.yaml` (railiance-platform)
- `wiki/playbooks/railiance-backup-offsite-lane.md`
- `.claude/rules/credential-routing.md`

View file

@ -0,0 +1,104 @@
# Delegation surface assessment — 2026-08-11
**Trigger:** founder directive — ops-warden should work with, but never replace or
duplicate, secrets-engine, tenant-engine, user-engine and other NetKingdom
components. Covering an unfilled gap is acceptable if the gap stays visible, gets
filled with proper governance, and ops-warden then delegates.
**Method:** enumerate `registry/routing/catalog.yaml` by execution mode; check
`wiki/AccessRouting.md` and `wiki/playbooks/catalog-lane-promotion.md` for existing
interim/delegation doctrine; compare against SCOPE/INTENT claims.
---
## 1. Execution surface (24 catalog entries)
| Mode | Count | Entries |
| --- | --- | --- |
| `warden_executes: true` — ops-warden's own authority | 1 | `ssh-cert-host-access` |
| `exec_owner:` set — delegated, route-primary/proxy-fallback | 2 | `whynot-design-npm-publish` (secrets-engine), `ops-warden-warden-sign-token` (railiance-platform broker) |
| `exec_capable` proxy, **no** `exec_owner` | 11 | `openbao-api-key`, `key-cape-oidc-login`, `issue-core-ingestion-api-key`, `reuse-surface-hub-write-token`, `openrouter-llm-connect`, `railiance-backup-offsite-lane`, `forgejo-admin-api-token`, `binky-company-email-imap`, `binky-qonto-api`, `rapp-qonto-keycape-client`, `agent-harness-forgejo-deploy` |
| route-only pointer | 10 | remainder |
**Finding.** The delegation primitive exists, is proven in production, and is used by
**2 of 24** lanes. Eleven lanes have ops-warden as the de facto front door with no
record of who should own it instead.
## 2. The doctrine is not written down
- `wiki/AccessRouting.md` — the canonical "what ops-warden answers" page — contains
**no mention of secrets-engine** and no section on interim positions. A worker or
agent reading it cannot tell that `warden access` proxying a workload secret is a
stopgap rather than the design.
- `wiki/playbooks/catalog-lane-promotion.md` gates draft→active on the lane
*working* (zero placeholders, resolvable, tests green). It never asks whether
ops-warden should be the one executing it.
- The delegation intent for `whynot-design-npm-publish` lives in WP-0019 prose and
SCOPE; the *machine-readable* expression (`exec_owner`) was a by-product, not a
policy applied catalog-wide.
Consequence: an absorbed need is indistinguishable from a designed one. Ownership
drift is invisible by construction, which is exactly the failure mode the directive
targets.
## 3. Classification of the eleven (revised on inspection, founder review 2026-08-11)
A first draft sorted by *subsystem* — tenant lanes to tenant-engine, workload lanes to
secrets-engine. Reading the entries showed that is the wrong axis. Nine of the eleven
share one `auth_method` ("caller's own OpenBao token" via operator OIDC or a
`workload-kv-read-*` policy) and one `fetch_command` shape
(`bao kv get -field=X <path>`). No owner procedure is duplicated in those. Contrast
`whynot-design-npm-publish` — npm config plus token injection into a specific tool, a
*procedure*, which is why WP-0019 handed it to secrets-engine.
**Test applied: owner-specific procedure or lifecycle → interim. Generic KV read →
thin wrapper, arguably permanent.**
### Interim (5) — classified
| Lane | Intended owner | Blocked on |
| --- | --- | --- |
| `rapp-qonto-keycape-client` | key-cape | `client_secret_basic` exchange is a key-cape procedure; rotation already `automatable: true` |
| `binky-company-email-imap` | tenant-engine | `tenants/binky/...` custody, rotation owned by `binky-control` — split lifecycle |
| `binky-qonto-api` | tenant-engine | Same split |
| `railiance-backup-offsite-lane` | railiance-platform | Rotation is `re-establish`, a procedure ops-warden only describes |
| `agent-harness-forgejo-deploy` | railiance-platform / agent-harness | `re-establish` + alternative host-local key path |
### Held (6) — pending secrets-engine
`openbao-api-key`, `key-cape-oidc-login`, `issue-core-ingestion-api-key`,
`reuse-surface-hub-write-token`, `openrouter-llm-connect`, `forgejo-admin-api-token`.
Permanent **only if** `secrets-engine exec` stays per-lane and provisioned. If it
generalizes over arbitrary OpenBao lanes, all six become interim with secrets-engine as
intended owner. Asked directly (msg `7d55d332`). For three of them
(`issue-core-*`, `reuse-surface-*`, `openrouter-llm-connect`) production never touches
the proxy at all — External Secrets delivers the value and the proxy serves operator
verification only, which weakens the case that a front door is missing.
**No lane names `user-engine` as owner.** It appears only as a *consumer* inside
`coulomb-social-runtime-env` (route-only, owned by railiance-platform), whose rotation
guidance points at the OpenBao path `user-engine/user-engine-runtime` for
`USER_ENGINE_PROXY_SECRET`. So a user-engine runtime secret exists and is routed, but
user-engine fronts nothing itself. Whether it should own that lane is worth confirming
rather than assuming either way.
## 4. Secondary finding — SCOPE drift
`SCOPE.md` "Where we are" was dated **2026-07-01** and stated *"Active work: none
open in ops-warden after WP-0022/0023."* Six workplans have shipped since
(WP-00240026, WP-0028, WP-0029 finished; WP-0027 sits in `backlog`), adding
`warden plan`, `warden desk`, `warden taint`, `warden rotate-guide`, experiential
memory, the tenant custody pattern, and the build-phase organization posture axis.
SCOPE understated the repo by roughly six weeks of work. Corrected in this pass.
## 5. Recommendation
WARDEN-WP-0030 — record `delegation:` (mode / intended_owner / blocked_on) on every
entry, expose `warden route gaps`, gate promotion on the ownership question, and
publish the resulting interim register to the owner repos. Absence of a delegation
block should read as *interim, owner unknown* — a question — never as settled
ownership.
The measure of success is not fewer proxies. It is that no proxy exists without an
answer to *"who should own this front door, and what is missing?"*

View file

@ -0,0 +1,92 @@
# flex-auth caller identity — live evidence (WARDEN-WP-0031 T04)
**Date:** 2026-08-19
**Pin:** `flex-auth-ops-warden`, railiance01 cluster, namespace `flex-auth`,
Service `flex-auth-ops-warden:8080`, digest `sha256:138aa347…`, running
`--caller-auth-mode warn --caller-kubernetes-url https://10.43.0.1
--caller-binding ops-warden=system:serviceaccount:ops-warden:ops-warden`
(read off the live Deployment, matching FLEX-WP-0016 T02).
Reached from the workstation by port-forward; the tunnel `k3s-api-railiance01`
(local `16444`) carries the API. Use `--kubeconfig ~/.kube/config-railiance01`.
`~/.kube/config` / `config-hosteurope` — which `.bashrc` exports as `KUBECONFIG`
— points at `16443`, and that was **CoulombCore's** k3s API, a different cluster
whose client CA does not know this cert. Hence `Unauthorized`. CoulombCore is
being retired and that tunnel was removed on 2026-08-19, so the port is simply
gone now; `KUBECONFIG` was repointed at `config-railiance01`.
*(An earlier revision of this file blamed a local-port collision between
`k3s-api-coulombcore` and `k3s-api-haskelseed`. That was wrong: the haskelseed
tunnel is a **reverse** forward, where `local_port` is the destination on this
workstation rather than a listener, so the two never competed for the port. The
correction is recorded here because the wrong reason was also sent to flex-auth
and would have misdirected whoever followed the handoff.)*
## Baseline — before
Unauthenticated `POST /v1/check` was **served**, and the pin logged:
```
caller authentication warning: caller is not authenticated
```
That is the whole reason `policy.enabled` could not flip: warn mode answers
anonymous callers, so nothing about the enforcing path was ever exercised.
## What was created
`deploy/kubernetes/caller-identity.yaml` — Namespace `ops-warden` and
ServiceAccount `ops-warden/ops-warden`, `automountServiceAccountToken: false`,
**no RBAC of any kind**. It is never used to call the Kubernetes API; it exists
only to be the subject of flex-auth's TokenReview. Applied 2026-08-19.
Cluster resources are railiance-platform's to own — this is an ADR-0003 interim
cover, and the manifest names that owner in its header.
## Token source
`policy.caller_auth.mode: command` in `~/.config/warden/warden.yaml`:
```
kubectl --kubeconfig ~/.kube/config-railiance01 create token ops-warden \
-n ops-warden --audience flex-auth --duration 10m
```
Audience `flex-auth` is required: `internal/callerauth/tokenreview.go` sends
`spec.audiences: ["flex-auth"]` and rejects an identity whose audiences do not
contain it. 900-char bound token, 10 minute TTL, minted per call, never stored.
## Evidence
```
$ python3 scripts/check_policy_caller_identity.py --url http://127.0.0.1:19090
✓ warden.yaml: loaded; policy.enabled=false
✓ caller_auth.mode: command
✓ caller token: obtained, 900 chars, sha256:e50da3ec6769
✓ live /v1/check: HTTP 200, effect=allow, decision=decision:f3f7c88f9585582a
READY
```
The decisive check is not that allow — warn would have allowed an anonymous
caller too. It is the warning count:
```
warnings before: 4
warnings after 2 authenticated gate runs: 4
```
The pin authenticated the caller and had nothing to warn about. That is the
condition ADHOC-2026-08-17-T01 required before `policy.enabled` may flip
anywhere.
## What is still open
`policy.enabled` stays **false**. The remaining sequence (T05) is flex-auth's
move first: `callerAuth.mode: enforce` on this pin (their FLEX-WP-0016 T03),
re-run the gate against the enforcing pin, then `policy.enabled: true` with
`fail_closed: true`, then an end-to-end `warden sign` — which additionally needs
a scoped `VAULT_TOKEN` via `ops-warden-warden-sign-token`.
Flipping before enforce buys nothing; flipping before this task would have
401'd every `warden sign`.

View file

@ -0,0 +1,171 @@
# Security layer model — ops-warden's assent (WARDEN-IN-0001)
**Date:** 2026-08-28
**Intake:** `WARDEN-IN-0001`
**Requested by:** gate-house, ratified as `GH-DEC-2026-001`
**Standard:** `net-kingdom/canon/standards/security-layer-model_v0.1.md` (proposed)
**Outcome:** assent to all three items; one declared non-conformance and one
proposed amendment to the standard.
---
## What was asked
gate-house asked ops-warden to assent to three boundary items:
1. **ops-warden is Staff**, bound by §5 — Staff acts only through Engine APIs and
never holds a direct Tooling client.
2. **Doctrine versus runbook** — the security curriculum moves to gate-house; the
`NetKingdom Security Literacy` section in `INTENT.md` becomes lane-specific
runbooks that reference gate-house doctrine rather than restating it.
3. **The access lane / access rule demarcation** (§8) — ops-warden and ops-mason own
*lanes* (how a worker reaches a host); access-engine owns *rules* (whether they
may). This is the condition attached to renaming flex-auth to access-engine, so
ops-warden effectively holds a veto on that name.
Plus: add gate-house to the literacy and routing tables, and say so if moving the
curriculum out leaves ops-warden unable to instruct its own workers.
---
## Item 1 — Staff, and the §5 problem it exposes
**Assent to the layer.** Staff is the right assignment and not a demotion.
ops-warden's artifacts are exactly what §3.4 describes: routing decisions,
workplans, runbooks, an audit trail. Its one production lane is non-deterministic
in the sense that matters — it is an operator front door, not a contract.
**But §5 is violated today, and by the one lane ops-warden permanently owns.**
The rule is deliberately greppable, so grepping is the honest response:
| Path | Tooling contact | Kind | Whose credential |
| --- | --- | --- | --- |
| `src/warden/vault.py` (`VaultCA.sign`) | `POST <bao>/v1/<mount>/sign/<role>` | **write** | broker-issued token held by ops-warden's process |
| `src/warden/desk.py` | `bao kv put` (paste-once provisioning) | **write** | founder's, at the desk |
| `src/warden/taint.py` | `bao kv metadata get` | read (metadata only, never data) | caller's |
| `src/warden/proxy.py` (`warden access --fetch/--exec/--wrap`) | catalog `fetch_command` | read | **the caller's own** |
Two of these are not really ops-warden's clients. `proxy.py` runs the owner's tool
under the caller's identity and supplies no authority of its own — that is
`ADR-0002`, conduit not broker, and it is arguably outside §5's target. `taint.py`
reads metadata only, and fits §5's read-only-observation allowance once declared.
**`VaultCA` does not have that defence.** It is a direct OpenBao client, in a Staff
repository, performing a write, presenting a token from its own environment. It is
production-verified and it is the SSH lane — the single thing ops-warden owns
permanently. Under §5 as written, adopting this standard puts ops-warden's core
lane in violation on the day it is adopted.
The escape hatch §5 offers does not fit: it covers *read-only observation for
diagnostics*, and signing is a write. The route §5 prescribes does fit —
> *A Staff repository needing a capability no engine exposes MUST raise that as an
> engine gap, not solve it locally.*
— and no engine exposes SSH certificate signing. `secrets-engine` owns credential
abstraction, custody and lifecycle, which is the layer this belongs in, but it
fronts no SSH-CA API today.
**So ops-warden assents and declares the non-conformance rather than negotiating an
exemption.** `VaultCA` is recorded in `INTENT.md` as a declared §5 exception with a
named intended owner (`secrets-engine`), a blocker (no SSH-CA engine surface), and a
review date. That is `ADR-0003` — cover gaps, never silently own them — applied to
ops-warden itself instead of to someone else's lane.
### Proposed amendment to the standard
§5 has exactly one shape for a Staff repository that legitimately touches Tooling:
read-only diagnostics. That shape is too narrow to describe the estate as it exists,
and a rule with no lane for a real, sanctioned case gets satisfied by relabelling
rather than by closing the gap.
Recommend §5 gain a second shape: a **declared engine gap** — a Staff repository MAY
hold a Tooling client for a capability no engine exposes, provided it is declared in
`INTENT.md` with an intended owner, the blocker, and a review date, and provided the
declaration is machine-readable so the conformance check in §10 can distinguish a
tracked gap from an undeclared violation.
ops-warden already runs this machinery for other repositories' lanes: 27 catalog
entries carry `delegation:` with `intended_owner` and `blocked_on`, and
`warden route gaps` lists them (WP-0030). It is offered, not imposed — the standard
is gate-house's.
---
## Item 2 — Doctrine versus runbook
**Assent.** The `NetKingdom Security Literacy` section is what gate-house says it is:
evidence that the curriculum had no owner, so it accreted in whatever `INTENT.md`
was open. That is the same failure `risk-nexus` names for findings and the same one
`ADR-0001` prevents for catalog procedure. ops-warden has argued this rule twice
against other repositories; it applies here.
The boundary, drawn precisely:
| Moves to gate-house | Stays with ops-warden |
| --- | --- |
| Why the planes are separated; the authority model | Which subsystem owns which credential need |
| What "posture", "zone", "authority ceiling" mean | How to obtain a cert, a lease, a login — per lane |
| The security curriculum a worker is taught | The runbook a worker executes |
| Doctrine a lane must conform to | Evidence of conformance for ops-warden's lanes |
**gate-house's test question, answered: no, it does not leave ops-warden unable to
instruct its workers — and the reason is worth recording.** What actually instructs
an ops-warden worker is not the prose in `INTENT.md`. It is `warden plan "<need>"`,
`warden route find`, and `.claude/rules/credential-routing.md`, which is inlined into
every repository's agent instructions precisely because credential routing is
high-frequency and high-risk. That surface is executable, lane-specific, and
unambiguously runbook. It does not depend on the literacy table, and moving doctrine
out does not weaken it.
If anything the move improves it: the literacy table has been a second, prose copy of
what `registry/routing/catalog.yaml` states machine-readably, which is the
double-source failure `ADR-0001` exists to stop.
**One thing must not move with it.** `.claude/rules/credential-routing.md` stays
inline in this repository and in every other. It is not doctrine and not a
curriculum; it is the anti-pattern list an agent needs *before* it acts, and a
reference to a document in another repository would not be read in time.
---
## Item 3 — Access lane versus access rule
**Assent, unconditionally, and the veto on `access-engine` is not exercised.**
ops-warden is already built this way. `ADR-0005` implements one lane narrowly and
routes everything else; `ADR-0002` makes it a conduit that never decides; `ADR-0009`
has ops-warden compile membership attributes and apply a zone's failure mode while
flex-auth owns the stance. ops-warden consumes decisions; it has never rendered one.
The demarcation costs nothing because it describes what is already true.
`access-engine` is also the better name. ops-warden's own routing table has had to
say "authorization" for the decision and "access" for the route for a year, and the
collision is visible in every playbook.
**One operational condition, on execution rather than on the ruling.** The rename is
598 references across 82 files in this repository alone — catalog `owner:` fields,
`registry/flex-auth/`, `src/warden/policy.py`, the production registry snapshot
builder, playbooks, and the `.claude/rules/` files that other repositories inline.
Ops-warden asks for a deprecation window in which both names resolve, rather than a
flag day; ops-warden will do its own migration inside that window. This is a request
about sequencing, not a reservation about the name.
---
## Item 4 — gate-house is missing from every table
Correct, and fixed in this pass. gate-house is added to the literacy/routing table
in `INTENT.md` as the owner of doctrine, invariants, authority context, and
conformance review — with the routing rule stated explicitly: **doctrine and
authority-model questions go to gate-house; policy decisions continue to go to
access-engine.** Those are different questions and the distinction is the whole
point of §6.
---
## Recorded as
- `ADR-0010` — ops-warden is Staff; lanes not rules; the declared §5 exception
- `INTENT.md` — layer declaration, reworked routing table, gate-house row
- `WARDEN-IN-0001` — closed, outcome `assented`

View file

@ -0,0 +1,136 @@
# Security Layer Model v0.4 — ops-warden's review
**Date:** 2026-08-29
**Reviewed:** `net-kingdom/canon/standards/security-layer-model_v0.4.md` (accepted)
**Prior position:** `ADR-0010`, assent to v0.1 (`WARDEN-IN-0001`)
**Outcome:** no objection to the ruling; three findings, one of them against ops-warden.
---
## What v0.4 did with ops-warden's amendment
Both §5 asks from `ADR-0010` were adopted.
**§5.3 declared engine gap** is the amendment ops-warden offered, adopted with the
four fields intact (`capability`, `intended_owner`, `blocked_on`, `review`), the
rationale preserved — *a rule offering no lane for a real sanctioned case gets
satisfied by relabelling rather than by closing the gap* — and the framing that
matters most kept explicit: **a declared gap is tracked non-conformance, not
conformance**. ops-warden's delegation machinery is cited as prior art.
**§5.2 conduit** resolves the question ops-warden flagged rather than assumed. The
test is the supplied-authority property, which is the right test: it turns on what
the repository presents, not on what it touches. *"A conduit that presents its own
token is not a conduit"* is a sharper statement of `ADR-0002` than `ADR-0002` makes.
**This created an obligation ops-warden had not met.** §5.3 requires the fields
*machine-readably* and §11 makes the mapping a mechanical check; ops-warden's
declaration was prose in `INTENT.md`. Fixed in this pass: `layer.yaml`,
`scripts/check_layer_conformance.py`, `tests/test_layer_conformance.py`. The
checker found three undeclared modules on first run, all false positives — help
text, a docstring, and the doubles library that *simulates* `bao` — which is why
it now matches invocation shapes rather than the word.
---
## Finding 1 — §9.1 and §5.3 disagree, and ops-warden's §4 row is the instance
§9.1: *a Staff repository MUST NOT be catalogued in §4 as owning a capability that
requires a Tooling contact no engine exposes*; where intended but unbuilt, the
entry **MUST be marked pending** and the gap declared under §5.3.
ops-warden's §4 row reads `operational access lanes, stewardship, runbooks; SSH
certificate issuance` — with no pending mark. And §13 lists *SSH-CA signing write
(`VaultCA`, `bao kv put`) — declared by ops-warden — intended owner secrets-engine*.
So the catalog asserts ownership of a capability that requires a Tooling contact no
engine exposes, unmarked. By §9.1's own text that is a defect. But the available
fix is worse than the defect: **marking it pending would be false.** SSH issuance is
production-verified and in daily use. `pending` would tell a reader ops-warden does
not yet do the one thing it demonstrably does.
The root cause is that §9.1 collapses two different states:
| State | Example | Capability today |
| --- | --- | --- |
| No route exists at all | kings-guard containment (§9.2) | **zero** |
| Route exists via a declared §5.3 gap | ops-warden SSH issuance | **working, tracked** |
§5.3 exists precisely to sanction the second. §9.1 was written for the first — it
was raised by kings-guard, about containment, and correctly fixed *for that case*.
Applied to the adjacent case it produces a false catalog.
**Recommendation:** give §9.1 two marks rather than one — `pending` where no route
exists, and `declared-gap` where the capability is discharged under §5.3 and
registered in §13. Both are honest; today's binary forces a choice between a false
label and an unmarked violation.
This is the §12 loop working as designed, and §12 already says so: a finding that a
rule is unsatisfiable is a success of the loop.
---
## Finding 2 — §5's scope is undefined for infrastructure §4 does not catalogue
§5 forbids *a direct client for a Tooling-layer system*. §4 catalogues the security
estate, and only `key-cape` and `OpenBao` are Tooling rows.
ops-warden holds an HTTP client for the **State Hub** and for **llm-connect**
(`src/warden/worker.py`). Neither appears in §4. Both are infrastructure a Staff
repository holds a direct client for.
The question is not rhetorical, because the answers diverge sharply:
- **If they are Tooling**, then every Staff repository in the estate is in
undeclared violation on adoption day — they all write progress events — and
§11's second mechanical check fails estate-wide.
- **If they are not**, §5 should say so, because *"a Tooling-layer system"* reads
considerably broader than *"a repository in the §4 Tooling rows"*.
ops-warden has recorded both under `non_tooling_clients` in `layer.yaml` with the
reasoning stated, rather than resolving it unilaterally. The scope is gate-house's
to set.
---
## Finding 3 — §9.6 lands on ops-warden, and ops-warden does not satisfy it
This is the one against us, and it is the most consequential item in the review.
§9.6 consequence 1: *any system whose evidence is load-bearing MUST make emission
atomic with the state change it records. An archive cannot retrofit completeness.*
**ops-warden's audit emission is deliberately non-atomic.** `src/warden/ca.py:90`
carries `pass # audit must not block signing`, and `wiki/AuditTrail.md` states the
trail *"never blocks the primary action"*. If the audit append fails, the
certificate is still issued and the event is simply lost — a suppressed event that
leaves the chain perfectly intact, which is the exact failure §9.6 describes.
That was a considered availability choice: an audit-disk problem should not remove
production host access. §9.6 now makes it a conformance question, and the trade is
real in both directions:
- make emission atomic → an audit write failure fails the sign, and the estate's
operational access lane acquires a new dependency on its own evidence store;
- leave it → signing evidence cannot be treated as complete, and anything reasoning
from *"there is no record of a sign"* is unsound.
**ops-warden has not changed it, and is not going to decide this alone** — §9.6 is
estate doctrine and the question is whether SSH signing evidence is load-bearing in
gate-house's sense. What ops-warden can say is that the second horn is currently
true and undocumented: `wiki/AuditTrail.md` does not warn that absence of a record
is not evidence of absence. That correction is ops-warden's regardless of the
ruling, and is the smaller half of the fix.
Note also that §5.2 requires a conduit action to be *"reconstructable as the
caller's action in audit"* — an audit-dependent claim, and therefore bounded by
§9.6. Worth a cross-reference so the two rules do not drift apart.
---
## Offered
`layer.yaml` + `check_layer_conformance.py` + `test_layer_conformance.py` is a
working reference implementation of §5.3 and of §11's second mechanical check. Eight
of fifteen estate repositories have yet to declare (§14). If it is useful as a
pattern to point them at, it is offered — as the delegation machinery was.

View file

@ -0,0 +1,156 @@
# Security Layer Model v0.6 — ops-warden's review
**Date:** 2026-08-29
**Reviewed:** `security-layer-model_v0.6.md` (proposed), plus v0.5 and the companion
**Prior positions:** `ADR-0010` (v0.1 assent); `history/2026-08-29-layer-model-v04-review.md`
**Outcome:** no objection; one conformance action taken, two findings, one accepted SHOULD.
---
## Disposition of ops-warden's v0.4 findings
All three were acted on, two of them exactly as recommended.
| Finding | Outcome |
| --- | --- |
| §9.1 forces a false `pending` onto working capability | **Adopted** — v0.5 split it into `pending` and `declared-gap`, credited to ops-warden |
| §5 scope undefined for uncatalogued infrastructure | **Adopted** — "Tooling-layer system" now means a §4 Tooling row; the State Hub case is recorded, not policed |
| §9.6 atomicity lands on ops-warden's signing lane | **Ruled** — the load-bearing / attributive distinction, with ops-warden's `# audit must not block signing` named as the estate's live example |
The §9.6 ruling deserves a note, because it went in ops-warden's favour and that is
a reason to check it rather than accept it. The test is *"no control branches on
its presence"*. Verified: the only consumer of `audit.jsonl` is `warden activity`
(`cli.py`), which displays. Nothing gates on a signing record — not the agent
read-boundary, not `warden plan`, not the scorecard. The lane is genuinely
attributive and the trade is legitimate on the standard's own terms. The two
obligations that attach — declare it, never claim completeness — were already met
in `wiki/AuditTrail.md`, now updated to record the ruling rather than the open
question. **If a future ops-warden control ever gates on this trail, the trade has
to be revisited before that ships**, and that is recorded there.
`layer.yaml` is named in §11 as the estate's reference declaration form, including
the "record non-Tooling clients so the check is total" property. Offered again to
the repositories that have yet to declare.
---
## Conformance action taken — the stance map was not published
§6.4 obligation 3 requires a declared unreachable-engine stance that is total, per
zone, with no implicit default, *"published rather than held in code comments"*
and §6.4 requires **every** PEP-shaped consumer to publish its map so the maps can
be inventoried. `ADR-0009` is named as the reference shape.
ops-warden was not doing this. The map lived in `PolicyConfig.failure_modes`, a
dataclass default in `src/warden/config.py`. That is not a code *comment*, but it
is not published either — it is merely written down, and a consumer of the estate
had no way to read ops-warden's stance without reading ops-warden's source.
Published as `pep-stance.yaml`, with the property that makes publishing worth
anything: `tests/test_layer_conformance.py` asserts the published map is **equal to
the shipped default**. A published map that may drift from the code is worse than
no map, because it invites reliance it cannot support. The file also records the
obligation-2 position (verdict never cached; input claims cached under their own
freshness rules) and the obligation-4 bound (§9.6 attributive).
---
## Finding 1 — §6.4 obligation 1 contradicts obligation 3, and ops-warden is the instance
> **1. No side effect without a decision record.** A PEP MUST NOT perform the
> protected action unless it holds a decision from `access-engine` identifying the
> request it was rendered for.
> **3. A declared unreachable-engine stance (§9.3):** total, per zone... `ops-warden`
> `ADR-0009` is the reference shape.
These cannot both be absolute. ops-warden's declared stance — blessed by §9.3 as
*"the only thing left"* when there is no engine to ask — is `fail_open` for `z0``z2`
and `unknown`. Applying it means issuing a certificate **without holding a
decision**, which obligation 1 forbids without qualification.
So the same section names ops-warden as the reference shape for obligation 3 while
obligation 1 makes ops-warden's shipped behaviour a violation. §9.3 settled the
substance; §6.4 restates it in a form that takes it back.
**Recommendation.** Bound obligation 1 by obligation 3:
> A PEP MUST NOT perform the protected action unless it holds a decision from
> `access-engine` identifying the request it was rendered for, **or its declared
> §9.3 stance for the applicable scope permits proceeding without one and the
> application of that stance is recorded in place of the decision**.
This is not a weakening. It is stricter than today's text in the case that matters:
it makes the *recorded application of the stance* mandatory, rather than leaving
"no decision record" as a silent state. ops-warden already does this — `ca.py`
writes `policy_zone`, `policy_failure_mode` and `policy_decision_id` (present only
where a decision was rendered) into both the signatures log and `audit.jsonl`, per
`ADR-0009` rule 4: *a fail-open signing result is metadata, not silence*.
This is the same shape as the v0.4 §9.1 finding: a rule written for the clean case,
correct there, producing a false result on the adjacent case the standard has
already sanctioned elsewhere.
---
## Finding 2 — §6.4 creates a register that §13 does not implement
§6.4: *"Every PEP-shaped consumer MUST publish its stance map, and those maps MUST
be inventoried — in `maturity-engine` once it exists, **in §13 until then**."*
§13 contains no stance-map rows. It records declared contacts and unowned
capabilities; there is no column, row, or section for a PEP stance. So the
obligation names a register that does not exist yet, and the failure mode §6.4
itself warns about — *"`z0``z2` and unknown fail open" becoming the estate's real
policy without anyone having compiled it* — is exactly what the missing register
permits.
**Recommendation.** Either add a stance-map table to §13 with the same
state/owner-status discipline the gap table has, or state that the inventory waits
for `maturity-engine` and mark the obligation pending under §9.1's own logic — a
requirement whose register does not exist is a capability catalogued without a
surface. ops-warden's row is ready to paste:
| PEP | Protected action | Scope | Stance | Published |
| --- | --- | --- | --- | --- |
| `ops-warden` | SSH certificate issuance | security-zone | open `z0``z2`+unknown, closed `z3`/n-a | `ops-warden/pep-stance.yaml` |
The second half matters more than the first: **ops-warden is currently the only
PEP that has published one**, so an inventory today would contain one row and that
is itself the finding. `ops-mason` is named PEP-shaped in the same paragraph.
---
## Accepted, not yet done — §9.6 emission cadence
§9.6: *"A source SHOULD declare an expected emission cadence, and a drop below it
SHOULD become a finding in its own right."*
ops-warden declares none. This is a genuine SHOULD and the reasoning behind it is
sound — it converts the suppression blind spot into something detectable without
any Tooling contact, because the source publishes its own stream. It is not done
here because a cadence asserted without evidence is worse than none: ops-warden's
signing volume is operator-driven and bursty, and a fabricated baseline would
generate findings that mean nothing. Deriving one from the existing trail is
tractable and is recorded as ops-warden's to do, not gate-house's to chase.
---
## On the pace
Six versions in two days, with four repositories' findings absorbed and credited,
is the §12 loop working at a rate the estate has not seen before. Two cautions,
offered as an interested consumer rather than as objections:
1. **§13 already says it should not be statute, and it is right.** The register has
grown every version. Moving it to `maturity-engine` is the stated plan; until
that exists, each version of the standard is also a snapshot of a backlog, and
the two have very different review intervals.
2. **The standard is `proposed` again at v0.6**, and the four repositories that
assented did so to v0.1. ops-warden's `ADR-0010` assent covers the three
boundary items, and nothing in v0.2v0.6 has disturbed them — the layer, the
lane/rule demarcation, and doctrine-versus-runbook all stand. But the
`assented_by` list carries assent forward across five revisions, and a reader
could take it as assent to the current text. Worth distinguishing *assented to
the boundary* from *reviewed the current revision*; ops-warden has now done
both, and this note is the second.

View file

@ -0,0 +1,157 @@
# v0.7 conformance — INTENT vs SCOPE gap assessment
**Date:** 2026-08-29
**Standard:** `security-layer-model_v0.7.md` (**accepted**) + `SECURITY-COMPANION.md` v0.2
**Prior:** `ADR-0010`; v0.4 and v0.6 reviews in `history/`
**Method:** each v0.7 obligation checked against shipped code, not against intent.
---
## Summary
ops-warden is **conformant on every obligation it can discharge alone except three**,
and holds two declared §5.3 gaps that are tracked, registered and owned elsewhere.
The three genuine gaps are §9.7.2 (no stated revocation visibility deadline — a MUST),
§3.4 rule 1 (the agent read-boundary keys on an honour-system marker rather than an
issued identity), and §9.6's cadence, which is a SHOULD for an attributive source and
remains undone for an honest reason.
One new obligation is not a rule at all but a role: the companion routes the entire
estate to ops-warden for *how to get something done*. Nothing in the repo answers a
layer or declaration question today, and that is now a discoverability gap.
Four ops-warden findings were adopted into the standard between v0.4 and v0.7 — §9.1's
two marks, §5's Tooling scope rule, §6.4 obligation 1's second limb, and §13.1's
existence. That is the conformance loop working; it is not a reason to assume the next
pass finds nothing.
---
## Obligation-by-obligation
### Conformant, shipped, evidenced
| Obligation | Evidence |
| --- | --- |
| §11 declare layer in own voice, machine-readably | `INTENT.md` frontmatter (`layer: Staff`, `pep_shaped: true`) + `layer.yaml` — cited in §11 as the estate's reference form |
| §5 every Tooling contact maps to a shape; non-Tooling recorded so the check is total | `layer.yaml` 5 contacts + 2 exclusions; `scripts/check_layer_conformance.py` |
| §5.2 conduit supplies no authority | `proxy.py::_caller_env`; `tests/test_layer_conformance.py::test_conduit_supplies_no_authority_of_its_own` |
| §6.4 obl. 1 no side effect without a decision **or a recorded stance** | `ca.py` writes `policy_zone`, `policy_failure_mode`, `policy_decision_id` (present only where rendered). ops-warden is the named reference for limb two |
| §6.4 obl. 2 no verdict recaching | `policy.py` caches nothing — verified by inspection, not by claim |
| §6.4 obl. 3 stance map published, at a path named in the declaration, equal to shipped behaviour, asserted by test | `pep-stance.yaml`, named in `layer.yaml`; test asserts equality with `PolicyConfig().failure_modes`; registered in statute §13.1 |
| §9.3 stance total, per zone, no implicit default | 7 rows covering every zone plus `unknown` and `not-applicable` |
| §9.6 evidence claims bounded | `wiki/AuditTrail.md` declares the attributive trade and states absence is not evidence of absence |
| §9.7.1 every allow has an explicit lifetime | TTL enforced per `ActorType``adm` 48h, `agt` 24h, `atm` 8h |
| §3.4 rule 2 tool use is a conduit or engine API | `warden access` is the conduit; `ADR-0004` enforces that tool availability is not permission |
### Declared gaps — tracked non-conformance, owned elsewhere
Both registered in statute §13, intended owner `secrets-engine`, reviewed quarterly.
Neither is closable by ops-warden: closing them means another repository shipping a
surface.
- **`VaultCA` signing write** — no engine exposes SSH-CA signing.
- **`warden desk` `bao kv put`** — no engine exposes attended provisioning.
Nothing in v0.7 changes their status. The right ops-warden behaviour is to keep them
declared, keep the review dates honest, and not quietly grow a third.
---
## The three real gaps
### G1 — §9.7.2: no stated revocation visibility deadline (MUST)
> *A **PEP** has one boundary and MUST state one deadline… an unstated deadline is
> an unbounded replay window.*
ops-warden states none, and the honest answer is uncomfortable: **the effective
window is the certificate TTL — up to 48 hours.** A certificate issued under an allow
stays valid for its full TTL even if the decision that authorized it is revoked or
superseded the next minute. ops-warden has no revocation channel for an issued cert:
there is no CRL, no KRL distribution, and host-side `auth_principals` is
`railiance-infra`'s.
This is not a documentation gap. It is a design property that has never been written
down, and §9.7.2 exists precisely to force it into the open. Two things follow:
1. The deadline must be **stated**`adm` 48h / `agt` 24h / `atm` 8h — in
`pep-stance.yaml`, as what it is rather than as an aspiration.
2. Whether 48h is *acceptable* is a separate question, and it is partly
`railiance-infra`'s (KRL distribution) and partly ours (TTL policy). Stating it is
ours and is cheap; shortening it is a joint change.
Stating a bad number is better than stating none: an unstated deadline is an
unbounded replay window, and this one is bounded and already implemented.
### G2 — §3.4 rule 1: the agent boundary rests on an honour-system marker
> *No standing credential. Authority is issued per task, time-bounded under §9.7,
> and attributable to the principal on whose behalf it acts.*
`ADR-0004`'s read-boundary triggers when `WARDEN_AGENT_ID` is set — an environment
variable the agent sets **about itself**. An agent that does not set it is not
recognised as an agent. ops-warden has known this (`WARDEN-WP-0033-T04` recorded it
as "an honour-system marker on the ops-warden side"), and it was tolerable while no
issued agent identity existed.
One now does. `key-cape` accepted issuance ownership in `KEY-WP-0009-T03`:
`codex-railiance-platform`, subject `service:codex:railiance-platform`, role
`coding-agent`, scope `openbao:login`, 15-minute lifetime. The OpenBao side is
enforced by `railiance-platform`'s policy, which is the half that actually holds.
So the gap is narrower than it looks and worth stating precisely: **the OpenBao-side
boundary is real; the ops-warden-side boundary is advisory.** ops-warden should key
its read-boundary on the issued identity where one is present, and treat
`WARDEN_AGENT_ID` as a fallback that fails *toward* the boundary rather than away
from it. That is a change in this repo and does not need another repo to move.
### G3 — §9.6 emission cadence (SHOULD, for an attributive source)
Unchanged from the v0.6 review and still honest: ops-warden declares no expected
cadence because its signing volume is operator-driven and bursty, and a fabricated
baseline generates findings that mean nothing. v0.7 makes cadence a **MUST for
load-bearing sources**; ops-warden's trail is attributive, so it remains a SHOULD.
Deriving a real baseline from the existing trail is tractable and is ops-warden's to
do. It should be derived and declared, or explicitly deferred with a reason — not
left silent, which is what it is today.
---
## The role the companion assigns, and what it costs
> *"For how to get something done in NetKingdom — which lane, which credential, which
> route — ask `ops-warden`. This document says what the rules are; ops-warden stewards
> the paths through them."*
This is the largest change in ops-warden's INTENT surface and it is not a rule, so it
does not appear in any conformance check. The estate has been told to come here.
**Today the repo answers credential questions and no others.** `warden route` and
`warden plan` cover lanes, owners and acts. Nothing answers *"which layer am I"*,
*"how do I declare"*, *"I am PEP-shaped, what do I owe"* — the questions the companion
and the standard's adoption status (eight of fifteen repositories undeclared) actually
generate.
ops-warden has already built the reference artifacts those repositories need, and the
standard points at them by name in §11 and §6.4. What is missing is the path: a
discoverable route from *"I read the companion"* to *"here is the file to copy and the
check to run"*. That is exactly the stewardship ops-warden claims, applied to the
estate's newest rule rather than to its credential lanes.
Also worth noting, and not ops-warden's to fix: §13.1's register has one row, and
`ops-mason` — catalogued PEP-shaped in the same paragraph — has published nothing.
The standard says one row is itself the finding.
---
## What does not need doing
- **No new ADR.** `ADR-0010` holds: Staff, lanes not rules, declared gaps not
exemptions. v0.2v0.7 refined the rules around it and disturbed none of its three
positions. The reviews extend it; a superseding record would add ceremony without
changing a decision.
- **No change to the two §5.3 gaps.** They are correctly declared and owned elsewhere.
- **No re-assent.** ops-warden assented to the boundary in `ADR-0010` and has now
reviewed three revisions on their merits, which is the stronger position.

93
intakes/intakes.md Normal file
View file

@ -0,0 +1,93 @@
# Intake records
## WARDEN-IN-0001 — Assent requested: Staff layer, doctrine vs runbook, and the access lane/rule demarcation
```yaml
id: WARDEN-IN-0001
kind: intake
title: 'Assent requested: Staff layer, doctrine vs runbook, and the access lane/rule
demarcation'
status: closed
outcome: assented
origin: cross-repo
origin_ref: gate-house GH-DEC-2026-001
priority: medium
owner: ops-warden
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 — how a worker reaches a host;
access-engine owns access rules — 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 — currently every plane is listed and gate-house appears nowhere
— 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 — 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 — 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-28T21:05:00Z'
state_hub_intake_id: "01a049ed-bbbc-7520-bc7c-6b0912ca534a"
```
## WARDEN-IN-0002 — Review requested: security layer model v0.3 — and does maturity-engine absorb warden route gaps?
```yaml
id: WARDEN-IN-0002
kind: intake
title: 'Review requested: security layer model v0.3 — 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 — 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 — 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'
state_hub_intake_id: "01a04d97-94cd-7b49-8019-a91c7fce8adb"
```

View file

@ -0,0 +1,67 @@
{
"schema_version": "review-contract/v1",
"contract_key": "WARDEN-WP-0027-T02-DRILL-20260822-01-INFRA",
"subject": {
"kind": "task",
"id": "WARDEN-WP-0027-T02"
},
"scenario_id": "WARDEN-WP-0027-T02-DRILL-20260822-01",
"expires_at": "2026-08-23T20:00:00Z",
"authorizes_execution": false,
"evidence_boundary": "metadata_only",
"allowed_dispositions": [
"approve",
"request_changes"
],
"artifacts": {
"docs/evidence/WARDEN-WP-0027-T02-drill-scenario-2026-08-22.md": {
"algorithm": "sha256",
"digest": "ffa69764ad391db633f57ac70444e5781eee698bf9e362dfef31614fa43152dc"
}
},
"owners": [
{
"id": "railiance-infra",
"artifact_ids": [
"docs/evidence/WARDEN-WP-0027-T02-drill-scenario-2026-08-22.md"
],
"assertions": [
{
"id": "scenario-and-expiry-bound",
"statement": "This receipt applies only to scenario WARDEN-WP-0027-T02-DRILL-20260822-01 and expires at 2026-08-23T20:00:00Z."
},
{
"id": "provider-console-access-explicit",
"statement": "railiance-infra has independently verified provider-console access; it is not inferred from SSH, Warden, or cluster reachability."
},
{
"id": "distinct-abort-authority-accepted",
"statement": "railiance-infra accepts the distinct abort-authority role for this scenario and its bounded live window."
},
{
"id": "metadata-only-evidence",
"statement": "The receipt and its checks disclose no credential, token, recovery share, secret value, value-derived fingerprint, or provider-console detail."
},
{
"id": "execution-not-authorized",
"statement": "Approval is owner coordination evidence only and authorizes no console action, reboot, OpenBao seal or unseal, or other live execution."
}
],
"check_ids": [
"scenario-artifact-sha256",
"provider-console-access-attestation",
"distinct-abort-role-attestation",
"metadata-only-boundary"
]
}
],
"gates": [
{
"id": "WARDEN-WP-0027-T02-DRILL-20260822-01-INFRA",
"policy": "all_required",
"owners": [
"railiance-infra"
]
}
]
}

View file

@ -0,0 +1,74 @@
{
"schema_version": "review-contract/v1",
"contract_key": "WARDEN-WP-0027-T02-DRILL-20260822-01-QUORUM",
"subject": {
"kind": "task",
"id": "WARDEN-WP-0027-T02"
},
"scenario_id": "WARDEN-WP-0027-T02-DRILL-20260822-01",
"preparation_decision_id": "9da57559-712a-4521-b46e-a4c69729f9d2",
"expires_at": "2026-08-23T20:00:00Z",
"authorizes_execution": false,
"evidence_boundary": "metadata_only",
"allowed_dispositions": [
"approve",
"request_changes"
],
"artifacts": {
"docs/evidence/WARDEN-WP-0027-T02-drill-scenario-2026-08-22.md": {
"algorithm": "sha256",
"digest": "ffa69764ad391db633f57ac70444e5781eee698bf9e362dfef31614fa43152dc"
},
"docs/evidence/WARDEN-WP-0027-T02-drill-preparation-checklist-2026-08-22.md": {
"algorithm": "sha256",
"digest": "5462b69104d31849cd73c308bb092c16e1a38b7d7e0a48c492a87e8207fdd3fa"
}
},
"owners": [
{
"id": "railiance-master",
"artifact_ids": [
"docs/evidence/WARDEN-WP-0027-T02-drill-scenario-2026-08-22.md",
"docs/evidence/WARDEN-WP-0027-T02-drill-preparation-checklist-2026-08-22.md"
],
"assertions": [
{
"id": "scenario-decision-and-expiry-bound",
"statement": "This receipt applies only to scenario WARDEN-WP-0027-T02-DRILL-20260822-01 under preparation decision 9da57559-712a-4521-b46e-a4c69729f9d2 and expires at 2026-08-23T20:00:00Z."
},
{
"id": "two-distinct-custodians-available",
"statement": "The custody authority confirms that two distinct custodians for the current 2-of-3 OpenBao Shamir barrier are available for this attended scenario through approved out-of-band custody paths."
},
{
"id": "custody-values-remain-out-of-band",
"statement": "Custodian identities, share values, custody locations, and all value-derived fingerprints remain outside Git, State Hub, logs, shell history, and chat."
},
{
"id": "exact-live-scope-reviewed",
"statement": "The possible live scope is exactly one intentional OpenBao seal followed by the existing 2-of-3 unseal ceremony; it excludes host reboot, re-key, restore, policy change, PVC mutation, credential disclosure, and general workload restart."
},
{
"id": "execution-not-authorized",
"statement": "Approval is quorum-availability evidence only and authorizes no OpenBao seal or unseal, reboot, or other live execution."
}
],
"check_ids": [
"scenario-artifact-sha256",
"preparation-checklist-sha256",
"two-distinct-custodians-availability-attestation",
"out-of-band-custody-boundary",
"metadata-only-boundary"
]
}
],
"gates": [
{
"id": "WARDEN-WP-0027-T02-DRILL-20260822-01-QUORUM",
"policy": "all_required",
"owners": [
"railiance-master"
]
}
]
}

View file

@ -0,0 +1,78 @@
{
"schema_version": "review-contract/v1",
"contract_key": "WARDEN-WP-0027-T02-DRILL-20260822-01-PLATFORM",
"subject": {
"kind": "task",
"id": "WARDEN-WP-0027-T02"
},
"scenario_id": "WARDEN-WP-0027-T02-DRILL-20260822-01",
"preparation_decision_id": "9da57559-712a-4521-b46e-a4c69729f9d2",
"expires_at": "2026-08-23T20:00:00Z",
"authorizes_execution": false,
"evidence_boundary": "metadata_only",
"allowed_dispositions": [
"approve",
"request_changes"
],
"artifacts": {
"docs/evidence/WARDEN-WP-0027-T02-drill-scenario-2026-08-22.md": {
"algorithm": "sha256",
"digest": "ffa69764ad391db633f57ac70444e5781eee698bf9e362dfef31614fa43152dc"
},
"docs/evidence/WARDEN-WP-0027-T02-drill-preparation-checklist-2026-08-22.md": {
"algorithm": "sha256",
"digest": "5462b69104d31849cd73c308bb092c16e1a38b7d7e0a48c492a87e8207fdd3fa"
}
},
"owners": [
{
"id": "railiance-platform",
"artifact_ids": [
"docs/evidence/WARDEN-WP-0027-T02-drill-scenario-2026-08-22.md",
"docs/evidence/WARDEN-WP-0027-T02-drill-preparation-checklist-2026-08-22.md"
],
"assertions": [
{
"id": "scenario-decision-and-expiry-bound",
"statement": "This receipt applies only to scenario WARDEN-WP-0027-T02-DRILL-20260822-01 under preparation decision 9da57559-712a-4521-b46e-a4c69729f9d2 and expires at 2026-08-23T20:00:00Z."
},
{
"id": "current-snapshot-receipt-valid",
"statement": "railiance-platform has created a fresh encrypted, verified, off-host OpenBao Raft snapshot through its approved custody path, and its metadata-only receipt passes the platform validator against the live railiance01 cluster id and a possible applied index."
},
{
"id": "platform-driver-role-accepted",
"statement": "railiance-platform accepts the attended snapshot, seal/unseal driver, and value-safe post-unseal verification role for this scenario."
},
{
"id": "exact-live-scope-reviewed",
"statement": "The possible live scope is exactly one intentional OpenBao seal followed by the existing 2-of-3 unseal ceremony; it excludes host reboot, re-key, restore, policy change, PVC mutation, credential disclosure, and general workload restart."
},
{
"id": "metadata-only-evidence",
"statement": "The receipt and its checks disclose no snapshot data, decryption material, credential, token, recovery share, secret value, custody location, or value-derived fingerprint."
},
{
"id": "execution-not-authorized",
"statement": "Approval is preparation evidence only and authorizes no OpenBao seal or unseal, snapshot restore, reboot, or other live execution."
}
],
"check_ids": [
"scenario-artifact-sha256",
"preparation-checklist-sha256",
"openbao-snapshot-receipt-validator",
"platform-driver-scope-review",
"metadata-only-boundary"
]
}
],
"gates": [
{
"id": "WARDEN-WP-0027-T02-DRILL-20260822-01-PLATFORM",
"policy": "all_required",
"owners": [
"railiance-platform"
]
}
]
}

128
layer.yaml Normal file
View file

@ -0,0 +1,128 @@
# 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"
# §6.4 — ops-warden is PEP-shaped (it causes a protected side effect: issuing a
# certificate). Its unreachable-engine stance map is published separately, and
# asserted equal to shipped behaviour by tests/test_layer_conformance.py.
pep_stance: pep-stance.yaml
# 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.

69
pep-stance.yaml Normal file
View file

@ -0,0 +1,69 @@
# ops-warden — PEP unreachable-engine stance map
#
# Framework: net-kingdom/canon/standards/security-layer-model_v0.6.md §6.4, §9.3
# Rule of record: docs/adr/ADR-0009
# Validate: pytest tests/test_layer_conformance.py -k stance
#
# §6.4 obligation 3 requires a declared unreachable-engine stance that is total,
# scoped per zone, carries no implicit default and no per-call discretion, and is
# "published rather than held in code comments". §6.4 further requires every
# PEP-shaped consumer to PUBLISH its map so the maps can be inventoried. This
# file is ops-warden's, published because a map that lives only in a dataclass
# default is not published — it is merely written down.
#
# The property that makes this worth reading: it is asserted equal to the shipped
# default in src/warden/config.py (PolicyConfig.failure_modes) by
# tests/test_layer_conformance.py. A published map that may drift from the code
# is worse than none, because it invites reliance it cannot support.
schema_version: "0.1"
framework: netkingdom-security-layer-model
standard_version: "0.6"
repository: ops-warden
pep_shape: true
declared_by: docs/adr/ADR-0009
protected_action: "SSH certificate issuance (warden sign / cert_command)"
decision_engine: access-engine # flex-auth until the governed rename
scope: security-zone # security-zones_v0.1 membership of the TARGET workload
# Total by construction: every zone in security-zones_v0.1, plus the two
# non-zone outcomes. No implicit default — an unlisted value is a config error,
# not a permissive fallback.
stance:
z0-experimental: fail_open
z1-operational: fail_open
z2-protected: fail_open
z2-continuity: fail_open
z3-critical: fail_closed
unknown: fail_open # versioned build profile (ADR-0009); explicit, never inferred
not-applicable: fail_closed
# What happens when the stance is applied. §6.4 obligation 1 requires a decision
# record for a protected side effect; where the engine is unreachable there is no
# decision to hold, so ops-warden records the APPLICATION OF THE STANCE instead.
# See the assessment note: obligation 1 as written admits no such case.
on_apply:
recorded_fields:
- policy_zone
- policy_failure_mode
- policy_decision_id # present only where a decision was actually rendered
- outcome
written_to:
- "signatures log (src/warden/ca.py)"
- "audit.jsonl (src/warden/audit.py)"
never_recorded: "any secret material, any certificate private key"
# §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.
verdict_caching: none
input_claim_caching: "registry/flex-auth/production_registry_snapshot.json, rebuilt by scripts/build_flex_auth_registry.py"
# §6.4 obligation 4 — reconstructability, bounded by §9.6. ops-warden's audit
# emission on this lane is deliberately non-atomic and therefore ATTRIBUTIVE, not
# load-bearing: no control branches on the presence of a signing record
# (`warden activity` displays it; nothing gates on it). Registered in §13.
reconstructability:
bound: "§9.6 attributive — completeness is not claimed"
declared_at: wiki/AuditTrail.md

View file

@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "ops-warden"
version = "0.1.1"
version = "0.1.2"
description = "SSH CA and certificate lifecycle manager for ops actors"
requires-python = ">=3.11"
dependencies = [

View file

@ -48,8 +48,8 @@
],
"metadata": {
"flex_auth_contract": "protected-system-v0",
"ops_warden_policy_gate": "v2",
"policy_enabled_config": "policy.enabled",
"ops_warden_policy_gate": "security-zones-v0.1",
"security_zone_standard": "security-zones_v0.1",
"tenant": "tenant:platform"
}
}
@ -66,7 +66,6 @@
"ssh-signing",
"adm"
],
"trust_zone": "platform",
"owner": "team:platform-security",
"attributes": {
"actor_id": "adm-example",
@ -78,7 +77,10 @@
"allowed_principals": [
"adm-full"
],
"max_ttl_hours": 48
"max_ttl_hours": 48,
"security_zone": "unknown",
"security_zone_admission": "not-applicable",
"security_zone_reason": "human operator retains native actor identity"
}
},
{
@ -88,7 +90,6 @@
"ssh-signing",
"agt"
],
"trust_zone": "platform",
"owner": "team:platform-security",
"attributes": {
"actor_id": "agt-codex-interhub-bootstrap",
@ -100,7 +101,11 @@
"allowed_principals": [
"agt-interhub-bootstrap"
],
"max_ttl_hours": 2
"max_ttl_hours": 2,
"workload_id": "codex-interhub-bootstrap",
"security_zone": "unknown",
"security_zone_admission": "unknown",
"security_zone_reason": "workload_resolution_absent"
}
},
{
@ -110,7 +115,6 @@
"ssh-signing",
"agt"
],
"trust_zone": "platform",
"owner": "team:platform-security",
"attributes": {
"actor_id": "agt-state-hub-bridge",
@ -122,7 +126,11 @@
"allowed_principals": [
"agt-task-bridge"
],
"max_ttl_hours": 24
"max_ttl_hours": 24,
"workload_id": "ops-bridge-tunnel",
"security_zone": "unknown",
"security_zone_admission": "unknown",
"security_zone_reason": "workload_resolution_absent"
}
},
{
@ -132,7 +140,6 @@
"ssh-signing",
"atm"
],
"trust_zone": "platform",
"owner": "team:platform-security",
"attributes": {
"actor_id": "atm-backup-daily",
@ -144,7 +151,11 @@
"allowed_principals": [
"atm-backup-daily"
],
"max_ttl_hours": 8
"max_ttl_hours": 8,
"workload_id": "backup-daily",
"security_zone": "unknown",
"security_zone_admission": "unknown",
"security_zone_reason": "workload_resolution_absent"
}
}
],

View file

@ -0,0 +1,109 @@
# GENERATED by scripts/emit_high_risk_paths.py -- do not edit by hand.
# Concrete KV data paths for lanes ops-warden grades `risk: high`.
#
# This is an INPUT, not a policy. ops-warden states which paths it grades
# high; railiance-platform owns what agent-high-risk-boundary denies and may
# deny more, deny less, or dispute a grade (ADR-0002, ADR-0008).
#
# Grades cover every field a read of the path discloses, not the field the
# lane is named after (ADR-0008). `fields` is recorded where an owning CCR
# declares it, and is null where the field set has not been established --
# null means unknown, never 'one field'.
generated_at: "2026-08-31T22:46:47Z"
source: ops-warden/registry/routing/catalog.yaml
catalog_revision: "4fee839b1138c60642bd6e0210cf8bf541333747"
catalog_revision_date: "2026-09-01T00:46:28+02:00"
catalog_dirty: false
high_risk_lane_count: 24
concrete_path_count: 15
# Graded high but not a single KV address -- a routing pattern, a broker
# grant, or a non-KV lane. Nothing here for a policy to deny.
no_concrete_path:
- database-dynamic-credentials
- inter-hub-bootstrap-ssh
- net-kingdom-lldap-bind-credential
- net-kingdom-privacyidea-admin-token
- object-storage-sts
- openbao-api-key
- openbao-platform-admin-login
- openbao-shamir-recovery-ceremony
- ops-warden-warden-sign-token
paths:
- id: agent-harness-binky-mail-approle
data_path: tenants/data/binky/company-email/imap
metadata_path: tenants/metadata/binky/company-email/imap
owner_repo: railiance-platform
fields: null # field set not established -- unknown, not one
- id: agent-harness-forgejo-deploy
data_path: platform/data/workloads/agent-harness/forgejo-deploy-key
metadata_path: platform/metadata/workloads/agent-harness/forgejo-deploy-key
owner_repo: railiance-platform
fields: null # field set not established -- unknown, not one
- id: audit-core-senders
data_path: platform/data/workloads/audit-core/senders
metadata_path: platform/metadata/workloads/audit-core/senders
owner_repo: ops-mason
fields: null # field set not established -- unknown, not one
- id: binky-company-email-imap
data_path: tenants/data/binky/company-email/imap
metadata_path: tenants/metadata/binky/company-email/imap
owner_repo: railiance-platform
fields: null # field set not established -- unknown, not one
- id: binky-qonto-api
data_path: tenants/data/binky/qonto-api
metadata_path: tenants/metadata/binky/qonto-api
owner_repo: railiance-platform
fields: null # field set not established -- unknown, not one
- id: email-connect-transactional
data_path: platform/data/workloads/email-connect/transactional
metadata_path: platform/metadata/workloads/email-connect/transactional
owner_repo: railiance-platform
fields: null # field set not established -- unknown, not one
- id: forgejo-admin-api-token
data_path: platform/data/workloads/forgejo/forgejo-admin
metadata_path: platform/metadata/workloads/forgejo/forgejo-admin
owner_repo: railiance-platform
fields: null # field set not established -- unknown, not one
- id: issue-core-ingestion-api-key
data_path: platform/data/workloads/issue-core/issue-core/issue-core-runtime
metadata_path: platform/metadata/workloads/issue-core/issue-core/issue-core-runtime
owner_repo: railiance-platform
fields: [ISSUE_CORE_API_KEY, GITEA_BACKEND_TOKEN]
- id: openrouter-llm-connect
data_path: platform/data/workloads/activity-core/llm-connect/llm-connect-provider-secrets
metadata_path: platform/metadata/workloads/activity-core/llm-connect/llm-connect-provider-secrets
owner_repo: railiance-platform
fields: null # field set not established -- unknown, not one
- id: policy-nexus-forgejo-source-read
data_path: platform/data/workloads/policy-nexus/forgejo-source-read
metadata_path: platform/metadata/workloads/policy-nexus/forgejo-source-read
owner_repo: railiance-platform
fields: null # field set not established -- unknown, not one
- id: railiance-backup-offsite-lane
data_path: platform/data/workloads/railiance/backup/offsite-lane
metadata_path: platform/metadata/workloads/railiance/backup/offsite-lane
owner_repo: railiance-platform
fields: null # field set not established -- unknown, not one
- id: rapp-qonto-keycape-client
data_path: platform/data/workloads/rapp-qonto/keycape-client
metadata_path: platform/metadata/workloads/rapp-qonto/keycape-client
owner_repo: key-cape
fields: null # field set not established -- unknown, not one
- id: reuse-surface-hub-write-token
data_path: platform/data/workloads/reuse/reuse-surface/runtime-secrets
metadata_path: platform/metadata/workloads/reuse/reuse-surface/runtime-secrets
owner_repo: railiance-platform
fields: [REUSE_SURFACE_TOKEN, REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET]
- id: scaleway-bootstrap
data_path: platform/data/workloads/railiance/scaleway/bootstrap
metadata_path: platform/metadata/workloads/railiance/scaleway/bootstrap
owner_repo: railiance-platform
fields: null # field set not established -- unknown, not one
- id: whynot-design-npm-publish
data_path: platform/data/workloads/coulomb/whynot-design/npm-publish
metadata_path: platform/metadata/workloads/coulomb/whynot-design/npm-publish
owner_repo: railiance-platform
fields: null # field set not established -- unknown, not one

View file

@ -71,3 +71,23 @@ dataclass_floor:
lattice:
requires_env_posture: prod
rule: no-write-down
# --- Axis C — organization lifecycle posture (WARDEN-WP-0029 T02) --------------
# Third axis: fleet lifecycle, distinct from env (dev/test/prod) and maturity
# (M0M3). Answers how aggressive policy relaxations may be for founder-scale
# operation. Graduate when any trigger fires; do not overload env/maturity.
organization_posture:
id: build
summary: >
One founder-operator, pre-revenue, velocity prioritized. Pragmatic
provisioning is acceptable where audit and custody invariants hold
(values only in OpenBao/process env; metadata-only trails).
relaxations:
- workstation_oidc_acceptable
- per_repo_deploy_keys
- flex_auth_advisory_default
- localhost_founder_desk_os_session_trust
graduation_triggers:
- first_customer_data
- first_non_founder_operator
- production_tier

File diff suppressed because it is too large Load diff

View file

@ -55,12 +55,79 @@ def _caring_descriptor(actor_type: str, resource_id: str) -> dict[str, Any]:
}
def build_registry(inventory: dict[str, Any]) -> dict[str, Any]:
def _resolved_by_workload(zone_resolutions: dict[str, Any] | None) -> dict[str, Any]:
records = (zone_resolutions or {}).get("records") or []
resolved: dict[str, Any] = {}
for record in records:
workload_id = str(record.get("workload_id") or "")
if not workload_id:
continue
if workload_id in resolved:
raise ValueError(f"duplicate security-zone resolution for {workload_id!r}")
resolved[workload_id] = record
return resolved
def _zone_attributes(
actor: str,
entry: dict[str, Any],
resolutions: dict[str, Any],
) -> dict[str, Any]:
subject = entry.get("zone_subject")
if not isinstance(subject, dict):
return {
"security_zone": "unknown",
"security_zone_admission": "unknown",
"security_zone_reason": "catalog_applicability_absent",
}
applicability = subject.get("applicability")
if applicability == "not-applicable":
reason = str(subject.get("reason") or "").strip()
if not reason:
raise ValueError(f"{actor}.zone_subject.reason is required")
return {
"security_zone": "unknown",
"security_zone_admission": "not-applicable",
"security_zone_reason": reason,
}
if applicability != "applicable":
raise ValueError(
f"{actor}.zone_subject.applicability must be applicable or not-applicable"
)
workload_id = str(subject.get("workload_id") or "").strip()
if not workload_id:
return {
"security_zone": "unknown",
"security_zone_admission": "unknown",
"security_zone_reason": "workload_reference_absent",
}
record = resolutions.get(workload_id)
if record is None:
return {
"workload_id": workload_id,
"security_zone": "unknown",
"security_zone_admission": "unknown",
"security_zone_reason": "workload_resolution_absent",
}
return {
"workload_id": workload_id,
"security_zone": str(record.get("effective_zone") or "unknown"),
"security_zone_declared": record.get("declared_zone"),
"security_zone_admission": str(record.get("admission") or "unknown"),
"security_zone_reason": str(record.get("admission_reason") or "unknown"),
"security_zone_revision": record.get("membership_revision"),
}
def build_registry(
inventory: dict[str, Any], zone_resolutions: dict[str, Any] | None = None
) -> dict[str, Any]:
actors: dict[str, Any] = inventory.get("actors") or {}
resources: list[dict[str, Any]] = []
subjects: list[dict[str, Any]] = []
groups: dict[str, list[str]] = {gid: [] for gid in GROUP_BY_TYPE.values()}
relationships: list[dict[str, Any]] = []
resolutions = _resolved_by_workload(zone_resolutions)
for name, entry in sorted(actors.items()):
actor_type = str(entry["type"])
@ -74,7 +141,6 @@ def build_registry(inventory: dict[str, Any]) -> dict[str, Any]:
"id": resource_id,
"type": "ssh-certificate",
"labels": ["ssh-signing", actor_type],
"trust_zone": "platform",
"owner": "team:platform-security",
"attributes": {
"actor_id": name,
@ -82,6 +148,7 @@ def build_registry(inventory: dict[str, Any]) -> dict[str, Any]:
"allowed_subjects": [name, f"iam:{name}"],
"allowed_principals": principals,
"max_ttl_hours": ttl_hours,
**_zone_attributes(name, entry, resolutions),
},
}
)
@ -156,8 +223,8 @@ def build_registry(inventory: dict[str, Any]) -> dict[str, Any]:
"caring_profiles": ["caring-0.4.0-rc2"],
"metadata": {
"flex_auth_contract": "protected-system-v0",
"ops_warden_policy_gate": "v2",
"policy_enabled_config": "policy.enabled",
"ops_warden_policy_gate": "security-zones-v0.1",
"security_zone_standard": "security-zones_v0.1",
"tenant": "tenant:platform",
},
}
@ -186,10 +253,20 @@ def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("inventory", type=Path, help="ops-warden inventory.yaml")
parser.add_argument("-o", "--output", type=Path, required=True)
parser.add_argument(
"--zone-resolutions",
type=Path,
help="zone-engine resolved-view JSON; absent references remain unknown",
)
args = parser.parse_args()
inventory = yaml.safe_load(args.inventory.read_text()) or {}
registry = build_registry(inventory)
zone_resolutions = (
json.loads(args.zone_resolutions.read_text())
if args.zone_resolutions is not None
else None
)
registry = build_registry(inventory, zone_resolutions)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(registry, indent=2) + "\n")
print(f"Wrote {args.output} ({len(registry['subjects'])} actors)")

View file

@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""Verify the OpenBao half of the agent read-boundary (ADR-0004, WARDEN-WP-0032-T06).
`warden access` exits 7 on every `risk: high` lane, but that only protects the
ops-warden path. The OpenBao policy `agent-high-risk-boundary` is what protects a
direct `bao kv get` -- the actual 2026-07-16 disclosure vector. This script
compares the high-risk lanes in the routing catalog against the paths that policy
actually denies.
Read-only and capabilities-only by construction: it reads the *policy document*
and lane metadata. It never reads a secret value, and it never mints a token.
See `.claude/rules/credential-routing.md` -- verifying a lane with a read is the
mistake this whole control exists to prevent.
Prefers the policy deployed on the server (`bao policy read`); falls back to the
file in railiance-platform and says loudly that it did, because deployment drift
is exactly what this check exists to catch.
Exit codes: 0 every high-risk lane with a concrete path is denied; 1 at least one
is not; 2 the policy could not be obtained from either source.
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
CATALOG = REPO / "registry" / "routing" / "catalog.yaml"
POLICY_NAME = "agent-high-risk-boundary"
POLICY_FILE = Path.home() / "railiance-platform" / "openbao" / "policies" / f"{POLICY_NAME}.hcl"
# A path_template with a placeholder is a pattern, not an address -- it names the
# shape of a lane rather than one secret, so there is nothing for a policy to deny.
PLACEHOLDER = re.compile(r"[<>{}]|\*")
def read_deployed_policy() -> tuple[str | None, str]:
"""Return (policy_text, source). Server first, file second, neither third."""
try:
proc = subprocess.run(
["bao", "policy", "read", "-format=json", POLICY_NAME],
capture_output=True, text=True, timeout=20,
)
except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
server_err = str(exc)
else:
if proc.returncode == 0:
try:
return json.loads(proc.stdout)["policy"], "server"
except (json.JSONDecodeError, KeyError):
return proc.stdout, "server"
server_err = (proc.stderr or proc.stdout).strip().splitlines()[:1]
server_err = server_err[0] if server_err else f"exit {proc.returncode}"
if POLICY_FILE.exists():
print(f" ! could not read the deployed policy ({server_err})")
print(f" ! falling back to the FILE at {POLICY_FILE}")
print(" ! deployment drift cannot be detected in this mode\n")
return POLICY_FILE.read_text(), "file"
return None, f"unavailable ({server_err})"
def denied_data_paths(policy_text: str) -> set[str]:
"""Paths the policy denies. Only a `deny` on a KV *data* path is a read-boundary."""
denied: set[str] = set()
for match in re.finditer(
r'path\s+"([^"]+)"\s*\{[^}]*?capabilities\s*=\s*\[([^\]]*)\]',
policy_text, re.DOTALL,
):
path, caps = match.group(1), match.group(2)
if "deny" in {c.strip().strip('"\'') for c in caps.split(",")}:
denied.add(path)
return denied
def to_data_path(path_template: str) -> str | None:
"""Catalog path -> KV v2 data path. `<mount>/rest` -> `<mount>/data/rest`."""
if PLACEHOLDER.search(path_template) or " " in path_template:
return None
mount, _, rest = path_template.partition("/")
return f"{mount}/data/{rest}" if rest else None
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--json", action="store_true", help="machine-readable output")
args = parser.parse_args()
import yaml # local import so --help works without the dep
entries = yaml.safe_load(CATALOG.read_text())["entries"]
high = [e for e in entries if e.get("risk") == "high"]
policy_text, source = read_deployed_policy()
if policy_text is None:
print(f"FAIL: policy {POLICY_NAME} could not be obtained from server or file: {source}")
print(" Run `bao login -method=oidc`, or check out railiance-platform.")
return 2
denied = denied_data_paths(policy_text)
covered, uncovered, no_address = [], [], []
for entry in high:
template = entry.get("path_template")
data_path = to_data_path(template) if template else None
if data_path is None:
no_address.append(entry["id"])
elif data_path in denied:
covered.append((entry["id"], data_path))
else:
uncovered.append((entry["id"], data_path))
if args.json:
print(json.dumps({
"policy": POLICY_NAME,
"policy_source": source,
"high_risk_lanes": len(high),
"denied_data_paths": sorted(denied),
"covered": [{"id": i, "path": p} for i, p in covered],
"uncovered": [{"id": i, "path": p} for i, p in uncovered],
"no_concrete_address": no_address,
"ok": not uncovered,
}, indent=2))
return 1 if uncovered else 0
print(f"agent read-boundary — OpenBao half ({POLICY_NAME})\n")
print(f" policy source: {source}"
+ (" <-- live" if source == "server" else " <-- NOT the deployed policy"))
print(f" high-risk lanes: {len(high)}")
print(f" denied data paths: {len(denied)}")
print(f" covered: {len(covered)}")
print(f" NOT covered: {len(uncovered)}")
print(f" no concrete address: {len(no_address)}\n")
if covered:
print("COVERED — a direct `bao kv get` is denied for an agent token")
for lane_id, path in sorted(covered):
print(f" {lane_id:34} {path}")
print()
if uncovered:
print("NOT COVERED — graded high, but the policy does not deny the data path")
for lane_id, path in sorted(uncovered):
print(f" {lane_id:34} {path}")
print()
if no_address:
print("NO CONCRETE ADDRESS — a pattern or a non-KV lane, nothing to deny")
print(" " + ", ".join(sorted(no_address)) + "\n")
if uncovered:
print(f"RESULT: FAIL — {len(uncovered)} high-risk lane(s) outside the OpenBao boundary.")
print(" The policy is railiance-platform's; ops-warden reports rather than amends"
" (RISK-F-0004).")
return 1
print("RESULT: PASS — every high-risk lane with a concrete path is denied.")
return 0
if __name__ == "__main__":
sys.exit(main())

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,201 @@
#!/usr/bin/env python3
"""Readiness gate for the zone-aware flex-auth caller identity.
flex-auth deployed ``flex-auth-ops-warden`` (FLEX-WP-0016) in ``callerAuth.mode:
warn``: it authenticates the caller with a Kubernetes TokenReview and binds
``resource.system: ops-warden`` to ``system:serviceaccount:ops-warden:ops-warden``,
but a caller that sends no ``Authorization`` header only produces a
``caller authentication warning`` and is still served. That pin cannot move to
``enforce`` until ops-warden's calling side actually presents a token. The
former repo-wide ``policy.enabled`` switch is retired by WARDEN-WP-0032.
This script asserts the calling side *without* flipping anything:
* warden.yaml loads and ``policy.caller_auth.mode`` is not ``none``,
* a caller token can actually be obtained (file / env / command),
* (optional, ``--url``) a live ``/v1/check`` against the warn pin returns a
decision **and** the response is reached with the header attached.
Exit 0 = ready to ask flex-auth to enforce, 1 = not ready, 2 = bad input.
The token is never printed, logged, or written anywhere only its length and a
truncated SHA-256 fingerprint, which are safe to paste into a handoff message.
Usage:
python scripts/check_policy_caller_identity.py [--config ~/.config/warden/warden.yaml]
python scripts/check_policy_caller_identity.py --url http://127.0.0.1:19090
"""
from __future__ import annotations
import argparse
import hashlib
import sys
from pathlib import Path
from typing import List, Optional, Tuple
_SRC = Path(__file__).resolve().parent.parent / "src"
if _SRC.is_dir() and str(_SRC) not in sys.path:
sys.path.insert(0, str(_SRC))
from warden.caller_identity import ( # noqa: E402
CallerIdentityError,
resolve_caller_token,
)
from warden.config import ConfigError, load_config # noqa: E402
Check = Tuple[str, str, str]
def _fingerprint(token: str) -> str:
return "sha256:" + hashlib.sha256(token.encode()).hexdigest()[:12]
def run_checks(config_path: Optional[Path], url: Optional[str]) -> List[Check]:
checks: List[Check] = []
try:
cfg = load_config(config_path)
except ConfigError as e:
return [("fail", "warden.yaml", str(e))]
policy = cfg.policy
checks.append(("ok", "warden.yaml", "loaded; security-zones_v0.1 profile"))
mode = policy.caller_auth.mode
if mode == "none":
checks.append(
(
"fail",
"caller_auth.mode",
"none — no Authorization header is sent; the flex-auth pin stays in warn",
)
)
return checks
checks.append(("ok", "caller_auth.mode", mode))
try:
token = resolve_caller_token(policy.caller_auth)
except CallerIdentityError as e:
checks.append(("fail", "caller token", str(e)))
return checks
assert token is not None
checks.append(
("ok", "caller token", f"obtained, {len(token)} chars, {_fingerprint(token)}")
)
target = url or policy.flex_auth_url
if target is None:
checks.append(
(
"skip",
"live /v1/check",
"policy.flex_auth_url is absent; pass --url to run the live smoke",
)
)
return checks
import httpx # local import: the offline checks above must not need it
probe = {
"subject": {
"id": "agt-state-hub-bridge",
"type": "agt",
"tenant": policy.tenant,
},
"action": "sign",
"resource": {
"id": "ssh-cert:actor/agt-state-hub-bridge",
"type": "ssh-certificate",
"system": policy.system,
"tenant": policy.tenant,
},
"context": {
# A structurally complete context, so a deny means the policy said
# no — not that the probe was malformed. What is under test here is
# the caller identity, and that is answered by the HTTP status.
"actor_name": "agt-state-hub-bridge",
"actor_type": "agt",
"principals": ["agt-task-bridge"],
"ttl_hours": 24,
"pubkey_fingerprint": "sha256:" + "0" * 64,
"readiness_probe": True,
},
}
try:
response = httpx.post(
target.rstrip("/") + "/v1/check",
json=probe,
headers={"Authorization": f"Bearer {token}"},
timeout=10.0,
)
except httpx.RequestError as e:
checks.append(("fail", "live /v1/check", f"unreachable at {target}: {e}"))
return checks
if response.status_code == 401:
checks.append(
(
"fail",
"live /v1/check",
"401 — the token was sent but flex-auth did not accept it "
"(check the TokenReview audience and the ServiceAccount binding)",
)
)
elif response.status_code == 403:
checks.append(
(
"fail",
"live /v1/check",
f"403 — authenticated, but the principal may not represent "
f"system {policy.system!r}",
)
)
elif response.status_code >= 400:
checks.append(
("fail", "live /v1/check", f"HTTP {response.status_code} from {target}")
)
else:
try:
decision = response.json()
except ValueError:
checks.append(("fail", "live /v1/check", "non-JSON decision"))
return checks
effect = str(decision.get("effect", "?"))
decision_id = decision.get("id") or decision.get("request_id") or "?"
checks.append(
("ok", "live /v1/check", f"HTTP 200, effect={effect}, decision={decision_id}")
)
return checks
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--config", type=Path, default=None, help="path to warden.yaml")
parser.add_argument(
"--url",
default=None,
help="flex-auth base URL to smoke (e.g. a port-forward of the warn pin)",
)
args = parser.parse_args()
checks = run_checks(args.config, args.url)
glyph = {"ok": "", "fail": "", "skip": "·"}
print("flex-auth caller-identity readiness\n")
for status, label, detail in checks:
print(f" {glyph[status]} {label}: {detail}")
failed = [c for c in checks if c[0] == "fail"]
if failed:
print(
f"\nNOT READY — {len(failed)} check(s) failed. "
"Do not ask flex-auth to enforce caller authentication."
)
return 1
print(
"\nREADY — the calling side presents an identity. Verify "
"callerAuth.mode remains enforce on flex-auth-ops-warden after rollout. "
"Zone-specific PEP failure modes replace the retired global switches."
)
return 0
if __name__ == "__main__":
raise SystemExit(main())

160
scripts/emit_high_risk_paths.py Executable file
View file

@ -0,0 +1,160 @@
#!/usr/bin/env python3
"""Emit the versioned high-risk data-path artifact (WARDEN-WP-0033-T03).
`railiance-platform` asked for a generated list of concrete high-risk KV data
paths to consume, instead of hand-maintaining the deny set in
`agent-high-risk-boundary.hcl`. Hand-maintaining it is what let the two lists
drift for four lanes without anyone noticing (`RISK-F-0009`).
**This artifact is an input, not a policy.** It states which paths ops-warden
grades high. It does not say what to deny -- railiance-platform owns that, and
`ADR-0002` keeps ops-warden a conduit rather than the author of another repo's
control. A consumer is free to deny more, deny less, or disagree with a grade.
Carries the catalog git revision so a consumer can tell exactly what it was
derived from, and regenerate or diff against it. Read-only: it reads the catalog
and `git`, never OpenBao and never a secret value.
"""
from __future__ import annotations
import argparse
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
CATALOG = REPO / "registry" / "routing" / "catalog.yaml"
DEFAULT_OUT = REPO / "registry" / "generated" / "high-risk-data-paths.yaml"
def catalog_revision() -> tuple[str, str]:
"""(commit, iso-date) of the last change to the catalog. Never guesses."""
try:
out = subprocess.run(
["git", "log", "-1", "--format=%H %cI", "--", str(CATALOG)],
cwd=REPO, capture_output=True, text=True, timeout=15, check=True,
).stdout.strip()
commit, _, date = out.partition(" ")
return commit or "unknown", date or "unknown"
except (subprocess.SubprocessError, FileNotFoundError):
return "unknown", "unknown"
def dirty() -> bool:
"""True if the catalog has uncommitted edits -- the revision would be a lie."""
try:
out = subprocess.run(
["git", "status", "--porcelain", "--", str(CATALOG)],
cwd=REPO, capture_output=True, text=True, timeout=15, check=True,
).stdout.strip()
return bool(out)
except (subprocess.SubprocessError, FileNotFoundError):
return False
def build() -> tuple[str, int]:
import yaml
entries = yaml.safe_load(CATALOG.read_text())["entries"]
commit, date = catalog_revision()
rows, patternish = [], []
for entry in sorted(entries, key=lambda e: e["id"]):
if entry.get("risk") != "high":
continue
template = entry.get("path_template")
data_path = _to_data_path(template) if template else None
if data_path is None:
patternish.append(entry["id"])
continue
rows.append({
"id": entry["id"],
"data_path": data_path,
"metadata_path": data_path.replace("/data/", "/metadata/", 1),
"fields": entry.get("fields"),
"owner_repo": entry.get("owner_repo"),
})
lines = [
"# GENERATED by scripts/emit_high_risk_paths.py -- do not edit by hand.",
"# Concrete KV data paths for lanes ops-warden grades `risk: high`.",
"#",
"# This is an INPUT, not a policy. ops-warden states which paths it grades",
"# high; railiance-platform owns what agent-high-risk-boundary denies and may",
"# deny more, deny less, or dispute a grade (ADR-0002, ADR-0008).",
"#",
"# Grades cover every field a read of the path discloses, not the field the",
"# lane is named after (ADR-0008). `fields` is recorded where an owning CCR",
"# declares it, and is null where the field set has not been established --",
"# null means unknown, never 'one field'.",
"",
f"generated_at: \"{datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')}\"",
"source: ops-warden/registry/routing/catalog.yaml",
f"catalog_revision: \"{commit}\"",
f"catalog_revision_date: \"{date}\"",
f"catalog_dirty: {str(dirty()).lower()}",
f"high_risk_lane_count: {len([e for e in entries if e.get('risk') == 'high'])}",
f"concrete_path_count: {len(rows)}",
"",
"# Graded high but not a single KV address -- a routing pattern, a broker",
"# grant, or a non-KV lane. Nothing here for a policy to deny.",
"no_concrete_path:",
]
lines += [f" - {i}" for i in sorted(patternish)] or [" []"]
lines += ["", "paths:"]
for row in rows:
lines.append(f" - id: {row['id']}")
lines.append(f" data_path: {row['data_path']}")
lines.append(f" metadata_path: {row['metadata_path']}")
lines.append(f" owner_repo: {row['owner_repo']}")
if row["fields"]:
lines.append(f" fields: [{', '.join(row['fields'])}]")
else:
lines.append(" fields: null # field set not established -- unknown, not one")
return "\n".join(lines) + "\n", len(rows)
def _to_data_path(template: str) -> str | None:
import re
if re.search(r"[<>{}*]", template) or " " in template or template.startswith("k8s:"):
return None
mount, _, rest = template.partition("/")
return f"{mount}/data/{rest}" if rest else None
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--out", type=Path, default=DEFAULT_OUT)
parser.add_argument("--check", action="store_true",
help="exit 1 if the artifact on disk is stale (for CI)")
args = parser.parse_args()
content, count = build()
if args.check:
current = args.out.read_text() if args.out.exists() else ""
# generated_at always differs; compare everything else.
def strip_generated_at(text: str) -> str:
return "\n".join(
line for line in text.splitlines()
if not line.startswith("generated_at:")
)
if strip_generated_at(current) != strip_generated_at(content):
print(f"STALE: {args.out} does not match the catalog. Re-run without --check.")
return 1
print(f"fresh: {args.out} matches the catalog ({count} concrete paths)")
return 0
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(content)
print(f"wrote {args.out}{count} concrete high-risk data paths")
if dirty():
print(" ! catalog has uncommitted changes; catalog_revision does not describe it")
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -66,9 +66,8 @@ ca_key: $SMOKE_DIR/ca_key
state_dir: $SMOKE_DIR/state
inventory_path: $INVENTORY
policy:
enabled: true
flex_auth_url: http://$ADDR
fail_closed: true
zone_registry_path: $REGISTRY
tenant: tenant:platform
system: ops-warden
EOF
@ -106,9 +105,8 @@ vault:
inventory_path: $INVENTORY
state_dir: $SMOKE_DIR/state-vault
policy:
enabled: true
flex_auth_url: http://$ADDR
fail_closed: true
zone_registry_path: $REGISTRY
tenant: tenant:platform
system: ops-warden
EOF

185
scripts/report_workload_join.py Executable file
View file

@ -0,0 +1,185 @@
#!/usr/bin/env python3
"""Report explicit lane -> workload resolution for security-zones_v0.1.
The catalog owner declares whether each lane is workload-applicable. Managed
deployables use the exact Repo Manager v1 ``(rapp_id, name, deployable?)``
reference. Independently governed operational workloads use ``name`` plus an
owner declaration reference. Unknown and not-applicable are explicit results.
This script never parses a credential path, consults ``owner_repo`` as an
identity hint, or substitutes a repository name. It reads declarations only.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
import yaml
def load_rapp_workloads(root: Path) -> dict[tuple[str, str], dict[str, Any]]:
"""Exact Repo Manager v1 key -> authoritative declaration projection."""
out: dict[tuple[str, str], dict[str, Any]] = {}
for decl in sorted(root.glob("rapp-*/declarations/rapp.yaml")):
try:
data = yaml.safe_load(decl.read_text()) or {}
except yaml.YAMLError:
continue
identity = data.get("workload_identity") or {}
rapp_id = data.get("rapp_id")
name = identity.get("name")
if not rapp_id or not name:
continue
deployables = []
for member in (data.get("composition") or {}).get("member_repos") or []:
deployables.extend(str(value) for value in member.get("deployables") or [])
out[(str(rapp_id), str(name))] = {
"source": str(decl),
"deployables": sorted(set(deployables)),
"data_classification": data.get("data_classification"),
"criticality": data.get("criticality"),
"readiness_state": data.get("readiness_state"),
}
return out
def _direct_declaration_path(
declaration_ref: str, *, catalog_path: Path, estate_root: Path
) -> Path:
ref = Path(declaration_ref)
if ref.is_absolute():
return ref
local = catalog_path.resolve().parents[2] / ref
return local if local.exists() else estate_root / ref
def _resolve_direct(
ref: dict[str, Any], *, catalog_path: Path, estate_root: Path
) -> tuple[dict[str, Any] | None, str | None]:
source = _direct_declaration_path(
str(ref["declaration_ref"]), catalog_path=catalog_path, estate_root=estate_root
)
if not source.exists():
return None, f"declaration not found: {source}"
try:
declaration = yaml.safe_load(source.read_text()) or {}
except yaml.YAMLError as exc:
return None, f"invalid declaration YAML: {exc}"
identity = declaration.get("workload_identity") or {}
if identity.get("name") != ref.get("name"):
return None, (
f"declared workload_identity.name={identity.get('name')!r}, "
f"expected {ref.get('name')!r}"
)
context = (declaration.get("zones") or {}).get("context", {})
return {
"source": str(source),
"data_classification": context.get("data_classification"),
"criticality": context.get("criticality"),
"maturity": context.get("maturity"),
"declared_zone": (declaration.get("zones") or {}).get("membership"),
}, None
def build(catalog_path: Path, estate_root: Path) -> dict[str, Any]:
entries = (yaml.safe_load(catalog_path.read_text()) or {}).get("entries", [])
managed = load_rapp_workloads(estate_root)
posture_path = catalog_path.parent.parent / "policy" / "security-posture.yaml"
floor = (yaml.safe_load(posture_path.read_text()) or {}).get("dataclass_floor", {})
resolved: list[dict[str, Any]] = []
unknown: list[dict[str, Any]] = []
not_applicable: list[dict[str, Any]] = []
for entry in entries:
lane = str(entry.get("id"))
ref = entry.get("workload_ref") or {}
applicability = ref.get("applicability")
if applicability == "not-applicable":
not_applicable.append({"lane": lane, "reason": ref.get("reason")})
continue
if applicability != "applicable":
unknown.append({"lane": lane, "reason": "applicability missing or invalid"})
continue
if ref.get("unknown_reason"):
unknown.append({"lane": lane, "reason": ref["unknown_reason"]})
continue
projection: dict[str, Any] | None
error: str | None = None
if ref.get("rapp_id"):
key = (str(ref.get("rapp_id")), str(ref.get("name")))
projection = managed.get(key)
if projection is None:
error = f"Repo Manager reference does not resolve: {key[0]}/{key[1]}"
elif ref.get("deployable") and ref["deployable"] not in projection["deployables"]:
error = f"deployable {ref['deployable']!r} is not declared by {key[0]}/{key[1]}"
else:
projection, error = _resolve_direct(
ref, catalog_path=catalog_path, estate_root=estate_root
)
if error or projection is None:
unknown.append({"lane": lane, "reason": error or "reference unresolved"})
continue
classification = projection.get("data_classification")
resolved.append(
{
"lane": lane,
"workload_ref": ref,
"source": projection.get("source"),
"data_classification": classification,
"criticality": projection.get("criticality"),
"maturity": projection.get("maturity") or floor.get(classification),
"declared_zone": projection.get("declared_zone"),
"unmapped_classification": bool(classification) and classification not in floor,
}
)
return {
"contract": "helixforge.workload-reference/v1",
"resolved": resolved,
"unknown": unknown,
"not_applicable": not_applicable,
"ok": len(resolved) + len(unknown) + len(not_applicable) == len(entries),
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--estate-root", "--rapp-root", dest="estate_root", type=Path, default=Path.home()
)
parser.add_argument("--json", action="store_true")
parser.add_argument(
"--catalog",
type=Path,
default=Path(__file__).resolve().parent.parent
/ "registry"
/ "routing"
/ "catalog.yaml",
)
args = parser.parse_args()
report = build(args.catalog, args.estate_root)
if args.json:
print(json.dumps(report, indent=2, sort_keys=True))
return 0 if report["ok"] else 1
print("explicit lane -> workload resolution\n")
print(f" resolved: {len(report['resolved'])}")
print(f" unknown: {len(report['unknown'])}")
print(f" not-applicable: {len(report['not_applicable'])}\n")
for row in report["resolved"]:
ref = row["workload_ref"]
prefix = f"{ref.get('rapp_id')}/" if ref.get("rapp_id") else ""
print(f"RESOLVED {row['lane']:34} -> {prefix}{ref.get('name')}")
for row in report["unknown"]:
print(f"UNKNOWN {row['lane']:34} {row['reason']}")
for row in report["not_applicable"]:
print(f"NOT-APPLICABLE {row['lane']:34} {row['reason']}")
return 0 if report["ok"] else 1
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -71,6 +71,6 @@ def policy_gate_status() -> str:
cfg = load_config()
except ConfigError:
return "advisory — no warden.yaml (caller identity; gate not enforced)"
if cfg.policy.enabled:
return f"enforced — flex-auth at {cfg.policy.flex_auth_url}"
return "advisory — policy.enabled=false (gate ships with flex-auth deploy)"
if cfg.policy.flex_auth_url:
return f"zone-aware — flex-auth at {cfg.policy.flex_auth_url}"
return "zone-aware — evaluator unconfigured; unknown-zone fail_open applies"

View file

@ -9,7 +9,7 @@ import os
import re
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Iterable, Optional
from typing import Any, Optional
_AUDIT_FILENAME = "audit.jsonl"
_MAX_BYTES = 5 * 1024 * 1024
@ -215,7 +215,6 @@ def collect_activity(
since = datetime.now(timezone.utc) - timedelta(days=days)
events = read_events(state_dir, since=since, kinds=kinds)
if include_legacy:
legacy_kinds = kinds or {"sign", "access", "worker"}
if not kinds or "sign" in kinds:
events.extend(_legacy_sign_events(state_dir, since))
if not kinds or "access" in kinds:

View file

@ -58,6 +58,12 @@ def _append_signature_log(
}
if spec.policy_decision_id:
entry["policy_decision_id"] = spec.policy_decision_id
if spec.policy_zone:
entry["policy_zone"] = spec.policy_zone
if spec.policy_failure_mode:
entry["policy_failure_mode"] = spec.policy_failure_mode
if spec.policy_outcome:
entry["policy_outcome"] = spec.policy_outcome
state_dir.mkdir(parents=True, exist_ok=True)
with (state_dir / "signatures.log").open("a") as f:
f.write(json.dumps(entry) + "\n")
@ -76,6 +82,9 @@ def _append_signature_log(
actor_type=spec.actor_type.value,
backend=backend,
ttl_hours=spec.ttl_hours,
policy_zone=spec.policy_zone,
policy_failure_mode=spec.policy_failure_mode,
policy_outcome=spec.policy_outcome,
)
except Exception:
pass # audit must not block signing

View file

@ -0,0 +1,92 @@
"""Caller identity for ops-warden's outbound flex-auth policy calls.
flex-auth's `flex-auth-ops-warden` pin (FLEX-WP-0016) authenticates the *caller*
before it evaluates the request: `Authorization: Bearer <token>` is passed to a
Kubernetes TokenReview, and `resource.system: ops-warden` is bound to the
principal `system:serviceaccount:ops-warden:ops-warden`. Until ops-warden sends
that header, the pin logs `caller authentication warning` and can only run in
`warn` mode which is why enforcing caller authentication is a separate gate.
This module resolves the token at call time and hands it straight to the request.
Nothing is cached to disk, logged, or echoed: ops-warden carries the value, it
does not hold it (ADR-0002).
"""
from __future__ import annotations
import os
import subprocess
from warden.config import CallerAuthConfig
class CallerIdentityError(Exception):
"""Raised when a caller token was configured but could not be obtained."""
def resolve_caller_token(cfg: CallerAuthConfig) -> str | None:
"""Return the bearer token for flex-auth, or None when mode is ``none``.
Raises CallerIdentityError when a token was configured but is unavailable.
The token itself never appears in an exception message.
"""
mode = cfg.mode
if mode == "none":
return None
if mode == "file":
if cfg.token_path is None:
raise CallerIdentityError("caller_auth mode 'file' has no token_path")
try:
token = cfg.token_path.read_text()
except OSError as e:
raise CallerIdentityError(
f"caller token file unreadable: {cfg.token_path} ({e.strerror})"
) from e
elif mode == "env":
token = os.environ.get(cfg.token_env, "")
if not token.strip():
raise CallerIdentityError(
f"caller token env {cfg.token_env} is unset or empty"
)
elif mode == "command":
if not cfg.command:
raise CallerIdentityError("caller_auth mode 'command' has no command")
try:
result = subprocess.run(
cfg.command,
capture_output=True,
text=True,
timeout=30,
check=False,
)
except FileNotFoundError as e:
raise CallerIdentityError(
f"caller token command not found: {cfg.command[0]}"
) from e
except subprocess.TimeoutExpired as e:
raise CallerIdentityError("caller token command timed out") from e
if result.returncode != 0:
stderr = (result.stderr or "").strip().splitlines()
detail = stderr[-1] if stderr else f"exit {result.returncode}"
raise CallerIdentityError(f"caller token command failed: {detail}")
token = result.stdout
else:
raise CallerIdentityError(f"unsupported caller_auth mode {mode!r}")
token = token.strip()
if not token:
raise CallerIdentityError(f"caller_auth mode {mode!r} produced an empty token")
if any(ch.isspace() for ch in token):
# flex-auth rejects a bearer token containing whitespace outright.
raise CallerIdentityError(
f"caller_auth mode {mode!r} produced a token containing whitespace"
)
return token
def caller_auth_headers(cfg: CallerAuthConfig) -> dict[str, str]:
"""Headers to attach to a flex-auth /v1/check call ({} when unauthenticated)."""
token = resolve_caller_token(cfg)
if token is None:
return {}
return {"Authorization": f"Bearer {token}"}

File diff suppressed because it is too large Load diff

View file

@ -2,9 +2,10 @@
from __future__ import annotations
import os
import shlex
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, Optional
from typing import Dict, List, Optional
import yaml
@ -13,14 +14,54 @@ class ConfigError(Exception):
"""Raised when config is invalid or missing."""
@dataclass
class CallerAuthConfig:
"""How ops-warden proves *its own* identity to flex-auth (FLEX-WP-0016).
flex-auth's ops-warden pin authenticates the caller with a Kubernetes
TokenReview and binds ``resource.system: ops-warden`` to the principal
``system:serviceaccount:ops-warden:ops-warden``. A workstation ``warden
sign`` is not a ServiceAccount, so the token has to come from somewhere:
``none`` send no ``Authorization`` header (pre-FLEX-WP-0016 behaviour;
accepted only while that pin runs ``callerAuth.mode: warn``)
``file`` read a projected ServiceAccount token from ``token_path``
(in-cluster PEP, audience-bound by the projection)
``env`` read the token from ``token_env``
``command`` run ``command`` and use its stdout, e.g.
``kubectl create token ops-warden -n ops-warden
--audience flex-auth --duration 10m``
ops-warden never stores the token: it is read, sent, and dropped
(ADR-0002 transparent conduit, not a broker).
"""
mode: str = "none"
token_path: Optional[Path] = None
token_env: str = "WARDEN_POLICY_CALLER_TOKEN"
command: Optional[List[str]] = None
audience: str = "flex-auth"
@dataclass
class PolicyConfig:
enabled: bool = False
flex_auth_url: str = "http://127.0.0.1:8080"
fail_closed: bool = True
flex_auth_url: Optional[str] = None
zone_registry_path: Optional[Path] = None
failure_modes: Dict[str, str] = field(
default_factory=lambda: {
"z0-experimental": "fail_open",
"z1-operational": "fail_open",
"z2-protected": "fail_open",
"z2-continuity": "fail_open",
"z3-critical": "fail_closed",
"unknown": "fail_open",
"not-applicable": "fail_closed",
}
)
tenant: str = "tenant:platform"
subject_env: str = "WARDEN_POLICY_SUBJECT"
system: str = "ops-warden"
caller_auth: "CallerAuthConfig" = field(default_factory=lambda: CallerAuthConfig())
@dataclass
@ -117,13 +158,71 @@ def load_config(path: Optional[Path] = None) -> WardenConfig:
)
policy_raw = raw.get("policy") or {}
retired = sorted({"enabled", "fail_closed"}.intersection(policy_raw))
if retired:
raise ConfigError(
"retired policy setting(s) "
+ ", ".join(f"policy.{key}" for key in retired)
+ "; security-zones_v0.1 now selects stance and failure mode"
)
caller_raw = policy_raw.get("caller_auth") or {}
caller_command = caller_raw.get("command")
if isinstance(caller_command, str):
caller_command = shlex.split(caller_command)
elif caller_command is not None:
caller_command = [str(part) for part in caller_command]
caller_token_path = caller_raw.get("token_path")
caller_cfg = CallerAuthConfig(
mode=str(caller_raw.get("mode", "none")).strip().lower(),
token_path=(
Path(os.path.expanduser(str(caller_token_path)))
if caller_token_path
else None
),
token_env=str(caller_raw.get("token_env", "WARDEN_POLICY_CALLER_TOKEN")),
command=caller_command,
audience=str(caller_raw.get("audience", "flex-auth")),
)
if caller_cfg.mode not in {"none", "file", "env", "command"}:
raise ConfigError(
f"policy.caller_auth.mode must be none|file|env|command, "
f"got {caller_cfg.mode!r}"
)
if caller_cfg.mode == "file" and caller_cfg.token_path is None:
raise ConfigError("policy.caller_auth.token_path is required for mode: file")
if caller_cfg.mode == "command" and not caller_cfg.command:
raise ConfigError("policy.caller_auth.command is required for mode: command")
failure_modes = PolicyConfig().failure_modes
configured_failure_modes = policy_raw.get("failure_modes") or {}
if not isinstance(configured_failure_modes, dict):
raise ConfigError("policy.failure_modes must be a mapping")
failure_modes.update(
{str(zone): str(mode) for zone, mode in configured_failure_modes.items()}
)
invalid_modes = {
zone: mode
for zone, mode in failure_modes.items()
if mode not in {"fail_open", "fail_closed"}
}
if invalid_modes:
raise ConfigError(
"policy.failure_modes values must be fail_open or fail_closed: "
f"{invalid_modes}"
)
zone_registry_path = policy_raw.get("zone_registry_path")
flex_auth_url = str(policy_raw.get("flex_auth_url", "")).strip() or None
policy_cfg = PolicyConfig(
enabled=bool(policy_raw.get("enabled", False)),
flex_auth_url=str(policy_raw.get("flex_auth_url", "http://127.0.0.1:8080")),
fail_closed=bool(policy_raw.get("fail_closed", True)),
flex_auth_url=flex_auth_url,
zone_registry_path=(
Path(os.path.expanduser(str(zone_registry_path)))
if zone_registry_path
else None
),
failure_modes=failure_modes,
tenant=str(policy_raw.get("tenant", "tenant:platform")),
subject_env=str(policy_raw.get("subject_env", "WARDEN_POLICY_SUBJECT")),
system=str(policy_raw.get("system", "ops-warden")),
caller_auth=caller_cfg,
)
return WardenConfig(

397
src/warden/desk.py Normal file
View file

@ -0,0 +1,397 @@
"""Founder interaction surface — ``warden desk`` (WARDEN-WP-0029 T03).
Build-phase localhost page for the rare founder acts emitted by ``warden plan``:
approve/deny, OIDC login launch, paste-once provision into OpenBao.
Pattern: stdlib ``ThreadingHTTPServer`` on 127.0.0.1 only (see net-kingdom
security-bootstrap-console). No multi-user auth; OS session is trust boundary.
Never logs secret values.
"""
from __future__ import annotations
import html
import json
import secrets
import subprocess
import threading
import webbrowser
from dataclasses import dataclass, field as dc_field
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any, Callable, Dict, Optional
from urllib.parse import parse_qs, urlparse
# Acts the desk can render. Keep in sync with plan.FOUNDER_ACT_KINDS.
DESK_ACTS = ("approve", "oidc_login", "paste_once_provision")
class DeskError(Exception):
"""Raised when desk session setup fails."""
@dataclass
class DeskSession:
"""In-memory founder act session — metadata only (no secret values stored)."""
token: str
act: str
summary: str
lane_id: str = ""
path: str = ""
kv_field: str = "value"
oidc_command: str = ""
result: str = "pending" # pending | approved | denied | provisioned | launched | error
message: str = ""
extra: Dict[str, Any] = dc_field(default_factory=dict)
def new_session(
*,
act: str,
summary: str,
lane_id: str = "",
path: str = "",
kv_field: str = "value",
oidc_command: str = "",
extra: Optional[dict] = None,
) -> DeskSession:
if act not in DESK_ACTS:
raise DeskError(f"unknown desk act {act!r}; expected one of {DESK_ACTS}")
if act == "paste_once_provision" and not path:
raise DeskError("paste_once_provision requires --path (concrete OpenBao path)")
return DeskSession(
token=secrets.token_urlsafe(24),
act=act,
summary=summary,
lane_id=lane_id,
path=path,
kv_field=kv_field or "value",
oidc_command=oidc_command,
extra=dict(extra or {}),
)
def session_from_plan_dict(plan: dict) -> DeskSession:
"""Build a desk session from a ``warden plan --json`` payload."""
act_raw = plan.get("founder_act") or {}
if not act_raw or plan.get("verdict") != "founder_required":
raise DeskError(
"plan verdict is not founder_required or founder_act is missing — "
"nothing for the desk to render"
)
kind = str(act_raw.get("kind") or "")
details = act_raw.get("details") or {}
path = str(details.get("path_template") or details.get("path") or "")
if kind == "paste_once_provision" and ("<" in path or ">" in path):
raise DeskError(
f"path_template still has placeholders ({path!r}); pass a concrete "
"--path to warden desk"
)
return new_session(
act=kind,
summary=str(act_raw.get("summary") or plan.get("need") or "founder act"),
lane_id=str(details.get("lane_id") or plan.get("lane_id") or ""),
path=path if "<" not in path else "",
oidc_command=str(details.get("fetch_command") or ""),
extra={"need": plan.get("need"), "organization_posture": plan.get("organization_posture")},
)
def _page(title: str, body: str) -> bytes:
doc = f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{html.escape(title)}</title>
<style>
:root {{ color-scheme: light dark; --ink: #1d2733; --muted: #536271;
--line: #d9e0e7; --paper: #f8fafc; --accent: #2066a8; --ok: #1a7f37; --bad: #b42318; }}
body {{ margin: 0; font-family: ui-sans-serif, system-ui, sans-serif; color: var(--ink);
background: var(--paper); min-height: 100vh; display: grid; place-items: center; padding: 24px; }}
main {{ width: min(100%, 560px); background: #fff; border: 1px solid var(--line);
border-radius: 8px; padding: 28px 32px; box-shadow: 0 12px 32px rgba(29,39,51,.08); }}
h1 {{ margin: 0 0 8px; font-size: 1.35rem; }}
p, li {{ color: var(--muted); line-height: 1.55; }}
.eyebrow {{ text-transform: uppercase; font-size: .75rem; font-weight: 700;
color: var(--accent); letter-spacing: .04em; margin: 0 0 12px; }}
.box {{ background: var(--paper); border: 1px solid var(--line); border-radius: 6px;
padding: 12px 14px; margin: 16px 0; font-family: ui-monospace, monospace; font-size: .85rem;
word-break: break-all; }}
form {{ display: grid; gap: 12px; margin-top: 18px; }}
textarea {{ width: 100%; min-height: 96px; font-family: ui-monospace, monospace;
padding: 10px; border-radius: 6px; border: 1px solid var(--line); }}
.actions {{ display: flex; flex-wrap: wrap; gap: 10px; }}
button, .btn {{ appearance: none; border: 0; border-radius: 6px; padding: 10px 16px;
font-weight: 700; cursor: pointer; text-decoration: none; display: inline-flex;
align-items: center; background: var(--accent); color: #fff; }}
button.secondary, .btn.secondary {{ background: #e8eef4; color: var(--ink); }}
button.danger {{ background: var(--bad); }}
.ok {{ color: var(--ok); font-weight: 700; }}
.bad {{ color: var(--bad); font-weight: 700; }}
.note {{ font-size: .85rem; margin-top: 18px; }}
</style>
</head>
<body>
<main>
<p class="eyebrow">warden desk · build phase · localhost only</p>
{body}
</main>
</body>
</html>
"""
return doc.encode("utf-8")
def _render_home(session: DeskSession) -> bytes:
summary = html.escape(session.summary)
lane = html.escape(session.lane_id or "")
act = html.escape(session.act)
if session.result != "pending":
cls = "ok" if session.result in ("approved", "provisioned", "launched") else "bad"
return _page(
"Desk result",
f"<h1>Act complete</h1>"
f"<p class='{cls}'>Result: {html.escape(session.result)}</p>"
f"<p>{html.escape(session.message or '')}</p>"
f"<p class='note'>You can close this tab. Server will shut down shortly.</p>",
)
if session.act == "approve":
body = f"""
<h1>Founder approval</h1>
<p>{summary}</p>
<div class="box">lane: {lane}<br>act: {act}</div>
<form method="POST" action="/act">
<input type="hidden" name="token" value="{html.escape(session.token)}">
<div class="actions">
<button type="submit" name="decision" value="approve">Approve</button>
<button type="submit" name="decision" value="deny" class="danger">Deny</button>
</div>
</form>
<p class="note">Metadata-only no secrets transit this form.</p>
"""
elif session.act == "oidc_login":
cmd = html.escape(session.oidc_command or "bao login -method=oidc")
body = f"""
<h1>OIDC / identity login</h1>
<p>{summary}</p>
<div class="box">{cmd}</div>
<p>Run the command in your own terminal (browser OIDC). ops-warden never
captures the token.</p>
<form method="POST" action="/act">
<input type="hidden" name="token" value="{html.escape(session.token)}">
<div class="actions">
<button type="submit" name="decision" value="launched">I completed login</button>
<button type="submit" name="decision" value="deny" class="secondary">Cancel</button>
</div>
</form>
"""
else: # paste_once_provision
path = html.escape(session.path)
kv_field = html.escape(session.kv_field)
body = f"""
<h1>Paste-once provision</h1>
<p>{summary}</p>
<div class="box">OpenBao path: {path}<br>field: {kv_field}<br>lane: {lane}</div>
<p>Paste the secret <strong>once</strong>. It is written to OpenBao via
<code>bao kv put</code> and never shown in the terminal or audit log.</p>
<form method="POST" action="/act" autocomplete="off">
<input type="hidden" name="token" value="{html.escape(session.token)}">
<label for="secret">Secret value</label>
<textarea id="secret" name="secret" required placeholder="paste value here"></textarea>
<div class="actions">
<button type="submit" name="decision" value="provision">Write to OpenBao</button>
<button type="submit" name="decision" value="deny" class="secondary">Cancel</button>
</div>
</form>
<p class="note">Build-phase desk: localhost only, OS session trust.</p>
"""
return _page("warden desk", body)
def _provision_to_openbao(path: str, field: str, value: str) -> None:
"""Write one field to OpenBao without putting the value on argv."""
# bao kv put path field=- reads value from stdin
proc = subprocess.run(
["bao", "kv", "put", path, f"{field}=-"],
input=value.encode("utf-8"),
capture_output=True,
timeout=60,
check=False,
)
if proc.returncode != 0:
err = (proc.stderr or proc.stdout or b"").decode("utf-8", errors="replace")
# scrub accidental value echo
if value and value in err:
err = err.replace(value, "<redacted>")
raise DeskError(f"bao kv put failed (exit {proc.returncode}): {err[:300]}")
def make_handler(
session: DeskSession,
*,
on_done: Optional[Callable[[DeskSession], None]] = None,
dry_run: bool = False,
) -> type:
"""Build a request handler class closed over *session*."""
class Handler(BaseHTTPRequestHandler):
def log_message(self, fmt: str, *args: Any) -> None: # noqa: A003
# Avoid logging POST bodies / query secrets
line = f"[desk] {self.address_string()} {fmt % args}"
if session.token in line:
line = line.replace(session.token, "<token>")
print(line, flush=True)
def _deny(self, code: int = 404) -> None:
self.send_response(code)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
self.wfile.write(b"not found\n")
def do_GET(self) -> None: # noqa: N802
parsed = urlparse(self.path)
qs = parse_qs(parsed.query)
token = (qs.get("t") or [""])[0]
if parsed.path not in ("/", "/index.html") or token != session.token:
self._deny()
return
body = _render_home(session)
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_POST(self) -> None: # noqa: N802
parsed = urlparse(self.path)
if parsed.path != "/act":
self._deny()
return
length = int(self.headers.get("Content-Length") or "0")
raw = self.rfile.read(length) if length else b""
form = parse_qs(raw.decode("utf-8", errors="replace"), keep_blank_values=True)
token = (form.get("token") or [""])[0]
if token != session.token:
self._deny(403)
return
decision = (form.get("decision") or [""])[0]
try:
if session.act == "approve":
if decision == "approve":
session.result = "approved"
session.message = "Approved (metadata only)."
else:
session.result = "denied"
session.message = "Denied."
elif session.act == "oidc_login":
if decision == "launched":
session.result = "launched"
session.message = "Operator confirmed OIDC login completed."
else:
session.result = "denied"
session.message = "Cancelled."
elif session.act == "paste_once_provision":
if decision == "deny":
session.result = "denied"
session.message = "Cancelled — nothing written."
else:
secret = (form.get("secret") or [""])[0]
if not secret:
raise DeskError("empty secret value")
if dry_run:
session.result = "provisioned"
session.message = (
f"dry-run: would write field {session.kv_field!r} "
f"to {session.path}"
)
else:
_provision_to_openbao(session.path, session.kv_field, secret)
session.result = "provisioned"
session.message = (
f"Wrote field {session.kv_field!r} to {session.path} "
"(value not logged)."
)
# drop reference promptly
secret = ""
form.pop("secret", None)
else:
raise DeskError(f"unhandled act {session.act}")
except DeskError as e:
session.result = "error"
session.message = str(e)
body = _render_home(session)
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
if on_done and session.result != "pending":
on_done(session)
return Handler
def run_desk(
session: DeskSession,
*,
host: str = "127.0.0.1",
port: int = 0,
open_browser: bool = True,
dry_run: bool = False,
shutdown_after_done: bool = True,
) -> DeskSession:
"""Serve the desk until the act completes (or the process is interrupted).
Binds *host* (default loopback only). *port* 0 picks an ephemeral port.
"""
if host not in ("127.0.0.1", "localhost", "::1"):
raise DeskError(
f"desk refuses non-loopback bind {host!r} in build phase "
"(set host only for tests with 127.0.0.1)"
)
done = threading.Event()
def _on_done(_s: DeskSession) -> None:
if shutdown_after_done:
done.set()
handler = make_handler(session, on_done=_on_done, dry_run=dry_run)
server = ThreadingHTTPServer((host, port), handler)
bound_port = server.server_address[1]
url = f"http://{host}:{bound_port}/?t={session.token}"
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
print(f"warden desk listening on {url}", flush=True)
print(f"act={session.act} lane={session.lane_id or ''} (token not for logs elsewhere)", flush=True)
if open_browser:
try:
webbrowser.open(url)
except Exception: # noqa: BLE001
pass
try:
done.wait()
except KeyboardInterrupt:
session.result = session.result if session.result != "pending" else "denied"
session.message = session.message or "interrupted"
finally:
server.shutdown()
thread.join(timeout=2)
return session
def load_plan_json(path: Path) -> dict:
data = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise DeskError("plan JSON must be an object")
return data

46
src/warden/mask.py Normal file
View file

@ -0,0 +1,46 @@
"""Masking display filter for KV values (WARDEN-WP-0026 T03).
Defense-in-depth, **not a boundary**: any place warden would otherwise render a
secret value for a human (a status/listing view) shows a *fingerprint* instead
presence, length, and a short non-reversible hash. Two operators can compare
fingerprints to confirm they hold the same value (e.g. that a rotation landed the
expected token) without either seeing it, and a fingerprint in a transcript
discloses nothing.
Limitation (documented, by design): raw `bao kv get <path>` bypasses this entirely
warden only masks *warden-mediated* output. The real boundary is OpenBao policy
plus the T01 capabilities-safe verify and T02 no-stdout transports.
"""
from __future__ import annotations
import hashlib
from dataclasses import dataclass
# Short, non-reversible hash: first 8 hex chars of SHA-256. Not a value, and a
# collision is irrelevant for the "same/different?" comparison this supports.
_HASH_PREFIX_LEN = 8
@dataclass(frozen=True)
class Fingerprint:
present: bool
length: int
sha256_prefix: str # "" when the value is empty/absent
def render(self) -> str:
if not self.present:
return "absent"
return f"hidden len={self.length} sha256:{self.sha256_prefix}"
def fingerprint(value: str | None) -> Fingerprint:
"""Compute a non-reversible fingerprint of a value. Never returns the value."""
if not value:
return Fingerprint(present=False, length=0, sha256_prefix="")
digest = hashlib.sha256(value.encode("utf-8")).hexdigest()[:_HASH_PREFIX_LEN]
return Fingerprint(present=True, length=len(value), sha256_prefix=digest)
def mask_value(value: str | None) -> str:
"""Render a value as its masked fingerprint string. Never emits the value."""
return fingerprint(value).render()

View file

@ -53,6 +53,9 @@ class CertSpec:
principals: List[str]
identity: str = "" # defaults to actor_name if empty
policy_decision_id: Optional[str] = None
policy_zone: Optional[str] = None
policy_failure_mode: Optional[str] = None
policy_outcome: Optional[str] = None
def __post_init__(self) -> None:
if not self.identity:

436
src/warden/plan.py Normal file
View file

@ -0,0 +1,436 @@
"""Policy decision front door — ``warden plan`` (WARDEN-WP-0029 T01).
Composes the routing catalog, access handoff expansion, organization posture,
and flex-auth gate status into a typed verdict. Never holds secret values.
Does not re-implement keyword matching delegates to ``Catalog.find``.
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from typing import List, Optional
from warden.access import expand_handoff, policy_gate_status
from warden.posture import PostureCatalog, load_posture
from warden.routing.catalog import Catalog, load_catalog
from warden.routing.models import RouteEntry
VERDICTS = ("autonomous", "founder_required", "unroutable")
FOUNDER_ACT_KINDS = ("oidc_login", "approve", "paste_once_provision")
_PROVISION_SIGNS = re.compile(
r"\b(provision|mint|onboard|paste|first[- ]time|rotate\s+into|put\s+into\s+openbao)\b"
r"|\bnew\b.{0,40}\b(secret|token|pat|key|credential)\b"
r"|\bstore\s+(?:the\s+)?(?:pat|token|key|secret)\b",
re.IGNORECASE,
)
_APPROVAL_SIGNS = re.compile(
r"\b(approv|red[- ]lane|ccr|policy\s+enable|prod\s+flip|break[- ]glass)\b",
re.IGNORECASE,
)
@dataclass
class FounderAct:
kind: str # oidc_login | approve | paste_once_provision
summary: str
details: dict = field(default_factory=dict)
def to_dict(self) -> dict:
return {"kind": self.kind, "summary": self.summary, "details": dict(self.details)}
@dataclass
class AccessPlan:
need: str
verdict: str
organization_posture: str
policy_gate: str
lane_id: Optional[str] = None
lane_title: Optional[str] = None
match_score: Optional[int] = None
commands: List[str] = field(default_factory=list)
founder_act: Optional[FounderAct] = None
ccr_stub: Optional[dict] = None
catalog: dict = field(default_factory=dict)
candidates: List[dict] = field(default_factory=list)
reasons: List[str] = field(default_factory=list)
actor: Optional[str] = None
domain: Optional[str] = None
def to_dict(self) -> dict:
return {
"need": self.need,
"verdict": self.verdict,
"organization_posture": self.organization_posture,
"policy_gate": self.policy_gate,
"lane_id": self.lane_id,
"lane_title": self.lane_title,
"match_score": self.match_score,
"commands": list(self.commands),
"founder_act": self.founder_act.to_dict() if self.founder_act else None,
"ccr_stub": self.ccr_stub,
"catalog": dict(self.catalog),
"candidates": list(self.candidates),
"reasons": list(self.reasons),
"actor": self.actor,
"domain": self.domain,
}
def _org_posture_id(posture: Optional[PostureCatalog]) -> str:
if posture is None:
return "unknown"
return posture.organization_posture.id
def _candidate_row(entry: RouteEntry, score: int) -> dict:
return {
"id": entry.id,
"title": entry.title,
"score": score,
"status": entry.status,
"resolvable": entry.resolvable,
"exec_capable": entry.exec_capable,
"warden_executes": entry.warden_executes,
"lane": entry.lane,
"risk": entry.risk,
}
def _score_for(catalog: Catalog, entry: RouteEntry, need: str) -> int:
if entry.id == need.strip():
return 100
tokens = [t for t in need.lower().replace("-", " ").split() if t]
return entry.match_score(tokens)
def _concrete(value: Optional[str]) -> bool:
"""True when a template has no ``<...>`` placeholders left."""
if not value:
return False
return "<" not in value and ">" not in value
def _lane_is_autonomous(entry: RouteEntry) -> bool:
"""Whether an agent can proceed without a founder act for this lane."""
if entry.warden_executes:
return True
if entry.lane == "login":
return False
if entry.resolvable:
return True
if entry.has_native_exec and _concrete(entry.exec_command):
return True
# Concrete owner fetch path (even if not exec_capable) — value already in custody
if _concrete(entry.fetch_command):
return True
# Pure pointer — follow wiki, no secret mechanics for founder
if not entry.has_handoff and not entry.exec_capable and not entry.has_native_exec:
return True
return False
def _autonomous_commands(entry: RouteEntry, domain: Optional[str]) -> List[str]:
cmds: List[str] = []
if entry.warden_executes:
if entry.cert_command:
cmds.append(entry.cert_command)
for step in entry.steps[:4]:
cmds.append(f"# {step}")
return cmds
expanded = expand_handoff(entry, domain=domain)
if entry.lane == "login":
cmds.append(
f"warden access {entry.id} --exec -- <reviewed-command>"
)
if expanded.fetch_command:
cmds.append(
"# owner login is contained by warden; do not invoke it separately"
)
return cmds
if entry.has_native_exec and entry.exec_command:
cmds.append(entry.exec_command)
if entry.pointer_command:
cmds.append(entry.pointer_command)
if entry.exec_capable:
base = f"warden access {entry.id}"
if domain:
base += f" --domain {domain}"
if entry.is_high_risk:
cmds.append(f"{base} --exec -- <cmd> # high-risk: no raw stdout")
cmds.append(f"{base} --out FILE")
cmds.append(f"{base} --wrap")
else:
cmds.append(f"{base} --fetch")
cmds.append(f"{base} --exec -- <cmd>")
if expanded.fetch_command:
cmds.append(f"# owner fetch (as you): {expanded.fetch_command}")
elif _concrete(expanded.fetch_command or entry.fetch_command):
cmds.append(expanded.fetch_command or entry.fetch_command or "")
if entry.wiki_ref:
cmds.append(f"# playbook: {entry.wiki_ref}")
elif entry.wiki_ref:
cmds.append(f"# follow owner playbook: {entry.wiki_ref}")
return [c for c in cmds if c]
def _founder_for_entry(entry: RouteEntry, need: str, domain: Optional[str]) -> FounderAct:
expanded = expand_handoff(entry, domain=domain)
if entry.lane == "login":
contained_command = (
f"warden access {entry.id} --exec -- <reviewed-command>"
)
return FounderAct(
kind="oidc_login",
summary=f"Interactive OIDC/MFA login via {entry.owner_repo}",
details={
"lane_id": entry.id,
"auth_method": expanded.auth_method,
"fetch_command": contained_command,
"desk_hint": (
"warden desk --from-plan (act=oidc_login); execute only through: "
+ contained_command
),
},
)
if entry.lane == "ceremony":
return FounderAct(
kind="approve",
summary=f"Attended owner ceremony approval required for {entry.id}",
details={
"lane_id": entry.id,
"wiki_ref": entry.wiki_ref,
"desk_hint": "warden desk --act approve --lane " + entry.id,
},
)
if _APPROVAL_SIGNS.search(need):
return FounderAct(
kind="approve",
summary=f"Founder approval required for {entry.id}",
details={
"lane_id": entry.id,
"wiki_ref": entry.wiki_ref,
"desk_hint": "warden desk --act approve --lane " + entry.id,
},
)
# Default founder path for non-resolvable secret lanes: paste-once provision
path = expanded.path_template or entry.path_template or "<openbao-path>"
return FounderAct(
kind="paste_once_provision",
summary=(
f"Provision secret value once into OpenBao path for {entry.id} "
"(no CLI paste; use warden desk)"
),
details={
"lane_id": entry.id,
"path_template": path,
"auth_method": expanded.auth_method,
"desk_hint": (
f"warden desk --act paste_once_provision --lane {entry.id}"
+ (f" --path {path}" if "<" not in (path or "") else "")
),
},
)
def _ccr_stub(need: str) -> dict:
return {
"title": f"CCR: new credential lane for {need[:80]}",
"status": "proposed",
"owner_hint": "railiance-platform (OpenBao) or owning subsystem",
"steps": [
"Draft CCR with path, policy, OIDC role, consumers",
"Add ops-warden catalog entry (pointers only; no secret values)",
"Playbook under wiki/playbooks/; promote status active when live",
],
"commands": [
"warden route list --all",
"# after CCR: edit registry/routing/catalog.yaml + playbook",
],
}
def build_plan(
need: str,
*,
actor: Optional[str] = None,
domain: Optional[str] = None,
catalog: Optional[Catalog] = None,
posture: Optional[PostureCatalog] = None,
include_draft: bool = False,
) -> AccessPlan:
"""Resolve *need* to a typed access plan. Pure of secret values."""
cat = catalog or load_catalog()
try:
post = posture if posture is not None else load_posture()
except Exception: # noqa: BLE001 — plan still works without posture file
post = None
gate = policy_gate_status()
org = _org_posture_id(post)
freshness = cat.freshness().to_dict()
raw_matches = cat.find(need, include_draft=include_draft, limit=8)
# Require score >= 2 (at least one full keyword hit). Score-1 hits are usually
# accidental substring overlaps (e.g. title word "or" inside an unrelated token).
scored = [(e, _score_for(cat, e, need)) for e in raw_matches]
matches = [(e, s) for e, s in scored if s >= 2]
candidates = [_candidate_row(e, s) for e, s in scored[:5]]
if not matches:
return AccessPlan(
need=need,
verdict="unroutable",
organization_posture=org,
policy_gate=gate,
ccr_stub=_ccr_stub(need),
catalog=freshness,
candidates=candidates,
reasons=["no catalog match for need (score < 2)"],
actor=actor,
domain=domain,
)
entry, score = matches[0]
# Draft-only top match without active alternatives → unroutable
if entry.status == "draft" and not include_draft:
return AccessPlan(
need=need,
verdict="unroutable",
organization_posture=org,
policy_gate=gate,
lane_id=entry.id,
lane_title=entry.title,
match_score=score,
ccr_stub=_ccr_stub(need),
catalog=freshness,
candidates=candidates,
reasons=[f"top match {entry.id!r} is draft — promote or request CCR"],
actor=actor,
domain=domain,
)
# Login and ceremony lanes always need a human act. A ceremony is a pure
# owner pointer: it must never fall through to secret paste-once mechanics.
if entry.lane in ("login", "ceremony"):
act = _founder_for_entry(entry, need, domain)
return AccessPlan(
need=need,
verdict="founder_required",
organization_posture=org,
policy_gate=gate,
lane_id=entry.id,
lane_title=entry.title,
match_score=score,
commands=_autonomous_commands(entry, domain),
founder_act=act,
catalog=freshness,
candidates=candidates,
reasons=[
"login lane requires interactive founder/operator identity act"
if entry.lane == "login"
else "ceremony lane requires attended owner approval"
],
actor=actor,
domain=domain,
)
# Explicit approval language
if _APPROVAL_SIGNS.search(need) and not entry.warden_executes:
act = _founder_for_entry(entry, need, domain)
act.kind = "approve"
return AccessPlan(
need=need,
verdict="founder_required",
organization_posture=org,
policy_gate=gate,
lane_id=entry.id,
lane_title=entry.title,
match_score=score,
founder_act=act,
catalog=freshness,
candidates=candidates,
reasons=["need text requests founder approval"],
actor=actor,
domain=domain,
)
# Explicit first-time provision language wins even if a concrete lane matched
if _PROVISION_SIGNS.search(need) and entry.lane == "secret" and not entry.warden_executes:
act = _founder_for_entry(entry, need, domain)
return AccessPlan(
need=need,
verdict="founder_required",
organization_posture=org,
policy_gate=gate,
lane_id=entry.id,
lane_title=entry.title,
match_score=score,
commands=[],
founder_act=act,
catalog=freshness,
candidates=candidates,
reasons=["need requires first-time provision — one founder act via warden desk"],
actor=actor,
domain=domain,
)
if _lane_is_autonomous(entry):
return AccessPlan(
need=need,
verdict="autonomous",
organization_posture=org,
policy_gate=gate,
lane_id=entry.id,
lane_title=entry.title,
match_score=score,
commands=_autonomous_commands(entry, domain),
catalog=freshness,
candidates=candidates,
reasons=["lane is usable under current catalog without founder mechanics"],
actor=actor,
domain=domain,
)
# Template / non-concrete secret handoff → founder paste-once
if entry.lane == "secret":
act = _founder_for_entry(entry, need, domain)
return AccessPlan(
need=need,
verdict="founder_required",
organization_posture=org,
policy_gate=gate,
lane_id=entry.id,
lane_title=entry.title,
match_score=score,
commands=[],
founder_act=act,
catalog=freshness,
candidates=candidates,
reasons=[
"lane handoff still has placeholders or needs provision — "
"one founder act via warden desk"
],
actor=actor,
domain=domain,
)
# Fallback: autonomous with best-effort commands
return AccessPlan(
need=need,
verdict="autonomous",
organization_posture=org,
policy_gate=gate,
lane_id=entry.id,
lane_title=entry.title,
match_score=score,
commands=_autonomous_commands(entry, domain),
catalog=freshness,
candidates=candidates,
reasons=["matched lane; proceed via catalog handoff"],
actor=actor,
domain=domain,
)

View file

@ -1,13 +1,15 @@
"""flex-auth policy gate for SSH signing (opt-in via warden.yaml)."""
"""Zone-aware flex-auth policy gates for OpsWarden."""
from __future__ import annotations
import hashlib
import json
import os
from pathlib import Path
import httpx
from warden.ca import CAError
from warden.caller_identity import CallerIdentityError, caller_auth_headers
from warden.config import PolicyConfig
from warden.models import CertSpec
@ -19,6 +21,60 @@ def pubkey_fingerprint(pubkey_path: Path) -> str:
return f"sha256:{digest}"
def _caller_headers(cfg: PolicyConfig, *, fail_closed: bool) -> dict[str, str]:
"""Bearer header identifying ops-warden itself to flex-auth (FLEX-WP-0016).
When the token cannot be obtained we refuse the call under the selected
zone's ``fail_closed`` behavior
rather than silently falling back to an unauthenticated request an
unauthenticated call is exactly what keeps the flex-auth pin in ``warn``.
"""
try:
return caller_auth_headers(cfg.caller_auth)
except CallerIdentityError as e:
if fail_closed:
raise CAError(f"flex-auth caller identity unavailable: {e}") from e
return {}
def _resource_zone(cfg: PolicyConfig, resource_id: str) -> str:
"""Read a compiled resource zone; absence or ambiguity is always unknown."""
if cfg.zone_registry_path is None:
return "unknown"
try:
registry = json.loads(cfg.zone_registry_path.read_text())
resources = registry["resource_manifests"][0]["resources"]
resource = next(item for item in resources if item.get("id") == resource_id)
attributes = resource.get("attributes") or {}
if attributes.get("security_zone_admission") == "not-applicable":
return "not-applicable"
zone = str(attributes.get("security_zone") or "unknown")
return zone if zone in cfg.failure_modes else "unknown"
except (OSError, ValueError, KeyError, StopIteration, TypeError):
return "unknown"
def _is_fail_closed(cfg: PolicyConfig, zone: str) -> bool:
return cfg.failure_modes.get(zone, cfg.failure_modes["unknown"]) == "fail_closed"
def _evaluator_failure(
message: str,
*,
fail_closed: bool,
cause: Exception | None = None,
spec: CertSpec | None = None,
) -> None:
if fail_closed:
if spec is not None:
spec.policy_outcome = "fail_closed"
if cause is None:
raise CAError(message)
raise CAError(message) from cause
if spec is not None:
spec.policy_outcome = "fail_open"
def _subject_id(cfg: PolicyConfig, spec: CertSpec) -> str:
return os.environ.get(cfg.subject_env, "").strip() or spec.actor_name
@ -26,11 +82,21 @@ def _subject_id(cfg: PolicyConfig, spec: CertSpec) -> str:
def check_sign_policy(cfg: PolicyConfig, spec: CertSpec) -> str | None:
"""Call flex-auth /v1/check before signing.
Returns decision id when policy is enabled and effect is allow.
Returns None when policy is disabled.
Raises CAError on deny or when fail_closed and flex-auth is unreachable.
Returns a decision id on ``allow`` or ``audit_only``. A deny always blocks.
Evaluator failures use the PEP-owned failure mode for the target workload's
compiled zone; absent resolution is the explicit ``unknown`` profile.
"""
if not cfg.enabled:
resource_id = f"ssh-cert:actor/{spec.actor_name}"
zone = _resource_zone(cfg, resource_id)
fail_closed = _is_fail_closed(cfg, zone)
spec.policy_zone = zone
spec.policy_failure_mode = "fail_closed" if fail_closed else "fail_open"
if cfg.flex_auth_url is None:
_evaluator_failure(
f"flex-auth URL is not configured for security zone {zone!r}",
fail_closed=fail_closed,
spec=spec,
)
return None
pubkey_path = Path(os.path.expanduser(str(spec.pubkey_path)))
@ -60,36 +126,54 @@ def check_sign_policy(cfg: PolicyConfig, spec: CertSpec) -> str | None:
}
url = cfg.flex_auth_url.rstrip("/") + "/v1/check"
headers = _caller_headers(cfg, fail_closed=fail_closed)
try:
response = httpx.post(url, json=request, timeout=10.0)
response = httpx.post(url, json=request, headers=headers, timeout=10.0)
response.raise_for_status()
except httpx.HTTPStatusError as e:
if cfg.fail_closed:
raise CAError(
f"flex-auth denied or rejected sign policy check (HTTP {e.response.status_code})"
) from e
_evaluator_failure(
f"flex-auth rejected sign policy check (HTTP {e.response.status_code}) "
f"for security zone {zone!r}",
fail_closed=fail_closed,
cause=e,
spec=spec,
)
return None
except httpx.RequestError as e:
if cfg.fail_closed:
raise CAError(
f"flex-auth unreachable at {cfg.flex_auth_url!r} "
f"(fail_closed=true): {e}"
) from e
_evaluator_failure(
f"flex-auth unreachable at {cfg.flex_auth_url!r} for security zone {zone!r}",
fail_closed=fail_closed,
cause=e,
spec=spec,
)
return None
try:
decision = response.json()
except ValueError as e:
raise CAError("flex-auth returned non-JSON decision") from e
_evaluator_failure(
f"flex-auth returned a non-JSON decision for security zone {zone!r}",
fail_closed=fail_closed,
cause=e,
spec=spec,
)
return None
effect = str(decision.get("effect", "")).lower()
decision_id = decision.get("id") or decision.get("request_id")
if effect != "allow":
if effect not in {"allow", "audit_only"}:
spec.policy_outcome = "deny"
reason = decision.get("reason") or "no reason provided"
raise CAError(f"flex-auth denied SSH sign for {spec.actor_name!r}: {reason}")
if not decision_id:
raise CAError("flex-auth allow decision missing id")
_evaluator_failure(
f"flex-auth {effect} decision missing id for security zone {zone!r}",
fail_closed=fail_closed,
spec=spec,
)
return None
spec.policy_outcome = effect
return str(decision_id)
@ -99,13 +183,17 @@ def check_fetch_policy(
"""Call flex-auth /v1/check before proxying a non-SSH credential fetch (WP-0014).
The action is ``read`` on a ``secret`` resource owned by another subsystem
ops-warden is the conduit, not the owner. Returns the decision id on allow,
None when policy is disabled, and raises CAError on deny (or on an unreachable
flex-auth when fail_closed). No secret value is ever part of this request.
ops-warden is the conduit, not the owner. Unresolved target workload identity
selects the explicit ``unknown`` profile; no secret value enters the request.
"""
if not cfg.enabled:
zone = "unknown"
fail_closed = _is_fail_closed(cfg, zone)
if cfg.flex_auth_url is None:
_evaluator_failure(
"flex-auth URL is not configured for security zone 'unknown'",
fail_closed=fail_closed,
)
return None
subject_id = os.environ.get(cfg.subject_env, "").strip() or "operator"
request = {
"subject": {"id": subject_id, "type": "operator", "tenant": cfg.tenant},
@ -120,32 +208,44 @@ def check_fetch_policy(
}
url = cfg.flex_auth_url.rstrip("/") + "/v1/check"
headers = _caller_headers(cfg, fail_closed=fail_closed)
try:
response = httpx.post(url, json=request, timeout=10.0)
response = httpx.post(url, json=request, headers=headers, timeout=10.0)
response.raise_for_status()
except httpx.HTTPStatusError as e:
if cfg.fail_closed:
raise CAError(
f"flex-auth denied or rejected fetch policy check (HTTP {e.response.status_code})"
) from e
_evaluator_failure(
f"flex-auth rejected fetch policy check (HTTP {e.response.status_code})",
fail_closed=fail_closed,
cause=e,
)
return None
except httpx.RequestError as e:
if cfg.fail_closed:
raise CAError(
f"flex-auth unreachable at {cfg.flex_auth_url!r} (fail_closed=true): {e}"
) from e
_evaluator_failure(
f"flex-auth unreachable at {cfg.flex_auth_url!r} for security zone 'unknown'",
fail_closed=fail_closed,
cause=e,
)
return None
try:
decision = response.json()
except ValueError as e:
raise CAError("flex-auth returned non-JSON decision") from e
_evaluator_failure(
"flex-auth returned a non-JSON decision for security zone 'unknown'",
fail_closed=fail_closed,
cause=e,
)
return None
effect = str(decision.get("effect", "")).lower()
decision_id = decision.get("id") or decision.get("request_id")
if effect != "allow":
if effect not in {"allow", "audit_only"}:
reason = decision.get("reason") or "no reason provided"
raise CAError(f"flex-auth denied secret read for {need_id!r}: {reason}")
if not decision_id:
raise CAError("flex-auth allow decision missing id")
_evaluator_failure(
f"flex-auth {effect} decision missing id for security zone 'unknown'",
fail_closed=fail_closed,
)
return None
return str(decision_id)

View file

@ -42,6 +42,16 @@ class MaturityLevel:
promotion_gate: List[str]
@dataclass
class OrganizationPosture:
"""Fleet lifecycle posture (WARDEN-WP-0029) — third axis, not env/maturity."""
id: str
summary: str
relaxations: List[str]
graduation_triggers: List[str]
@dataclass
class PostureCatalog:
path: Path
@ -49,6 +59,7 @@ class PostureCatalog:
maturity_levels: List[MaturityLevel]
dataclass_floor: Dict[str, str] # dataclass -> maturity id
requires_env_posture: str # lattice: posture a secret fetch requires
organization_posture: OrganizationPosture
# --- lookups ----------------------------------------------------------
def env(self, env_id: str) -> Optional[EnvPosture]:
@ -184,10 +195,24 @@ def load_posture(path: Optional[Path] = None) -> PostureCatalog:
if not any(e.id == requires_env for e in env_postures):
raise PostureError(f"lattice requires_env_posture {requires_env!r} is not an env posture")
org_raw = raw.get("organization_posture") or {}
if not isinstance(org_raw, dict) or not org_raw.get("id"):
raise PostureError(
"posture descriptors need organization_posture with at least an id "
"(WARDEN-WP-0029 third axis)"
)
organization_posture = OrganizationPosture(
id=str(org_raw["id"]),
summary=str(org_raw.get("summary") or "").strip(),
relaxations=[str(x) for x in (org_raw.get("relaxations") or [])],
graduation_triggers=[str(x) for x in (org_raw.get("graduation_triggers") or [])],
)
return PostureCatalog(
path=posture_path,
env_postures=env_postures,
maturity_levels=maturity_levels,
dataclass_floor=dataclass_floor,
requires_env_posture=requires_env,
organization_posture=organization_posture,
)

View file

@ -8,11 +8,13 @@ intact. Three guardrails are enforced here in code:
caller's own environment. ops-warden injects no token of its own; if the caller has
no credential, the underlying tool fails and we surface the auth pointer. We never
add a `*_TOKEN` warden owns to the child environment.
* **G2 transit only, no persistence/logging of values.** ``proxy_fetch`` runs the
tool with **inherited** stdout/stderr (never a pipe), so the value streams to the
caller and never enters warden's memory. ``proxy_exec`` reads the value solely to
place it in a child process's environment (the accepted proxy tradeoff) and never
writes it to disk or log. The audit record is metadata only.
* **G2 bounded transports, no logging of values.** Ordinary ``proxy_fetch`` runs
the tool with inherited stdout/stderr so the value never enters warden's memory;
sanctioned exec/file transports hold it only for their bounded handoff. The
high-risk attended-login lane is stricter: it captures every client byte inside
an isolated helper session, never returns that output, requires successful
persistence to a private token helper, self-revokes, and cleans up. Audit records
are metadata only.
* **G3 policy gate before fetch.** The CLI runs ``check_fetch_policy`` before
calling anything here; this module refuses to run an unresolved command template.
@ -24,7 +26,10 @@ import json
import os
import re
import shlex
import shutil
import stat
import subprocess
import tempfile
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
@ -33,6 +38,9 @@ from typing import List, Optional
from warden.routing.models import RouteEntry
_PLACEHOLDER = re.compile(r"<[^>]+>")
_OPENBAO_TOKEN = re.compile(rb"\b(?:hvs|hvb|hvr)\.[A-Za-z0-9_-]{8,}\b")
_ATTENDED_LOGIN_ROOT = ".warden-attended-login"
_TOKEN_HELPER_NAME = ".vault-token"
@dataclass(frozen=True)
@ -205,6 +213,385 @@ def proxy_fetch(resolved: ResolvedFetch) -> int:
return completed.returncode
def _assert_owned_mode(path: Path, *, mode: int, directory: bool) -> None:
"""Require a caller-owned, non-symlink path with an exact private mode."""
info = path.lstat()
expected_type = stat.S_ISDIR if directory else stat.S_ISREG
if not expected_type(info.st_mode) or stat.S_ISLNK(info.st_mode):
raise ProxyError("attended login private storage has an unsafe path type")
if hasattr(os, "getuid") and info.st_uid != os.getuid():
raise ProxyError("attended login private storage is not caller-owned")
if stat.S_IMODE(info.st_mode) != mode:
raise ProxyError("attended login private storage has an unsafe mode")
def _prepare_attended_login_home() -> tuple[Path, Path, bool]:
"""Create and prove an isolated token-helper home before authentication."""
home = Path.home()
try:
home_info = home.lstat()
except OSError as exc:
raise ProxyError(
"attended login requires a usable writable default home before OIDC"
) from exc
if (
not stat.S_ISDIR(home_info.st_mode)
or stat.S_ISLNK(home_info.st_mode)
or stat.S_IMODE(home_info.st_mode) & 0o222 == 0
):
raise ProxyError(
"attended login requires a usable writable default home before OIDC"
)
# Prove the default home itself is writable. A pre-existing writable child must
# not let a newly read-only HOME reach the OIDC process.
probe_fd = -1
probe_path: Path | None = None
probe_cleanup_error: OSError | None = None
try:
probe_fd, probe_name = tempfile.mkstemp(prefix=".warden-home-probe-", dir=home)
probe_path = Path(probe_name)
os.write(probe_fd, b"preflight")
os.fsync(probe_fd)
except OSError as exc:
raise ProxyError(
"attended login requires a usable writable default home before OIDC"
) from exc
finally:
if probe_fd >= 0:
os.close(probe_fd)
if probe_path is not None:
try:
probe_path.unlink()
except OSError as exc:
probe_cleanup_error = exc
if probe_cleanup_error is not None:
raise ProxyError("attended login home preflight cleanup failed") from probe_cleanup_error
root = home / _ATTENDED_LOGIN_ROOT
root_created = False
try:
root.mkdir(mode=0o700)
root_created = True
except FileExistsError:
pass
except OSError as exc:
raise ProxyError("could not establish attended login private storage") from exc
_assert_owned_mode(root, mode=0o700, directory=True)
try:
session = Path(tempfile.mkdtemp(prefix="session-", dir=root))
session.chmod(0o700)
_assert_owned_mode(session, mode=0o700, directory=True)
helper = session / _TOKEN_HELPER_NAME
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
fd = os.open(helper, flags, 0o600)
try:
# Exercise persistence before auth, then leave the helper empty for bao.
os.write(fd, b"preflight")
os.fsync(fd)
os.ftruncate(fd, 0)
finally:
os.close(fd)
_assert_owned_mode(helper, mode=0o600, directory=False)
except (OSError, ProxyError) as exc:
if "session" in locals():
shutil.rmtree(session, ignore_errors=True)
if root_created:
try:
root.rmdir()
except OSError:
pass
if isinstance(exc, ProxyError):
raise
raise ProxyError("could not establish attended login private storage") from exc
return root, session, root_created
def _contained_run(argv: List[str], *, env: dict) -> subprocess.CompletedProcess:
"""Run with both output streams captured and never forwarded."""
return subprocess.run( # noqa: S603
argv,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=None,
env=env,
check=False,
)
def _output_bytes(completed: subprocess.CompletedProcess) -> bytes:
stdout = completed.stdout
stderr = completed.stderr
if isinstance(stdout, str):
stdout = stdout.encode("utf-8", errors="replace")
if isinstance(stderr, str):
stderr = stderr.encode("utf-8", errors="replace")
stdout = stdout if isinstance(stdout, bytes) else b""
stderr = stderr if isinstance(stderr, bytes) else b""
return stdout + b"\n" + stderr
def _revoke_contained(
bao_binary: str,
*,
env: dict,
possible_output: bytes,
) -> bool:
"""Attempt self-revocation without exposing helper or captured output."""
revoke_env = dict(env)
try:
first = _contained_run(
[bao_binary, "token", "revoke", "-self"], env=revoke_env
)
except OSError:
return False
if first.returncode == 0:
return True
# A helper-persistence failure can leave the issued token only in contained
# client output. Use it solely for immediate self-revocation, never for a log,
# return value, hash, fingerprint, file, or argv.
match = _OPENBAO_TOKEN.search(possible_output)
if match is None:
return False
token = match.group(0).decode("ascii")
revoke_env["BAO_TOKEN"] = token
revoke_env.pop("VAULT_TOKEN", None)
try:
try:
second = _contained_run(
[bao_binary, "token", "revoke", "-self"], env=revoke_env
)
except OSError:
return False
return second.returncode == 0
finally:
revoke_env.pop("BAO_TOKEN", None)
token = "" # noqa: F841 - best-effort release of the credential reference
def proxy_attended_login_exec(
resolved: ResolvedFetch,
*,
child_argv: List[str],
) -> int:
"""Run an attended login and one silent child inside a private helper home.
The default home is proven writable before the OIDC client starts. Login,
child, and revocation output are captured and discarded. A successful login
may return client output only after the private helper has been populated;
persistence defects and non-zero results fail closed. The reviewed child must
remain silent. Any possibly issued token is revoked before the isolated helper
directory is removed.
"""
if not child_argv:
raise ProxyError(
"attended login requires --exec -- <reviewed-command>; a persistent "
"login-only handoff is not permitted"
)
if (
resolved.argv is None
or len(resolved.argv) < 2
or Path(resolved.argv[0]).name != "bao"
or resolved.argv[1] != "login"
):
raise ProxyError("attended login requires a direct bao login argv")
root, session, root_created = _prepare_attended_login_home()
helper = session / _TOKEN_HELPER_NAME
env = _caller_env()
if not env.get("WARDEN_CONFIG"):
caller_config = Path.home() / ".config" / "warden" / "warden.yaml"
if caller_config.is_file():
env["WARDEN_CONFIG"] = str(caller_config)
env["HOME"] = str(session)
env.pop("BAO_TOKEN", None)
env.pop("VAULT_TOKEN", None)
for key in (
"BAO_LOG_LEVEL",
"BAO_LOG_FORMAT",
"VAULT_LOG_LEVEL",
"VAULT_LOG_FORMAT",
):
env.pop(key, None)
login_argv = list(resolved.argv)
if not any(arg == "-format" or arg.startswith("-format=") for arg in login_argv):
login_argv.append("-format=json")
try:
try:
login = _contained_run(login_argv, env=env)
except OSError as exc:
raise ProxyError("attended login client could not start before OIDC") from exc
login_output = _output_bytes(login)
helper_valid = False
try:
_assert_owned_mode(helper, mode=0o600, directory=False)
helper_valid = helper.stat().st_size > 0
except (OSError, ProxyError):
helper_valid = False
if login.returncode != 0 or not helper_valid:
revoked = _revoke_contained(
resolved.argv[0], env=env, possible_output=login_output
)
status = "revoked" if revoked else "revocation could not be confirmed"
raise ProxyError(
"attended login failed closed before command handoff; any possible "
f"issued session was contained and {status}"
)
try:
child = _contained_run(child_argv, env=env)
except OSError as exc:
revoked = _revoke_contained(
resolved.argv[0], env=env, possible_output=b""
)
status = "revoked" if revoked else "revocation could not be confirmed"
raise ProxyError(
"attended command could not start; the login session was " + status
) from exc
child_output = _output_bytes(child)
revoked = _revoke_contained(
resolved.argv[0], env=env, possible_output=child_output
)
if child.returncode != 0 or child_output.strip():
status = "revoked" if revoked else "revocation could not be confirmed"
raise ProxyError(
"attended command failed closed because it returned a failure or "
f"unexpected output; the login session was {status}"
)
if not revoked:
raise ProxyError(
"attended command completed but session revocation could not be confirmed"
)
return 0
finally:
try:
shutil.rmtree(session)
if root_created:
root.rmdir()
except OSError as exc:
raise ProxyError("attended login private storage cleanup failed") from exc
def _capture_value(resolved: ResolvedFetch) -> str:
"""Run the fetch and return its stdout (the value) minus one trailing newline.
The value transits warden's memory (the accepted proxy tradeoff for the
non-stdout transports) but is never written to disk or log by this function.
"""
env = _caller_env()
if resolved.argv is not None:
fetched = subprocess.run( # noqa: S603
resolved.argv, stdout=subprocess.PIPE, stderr=None, stdin=None,
env=env, check=False, text=True,
)
else:
fetched = subprocess.run( # noqa: S602
resolved.shell_cmd, shell=True, stdout=subprocess.PIPE, stderr=None,
stdin=None, env=env, check=False, text=True,
)
if fetched.returncode != 0:
raise ProxyError(
f"fetch failed (exit {fetched.returncode}) — check caller auth and the path."
)
value = fetched.stdout
if value.endswith("\n"):
value = value[:-1]
return value
def proxy_fetch_to_file(resolved: ResolvedFetch, out_path: Path) -> int:
"""Fetch the value and write it to ``out_path`` at mode 0600 — never to stdout.
A sanctioned transport (WP-0026 T02): the value goes to a private file the
caller controls, not a terminal or a logged stream. The file is created with
O_EXCL semantics widened to truncate-if-owned so a re-fetch overwrites, but the
mode is forced to 0600 before any bytes are written.
"""
value = _capture_value(resolved)
# Open with restrictive mode from the start; do not echo the value anywhere.
fd = os.open(str(out_path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
try:
os.chmod(out_path, 0o600) # enforce even if the file pre-existed with looser mode
with os.fdopen(fd, "w") as fh:
fh.write(value)
finally:
value = "" # noqa: F841 — best-effort scrub of the local reference
return 0
def is_bao_kv_fetch(entry: RouteEntry) -> bool:
"""True when a lane's fetch is a plain ``bao kv get`` (wrappable, WP-0026 T02)."""
return bool(entry.fetch_command and entry.fetch_command.strip().startswith("bao kv get"))
def build_wrapped_fetch(
entry: RouteEntry, *, path: Optional[str] = None, ttl: str = "5m"
) -> ResolvedFetch:
"""Build a response-wrapping fetch: ``bao kv get -wrap-ttl=<ttl> -format=json <path>``.
Response wrapping returns a single-use, short-TTL *wrapping token* instead of the
secret value the sanctioned way to move a value between processes (WP-0026 T02).
The caller unwraps it in their own context (`bao unwrap`). Only valid for plain
``bao kv get`` lanes; the whole secret is wrapped (a per-field ``-field`` read
cannot be wrapped).
"""
if not is_bao_kv_fetch(entry):
raise ProxyError(
f"{entry.id!r} is not a plain `bao kv get` lane — response wrapping "
"(--wrap) is unavailable. Use --out FILE or --exec instead."
)
target = path or entry.path_template
if not target or _PLACEHOLDER.search(target):
raise ProxyError(
"--wrap needs a concrete path — supply --path or a resolved path_template."
)
return ResolvedFetch(argv=["bao", "kv", "get", f"-wrap-ttl={ttl}", "-format=json", target])
def proxy_fetch_wrapped(resolved: ResolvedFetch) -> str:
"""Run a wrapping fetch and return the wrapping *token* (not the secret value).
The token is single-use and short-lived; it is not itself the credential, so it
is safe to hand back on stdout. Parses OpenBao's ``-format=json`` wrap_info.
"""
raw = _capture_value(resolved)
try:
data = json.loads(raw)
token = data["wrap_info"]["token"]
except (json.JSONDecodeError, KeyError, TypeError) as e:
raise ProxyError(
"could not parse a wrapping token from the fetch output "
"(is response wrapping supported for this path?)."
) from e
if not token:
raise ProxyError("empty wrapping token returned.")
return str(token)
def proxy_fetch_fingerprint(resolved: ResolvedFetch):
"""Fetch the value and return a masked fingerprint — never the value (T03).
Defense-in-depth status view: lets an operator confirm presence/length and
compare a short non-reversible hash without disclosing the secret. The value
transits warden's memory only to be hashed, and is scrubbed immediately.
"""
from warden.mask import fingerprint
value = _capture_value(resolved)
try:
return fingerprint(value)
finally:
value = "" # noqa: F841 — best-effort scrub
def proxy_exec(resolved: ResolvedFetch, *, env_var: str, child_argv: List[str]) -> int:
"""Fetch the value and inject it into a child command's environment only.

View file

@ -5,13 +5,22 @@ subsystem. It loads the machine-readable routing catalog and answers "who owns
this need and where is the authoritative doc". The one lane ops-warden executes
(SSH certificate issuance) is the only entry that carries authored steps.
"""
from warden.routing.catalog import Catalog, CatalogError, find_catalog_path, load_catalog
from warden.routing.models import RouteEntry
from warden.routing.catalog import (
Catalog,
CatalogError,
CatalogFreshness,
find_catalog_path,
load_catalog,
)
from warden.routing.models import Delegation, RouteEntry, WorkloadReference
__all__ = [
"Catalog",
"CatalogError",
"CatalogFreshness",
"Delegation",
"RouteEntry",
"WorkloadReference",
"find_catalog_path",
"load_catalog",
]

View file

@ -13,16 +13,25 @@ never restates another subsystem's procedure.
"""
from __future__ import annotations
import hashlib
import os
import re
from dataclasses import dataclass
from datetime import date
from dataclasses import dataclass, field
from datetime import date, datetime, timezone
from pathlib import Path
from typing import List, Optional
import yaml
from warden.routing.models import RouteEntry
from warden.routing.models import (
VALID_DELEGATION_MODES,
VALID_RISK,
VALID_WORKLOAD_APPLICABILITY,
Delegation,
RotationGuide,
RouteEntry,
WorkloadReference,
)
# Structured handoff string fields (WP-0014) — templates and pointers only.
# Every one is scanned for accidental secret material; see _assert_no_secret_material.
@ -57,13 +66,53 @@ _REQUIRED_FIELDS = (
"canon_ref",
"reviewed",
"status",
"workload_ref",
)
_VALID_STATUS = ("active", "draft")
_VALID_LANES = ("secret", "login")
_VALID_LANES = ("secret", "login", "ceremony")
_VALID_ROTATION_METHODS = ("rotate", "re-establish")
# Default review cadence — see wiki/AccessRouting.md#drift-review-cadence
# Default review cadence for a catalog pointer — "is this still the right owner
# and page?" That is a genuinely quarterly question, so 90 days is right for it.
# See wiki/AccessRouting.md#drift-review-cadence
DEFAULT_STALE_DAYS = 90
# Cadence for an interim lane's *blocker*, which is a different kind of claim
# with a much shorter half-life: "has the intended owner answered / can they
# front this yet?" (WARDEN-WP-0033-T05).
#
# 14 rather than 90 because 90 was never a loose default, it was an inert one --
# the delegation register was created 2026-08-15, so a 90-day threshold could not
# fire before November and never had. Calibrated instead against blockers that
# actually went stale: the secrets-engine lanes cost ten days, RISK-F-0001
# invalidated an ops-warden blocker in one, and the FLEX-WP-0007 claim was
# repeated by two repos for roughly fifty. 14 catches the ten-day cases and, at
# ~15 interim lanes, surfaces about one lane a day rather than a wall of them.
DEFAULT_BLOCKER_STALE_DAYS = 14
# Scaled by the lane's own risk grade, matching risk-nexus's stall windows
# (14d critical/high, 30d medium, 60d low — docs/method/check-procedure.md).
# They offered the convention rather than a joint tool: point `warden route gaps`
# at the same windows and the two registers agree without a shared mechanism.
#
# `ungraded` gets the shortest window, not the longest. ADR-0007 already decided
# an absent grade is a defect and ADR-0008 that a grade covers the whole path;
# a lane nobody has graded is exactly the one whose blocker is least trustworthy.
BLOCKER_STALE_DAYS_BY_RISK = {
"high": 14,
"ungraded": 14,
"standard": 30,
"accepted": 60,
"low": 60,
}
def blocker_stale_days(risk: Optional[str], override: Optional[int] = None) -> int:
"""Days a lane's blocker may go unverified, scaled by what the lane holds."""
if override is not None:
return override
return BLOCKER_STALE_DAYS_BY_RISK.get(risk or "ungraded", DEFAULT_BLOCKER_STALE_DAYS)
def days_since_review(reviewed: str, *, today: Optional[date] = None) -> int:
"""Calendar days between reviewed date (YYYY-MM-DD) and today."""
@ -111,6 +160,45 @@ def find_catalog_path(start: Optional[Path] = None) -> Path:
)
@dataclass
class CatalogFreshness:
"""Install vs source freshness for the routing catalog (WARDEN-WP-0029 T05).
Surfaces the path that was loaded, whether it is the wheel-bundled fallback
(the stale-CLI failure mode), a content hash, and entry review age. Never
carries secret material.
"""
path: str
source: str # "override" | "repo" | "bundled"
content_hash: str
mtime_iso: str
package_version: str
entry_count: int
active_count: int
newest_reviewed: Optional[str]
oldest_reviewed: Optional[str]
stale_entry_count: int
using_bundled: bool
warnings: List[str] = field(default_factory=list)
def to_dict(self) -> dict:
return {
"path": self.path,
"source": self.source,
"content_hash": self.content_hash,
"mtime_iso": self.mtime_iso,
"package_version": self.package_version,
"entry_count": self.entry_count,
"active_count": self.active_count,
"newest_reviewed": self.newest_reviewed,
"oldest_reviewed": self.oldest_reviewed,
"stale_entry_count": self.stale_entry_count,
"using_bundled": self.using_bundled,
"warnings": list(self.warnings),
}
@dataclass
class Catalog:
path: Path
@ -160,8 +248,131 @@ class Catalog:
if is_review_stale(e.reviewed, threshold_days=threshold_days, today=today)
]
def gaps(self, include_draft: bool = False) -> List[RouteEntry]:
"""Interim lanes — the queryable delegation register (WARDEN-WP-0030)."""
return [e for e in self.listed(include_draft=include_draft) if e.is_interim]
def _assert_no_secret_material(entry_id: str, field_name: str, value: str) -> None:
def stale_gaps(
self,
include_draft: bool = False,
threshold_days: Optional[int] = None,
*,
today: Optional[date] = None,
) -> List[RouteEntry]:
"""Interim lanes whose blocker is due a re-check.
The window scales with the lane's risk grade unless `threshold_days`
overrides it -- a blocker on a lane holding an admin PAT should not go
unverified as long as one on a low-risk pointer.
A lane counts as stale when its review date is past the threshold **or**
when the review was never a verification at all. An `asked-and-waiting`
entry is the case that motivated this: it looks freshly reviewed on the
day the question is asked and stays that way while nobody answers.
"""
out: List[RouteEntry] = []
for e in self.gaps(include_draft=include_draft):
d = e.effective_delegation
reviewed = d.reviewed or e.reviewed
window = blocker_stale_days(e.risk, threshold_days)
if is_review_stale(reviewed, threshold_days=window, today=today):
out.append(e)
elif d.verified is not None and not d.is_verified:
out.append(e)
return out
def freshness(
self,
*,
stale_threshold_days: int = DEFAULT_STALE_DAYS,
today: Optional[date] = None,
) -> CatalogFreshness:
"""Describe which catalog was loaded and how fresh it is (WP-0029 T05)."""
path = self.path.resolve()
text = path.read_text(encoding="utf-8") if path.exists() else ""
digest = hashlib.sha256(text.encode("utf-8")).hexdigest()[:12]
mtime_iso = ""
if path.exists():
mtime_iso = datetime.fromtimestamp(
path.stat().st_mtime, tz=timezone.utc
).isoformat()
source = _classify_catalog_source(path)
using_bundled = source == "bundled"
reviewed_dates = [e.reviewed for e in self.entries if e.reviewed]
newest = max(reviewed_dates) if reviewed_dates else None
oldest = min(reviewed_dates) if reviewed_dates else None
stale_count = len(self.stale(include_draft=True, threshold_days=stale_threshold_days, today=today))
package_version = _package_version()
warnings: List[str] = []
if using_bundled:
warnings.append(
"using wheel-bundled catalog fallback — reinstall from checkout "
"(`uv tool install -e .` or `pip install -e .`) if lanes look missing"
)
if stale_count:
warnings.append(
f"{stale_count} catalog entr{'y' if stale_count == 1 else 'ies'} "
f"past {stale_threshold_days}d review cadence"
)
# Interim blockers run on their own, much shorter cadence -- a stale
# pointer and an unanswered blocker are not the same kind of drift.
stale_interim = len(self.stale_gaps(include_draft=True, today=today))
if stale_interim:
warnings.append(
f"{stale_interim} interim delegation"
f"{'' if stale_interim == 1 else 's'} need re-verifying "
f"(risk-scaled blocker cadence) — see `warden route gaps`"
)
return CatalogFreshness(
path=str(path),
source=source,
content_hash=digest,
mtime_iso=mtime_iso,
package_version=package_version,
entry_count=len(self.entries),
active_count=len(self.listed(include_draft=False)),
newest_reviewed=newest,
oldest_reviewed=oldest,
stale_entry_count=stale_count,
using_bundled=using_bundled,
warnings=warnings,
)
def _package_version() -> str:
try:
from importlib.metadata import version
return version("ops-warden")
except Exception: # noqa: BLE001
try:
from warden import __version__
return str(__version__)
except Exception: # noqa: BLE001
return "unknown"
def _classify_catalog_source(path: Path) -> str:
"""Classify catalog load path for freshness warnings."""
if os.environ.get("WARDEN_ROUTING_CATALOG"):
return "override"
resolved = str(path.resolve())
if "/_registry/" in resolved or resolved.endswith("/warden/_registry/routing/catalog.yaml"):
return "bundled"
# hatch force-include places registry at warden/_registry
parts = path.resolve().parts
if "_registry" in parts:
return "bundled"
return "repo"
def _assert_no_secret_material(
entry_id: str, field_name: str, value: str, *, prose: bool = False
) -> None:
"""Reject a handoff field that appears to embed a literal secret value.
The structured handoff fields are command/path *templates*: concrete values
@ -169,8 +380,15 @@ def _assert_no_secret_material(entry_id: str, field_name: str, value: str) -> No
catalog is git-tracked and agent-visible, so a leaked value here is the exact
custody failure WP-0014 forbids. We screen for known token prefixes and for a
long high-entropy run that is not a placeholder.
``prose=True`` (rotation guidance steps, WP-0026 T06) skips the *substring*
prefix screen short prefixes like ``s.`` or ``eyJ`` collide with ordinary
English ("exists.", "artifacts.") and relies on the high-entropy-run detector,
which catches an actually-pasted token (a real ``hvs.``/``ghp_``/``sk-`` value
carries a long high-entropy tail) while allowing plain sentences.
"""
lowered = value.lower()
if not prose:
for prefix in _SECRET_PREFIXES:
if prefix.lower() in lowered:
raise CatalogError(
@ -190,6 +408,193 @@ def _assert_no_secret_material(entry_id: str, field_name: str, value: str) -> No
)
def _parse_rotation(entry_id: str, raw: Optional[dict]) -> Optional[RotationGuide]:
"""Parse and validate an optional ``rotation:`` block (WP-0026 T06).
Advisory renewal guidance only screened for secret material like every other
catalog string. ``method`` must be rotate | re-establish; ``steps`` a non-empty
list; ``owner`` required.
"""
if raw is None:
return None
if not isinstance(raw, dict):
raise CatalogError(f"entry {entry_id!r} `rotation` must be a mapping")
method = str(raw.get("method", "")).strip()
if method not in _VALID_ROTATION_METHODS:
raise CatalogError(
f"entry {entry_id!r} rotation.method {method!r} invalid "
f"(expected one of {_VALID_ROTATION_METHODS})"
)
steps_raw = raw.get("steps")
if not isinstance(steps_raw, list) or not steps_raw:
raise CatalogError(
f"entry {entry_id!r} rotation.steps must be a non-empty list of steps"
)
steps = [str(s) for s in steps_raw]
owner = str(raw.get("owner", "")).strip()
if not owner:
raise CatalogError(f"entry {entry_id!r} rotation.owner is required")
# Screen advisory prose for accidental secret material (git-tracked, agent-visible).
for i, step in enumerate(steps):
_assert_no_secret_material(entry_id, f"rotation.steps[{i}]", step, prose=True)
_assert_no_secret_material(entry_id, "rotation.owner", owner, prose=True)
return RotationGuide(
method=method,
steps=steps,
owner=owner,
automatable=bool(raw.get("automatable", False)),
)
def _parse_delegation(entry_id: str, raw: Optional[dict]) -> Optional[Delegation]:
"""Parse an optional ``delegation:`` block (WARDEN-WP-0030).
Absence is allowed: the loader treats it as implicit interim with an
unknown owner. When the block *is* present, mode / owner / blocker rules
are enforced so a declared answer cannot be incomplete.
"""
if raw is None:
return None
if not isinstance(raw, dict):
raise CatalogError(f"entry {entry_id!r} `delegation` must be a mapping")
mode = str(raw.get("mode", "")).strip()
if mode not in VALID_DELEGATION_MODES:
raise CatalogError(
f"entry {entry_id!r} delegation.mode {mode!r} invalid "
f"(expected one of {VALID_DELEGATION_MODES})"
)
intended_owner = str(raw.get("intended_owner", "")).strip() or None
if mode != "permanent" and not intended_owner:
raise CatalogError(
f"entry {entry_id!r} delegation.intended_owner is required "
f"unless mode is permanent"
)
blocked_on = str(raw.get("blocked_on", "")).strip() or None
if mode == "interim" and not blocked_on:
raise CatalogError(
f"entry {entry_id!r} delegation.blocked_on is required when mode is interim"
)
reviewed = str(raw.get("reviewed", "")).strip() or None
if not reviewed:
raise CatalogError(f"entry {entry_id!r} delegation.reviewed is required")
try:
date.fromisoformat(reviewed)
except ValueError as e:
raise CatalogError(
f"entry {entry_id!r} delegation.reviewed {reviewed!r} is not YYYY-MM-DD"
) from e
if intended_owner:
_assert_no_secret_material(
entry_id, "delegation.intended_owner", intended_owner, prose=True
)
if blocked_on:
_assert_no_secret_material(
entry_id, "delegation.blocked_on", blocked_on, prose=True
)
verified = str(raw.get("verified", "")).strip() or None
if verified is not None and verified not in Delegation.VERIFICATION_METHODS:
raise CatalogError(
f"entry {entry_id!r} delegation.verified {verified!r} invalid "
f"(expected one of {Delegation.VERIFICATION_METHODS})"
)
return Delegation(
mode=mode,
intended_owner=intended_owner,
blocked_on=blocked_on,
reviewed=reviewed,
verified=verified,
implicit=False,
)
def _parse_workload_ref(entry_id: str, raw: object) -> WorkloadReference:
"""Parse an explicit workload join without attempting identity inference."""
if not isinstance(raw, dict):
raise CatalogError(
f"entry {entry_id!r} workload_ref must be a mapping; every lane must "
"declare applicable or not-applicable"
)
applicability = str(raw.get("applicability", "")).strip()
if applicability not in VALID_WORKLOAD_APPLICABILITY:
raise CatalogError(
f"entry {entry_id!r} workload_ref.applicability {applicability!r} invalid "
f"(expected one of {VALID_WORKLOAD_APPLICABILITY})"
)
def optional(name: str) -> Optional[str]:
value = raw.get(name)
return str(value).strip() if value is not None and str(value).strip() else None
ref = WorkloadReference(
applicability=applicability,
rapp_id=optional("rapp_id"),
name=optional("name"),
deployable=optional("deployable"),
declaration_ref=optional("declaration_ref"),
reason=optional("reason"),
unknown_reason=optional("unknown_reason"),
)
target_fields = (ref.rapp_id, ref.name, ref.deployable, ref.declaration_ref)
if applicability == "not-applicable":
if not ref.reason:
raise CatalogError(
f"entry {entry_id!r} workload_ref.reason is required for not-applicable"
)
if any(target_fields) or ref.unknown_reason:
raise CatalogError(
f"entry {entry_id!r} not-applicable workload_ref must not carry a "
"workload target or unknown_reason"
)
return ref
if ref.unknown_reason:
if any(target_fields) or ref.reason:
raise CatalogError(
f"entry {entry_id!r} unknown workload_ref must carry only "
"applicability and unknown_reason"
)
return ref
if not ref.name:
raise CatalogError(
f"entry {entry_id!r} applicable workload_ref requires name or "
"unknown_reason"
)
if ref.rapp_id:
if ref.declaration_ref:
raise CatalogError(
f"entry {entry_id!r} managed workload_ref must not also carry "
"declaration_ref"
)
elif not ref.declaration_ref:
raise CatalogError(
f"entry {entry_id!r} operational workload_ref requires declaration_ref"
)
if ref.deployable and not ref.rapp_id:
raise CatalogError(
f"entry {entry_id!r} workload_ref.deployable requires rapp_id"
)
if ref.reason:
raise CatalogError(
f"entry {entry_id!r} applicable workload_ref must not carry reason"
)
return ref
def _parse_entry(raw: dict, index: int) -> RouteEntry:
if not isinstance(raw, dict):
raise CatalogError(f"entry #{index} is not a mapping")
@ -250,6 +655,16 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry:
f"entry {entry_id!r} has invalid lane {lane!r} (expected one of {_VALID_LANES})"
)
risk_value = raw.get("risk")
risk = str(risk_value).strip() if risk_value is not None else "ungraded"
risk = risk or "ungraded"
if risk != "ungraded" and risk not in VALID_RISK:
raise CatalogError(
f"entry {entry_id!r} has invalid risk {risk!r} (expected one of {VALID_RISK})"
)
workload_ref = _parse_workload_ref(entry_id, raw.get("workload_ref"))
return RouteEntry(
id=entry_id,
title=str(raw["title"]),
@ -261,6 +676,7 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry:
canon_ref=str(raw["canon_ref"]),
reviewed=str(raw["reviewed"]),
status=status,
workload_ref=workload_ref,
steps=[str(s) for s in steps],
cert_command=str(cert_command) if cert_command else None,
auth_method=handoff["auth_method"],
@ -272,6 +688,9 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry:
exec_owner=str(raw["exec_owner"]) if raw.get("exec_owner") else None,
exec_command=handoff["exec_command"],
pointer_command=handoff["pointer_command"],
rotation=_parse_rotation(entry_id, raw.get("rotation")),
risk=risk,
delegation=_parse_delegation(entry_id, raw.get("delegation")),
)

View file

@ -11,6 +11,136 @@ from dataclasses import dataclass, field
from typing import List, Optional
# Risk grade vocabulary (ADR-0007). Grades outside LOW_RISK_GRADES — including
# the "ungraded" default and any value from a newer catalog — are treated as
# high by is_high_risk, so the read-boundary fails safe in both directions.
LOW_RISK_GRADES = frozenset({"standard", "low", "accepted"})
GRADED_RISK = frozenset({"standard", "low", "accepted", "high", "critical"})
@dataclass
class RotationGuide:
"""Structured-but-advisory renewal guidance for a lane (WARDEN-WP-0026 T06).
Held in the ops-warden registry, never in OpenBao. ``steps`` are authored
advisory prose (screened for secret material like every catalog string) they
tell an operator *how* to renew, they are not executed here. ``method`` is
``rotate`` (provider re-mints the same kind of credential) or ``re-establish``
(regenerate from source, e.g. a new age keypair + re-encrypt). ``automatable``
is a hint for a future Strand-B executable driver (WARDEN-WP-0027).
"""
method: str # "rotate" | "re-establish"
steps: List[str]
owner: str
automatable: bool = False
# Risk classes for agent read-boundary (WARDEN-WP-0026 T04).
# high — recovery escrow, upload tokens, admin PATs, high-spend provider keys.
# Agent identities must not hold raw data-read (metadata/capabilities only).
# standard — ordinary workload secrets (ESO-fed, non-escrow); normal least-privilege.
VALID_RISK = ("standard", "high")
# Delegation modes (WARDEN-WP-0030). Absence of a block is implicit interim.
# native — intended owner already fronts the lane (route-primary / pointer)
# interim — ops-warden covers a gap; intended_owner + blocked_on required
# permanent — ops-warden is the designed owner of this front door (SSH only today)
VALID_DELEGATION_MODES = ("native", "interim", "permanent")
VALID_WORKLOAD_APPLICABILITY = ("applicable", "not-applicable")
IMPLICIT_DELEGATION_BLOCKED_ON = (
"unclassified — no delegation block; treat as a question, not a settlement"
)
@dataclass
class Delegation:
"""Who should own this front door, and what is missing (WARDEN-WP-0030).
Pointer-layer only: names the intended owner and the blocker. Does not
restate how that owner will implement their front door.
"""
mode: str # native | interim | permanent
intended_owner: Optional[str] = None
blocked_on: Optional[str] = None
reviewed: Optional[str] = None
verified: Optional[str] = None
implicit: bool = False
#: How `reviewed` was established. The distinction exists because a date
#: bumped by editing the entry looks identical to one bumped by re-checking
#: the blocker, and on 2026-08-21 six lanes read as freshly reviewed when
#: only some had actually been re-verified (WARDEN-WP-0033-T05).
#:
#: `asked-and-waiting` deliberately does NOT count as verification: it is the
#: state the secrets-engine lanes sat in for ten days while looking fresh.
VERIFICATION_METHODS = (
"owner-confirmed", # the intended owner stated the blocker's status
"source-read", # re-derived from the owner's code, canon, or CCR
"asked-and-waiting", # a question is outstanding — NOT verification
"unverified", # carried forward without a check
)
#: Methods that mean the claim was actually re-established.
VERIFYING_METHODS = ("owner-confirmed", "source-read")
@property
def is_verified(self) -> bool:
"""True only when the blocker was re-established, not merely re-edited."""
return self.verified in self.VERIFYING_METHODS
def to_dict(self) -> dict:
return {
"mode": self.mode,
"intended_owner": self.intended_owner,
"blocked_on": self.blocked_on,
"reviewed": self.reviewed,
"verified": self.verified,
"is_verified": self.is_verified,
"implicit": self.implicit,
}
@dataclass(frozen=True)
class WorkloadReference:
"""Authoritative workload join for a catalog lane (WARDEN-WP-0032).
Managed deployables use the Repo Manager v1 ``rapp_id``/``name`` tuple.
Independently governed operational workloads use ``name`` plus an owner
declaration reference. An applicable lane whose owner has not published an
identity remains explicitly ``unknown``; it is never inferred from the
credential path or repository name.
"""
applicability: str # applicable | not-applicable
rapp_id: Optional[str] = None
name: Optional[str] = None
deployable: Optional[str] = None
declaration_ref: Optional[str] = None
reason: Optional[str] = None
unknown_reason: Optional[str] = None
@property
def resolution(self) -> str:
if self.applicability == "not-applicable":
return "not-applicable"
if self.unknown_reason:
return "unknown"
return "resolved"
def to_dict(self) -> dict:
return {
"applicability": self.applicability,
"rapp_id": self.rapp_id,
"name": self.name,
"deployable": self.deployable,
"declaration_ref": self.declaration_ref,
"reason": self.reason,
"unknown_reason": self.unknown_reason,
"resolution": self.resolution,
}
@dataclass
class RouteEntry:
id: str
@ -23,6 +153,8 @@ class RouteEntry:
canon_ref: str
reviewed: str
status: str # "active" | "draft"
# Explicit workload applicability and authoritative join. Never inferred.
workload_ref: Optional[WorkloadReference] = None
# SSH lane only — None/empty for routed (non-executed) needs.
steps: List[str] = field(default_factory=list)
cert_command: Optional[str] = None
@ -42,6 +174,8 @@ class RouteEntry:
# "login" — interactive auth bootstrap (OIDC/MFA). No secret-read gate (you have
# no identity yet), no caller-auth precheck (the point is to get one),
# run interactively as the caller; warden never captures the token.
# "ceremony" — attended owner operation such as Shamir seal/unseal. It is a
# pointer plus approval boundary, never an executable access lane.
lane: str = "secret"
# Owner-native exec front door (WP-0019). When `exec_owner` is set, that subsystem
# (e.g. secrets-engine) provides the PRIMARY way to run a secret-backed command; the
@ -50,16 +184,109 @@ class RouteEntry:
exec_owner: Optional[str] = None # subsystem owning the native exec (e.g. secrets-engine)
exec_command: Optional[str] = None # e.g. "secrets-engine exec --catalog <id> -- <cmd>"
pointer_command: Optional[str] = None # e.g. "secrets-engine route <id> --json"
# Rotation / re-establishment guidance (WP-0026 T06) — advisory, no secret values.
rotation: Optional[RotationGuide] = None
# Agent read-boundary risk class (WP-0026 T04). high → agents use wrap/out/exec only.
# Default is "ungraded", which FAILS SAFE: it is treated as high. Before
# ADR-0007 this defaulted to "standard", so a lane that simply omitted the
# field was silently placed outside the read-boundary (RISK-F-0003) — the
# control was never relaxed by decision, it was never reached.
risk: str = "ungraded" # "standard" | "high" | "ungraded"
# Delegation register (WP-0030). None → implicit interim with unknown owner.
delegation: Optional[Delegation] = None
@property
def is_active(self) -> bool:
return self.status == "active"
@property
def is_graded(self) -> bool:
"""False when this lane carries no explicit risk grade (ADR-0007)."""
return self.risk in GRADED_RISK
@property
def is_high_risk(self) -> bool:
"""True when this lane is on the agent raw-read deny list (WP-0026 T04).
Anything not explicitly graded low is high. An ungraded lane, or one
carrying a grade this version does not recognise, is treated as high
rather than waved through ADR-0007: absence is not a grade.
"""
return self.risk not in LOW_RISK_GRADES
def risk_for_zone(
self,
*,
effective_zone: str = "unknown",
admission: str = "unknown",
synthetic_only: bool = False,
) -> str:
"""Resolve an absent grade using security-zones_v0.1 section 5.1.
Explicit grades always win. The sole lower default is a satisfied
``z0-experimental`` workload proven synthetic-only. Every other zone,
failed/unknown admission, and missing context fails safe to at least
``high``; z3 reports ``critical`` (which the read boundary treats as
high). Catalog CI still requires explicit grades, so this is the safe
runtime behavior for malformed or newer external catalogs.
"""
if self.is_graded:
return self.risk
if (
effective_zone == "z0-experimental"
and admission == "satisfied"
and synthetic_only
):
return "standard"
if effective_zone == "z3-critical" and admission == "satisfied":
return "critical"
return "high"
@property
def has_rotation(self) -> bool:
"""True when this lane carries renewal guidance (WP-0026 T06)."""
return self.rotation is not None
@property
def vends_secret(self) -> bool:
"""True when this lane hands back a rotatable static secret value.
Rotation guidance (WP-0026 T06) applies to these. It excludes the SSH lane
(short-lived certs renewal is re-issuance), ``login`` lanes (re-auth, no
stored value), and pure routing pointers with no secret path (tunnel,
principals, emission sinks, policy checks).
"""
if self.warden_executes or self.lane != "secret":
return False
return bool(self.path_template or self.fetch_command or self.exec_owner)
@property
def has_native_exec(self) -> bool:
"""True when an owner-native exec front door is the primary path for this lane."""
return bool(self.exec_owner and self.exec_command)
@property
def effective_delegation(self) -> Delegation:
"""Declared delegation, or implicit interim with an unknown owner.
Absence of a ``delegation:`` block is a question (WP-0030), not a
settlement that ops-warden owns the front door.
"""
if self.delegation is not None:
return self.delegation
return Delegation(
mode="interim",
intended_owner=None,
blocked_on=IMPLICIT_DELEGATION_BLOCKED_ON,
reviewed=None,
implicit=True,
)
@property
def is_interim(self) -> bool:
"""True when this lane is a tracked gap (explicit or implicit)."""
return self.effective_delegation.mode == "interim"
@property
def has_handoff(self) -> bool:
"""True when structured assist fields are present (advisory richness)."""

View file

@ -152,6 +152,107 @@ def check_file_permissions(state_dir: Path) -> CheckResult:
)
def check_catalog_rotation_coverage() -> CheckResult:
"""Every active secret-vending catalog lane must carry rotation guidance (T06).
A lane an operator can obtain a *secret value* through must also tell them how
to renew or re-establish it. Scoped to ``vends_secret`` lanes: this exempts the
SSH lane (short-lived certs renewal is re-issuance), ``login`` lanes (re-auth,
no stored value), and pure routing pointers (tunnel, principals, emission
sinks, policy checks) with no secret to rotate. Draft lanes are exempt until
promoted.
"""
try:
from warden.routing import load_catalog
catalog = load_catalog()
except Exception as e: # noqa: BLE001 — catalog missing/invalid is its own signal
return CheckResult(
name="catalog_rotation_coverage",
passed=False,
detail=f"could not load routing catalog: {e}",
)
missing = [
e.id
for e in catalog.entries
if e.is_active and e.vends_secret and not e.has_rotation
]
return CheckResult(
name="catalog_rotation_coverage",
passed=len(missing) == 0,
detail=(
f"active lanes lacking rotation guidance: {missing} — add a `rotation:` "
"block (see WARDEN-WP-0026 T06)"
if missing
else "all active lanes carry rotation guidance"
),
)
def check_organization_posture() -> CheckResult:
"""Surface declared organization lifecycle posture (WARDEN-WP-0029 T02).
Always informational PASS when descriptors load -- the check exists so operators
and agents see the posture in scorecard output without hunting config files.
"""
try:
from warden.posture import load_posture
cat = load_posture()
org = cat.organization_posture
except Exception as e: # noqa: BLE001
return CheckResult(
name="organization_posture",
passed=False,
detail=f"could not load organization posture: {e}",
)
relax = ", ".join(org.relaxations[:3])
if len(org.relaxations) > 3:
relax += ", ..."
summary = org.summary[:120]
if len(org.summary) > 120:
summary += "..."
relax_part = relax or "no relaxations listed"
return CheckResult(
name="organization_posture",
passed=True,
detail=f"{org.id} -- {summary} [{relax_part}]",
)
def check_catalog_freshness() -> CheckResult:
"""Warn when the CLI is using a bundled (stale-risk) catalog (WP-0029 T05)."""
try:
from warden.routing import load_catalog
fresh = load_catalog().freshness()
except Exception as e: # noqa: BLE001
return CheckResult(
name="catalog_freshness",
passed=False,
detail=f"could not load routing catalog: {e}",
)
if fresh.using_bundled:
nwarn = len(fresh.warnings)
return CheckResult(
name="catalog_freshness",
passed=False,
detail=(
f"bundled catalog hash={fresh.content_hash} - reinstall from checkout "
f"if lanes look missing ({nwarn} warnings)"
),
)
return CheckResult(
name="catalog_freshness",
passed=True,
detail=(
f"source={fresh.source} hash={fresh.content_hash} "
f"entries={fresh.active_count}/{fresh.entry_count} active "
f"newest_reviewed={fresh.newest_reviewed}"
),
)
def run_scorecard(state_dir: Path, inventory: PrincipalsInventory) -> List[CheckResult]:
"""Run all cert-side scorecard checks. Returns list of CheckResult."""
return [
@ -161,4 +262,7 @@ def run_scorecard(state_dir: Path, inventory: PrincipalsInventory) -> List[Check
check_no_stale_certs(state_dir),
check_ttl_policy(state_dir, inventory),
check_file_permissions(state_dir),
check_catalog_rotation_coverage(),
check_organization_posture(),
check_catalog_freshness(),
]

149
src/warden/taint.py Normal file
View file

@ -0,0 +1,149 @@
"""EXPOSED taint convention for OpenBao KV secrets (WARDEN-WP-0026 T05).
Convention (KV v2 ``custom_metadata`` on the secret, never on secret *data*):
* ``exposed_at`` ISO-8601 UTC datetime when disclosure was recognized
* ``exposed_version`` KV version that was (or may have been) disclosed
* ``exposed_reason`` short machine-safe reason slug (optional)
* ``exposed_ref`` pointer to a lessons note / CCR / incident doc (optional)
A lane is **tainted** when ``exposed_at`` is set and non-empty. Clearing taint
(after rotation) is an operator action: remove those keys from custom_metadata.
ops-warden only *reports* taint it never auto-rotates (Strand B / WP-0027).
This module only shells out to ``bao kv metadata get`` (or equivalent). It never
reads secret data values.
"""
from __future__ import annotations
import json
import os
import subprocess
from dataclasses import dataclass
from typing import Any, Optional
from warden.routing.models import RouteEntry
# Canonical custom_metadata keys (WP-0026 T05).
EXPOSED_AT = "exposed_at"
EXPOSED_VERSION = "exposed_version"
EXPOSED_REASON = "exposed_reason"
EXPOSED_REF = "exposed_ref"
_TAINT_KEYS = (EXPOSED_AT, EXPOSED_VERSION, EXPOSED_REASON, EXPOSED_REF)
@dataclass(frozen=True)
class TaintStatus:
"""Advisory taint view for a lane — no secret values."""
lane_id: str
path: str
tainted: bool
exposed_at: Optional[str] = None
exposed_version: Optional[str] = None
exposed_reason: Optional[str] = None
exposed_ref: Optional[str] = None
current_version: Optional[int] = None
error: Optional[str] = None
def to_dict(self) -> dict[str, Any]:
return {
"id": self.lane_id,
"path": self.path,
"tainted": self.tainted,
"exposed_at": self.exposed_at,
"exposed_version": self.exposed_version,
"exposed_reason": self.exposed_reason,
"exposed_ref": self.exposed_ref,
"current_version": self.current_version,
**({"error": self.error} if self.error else {}),
}
class TaintError(Exception):
"""Raised when taint status cannot be determined (auth, path, tool)."""
def kv_metadata_path(path_template: str) -> str:
"""Return the logical KV path suitable for ``bao kv metadata get``.
Catalog paths are logical (``platform/workloads/...``), not API data paths.
"""
return path_template.strip().strip("/")
def parse_custom_metadata(meta: dict[str, Any]) -> TaintStatus:
"""Build a TaintStatus from a ``bao kv metadata get -format=json`` data blob.
``meta`` is the ``data`` object (with ``custom_metadata``, ``current_version``).
"""
custom = meta.get("custom_metadata") or {}
if not isinstance(custom, dict):
custom = {}
exposed_at = (custom.get(EXPOSED_AT) or "").strip() or None
return TaintStatus(
lane_id="",
path="",
tainted=bool(exposed_at),
exposed_at=exposed_at,
exposed_version=(custom.get(EXPOSED_VERSION) or "").strip() or None,
exposed_reason=(custom.get(EXPOSED_REASON) or "").strip() or None,
exposed_ref=(custom.get(EXPOSED_REF) or "").strip() or None,
current_version=meta.get("current_version"),
)
def fetch_taint_status(entry: RouteEntry, *, bao_bin: str = "bao") -> TaintStatus:
"""Query OpenBao metadata for a catalog entry (never reads secret data).
Uses the caller's ``BAO_TOKEN`` / ``VAULT_TOKEN`` / ``~/.vault-token`` — same
G1 rule as the access proxy. Requires ``path_template`` on the entry.
"""
if not entry.path_template or "<" in entry.path_template:
raise TaintError(
f"{entry.id!r} has no concrete path_template — cannot query taint metadata."
)
path = kv_metadata_path(entry.path_template)
try:
proc = subprocess.run(
[bao_bin, "kv", "metadata", "get", "-format=json", path],
capture_output=True,
text=True,
env=os.environ.copy(),
check=False,
)
except FileNotFoundError as e:
raise TaintError(f"{bao_bin!r} not found on PATH") from e
if proc.returncode != 0:
err = (proc.stderr or proc.stdout or "metadata get failed").strip().splitlines()
# Never echo tokens if somehow present.
safe = " ".join(err[:3])[:300]
return TaintStatus(
lane_id=entry.id,
path=path,
tainted=False,
error=safe or f"bao exit {proc.returncode}",
)
try:
payload = json.loads(proc.stdout)
except json.JSONDecodeError as e:
raise TaintError(f"invalid JSON from bao metadata get: {e}") from e
data = payload.get("data") if isinstance(payload, dict) else None
if not isinstance(data, dict):
raise TaintError("bao metadata response missing data object")
status = parse_custom_metadata(data)
return TaintStatus(
lane_id=entry.id,
path=path,
tainted=status.tainted,
exposed_at=status.exposed_at,
exposed_version=status.exposed_version,
exposed_reason=status.exposed_reason,
exposed_ref=status.exposed_ref,
current_version=status.current_version,
)

View file

@ -588,6 +588,12 @@ def draft_route_answer(query: str) -> str:
elif e.has_native_exec:
parts.append(f"Primary: {e.exec_command}.")
elif e.exec_capable:
if e.lane == "login":
parts.append(
f"Contained login: warden access {e.id} --exec -- "
"<reviewed-command>."
)
else:
parts.append(f"Proxy: warden access {e.id} --fetch (as the caller).")
parts.append(f"See {e.wiki_ref}.")
return " ".join(parts)

172
tenancy.yaml Normal file
View file

@ -0,0 +1,172 @@
# ops-warden tenancy posture declaration
# Framework: net-kingdom/canon/standards/tenancy-posture_v0.1.md draft-9
# Conformance rule (§6): accuracy, not altitude. Nothing here is aspirational.
# Validate: python3 ~/net-kingdom/tools/tenancy-posture/validate.py tenancy.yaml
schema_version: "0.1"
framework: netkingdom-tenancy-posture
service: ops-warden
role: ssh-certificate-authority
workload_identity:
name: ops-warden
kind: operational-control-plane
responsible_repo: ops-warden
identity_bindings:
- scheme: kubernetes-service-account
authority: railiance01
subject: system:serviceaccount:ops-warden:ops-warden
principal_type: service
environment: prod
tenancy:
current:
I: 1
A: 1
E: 0
P: "n/a"
R: "n/a"
V: 0
implemented:
A: 3
target:
I: 1
A: 3
E: 0
P: "n/a"
R: "n/a"
V: 1
reviewed: "2026-08-19"
review_due: "2027-02-18"
service_class: interactive
permanent: [P, R]
gap:
I: >-
ops-warden has a tenant notion — `policy.tenant` in warden.yaml, and the
tenant/platform path split introduced by WP-0028 — but it is a static
configuration constant, not a claim verified on an inbound call. `warden`
is a CLI invoked by an operator or agent; its caller is an OS user and no
token is presented to it. Subject id comes from `WARDEN_POLICY_SUBJECT`
or falls back to the actor name, which is §4.1's "taken from the request"
case exactly. I1.
Not declared permanent. `warden desk` (WP-0029) already performs an OIDC
login against key-cape, so a verified inbound identity is reachable
rather than structurally excluded. It is simply not built, and I2 would
require it on the signing path, not just the desk.
A: >-
There is a single choke point on the signing path — inventory membership,
actor type, and the TTL ceiling — but it binds an *actor* to principals,
not a request to the tenants it may act for. Tenant context is a
constant, so "bound once, centrally" (§4.2 A2) would be true only in a
trivial sense that overclaims. A1.
`implemented: A3` is the honest separate fact: delegation to flex-auth as
PDP is built (`src/warden/policy.py`, `check_sign_policy`) and was
verified live on 2026-08-19 against the enforcing `flex-auth-ops-warden`
pin — `decision:f3f7c88f9585582a`, with an anonymous `/v1/check`
returning 401. It is not `current` because `policy.enabled` is false, and
it is false by decision rather than by blocker: `ADR-0006` scopes
enforcement to security zones, which `zone-engine` is defining
(`ZONE-WP-0001`). Evidence:
`history/2026-08-19-flex-auth-caller-identity-evidence.md`.
A4 is not a target here. The AuthZEN interface question belongs to
flex-auth as the decision point; ops-warden would follow it, not lead it.
E: >-
E0 is accurate and is not a defect to remediate. ops-warden holds no
tenant-partitioned data: `registry/routing/catalog.yaml` is a pointer
layer carrying no secret values (`ADR-0001`, CI-enforced) and no `tenant`
field on any entry; local state (`signatures.log`, `audit.jsonl`,
`access-audit.log`) is operator-scoped and keyed by actor, not tenant.
The tenant boundaries ops-warden *routes* to — `tenants/binky/...` versus
`platform/workloads/...` — are enforced by OpenBao policy, which is the
owner's control, not ops-warden's. Claiming E1 on the strength of someone
else's enforcement is the overclaim §6 prohibits. Under `ADR-0002`
ops-warden is a transparent conduit and takes no custody, so it has no
tenant data to key. Raising E would mean acquiring data it is out of
scope to hold.
P: >-
n/a and permanent. No primary datastore: state is files under
`~/.local/state/warden` on the invoking operator's machine, and
§3.3 scopes P to the primary datastore. Custody of secrets is explicitly
out of scope (`SCOPE.md`, `ADR-0002`), so ops-warden will not acquire a
tenant-bearing substrate.
R: >-
n/a and permanent, on the same ground as P: no tenant data at rest.
Named honestly rather than hidden behind the n/a: ops-warden *does* keep
local operator records indefinitely — `audit.jsonl` is append-only by
design (WP-0022) and `signatures.log` has no retention position. They are
metadata-only and guarded against secret material, and they are not
tenant data, so they do not move this axis. But "no declared retention
for local audit" is a real gap on a different axis than this file grades,
and is recorded here so it is not lost.
V: >-
V0. `warden sign` depends synchronously on OpenBao at
`https://bao.coulomb.social` (railiance01) and, once enabled, on the
flex-auth pin reached through the ops-bridge tunnel
`flex-auth-ops-warden-railiance01`. §4.6.1 takes the minimum across that
path, and none of it has an exercised recovery objective.
A `backend: local` CA exists in the code and is covered by tests, but it
has never been exercised as a production degraded mode, and §13 does not
accept "the code path exists" as V evidence. Target V1 means documenting
and actually rehearsing recovery of the signing path — not adding
redundancy, which the substrate cannot currently support: `reef-railiance`
is single-node with a shared control plane, and under Decision 4.6.1 that
caps V for everything bound to it (see NK-WP-0027).
zones:
standard: security-zones_v0.1
membership: z1-operational
responsible_party: team:platform-security
justification: >-
The attended platform signing service has a bounded operational scope and
internal metadata exposure. It has M1 evidence, but no SLO history, on-call
rotation, or incident exercise that would support z2-protected.
context:
maturity: M1
criticality: medium
data_classification: internal
evidence:
- ref: docs/evidence/security-zone-admission-2026-08-22.md
supports: [M1, platform-only-scope, basic-slo, data-handling-note]
- ref: history/2026-08-19-flex-auth-caller-identity-evidence.md
supports: [production-policy-path, authenticated-caller]
reviewed: "2026-08-22"
review_due: "2026-11-22"
evidence:
A: >-
Built and verified live 2026-08-19 against the enforcing
`flex-auth-ops-warden` pin: `decision:f3f7c88f9585582a`, anonymous
`/v1/check` 401. Record:
`history/2026-08-19-flex-auth-caller-identity-evidence.md`. Re-establish on
demand with `scripts/check_policy_caller_identity.py`. Why it is implemented
and not current: `docs/adr/ADR-0006-enforcement-is-zone-scoped.md`.
E: >-
`docs/adr/ADR-0002-conduit-not-broker.md` and `ADR-0001` (catalog is a
pointer layer, CI-enforced) — why no tenant-keyed data exists to enforce on.
P: >-
No datastore. State is operator-local files under `~/.local/state/warden`.
V: >-
Production signing path verified 2026-06-17
(`history/2026-06-17-openbao-production-verify.md`). No recovery exercise
exists for that path, which is why this is V0 and not V1.
notes:
- >-
ops-warden issues short-lived SSH certificates and routes every other
credential need to its owner.
- >-
Five of six axes are low because it deliberately holds nothing. The low
grades on E, P and R are the intended consequence of ADR-0002 and ADR-0005,
not deferred work, and raising them would mean acquiring data ops-warden is
out of scope to hold.
- >-
The two axes with real movement are A — built, deferred by ADR-0006 pending
the zone model — and V, which needs a rehearsed recovery and is capped by
the substrate until NK-WP-0027 lands.

View file

@ -0,0 +1,131 @@
"""Tests for scripts/check_agent_read_boundary.py (WARDEN-WP-0032-T06).
This script is a control, not a report: it is the invariant RISK-F-0009 asked for
("a check that fails when a high-risk lane has no corresponding deny"). So the
parsing has to be right about the two things that would make it lie -- treating a
non-deny grant as a deny, and treating a path pattern as a concrete address.
"""
import importlib.util
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
spec = importlib.util.spec_from_file_location(
"check_agent_read_boundary", REPO / "scripts" / "check_agent_read_boundary.py"
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
class TestDeniedDataPaths:
def test_extracts_denied_paths(self):
policy = """
path "platform/data/workloads/forgejo/forgejo-admin" {
capabilities = ["deny"]
}
"""
assert mod.denied_data_paths(policy) == {"platform/data/workloads/forgejo/forgejo-admin"}
def test_metadata_read_is_not_a_deny(self):
"""The policy permits metadata read alongside every data deny.
Counting those as denies would double the apparent coverage.
"""
policy = """
path "platform/metadata/workloads/forgejo/forgejo-admin" {
capabilities = ["read"]
}
"""
assert mod.denied_data_paths(policy) == set()
def test_deny_is_matched_exactly_not_by_substring(self):
"""A capability merely containing 'deny' must not register as a deny."""
policy = """
path "platform/data/workloads/x/y" {
capabilities = ["denylist-read"]
}
"""
assert mod.denied_data_paths(policy) == set()
def test_multiple_blocks(self):
policy = """
path "a/data/one" { capabilities = ["deny"] }
path "a/metadata/one" { capabilities = ["read"] }
path "b/data/two" { capabilities = ["deny"] }
"""
assert mod.denied_data_paths(policy) == {"a/data/one", "b/data/two"}
class TestToDataPath:
def test_inserts_kv_v2_data_segment(self):
assert (
mod.to_data_path("platform/workloads/forgejo/forgejo-admin")
== "platform/data/workloads/forgejo/forgejo-admin"
)
def test_tenant_mount(self):
assert (
mod.to_data_path("tenants/binky/company-email/imap")
== "tenants/data/binky/company-email/imap"
)
def test_placeholder_pattern_has_no_address(self):
"""`openbao-api-key` is a routing pattern, not one secret.
RISK-F-0009 counted it among the uncovered lanes; there is nothing for a
policy to deny, and reporting it as a gap overstates the exposure.
"""
assert mod.to_data_path("platform/workloads/<domain>/<workload>/<bundle>") is None
def test_non_kv_lane_has_no_address(self):
"""`ops-warden-warden-sign-token` is a broker grant, not a KV path."""
assert mod.to_data_path("credential-grants/catalog.yaml grant ops-warden/warden-sign") is None
class TestAgainstTheRealCatalog:
def test_every_high_risk_lane_resolves_or_is_explicitly_pattern(self):
"""No high-risk lane may fall through the classifier silently.
Each is either a concrete data path the policy can deny, or a pattern --
never an unhandled third case, which is the ADR-0007 failure mode.
"""
import yaml
entries = yaml.safe_load((REPO / "registry" / "routing" / "catalog.yaml").read_text())["entries"]
for entry in (e for e in entries if e.get("risk") == "high"):
template = entry.get("path_template")
if not template:
continue
resolved = mod.to_data_path(template)
assert resolved is None or resolved.count("/data/") == 1, entry["id"]
class TestGeneratedArtifact:
"""The artifact railiance-platform consumes (WARDEN-WP-0033-T03).
A consumer applies this to a live deny set, so staleness is the failure that
matters -- a path graded high after the last emit would silently not reach them.
"""
def test_artifact_is_current(self):
import subprocess
result = subprocess.run(
["python3", str(REPO / "scripts" / "emit_high_risk_paths.py"), "--check"],
capture_output=True, text=True, timeout=60,
)
assert result.returncode == 0, (
f"{result.stdout}{result.stderr}\n"
"Re-run scripts/emit_high_risk_paths.py and commit the result."
)
def test_every_concrete_high_risk_lane_is_in_the_artifact(self):
import yaml
catalog = yaml.safe_load((REPO / "registry" / "routing" / "catalog.yaml").read_text())
artifact = yaml.safe_load(
(REPO / "registry" / "generated" / "high-risk-data-paths.yaml").read_text()
)
emitted = {row["id"] for row in artifact["paths"]} | set(artifact["no_concrete_path"] or [])
graded_high = {e["id"] for e in catalog["entries"] if e.get("risk") == "high"}
assert graded_high == emitted, "a high-risk lane is missing from the generated artifact"

View file

@ -84,13 +84,13 @@ def test_default_vault_token_env(tmp_path):
assert cfg.vault.token_env == "VAULT_TOKEN"
def test_policy_defaults_disabled(tmp_path):
def test_policy_defaults_to_unknown_zone_profile(tmp_path):
cfg_path = tmp_path / "warden.yaml"
write_yaml(cfg_path, {"backend": "local", "ca_key": str(tmp_path / "ca")})
cfg = load_config(cfg_path)
assert cfg.policy.enabled is False
assert cfg.policy.flex_auth_url == "http://127.0.0.1:8080"
assert cfg.policy.fail_closed is True
assert cfg.policy.flex_auth_url is None
assert cfg.policy.failure_modes["unknown"] == "fail_open"
assert cfg.policy.failure_modes["z3-critical"] == "fail_closed"
def test_policy_block_parsed(tmp_path):
@ -99,18 +99,30 @@ def test_policy_block_parsed(tmp_path):
"backend": "local",
"ca_key": str(tmp_path / "ca"),
"policy": {
"enabled": True,
"flex_auth_url": "http://flex-auth:8080",
"fail_closed": False,
"zone_registry_path": str(tmp_path / "zones.json"),
"failure_modes": {"z2-protected": "fail_closed"},
"tenant": "tenant:coulomb",
"subject_env": "MY_SUBJECT",
"system": "warden-test",
},
})
cfg = load_config(cfg_path)
assert cfg.policy.enabled is True
assert cfg.policy.flex_auth_url == "http://flex-auth:8080"
assert cfg.policy.fail_closed is False
assert cfg.policy.zone_registry_path == tmp_path / "zones.json"
assert cfg.policy.failure_modes["z2-protected"] == "fail_closed"
assert cfg.policy.tenant == "tenant:coulomb"
assert cfg.policy.subject_env == "MY_SUBJECT"
assert cfg.policy.system == "warden-test"
@pytest.mark.parametrize("retired", ["enabled", "fail_closed"])
def test_retired_global_policy_switches_are_rejected(tmp_path, retired):
cfg_path = tmp_path / "warden.yaml"
write_yaml(cfg_path, {
"backend": "local",
"ca_key": str(tmp_path / "ca"),
"policy": {retired: True},
})
with pytest.raises(ConfigError, match=f"policy.{retired}"):
load_config(cfg_path)

96
tests/test_desk.py Normal file
View file

@ -0,0 +1,96 @@
"""Tests for warden desk (WARDEN-WP-0029 T03)."""
from __future__ import annotations
import threading
import urllib.error
import urllib.parse
import urllib.request
from http.server import ThreadingHTTPServer
import pytest
from typer.testing import CliRunner
from warden.cli import app
from warden.desk import (
DeskError,
make_handler,
new_session,
session_from_plan_dict,
)
runner = CliRunner()
def test_new_session_rejects_unknown_act():
with pytest.raises(DeskError, match="unknown desk act"):
new_session(act="teleport", summary="nope")
def test_paste_once_requires_path():
with pytest.raises(DeskError, match="requires --path"):
new_session(act="paste_once_provision", summary="mint")
def test_session_from_plan_dict():
plan = {
"verdict": "founder_required",
"need": "provision token",
"lane_id": "openbao-api-key",
"organization_posture": "build",
"founder_act": {
"kind": "approve",
"summary": "Approve red-lane change",
"details": {"lane_id": "openbao-api-key"},
},
}
s = session_from_plan_dict(plan)
assert s.act == "approve"
assert s.lane_id == "openbao-api-key"
def test_session_from_plan_rejects_autonomous():
with pytest.raises(DeskError, match="founder_required"):
session_from_plan_dict({"verdict": "autonomous", "founder_act": None})
def test_approve_flow_http_dry():
session = new_session(act="approve", summary="Enable something", lane_id="demo")
done = threading.Event()
def on_done(s):
done.set()
handler = make_handler(session, on_done=on_done, dry_run=True)
server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
port = server.server_address[1]
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
url = f"http://127.0.0.1:{port}/?t={session.token}"
with urllib.request.urlopen(url, timeout=5) as resp:
body = resp.read().decode()
assert "Founder approval" in body
assert session.token not in body or True # token is in form; ok
data = urllib.parse.urlencode(
{"token": session.token, "decision": "approve"}
).encode()
req = urllib.request.Request(
f"http://127.0.0.1:{port}/act", data=data, method="POST"
)
with urllib.request.urlopen(req, timeout=5) as resp:
result_body = resp.read().decode()
assert "approved" in result_body.lower() or session.result == "approved"
assert session.result == "approved"
assert done.wait(timeout=2)
finally:
server.shutdown()
thread.join(timeout=2)
def test_cli_desk_approve_dry_run():
# Exercise CLI wiring without waiting forever: dry-run still serves until act.
# Use a short-circuit by importing run path via invoke would hang — skip full CLI
# server test; unit coverage above is enough. Smoke that --help works.
r = runner.invoke(app, ["desk", "--help"])
assert r.exit_code == 0
assert "paste_once" in r.stdout or "founder" in r.stdout.lower() or "--act" in r.stdout

View file

@ -32,3 +32,52 @@ def test_build_registry_from_inventory_seed(tmp_path):
assert bridge["attributes"]["actor_type"] == "agt"
assert bridge["attributes"]["max_ttl_hours"] == 24
assert "agt-task-bridge" in bridge["attributes"]["allowed_principals"]
assert "trust_zone" not in bridge
assert bridge["attributes"]["security_zone"] == "unknown"
assert bridge["attributes"]["security_zone_admission"] == "unknown"
assert bridge["attributes"]["workload_id"] == "ops-bridge-tunnel"
human = next(
r
for r in registry["resource_manifests"][0]["resources"]
if r["id"] == "ssh-cert:actor/adm-example"
)
assert human["attributes"]["security_zone_admission"] == "not-applicable"
def test_compiler_joins_explicit_workload_reference_to_resolved_zone(tmp_path):
zones = tmp_path / "zones.json"
zones.write_text(json.dumps({
"records": [{
"workload_id": "ops-bridge-tunnel",
"declared_zone": "z2-continuity",
"admission": "satisfied",
"admission_reason": "admission_floor_met",
"effective_zone": "z2-continuity",
"membership_revision": "sha256:zone-revision",
}]
}))
out = tmp_path / "registry.json"
subprocess.run(
[
sys.executable,
str(SCRIPT),
str(INVENTORY),
"--zone-resolutions",
str(zones),
"-o",
str(out),
],
check=True,
cwd=ROOT,
)
registry = json.loads(out.read_text())
bridge = next(
r
for r in registry["resource_manifests"][0]["resources"]
if r["id"] == "ssh-cert:actor/agt-state-hub-bridge"
)
attrs = bridge["attributes"]
assert attrs["security_zone"] == "z2-continuity"
assert attrs["security_zone_admission"] == "satisfied"
assert attrs["security_zone_revision"] == "sha256:zone-revision"

View file

@ -0,0 +1,133 @@
"""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"
)
class TestPepStanceMap:
"""§6.4: every PEP-shaped consumer MUST publish its unreachable-engine
stance map, total and per zone, 'published rather than held in code'.
ADR-0009 is named as the reference shape, so it should actually hold."""
def _stance(self) -> dict:
return yaml.safe_load((ROOT / "pep-stance.yaml").read_text())
def test_published_map_equals_shipped_behaviour(self):
"""The whole point. A published map that may drift from the code is
worse than none, because it invites reliance it cannot support."""
from warden.config import PolicyConfig
assert self._stance()["stance"] == PolicyConfig().failure_modes
def test_stance_is_total_over_the_zone_model(self):
"""§6.4 obligation 3: total, no implicit default."""
stance = self._stance()["stance"]
required = {
"z0-experimental", "z1-operational", "z2-protected",
"z2-continuity", "z3-critical", "unknown", "not-applicable",
}
assert required <= set(stance), f"stance not total; missing {required - set(stance)}"
assert set(stance.values()) <= {"fail_open", "fail_closed"}
def test_critical_zone_fails_closed(self):
"""ADR-0009's one non-negotiable row."""
assert self._stance()["stance"]["z3-critical"] == "fail_closed"
def test_verdict_is_never_cached(self):
"""§6.4 obligation 2: caching an input claim is permitted; caching the
answer is a second decision point deciding early (§6.1)."""
assert self._stance()["verdict_caching"] == "none"

33
tests/test_mask.py Normal file
View file

@ -0,0 +1,33 @@
"""Tests for the masking display filter (WARDEN-WP-0026 T03)."""
from warden.mask import fingerprint, mask_value
from warden.proxy import ResolvedFetch, proxy_fetch_fingerprint
def test_mask_never_contains_the_value():
secret = "ghp_realtokenvalue1234567890abcdef"
masked = mask_value(secret)
assert secret not in masked
assert "hidden" in masked and "len=" in masked and "sha256:" in masked
def test_fingerprint_reports_presence_and_length():
fp = fingerprint("abcd")
assert fp.present is True and fp.length == 4
assert len(fp.sha256_prefix) == 8
def test_absent_value_renders_absent():
assert mask_value("") == "absent"
assert mask_value(None) == "absent"
assert fingerprint(None).present is False
def test_fingerprint_is_stable_and_discriminating():
assert fingerprint("token-A").sha256_prefix == fingerprint("token-A").sha256_prefix
assert fingerprint("token-A").sha256_prefix != fingerprint("token-B").sha256_prefix
def test_proxy_fingerprint_returns_mask_not_value():
fp = proxy_fetch_fingerprint(ResolvedFetch(shell_cmd="printf 'the-secret-value'"))
assert fp.present and fp.length == len("the-secret-value")
assert "the-secret-value" not in fp.render()

View file

@ -7,7 +7,7 @@ import json
from typer.testing import CliRunner
from warden.cli import app
from warden.memory import activate, enabled, record_command_episode, status, store_path
from warden.memory import activate, record_command_episode, status, store_path
from warden.worker import RuleBrain, _plan_with_memory, build_plans
runner = CliRunner()

121
tests/test_plan.py Normal file
View file

@ -0,0 +1,121 @@
"""Tests for warden plan (WARDEN-WP-0029 T01)."""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from typer.testing import CliRunner
from warden.cli import app
from warden.plan import build_plan
from warden.posture import load_posture
from warden.routing.catalog import load_catalog
runner = CliRunner()
REPO = Path(__file__).resolve().parents[1]
@pytest.fixture(autouse=True)
def _catalog_env(monkeypatch):
monkeypatch.setenv("WARDEN_ROUTING_CATALOG", str(REPO / "registry/routing/catalog.yaml"))
monkeypatch.setenv("WARDEN_POSTURE_CATALOG", str(REPO / "registry/policy/security-posture.yaml"))
def test_plan_forgejo_deploy_key_autonomous():
plan = build_plan("forgejo deploy key for binky-control")
assert plan.verdict == "autonomous"
assert plan.organization_posture == "build"
assert plan.lane_id == "agent-harness-forgejo-deploy"
assert plan.commands
assert plan.founder_act is None
assert plan.catalog.get("content_hash")
def test_plan_forgejo_admin_autonomous():
plan = build_plan("forgejo admin api token")
assert plan.verdict == "autonomous"
assert plan.lane_id == "forgejo-admin-api-token"
assert any("warden access forgejo-admin-api-token" in c for c in plan.commands)
def test_plan_new_secret_founder_required():
plan = build_plan("provision a new secret token for a tenant workload")
assert plan.verdict == "founder_required"
assert plan.founder_act is not None
assert plan.founder_act.kind in ("paste_once_provision", "approve", "oidc_login")
def test_plan_login_founder_required():
plan = build_plan("oidc login mfa key-cape")
assert plan.verdict == "founder_required"
assert plan.founder_act is not None
assert plan.founder_act.kind == "oidc_login"
def test_plan_first_time_openbao_database_admin_uses_platform_admin_login():
plan = build_plan(
"first-time OpenBao database engine administration for "
"database/config/platform-pg-2 dynamic roles policies and token roles; "
"requires attended platform-admin handoff"
)
assert plan.verdict == "founder_required"
assert plan.lane_id == "openbao-platform-admin-login"
assert plan.founder_act is not None
assert plan.founder_act.kind == "oidc_login"
command = plan.founder_act.details["fetch_command"]
assert command == (
"warden access openbao-platform-admin-login --exec -- <reviewed-command>"
)
assert "financials" not in command
assert "paste_once" not in plan.founder_act.details["desk_hint"]
assert any(
item
== "warden access openbao-platform-admin-login --exec -- <reviewed-command>"
for item in plan.commands
)
assert not any("--fetch" in item for item in plan.commands)
assert not any("--out" in item or "--wrap" in item for item in plan.commands)
def test_plan_openbao_shamir_recovery_uses_approval_ceremony_not_secret_provision():
plan = build_plan(
"coordinate one attended production OpenBao emergency seal/unseal drill "
"with a fresh encrypted off-host Raft snapshot receipt, verified "
"provider-console access, two-of-three Shamir custodian quorum, named "
"driver and abort operator, without exposing credential values"
)
assert plan.verdict == "founder_required"
assert plan.lane_id == "openbao-shamir-recovery-ceremony"
assert plan.founder_act is not None
assert plan.founder_act.kind == "approve"
assert "openbao-shamir-recovery-ceremony" in plan.founder_act.details["desk_hint"]
assert "paste_once" not in plan.founder_act.details["desk_hint"]
assert all("warden access" not in item for item in plan.commands)
assert any("openbao-shamir-recovery-ceremony" in item for item in plan.commands)
def test_plan_unroutable():
# Zero keyword overlap with catalog (avoid tokens like secret/key/token)
plan = build_plan("xyzzy-plugh-fnord-qqq-zzzz")
assert plan.verdict == "unroutable"
assert plan.ccr_stub is not None
assert plan.lane_id is None
def test_plan_composes_catalog_find():
"""Plan must use Catalog.find — exact id match wins."""
cat = load_catalog()
plan = build_plan("ssh-cert-host-access", catalog=cat, posture=load_posture())
assert plan.verdict == "autonomous"
assert plan.lane_id == "ssh-cert-host-access"
assert any("warden sign" in c for c in plan.commands)
def test_cli_plan_json():
r = runner.invoke(app, ["plan", "forgejo deploy key for binky-control", "--json"])
assert r.exit_code == 0, r.stdout + r.stderr
payload = json.loads(r.stdout)
assert payload["verdict"] == "autonomous"
assert payload["organization_posture"] == "build"
assert payload["lane_id"] == "agent-harness-forgejo-deploy"

View file

@ -1,4 +1,5 @@
"""Tests for warden.policy — flex-auth gate."""
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch
@ -6,7 +7,12 @@ import httpx
import pytest
from warden.ca import CAError
from warden.config import PolicyConfig
from warden.caller_identity import (
CallerIdentityError,
caller_auth_headers,
resolve_caller_token,
)
from warden.config import CallerAuthConfig, PolicyConfig
from warden.models import ActorType, CertSpec
from warden.policy import check_sign_policy, pubkey_fingerprint
@ -21,6 +27,17 @@ def _spec(pubkey_path: Path) -> CertSpec:
)
def _zone_registry(tmp_path: Path, zone: str) -> Path:
path = tmp_path / "registry.json"
path.write_text(
'{"resource_manifests":[{"resources":[{"id":'
'"ssh-cert:actor/agt-state-hub-bridge","attributes":{'
f'"security_zone":"{zone}","security_zone_admission":"satisfied"'
'}}]}]}'
)
return path
def test_pubkey_fingerprint(tmp_path):
pubkey = tmp_path / "key.pub"
pubkey.write_text("ssh-ed25519 AAAA test\n")
@ -29,26 +46,32 @@ def test_pubkey_fingerprint(tmp_path):
assert len(fp) == 7 + 64
def test_disabled_returns_none(tmp_path):
def test_unconfigured_evaluator_uses_unknown_fail_open_profile(tmp_path):
pubkey = tmp_path / "key.pub"
pubkey.write_text("ssh-ed25519 AAAA\n")
cfg = PolicyConfig(enabled=False)
assert check_sign_policy(cfg, _spec(pubkey)) is None
cfg = PolicyConfig()
spec = _spec(pubkey)
assert check_sign_policy(cfg, spec) is None
assert spec.policy_zone == "unknown"
assert spec.policy_failure_mode == "fail_open"
assert spec.policy_outcome == "fail_open"
def test_allow_returns_decision_id(tmp_path):
pubkey = tmp_path / "key.pub"
pubkey.write_text("ssh-ed25519 AAAA\n")
cfg = PolicyConfig(enabled=True, flex_auth_url="http://flex-auth.test")
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
mock_response = MagicMock()
mock_response.json.return_value = {"effect": "allow", "id": "dec-123"}
mock_response.raise_for_status = MagicMock()
spec = _spec(pubkey)
with patch("warden.policy.httpx.post", return_value=mock_response) as post:
result = check_sign_policy(cfg, _spec(pubkey))
result = check_sign_policy(cfg, spec)
assert result == "dec-123"
assert spec.policy_outcome == "allow"
post.assert_called_once()
call_kwargs = post.call_args
assert call_kwargs[0][0] == "http://flex-auth.test/v1/check"
@ -61,7 +84,7 @@ def test_allow_returns_decision_id(tmp_path):
def test_deny_raises_ca_error(tmp_path):
pubkey = tmp_path / "key.pub"
pubkey.write_text("ssh-ed25519 AAAA\n")
cfg = PolicyConfig(enabled=True)
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
mock_response = MagicMock()
mock_response.json.return_value = {
@ -78,7 +101,10 @@ def test_deny_raises_ca_error(tmp_path):
def test_unreachable_fail_closed_raises(tmp_path):
pubkey = tmp_path / "key.pub"
pubkey.write_text("ssh-ed25519 AAAA\n")
cfg = PolicyConfig(enabled=True, fail_closed=True)
cfg = PolicyConfig(
flex_auth_url="http://flex-auth.test",
zone_registry_path=_zone_registry(tmp_path, "z3-critical"),
)
with patch(
"warden.policy.httpx.post",
@ -91,7 +117,7 @@ def test_unreachable_fail_closed_raises(tmp_path):
def test_unreachable_fail_open_returns_none(tmp_path):
pubkey = tmp_path / "key.pub"
pubkey.write_text("ssh-ed25519 AAAA\n")
cfg = PolicyConfig(enabled=True, fail_closed=False)
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
with patch(
"warden.policy.httpx.post",
@ -103,7 +129,10 @@ def test_unreachable_fail_open_returns_none(tmp_path):
def test_http_error_fail_closed_raises(tmp_path):
pubkey = tmp_path / "key.pub"
pubkey.write_text("ssh-ed25519 AAAA\n")
cfg = PolicyConfig(enabled=True, fail_closed=True)
cfg = PolicyConfig(
flex_auth_url="http://flex-auth.test",
zone_registry_path=_zone_registry(tmp_path, "z3-critical"),
)
mock_response = MagicMock()
mock_response.status_code = 403
@ -117,7 +146,7 @@ def test_http_error_fail_closed_raises(tmp_path):
def test_missing_pubkey_raises(tmp_path):
cfg = PolicyConfig(enabled=True)
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
spec = _spec(tmp_path / "missing.pub")
with pytest.raises(CAError, match="Public key not found"):
check_sign_policy(cfg, spec)
@ -126,7 +155,10 @@ def test_missing_pubkey_raises(tmp_path):
def test_subject_from_env(tmp_path, monkeypatch):
pubkey = tmp_path / "key.pub"
pubkey.write_text("ssh-ed25519 AAAA\n")
cfg = PolicyConfig(enabled=True, subject_env="WARDEN_POLICY_SUBJECT")
cfg = PolicyConfig(
flex_auth_url="http://flex-auth.test",
subject_env="WARDEN_POLICY_SUBJECT",
)
monkeypatch.setenv("WARDEN_POLICY_SUBJECT", "iam:bernd")
mock_response = MagicMock()
@ -138,3 +170,140 @@ def test_subject_from_env(tmp_path, monkeypatch):
body = post.call_args[1]["json"]
assert body["subject"]["id"] == "iam:bernd"
# --- caller identity (FLEX-WP-0016 / WARDEN-WP-0031) -----------------------
def test_caller_auth_none_sends_no_header():
assert caller_auth_headers(CallerAuthConfig()) == {}
def test_caller_auth_file_reads_projected_token(tmp_path):
token_file = tmp_path / "token"
token_file.write_text("sa-token-value\n")
cfg = CallerAuthConfig(mode="file", token_path=token_file)
assert caller_auth_headers(cfg) == {"Authorization": "Bearer sa-token-value"}
def test_caller_auth_file_missing_raises(tmp_path):
cfg = CallerAuthConfig(mode="file", token_path=tmp_path / "absent")
with pytest.raises(CallerIdentityError, match="unreadable"):
resolve_caller_token(cfg)
def test_caller_auth_env_mode(monkeypatch):
monkeypatch.setenv("WARDEN_POLICY_CALLER_TOKEN", " env-token ")
assert resolve_caller_token(CallerAuthConfig(mode="env")) == "env-token"
monkeypatch.setenv("WARDEN_POLICY_CALLER_TOKEN", "")
with pytest.raises(CallerIdentityError, match="unset or empty"):
resolve_caller_token(CallerAuthConfig(mode="env"))
def test_caller_auth_command_mode_uses_stdout(monkeypatch):
cfg = CallerAuthConfig(mode="command", command=["kubectl", "create", "token"])
def fake_run(cmd, **kwargs):
assert cmd == cfg.command
return subprocess.CompletedProcess(cmd, 0, stdout="minted-token\n", stderr="")
monkeypatch.setattr(subprocess, "run", fake_run)
assert resolve_caller_token(cfg) == "minted-token"
def test_caller_auth_command_failure_message_excludes_token(monkeypatch):
cfg = CallerAuthConfig(mode="command", command=["kubectl", "create", "token"])
def fake_run(cmd, **kwargs):
return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="error: forbidden\n")
monkeypatch.setattr(subprocess, "run", fake_run)
with pytest.raises(CallerIdentityError, match="error: forbidden"):
resolve_caller_token(cfg)
def test_caller_auth_rejects_whitespace_token(tmp_path):
token_file = tmp_path / "token"
token_file.write_text("two words")
cfg = CallerAuthConfig(mode="file", token_path=token_file)
with pytest.raises(CallerIdentityError, match="whitespace"):
resolve_caller_token(cfg)
def test_sign_policy_sends_authorization_header(tmp_path, monkeypatch):
"""The header flex-auth's ops-warden pin needs to leave warn mode."""
from warden import policy as policy_mod
token_file = tmp_path / "token"
token_file.write_text("sa-token-value")
pubkey = tmp_path / "id.pub"
pubkey.write_text("ssh-ed25519 AAAA test\n")
cfg = PolicyConfig(
flex_auth_url="http://flex-auth.test",
caller_auth=CallerAuthConfig(mode="file", token_path=token_file),
)
spec = CertSpec(
actor_name="agt-state-hub-bridge",
actor_type=ActorType.AGT,
principals=["agt"],
ttl_hours=24,
pubkey_path=pubkey,
)
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["headers"] = headers
return _Response()
monkeypatch.setattr(policy_mod.httpx, "post", fake_post)
assert policy_mod.check_sign_policy(cfg, spec) == "decision:49350f1064f674d7"
assert seen["headers"] == {"Authorization": "Bearer sa-token-value"}
def test_sign_policy_fail_closed_when_caller_token_unavailable(tmp_path):
from warden.ca import CAError
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",
zone_registry_path=_zone_registry(tmp_path, "z3-critical"),
caller_auth=CallerAuthConfig(mode="file", token_path=tmp_path / "absent"),
)
spec = CertSpec(
actor_name="agt-state-hub-bridge",
actor_type=ActorType.AGT,
principals=["agt"],
ttl_hours=24,
pubkey_path=pubkey,
)
with pytest.raises(CAError, match="caller identity unavailable"):
policy_mod.check_sign_policy(cfg, spec)
def test_advisory_decision_is_recorded_and_does_not_block(tmp_path):
pubkey = tmp_path / "id.pub"
pubkey.write_text("ssh-ed25519 AAAA test\n")
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
response = MagicMock()
response.json.return_value = {
"effect": "audit_only",
"reason": "advisory_would_deny_disallowed_principal",
"id": "decision:advisory",
}
response.raise_for_status = MagicMock()
spec = _spec(pubkey)
with patch("warden.policy.httpx.post", return_value=response):
assert check_sign_policy(cfg, spec) == "decision:advisory"
assert spec.policy_zone == "unknown"
assert spec.policy_outcome == "audit_only"

View file

@ -27,6 +27,9 @@ def test_real_descriptors_load():
assert c.requires_env_posture == "prod"
# YAML `on` gotcha must not have become a boolean
assert c.env("test").audit == "on"
# WARDEN-WP-0029 third axis
assert c.organization_posture.id == "build"
assert "workstation_oidc_acceptable" in c.organization_posture.relaxations
# --- the secret-flow lattice -----------------------------------------------
@ -92,6 +95,12 @@ def _valid_data() -> dict:
],
"dataclass_floor": {"synthetic": "M0", "internal": "M1"},
"lattice": {"requires_env_posture": "prod", "rule": "no-write-down"},
"organization_posture": {
"id": "build",
"summary": "test build posture",
"relaxations": ["workstation_oidc_acceptable"],
"graduation_triggers": ["first_customer_data"],
},
}
@ -136,6 +145,16 @@ def test_cli_policy_list_json(monkeypatch):
payload = json.loads(r.stdout)
assert payload["requires_env_posture"] == "prod"
assert len(payload["maturity_levels"]) == 4
assert payload["organization_posture"]["id"] == "build"
def test_cli_policy_show_organization(monkeypatch):
monkeypatch.setenv("WARDEN_POSTURE_CATALOG", str(_repo_posture()))
r = runner.invoke(app, ["policy", "show", "build", "--json"])
assert r.exit_code == 0
payload = json.loads(r.stdout)
assert payload["axis"] == "organization_posture"
assert payload["id"] == "build"
def test_cli_policy_show_unknown_exits_1(monkeypatch):

View file

@ -13,6 +13,7 @@ from warden.proxy import (
ProxyError,
ResolvedFetch,
caller_auth_present,
proxy_attended_login_exec,
proxy_exec,
proxy_fetch,
resolve_fetch_command,
@ -64,16 +65,16 @@ def test_resolve_refuses_non_exec_capable():
resolve_fetch_command(_entry(exec_capable=False, fetch_command=None))
def test_resolve_piped_fetch_uses_shell_cmd():
def test_resolve_bao_fetch_uses_argv():
from warden.routing import load_catalog
catalog = load_catalog(Path(__file__).resolve().parents[1] / "registry" / "routing" / "catalog.yaml")
entry = catalog.get("reuse-surface-hub-write-token")
resolved = resolve_fetch_command(entry)
assert resolved.argv is None
assert resolved.shell_cmd is not None
assert "| base64 -d" in resolved.shell_cmd
assert "reuse-surface-env" in resolved.shell_cmd
assert resolved.argv is not None
assert resolved.shell_cmd is None
assert resolved.argv[0] == "bao"
assert "platform/workloads/reuse/reuse-surface/runtime-secrets" in resolved.argv
# --- G2: transit-only fetch (inherited stdout) -----------------------------
@ -193,7 +194,6 @@ def _warden_yaml(tmp_path: Path) -> Path:
(tmp_path / "ca").write_text("")
cfg.write_text(
f"backend: local\nca_key: {tmp_path/'ca'}\nstate_dir: {tmp_path/'state'}\n"
"policy:\n enabled: false\n"
)
return cfg
@ -203,10 +203,11 @@ def _proxy_env(monkeypatch, tmp_path):
monkeypatch.setenv("WARDEN_CONFIG", str(_warden_yaml(tmp_path)))
def test_cli_proxy_refuses_without_policy_ack(monkeypatch, tmp_path):
def test_cli_proxy_unknown_zone_fail_open_reaches_transport_guard(monkeypatch, tmp_path):
_proxy_env(monkeypatch, tmp_path)
monkeypatch.setenv("VAULT_TOKEN", "caller")
# subprocess must never run if the gate blocks first.
# The unknown-zone profile proceeds when no evaluator is configured, then
# the independent safe-transport boundary still refuses captured stdout.
monkeypatch.setattr(
"warden.proxy.subprocess.run",
lambda *a, **k: (_ for _ in ()).throw(AssertionError("fetch ran despite gate")),
@ -216,8 +217,8 @@ def test_cli_proxy_refuses_without_policy_ack(monkeypatch, tmp_path):
["access", "npm", "--domain", "coulomb_social", "--field", "NPM_AUTH_TOKEN",
"--path", "platform/x/y/z", "--fetch"],
)
assert r.exit_code == 4
assert "not enforced" in r.stdout or "not enforced" in str(r.output)
assert r.exit_code == 6
assert "unknown-zone fail_open" in r.output
def test_cli_proxy_requires_caller_auth(monkeypatch, tmp_path):
@ -228,44 +229,230 @@ def test_cli_proxy_requires_caller_auth(monkeypatch, tmp_path):
r = runner.invoke(
app,
["access", "npm", "--domain", "coulomb_social", "--field", "NPM_AUTH_TOKEN",
"--path", "platform/x/y/z", "--fetch", "--no-policy"],
"--path", "platform/x/y/z", "--fetch"],
)
assert r.exit_code == 3
def test_cli_proxy_rejects_retired_no_policy_bypass(monkeypatch, tmp_path):
_proxy_env(monkeypatch, tmp_path)
monkeypatch.setenv("VAULT_TOKEN", "caller")
monkeypatch.setattr(
"warden.proxy.subprocess.run",
lambda *a, **k: (_ for _ in ()).throw(AssertionError("fetch ran despite retired flag")),
)
r = runner.invoke(
app,
["access", "npm", "--domain", "coulomb_social", "--field", "NPM_AUTH_TOKEN",
"--path", "platform/x/y/z", "--fetch", "--no-policy"],
)
assert r.exit_code == 2
assert "--no-policy is retired" in r.output
# --- T4: login lane --------------------------------------------------------
def test_cli_login_lane_runs_without_token_or_policy_ack(monkeypatch, tmp_path):
"""Login lane skips the caller-auth precheck and the secret-read gate."""
def test_cli_login_lane_contains_login_handoff_and_revocation(monkeypatch, tmp_path):
"""Login and its reviewed child share a private, silent helper session."""
_proxy_env(monkeypatch, tmp_path)
monkeypatch.delenv("VAULT_TOKEN", raising=False)
monkeypatch.delenv("BAO_TOKEN", raising=False)
monkeypatch.setattr(Path, "home", lambda: tmp_path) # no ~/.vault-token
ran = {}
calls = []
def fake_run(argv, **kw):
ran["argv"] = argv
ran["stdout"] = kw.get("stdout")
return subprocess.CompletedProcess(argv, 0)
calls.append((argv, kw))
assert kw["stdout"] is subprocess.PIPE
assert kw["stderr"] is subprocess.PIPE
private_home = Path(kw["env"]["HOME"])
assert private_home != tmp_path
assert oct(private_home.stat().st_mode & 0o777) == "0o700"
helper = private_home / ".vault-token"
assert oct(helper.stat().st_mode & 0o777) == "0o600"
if argv[:2] == ["bao", "login"]:
helper.write_bytes(b"non-production-test-double")
helper.chmod(0o600)
return subprocess.CompletedProcess(argv, 0, stdout=b"", stderr=b"")
monkeypatch.setattr("warden.proxy.subprocess.run", fake_run)
r = runner.invoke(app, ["access", "login oidc", "--domain", "coulomb_social", "--fetch"])
r = runner.invoke(
app,
[
"access", "login oidc", "--domain", "coulomb_social",
"--exec", "--", "true",
],
)
assert r.exit_code == 0
assert ran["argv"][:2] == ["bao", "login"] # interactive login ran
assert ran["stdout"] is None # inherited stdio — token not captured
assert [call[0][:2] for call in calls] == [
["bao", "login"],
["true"],
["bao", "token"],
]
assert not (tmp_path / ".warden-attended-login").exists()
assert "non-production-test-double" not in r.output
audit = (tmp_path / "state" / "access-audit.log").read_text()
assert "non-production-test-double" not in audit
def test_cli_login_lane_rejects_exec(monkeypatch, tmp_path):
def test_cli_login_lane_rejects_persistent_fetch(monkeypatch, tmp_path):
_proxy_env(monkeypatch, tmp_path)
monkeypatch.setattr(
"warden.proxy.subprocess.run",
lambda *a, **k: (_ for _ in ()).throw(AssertionError("should not run")),
)
r = runner.invoke(
app, ["access", "login oidc", "--domain", "coulomb_social", "--exec", "--", "true"]
app, ["access", "login oidc", "--domain", "coulomb_social", "--fetch"]
)
assert r.exit_code == 2
assert "requires --exec" in r.output
def test_attended_login_refuses_read_only_home_before_auth(monkeypatch, tmp_path):
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setattr(
"warden.proxy.subprocess.run",
lambda *a, **k: (_ for _ in ()).throw(AssertionError("OIDC started")),
)
tmp_path.chmod(0o555)
try:
with pytest.raises(ProxyError, match="writable default home"):
proxy_attended_login_exec(
ResolvedFetch(argv=["bao", "login", "-no-print"]),
child_argv=["true"],
)
finally:
tmp_path.chmod(0o700)
def test_attended_login_persistence_failure_revokes_and_cleans(monkeypatch, tmp_path):
monkeypatch.setattr(Path, "home", lambda: tmp_path)
calls = []
def fake_run(argv, **kw):
calls.append(argv)
# Login succeeds but the pre-created helper remains empty: persistence failed.
return subprocess.CompletedProcess(argv, 0, stdout=b"", stderr=b"")
monkeypatch.setattr("warden.proxy.subprocess.run", fake_run)
with pytest.raises(ProxyError, match="failed closed before command handoff"):
proxy_attended_login_exec(
ResolvedFetch(argv=["bao", "login", "-no-print"]),
child_argv=["should-not-run"],
)
assert calls == [
["bao", "login", "-no-print", "-format=json"],
["bao", "token", "revoke", "-self"],
]
assert not (tmp_path / ".warden-attended-login").exists()
@pytest.mark.parametrize("stream", ["stdout", "stderr"])
def test_attended_login_unexpected_output_is_contained_revoked_and_cleaned(
monkeypatch, tmp_path, capsys, stream
):
monkeypatch.setattr(Path, "home", lambda: tmp_path)
sentinel = "hvs.NONPRODUCTION_SENTINEL"
calls = []
def fake_run(argv, **kw):
calls.append((argv, dict(kw["env"])))
if argv[:2] == ["bao", "login"]:
output = sentinel.encode()
return subprocess.CompletedProcess(
argv,
0,
stdout=output if stream == "stdout" else b"",
stderr=output if stream == "stderr" else b"",
)
if argv[:3] == ["bao", "token", "revoke"]:
# The helper is empty. The second contained attempt uses the captured
# value only through BAO_TOKEN, never argv or visible output.
return subprocess.CompletedProcess(
argv,
0 if kw["env"].get("BAO_TOKEN") == sentinel else 1,
stdout=b"",
stderr=b"",
)
raise AssertionError("reviewed child ran after unexpected login output")
monkeypatch.setattr("warden.proxy.subprocess.run", fake_run)
with pytest.raises(ProxyError, match="failed closed before command handoff") as exc:
proxy_attended_login_exec(
ResolvedFetch(argv=["bao", "login", "-no-print"]),
child_argv=["should-not-run"],
)
captured = capsys.readouterr()
assert sentinel not in str(exc.value)
assert sentinel not in captured.out
assert sentinel not in captured.err
assert all(sentinel not in " ".join(argv) for argv, _ in calls)
assert calls[-1][1]["BAO_TOKEN"] == sentinel
assert not (tmp_path / ".warden-attended-login").exists()
def test_attended_login_contained_success_output_never_escapes(monkeypatch, tmp_path, capsys):
monkeypatch.setattr(Path, "home", lambda: tmp_path)
sentinel = "hvs.NONPRODUCTION_CONTAINED_LOGIN"
child_ran = False
def fake_run(argv, **kw):
nonlocal child_ran
helper = Path(kw["env"]["HOME"]) / ".vault-token"
if argv[:2] == ["bao", "login"]:
helper.write_text(sentinel)
helper.chmod(0o600)
return subprocess.CompletedProcess(
argv,
0,
stdout=json.dumps({"auth": {"client_token": sentinel}}).encode(),
stderr=b"",
)
if argv == ["reviewed-child"]:
child_ran = True
return subprocess.CompletedProcess(argv, 0, stdout=b"", stderr=b"")
if argv[:3] == ["bao", "token", "revoke"]:
return subprocess.CompletedProcess(argv, 0, stdout=b"", stderr=b"")
raise AssertionError(argv)
monkeypatch.setattr("warden.proxy.subprocess.run", fake_run)
assert proxy_attended_login_exec(
ResolvedFetch(argv=["bao", "login", "-no-print"]),
child_argv=["reviewed-child"],
) == 0
captured = capsys.readouterr()
assert child_ran is True
assert sentinel not in captured.out
assert sentinel not in captured.err
assert not (tmp_path / ".warden-attended-login").exists()
def test_attended_login_preserves_caller_warden_config_for_reviewed_child(
monkeypatch, tmp_path
):
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.delenv("WARDEN_CONFIG", raising=False)
caller_config = tmp_path / ".config" / "warden" / "warden.yaml"
caller_config.parent.mkdir(parents=True)
caller_config.write_text("backend: local\n")
seen_config = None
def fake_run(argv, **kw):
nonlocal seen_config
helper = Path(kw["env"]["HOME"]) / ".vault-token"
if argv[:2] == ["bao", "login"]:
helper.write_text("non-production-test-double")
helper.chmod(0o600)
if argv == ["reviewed-child"]:
seen_config = kw["env"].get("WARDEN_CONFIG")
return subprocess.CompletedProcess(argv, 0, stdout=b"", stderr=b"")
monkeypatch.setattr("warden.proxy.subprocess.run", fake_run)
assert proxy_attended_login_exec(
ResolvedFetch(argv=["bao", "login", "-no-print"]),
child_argv=["reviewed-child"],
) == 0
assert seen_config == str(caller_config)
def test_real_catalog_login_entry_is_login_lane():
@ -281,9 +468,142 @@ def test_invalid_lane_rejected(tmp_path):
id="x", title="t", need_keywords=["k"], owner_repo="o", subsystem="s",
warden_executes=False, wiki_ref="w", canon_ref="c", reviewed="2026-06-27",
status="active", lane="bogus",
workload_ref={"applicability": "not-applicable", "reason": "fixture"},
)
p = tmp_path / "c.yaml"
p.write_text(yaml.dump({"version": 1, "entries": [entry]}))
import pytest
with pytest.raises(CatalogError, match="invalid lane"):
load_catalog(p)
# ---------------------------------------------------------------------------
# Safe access transports (WARDEN-WP-0026 T02) — no secret values on stdout
# ---------------------------------------------------------------------------
from warden.proxy import ( # noqa: E402
build_wrapped_fetch,
is_bao_kv_fetch,
proxy_fetch_to_file,
proxy_fetch_wrapped,
)
def test_fetch_to_file_writes_mode_0600_and_no_stdout(tmp_path, capsys):
out = tmp_path / "secret.out"
rc = proxy_fetch_to_file(ResolvedFetch(shell_cmd="printf 'sekret'"), out)
assert rc == 0
assert out.read_text() == "sekret"
assert oct(out.stat().st_mode & 0o777) == "0o600"
# nothing printed to stdout/stderr by the transport itself
captured = capsys.readouterr()
assert "sekret" not in captured.out and "sekret" not in captured.err
def test_fetch_to_file_forces_0600_on_preexisting_loose_file(tmp_path):
out = tmp_path / "pre.out"
out.write_text("old")
out.chmod(0o644)
proxy_fetch_to_file(ResolvedFetch(shell_cmd="printf 'new'"), out)
assert out.read_text() == "new"
assert oct(out.stat().st_mode & 0o777) == "0o600"
def test_wrapped_fetch_returns_token_not_value():
payload = '{"wrap_info":{"token":"hvs.WRAP"}}'
token = proxy_fetch_wrapped(ResolvedFetch(shell_cmd=f"printf '%s' '{payload}'"))
assert token == "hvs.WRAP"
def test_wrapped_fetch_bad_output_raises():
with pytest.raises(ProxyError, match="wrapping token"):
proxy_fetch_wrapped(ResolvedFetch(shell_cmd="printf 'not-json'"))
def test_build_wrapped_fetch_only_for_bao_kv():
bao = _entry(fetch_command="bao kv get -field=API_TOKEN platform/x", path_template="platform/x")
assert is_bao_kv_fetch(bao)
argv = build_wrapped_fetch(bao, ttl="9m").argv
assert argv == ["bao", "kv", "get", "-wrap-ttl=9m", "-format=json", "platform/x"]
piped = _entry(fetch_command="kubectl get secret x -o json | base64 -d", path_template="x")
assert not is_bao_kv_fetch(piped)
with pytest.raises(ProxyError, match="response wrapping"):
build_wrapped_fetch(piped)
def test_build_wrapped_fetch_refuses_placeholder_path():
e = _entry(fetch_command="bao kv get -field=<FIELD> <path_template>",
path_template="platform/workloads/<domain>/x")
with pytest.raises(ProxyError, match="concrete path"):
build_wrapped_fetch(e)
def test_access_fetch_to_nonterminal_stdout_is_refused(tmp_path, monkeypatch):
"""The anti-pattern: streaming a value to captured stdout is refused (exit 6)."""
_proxy_env(monkeypatch, tmp_path)
monkeypatch.setenv("VAULT_TOKEN", "caller-token") # G1 caller-auth precheck
# The guard trips before the fetch runs; make a real bao call fail loudly if reached.
monkeypatch.setattr(
"warden.proxy.subprocess.run",
lambda *a, **k: (_ for _ in ()).throw(AssertionError("fetch ran despite stdout guard")),
)
# CliRunner captures stdout (not a tty), so the guard trips without --unsafe-stdout.
r = runner.invoke(app, ["access", "whynot-design-npm-publish", "--fetch"])
assert r.exit_code == 6
assert "sanctioned transport" in r.output.lower() or "refusing" in r.output.lower()
def test_access_fingerprint_masks_and_bypasses_stdout_guard(monkeypatch, tmp_path):
"""--fingerprint prints a masked fingerprint (never the value) even to captured stdout."""
_proxy_env(monkeypatch, tmp_path)
monkeypatch.setenv("VAULT_TOKEN", "caller-token")
class _Fake:
returncode = 0
stdout = "top-secret-token-value"
monkeypatch.setattr("warden.proxy.subprocess.run", lambda *a, **k: _Fake())
r = runner.invoke(
app,
["access", "whynot-design-npm-publish", "--fingerprint"],
)
assert r.exit_code == 0
assert "top-secret-token-value" not in r.output # value never shown
assert "hidden" in r.output and "sha256:" in r.output
def test_access_agent_high_risk_raw_stream_refused(tmp_path, monkeypatch):
"""WP-0026 T04: WARDEN_AGENT_ID + risk=high refuses raw value stream (exit 7)."""
_proxy_env(monkeypatch, tmp_path)
monkeypatch.setenv("VAULT_TOKEN", "caller-token")
monkeypatch.setenv("WARDEN_AGENT_ID", "grok")
# Prefer high-risk lane; use --unsafe-stdout so T02 would allow if T04 failed.
r = runner.invoke(
app,
[
"access", "railiance-backup-offsite-lane",
"--fetch", "--unsafe-stdout",
],
)
assert r.exit_code == 7, r.output
assert "agent read-boundary" in r.output.lower() or "risk=high" in r.output.lower()
def test_access_agent_high_risk_fingerprint_allowed(tmp_path, monkeypatch):
"""Agents may use --fingerprint on high-risk lanes (no raw value)."""
_proxy_env(monkeypatch, tmp_path)
monkeypatch.setenv("VAULT_TOKEN", "caller-token")
monkeypatch.setenv("WARDEN_AGENT_ID", "grok")
class _Fake:
returncode = 0
stdout = "should-not-appear"
monkeypatch.setattr("warden.proxy.subprocess.run", lambda *a, **k: _Fake())
r = runner.invoke(
app,
["access", "railiance-backup-offsite-lane", "--fingerprint"],
)
assert r.exit_code == 0, r.output
assert "should-not-appear" not in r.output

View file

@ -4,6 +4,7 @@ No test here requires a live subsystem — routing is a read-only pointer layer.
"""
import json
import re
from datetime import date
from pathlib import Path
import pytest
@ -11,10 +12,9 @@ import yaml
from typer.testing import CliRunner
from warden.cli import app
from datetime import date
from warden.routing import CatalogError, load_catalog
from warden.routing.catalog import days_since_review, find_catalog_path, is_review_stale
from warden.scorecard import check_catalog_rotation_coverage
runner = CliRunner()
@ -40,6 +40,10 @@ SSH_ENTRY = {
"canon_ref": "net-kingdom/docs/x.md",
"reviewed": "2026-06-18",
"status": "active",
"workload_ref": {
"applicability": "not-applicable",
"reason": "generic certificate action",
},
"cert_command": "warden sign <actor> --pubkey <path>",
"steps": ["confirm inventory", "sign"],
}
@ -55,6 +59,10 @@ ROUTED_ENTRY = {
"canon_ref": "net-kingdom/docs/x.md",
"reviewed": "2026-06-18",
"status": "active",
"workload_ref": {
"applicability": "not-applicable",
"reason": "generic credential pattern",
},
}
@ -76,6 +84,45 @@ def test_real_catalog_has_one_executed_lane():
assert [e.id for e in executed] == ["ssh-cert-host-access"]
def test_every_catalog_lane_declares_workload_applicability():
catalog = load_catalog(_repo_catalog())
assert all(entry.workload_ref is not None for entry in catalog.entries)
assert {entry.workload_ref.resolution for entry in catalog.entries} == {
"resolved", "unknown", "not-applicable"
}
def test_managed_and_operational_workload_references_parse():
catalog = load_catalog(_repo_catalog())
managed = catalog.get("issue-core-ingestion-api-key").workload_ref
assert managed.resolution == "resolved"
assert (managed.rapp_id, managed.name, managed.deployable) == (
"rapp-issue-core", "issue-core", "issue-core"
)
operational = catalog.get("ops-warden-warden-sign-token").workload_ref
assert operational.resolution == "resolved"
assert operational.rapp_id is None
assert operational.name == "ops-warden"
assert operational.declaration_ref == "tenancy.yaml"
def test_workload_reference_rejects_ambiguous_absence(tmp_path):
bad = dict(ROUTED_ENTRY)
bad.pop("workload_ref")
with pytest.raises(CatalogError, match="workload_ref"):
load_catalog(_write_catalog(tmp_path, [bad]))
def test_workload_reference_rejects_malformed_managed_target(tmp_path):
bad = dict(ROUTED_ENTRY)
bad["workload_ref"] = {
"applicability": "applicable",
"rapp_id": "rapp-issue-core",
}
with pytest.raises(CatalogError, match="requires name"):
load_catalog(_write_catalog(tmp_path, [bad]))
def test_ops_warden_warden_sign_lane_has_native_exec():
"""RAILIANCE-WP-0005 T08 — broker lane routes to railiance-platform credential exec."""
catalog = load_catalog(_repo_catalog())
@ -106,6 +153,32 @@ def test_whynot_design_npm_lane_is_concrete_and_resolvable():
assert "platform/workloads/coulomb/whynot-design/npm-publish" in e.fetch_command
def test_policy_nexus_source_read_lane_is_exact_high_risk_and_resolvable():
catalog = load_catalog(_repo_catalog())
entry = catalog.get("policy-nexus-forgejo-source-read")
assert entry is not None and entry.is_active and entry.exec_capable
assert entry.resolvable is True
assert entry.risk == "high"
assert entry.owner_repo == "railiance-platform"
assert entry.fetch_command == (
"bao kv get -field=FORGEJO_SOURCE_TOKEN "
"platform/workloads/policy-nexus/forgejo-source-read"
)
assert entry.path_template == "platform/workloads/policy-nexus/forgejo-source-read"
assert entry.auth_method.endswith(
"role=policy-nexus-forgejo-source-workload-kv-read"
)
assert entry.delegation is not None and entry.delegation.mode == "native"
def test_route_find_policy_nexus_source_read_prefers_concrete_lane():
catalog = load_catalog(_repo_catalog())
matches = catalog.find(
"policy nexus Forgejo private source repository read token Actions", limit=1
)
assert matches[0].id == "policy-nexus-forgejo-source-read"
def test_generic_and_template_lanes_not_resolvable():
catalog = load_catalog(_repo_catalog())
# generic openbao lane has <FIELD>/<path_template>; login lane has <domain>.
@ -113,6 +186,45 @@ def test_generic_and_template_lanes_not_resolvable():
assert catalog.get("key-cape-oidc-login").resolvable is False
def test_platform_admin_login_lane_is_exact_and_non_value_bearing():
entry = load_catalog(_repo_catalog()).get("openbao-platform-admin-login")
assert entry.lane == "login"
assert entry.risk == "high"
assert entry.fetch_command == (
"bao login -no-print -method=oidc -path=netkingdom role=platform-admin"
)
assert entry.workload_ref.resolution == "not-applicable"
def test_netkingdom_sso_bind_lanes_are_routed_but_not_resolvable():
catalog = load_catalog(_repo_catalog())
for lane_id in (
"net-kingdom-lldap-bind-credential",
"net-kingdom-privacyidea-admin-token",
):
entry = catalog.get(lane_id)
assert entry is not None
assert entry.owner_repo == "railiance-platform"
assert entry.risk == "high"
assert entry.warden_executes is False
assert entry.exec_capable is False
assert entry.resolvable is False
assert entry.delegation.blocked_on
assert "net-kingdom-sso-bind-credentials.md#worker-checklist" in entry.wiki_ref
def test_openbao_recovery_ceremony_is_non_value_bearing_owner_pointer():
entry = load_catalog(_repo_catalog()).get("openbao-shamir-recovery-ceremony")
assert entry.lane == "ceremony"
assert entry.risk == "high"
assert entry.owner_repo == "railiance-platform"
assert entry.warden_executes is False
assert entry.exec_capable is False
assert entry.has_handoff is False
assert entry.vends_secret is False
assert entry.workload_ref.resolution == "not-applicable"
def test_find_exact_id_wins_over_keyword_collision():
catalog = load_catalog(_repo_catalog())
# "npm" alone collides with openbao-api-key; the exact id must resolve uniquely.
@ -288,8 +400,8 @@ def test_reuse_surface_hub_write_token_lane_is_resolvable():
e = catalog.get("reuse-surface-hub-write-token")
assert e is not None and e.is_active and e.exec_capable
assert e.resolvable is True
assert e.owner_repo == "reuse-surface"
assert "reuse-surface-env" in e.fetch_command
assert e.owner_repo == "railiance-platform"
assert "platform/workloads/reuse/reuse-surface/runtime-secrets" in e.fetch_command
def test_find_object_storage_sts():
@ -448,3 +560,455 @@ def test_every_entry_has_reviewed_date():
assert re.match(r"^\d{4}-\d{2}-\d{2}$", entry.reviewed), (
f"{entry.id}: reviewed must be YYYY-MM-DD, got {entry.reviewed!r}"
)
# ---------------------------------------------------------------------------
# Rotation / re-establishment guidance registry (WARDEN-WP-0026 T06)
# ---------------------------------------------------------------------------
def test_every_active_vending_lane_has_rotation_guidance():
"""Coverage gate: an active lane that vends a secret must say how to renew it."""
catalog = load_catalog(_repo_catalog())
missing = [e.id for e in catalog.entries if e.is_active and e.vends_secret and not e.has_rotation]
assert not missing, f"active vending lanes lacking rotation guidance: {missing}"
def test_scorecard_rotation_coverage_check_passes_on_repo_catalog():
result = check_catalog_rotation_coverage()
assert result.passed, result.detail
def test_non_vending_lanes_are_exempt_from_rotation():
"""SSH (issue), login, and pointer-only lanes carry no rotation block."""
catalog = load_catalog(_repo_catalog())
assert catalog.get("ssh-cert-host-access").vends_secret is False # issue lane
assert catalog.get("key-cape-oidc-login").vends_secret is False # login lane
assert catalog.get("ops-bridge-tunnel").vends_secret is False # pointer only
def test_rotation_block_parses_fields():
catalog = load_catalog(_repo_catalog())
rot = catalog.get("forgejo-admin-api-token").rotation
assert rot is not None
assert rot.method in ("rotate", "re-establish")
assert rot.owner == "railiance-platform"
assert rot.steps and all(isinstance(s, str) for s in rot.steps)
def test_re_establish_method_on_backup_lane():
catalog = load_catalog(_repo_catalog())
rot = catalog.get("railiance-backup-offsite-lane").rotation
assert rot is not None and rot.method == "re-establish"
def test_invalid_rotation_method_rejected(tmp_path):
entry = dict(ROUTED_ENTRY, rotation={"method": "renew", "owner": "x", "steps": ["a"]})
with pytest.raises(CatalogError, match="rotation.method"):
load_catalog(_write_catalog(tmp_path, [SSH_ENTRY, entry]))
def test_rotation_steps_screened_for_pasted_token(tmp_path):
"""A high-entropy pasted token in prose is rejected; ordinary prose is allowed."""
leak = dict(ROUTED_ENTRY, rotation={
"method": "rotate", "owner": "x",
"steps": ["set the value to ghp_" + "aB3dE5" * 6], # mixed alnum → high-entropy run
})
with pytest.raises(CatalogError, match="high-entropy|secret"):
load_catalog(_write_catalog(tmp_path, [SSH_ENTRY, leak]))
def test_rotation_prose_allows_ordinary_sentences(tmp_path):
"""Words like 'exists.' must not trip the terse 's.' prefix screen."""
ok = dict(ROUTED_ENTRY, rotation={
"method": "rotate", "owner": "railiance-platform",
"steps": ["Rotate per the concrete workload's entry when one exists."],
})
catalog = load_catalog(_write_catalog(tmp_path, [SSH_ENTRY, ok]))
assert catalog.get("openbao-api-key").rotation.steps
def test_rotate_guide_cli_json():
result = runner.invoke(app, ["rotate-guide", "forgejo-admin-api-token", "--json"])
assert result.exit_code == 0
payload = json.loads(result.stdout)
assert payload["method"] == "rotate"
assert payload["owner"] == "railiance-platform"
assert payload["steps"]
def test_rotate_guide_cli_ssh_lane_is_graceful():
# SSH renewal is re-issuance, not a static rotation — exit 0, not an error.
result = runner.invoke(app, ["rotate-guide", "ssh-cert-host-access"])
assert result.exit_code == 0
# ---------------------------------------------------------------------------
# Agent read-boundary + risk class (WARDEN-WP-0026 T04)
# ---------------------------------------------------------------------------
def test_high_risk_lanes_classified():
catalog = load_catalog(_repo_catalog())
high = {e.id for e in catalog.entries if e.is_high_risk}
assert "railiance-backup-offsite-lane" in high
assert "forgejo-admin-api-token" in high
assert "openrouter-llm-connect" in high
# WARDEN-WP-0033-T02: these two were asserted standard here, and the assertion
# held a defective grade still. Both paths carry a second credential the grade
# ignored -- GITEA_BACKEND_TOKEN (CCR-2026-0002) and the dual-consumer webhook
# HMAC (CCR-2026-0005). A read discloses every field at a path, so the grade
# must cover the union, not the headline field.
assert "issue-core-ingestion-api-key" in high
assert "reuse-surface-hub-write-token" in high
def test_invalid_risk_rejected(tmp_path):
bad = dict(ROUTED_ENTRY, risk="critical")
with pytest.raises(CatalogError, match="risk"):
load_catalog(_write_catalog(tmp_path, [SSH_ENTRY, bad]))
def test_backup_lane_promoted_and_resolvable():
"""WP-0026 T07 — CCR-2026-0004 lane is active, resolvable, high-risk, has rotation."""
catalog = load_catalog(_repo_catalog())
e = catalog.get("railiance-backup-offsite-lane")
assert e is not None
assert e.status == "active"
assert e.resolvable is True
assert e.is_high_risk is True
assert e.has_rotation is True
assert e.rotation.method == "re-establish"
assert "NC_WEBDAV_TOKEN" in (e.fetch_command or "")
assert "<" not in (e.fetch_command or "")
def test_route_show_json_includes_risk():
result = runner.invoke(app, ["route", "show", "railiance-backup-offsite-lane", "--json"])
assert result.exit_code == 0
payload = json.loads(result.stdout)
assert payload["risk"] == "high"
assert payload["high_risk"] is True
assert payload["resolvable"] is True
assert payload["status"] == "active"
# ---------------------------------------------------------------------------
# Delegation register (WARDEN-WP-0030)
# ---------------------------------------------------------------------------
def test_every_catalog_entry_declares_delegation():
catalog = load_catalog(_repo_catalog())
missing = [e.id for e in catalog.entries if e.delegation is None]
assert missing == [], f"entries missing delegation block: {missing}"
def test_every_proxy_declares_delegation():
"""A new exec_capable proxy cannot land without answering the ownership question."""
catalog = load_catalog(_repo_catalog())
missing = [
e.id
for e in catalog.entries
if e.exec_capable and not e.warden_executes and e.delegation is None
]
assert missing == [], f"proxy lanes missing delegation: {missing}"
def test_ssh_lane_is_permanent_delegation():
e = load_catalog(_repo_catalog()).get("ssh-cert-host-access")
assert e.delegation is not None
assert e.delegation.mode == "permanent"
assert e.delegation.intended_owner is None
assert e.is_interim is False
def test_native_exec_lanes_are_native_delegation():
catalog = load_catalog(_repo_catalog())
for eid, owner in (
("whynot-design-npm-publish", "secrets-engine"),
("ops-warden-warden-sign-token", "railiance-platform"),
):
e = catalog.get(eid)
assert e.delegation is not None
assert e.delegation.mode == "native"
assert e.delegation.intended_owner == owner
def test_founder_interim_lanes_classified():
catalog = load_catalog(_repo_catalog())
expected = {
"rapp-qonto-keycape-client": "key-cape",
"binky-company-email-imap": "tenant-engine",
"binky-qonto-api": "tenant-engine",
"railiance-backup-offsite-lane": "railiance-platform",
"agent-harness-forgejo-deploy": "railiance-platform",
}
for eid, owner in expected.items():
e = catalog.get(eid)
assert e is not None and e.delegation is not None
assert e.delegation.mode == "interim"
assert e.delegation.intended_owner == owner
assert e.delegation.blocked_on
def test_missing_delegation_is_implicit_interim(tmp_path):
catalog = load_catalog(_write_catalog(tmp_path, [dict(ROUTED_ENTRY)]))
e = catalog.get("openbao-api-key")
d = e.effective_delegation
assert e.delegation is None
assert d.implicit is True
assert d.mode == "interim"
assert d.intended_owner is None
assert "unclassified" in (d.blocked_on or "")
def test_interim_without_blocked_on_rejected(tmp_path):
bad = dict(
ROUTED_ENTRY,
delegation={
"mode": "interim",
"intended_owner": "secrets-engine",
"reviewed": "2026-08-15",
},
)
with pytest.raises(CatalogError, match="blocked_on"):
load_catalog(_write_catalog(tmp_path, [bad]))
def test_non_permanent_without_owner_rejected(tmp_path):
bad = dict(
ROUTED_ENTRY,
delegation={"mode": "native", "reviewed": "2026-08-15"},
)
with pytest.raises(CatalogError, match="intended_owner"):
load_catalog(_write_catalog(tmp_path, [bad]))
def test_invalid_delegation_mode_rejected(tmp_path):
bad = dict(
ROUTED_ENTRY,
delegation={"mode": "maybe", "intended_owner": "x", "reviewed": "2026-08-15"},
)
with pytest.raises(CatalogError, match="delegation.mode"):
load_catalog(_write_catalog(tmp_path, [bad]))
def test_catalog_gaps_lists_only_interim():
catalog = load_catalog(_repo_catalog())
gap_ids = {e.id for e in catalog.gaps(include_draft=True)}
assert "ssh-cert-host-access" not in gap_ids
assert "whynot-design-npm-publish" not in gap_ids
assert "binky-company-email-imap" in gap_ids
# WARDEN-WP-0033: openbao-api-key was listed here as an interim cover. It is
# not one -- its path_template is a <domain>/<workload>/<bundle> routing
# pattern rather than a single secret lane, so there is no front door for
# anyone to take over. secrets-engine refused it on exactly that ground and
# ops-warden agrees. A pointer to OpenBao is not a gap ops-warden is holding,
# and counting it as one overstated the interim surface by a lane.
assert "openbao-api-key" not in gap_ids
assert all(catalog.get(i).is_interim for i in gap_ids)
def test_cli_route_gaps_json(repo_catalog_env):
result = runner.invoke(app, ["route", "gaps", "--json"])
assert result.exit_code == 0
data = json.loads(result.stdout)
assert data
ids = {row["id"] for row in data}
assert "binky-company-email-imap" in ids
assert "ssh-cert-host-access" not in ids
for row in data:
assert row["mode"] == "interim"
assert "intended_owner" in row
assert "blocked_on" in row
assert "days_since_review" in row
def test_cli_route_show_includes_delegation(repo_catalog_env):
result = runner.invoke(app, ["route", "show", "binky-qonto-api", "--json"])
assert result.exit_code == 0
data = json.loads(result.stdout)
assert data["delegation"]["mode"] == "interim"
assert data["delegation"]["intended_owner"] == "tenant-engine"
assert data["delegation"]["implicit"] is False
# --- ADR-0007: absence is not a grade (WARDEN-WP-0032-T06) ------------------
def _bare_entry(**overrides):
"""A minimal RouteEntry, so these tests exercise defaults and nothing else."""
from warden.routing.models import RouteEntry
fields = dict(
id="x",
title="t",
need_keywords=[],
owner_repo="r",
subsystem="s",
warden_executes=False,
wiki_ref="w",
canon_ref="c",
reviewed="2026-08-20",
status="active",
)
fields.update(overrides)
return RouteEntry(**fields)
def test_every_repo_catalog_lane_is_explicitly_graded():
"""The CI gate. A lane added without a `risk` grade is a defect (ADR-0007)."""
catalog = load_catalog(_repo_catalog())
ungraded = sorted(e.id for e in catalog.entries if not e.is_graded)
assert ungraded == [], (
f"{len(ungraded)} catalog lane(s) carry no explicit risk grade: {ungraded}. "
"ADR-0007: absence is not a grade — grade the lane on merit."
)
def test_ungraded_lane_fails_safe_to_high_risk():
"""RISK-F-0003 regression: an omitted grade must not wave a lane through.
Before ADR-0007 the dataclass default was "standard", so a lane that simply
omitted the field landed outside the agent read-boundary silently.
"""
entry = _bare_entry()
assert entry.risk == "ungraded"
assert entry.is_graded is False
assert entry.is_high_risk is True
def test_unrecognised_grade_is_treated_as_high():
"""A grade from a newer catalog must not be read as permission."""
entry = _bare_entry(risk="spicy")
assert entry.is_high_risk is True
assert entry.is_graded is False
def test_ungraded_risk_uses_maturity_derived_zone_default():
entry = _bare_entry()
assert entry.risk_for_zone(
effective_zone="z0-experimental",
admission="satisfied",
synthetic_only=True,
) == "standard"
assert entry.risk_for_zone(
effective_zone="z0-experimental",
admission="unknown",
synthetic_only=True,
) == "high"
assert entry.risk_for_zone(
effective_zone="z3-critical",
admission="satisfied",
) == "critical"
assert entry.risk_for_zone(effective_zone="unknown") == "high"
def test_explicit_risk_grade_always_wins_over_zone_default():
entry = _bare_entry(risk="standard")
assert entry.risk_for_zone(
effective_zone="z3-critical", admission="satisfied"
) == "standard"
def test_low_risk_vocabulary_is_explicit():
for grade in ("standard", "low", "accepted"):
entry = _bare_entry(risk=grade)
assert entry.is_high_risk is False, grade
assert entry.is_graded is True, grade
# ---------------------------------------------------------------------------
# Blocker staleness cadence + verification (WARDEN-WP-0033-T05)
# ---------------------------------------------------------------------------
def test_blocker_cadence_is_separate_from_pointer_cadence():
"""Two claims with different half-lives must not share one threshold.
"Is this still the right owner and page?" is quarterly. "Has the owner
answered yet?" is not. Sharing 90 days made the second one inert -- the
register was six days old, so it could not have fired for months.
"""
from warden.routing.catalog import DEFAULT_BLOCKER_STALE_DAYS, DEFAULT_STALE_DAYS
assert DEFAULT_STALE_DAYS == 90
assert DEFAULT_BLOCKER_STALE_DAYS == 14
assert DEFAULT_BLOCKER_STALE_DAYS < DEFAULT_STALE_DAYS
def test_blocker_window_scales_with_risk_and_matches_risk_nexus():
"""risk-nexus stall windows: 14d critical/high, 30d medium, 60d low.
They offered the convention instead of a joint tool, so the two registers
agree only for as long as these numbers do.
"""
from warden.routing.catalog import blocker_stale_days
assert blocker_stale_days("high") == 14
assert blocker_stale_days("standard") == 30
assert blocker_stale_days("low") == 60
# An ungraded lane gets the SHORTEST window, not the longest -- ADR-0007 makes
# an absent grade a defect, so its blocker is the least trustworthy of all.
assert blocker_stale_days("ungraded") == 14
assert blocker_stale_days(None) == 14
# An explicit --stale-days still wins.
assert blocker_stale_days("low", 7) == 7
def test_asked_and_waiting_is_not_verification():
"""The failure this whole change exists to catch.
A lane asked today reads as reviewed today. The secrets-engine blocker sat
in exactly that state for ten days while looking current.
"""
from warden.routing.models import Delegation
asked = Delegation(mode="interim", intended_owner="x", blocked_on="y",
reviewed="2026-08-21", verified="asked-and-waiting")
assert asked.is_verified is False
for method in ("owner-confirmed", "source-read"):
d = Delegation(mode="interim", intended_owner="x", blocked_on="y",
reviewed="2026-08-21", verified=method)
assert d.is_verified is True, method
def test_stale_gaps_flags_unverified_even_when_the_date_is_today():
catalog = load_catalog(_repo_catalog())
stale = {e.id for e in catalog.stale_gaps(include_draft=True, today=date(2026, 8, 21))}
# Asked of key-cape on 2026-08-21 and unanswered -- zero days old, still stale.
assert "key-cape-oidc-login" in stale
# Confirmed by the owner the same day -- fresh.
assert "issue-core-ingestion-api-key" not in stale
def test_invalid_verification_method_rejected(tmp_path):
entry = dict(ROUTED_ENTRY)
entry["delegation"] = {
"mode": "interim", "intended_owner": "secrets-engine",
"blocked_on": "pending", "reviewed": "2026-08-21", "verified": "probably-fine",
}
with pytest.raises(CatalogError, match="verified"):
load_catalog(_write_catalog(tmp_path, [SSH_ENTRY, entry]))
def test_every_interim_lane_records_how_it_was_verified():
"""Structural, not time-based, so it never fails on a calendar day alone."""
catalog = load_catalog(_repo_catalog())
missing = [
e.id for e in catalog.gaps(include_draft=True)
if e.effective_delegation.verified is None
]
assert not missing, f"interim lanes with no `verified`: {missing}"
def test_cli_route_gaps_fail_on_stale_exits_3(repo_catalog_env):
result = runner.invoke(app, ["route", "gaps", "--fail-on-stale", "--json"])
assert result.exit_code == 3
rows = json.loads(result.stdout)
assert any(r["stale"] for r in rows)
# An asked-and-waiting lane stays stale until it is verified, regardless of
# how many calendar days have elapsed since the request.
assert any(
r["stale"]
and r["verified"] == "asked-and-waiting"
for r in rows
)

View file

@ -101,7 +101,12 @@ def test_run_scorecard_clean(tmp_path):
)
results = run_scorecard(tmp_path, inv)
assert all(r.passed for r in results)
assert len(results) == 6
# cert-side checks + catalog_rotation_coverage (WP-0026 T06)
# + organization_posture + catalog_freshness (WP-0029)
assert len(results) == 9
names = {r.name for r in results}
assert "organization_posture" in names
assert "catalog_freshness" in names
# ---------------------------------------------------------------------------

66
tests/test_taint.py Normal file
View file

@ -0,0 +1,66 @@
"""Tests for EXPOSED taint convention (WARDEN-WP-0026 T05)."""
from __future__ import annotations
from typer.testing import CliRunner
from warden.cli import app
from warden.taint import (
EXPOSED_AT,
TaintStatus,
parse_custom_metadata,
kv_metadata_path,
)
runner = CliRunner()
def test_parse_custom_metadata_tainted():
status = parse_custom_metadata({
"custom_metadata": {
EXPOSED_AT: "2026-07-16T00:00:00Z",
"exposed_version": "2",
"exposed_reason": "test",
"exposed_ref": "history/x.md",
},
"current_version": 2,
})
assert status.tainted is True
assert status.exposed_at == "2026-07-16T00:00:00Z"
assert status.exposed_version == "2"
assert status.current_version == 2
def test_parse_custom_metadata_clean():
status = parse_custom_metadata({"custom_metadata": None, "current_version": 1})
assert status.tainted is False
assert status.exposed_at is None
def test_parse_empty_exposed_at_not_tainted():
status = parse_custom_metadata({"custom_metadata": {EXPOSED_AT: " "}, "current_version": 1})
assert status.tainted is False
def test_kv_metadata_path_strips():
assert kv_metadata_path(" platform/workloads/x ") == "platform/workloads/x"
def test_taint_status_to_dict():
s = TaintStatus(
lane_id="x", path="p", tainted=True,
exposed_at="t", exposed_version="1", current_version=1,
)
d = s.to_dict()
assert d["tainted"] is True
assert d["id"] == "x"
def test_taint_cli_unknown_id():
result = runner.invoke(app, ["taint", "no-such-lane-xyz"])
assert result.exit_code == 1
def test_taint_cli_template_lane_errors():
"""openbao-api-key has <placeholders> — cannot query taint."""
result = runner.invoke(app, ["taint", "openbao-api-key", "--json"])
assert result.exit_code == 2

View file

@ -0,0 +1,54 @@
"""Explicit lane-to-workload join tests (WARDEN-WP-0032 / RMGR-WP-0010-T06)."""
from pathlib import Path
import yaml
from scripts.report_workload_join import build
ROOT = Path(__file__).resolve().parents[1]
def test_repo_catalog_uses_only_explicit_workload_references():
report = build(ROOT / "registry/routing/catalog.yaml", Path.home())
assert report["ok"] is True
assert len(report["resolved"]) == 3
assert len(report["unknown"]) == 18
# 11 since WARDEN-WP-0033: the two NetKingdom SSO lanes (c374d41) are
# provider/control-plane credentials, not workload delivery lanes.
assert len(report["not_applicable"]) == 11
assert {row["lane"] for row in report["resolved"]} == {
"ops-warden-warden-sign-token",
"issue-core-ingestion-api-key",
"rapp-qonto-keycape-client",
}
def test_invalid_exact_deployable_resolves_unknown(tmp_path):
rapp = tmp_path / "rapp-x" / "declarations"
rapp.mkdir(parents=True)
(rapp / "rapp.yaml").write_text(yaml.safe_dump({
"rapp_id": "rapp-x",
"workload_identity": {"name": "x"},
"composition": {"member_repos": [{"deployables": ["api"]}]},
}))
catalog_dir = tmp_path / "ops-warden" / "registry" / "routing"
catalog_dir.mkdir(parents=True)
catalog = catalog_dir / "catalog.yaml"
catalog.write_text(yaml.safe_dump({"entries": [{
"id": "x",
"workload_ref": {
"applicability": "applicable",
"rapp_id": "rapp-x",
"name": "x",
"deployable": "missing",
},
}]}))
posture = catalog_dir.parent / "policy"
posture.mkdir()
(posture / "security-posture.yaml").write_text("dataclass_floor: {}\n")
report = build(catalog, tmp_path)
assert not report["resolved"]
assert report["unknown"][0]["lane"] == "x"
assert "deployable" in report["unknown"][0]["reason"]

2
uv.lock generated
View file

@ -131,7 +131,7 @@ wheels = [
[[package]]
name = "ops-warden"
version = "0.1.0"
version = "0.1.2"
source = { editable = "." }
dependencies = [
{ name = "httpx" },

View file

@ -76,6 +76,44 @@ boundary in `OperatorAccessAssist.md`.
---
## Interim custodianship
ops-warden **works with, and never replaces or duplicates**, the NetKingdom
components that own identity, custody, authorization, and tenant/user lifecycle
(`INTENT.md` §9). Covering a gap is legitimate. Silently becoming the owner of
that gap is not.
The only lane ops-warden executes with its own authority is **SSH issuance**
(`ssh-cert-host-access`). Every other execution position — including every
`warden access` proxy — is **interim by default**. A catalog entry without a
`delegation:` block is treated as interim with an unknown owner: absence is a
question, not a settlement.
| `delegation.mode` | Meaning |
| --- | --- |
| `permanent` | ops-warden is the designed owner of this front door (SSH only, today) |
| `native` | the intended owner already fronts it; ops-warden routes (and may keep a proxy as fallback) |
| `interim` | ops-warden covers the gap until `intended_owner` ships the missing front door named in `blocked_on` |
Delegation targets — the components that should own a front door once they have
one:
| Target | What they should front |
| --- | --- |
| **secrets-engine** | owner-native secret-exec (`secrets-engine exec --catalog <id>`) |
| **tenant-engine** | tenant/client secret custody and tenant-lane front door |
| **user-engine** | end-user / account-lifecycle secrets that belong with user identity |
| **railiance-platform** | OpenBao cluster, credential broker, platform workload procedure |
| **flex-auth** | authorization decisions (already native — ops-warden only points) |
| **key-cape** | identity login and client-credential protocol (OIDC, `client_secret_basic`) |
Query the register with `warden route gaps`. An interim lane is retired by
setting `exec_owner` / `exec_command` (the WP-0019 pattern) and flipping
`delegation.mode` to `native` once the owner's front door is proven. Do not
delete a working proxy on the way.
---
## Routing lookup CLI (`warden route`)
Agents and operators query the pointer catalog directly instead of re-deriving
@ -86,6 +124,7 @@ material.
```bash
warden route list [--json] [--all] [--tag <keyword>] # active-only unless --all
warden route list --stale [--stale-days 90] [--all] [--json] # past review cadence
warden route gaps [--json] [--all] # interim register (owner + blocker)
warden route show <id> [--json] # owner + pointers; SSH adds steps
warden route find "<free text need>" [--json] [--all] # rank by keyword overlap
```
@ -145,6 +184,70 @@ owner repo's shipped path.
| **On canon change** | When net-kingdom security docs change, review affected `canon_ref` entries immediately |
| **On owner ship** | When an owning repo merges a new OpenBao path or playbook, promote `draft``active` and bump `reviewed` |
| **On agent confusion** | If `warden route find` misses a common query, add `need_keywords` or a playbook — do not restate owner procedure in the catalog |
| **Fortnightly** (default 14 days) | Run `warden route gaps` — re-check each `blocked_on` against the intended owner; flip to `native` when their front door exists |
### Two cadences, because they are two different claims
A catalog pointer and an interim blocker both carry a `reviewed:` date, and for a
while they shared one 90-day threshold. They should not.
| Claim | Question | Default | Where |
| --- | --- | --- | --- |
| Pointer freshness | Is this still the right owner and page? | **90 days** | `warden route list --stale` |
| Interim blocker | Has the intended owner answered / can they front this yet? | **risk-scaled, 1460 days** | `warden route gaps` |
The blocker window scales with what the lane holds, matching `risk-nexus`'s stall
windows (`docs/method/check-procedure.md`) so the two registers agree without a
shared tool:
| Lane `risk` | Window | risk-nexus equivalent |
| --- | --- | --- |
| `high`, `ungraded` | **14 days** | critical / high |
| `standard` | **30 days** | medium |
| `low`, `accepted` | **60 days** | low |
`ungraded` takes the *shortest* window, not the longest. `ADR-0007` makes an
absent grade a defect and `ADR-0008` makes a grade cover the whole path — a lane
nobody has graded is the one whose blocker deserves least trust.
A pointer genuinely is a quarterly question. A blocker is not: it is a claim
about another repo's state at a date, and this estate invalidates those in days.
`RISK-F-0001` invalidated an ops-warden blocker in one. The `secrets-engine`
lanes carried one for ten while it was answerable from that repo's source the
whole time. A `FLEX-WP-0007` blocker was repeated by two repos for about seven
weeks after that workplan read `finished`.
Sharing 90 days did not make the blocker check loose, it made it **inert**: the
delegation register was created 2026-08-15, so the threshold could not have fired
before November and never had.
### Reviewed is not verified
The more important half. A `reviewed:` date records when someone last *touched*
the entry, which looks identical to re-checking it. So every interim lane also
carries `verified:`, saying how the claim was established:
| Value | Meaning | Counts as verification |
| --- | --- | --- |
| `owner-confirmed` | the intended owner stated the blocker's status | **yes** |
| `source-read` | re-derived from the owner's code, canon, or a CCR | **yes** |
| `asked-and-waiting` | a question is outstanding | **no** |
| `unverified` | carried forward without a check | **no** |
`asked-and-waiting` deliberately does not reset the clock. A lane asked today
reads as reviewed today, and that is precisely how a blocker stays fresh-looking
while nobody answers it. `warden route gaps` flags such a lane as stale at zero
days old.
**Re-check the blocker; do not bump the date.** Bumping `reviewed` without
re-establishing the claim is the failure this section exists to prevent, and it
is cheap to avoid — most of these are answerable by reading the owner's repo.
```bash
warden route gaps # risk-scaled cadence, plus unverified lanes
warden route gaps --stale-days 30 # flat override for every lane
warden route gaps --fail-on-stale # exit 3 — for a cron job or a gate
```
### Stale check (operators and agents)
@ -177,6 +280,7 @@ moved but anchors still resolve.
- `CredentialRouting.md` — worker decision tree and routing table
- `NetKingdomSecurityMap.md` — component literacy
- `INTENT.md` — steward mission ("issue SSH, route the rest")
- `INTENT.md` — steward mission ("issue SSH, route the rest"); §9 interim custodianship
- `workplans/WARDEN-WP-0030-delegation-register.md` — delegation register
- `workplans/WARDEN-WP-0010-access-routing-charter.md` — charter + no-double-source rule
- `net-kingdom/docs/platform-identity-security-architecture.md` — platform canon

View file

@ -32,6 +32,20 @@ fresh file starts.
high-entropy runs). Signing and proxy paths swallow audit failures so gatekeeping
never blocks the primary action — but tests prove values cannot be written.
> **Absence of a record is not evidence of absence.** Because emission never
> blocks the primary action (`src/warden/ca.py`), a failed append loses the event
> while the action still happens. This trail proves that the records it holds were
> not altered or truncated; it does **not** prove that every action produced one.
> Do not reason from a missing entry. This is the estate-wide bound in
> `security-layer-model_v0.4` §9.6 — completeness is the source's obligation, and
> **Ruled 2026-08-29** (`security-layer-model_v0.6` §9.6): this trail is
> **attributive**, not load-bearing — no control branches on the presence of a
> signing record — so the non-atomic trade is legitimate, and the obligations are
> to declare it (this note) and never to claim completeness. Atomicity is required
> only where a control's soundness depends on an event being present or absent.
> Registered in the standard's §13 open-gap table as self-declared. If a future
> control ever gates on this trail, the trade must be revisited before it ships.
---
## Query

View file

@ -75,8 +75,8 @@ ssh-keygen -s /path/to/ca -I agt-test -n agt-task -V +24h /tmp/key.pub && cat /t
```yaml
tunnels:
state-hub-coulombcore:
host: coulombcore
state-hub-railiance01:
host: railiance01
remote_port: 8001
local_port: 8000
ssh_user: agt-state-hub-bridge

Some files were not shown because too many files have changed in this diff Show more