Compare commits

..

3 commits

Author SHA1 Message Date
6e66a56653 fix(routing): state-hub lane uses Kubernetes auth, not an AppRole
The construction plan proposed an AppRole from a survey written with no
OpenBao session. The phase-4 live survey found kubernetes/ auth already
enabled on this cluster with four external-secrets-* roles using it, and
the founder ruled for it on 2026-08-27.

The pod authenticates with its own projected ServiceAccount token, so the
lane has no role_id/secret_id to deliver, store, or rotate. Bound to
state-hub/state-hub and deliberately not to default, which would grant
the lane to every pod in the namespace.

Policy and role are built and capability-verified. Entry stays draft: the
KV path holds no token until paste_once_provision delivers one, and the
ServiceAccount does not exist yet (STATE-WP-0084-T02).

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 3377672@bnt-lap001
Assistant-Session: 15463ccf-238f-4e13-b163-93aa25c6d166
2026-08-27 22:49:03 +02:00
6294e4b521 chore(registry): regenerate high-risk data paths
Picks up state-hub-forge-derivation-read, and two lanes added on
2026-08-23 that were never regenerated after
(net-kingdom-lldap-bind-credential, net-kingdom-privacyidea-admin-token).
21 -> 24.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 3377672@bnt-lap001
Assistant-Session: 15463ccf-238f-4e13-b163-93aa25c6d166
2026-08-26 21:56:55 +02:00
3a4333d850 feat(routing): propose state-hub-forge-derivation-read lane (draft)
warden route find "state hub read private repository" returned nothing,
which is why the need had to be reasoned out from first principles
instead of looked up.

Pointer-only (warden_executes: false): ops-mason builds the AppRole,
policy, and KV path under MASON-WP-0003; the forge owner mints the
value; paste_once_provision delivers it. ops-warden routes and executes
nothing.

Graded high on breadth, not on write authority — the token is read-only
but organisation-wide, so a leak reads every private repository.
Approved at that breadth 2026-08-26 (ops-mason plan §6).

status: draft until MASON-WP-0003-T02 has created the structure and the
negative capability check passes.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 3377672@bnt-lap001
Assistant-Session: 15463ccf-238f-4e13-b163-93aa25c6d166
2026-08-26 21:56:44 +02:00
157 changed files with 379 additions and 18353 deletions

View file

@ -16,8 +16,6 @@ distinction that matters here.
| `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

View file

@ -8,12 +8,6 @@ ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Eve
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.
Secrets practice follows SecurityCanon `SEC-DSM` 1.0.0
(`../security-canon/infospace/standards/devops-secrets-management/SecurityCanonDevOpsSecretsManagementStandard.md`).
Use its preference hierarchy (§5) and decision procedure (§37) before proposing
a new lane. SSH certificates are the ephemeral credential this repo issues.
Static secrets stay with their owner. This repo routes to that owner.
### Lookup (do this first)
**Always plan before drafting any founder credential step** (WARDEN-WP-0029):

34
.custodian-brief.md Normal file
View file

@ -0,0 +1,34 @@
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
# Custodian Brief — ops-warden
**Domain:** infotech
**Last synced:** 2026-08-25 18:17 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
### Native lane handoff — review secrets-engine's catalog admission, and fix what it exposed
Progress: 5/6 done | workplan_id: `4627d89b-4b00-562a-81e9-76e96f90fa7e`
**Open tasks:**
- ! Tasks `5acac140`
### 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:** 9 message(s) older than 3 day(s) — triage at session start.
**Missing thread_id:** 7 unread message(s) lack supersession chains.
- ! secrets-engine: Re: Need the warden-sign live apply/handoff (FLEX-WP-0007 T4) `80456912`
---
## MCP Orientation (when available)
If the state-hub MCP server is reachable, call:
`get_domain_summary("infotech")`
This provides richer cross-domain context.
If the MCP call fails, use this file as your orientation source.

View file

@ -1 +0,0 @@
{"version": 1, "repo_slug": "ops-warden"}

View file

@ -1,8 +0,0 @@
# Explicit opt-in recipe; no environment files or external make includes.
CUSTODIAN_SOURCE ?= /home/worsch/the-custodian
GENERATED_REPO_SLUG ?= ops-warden
.PHONY: generated-work-records generated-work-records-check
generated-work-records:
python3 "$(CUSTODIAN_SOURCE)/scripts/generate_staged_work_records.py" --repo . --repo-slug "$(GENERATED_REPO_SLUG)"
generated-work-records-check:
python3 "$(CUSTODIAN_SOURCE)/scripts/generate_staged_work_records.py" --repo . --repo-slug "$(GENERATED_REPO_SLUG)" --check

3
.gitignore vendored
View file

@ -176,6 +176,3 @@ cython_debug/
*.swp
.claude/ralph-loop.local.md
# Local State Hub orientation snapshot; authored work records remain tracked.
/.custodian-brief.md

File diff suppressed because it is too large Load diff

View file

@ -190,33 +190,6 @@ get wrong.
<!-- Append repo-specific agent instructions below this marker.
The state-hub template sync preserves content after this line. -->
## Secrets practice guide
From 2026-10-11, secrets practice follows SecurityCanon `SEC-DSM` 1.0.0:
`../security-canon/infospace/standards/devops-secrets-management/SecurityCanonDevOpsSecretsManagementStandard.md`
Gate House teaches that standard. This repo operates lanes inside it. SSH
certificates (`warden sign`) are the short-lived credential this repo issues.
Every other credential is routed, not stored here.
Before adding or changing a lane, apply §5 and §37:
- Prefer native or federated identity, then an ephemeral credential, then an
automatically rotated secret, then a vaulted static secret.
- Do not become the vault (`DSM-AP-002`, `DSM-AP-006`, `DSM-AP-015`). OpenBao
remains custody. This repo remains the conduit.
- Do not have a human carry a secret (`DSM-HUM-001`). Desk paste-once and
`warden access --out` / `--exec` / `--wrap` stay the sanctioned transports.
- Do not write secret values into audit, chat, workplans, or stdout
(`DSM-AUDIT-002`).
- A tool that can read a secret is agent authority (`DSM-AP-020`,
`DSM-AGENT-008`). High-risk lanes stay behind the agent read boundary.
A residual static secret that cannot be eliminated is still someone else's
custody. The catalog entry points at the owner and records rotation and
revocation. It does not hold the value.
## Experiential memory (WARDEN-WP-0024)
ops-warden shares a **phase-memory** store across worker ticks, coding agent
@ -248,19 +221,6 @@ Install bundled memory: `make install-all` then `make check-memory` from
## Workplan Convention (ADR-001)
### State Hub reads and caller identity
Use `GET /workplans/{id}` and `GET /tasks/?workplan_id=<id>` for reads.
Do not send the legacy `workstream_id` query parameter or call `/workstreams/`
routes (410 Gone). Keep `state_hub_workstream_id` in file frontmatter unchanged.
Send `X-StateHub-Component: ops-warden` on direct HTTP requests so operational
reviews can attribute callers. Example:
```bash
curl -fsS -H 'X-StateHub-Component: ops-warden' \
'http://127.0.0.1:8000/tasks/?workplan_id=<workplan-uuid>'
```
Work items originate as files in this repo — not in the hub. The hub is a
read/cache/index layer that rebuilds from files.
@ -318,25 +278,3 @@ To create a new workplan:
1. Write the file following the format above
2. Notify the custodian operator to run `make fix-consistency REPO=ops-warden`
(or send a message to the hub agent via `POST /messages/`)
<!-- BEGIN EMERGENT-SECURITY-PILOT -->
## NetKingdom situational knowledge pilot
For security-related work, read the shared
[Emergent Security orientation](../emergent-security/knowledge/cards/start.md)
and the task's card in that package. From this repository root, validate and read:
```bash
PYTHONPATH=../emergent-security/src python3 -m emergent_security.knowledge --repositories .. show start --audience generic
```
Use the matching provider audience when known and retrieve the relevant card.
Record package version, digest and source status; reload after resume/compaction
or a repository switch. A withheld card requires source review; an offline
snapshot is orientation only. If the sibling checkout is absent, use the trusted
installed `emergent-knowledge` package or an explicitly reviewed exported bundle;
do not silently download or execute a replacement.
This pilot adds situational knowledge, not authority, live readiness or a change
to this repository's owner contracts. Work: `emergent-security/EMERGENT-WP-0002`.
<!-- END EMERGENT-SECURITY-PILOT -->

View file

@ -11,25 +11,3 @@
@.claude/rules/credential-routing.md
@.claude/rules/finding-routing.md
@.claude/rules/agents.md
<!-- BEGIN EMERGENT-SECURITY-PILOT -->
## NetKingdom situational knowledge pilot
For security-related work, read the shared
[Emergent Security orientation](../emergent-security/knowledge/cards/start.md)
and the task's card in that package. From this repository root, validate and read:
```bash
PYTHONPATH=../emergent-security/src python3 -m emergent_security.knowledge --repositories .. show start --audience generic
```
Use the matching provider audience when known and retrieve the relevant card.
Record package version, digest and source status; reload after resume/compaction
or a repository switch. A withheld card requires source review; an offline
snapshot is orientation only. If the sibling checkout is absent, use the trusted
installed `emergent-knowledge` package or an explicitly reviewed exported bundle;
do not silently download or execute a replacement.
This pilot adds situational knowledge, not authority, live readiness or a change
to this repository's owner contracts. Work: `emergent-security/EMERGENT-WP-0002`.
<!-- END EMERGENT-SECURITY-PILOT -->

148
INTENT.md
View file

@ -1,77 +1,5 @@
---
# THE layer declaration (§11, GH-DEC-2026-017 §1). `layer.yaml` is derived from
# this frontmatter and must agree with it; it does not govern. Comparison of the
# §3 vocabulary is ASCII case-insensitive, so `Staff` here and `staff` there are
# the same value and neither is re-spelled (GH-DEC-2026-017 §2).
layer: Staff
role: null # Engines only: PDP | PIP | Evidence | Lifecycle
pep_shaped: true # §6.4 — issuing a certificate is a protected side effect
# No `standard_version` here or in the sidecar: GH-DEC-2026-017 §5 / A12 —
# the declared layer is a standing property that does not change when the
# standard is revised, so a version in the declaration reads as a validity
# condition it is not. Version-scoped state belongs in the derived conformance
# record. The version ops-warden assented at is recorded with the assent
# (ADR-0010), which is where a version-at-a-boundary belongs.
standard: net-kingdom/canon/standards/security-layer-model
companion: net-kingdom/SECURITY-COMPANION.md
declaration: layer.yaml # derived form; this frontmatter governs
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.8` §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
@ -130,15 +58,14 @@ owns one lane and points at the rest:
(secrets-engine `exec`, railiance-platform `credential exec`) ops-warden routes to
the owner's front door — it does not mint tokens or run the owner's tool itself.
This is the assist layer, not a universal broker: custody stays in OpenBao /
secrets-engine / the platform broker; authorization in access-engine
(repository `flex-auth`).
secrets-engine / the platform broker; authorization in flex-auth.
3. **Steward workload security posture conformance.** Author the ops-security slice
for environment posture (`dev/test/prod`) and workload maturity (`M0-M3`), then
ship descriptors and read-only checks that identify whether a secret-flow blocker
is real, owner-routed, or removable with a contract double. Runtime enforcement
remains access-engine; custody remains OpenBao.
remains flex-auth; custody remains OpenBao.
4. **Align** runbooks, wiki, inventory patterns, and scorecard checks with
NetKingdom canon as the platform evolves (OpenBao-first, access-engine policy,
NetKingdom canon as the platform evolves (OpenBao-first, flex-auth policy,
key-cape IAM Profile, railiance deployment layers).
5. **Issue** short-lived SSH certificates for `adm` / `agt` / `atm` actors when
host or ops reachability requires the SSH lane — via `warden sign`,
@ -150,52 +77,30 @@ owns one lane and points at the rest:
---
## Lane routing — who owns which need
## NetKingdom Security Literacy
**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.
ops-warden should be fluent in the platform architecture documented in
`net-kingdom` — especially:
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.
| 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 |
| **tenant-engine** | Tenant/client secret custody and front door | Route tenant lanes once fronted; ops-warden's current tenant proxies are interim (§9) |
| **user-engine** | End-user identity and account lifecycle | No ops-warden lane today; route rather than absorb |
| **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 |
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)
---
@ -218,9 +123,8 @@ Canonical references:
| Need | Route to |
| --- | --- |
| OIDC login, MFA, human identity claims | key-cape / Keycloak (NetKingdom IAM Profile) |
| 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 (+ access-engine policy where required) |
| Policy decision — may actor X access resource Y | 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 |
| `/etc/ssh/auth_principals/`, host hardening | railiance-infra |
@ -343,7 +247,7 @@ ops-warden (issue SSH; route / assist the rest)
+-- Generic API / DB / provider secret? -> OpenBao path
| (warden access proxies as caller when exec_capable)
|
+-- Authorization required? ------------> access-engine decision
+-- Authorization required? ------------> flex-auth decision
| (caller-side gate on sign + access when policy.enabled)
|
+-- Identity / MFA required? -------------> key-cape / Keycloak path
@ -372,7 +276,7 @@ for one security lane plus operational guidance.
ops-warden should appear in NetKingdom responsibility and pattern material as
the **operational SSH credential authority**, not as a replacement for
OpenBao or access-engine.
OpenBao or flex-auth.
---
@ -399,8 +303,8 @@ ops-warden is succeeding when:
## Non-goals
- Universal credential broker for all secret types
- Runtime enforcement of the workload secret-flow lattice (access-engine owns that)
- Replacing OpenBao, access-engine, key-cape, or railiance deployment ownership
- Runtime enforcement of the workload secret-flow lattice (flex-auth owns that)
- Replacing OpenBao, flex-auth, key-cape, or railiance deployment ownership
- Storing Inter-Hub, LLM provider, or other long-lived API keys
- Host-side SSH configuration deployment
- **Duplicating or restating another subsystem's procedure** — routing material
@ -417,7 +321,7 @@ ops-warden is succeeding when:
The repository shipped the SSH CA CLI first (WARDEN-WP-0001–0003). The
stewardship and NetKingdom-alignment mission is the **next stratum** — docs,
routing canon, inventory standards, production OpenBao SSH engine alignment,
access-engine integration design, and NetKingdom cross-links — without collapsing
flex-auth integration design, and NetKingdom cross-links — without collapsing
platform boundaries.
See `wiki/CredentialRouting.md` for worker-facing routing,

View file

@ -1,6 +1,5 @@
# ops-warden development and install targets
ROOT := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
UNIT_PYTHON ?= $(ROOT)/.venv/bin/python
PHASE_MEMORY_REPO ?= $(abspath $(ROOT)/../phase-memory)
.DEFAULT_GOAL := help
@ -25,12 +24,12 @@ sync:
uv sync
install-warden:
uv tool install . --force --reinstall-package ops-warden
uv tool install . --force
install-memory:
@test -d "$(PHASE_MEMORY_REPO)" || (echo "ERROR: phase-memory not found at $(PHASE_MEMORY_REPO). Clone it or set PHASE_MEMORY_REPO."; exit 1)
@test -f "$(PHASE_MEMORY_REPO)/pyproject.toml" || (echo "ERROR: $(PHASE_MEMORY_REPO) is not a phase-memory checkout."; exit 1)
uv tool install . --with-editable "$(PHASE_MEMORY_REPO)" --force --reinstall-package ops-warden
uv tool install . --with-editable "$(PHASE_MEMORY_REPO)" --force
install: install-warden
@ -44,14 +43,7 @@ verify-memory: check-memory
@warden memory status
test:
"$(UNIT_PYTHON)" -I scripts/run_unit_tests.py --phase-memory-root "$(PHASE_MEMORY_REPO)" $(TEST_ARGS)
PYTHONPATH=src:$(PHASE_MEMORY_REPO)/src uv run pytest
lint:
uv run ruff check .
# CUST-WP-0079: explicit staged work-record generation, using the owner renderer.
.PHONY: generated-work-records generated-work-records-check
generated-work-records: ## Generate and stage WORK-RECORDS from staged sources
$(MAKE) --no-print-directory -f .generated-work-records.mk generated-work-records
generated-work-records-check: ## Verify staged index without writes or publication
$(MAKE) --no-print-directory -f .generated-work-records.mk generated-work-records-check
uv run ruff check .

View file

@ -64,13 +64,6 @@ uv sync
uv tool install . --with-editable ../phase-memory --force
```
For a working checkout, an editable install keeps the `warden` CLI in step
with `git pull`. This avoids stale-wheel builds:
```bash
uv tool install -e . --with-editable ../phase-memory --force
```
Or run without installing:
```bash

View file

@ -21,9 +21,8 @@ access guidance aligned with NetKingdom canon.
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
Railiance OpenBao (`warden sign`, host CA trust deployed). Since public
`bao.coulomb.social` was retired, production signing reaches OpenBao through
the ops-bridge tunnel (`http://127.0.0.1:18200`).
Railiance OpenBao (`warden sign` against `https://bao.coulomb.social`, host CA trust
deployed).
**Access routing** is shipped: `wiki/AccessRouting.md`, credential routing wiki,
NetKingdom security map, machine-readable pointer catalog
@ -86,28 +85,6 @@ 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; **derived** from `INTENT.md`, which governs (`GH-DEC-2026-017`) | 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
ops-warden executes exactly one lane with its own authority and routes/assists the rest.
@ -179,7 +156,6 @@ superseding ADR, never an in-place edit.
| `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
@ -415,11 +391,6 @@ Remaining production distance is also in other repos' lanes (see Known gaps).
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/`)
- **Per-actor issuance limits:** WARDEN-WP-0045. The issuance contract carries a
per-actor extension allow-list and a `max_ttl_hours` ceiling. The new profile
`agt-custodian` (12h, `permit-pty` only) has the attended lane
`agt-custodian-ssh-cert`. Its first issuance was done by the founder on
2026-10-20 for the supervised interactive agents (CUST-WP-0073).
- **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
@ -462,12 +433,11 @@ 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 — ruled name `access-engine`; the only policy decision point. Policy package shipped (FLEX-WP-0006); runtime deploy FLEX-WP-0007 |
| `flex-auth` | Authorization; 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) 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) |
@ -484,9 +454,7 @@ type: security
title: SSH certificate issuance
description: Issues short-lived CA-signed SSH certificates for adm/agt/atm actors via a
pluggable cert_command interface; documents NetKingdom operational access routing;
supports local CA and OpenBao/Vault-compatible SSH engine backends. Per-actor
extension allow-lists and max_ttl_hours ceilings bound each profile (e.g.
agt-custodian: 12h, permit-pty only).
supports local CA and OpenBao/Vault-compatible SSH engine backends.
keywords: [ssh, certificate, ca, credential, warden, ops-warden, pki, openbao, vault, netkingdom]
```
@ -518,9 +486,6 @@ keywords: [access, credential, secret, npm, token, api-key, openbao, key-cape, l
| `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` | Derived layer declaration: every Tooling contact and its §5 shape (`INTENT.md` frontmatter governs) |
| `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 |
@ -542,24 +507,3 @@ keywords: [access, credential, secret, npm, token, api-key, openbao, key-cape, l
| `history/2026-06-18-access-routing-intent-shift-assessment.md` | Routing charter decision |
| `history/2026-06-23-flex-auth-policy-gate-production-smoke.md` | Policy gate smoke evidence |
| `net-kingdom/docs/platform-identity-security-architecture.md` | Platform security canon |
## Staged work-record generation — 2026-10-19
The repository explicitly opts into the shared staged work-record generator via
`.generated-work-records.json` and its dedicated `.generated-work-records.mk`.
`make generated-work-records` uses the existing State Hub renderer through the
Custodian adapter, stages only `WORK-RECORDS.md`, and refuses unreconciled
unstaged index edits. `make generated-work-records-check` checks without writes.
The shared commit hook retains its generated-only guard and repository-specific
scanner routing; meaningful source commits regenerate the index before any
configured scan.
Generation performs no Hub calls, commits, pushes, credential access or live
changes. Source inventories and other generated artifacts retain their owners.
## Local orientation brief policy — 2026-10-19
`.custodian-brief.md` is a local, ignored State Hub orientation snapshot. Its
existing bytes remain available for offline session startup; refreshing it does
not require a Git commit. Authored workplans and the generated WORK-RECORDS index
retain their existing source and staged-generation contracts. No runtime or
credential change follows from this repository hygiene policy.

View file

@ -8,12 +8,10 @@
| 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-ADHOC-2026-09-08 | finished | — | workplans/ADHOC-2026-09-08.md |
| workplan | WARDEN-WP-ADHOC-2026-10-03 | finished | — | workplans/ADHOC-2026-10-03.md |
| workplan | ADHOC-2026-06-27 | finished | — | workplans/ADHOC-2026-06-27.md |
| workplan | ADHOC-2026-06-29 | finished | — | workplans/ADHOC-2026-06-29.md |
| workplan | ADHOC-2026-08-11 | finished | — | workplans/ADHOC-2026-08-11.md |
| workplan | 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 |
@ -25,41 +23,22 @@
| 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 | blocked | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.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 | active | — | 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 |
| workplan | WARDEN-WP-0037 | blocked | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md |
| workplan | WARDEN-WP-0038 | finished | — | workplans/WARDEN-WP-0038-plan-mutation-intent.md |
| workplan | WARDEN-WP-0039 | finished | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md |
| workplan | WARDEN-WP-0040 | active | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md |
| workplan | WARDEN-WP-0041 | finished | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md |
| workplan | WARDEN-WP-0042 | finished | — | workplans/WARDEN-WP-0042-openbao-structure-survey-route.md |
| workplan | WARDEN-WP-0043 | finished | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md |
| workplan | WARDEN-WP-0044 | active | — | workplans/WARDEN-WP-0044-security-case-follow-through.md |
| workplan | WARDEN-WP-0045 | finished | — | workplans/WARDEN-WP-0045-agt-custodian-issuance-profile.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-ADHOC-2026-09-08-T01 | done | — | workplans/ADHOC-2026-09-08.md |
| task | WARDEN-WP-ADHOC-2026-09-08-T02 | done | — | workplans/ADHOC-2026-09-08.md |
| task | WARDEN-WP-ADHOC-2026-09-08-T03 | done | — | workplans/ADHOC-2026-09-08.md |
| task | WARDEN-WP-ADHOC-2026-09-08-T04 | done | — | workplans/ADHOC-2026-09-08.md |
| task | WARDEN-WP-ADHOC-2026-09-08-T05 | done | — | workplans/ADHOC-2026-09-08.md |
| task | WARDEN-WP-ADHOC-2026-09-08-T06 | done | — | workplans/ADHOC-2026-09-08.md |
| task | WARDEN-WP-ADHOC-2026-10-03-T01 | done | — | workplans/ADHOC-2026-10-03.md |
| workplan | WARDEN-WP-0033 | active | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
| task | ADHOC-2026-06-27-T01 | done | — | workplans/ADHOC-2026-06-27.md |
| task | ADHOC-2026-06-29-T01 | done | — | workplans/ADHOC-2026-06-29.md |
| task | ADHOC-2026-08-11-T01 | done | — | workplans/ADHOC-2026-08-11.md |
| task | ADHOC-2026-08-11-T02 | done | — | workplans/ADHOC-2026-08-11.md |
| task | ADHOC-2026-08-11-T03 | done | — | workplans/ADHOC-2026-08-11.md |
| task | ADHOC-2026-08-17-T01 | done | — | workplans/ADHOC-2026-08-17.md |
| task | ADHOC-2026-08-17-T02 | done | — | workplans/ADHOC-2026-08-17.md |
| task | ADHOC-2026-08-17-T03 | done | — | workplans/ADHOC-2026-08-17.md |
| task | 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 |
@ -113,7 +92,7 @@
| 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 | todo | — | 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 |
@ -147,46 +126,6 @@
| 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-T04 | wait | — | 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 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md |
| task | WARDEN-WP-0034-T02 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md |
| task | WARDEN-WP-0034-T03 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md |
| task | WARDEN-WP-0034-T04 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md |
| task | WARDEN-WP-0034-T05 | wait | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md |
| task | WARDEN-WP-0034-T06 | done | — | 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 |
| task | WARDEN-WP-0037-T01 | done | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md |
| task | WARDEN-WP-0037-T02 | done | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md |
| task | WARDEN-WP-0037-T03 | wait | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md |
| task | WARDEN-WP-0038-T01 | done | — | workplans/WARDEN-WP-0038-plan-mutation-intent.md |
| task | WARDEN-WP-0039-T01 | done | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md |
| task | WARDEN-WP-0039-T02 | done | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md |
| task | WARDEN-WP-0039-T03 | done | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md |
| task | WARDEN-WP-0040-T01 | wait | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md |
| task | WARDEN-WP-0040-T02 | wait | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md |
| task | WARDEN-WP-0040-T03 | wait | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md |
| task | WARDEN-WP-0040-T04 | done | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md |
| task | WARDEN-WP-0041-T01 | done | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md |
| task | WARDEN-WP-0041-T02 | done | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md |
| task | WARDEN-WP-0041-T03 | done | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md |
| task | WARDEN-WP-0041-T04 | done | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md |
| task | WARDEN-WP-0042-T01 | done | — | workplans/WARDEN-WP-0042-openbao-structure-survey-route.md |
| task | WARDEN-WP-0042-T02 | done | — | workplans/WARDEN-WP-0042-openbao-structure-survey-route.md |
| task | WARDEN-WP-0043-T01 | done | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md |
| task | WARDEN-WP-0043-T02 | done | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md |
| task | WARDEN-WP-0043-T03 | done | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md |
| task | WARDEN-WP-0044-T01 | done | — | workplans/WARDEN-WP-0044-security-case-follow-through.md |
| task | WARDEN-WP-0044-T02 | done | — | workplans/WARDEN-WP-0044-security-case-follow-through.md |
| task | WARDEN-WP-0044-T03 | progress | — | workplans/WARDEN-WP-0044-security-case-follow-through.md |
| task | WARDEN-WP-0044-T04 | wait | — | workplans/WARDEN-WP-0044-security-case-follow-through.md |
| task | WARDEN-WP-0045-T01 | done | — | workplans/WARDEN-WP-0045-agt-custodian-issuance-profile.md |
| task | WARDEN-WP-0045-T02 | done | — | workplans/WARDEN-WP-0045-agt-custodian-issuance-profile.md |
| task | WARDEN-WP-0045-T03 | done | — | workplans/WARDEN-WP-0045-agt-custodian-issuance-profile.md |
| intake | WARDEN-IN-0001 | closed | — | intakes/intakes.md |
| intake | WARDEN-IN-0002 | closed | — | intakes/intakes.md |
| intake | WARDEN-IN-0003 | closed | — | intakes/intakes.md |

View file

@ -5,15 +5,15 @@ title: "ADR-0004 — High-risk lanes refuse raw value streaming to agent session
domain: infotech
repo: ops-warden
status: accepted
version: "1.1"
revision: "2"
version: "1.0"
revision: "1"
owner: ops-warden
binds: "ops-warden; any agent runtime calling warden access"
created: "2026-07-20"
updated: "2026-09-04"
last_reviewed: "2026-09-04"
updated: "2026-08-18"
last_reviewed: "2026-08-18"
review_interval: 6m
enforced_by: "src/warden/access.py + src/warden/cli.py (exit 7); OpenBao policy agent-high-risk-boundary"
enforced_by: "src/warden/access.py (exit 7); OpenBao policy agent-high-risk-boundary"
supersedes: ""
successor: ""
---
@ -48,13 +48,6 @@ refuses to stream the raw value and exits 7.** The agent is not blocked from doi
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*.
**Revision 2 also recognizes the issued coding-agent subject.** When
`WARDEN_POLICY_SUBJECT` is the KeyCape-issued
`service:codex:railiance-platform` identity, the same refusal applies without
`WARDEN_AGENT_ID`. The legacy variable remains a fail-toward-safety fallback.
Ops-warden reads this identity marker; it does not validate the token or decide
whether the principal is authorized.
**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
@ -80,12 +73,10 @@ 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 CLI recognizes the
issued subject when the runtime presents it and otherwise relies on
`WARDEN_AGENT_ID`; both markers remain caller-visible and therefore advisory. This
ADR raises the floor for cooperating runtimes and hardens the store behind them; it
does not claim to stop a determined caller. The OpenBao
`agent-high-risk-boundary` policy is the enforced control.
**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

View file

@ -1,109 +0,0 @@
---
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`

View file

@ -71,4 +71,3 @@ and the publication is a defect.
| `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

@ -1,64 +0,0 @@
# Safe unit-test entry point
Use `make test`, or `.venv/bin/python -I scripts/run_unit_tests.py`. Dependencies
must already be installed; this path does not run uv, synchronize or download.
`UNIT_PYTHON` may select another existing interpreter. `PHASE_MEMORY_REPO` remains
an explicit local source option. Pass pytest options with `TEST_ARGS`, for example
`make test TEST_ARGS='-q tests/test_safe_unit_runner.py'`.
The launcher creates an empty temporary HOME and temporary fixture root and starts
an isolated (`-I`) Python interpreter with a constructed environment. Caller token,
proxy, Python, pytest, XDG and Warden configuration variables are never copied or
inventoried. Third-party pytest plugin autoload is disabled. Tests can explicitly
introduce synthetic variables with monkeypatch, but allowed children always get
the original temporary HOME and a fresh environment, never those variables.
Before pytest import/collection, a Python audit hook and subprocess wrapper:
- deny file opens outside the selected source roots, installed interpreter/library
roots and fixture root; writes through open are limited to the fixture root;
- deny sockets, DNS and process creation except the closed fixture inventory;
- refuse shell, executable, preexec and inherited-FD overrides outside that inventory;
- install the same file/socket/process denial before executing every allowed
Python child, including BrowserInputs' exact validator code and reverified copied
module hashes. Validator children cannot start further processes.
The process inventory in `scripts/run_unit_tests.py` is intentionally small:
BrowserInputs validation, four metadata-checker scripts with fixed argument shapes,
the exact original-intent restart probe, the synthetic containment print, and
closed guard-regression probes. Metadata checkers receive only fixture output
paths. Only the generated-artifact checker child can run the exact read-only Git
log/status queries for this repository's catalog, with global/system config and
optional locks disabled, hooks disabled and fsmonitor disabled. Those local Git
commands are not provider operations.
Two reviewed non-Python cases remain: exact-byte, temporary contract-double scripts
with a closed argv list, and five exact fixed `printf` commands used by existing
transport tests. They run Bash without profile/rc startup and without inherited
environment; no arbitrary shell text, provider binary, SSH, kubectl or OpenBao
command is permitted. Default unit selection excludes four integration-marked
SSH-keygen tests. Selecting them explicitly does not grant their native commands.
Only `test_desk.py::test_approve_flow_http_dry` receives a network lease. It may
bind `127.0.0.1:0` and connect to the endpoint actually allocated to that test's
socket. DNS is replaced by numeric fixture resolution. Another loopback service
(e.g. the real Hub on port 8000), Unix sockets and remote endpoints stay denied.
The lease and registered endpoints are cleared after the test.
Coverage tests now use the explicit public declaration projection snapshot in
`tests/fixtures/workload-coverage`, with provenance hashes. They compare the
published source snapshot rather than silently consulting the workstation HOME.
Run the existing coverage report separately with an explicit estate root when
measuring current estate state. Tests of the acknowledged NetKingdom source use
`--netkingdom-root` (default: sibling source checkout), never a HOME symlink. The
optional Phase Memory library's one existing public coordination-profile fixture
is explicitly readable alongside its source; no runtime memory store is inherited.
This is an accident guard for trusted unit tests, not a security sandbox for
hostile Python, native extensions or hostile installed dependencies. It does not
revoke already-open descriptors, prevent every filesystem mutation syscall, or
provide OS process/network isolation. Fixed Bash/Git fixture processes rely on
the reviewed executable and source/argument shapes; Python audit hooks do not run
inside those binaries. Do not run untrusted test code under a privileged runtime.
Direct `pytest` bypasses this launcher and is not equivalent validation. No
production Warden boundary, grant, catalog, source pin or owner credential changes.

View file

@ -1,65 +0,0 @@
# ops-warden — documentary feedback on the Emergent read-only pilot
Date: 2026-10-03. Reviewer: Codex repository worker under the user's
EMERGENT-WP-0002 delegation. Owner checkout: `c9ebe0122a969e9a9188154d152133d8dc50cfcc`.
This is scoped documentary feedback, not institution/human assent, independent
assurance, current-release provider certification or authorization for effects.
## Evidence and version
Retained report: `emergent-security/docs/evidence/2026-10-03-closure-pilot-ops-warden.json`.
Actual-checkout read-only Claude scenario, Claude Code 2.1.288 and
claude-opus-5-5, knowledge 0.2.0, digest
`33bd6ea3b7bf3ea28144ffb20a62b4fcf4b2b7cb7aeddc1da0585fa6853b6162`,
source status verified-files, implementing Codex semantic review. One answered
scenario returned the shared `access` card and refused unsafe authority shortcuts.
The worker agrees with the bounded results subject to the qualifications below.
No new provider session was run; old results retain their original version.
The coordinator's corrected release is 0.2.2, digest
`19de26540c693b94ac8c19d7ed2ddd901acbc828c9b8a78b44c1a43ca468222c`.
Current read-only card retrieval reported verified-files; source-file agreement
is not live-state verification. The coordinator reports unchanged card wording,
retrieval integrity/lifecycle fixes and refreshed Glas source. Those changes
need affected tests and independent close-out before adoption.
## Findings and next scoped scenarios
Reviewed the retained access answer against INTENT/SCOPE, AGENTS' plan-first
rule, WARDEN-WP-0029, WARDEN-WP-0039 and the current access card. Correct: catalog
discovery does not grant authority, a readable broader token cannot be selected,
SSH is Warden's issuance lane, other lifecycle belongs to the named owner, and
`warden plan` precedes drafting any founder credential act. The answer knows the
plan's autonomous/founder_required/unroutable branches and sanctioned transports.
Qualification: its generalized informed-decision/approval-engine handoff must
not be read as proof that every typed `warden desk` login/provision act already
has that integration. Use the actual owner surface and prerequisite contract.
`warden plan`'s autonomous verdict is not a generic offline AUTONOMOUS grant.
Explicit 401/403 refusals stop the effect; they are not outage/fail-open cases.
WARDEN-WP-0039-T03 still needs founder go and exact route evidence; the pilot
does not admit the new fetch pin or close that work.
Next bounded current-release cases: catalogue discovery then required plan,
missing/stale lane, founder_required typed act without drafting a file drop,
wrong-owner policy refusal, withdrawn source procedure refresh, and missing
source events. Answers must name the route/source owner and preserve unknown
scope; no credential read, CA issue, login or live policy probe is part of this
review. Retain WARDEN-WP-0039/WARDEN-WP-0040 as owners of their operational gates.
## Continuity and wider use
Before counting a replacement-release pilot, record exact package digest/commit,
provider/client/model metadata, the card/source receipt, task/delegator and scope,
scenario corpus, all returned answers, review provenance and failures. Obtain
actual resume/compaction and repository/provider-transition evidence separately;
a fresh session or reload unit test does not prove either. Independent whitehat
review and Glas/Custodian readiness remain distinct gates. Broader bounded work
requires a named owner and exact target/delegation, never the knowledge receipt
as its grant. The existing tested pointer rollback applies; no instruction file
was rewritten by this review.
Work record: WARDEN-WP-ADHOC-2026-10-03-T01. Documentary feedback is complete.
Current-release pilot and distribution acceptance remain EMERGENT-WP-0002-T08/T09
and the existing open adoption decision d20a9750-2b29-534e-8227-fb866581a6ca;
no foreign task list or duplicate blocked plan was created here.

View file

@ -1,44 +0,0 @@
{
"status": "passed",
"scope": "offline draft cross-owner CLI fixture, no activation or live evidence",
"lane_id": "example-workload-kv",
"ccr_id": "CCR-2099-9001",
"warden_stage": "/tmp/warden-cross-owner-catalog",
"platform_stage": "/tmp/warden-cross-owner-platform",
"fixture_sha256": "38647d436ccfa8893974c4b88c93d3bd94a9c0a21a2603b54bd84c0195954e32",
"catalog_request_parity": true,
"draft_only": true,
"unknown_workload_preserved": true,
"platform_auth_claims_unconfirmed": true,
"evidence_not_executed": true,
"tests": {
"warden_generator_routing_plan": "139 passed",
"platform_scaffold": "14 passed (platform-owner independently reported)",
"platform_window": "7 passed (platform-owner independently reported)"
},
"independent_platform_acceptance": {
"status": "passed",
"scope": "actual offline owner CLIs + independently verified deterministic cross-owner fixture; no admission or runtime act",
"platform_stage": "/tmp/p4-cross-platform-tj7_84lv/platform",
"warden_stage": "/tmp/warden-cross-owner-catalog",
"fixture_sha256": "38647d436ccfa8893974c4b88c93d3bd94a9c0a21a2603b54bd84c0195954e32",
"artifact_sha256": {
"README.md": "2f48aa6ef925f62722ca32d38283d746c0c386da751dec3f9a76fba125d24245",
"credential-change-requests/CCR-2099-9001.yaml": "e9db54694b966f186433aaf1f3de6685d8f34be166af9d966950bcca448f151f",
"docs/evidence/example-workload-kv.checklist.json": "7d9a58b160c958d8d46f7029398eef8e70f87f0d9423198fb789bdf8fe3a9f9a",
"manifests/example-workload-kv.draft.yaml": "2d23bee7451735e2a0aa74435c65a0968bd05e35c35625a4087e2f286a2408b5",
"openbao/auth/example-consumer-eso.draft.json": "545a03c95c50b68b0eb127ae5e72d904faf9f68ecfabe26deac27730905b476d",
"openbao/policies/workload-kv-read-example-consumer.hcl": "c2821cd6fe5bb16564366fb12fd37c559048841a41c6f2778ad2dad5901e4df4"
},
"new_scaffold_tests": 14,
"prior_window_tests": 7,
"unknown_workload_preserved": true,
"prospective_auth_claims_unconfirmed": true,
"frontdoor_pending_unresolvable": true,
"evidence_not_executed": true
},
"owner_source_commits": {
"ops-warden": "dfe436e4e05610cae96085dadd582e5a63525580",
"railiance-platform": "f5b7ff5118326f66be8ecd8330d4cda9e4d94520"
}
}

View file

@ -1,93 +0,0 @@
{
"owner": "ops-warden",
"accepted_standard_sha256": "3d8298560069ef471fba28d2b1245fb2309328f231dad1a667cdf0ce5a5a7c8b",
"accepted_source": "net-kingdom/canon/standards/security-layer-model_v0.8.md",
"acceptance_receipt": "net-kingdom/docs/security-layer-v0.8-acceptance.md",
"tests": {
"layer_conformance_passed": 31,
"source": "staged modified scripts/tests + read-only current source support",
"limitations": "mechanical declared scope only, not complete estate/runtime conformance",
"routing": {
"full_suite_passed": 99,
"full_suite_failed": 1,
"failure": "staging fixture missing unchanged wiki links; not product regression",
"focused_after_fixture_repair_passed": 2,
"focused_cases": [
"every_wiki_ref_anchor_resolves",
"route_find_layer_declaration_resolves_to_reference_playbook"
],
"environment": "WARDEN_MEMORY=0 and explicit staged WARDEN_ROUTING_CATALOG; no installed catalog/memory mutation"
}
},
"checker_runs": [
{
"argv": [],
"returncode": 0,
"stdout": "validated against: net-kingdom/canon/standards/security-layer-model_v0.8.md (accepted; sha256:3d8298560069ef471fba28d2b1245fb2309328f231dad1a667cdf0ce5a5a7c8b)\nscope: single repository ops-warden (catalogued in \u00a74); INTENT.md frontmatter, layer.yaml, src/warden/**/*.py\nPASS \u2014 4 module(s) with Tooling contact, all declared; validated against net-kingdom/canon/standards/security-layer-model_v0.8.md (accepted; sha256:3d8298560069ef471fba28d2b1245fb2309328f231dad1a667cdf0ce5a5a7c8b)\n",
"stderr": ""
},
{
"argv": [
"--report"
],
"returncode": 0,
"stdout": "validated against: net-kingdom/canon/standards/security-layer-model_v0.8.md (accepted; sha256:3d8298560069ef471fba28d2b1245fb2309328f231dad1a667cdf0ce5a5a7c8b)\nscope: single repository ops-warden (catalogued in \u00a74); INTENT.md frontmatter, layer.yaml, src/warden/**/*.py\nops-warden \u2014 layer: Staff (declared in INTENT.md; \u00a711 governing form)\n layer.yaml: derived from INTENT.md, layer: staff\ndeclared by docs/adr/ADR-0010\n\n \u00a75.3 ssh-ca-signing-write src/warden/vault.py -> secrets-engine review 2026-11-28\n \u00a75.3 desk-paste-once-provision src/warden/desk.py -> secrets-engine review 2026-11-28\n \u00a75.1 taint-metadata-read src/warden/taint.py\n \u00a75.2 access-proxy-conduit src/warden/proxy.py\n \u00a75.2 caller-identity-token src/warden/caller_identity.py\n\n2 declared gap(s) \u2014 tracked non-conformance, not conformance (\u00a711).\n\nPASS \u2014 every direct Tooling contact maps to a declared shape; validated against net-kingdom/canon/standards/security-layer-model_v0.8.md (accepted; sha256:3d8298560069ef471fba28d2b1245fb2309328f231dad1a667cdf0ce5a5a7c8b)\n",
"stderr": ""
}
],
"meaningful_regressions": {
"unicode_token_original_fold": "staff",
"original_accepted_non_ascii_token": true
},
"source_changes": [
{
"repo": "ops-warden",
"path": "scripts/check_layer_conformance.py",
"staged_path": "/tmp/layer-migration-stage/ops-warden/scripts/check_layer_conformance.py",
"original_sha256": "064be430b64072fd1620e6adc19b4e24a1790c9df06144a9670cd262faf66e8a",
"staged_sha256": "7262fae9fae7ed454c0765cc6859e83a74e59ef6eade849991e0b8ab9c66757b"
},
{
"repo": "ops-warden",
"path": "tests/test_layer_conformance.py",
"staged_path": "/tmp/layer-migration-stage/ops-warden/tests/test_layer_conformance.py",
"original_sha256": "3d39b0a24a65263b2be9f18add2b9c69e12b7926716d610e8200d5c788c1b696",
"staged_sha256": "93e3a8805460c4a6a4fd78734cf4dd85947a46e23d16a0d6898a43e8ec39275d"
},
{
"repo": "ops-warden",
"path": "INTENT.md",
"staged_path": "/tmp/layer-migration-stage/ops-warden/INTENT.md",
"original_sha256": "766c2d44851be7078a031575710843cd7d1278d665e874882c8e90980dcd0704",
"staged_sha256": "3e9382fefa52ee0970dfac133cc799656e5034760b26dccc1bf9bab65fc538d9"
},
{
"repo": "ops-warden",
"path": "registry/routing/catalog.yaml",
"staged_path": "/tmp/layer-migration-stage/ops-warden/registry/routing/catalog.yaml",
"original_sha256": "32b0093cf5b93025d2aa90397247fef6ccce73cde5f477895a90ad09ddb48643",
"staged_sha256": "09ee40df21b0ad8cad420be4613afcb1731967f68128631a94f9b96f3f31b318"
},
{
"repo": "ops-warden",
"path": "tests/test_routing.py",
"staged_path": "/tmp/layer-migration-stage/ops-warden/tests/test_routing.py",
"original_sha256": "e449a4e169ccb7021d1bc547c420628b2b618d713915b90cf68d0bcaf5d5430f",
"staged_sha256": "43998fccf67b1f0d1a6c8bde1a4768a32f8418fb72a8eda5bf321dd96c097d35"
},
{
"repo": "ops-warden",
"path": "wiki/playbooks/netkingdom-layer-declaration.md",
"staged_path": "/tmp/layer-migration-stage/ops-warden/wiki/playbooks/netkingdom-layer-declaration.md",
"original_sha256": "2dbef2235afdc33203fb01582220370051cf6c9eefc7c7d482ff05ebd615fb64",
"staged_sha256": "6a409a1c474ce8759b01cf5a62e5b95bba297e167a05fd8f8b616abdf6414306"
}
],
"knowledge_orientation": {
"package_version": "0.2.6",
"package_digest": "8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02",
"start_status": "withheld due changed sources; source reviewed directly"
},
"native_runtime_changed": false,
"pushed": false
}

View file

@ -1,78 +0,0 @@
{
"id": "WARDEN-CUST81-ATTENDED-DEPENDENCY-REPAIR-R1",
"revision": 1,
"status": "source_preparation_verified_not_live",
"reviewed_at": "2026-10-15T02:18:02.246091+00:00",
"base_commit": "1baeb0821cb6304464fab78932d8c6501f8c4cc6",
"scope": "Existing WARDEN-WP-0036 contained attended login; CUST-WP-0081-T02 dependency. No credential scope or live change approval expansion.",
"failed_attempts": [
{
"at": "2026-10-15T02:07:41.774194+00:00",
"exit_code": 10,
"outcome": "login_failed"
},
{
"at": "2026-10-15T02:10:58.943441+00:00",
"exit_code": 10,
"outcome": "login_failed"
}
],
"browser_evidence": {
"profile_count": 2,
"initialization_mtimes_utc": [
"2026-10-15T02:08:19.576345+00:00",
"2026-10-15T02:09:28.109549+00:00"
],
"prefs_file_presence_only": true,
"profile_contents_read": false
},
"cleanup_evidence": {
"custody_receipt_exists": false,
"bootstrap_receipt_exists": false,
"new_session_directory_present": false,
"preexisting_session_directory_mtime": "2026-10-14T22:41:38.098938+00:00",
"private_helper_read": false,
"server_revocation": "unconfirmed"
},
"findings": [
"The reviewed owner child never ran: Warden exit10 and absent child receipts.",
"Actual failure cause is unknown because captured login bytes were discarded. A 121-second duration is consistent with the Bao callback timeout, not proof.",
"Bao 2.5.5 internal helper uses .vault-token; Warden helper name is correct.",
"Standard flag parsing stops at auth data, but Bao setupEnv scans global format across argv. Trailing -format=json already chooses JSON output. Moving before role is defensive argv normalization, not a proven timeout fix.",
"Warden recovery matcher omitted s. tokens supported by this estate. This fix affects only existing in-memory emergency self-revocation after helper persistence failure."
],
"changes": [
"Insert synthesized -format=json immediately after bao login, before auth data.",
"Recognize s. in existing recovery-token matcher; preserve exact revoke-self-only use.",
"Expose only fixed diagnostic categories, numeric returncode, helper-present and output-shape booleans. Captured content is never surfaced."
],
"strict_output_allowlist_unchanged": true,
"focused_tests": {
"passed": 84,
"command": "python -m pytest -q tests/test_proxy.py",
"ruff": "passed"
},
"preserved_fixture_failures": [
"Initial task fixture omitted registry:50failed31passed; fixed by copying public registry.",
"Whole-suite attempt omitted scripts:5 collection errors; actual owner full suite required after integration."
],
"knowledge_orientation": {
"package_version": "0.2.6",
"digest": "8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02",
"status": "withheld; reviewed current NetKingdom SECURITY-COMPANION, gate-house INTENT, ops-warden AGENTS/INTENT and current Custodian environment source directly"
},
"source_references": [
"https://raw.githubusercontent.com/openbao/openbao/v2.5.5/command/main.go",
"https://raw.githubusercontent.com/openbao/openbao/v2.5.5/command/base.go",
"https://raw.githubusercontent.com/openbao/openbao/v2.5.5/command/login.go",
"https://raw.githubusercontent.com/openbao/openbao/v2.5.5/command/token/helper_internal.go"
],
"no_login_or_credentials": true,
"whole_suite": {
"status": "unverified_terminal_output_unavailable",
"handle": 39705,
"last_observed": "54 percent with six failures; no terminal result consumed by owner worker",
"terminal_handle_after_user_abort": "root poll reported Unknown process id",
"claim": "No full-suite pass is claimed; 84 focused proxy tests and Ruff passed."
}
}

View file

@ -1,172 +0,0 @@
# Which of ops-warden's two layer declarations governs — ruled, and applied
**Status: RULED and APPLIED, 2026-09-21.** `GH-DEC-2026-017` (`gate-house@def0af2`)
settles it: `INTENT.md` governs, the sidecar is derived, and the §3 vocabulary is
case-insensitive — so the two values were never in disagreement about a layer.
Route: `WARDEN-WP-0034-T06`, done. **Sections 1–3 below are the position as it
stood while the question was open, kept because the ruling is only legible
against the question it answers.** What changed, and what is still open, is
[further down](#the-ruling-landed--2026-09-21--and-it-is-applied).
## The fact
ops-warden carries both forms §11 of `security-layer-model` permits, and they
state different values:
| File | Value |
| --- | --- |
| `INTENT.md` frontmatter | `layer: Staff` |
| `layer.yaml` | `layer: staff` |
§11 accepts either form and does not say which governs when both exist and
disagree. A conformance run reading `INTENT.md` and a run reading `layer.yaml`
reach different answers for this repository, and **both are following the
standard**. That is the finding.
## What this is not
It is not a disagreement with anybody else. flex-auth's first published B1
(2026-09-20) reported that the estate spells `layer:` three ways *across*
repositories and that ops-warden was among the repositories that would fail a
validator on casing. flex-auth withdrew that on 2026-09-21: it came from a shell
pipeline that took the first `^layer:` match per repository across two files
without recording which file it came from, so it reported one value where there
were two. They rebuilt it as `tools/survey_layer_declarations.go` with a receipt
(`flex-auth docs/evidence/2026-09-21-layer-declaration-survey.json`), and running
it falsified the finding it was built to reproduce.
Nine of nine repositories carrying both forms do exactly what ops-warden does.
The split is *within* each repository and universal, not careless.
It is also not a typo. Either spelling is defensible: the custodian's
estate-wide sweep records that §3's own table capitalises, which makes the nine
lower-casing `layer.yaml` files the divergent ones against the standard's
spelling — and separately that §3 and §4 disagree inside the standard itself
(`Engines` in §3's table, `Engine` in §4's catalog rows). Neither of those is
settled either.
## ops-warden's position
1. **We are not aligning the two files on our own initiative.** Two questions
are open at gate-house — which form governs, and whether the §3 vocabulary is
case-sensitive — and they have different answers. Aligning now means picking
one, and picking one before the ruling is ops-warden authoring a ruling it
does not own, in the same move §11 forbids when it says a layer stated *about*
a repository by another repository is not a declaration.
2. **A unilateral fix would also destroy the evidence.** The finding is
*observable* precisely because nine repositories exhibit it. A repository
that quietly normalises its own files makes the estate look more conformant
than it is and shrinks the population the ruling is being made against.
3. **Neither value is in doubt.** ops-warden is Staff, declared in its own voice
in `ADR-0010`, and no reading of either file makes it anything else. What is
in doubt is only which file a mechanical check should read. No behaviour of
this repository depends on the answer.
4. **When the ruling lands**, whichever form it names, ops-warden changes the
other file to match in one commit, together with a regression test asserting
the two agree — so the next divergence is caught rather than surveyed.
## The ruling landed — 2026-09-21 — and it is APPLIED
`GH-DEC-2026-017` (gate-house `def0af2`, message `3715e247`, amendments A9–A13)
arrived hours after this page was written. It answers both questions and asked
ops-warden for two changes, because `layer.yaml` is the estate's reference form
and seven repositories copied it.
- **Precedence: `INTENT.md` governs.** Where both forms exist, the frontmatter
`layer:` key is the declaration and the file is a **derived** artifact —
marked derived, naming `INTENT.md` as its source, required to agree with it.
- **Casing: ruled away, not ruled on.** §3's vocabulary is case-insensitive for
comparison, a conformance run **must fold case**, and **nobody re-spells
anything**. `Staff` and `staff` agree. The plan in point 4 above — align the
files in one commit — is therefore **moot rather than completed**, and that
distinction is the point: the divergence was never a disagreement about a
layer, so there was never anything to align.
- **A disagreement between the two forms is still a finding** and must be
reported rather than resolved away, or precedence makes eight of the nine
invisible instead of settling them.
### What was applied, 2026-09-21
Verified first against gate-house's own committed files — `GH-DEC-2026-017` in
`decisions/decisions.md` at `gate-house@def0af2`, amendments A9–A13 in
`docs/amendments/v0.8-section-11-declaration-amendments.md`, and §3/§4/§11 of
`net-kingdom/canon/standards/security-layer-model_v0.8.md` — not from the inbox
message and not from this page, which is a secondary record. The decision and
this page's account of it agreed.
1. `standard_version` removed from **both** forms. The ruling's general form is
*a layer declaration MUST NOT carry a standard version*, and the governing
declaration is `INTENT.md`, so removing it from the sidecar alone would have
left the field in the one file that actually declares. `INTENT.md`'s
`standard:` pointer is de-versioned with it, for the same reason the field
goes: a version-pinned path reads as a validity condition. The version
ops-warden assented at stays with the assent (`ADR-0010`), which is where a
version-at-a-boundary belongs.
2. `derived: true` and `derived_from: INTENT.md` added to `layer.yaml`, and the
file's header now says plainly that it does not govern.
3. **No layer value changed.** `INTENT.md` still says `Staff`, `layer.yaml`
still says `staff`, and the comment marking the divergence is rewritten from
*unruled, do not touch* to *ruled, folding case is the checker's job*.
4. `scripts/check_layer_conformance.py` now reads `INTENT.md` as the governing
form, ASCII-folds before comparing, validates both values against §3's closed
four-token vocabulary (`Taxonomy` included — omitting it is the defect A9
records), requires the derived marking, and **rejects a returning
`standard_version` in either file**. It previously listed `standard_version`
as a required key, so it would have rejected the conforming declaration this
ruling produces.
5. `tests/test_layer_conformance.py` asserts the fold rather than equality. An
equality assertion would have been this repository performing the re-spelling
the ruling explicitly declined to order; the fold still fails on a real layer
divergence, which is what point 4 of this page originally wanted.
6. `pep-stance.yaml` left alone. Its `standard_version: "0.7"` with
`standard_version_reviewed: "0.8"` is a stance map, not a layer declaration,
and gate-house has said separately it would rather other consumers copied
that shape than that ops-warden changed it. `GH-DEC-2026-017` also states it
does not rule the sidecar's schema beyond the derived marking and the version.
### What the seven adopters must copy
The change set is written for them in
`wiki/playbooks/netkingdom-layer-declaration.md`. ops-warden does not edit their
files. The trap worth repeating: any adopter that also copied the checker has a
required-key list containing `standard_version`, so removing the field alone
turns a conforming declaration into a `MALFORMED` exit 2.
### Still open — where the removed version goes
`GH-DEC-2026-017` §5 and A12 both say version-scoped state *"belongs in the
derived conformance record, which under this section's derived-artifact rule
already MUST name what it derives from and carry the version or commit it was
derived at."* Both sentences say **already**, and ops-warden does not have one:
what it has is `scripts/check_layer_conformance.py`, a re-runnable check that
emits nothing durable. So it is unresolved whether §11 expects an **emitted
record** — a committed artifact carrying the version it was derived at — or
whether a re-runnable checker discharges the obligation, with the version
supplied by whoever runs it.
Asked of gate-house in message `4220413a` (2026-09-21). **No answer had arrived
when this change was applied, and the question is deliberately left open rather
than answered by choosing.** Nothing in the applied change depends on it: the
version is gone from the declaration either way, and the only thing the answer
decides is whether ops-warden must additionally emit a record to have somewhere
to put it.
## Why this page exists rather than an inbox thread
gate-house's own observation, recorded against ops-warden in another lane: a
correction that only matters months from now is badly housed in a message
thread, because a correction we merely read is one we can only get right by
remembering. A later session asking "why do these two files disagree, shall I
just fix it?" needs to find this page, not reconstruct the reasoning.
## References
- `net-kingdom/canon/standards/security-layer-model_v0.8.md` §3, §11 (proposed)
- `the-custodian/docs/assessments/2026-09-21-layer-declaration-boundaries.md`
— estate-wide view; questions 1, 2 and 3 are the ones that reach this page
- flex-auth message `719b3e9e` (correction), `docs/conformance/boundaries-review.md`
- `docs/adr/ADR-0010-ops-warden-is-staff.md` — the declaration itself
- `WARDEN-WP-0034-T06` — the live record of the wait

View file

@ -1,32 +0,0 @@
{
"initial_kernel_sha256": "eb6ee0cd00b2704904782e71c214c34a91d060086b49fbd4ba1b86b2290da7a7",
"initial_fixture_tests_passed": 23,
"initial_independent_probe": {
"fd_output_escaped": true,
"provenance_exception_escaped": true,
"fixture_only": true,
"native_execution": false,
"subprocess_exit": 1
},
"findings_disposition": {
"unbounded_source_parsing": "fixed:256KiB caps before JSON/YAML",
"python_only_output_withholding": "fixed:serialized fd-level+Python containment",
"provenance_error_payload": "fixed:contained verifier with fixed Refused; BaseException sanitized preserving running",
"aggregate_revocation": "fixed:separate required broker/parent receipt proofs",
"prior_attempt_receipt_reuse": "fixed:result/proof purposes include all Pins+operation+sequence+nonce attempt identity",
"undeclared_secret_parameters": "author self-audit fixed explicit acknowledged-type parameter gate"
},
"final_kernel_sha256": "3636d089bfa9dd3fe66971529305354ba1e7ed46572eeceaabeb3c9a064b76a8",
"final_fixture_tests_passed": 51,
"refined_containment_probe": {
"fd_output_escaped": false,
"provenance_exception_escaped": false,
"fixture_only": true,
"native_execution": false,
"subprocess_exit": 0
},
"refined_containment_probe_scope": "independent probe at preceding4edb183 kernel, unchanged containment in final3636d089; final51tests reverify all regressions",
"failed_outcomes_preserved": true,
"refinements_are_same_review_trial": true,
"native_verified": false
}

View file

@ -1,55 +0,0 @@
# Independent SecurityCase kernel review — 2026-10-11
Disposition: **agree to bounded local kernel preparation** at SHA-256
`3636d089bfa9dd3fe66971529305354ba1e7ed46572eeceaabeb3c9a064b76a8`.
Review ID `WARDEN-REVIEW-20261011-CASE-KERNEL-v1`. Exact source/test/owner records
and accepted design are hashed in the adjacent sources receipt. This implements
part of accepted design 78ab1439, not live authority or executable type acceptance.
The initial inspected kernel passed 23 fixture tests but independent synthetic
probes proved fd output escaped and raw provenance errors reached tracebacks.
Review also identified absent parser caps, aggregate revocation proof and missing
per-attempt binding. The findings receipt retains the failed revision and its
actual outcomes. Refinements are one trial, not separate scored successes.
The final exact revision independently passes **51 tests** (37 kernel, 14 reducer).
Source review confirms 256 KiB pre-parser bounds; closed descriptor metadata;
exact case/type/catalog/descriptor pins; independently checked acceptance,
structural materialization and refreshed caller-session provenance; and explicit
undeclared/fixed/secret parameter refusal. Caller-asserted human labels cannot
satisfy the injected verifier. Agent mint remains refused after human admission.
Private SQLite running intent commits before dispatch. Crash recovery persists
uncertainty without redispatch; success/no-change cannot replay. Safe retry needs
verified failed-before-change, unchanged pins, explicit retry and declared safety.
Each persisted attempt binds all exact Pins, operation, sequence and nonce. Owner
results and separate broker-grant/parent-session revocation proof purposes must
match it; stale proof reuse and absence of either required proof cannot succeed.
Reconciliation is a separate owner callback, not credential regeneration.
Serialized Python plus OS-fd containment discards adapter/verifier output without
payload retention, including inherited synchronous subprocess output. Typed
errors and sanitized interruptions preserve running intent without raw messages.
This contains trusted fixed owner callbacks; it is no sandbox for arbitrary
in-process hostile code or background children. Production adapters must reuse
admitted attended-login containment and tie installed code to owner source pins.
Rollback passes only verified case-created objects in reverse order, excludes
shared objects, refuses no-change undo and stops on uncertain/lost results.
Limits of 32 created identifiers and 128 transitions bound local receipts; owner
archival must precede further dispatch. Independent provenance checks remain
injected contracts, not an implemented authentication system.
No installed production adapters, native provenance verifier or dispatch CLI
exist. No dynamic import, inferred shell execution or live argv adapter was
introduced. Descriptive types lack accepted machine descriptors and remain
plan-only; no CLI may dispatch this harness as a native run. Full ordered
run-sheet orchestration, verification inventory, contained login integration,
owner adapter/type acceptance, actual materialization and first live case proof
remain WARDEN-WP-0044-T03 and existing owner tasks. Uncertain undo still requires
owner reconciliation. T03 remains in progress.
Fake tests verify local persistence/refusal/withholding only. They prove no
native identity, OpenBao grant/policy, consumer enforcement, independent audit
custody or production revocation. No token, live Secret, login, mutation or push
occurred during this review.

View file

@ -1,46 +0,0 @@
{
"review_id": "WARDEN-REVIEW-20261011-CASE-KERNEL-v1",
"disposition": "agree-bounded-local-kernel",
"approved_design_sha256": "78ab1439e4c5beb205aeccf15b1b2e706511be55ab195c5a3aa682f326c7c77c",
"sources": [
{
"owner_path": "ops-warden/src/warden/case_run.py",
"reviewed_source_path": "/tmp/warden-case-execution-stage/src/warden/case_run.py",
"sha256": "3636d089bfa9dd3fe66971529305354ba1e7ed46572eeceaabeb3c9a064b76a8"
},
{
"owner_path": "ops-warden/tests/test_case_run.py",
"reviewed_source_path": "/tmp/warden-case-execution-stage/tests/test_case_run.py",
"sha256": "1e70ceb858af0eb3a126132a3c3606706120c0caf58ca9d45deb43f217705c7e"
},
{
"owner_path": "ops-warden/docs/security-case-execution-kernel-v1.md",
"reviewed_source_path": "/tmp/warden-case-execution-stage/docs/security-case-execution-kernel-v1.md",
"sha256": "ed5b6432ada71d1065fc837e3638e248cd4803241b6430d6ded07c25c0094c8d"
},
{
"owner_path": "ops-warden/workplans/WARDEN-WP-0044-security-case-follow-through.md",
"reviewed_source_path": "/tmp/warden-case-execution-stage/workplans/WARDEN-WP-0044-security-case-follow-through.md",
"sha256": "c0029545bd2d8c015ae0cac87f6d53a2e5ee935b7bf5d5e1b81bf1ff5dbdc4f8"
},
{
"owner_path": "/home/worsch/ops-warden/docs/security-case-execution-proposal.md",
"sha256": "78ab1439e4c5beb205aeccf15b1b2e706511be55ab195c5a3aa682f326c7c77c"
},
{
"owner_path": "/home/worsch/net-kingdom/docs/security-case-execution-owner-review.md",
"sha256": "0f1637bfddbbdf50d336974791b7fbf4646efe7e32ca04beb3bf52effd0a28e6"
},
{
"owner_path": "/home/worsch/the-custodian/docs/evidence/2026-10-10-security-consolidation/human-acceptance-receipts.json",
"sha256": "85007938d246b6fedb1ef323386720261e4ac55a9bf95f92c4915b226da357a7"
},
{
"owner_path": "/home/worsch/ops-warden/src/warden/proxy.py",
"sha256": "2d12c432f21f529c5d67a59d584bffb55b8286bdc19ed0ec26d795f889ad989f"
}
],
"findings_sha256": "d583435077e15cd5239c1c748d1fc079a73b70d8a9ef6c61d79869e93aa1699b",
"live_authority": false,
"native_complete": false
}

View file

@ -1,37 +0,0 @@
# Browser producer pin refresh — 2026-10-16
WARDEN-WP-0044-T03 source review, scoped to NetKingdom commit
`015694ee414c802dc2ef1fa0120dfed610086e69`, against Warden `d79f065`.
Reviewed the exact committed schema, validator, evidence changes and the 21
producer contract tests. The optional bounded `execution_descriptors` map now
requires lowercase exact SHA-256 byte pins keyed to declared steps. The direct
evidence API refuses malformed maps. Absent and incomplete maps retain plan-only
behavior; the repair supplies no descriptor, adapter or execution authority.
This is compatible with Warden's independently authenticated admission gates.
Refresh only the validator and evidence module pins. Composer/capability module
hashes and both reviewed parameter-definition hashes remain exact and unchanged.
All four copied module bytes match the named NetKingdom commit and canonical
checkout. No fallback, wildcard or allowed-drift path is added.
Before the refresh, explicit canonical producer integration failed both positive
version tests (1.1/1.2), with 20 refusals passing. After refresh, 87 producer,
Executor and reducer checks pass, including 14 new malformed descriptor cases
and existing missing/changed-source checks across all four pinned modules.
The 21 producer descriptor-contract checks and focused Ruff checks also pass.
Use `WARDEN_TEST_NETKINGDOM_ROOT=/home/worsch/net-kingdom` explicitly: the
isolated checkout's default sibling can resolve another checkout.
Emergent knowledge 0.2.6 digest
`8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02`
was withheld due to source drift; start/local-work cards and actual source were
reviewed directly. It conveys no authority.
No new browser or identity-restore adapter was admitted or attested. No credential
or privileged execution occurred. Native structural receipts, authenticated owner
provenance, descriptor assent, attended sessions, ordered verification and the
first native case run remain open. T03 stays progress; T04 stays wait.
Exact pin hashes, tests and publication identity are retained under the Custodian
`docs/changes/netkingdom-target-2026-10-16/warden-browser-pin-refresh/` directory.

View file

@ -1,192 +0,0 @@
# Closed case execution kernel v1 — 2026-10-11
Owner implementation record for WARDEN-WP-0044-T03. The architectural proposal
at SHA256 78ab1439e4c5beb205aeccf15b1b2e706511be55ab195c5a3aa682f326c7c77c
was accepted through memo `infd-20261010-security-case-execution`, version 1,
approval e46b149b-ec3c-4b3b-98a7-dcb4934448ca, confirmed disposition
disp-cf44dacb-2117-4de7-8f90-cfa99f84f325 at 2026-10-10T23:23:31Z. The original
approved proposal remains byte-identical. That decision accepts source design;
it grants no execution session, credential or deployment authority.
`src/warden/case_run.py` advances the pure reducer into an executable injected
owner harness with private durable receipts. Descriptor version
`warden.case.execution.v1` is a closed local implementation seam, not an accepted
NetKingdom type schema. It contains fixed owner/adapter/source digest, actor,
lane, target parameter, effect, reviewed created-object identifiers declared
retry safety and explicit required revocation scopes (parent-session and broker-grant).
A mint descriptor cannot omit either scope. Unknown fields, free shell text and secret parameters are refused.
An acknowledged type must additionally pin the descriptor bytes under its
`execution_descriptors` map and independent owner evidence must authenticate
that exact machine-seam revision. Existing descriptive types lack this map and
remain plan-only. Their acknowledged labels do not authorize this new seam.
There are no installed owner adapters, native provenance verifier or dispatch CLI
in this change. Callables are injected only through a fixed adapter registry,
with immutable owner reference/source pins. A real integration must supply
independent owner/native verification or explicit attended manual evidence;
a bare JSON receipt identifier is insufficient. Fixture verifiers in tests
are deliberately fake and attest no native case. Caller sessions are independently
verified for exact principal/actor/lane at admission and again before dispatch;
input actor labels alone are never sufficient. Agent mint is refused, including
when WARDEN_AGENT_ID appears after initial human admission.
Each admission pins exact case/type/descriptor/catalog bytes, acceptance reference
and run-sheet identity. Actual ops-mason materialization evidence is required
against the case source, separately from its status label. Case parameters supply
an exact metadata target. Source/session changes cannot inherit journal success.
Owner results have a closed metadata shape, result evidence fingerprint and
separate independently authenticated grant/parent-session revocation evidence
fingerprints. An aggregate revoked boolean alone cannot satisfy either scope. Unknown result payloads, unverified evidence, absent revocation and
created objects outside the reviewed scope become uncertain, never success.
Python and OS stdout/stderr, including inherited child-process output, are
discarded without buffering or retention under a serialized fd containment
context. Independent provenance verification uses the same containment and
sanitizes exceptions. All source inputs are capped at 256 KiB before parsing.
Error text is not copied to the journal. KeyboardInterrupt/SystemExit and other
owner interruptions are rethrown with a fixed message while retaining running
intent for crash recovery.
The journal uses an owner-private 0700 directory and 0600 files, refuses symlinks
and insecure modes, uses SQLite FULL synchronization and commits running intent
before calling the adapter. A separate process lock prevents concurrent mutation.
A crash between dispatch and final receipt leaves running; the next lock holder
persists uncertain before refusing replay. Both intent and outcome remain in
history. Each persisted attempt has a unique identity bound to all exact pins,
operation (run/reconcile/rollback), sequence and nonce. Adapter results and both
revocation proof purposes carry that identity. Prior-attempt evidence cannot
verify a safe retry, even if a caller rewrites result labels. Descriptor-created
objects are capped at 32 and histories at 128 transitions, with dispatch stopped
before archival is required. A throw or lost response is not failed-before-change. A safe retry needs
verified failed-before-change, unchanged pins, descriptor retry-safe and explicit
retry. Success and no-change cannot replay.
Reconciliation is an explicit fixed owner callback, never the mutation callback.
It may verify an already-present native CAS result as no-change/success, or prove
failed-before-change; it never tells Warden to regenerate a credential. Uncertain
or failed-after-change requires this owner-native inspection before any retry.
Owner proof and revocation remain mandatory. Rollback visits only objects in a
verified creation receipt, in reverse order; no-change has no undo and shared
objects are excluded. Unknown/lost undo blocks another rollback.
The kernel operates individual admitted steps. Full ordered run-sheet orchestration,
verification inventory, contained login integration, native owner adapters and
CLI activation remain WARDEN-WP-0044-T03; no overall native-complete result is
implemented. Current Forgejo source is not materialized and has no acknowledged
machine descriptors. Its first native run remains an acceptance requirement,
with real materialization, separately authenticated broker-grant and parent-session
revocation receipts, session/owner receipts and consumer enforcement. This
implementation neither promotes a type nor changes any case status.
Validation: 37 new kernel tests plus 14 reducer tests pass (51 total). Tests open
an independent SQLite connection inside dispatch to prove running was committed,
crash after mutation and reopen a fresh journal to prove no duplicate invocation,
lose a response then reconcile without mint, require explicit retry, check exact
pins/session expiry, withhold malicious output and unconfirmed revocation, and
prove created-only reverse undo plus uncertain-undo refusal. Ruff formatting and
checks pass. No live token or target action occurred.
## Browser input projection — 2026-10-16 source preparation
An explicitly configured `BrowserInputs(netkingdom_root)` now validates browser
cases before admission. It calls the real NetKingdom type/case validator over
verified producer bytes in a disposable Python process (`-I`, empty environment,
timeout, no inherited output). The validator, composer, capability contract and lazy evidence
modules have exact source pins; missing or changed code refuses. There is no
automatic discovery or download. A producer upgrade needs reviewed pin changes.
Only the exact reviewed 1.1/1.2 browser parameter definitions qualify. A digest
of each complete parameter definition list prevents reclassifying secrets as
metadata, loosening constraints, or adding fields under a recognized version.
The complete case passes producer validation, including required fields,
constraints and credential-shape checks. Explicit fixed/default overrides,
unknown fields and secret references refuse. Security-sensitive browser metadata
can therefore be present without being passed to an owner adapter: the only
projection is its descriptor's exact public/operational target identifier.
No raw parameter values are added to invocations or persisted receipts.
`require_implemented=False` validates the acknowledged type's source, not its
execution authority. Existing descriptor/type/source/catalog/caller provenance
gates stay mandatory. Neither historical browser type contains the required
exact machine-descriptor map, and neither is promoted by this change. There are
still no installed native owner adapters, authenticated provenance verifier,
contained session/ordered run orchestration or live CLI dispatch. A journal digest
is a fingerprint and cannot authenticate native evidence. Completion still needs
owner-admitted descriptors and actual structural, mint/delivery, consumer and
revocation/rollback evidence. Local fixture success is not native acceptance.
Tests use the actual producer checkout (override sibling location with
`WARDEN_TEST_NETKINGDOM_ROOT`), the real Executor, SQLite journal and injected
test-only owner adapter/evidence verifier. Their descriptor extensions and case
materialized labels exist only in memory. Canonical case/type bytes stay intact.
Producer integration refinement: the accepted NetKingdom browser-case patch
`d32a7f51cbf3012bee3e4bb329f4259dea867eff1c59a2b01a65d72774488d05`
is now the tested producer revision. Its evidence module is imported lazily only
by `check-evidence`, outside this admission path; Warden nevertheless pins and
copies it with the complete reviewed producer set. Missing or modified bytes in
any of the four modules refuse admission. The test covers every dependency.
## Original-intent consistency boundary — 2026-10-17
`OriginalIntent` and the optional additive
`reconcile_original(ReconcileContext(original, attempt))` callback retain the
original run separately from a fresh reconciliation attempt. Existing one-argument
reconcile adapters remain source-compatible. Intent and pre-dispatch receipt are
committed in one SQLite transaction; lookup now requires their exact attempt,
actor, step/owner/target, source and pin correspondence. Missing legacy context,
an orphan receipt, changed context or an invalid bounded field contract refuses;
there is no backfill or new runtime holder.
Original intent serialization accepts only the fixed reviewed Invocation/Pins
field contract and bounded operational identifiers. Duplicate, oversized and
malformed original records refuse. Current Executor.admit leaves tenant and realm
empty: those reserved fields are not admitted tenant/realm provenance, and future
nonempty coordinates need an authoritative owner contract. Raw owner payloads,
credentials, parameters and caller output are not part of this context contract.
Local dispatch correspondence is not independent owner proof. A real owner must
still authenticate the exact original created-only result and original revocation,
current native revision and the fresh attempt's session/grant evidence before
recovery can count. No actual original holder/verifier or native adapter is added.
NetKingdom's current registration adapter still has no reconciliation callback;
source tests do not authorize its post-restart undo, replay or a changed caller/
session/run-sheet migration. WARDEN-WP-0044-T03 remains a native acceptance gate.
## Current caller versus original session reference — 2026-10-18 source
`Executor.admit` now accepts the optional `original_session_reference` for an
exact existing original-run continuation. It is metadata, never current
authentication. The normal `session_evidence` is independently verified as the
current caller's principal/actor/lane proof, including after restart, and remains
the evidence rechecked immediately before every journal write/owner callback.
The optional reference preserves the original run-sheet digest rather than
replacing immutable provenance with the new login. Before admitting it, under
the existing journal lock, the kernel requires an existing matching step journal
and `Journal.original_intent` correspondence: the original running receipt,
attempt, owner/target/principal/actor/lane, all six pins and exact adapter source.
Missing/orphan/legacy context, a changed reference or caller and a stale current
session refuse. The reference must differ from the current evidence identifier;
the actual session owner must additionally establish distinct authenticated
token/session identity, not just two labels. No new original context is created,
no completed operation can replay, and uncertainty is not cleared by readmission.
Omitting this option retains the old admission/pin semantics. Supplying a fresh
session alone still creates a different run sheet and cannot reuse a previous
journal. No existing descriptor, Invocation, Pins or receipt schema changes.
The new source seam does not implement changed-source/catalog/caller migration
or loss of rollback result; those still require the existing owner reconciliation
and recovery evidence. It is not general session management or another issuer.
A source integration fixture runs the actual producer-owned KeyCape status CLI
over HTTPS, the actual Warden kernel and a separately compiled generated Go
consumer. It verifies revoked-parent refusal before writes, distinct inverse
authentication in another Python process, original journal pin retention,
no business replay, exact synthetic target restore and independent read-only
negative cleanup observation. Case/type/catalog/owner attestations and the
target are synthetic; this is not Forgejo/browser/native acceptance or a real
approval. KeyCape's proposed online profile remains unadmitted. The private
unsigned CLI observation is not portable execution authority; actual owner
custody and original grant ownership are still mandatory before an inactive
response can establish revocation. No native verifier or adapter is installed.

View file

@ -1,136 +0,0 @@
# SecurityCase execution contract proposal — 2026-10-10
Status: proposed owner review; not an accepted runtime protocol or authority.
Owner: ops-warden. Related: WARDEN-WP-0044-T03, NK-WP-0048, MASON-WP-0010.
`warden case plan` now gives a valid descriptive run, but the type's
`entry_point`, `auth`, verification `check` and rollback `action` fields are
prose. No `warden case run` or `rollback` implementation exists. A case becoming
`materialized` cannot turn those descriptions into executable commands.
The proposal preserves caller authority and routes by owner. It introduces no
new credential, policy grant, shell command inferred from prose, or authority
for Warden to configure a consumer. Architectural contract acceptance belongs to
net-kingdom and adapter interfaces to their owners. Founder review is needed
only if the acknowledged type's authorization/session boundary changes; the
current absence of a machine contract is not an excuse to fabricate one.
## Proposed machine seam
Keep human descriptions. Add a separately validated `execution` reference to
steps and checks, resolving to owner-provided closed adapters. An adapter has:
- immutable reference (owner repo, interface id, version, source digest);
- closed parameter schema (no secret-sensitivity fields; no free shell text);
- declared actor mode and catalog lane, which must agree with the case plan;
- explicit side-effect class and target identifier deriving from case parameters;
- idempotency/reconciliation contract and metadata-only receipt schema;
- supported rollback reference or explicit owner-only undo.
Warden does not invent an argv contract for ExternalSecrets, group membership,
KeyCape registration or Forgejo configuration. Owner adapters may internally
call their reviewed tools. Warden invokes only the fixed entry point provided by
that adapter, with validated separate argv fields, and refuses unknown adapters
or templates. `entry_point` prose never goes to `shell=True` or `sh -c`.
An owner may initially supply a receipt-only adapter: the run sheet records that
step as pending until the owner performs it and provides independent evidence.
This gives useful resumable orchestration before every live adapter exists,
without implying native completion or transferring ownership.
## Run sheet and receipts
The run sheet pins exact case bytes (`case-ref`), accepted type bytes/version,
owner adapter digests, catalog revision, acceptance reference and ordered step
identities. Changed pins require a newly reviewed sheet; never inherit previous
success into a different exact revision without explicit reconciliation.
Each attempt yields an immutable metadata receipt containing these pins, step
id, owner, target, attempt id, result enum and evidence-reference fingerprints.
Allowed results: pending-owner, not-started, running, verified-success,
verified-no-change, failed-before-change, failed-after-change, uncertain.
Neither subprocess output nor arbitrary owner payload is copied into receipts.
Adapter error strings are not evidence and must not be logged verbatim.
Stop on failure or uncertainty. A thrown exception or lost process receipt is
uncertain, not failed-before-change. Uncertain/failed-after-change blocks retry
until the owner reconciles target state. `verified-success` and
`verified-no-change` are not repeated. Retry after failed-before-change requires
the same pins and adapter's explicit retry-safe classification; a replay-safe
CAS0 receipt is reconciled, not retried into secret generation. State persistence
must write running before dispatch and result afterwards; interruption between
those writes leaves uncertain on resume. Outbox failures remain visible and do
not get rewritten as verified audit delivery.
Owner receipt ingestion must authenticate provenance or be explicitly manual
attended evidence, never a bare trusted JSON argument. Source/local receipts are
attributive preparation until their owner contract defines independent native
verification. Overall native completion requires every applicable step and
verification plus confirmed session revocation; pending-owner is never success.
## Sessions, values and rollback
Reuse `proxy_attended_login_exec` containment/output suppression/revocation
rather than add a second login helper. A key-cape login lane and the OpenBao
broker issuer role are distinct grants; the executor must represent both and
never silently share/inherit a token. Grouping is permitted only within the
same declared identity/lane and preserves follow-through order. An agent
principal cannot dispatch generated-in-custody mint-secret. The broker continues
to rederive the exact case-ref independently.
Rollback traverses successful step receipts in reverse order and only invokes
an owner's reviewed undo for target changes that receipt proves were created
by this case. Existing shared groups, membership and policies cannot be deleted
by interpreting a type rollback sentence literally. 'No change' yields no undo;
failed/uncertain states require reconciliation first. Do not restore a prior
secret value by extracting it into Warden; custody rollback is owner-native.
## Owner acceptance requests to prepare
1. net-kingdom: accept the closed machine seam separately from descriptive type
fields, document materialized = structural acceptance, and avoid gating that
state on mint-secret which is itself a follow-through step. Pin the revised
schema/type and keep type acceptance provenance explicit.
2. railiance-platform: provide a generate adapter over existing `credential.py`
exact `--grant`, `--client-id`, `--case-file`, `--case-ref` and receipt result
codes; clarify reconcile semantics for already-present CAS0 and grant/session
revocation evidence. No new credential is required by this proposal.
3. key-cape/net-kingdom: provide registration/group/env adapters and verify/undo
contracts in the existing attended boundary; do not hand Warden a user-engine
runtime token or a new broad Kubernetes credential.
4. railiance-apps/Forgejo: provide consumer OIDC + groups enforcement adapter or
receipt-only step, exact auth binding for CCR-2026-0042, and source/native
group-denial verification. This stays consumer ownership.
5. ops-mason: give a hash-pinned materialization receipt and list of objects
created vs pre-existing, sufficient for collision checks and scoped rollback.
No coordination messages have been sent by preparation of this proposal.
## Implementation and verification after owner seam acceptance
Implement `case_run.py` with injected typed adapters and receipt storage, then
CLI run-sheet/receipt import/resume/rollback commands. Keep case_plan read-only.
Add type machine-seam validation without turning valid descriptive planning
into an unreviewed source-breaking schema migration.
Required tests: rejected changed pins; unaccepted/unmaterialized cases; unsupported
adapter/argv refusal; agent mint refusal; pending owner keeps native gate open;
verified success never replayed; lost receipt resumes as uncertain; crash after
running persistence prevents duplicate side effect; failed-before-change retries
only with declared retry-safe adapter; failed-after-change needs owner reconcile;
malicious/secret output excluded; inability to revoke remains unverified;
rollback visits created successful objects only, preserves shared memberships,
and stops on uncertain undo. Inject runners; no live login or target required.
WARDEN-WP-0044-T03 remains open. This packet resolves the precise source contract
deficit; it is not case materialization, a native run, or a type decision.
## Local reducer prototype
`src/warden/case_receipt_state.py` is proposed local source preparation, not an
accepted wire format. Its pure immutable transitions can be tested without any
owner adapter or shell dispatch. Persist the returned running intent before an
external side effect; on a lost result, recover marks uncertainty and blocks
replay. No receipt provenance, durable storage, CLI dispatch or live action is
implemented. `tests/test_case_receipt_state.py` covers the failure boundaries.

View file

@ -24,24 +24,6 @@ actors:
applicability: applicable
workload_id: codex-interhub-bootstrap
# CUST-WP-0073-T02 / WARDEN-WP-0045: supervised interactive agent account
# (Codex, Claude Code, Grok Build) on host account custodian-agt @ Railiance01.
# Issued only by the supervisor from the operator account; the agent distro
# (custodian-agents) holds no warden config, CA key or Vault token.
# No source-address: workstation egress IP is not stable (see wiki).
agt-custodian:
type: agt
principals:
- agt-custodian
ttl_hours: 12
max_ttl_hours: 12
extensions:
- permit-pty
description: "Supervised interactive custodian agent (custodian-agt on Railiance01)"
zone_subject:
applicability: applicable
workload_id: custodian-agents
adm-example:
type: adm
principals:

View file

@ -4,9 +4,7 @@
backend: vault
vault:
# OpenBao via the ops-bridge tunnel (railiance01). bao.coulomb.social is no
# longer public; it serves a notice, not the API.
addr: http://127.0.0.1:18200
addr: https://bao.coulomb.social
mount: ssh
role_map:
adm: adm-role
@ -26,9 +24,6 @@ policy:
# 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
# Fetch-check pin (CUST-WP-0075), used only by `warden access` fetches.
# No fallback to flex_auth_url; unset means every fetch is refused.
fetch_flex_auth_url: http://flex-auth-ops-warden-fetch.flex-auth.svc.cluster.local.:8080
zone_registry_path: registry/flex-auth/production_registry_snapshot.json
failure_modes:
z0-experimental: fail_open

View file

@ -1,171 +0,0 @@
# 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

@ -1,136 +0,0 @@
# 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

@ -1,156 +0,0 @@
# 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.2–v0.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

@ -1,157 +0,0 @@
# 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.2–v0.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.

View file

@ -1,175 +0,0 @@
# security-layer-model v0.8 — ops-warden review
**Date:** 2026-09-09
**Reviewing:** `net-kingdom/canon/standards/security-layer-model_v0.8.md` (status
`proposed`, net-kingdom@66eeaba), circulated for assent by gate-house
**Prior reviews:** `2026-08-29-layer-model-v04-review.md`, `-v06-review.md`,
`-v07-scope-intent-assessment.md`
## The cell in question
v0.8 §6.4 obligation 3 adds: *unknown is not a zone and MUST resolve to
`fail_closed`.* `pep-stance.yaml` declares `unknown: fail_open` under `ADR-0009`.
§13.1 records the cell as non-conformant.
## Finding 1 — assent. The rule is right and our falsifier check fails.
gate-house wrote the falsifier into GH-DEC-2026-009's reversal: a scope that is
genuinely unknown **and** genuinely low-consequence, where failing closed costs
availability with no security gain. Their expectation is that any such path is a
§5.1 read-only diagnostic carrying no PEP obligation at all.
We looked for one on our side and do not have it. The stance map governs exactly
one protected action — `warden sign`, SSH certificate issuance. That is a
credential-issuing side effect, not a diagnostic; §5.1 does not reach it. So the
predicted escape hatch is absent here, and the argument stands unrebutted on its
own terms:
- §9.3 licenses trading availability for openness **per zone**, knowingly, for a
named scope. Where the scope is unknown that trade was never made for the
request in hand, so resolving permissively does not extend a considered
decision — it invents one.
- Unknown is the cheapest state to induce. Our own `unknown` reasons are
`workload_resolution_absent` and `catalog_applicability_absent` — an
unregistered workload or a missing declaration, neither of which requires a
credential to arrange.
We accept that being unclassifiable must not buy permissiveness. The cell is
wrong as doctrine.
## Finding 2 — the rule is right and adopting it today would be a global fail-closed flag in all but name
This is the finding we would like recorded, and it is about adoption cost, not
about the rule.
The stance map applies only when the engine is unreachable, so the cell's bite is
proportional to how much of the target population sits in `unknown`. Measured
today:
| Population | Resolved to a zone | `unknown` | `not-applicable` |
| --- | --- | --- | --- |
| Signing targets (actor resources in the flex-auth snapshot) | **0** | **3** | 1 |
| Routing catalog lanes | 3 | 18 | 12 |
Every signing target that resolves at all resolves to `unknown`. So
`unknown: fail_closed` adopted today does not fail closed on an edge case; it
fails closed on **essentially every certificate ops-warden issues** whenever
flex-auth is unreachable.
That is the precise configuration `ADR-0006` rejected and `ADR-0009` replaced: one
value making the decision engine a uniform dependency of every signing path,
*including the continuity paths needed to repair that dependency*. If flex-auth is
unreachable and the operator needs an SSH certificate to reach the host and
restore it, `unknown: fail_closed` denies the certificate — and the target is
`unknown` precisely because nobody has classified the repair path.
The zone model already answers this: classify continuity paths `z2-continuity`,
whose stance is `fail_open`, and the deadlock does not arise. We accept that as
the right answer. The point is that it is *work not yet done*, and the reason it
is not done is not ours to fix — 18 of our 18 unknown lanes are unknown because
another repo has not published an authoritative workload identity declaration.
`ADR-0009` rule 3 forbids us from closing that gap with inference, correctly.
So as written, v0.8 §6.4 obligation 3 transfers an availability cost onto the
consumer for a classification gap the consumer cannot close alone, and it does so
at the moment of assent rather than at the moment the gap closes.
**What we ask for**, in preference order:
1. **§6.4 obligation 3 names a transition.** A consumer may declare
`unknown: fail_open` as a dated, published transitional state with a
classification-coverage figure attached, converting to `fail_closed` on
coverage rather than on calendar. This keeps the doctrine and prices the
adoption honestly.
2. Failing that, **§13.1 records classification coverage alongside stance.** A
register row reading `unknown: fail_closed` while 100% of that consumer's
targets are unknown is conformant and materially misleading; a reader cannot
tell a strict consumer from an unclassified one. This is the same defect
§11's *published map equals shipped behaviour* rule exists to prevent, one
level up: the map is now accurate about itself and inaccurate about its
effect.
## Finding 3 — §12's derived-artifact rule, small and in agreement
Six instances in one week matches our experience. `registry/generated/high-risk-data-paths.yaml`
carries `catalog_revision` and a `catalog_dirty` flag for exactly the reason §12
now generalises: a derived artifact whose revision does not describe the file it
came from is worse than one with no revision, because the stale one is trusted.
No change requested; recording the agreement since the rule is being generalised
from other repos' incidents.
## What ops-warden is doing about it
Not flipping the cell on a `proposed` standard, and not flipping it before
coverage exists — that ordering is `ADR-0006`'s whole holding. `WARDEN-WP-0040`
records the adoption sequence: raise signing-target classification coverage
first, then convert the cell, with the deadlock case classified before rather
than after. If v0.8 is accepted with obligation 3 unchanged, ops-warden's cell
remains a **declared gap** under §11 — tracked non-conformance with a named
owner and a route, which is the mark §9.1 exists for and which we argued for in
the v0.6 round.
## Axis declaration (second change touching us)
`pep-stance.yaml` already carries `scope: security-zone`. No change needed. We
agree with rejecting a forced migration onto zones — secrets-engine scoping by
catalog stage is an accurate statement about their control, and making them
assert a zone they cannot know would trade a real fact for a conformant fiction.
The unavailability of cross-axis aggregation is better recorded than papered
over, and §13.1 now says so.
---
## Ruling — GH-DEC-2026-011, 2026-09-09
gate-house closed the assent round the same day: **ask 1 declined, ask 2
adopted**, standard amended at `net-kingdom@64394e9`, dispositioned in
`gate-house/docs/conformance/2026-09-06-v08-assent-round.md`.
**Ask 1 (a dated transitional `unknown: fail_open`) — declined**, and the reason
is better than the ask. A sanctioned transitional `fail_open` is
*indistinguishable at runtime* from the stance the rule forbids, and it would make
the rule optional at the moment of adoption — the only moment it costs anything.
Dating an intention to stop being permissive does not make `unknown` any more
expensive for an attacker to induce while the transition runs. Accepted without
reservation; the argument defeats the ask on its own terms.
**Ask 2 (coverage published beside stance) — adopted.** §13.1 now carries a
Coverage column and this repo's figures are its first entries: *signing targets
0/3 resolved; routing lanes 3/21 resolved (2026-09-09, self-measured)*. Three
guards came with it, all of which we would have wanted: coverage does not soften
the stance, does not gate it, and never makes a non-conformant cell conformant; a
blank means "not reported" and never "complete"; and the decision carries an
explicit reversal condition — if a row is ever argued to be conformant *because*
its coverage is low, the column is doing harm and comes out.
That reversal condition is the right shape and it binds us first, since the column
exists because we asked. Our row reads non-conformant **and** low-coverage; those
are two facts, and the second is not a defence of the first.
**The deadlock was recorded rather than routed around.** §6.4 obligation 3 now
states that a stricter stance is not a licence to manufacture the membership that
makes it survivable — where a scope is unknown because another repository has
published no workload-identity declaration, the consumer MUST NOT infer one. That
is `ADR-0009` rule 3 promoted into the standard at exactly the point where the
pressure lands, which is more than we asked for.
### What we did in response
- `pep-stance.yaml` marks the `unknown` cell as a **declared gap** inline, with
the assent, the measured reason for not flipping, the declined ask, and
`WARDEN-WP-0040` as its route. The stance value is unchanged — v0.8 is still
`proposed` and v0.7 remains in force, so `standard_version` stays `0.7` with a
new `standard_version_reviewed: 0.8`.
- The coverage figures are published in `pep-stance.yaml` and, because we asked
for the column, **measured rather than hand-counted**:
`scripts/report_coverage.py` derives both populations from the artifacts the
runtime uses, reusing `report_workload_join.build` rather than re-deriving the
join. `tests/test_layer_conformance.py` asserts the published block equals the
measured one, and asserts the cell stays marked while it is `fail_open`.
A hand-counted figure in a register that explicitly does not recompute it would
decay silently, and a stale number beside a marked cell is worse than the blank
the other four rows carry — a blank at least reads as "not reported".
- `WARDEN-WP-0040` T04 is closed; T01–T03 stand unchanged. gate-house confirmed
they are not asking for the cell to be flipped before coverage exists.

View file

@ -1,110 +0,0 @@
# A-16 and A-17 — response from a repository that bears the cost
Date: 2026-09-21
Subject: gate-house `a5a1bcf`, INTENT.md Core Rules 16–17, ArchitectureBlueprint
A-16/A-17. No assent was requested. This is the "if it would cost you something
we have not counted" reply.
Both rules are right. Two things are worth sending back: a third instance of
A-16 that predates the rule, and one refinement to A-17 that our own declined
request exposes.
## A-16 — distinguishable routes
The rule as stated: where one observable state is reachable by two routes that
differ in security meaning, the record must say which route produced it; the
routes usually must behave *identically*, and safely.
**A third instance, built before the rule and without knowing it.**
`pep-stance.yaml`'s `on_apply` block records `policy_decision_id` **only where a
decision was actually rendered**, alongside `policy_zone`, `policy_failure_mode`
and `outcome`. A certificate issued because access-engine allowed it and a
certificate issued because the engine was unreachable and the stance said
`fail_open` are the same observable state — a signed cert in the signatures log
— reached by two routes that differ entirely in security meaning. The absent
`policy_decision_id` is what tells them apart.
We did not derive that from a principle. We built it because §6.4 obligation 1
requires a decision record for a protected side effect and there is no decision
to hold when the engine is unreachable, so we recorded the *application of the
stance* instead. That it lands exactly on A-16 is the useful part: the rule
reproduces a choice a consumer made independently under local pressure, which is
better evidence for graduation than our agreeing with it would be.
**The cost of A-16 here is small and we would pay it again.** One extra field on
an audit record. The failure it prevents is not small: without it, "the
signatures log shows 3 issuances, all allowed" reads as a statement about
authorization when it may be a statement about reachability.
## A-17 — fail-closed transitions, and the one thing we would add
We are one of the two cases. Our dated `unknown: fail_open` was declined, and
the decline was correct. The distinguishing case is *engine unreachable and
target zone unknown* — precisely where a transitional `fail_open` fails open. So
it is a permission, and a register that held it would be holding a permission
under a name that reads as a gap. We argued the other way at the time and were
wrong about it; A-17 states why in one sentence, which is more than our
objection did.
**The refinement.** A-17 as written tests *direction of failure at the
distinguishing case*. Our case shows that direction alone is not sufficient for
the thing a register is for, because a deviation that fails closed can still be
**unobservable**, and then nobody can tell when the transition has ended. What
actually made our non-conformance holdable was not a date and not the direction
— it was the marking plus a *measurement*: `0 of 3` signing targets resolve to a
zone, `3 of 23` routing lanes do, measured from the artifacts the runtime uses
rather than hand-counted.
So we would state the corollary explicitly, and it is A-16 applied to A-17:
**where transitional relief is admitted, the record must distinguish an outcome
produced under the relief from the same outcome produced conformantly** —
otherwise the transition has no detectable end, and a relief with no detectable
end is a permanent rule acquired by a different route. Two rules, one shape,
which is the thing you said you had not noticed until three variants sat
together.
**And a corollary on what replaces relief when it is refused.** When A-17
declines a transitional deviation, what the register should acquire instead is a
*measurement*, not a *date*. A date says when someone intends to look again; a
coverage figure says how far the conformant behaviour is from being affordable,
and it moves on its own when the work happens. Ours is the worked example of the
substitution — we asked for relief, were declined, and got a Coverage column
instead — and we would rather A-17 named that remedy than left each declined
consumer to invent it.
**Is two cases thin?** Yes, and here is the honest limit from inside one of
them: our case and key-cape's differ in more than direction of failure. Ours is
a *runtime* stance about an unreachable engine; theirs is an *assertion
provenance* question about a claim. A-17 reads as though the two are the same
kind of thing because both are transitional, and we cannot tell from one case
whether the rule survives a transition that is neither a runtime failure mode
nor a claim. We would not block graduation on that, but we would not count
ourselves as two independent data points either.
## One thing today's work adds, unprompted
You wrote that a register which does not recompute its figures makes every entry
a hand-count in a place that cannot detect staleness. Our published coverage
block went stale within twelve days: the routing-lane denominator moved from 30
to 38 as lanes were added, so `unknown` went 18 → 20 and `not_applicable` 12 →
15 while `resolved` stayed at 3. Nothing about our classification changed. The
only reason it was caught is the test asserting the published block equals what
`scripts/report_coverage.py` measures — the test you called the better half of
the pair, and it earned that today rather than in principle. Corrected figures
sent separately.
Classification did not regress. The denominator moved. That distinction is
itself an A-16 case: `unknown: 20` reached by "we classified less" and
`unknown: 20` reached by "there is more to classify" are the same number with
different meanings, and a Coverage column that carries only the number cannot
tell them apart. §13.1 may want the two populations dated, or the delta
attributed, if the column is to be read over time rather than at a point.
## References
- gate-house `a5a1bcf` — INTENT.md Core Rules 16–17, ArchitectureBlueprint A-16/A-17
- GH-DEC-2026-011 — the declined transitional `fail_open`; §13.1 Coverage column
- `pep-stance.yaml` — the marked cell, `on_apply`, `classification_coverage`
- `docs/adr/ADR-0009` — stance map rule of record
- `WARDEN-WP-0040` — the route out of the marked cell
- `history/2026-09-09-layer-model-v08-review.md` — the v0.8 review this follows

View file

@ -1,161 +0,0 @@
# Review — `oidc-browser-relying-party` 1.0 `follow_through` (WARDEN-WP-0044-T01)
Reviewed: net-kingdom `security-case-types/oidc-browser-relying-party/type.yaml`
(1.0, proposed), against canon `security-case-types_v0.1` and ops-warden
`ADR-0001`, `ADR-0002`, `ADR-0004`, `ADR-0005`, `ADR-0008`, and `ADR-0010`.
Reviewer: ops-warden, 2026-10-08. Feeds net-kingdom `NK-WP-0048-T05`.
**Verdict:** the step order, the attended grouping, and the rollback are sound.
Before ops-warden can plan a run of this type (`WARDEN-WP-0044-T02`), the schema
needs the changes R1–R3. R4–R7 are refinements.
## Required
**R1 — Bind each step to a catalog lane.** Add a `lane: <catalog-id>` field to
every follow-through step. `warden case plan` resolves each step through the
catalog, and it refuses a step whose lane is missing or not reviewed. That is
the T02 acceptance test. Today the steps name their auth only in prose, so
ops-warden cannot tell which lane applies. Two lanes do not exist yet:
| Step | Lane needed | Owner |
| --- | --- | --- |
| `ensure-groups` | identity-provisioner caller credential for `POST /v1/groups/ensure` / `revoke` | net-kingdom |
| `mint-secret` | broker grant that **generates a value in custody** at `{secret_ref}` | railiance-platform |
`key-cape-oidc-login` already covers the attended operator session.
**R2 — Define `warden_executes: true` narrowly.** It should mean: ops-warden
invokes the owner's declared entry point, through the existing caller proxy and
under the caller's identity. It must not mean that ops-warden holds a credential.
For `ensure-groups`, the "identity-provisioner service token … via the ops-warden
proxy" has to be a token the caller obtains through its own lane. If ops-warden
held a standing service token, it would be a broker, which `ADR-0002` rules out.
Please write this definition into the canon field description, not only into
this type.
**R3 — Generated secrets must never pass through the run.** `mint-secret` has to
generate the value inside the broker and custody. It must never be generated in
the run and written to custody afterwards. Add `value_handling: none |
generated_in_custody` per step, and refuse any step that would put a value on
ops-warden's stdout, argv, or receipt (`ADR-0004`, `ADR-0008`).
`custody-allowed-to-consumer` remains a capabilities-only check. That is correct.
## Refinements
**R4 — Per-case catalog entries.** The `catalog-entry` materialization item
(`oidc-client-{client_id}`) creates one route per case. `WARDEN-WP-0044-T04` and
`ADR-0001` point the other way: the type contributes one reviewed, parameterized
entry, and each case adds values, not routes. Proposal: drop `catalog-entry`
from `materialization`, and let the type declare a single `oidc-client` lane
whose path template uses `{client_id}`. The existing hand-built
`rapp-qonto-keycape-client` entry would later become a case of the type.
**R5 — Make ESO delivery a gate.** `bind-env` needs the delivered Secret to
exist, and KeyCape fails closed at start when it does not. Add a per-step
`ready_check` (or a verification item) after `deliver-secret`: ExternalSecret
`sso/keycape-{client_id}-client` reports `Ready=True`. This checks status only
and never reads the value. Without it, the attended session can start before
the Secret has been delivered.
**R6 — Membership comes only from the accepted record.** In `ensure-groups`, the
case controls the members (`initial_members`), and it runs unattended. The run
must pass exactly the memberships in the accepted case record and refuse
anything else. Group membership is an access grant.
**R7 — Missing verifications.** Add `mfa-pinned` (when `mfa_required == "true"`,
the KeyCape registration carries the MFA pin, checked through the non-secret
client view) and `secret-delivered` (R5). Every item keeps
`reads_secret_value: false`.
## Not in scope for ops-warden
`bind-env` and `register-client` are attended, and their owner is net-kingdom.
The justification is accepted: the live registry is a Kubernetes Secret.
ops-warden groups those steps into one `key-cape-oidc-login` session and does
not execute them. `hand-over` belongs to the consumer.
## Addendum 2026-10-09: R4 reconciled with ops-mason item 5
net-kingdom pointed out that R4 conflicts with MASON-WP-0010-T01 item 5
(per-case `{consuming_service}-keycape-client`). On reflection, R4 was wrong
as written. **R4 is withdrawn and replaced with R4′.**
A single parameterized entry would have to carry `workload_ref: not-applicable`,
as the generic `openbao-api-key` template does. That would drop the consumer's
workload reference, and `ADR-0009` needs that reference to select the zone and
PEP failure mode for this exact client. A per-case entry carries real
per-consumer data. It is not a second copy of a procedure, so `ADR-0001` is not
engaged.
**R4′:** keep one catalog entry per case, named
`{consuming_service}-keycape-client` (ops-mason's item 5). Template its
**body** in the type, as item 3 now requires for policies, so that cases add
values and never route design (WARDEN-WP-0044-T04). The body should contain:
- `owner_repo: key-cape`, `warden_executes: false`, `risk: high`,
`status: active` once verification passes
- `workload_ref: {applicability: applicable, name: {consuming_service},
declaration_ref: <consumer declaration>}`. If the consumer has not published
a declaration, the field takes the explicit unknown form, never an inferred one.
- `path_template: platform/workloads/keycape/{client_id}`, field `CLIENT_SECRET`
- `delegation: {mode: native, intended_owner: key-cape}`
- `rotation` steps referencing the type's mint-secret and verification, with
`automatable: false` until a run has been proven
Correction to R4: the existing `rapp-qonto-keycape-client` is a
client-credentials workload client at a different path. It is not a browser
relying party, so it would **not** become a case of this type. Only the naming
precedent carries over.
## Re-check 2026-10-09: revised 1.0 against R1–R7
`warden case check-type` run on the revised type: `schema_ok=True`. R1 (`lane`),
R2 (canon definition of `warden_executes`), R3 (`value_handling`), R4′
(templated `catalog-entry` body), R5 (`ready_check` on `deliver-secret`), R6
(exact memberships in `ensure-groups`) and R7 (`secret-delivered`,
`mfa-pinned`) are all present. Before T02 can plan a run, two lanes must
become reviewed catalog entries: `identity-provisioner-groups` (owner
net-kingdom) and `keycape-client-secret-generate` (owner railiance-platform).
ops-warden has no further required changes to the type.
## Addendum 2026-10-09: R8, `ensure-groups` has no caller-identity path
NK-WP-0048-T08 handed the ops-warden route for the provisioner's service token to
WARDEN-WP-0044-T01. That route cannot be written as the type declares it.
The `ensure-groups` step says `warden_executes: true`, auth "the caller's own
identity-provisioner credential through its lane; ops-warden invokes it via the
caller proxy and holds no token". The deployed provisioner has no per-caller
credential:
- `identity-provisioner/server.py` compares every `POST` bearer against one
`PROVISIONER_SERVICE_TOKEN`
- that token is user-engine's runtime secret
(`platform/workloads/user-engine/runtime`, property `PROVISIONER_TOKEN`,
delivered by `sso-mfa/k8s/user-engine/openbao-runtime.yaml`)
- NetworkPolicy `sso/identity-provisioner` admits port 8080 only from
user-engine pods, plus traefik for the `/setup/password` Ingress
A catalog lane over this would have to fetch user-engine's workload secret and
reach the service from outside its admitted peers. The operator would be acting
as user-engine, not as themselves. That breaks canon §3's narrow
`warden_executes` (caller identity, no standing credential) and ADR-0002. So
ops-warden adds no `identity-provisioner-groups` entry in this form.
**R8:** net-kingdom chooses one of these:
1. **Attended, in the existing session (recommended).** Run `groups ensure` in
the attended session that already runs `register-client`, through the same
contained kubectl (an exec into the provisioner pod, or a contained one-shot
job), with `warden_executes: false`, `attended: true` and lane
`key-cape-oidc-login`. This adds no attended session, because the type
already needs one, and nothing new has to be built.
2. **Give the provisioner a caller credential.** Add an operator-scoped
credential separate from user-engine's, such as an OIDC caller token
verified against KeyCape or an OpenBao-issued per-operator token, plus an
admitted ingress path for it. Then `identity-provisioner-groups` becomes a
real caller-proxy lane and `warden_executes: true` holds. This is more work,
and it is the shape the type describes today.
Until one of these lands, `warden case check-type` keeps reporting the lane
missing, which is correct.

View file

@ -1,202 +0,0 @@
# Review — `oidc-service-client` 0.1 `follow_through` (canon §8 owner review)
Reviewed: net-kingdom `security-case-types/oidc-service-client/` (0.1,
proposed, net-kingdom commit `5d5f15d`): `type.yaml`, `README.md`,
`examples/nk-bind-executor-approval.yaml` and `decision/packet.md` (memo
`nk-wp-0056-t14-oidc-service-client-r1`, hub decision `f8044f81`). Compared
against `oidc-browser-relying-party` 1.1 and this repo's earlier review
(`history/2026-10-08-oidc-rp-follow-through-review.md`, R1–R8, R4′). Also checked
against the routing catalog (`registry/routing/catalog.yaml`), the
`keycape-client-secret-generate` playbook, and railiance-platform
`scripts/keycape_client_secret_generate.py` (source read only).
Reviewer: ops-warden, 2026-10-31. Request: hub message `44400f30`.
**Verdict: change needed before acknowledgement.** The step order, the
value handling, and the custody design are sound, and they follow the browser
type's reviewed shape. A native per-call read with no Secret copy beside the
consumer is better than ESO delivery into the consumer's namespace. Three type
changes (R5, R6, R7) are needed. One external blocker (R1) must be recorded in
the type and memo, because `mint-secret` cannot run today. R8–R10 are
questions for net-kingdom, and the founder should see their answers in memo r2.
## Lanes and value handling
**R1 — `mint-secret` lane does not admit this type yet. Verdict: change needed
(record it as a blocker; the fix belongs to railiance-platform).** The lane
`keycape-client-secret-generate` is active, but the broker behind it refuses
this type in two places:
- `ACKNOWLEDGED_TYPES` contains only `oidc-browser-relying-party@1.1`, pinned by
source hash and parameter fingerprint. Any `oidc-service-client@0.1` case
exits 2 with `case_type_unacknowledged`.
- `check_case` requires `parameters.client_type == "confidential"`. This type
has no `client_type` parameter, because every service client is confidential.
The case therefore exits 2 with `case_client_not_confidential`, even after the
type is admitted.
railiance-platform needs a separately reviewed admission binding for
`oidc-service-client@0.1`. That binding needs:
- the type's pin and parameter fingerprint;
- a confidential check that follows from the type, not from a parameter.
The broker's own comment already asks for this ("a future … entry requires real
acknowledgment and a separately reviewed admission binding"). This should not be
worked around by adding a fixed `client_type` parameter to the type just to
satisfy the check.
Until that binding lands, the type, README and memo should state that
`mint-secret` is blocked on railiance-platform admission. Once railiance-platform
confirms, ops-warden will add this type to the catalog entry's `need_keywords`
and to "Used by" in the playbook.
**R2 — `mint-secret`: `warden_executes: true`, `attended: false`,
`generated_in_custody`. Verdict: ok, with one clarification.** This matches the
narrow canon §3 definition (R2 of the browser review). ops-warden starts the
owner's broker command as a child process in the caller's own environment and
holds no token. The value is generated inside custody, written once with CAS 0,
and verified from metadata only.
The clarification: `attended: false` means "no interactive change to a live
system". It does not mean "no human". The issuer role needs a
`net-kingdom-admins` aal2 OIDC login, and the broker refuses `WARDEN_AGENT_ID`
and root. So under NK-WP-0056, the bounded executor cannot mint its own client
secret. That is correct, and the README should say it.
**R3 — `deliver-secret`. Verdict: ok.** The ESO role is bound to
`external-secrets/external-secrets` only, with a 15m TTL, one policy, and no
default policy. It reads one path, with no metadata or list access. The
`ready_check` reads status only, which matches browser R5. The step has no
`lane` field, the same as browser 1.1. The draft catalog lane
`platform-secret-delivery` (CATALOG-1, 2026-10-17) belongs to the browser type's
1.3 work. Adopt it here only when browser 1.3 does.
**R4 — `bind-env` and `register-client` attended, in one `key-cape-oidc-login`
session. Verdict: ok for 0.1; see R9 for the forward path.** Both justifications
are accepted for the same reasons as the browser type:
- The live client registry is a Kubernetes Secret.
- KeyCape resolves the secret at start and fails closed.
- `bind-env` runs before `register-client` in one session.
- The run goes in a quiet window.
- `keycape check-config` runs before the restart.
ops-warden groups these steps and does not execute them. The type also lists the
consumer's possible actors (another workload in the namespace, a coding agent,
a compromised consumer), and it attends exactly the steps that touch a Secret.
## Catalog entry
**R5 — Rotation in the catalog-entry body points at a lane that refuses
rotation. Verdict: change needed.** The body says `rotation: mint-secret then
verification`, and the README says "Rotation is mint-secret, then the consumer's
next exchange". But `keycape-client-secret-generate` covers first generation
only. It refuses an existing path (exit 3), and its own catalog entry routes
rotation to railiance-platform as a task that does not exist yet.
Change the body to:
- `rotation: {method: rotate, owner: railiance-platform, automatable: false}`;
- a step that says no rotation lane exists yet, and that a rotation is a CAS
write at the custody path plus an attended KeyCape restart (KeyCape reads the
secret at start).
Correct the README limit to match. Writing a lane name that does not exist into
every per-case entry is the failure mode ADR-0001 warns about.
**R6 — Entry name `{consuming_service}-keycape-{client_id}`, and the missing
reader fields. Verdict: ok on the name; change needed on the body.**
*Name.* Adding `{client_id}` is right for this type. The type allows one
audience per case, so one consumer can hold several cases. The browser shape
`{consuming_service}-keycape-client` would collide on the second one. The
example yields `bounded-executor-keycape-nk-bind-executor-approval`, which is
unique and readable. `workload_ref` can point at the published declaration
`net-kingdom/tools/service-identity/declarations/bounded-executor.yaml`, so the
explicit unknown form is not needed for this case.
*Body.* The body omits the fields that tell `warden route` / `warden access`
how the consumer reads the secret, and that there is no human fetch path. Add:
- `lane: secret`
- `auth_method: "Kubernetes auth role workload-kv-read-keycape-{client_id}-{consuming_service}, ServiceAccount {consumer_namespace}/{consumer_service_account}, audience openbao, 15m; KeyCape reads through ESO role external-secrets-keycape-{client_id}"`
- `exec_capable: false` and no `fetch_command`. Only the workload reads the
secret. Without this, a catalog consumer could infer an operator
`bao kv get` route, the way `rapp-qonto-keycape-client` has one.
- `need_keywords` drawn from the case: `{consuming_service}`, `{client_id}`,
`{audience}`, `client-credentials`, `service-token`. Keep them case-specific,
as with the `keycape-client-secret-generate` lesson: broad phrases outrank
named lanes.
- `subsystem: KeyCape + OpenBao`
`owner_repo: key-cape` and `delegation: native, intended_owner key-cape` are
consistent with browser R4′. Keep them.
## Verification
**R7 — The claim "no Secret copy in the consumer's namespace" does not hold for
the first case, and nothing verifies it. Verdict: change needed.** In
`nk-bind-executor-approval`, `consumer_namespace` is `sso`. That is the same
namespace where `deliver-secret` writes `sso/keycape-{client_id}-client` for
KeyCape. So the copy does exist beside this consumer. It is acceptable only
because ServiceAccount `sso/nk-bind-executor` has no Secret access: its Role
covers `deployments/authelia` and `externalsecrets/authelia-secrets` only. The
type relies on that fact but never checks it. Add:
- verification `consumer-cannot-read-delivered-secret`:
`kubectl auth can-i get secrets/keycape-{client_id}-client -n sso --as system:serviceaccount:{consumer_namespace}:{consumer_service_account}`
must return `no`. Also check `list` and `watch` on secrets in `sso`. Set
`expect: deny` and `reads_secret_value: false`.
- in the README threat model, change "no Secret copy in the consumer's
namespace" to "the consumer has no Secret read anywhere; when
`consumer_namespace` is `sso`, the KeyCape copy is beside it and is protected
by RBAC alone".
**R8 — `service-token-claims` is reported by the consumer. Verdict: question.**
"Claims only; the verifier reads no secret and no token" is the right boundary,
and ops-warden accepts consumer-reported claims as evidence. But the consumer is
also the party under test. Two questions:
1. Can a second, consumer-independent check be added that reads the non-secret
KeyCape client view in the attended `register-client` session? It would
confirm `grant_types` = `client_credentials` only, no redirect URIs, and that
`serviceSubject`, `tenant`, `audience`, `scopes` and lifetime match the case.
The browser type does this with `mfa-pinned`.
2. Is `keycape verify-client` (key-cape `docs/native-authentication.md`) the
intended owner command for `service-token-claims` and `excess-scope-refused`?
It already reports a positive exchange and excess-scope denial without
printing tokens. If so, name it as the entry point, so the consumer's report
has a fixed shape. For the bounded executor, also say whether these test
exchanges need their own admission. Under NK-WP-0056 the executor acts only
under a consumed admission.
## Ownership and forward path
**R9 — Attended lane: `key-cape-oidc-login` or `keycape-attended-case-change`.
Verdict: question.** Since 2026-10-17 (OW-1.3-C1) the catalog has had a draft
`keycape-attended-case-change` lane, with `key-cape-oidc-login` as its
`session_lane`. It separates the change authority from the login. 0.1 correctly
follows the admitted browser 1.1. Please confirm that 0.1 moves to the new lane
in step with the browser type once that lane is promoted, and not on its own.
**R10 — `hand-over` owner. Verdict: question.**
`sso-mfa/k8s/bounded-executor/rbac.yaml` says the executor's packaging and apply
belong to `rapp-bounded-executor` (ADR-0015, founder decision `6d1adce9`). The
first case sets `consumer_owner_repo: net-kingdom`. If the consumer's
configuration (issuer, client, audience, OpenBao role) ships in the rapp, then
`consumer_owner_repo` should be `rapp-bounded-executor`. It names both the
`hand-over` owner and the rollback owner.
## Not in scope for ops-warden
Not reviewed here:
- `materialization` of the policies and roles: railiance-platform CCRs, as for
the browser type;
- the scope set `approval:create,approval:read,approval:consume`, and whether
one workload should both create and consume approvals: approval-engine and the
founder decide that;
- the `audience`-to-scope check named under "Known limits".
The rollback order is sound: KeyCape before custody, roles before policies, and
the ExternalSecret before the path.

View file

@ -1,164 +0,0 @@
# 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: closed
outcome: answered-mirror
resolution: 'Routing and the delegation facts STAY; maturity-engine MIRRORS read-only via warden route gaps --json; assent to the SSH-CA gap being tracked there. Answered 2026-09-30.'
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"
```
## WARDEN-IN-0003 — FLEX-WP-0020 handoff: flex-auth → access-engine repository rename, credential-route surface
```yaml
id: WARDEN-IN-0003
kind: intake
title: 'FLEX-WP-0020 handoff: flex-auth to access-engine repository rename, credential-route
surface'
status: closed
outcome: verified-no-change
resolution: 'Rename landed 2026-09-30; warden route find flex-auth-policy-check resolves under both names, no catalog change needed (e6ef060); owner_repo stays flex-auth per FLEX-DEC-2026-013.'
origin: cross-repo
origin_ref: flex-auth FLEX-WP-0020 (message 0a1956c5)
priority: medium
owner: ops-warden
requested_by: flex-auth
surface: credential-route
description: 'flex-auth is preparing the repository-coordinate rename flex-auth ->
access-engine. The repository UUID fda8ad85-a7d7-4055-8f21-902a533e59df and Forge
ID 42 are unchanged; RUNTIME names stay flex-auth per FLEX-DEC-2026-013. The ask:
review registry/routing/catalog.yaml''s owner repository field so routing still
resolves after the rename, create a live record on this side, and reply with its
id. No secret requested, none returned. The message is explicitly not completion
evidence.'
notes: 'REVIEWED 2026-09-21; one change made, one deliberately not made.
SCOPE. Exactly one catalog lane names flex-auth as owner_repo: flex-auth-policy-check
(authorization decision — may this actor perform this action), warden_executes false,
route-only, no credential flows. Two other lanes mention flex-auth without owning
anything by it: flex-auth-decision-envelope-signing-key is owned by railiance-platform,
and ten lanes carry a policy_ref string of the form "flex-auth check secret.read:<domain>".
CHANGED. access-engine added to flex-auth-policy-check''s need_keywords, so the lane
resolves under both names from today rather than from the rename. That is the
deprecation window ops-warden asked for in WARDEN-IN-0001 and is the only change
routing needs: warden route find matches on keywords and lane id, neither of which is
a repository coordinate.
NOT CHANGED, and this is the finding worth having. owner_repo is NOT purely a
repository coordinate in this catalog. src/warden/policy.py sends it verbatim as
resource.system, and again as context.owner_repo, on every flex-auth /v1/check.
FLEX-DEC-2026-013 keeps runtime names as flex-auth, so flipping owner_repo at rename
time would rename a policy resource the PDP matches on, under cover of a repository
rename — a runtime change wearing a coordinate change''s clothes, and one that would
fail at the check rather than at the edit. It flips when the runtime name flips, not
when the repository does. If access-engine intends resource.system to follow the
repository rather than the runtime, that is a policy question for flex-auth to answer
before the rename lands, not an inference for ops-warden to make from its own catalog.
The lane id flex-auth-policy-check, the subsystem string, and the policy_ref strings
are runtime-facing for the same reason and are unchanged.
Related and unresolved: WARDEN-WP-0039-T03 already waits on the credential proxy''s
admitted policy binding, which is the same resource.system field seen from the
delegated-read side. The rename must not be allowed to answer that question by
accident.
RENAME LANDED 2026-09-30 (flex-auth msg 2026-09-30; access-engine head b943d9a).
Verified after the landing: warden route find resolves flex-auth-policy-check under
both "access-engine" and "flex-auth" keywords; no catalog edit was required. The
open question above is answered by FLEX-DEC-2026-013 (runtime names, including the
policy resource.system string, stay flex-auth), so owner_repo/subsystem stay
flex-auth until the runtime name changes. If flex-auth intends otherwise it must
say so explicitly. Intake stays open for the owner to close.'
created: '2026-09-21T00:00:00Z'
updated: '2026-09-30T00:00:00Z'
state_hub_intake_id: "01a0c153-98e1-7705-bf82-a58c85df980f"
```

View file

@ -1,156 +0,0 @@
# ops-warden — DERIVED form of the NetKingdom security layer declaration
#
# THIS FILE DOES NOT GOVERN. The declaration is the `layer:` key in INTENT.md's
# frontmatter; this file is a derived artifact under §11's derived-artifact rule
# and must agree with it (GH-DEC-2026-017 §1, amendment A11). A disagreement
# between the two forms is a finding in its own right and is reported, never
# resolved away by precedence.
#
# Framework: net-kingdom/canon/standards/security-layer-model
# 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.2"
framework: netkingdom-security-layer-model
# §11 derived-artifact marking (GH-DEC-2026-017 §1 / A11).
derived: true
derived_from: INTENT.md
# NO `standard_version` KEY, AND DO NOT ADD ONE BACK.
# GH-DEC-2026-017 §5 / amendment A12: a layer declaration MUST NOT carry a
# standard version. The declared layer is a standing property of the repository
# and does not change when the standard is revised; a version here makes every
# revision read as though it invalidated every declaration. Version-scoped state
# belongs in the derived conformance record, which already must carry the
# version or commit it was derived at. Keeping the field "for information" was
# declined explicitly: a field that is present will be branched on. Absence is
# enforced by tests/test_layer_conformance.py.
repository: ops-warden
# RULED, 2026-09-21 (GH-DEC-2026-017 §2): INTENT.md declares `Staff` and this
# file declares `staff`, and THAT IS NOT A DISAGREEMENT. §3's vocabulary is
# closed — {Taxonomy, Tooling, Engine, Staff} — and comparison is ASCII
# case-insensitive: a conformance run MUST fold case before comparing, and a
# lowercase declaration is conforming, not tolerated. No repository was asked to
# re-spell anything. Do not "fix" this line to match INTENT.md, or the reverse;
# folding case is the checker's job, and it does it.
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.

View file

@ -1,160 +0,0 @@
# ops-warden — PEP unreachable-engine stance map
#
# Framework: net-kingdom/canon/standards/security-layer-model_v0.7.md §6.4, §9.3, §9.7
# 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
# v0.7 is the accepted standard and the one in force. v0.8 is `proposed`: its
# assent round closed 2026-09-09 (GH-DEC-2026-011) with nine corrections applied,
# but the version is not yet accepted, so this declaration stays pinned to what
# binds. The one cell v0.8 would change is marked inline below rather than
# silently pre-adopted.
standard_version: "0.7"
standard_version_reviewed: "0.8" # reviewed and assented; see history/2026-09-09-layer-model-v08-review.md
repository: ops-warden
pep_shape: true
declared_by: docs/adr/ADR-0009
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
# DECLARED GAP under security-layer-model v0.8 §6.4 obligation 3 (GH-DEC-2026-011,
# net-kingdom@64394e9): unknown is not a zone and MUST resolve to fail_closed.
# ops-warden ASSENTED to that rule — we went looking for the §5.1 read-only
# diagnostic its reversal clause predicts and do not have one, because this map
# governs `warden sign`, a credential-issuing side effect.
#
# The cell has not been flipped, and the reason is measured rather than argued:
# 0 of 3 signing targets resolve to a zone, so converting today would fail closed
# on essentially every certificate whenever the engine is unreachable — including
# the certificate an operator needs to reach the host and repair it. That is
# ADR-0006's rejected configuration reached from another direction.
#
# We asked for a coverage-gated transitional fail_open and were DECLINED: a
# sanctioned transitional fail_open is indistinguishable at runtime from the
# stance the rule forbids. Our second preference was adopted instead — §13.1 now
# carries a Coverage column, and this repo's figures are its first entries.
#
# So this is tracked non-conformance with a route, not an exemption.
# Route: WARDEN-WP-0040. Register row: §13.1, marked.
unknown: fail_open # non-conformant at v0.8; see above
not-applicable: fail_closed
# What happens when the stance is applied. §6.4 obligation 1 requires a decision
# 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.
# Classification coverage, published beside the stance because v0.8 §6.4
# obligation 3 now requires it and because ops-warden asked for the column.
# Coverage is DISCLOSURE, never a transitional licence: it does not soften this
# map's stance, does not gate it, and never makes the marked cell conformant.
# Self-measured; regenerate with `python scripts/report_coverage.py`.
# 2026-09-21 re-measure: routing lanes grew from 30 to 38 catalogued entries
# since 2026-09-09, so unknown rose 18 -> 20 and not-applicable 12 -> 15 while
# RESOLVED STAYED AT 3. Classification did not regress; the denominator moved.
# Signing targets are unchanged. Reported to gate-house per §13.1 — a figure in
# a register that cannot detect its own staleness is only as good as the last
# re-measure, and this one was stale by eight lanes before the test caught it.
# 2026-10-05: inbound-library-imap-approle added as an applicable lane without a
# published workload declaration, so routing unknown rose 20 -> 21; resolved unchanged.
# 2026-10-08: platform-supplier-credentials added as a not-applicable path pattern,
# so routing not_applicable rose 16 -> 17.
# 2026-10-20: agt-custodian-ssh-cert added as a not-applicable ceremony lane
# (WARDEN-WP-0045), so routing not_applicable rose 22 -> 23; signing target
# agt-custodian names workload custodian-agents without a published resolution,
# so signing unknown rose 3 -> 4.
classification_coverage:
measured: "2026-10-20"
attribution: self-measured
signing_targets:
resolved: 0
unknown: 4
not_applicable: 1
routing_lanes:
resolved: 4
unknown: 22
not_applicable: 23
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
# §9.7.2 — the certificate-verification PEP has no active revocation channel.
# A previously allowed certificate remains usable until its enforced TTL expires,
# so the visibility deadline is exactly the maximum issued lifetime per actor type.
revocation_visibility:
boundary: "SSH certificate verification at the target host"
mechanism: ttl_expiry
revocation_channel: none
deadline_hours:
adm: 48
agt: 24
atm: 8
note: >-
No CRL or KRL distribution exists. Host-side authorization belongs to
railiance-infra; shortening or actively recalling this window is a joint
design question, not an implied capability of ops-warden.
# §9.6 — signing evidence is attributive, not load-bearing. Measurement as of
# 2026-09-04 found three signature records across 79 calendar days, concentrated
# on only two active days (1 event on 2026-06-17; 2 within 40 minutes on
# 2026-08-22). That bursty operator-driven sample cannot support a useful rate
# threshold, so cadence is explicitly deferred instead of fabricated.
emission_cadence:
classification: attributive
status: deferred
measured_as_of: "2026-09-04"
observed_window:
first: "2026-06-17T23:18:33Z"
last: "2026-09-04T21:21:30Z"
signature_records: 3
active_signature_days: 2
daily_counts:
"2026-06-17": 1
"2026-08-22": 2
reason: "operator-driven issuance is too sparse and bursty for a meaningful rate threshold"

View file

@ -1,104 +0,0 @@
---
id: capability.security.access-routing
owner: ops-warden
status: draft
domain: infotech
maturity:
discovery:
current: D3
target: D5
confidence: medium
rationale: Owner scope, neighboring responsibilities, concrete use cases and limitations
are documented; no exhaustive or generalized maturity claim.
availability:
current: A3
target: A3
confidence: medium
rationale: 'Source interface assessed 2026-10-14: warden route and access catalog;
exact lane conduit and secret-exec routing. Availability describes the delivery
form, not production acceptance. Routing grants no authority; interim delegates
and attended high-risk lanes persist.'
external_evidence:
completeness:
level: C1
confidence: low
basis: Source artifacts establish a bounded capability; no independent consumer-completeness
assessment in this review.
reliability:
level: R0
confidence: low
basis: No longitudinal consumer reliability measurement established by this review.
name: Credential routing and contained access
summary: warden route and access catalog; exact lane conduit and secret-exec routing.
tags:
- security
- netkingdom
- framework-reuse
discovery:
intent: warden route and access catalog; exact lane conduit and secret-exec routing
includes:
- warden route and access catalog; exact lane conduit and secret-exec routing
excludes:
- Routing grants no authority; interim delegates and attended high-risk lanes persist
use_cases:
- 'U04: Protected operation obtains and consumes exact human approval before its
side effect'
- 'U05: Operator provisions and delivers a scoped workload credential through the
approved owner lane'
- 'U06: Operator or automation obtains a bounded SSH certificate and maintains private
connectivity'
availability:
current_level: A3
target_level: A3
current_artifacts:
- SCOPE.md
- wiki/CredentialRouting.md
consumption_modes:
- library
- CLI
relations:
related_to:
- capability.security.credential-lifecycle
- capability.authorization.policy-evaluate
- capability.ops.tunnel-bridge
- capability.security.structure-construction
evidence:
documentation:
- SCOPE.md
- wiki/CredentialRouting.md
consumer_guidance:
recommended_for:
- Protected operation obtains and consumes exact human approval before its side
effect
- Operator provisions and delivers a scoped workload credential through the approved
owner lane
- Operator or automation obtains a bounded SSH certificate and maintains private
connectivity
not_recommended_for:
- Routing grants no authority; interim delegates and attended high-risk lanes persist
known_limitations:
- Routing grants no authority; interim delegates and attended high-risk lanes persist
- 2026-10-14 architecture review estimates operational maturity separately; a registry
entry does not authorize execution or establish deployment.
---
# Credential routing and contained access
warden route and access catalog; exact lane conduit and secret-exec routing.
## Reuse boundary
Routing grants no authority; interim delegates and attended high-risk lanes persist.
## Supported use cases
- U04: Protected operation obtains and consumes exact human approval before its side effect
- U05: Operator provisions and delivers a scoped workload credential through the approved owner lane
- U06: Operator or automation obtains a bounded SSH certificate and maintains private connectivity
## Evidence and integration
- [SCOPE.md](../../SCOPE.md)
- [wiki/CredentialRouting.md](../../wiki/CredentialRouting.md)
Assessment: CUST-WP-0081, 2026-10-14. Relations identify interfaces to compose; they do not certify native integration. Existing external evidence levels are retained where present. New entries use conservative C1/R0 until consumer assessment exists.

View file

@ -1,104 +0,0 @@
---
id: capability.security.case-follow-through
owner: ops-warden
status: draft
domain: infotech
maturity:
discovery:
current: D3
target: D5
confidence: medium
rationale: Owner scope, neighboring responsibilities, concrete use cases and limitations
are documented; no exhaustive or generalized maturity claim.
availability:
current: A2
target: A2
confidence: medium
rationale: 'Source interface assessed 2026-10-14: KnownSecurityCase route, plan
and operational receipts. Availability describes the delivery form, not production
acceptance. Execution requires accepted structure and exact owner contracts;
case closure is not automatic.'
external_evidence:
completeness:
level: C1
confidence: low
basis: Source artifacts establish a bounded capability; no independent consumer-completeness
assessment in this review.
reliability:
level: R0
confidence: low
basis: No longitudinal consumer reliability measurement established by this review.
name: Security case operational follow-through
summary: KnownSecurityCase route, plan and operational receipts.
tags:
- security
- netkingdom
- framework-reuse
discovery:
intent: KnownSecurityCase route, plan and operational receipts
includes:
- KnownSecurityCase route, plan and operational receipts
excludes:
- Execution requires accepted structure and exact owner contracts; case closure
is not automatic
use_cases:
- 'U01: Architect composes a security deployment from explicit owner contracts and
case types'
- 'U05: Operator provisions and delivers a scoped workload credential through the
approved owner lane'
- 'U06: Operator or automation obtains a bounded SSH certificate and maintains private
connectivity'
availability:
current_level: A2
target_level: A2
current_artifacts:
- workplans/WARDEN-WP-0044-security-case-follow-through.md
consumption_modes:
- CLI
- source
relations:
related_to:
- capability.security.credential-lifecycle
- capability.authorization.policy-evaluate
- capability.ops.tunnel-bridge
- capability.security.structure-construction
evidence:
documentation:
- workplans/WARDEN-WP-0044-security-case-follow-through.md
consumer_guidance:
recommended_for:
- Architect composes a security deployment from explicit owner contracts and case
types
- Operator provisions and delivers a scoped workload credential through the approved
owner lane
- Operator or automation obtains a bounded SSH certificate and maintains private
connectivity
not_recommended_for:
- Execution requires accepted structure and exact owner contracts; case closure
is not automatic
known_limitations:
- Execution requires accepted structure and exact owner contracts; case closure
is not automatic
- 2026-10-14 architecture review estimates operational maturity separately; a registry
entry does not authorize execution or establish deployment.
---
# Security case operational follow-through
KnownSecurityCase route, plan and operational receipts.
## Reuse boundary
Execution requires accepted structure and exact owner contracts; case closure is not automatic.
## Supported use cases
- U01: Architect composes a security deployment from explicit owner contracts and case types
- U05: Operator provisions and delivers a scoped workload credential through the approved owner lane
- U06: Operator or automation obtains a bounded SSH certificate and maintains private connectivity
## Evidence and integration
- [workplans/WARDEN-WP-0044-security-case-follow-through.md](../../workplans/WARDEN-WP-0044-security-case-follow-through.md)
Assessment: CUST-WP-0081, 2026-10-14. Relations identify interfaces to compose; they do not certify native integration. Existing external evidence levels are retained where present. New entries use conservative C1/R0 until consumer assessment exists.

View file

@ -1,38 +1,39 @@
---
id: capability.security.ssh-certificate-issuance
name: Decision-bound SSH certificates
summary: warden sign with local and OpenBao backends and adm/agt/atm actors.
name: SSH Certificate Issuance
summary: Issue short-lived CA-signed SSH certificates for adm, agt, and atm actors through a stable cert_command CLI interface; steward operational access routing across NetKingdom security lanes.
owner: ops-warden
status: draft
domain: helix_forge
tags:
- ssh
- certificate
- ca
- ops-warden
- openbao
- security
- netkingdom
- framework-reuse
- ssh
- certificate
- ca
- ops-warden
- openbao
- security
maturity:
discovery:
current: D4
target: D5
confidence: medium
rationale: 'SCOPE, AccessManagementDirective alignment, config runbooks, and cert_command
contract are documented; production OpenBao integration is documented but engine
deployment lives in railiance-platform. A machine-readable routing catalog (registry/routing/catalog.yaml)
and wiki/AccessRouting.md make the "issue SSH, route the rest" boundary discoverable.
'
rationale: >
SCOPE, AccessManagementDirective alignment, config runbooks, and cert_command
contract are documented; production OpenBao integration is documented but
engine deployment lives in railiance-platform. A machine-readable routing
catalog (registry/routing/catalog.yaml) and wiki/AccessRouting.md make the
"issue SSH, route the rest" boundary discoverable.
availability:
current: A3
target: A5
confidence: medium
rationale: 'Source interface assessed 2026-10-14: warden sign with local and OpenBao
backends and adm/agt/atm actors. Availability describes the delivery form, not
production acceptance. Direct Tooling contact is a declared gap; certificate
reachability is not target authorization.'
rationale: >
Installable `warden` CLI and `ops-ssh-wrapper` entry points; ops-bridge and
other callers integrate via cert_command without backend-specific branching.
A `warden route` lookup over the pointer catalog (WARDEN-WP-0011) will move
routing discovery from wiki prose to a structured surface for agents (A3 -> A4).
external_evidence:
completeness:
level: C3
@ -40,100 +41,97 @@ external_evidence:
confidence: medium
basis: scope_vs_intent_and_consumer_expectations
satisfied_expectations:
- local and OpenBao/Vault-compatible signing backends
- TTL policy enforcement per actor type
- principals inventory and cert-side scorecard
- signatures audit log and stale-cert cleanup
- cert_command stdout contract for ops-bridge
- per-actor extension allow-list and max_ttl_hours ceiling (WARDEN-WP-0045)
- local and OpenBao/Vault-compatible signing backends
- TTL policy enforcement per actor type
- principals inventory and cert-side scorecard
- signatures audit log and stale-cert cleanup
- cert_command stdout contract for ops-bridge
broken_expectations:
- host-side principal deployment not owned here
- OpenBao SSH engine mount not deployed from this repo
- host-side principal deployment not owned here
- OpenBao SSH engine mount not deployed from this repo
out_of_scope_expectations:
- long-lived API key custody
- tunnel lifecycle management
- Vault/OpenBao cluster operations
- long-lived API key custody
- tunnel lifecycle management
- Vault/OpenBao cluster operations
reliability:
level: R2
name: Tolerable
confidence: medium
basis: consumer_quality_signals
known_reliability_risks:
- production signing depends on OpenBao availability and token policy
- local backend requires protected CA key handling by operators
- production signing depends on OpenBao availability and token policy
- local backend requires protected CA key handling by operators
discovery:
intent: warden sign with local and OpenBao backends and adm/agt/atm actors
intent: >
Give the ops fleet short-lived SSH credentials for humans, agents, and
automations without static keys, through a single cert_command surface that
callers can rely on regardless of CA backend; route non-SSH credential needs
to the correct NetKingdom subsystems (OpenBao, flex-auth, key-cape).
includes:
- warden sign with local and OpenBao backends and adm/agt/atm actors
- certificate signing for adm, agt, and atm actors
- actor principals inventory and TTL policy
- cert_command interface (`warden sign`)
- cert-side compliance scorecard and signatures log
- ops-ssh-wrapper for automatic cert acquisition
- NetKingdom credential routing and alignment documentation
- machine-readable routing pointer catalog (registry/routing/catalog.yaml)
excludes:
- Direct Tooling contact is a declared gap; certificate reachability is not target
authorization
- tunnel lifecycle
- host /etc/ssh/auth_principals deployment
- OpenBao or Vault cluster setup
- long-lived secret storage
assumptions:
- callers supply actor public keys; humans self-issue admin keys
- production platform uses OpenBao with Vault-compatible SSH engine API
- callers supply actor public keys; humans self-issue admin keys
- production platform uses OpenBao with Vault-compatible SSH engine API
use_cases:
- ops-bridge tunnel cert_command
- Inter-Hub bootstrap short-lived agent access
- 'U06: Operator or automation obtains a bounded SSH certificate and maintains private
connectivity'
- ops-bridge tunnel cert_command
- Inter-Hub bootstrap short-lived agent access
research_memos:
- ops-warden/SCOPE.md
- ops-warden/wiki/CertCommandInterface.md
- ops-warden/wiki/OpsWardenConfig.md
- ops-warden/wiki/AccessRouting.md
- ops-warden/SCOPE.md
- ops-warden/wiki/CertCommandInterface.md
- ops-warden/wiki/OpsWardenConfig.md
- ops-warden/wiki/AccessRouting.md
availability:
current_level: A3
target_level: A5
current_artifacts:
- SCOPE.md
- README.md
- ops-warden/src/warden/
- ops-warden/wiki/CertCommandInterface.md
- ops-warden/wiki/OpsWardenConfig.md
target_artifacts:
- packaged ops-warden release with documented OpenBao role bootstrap
- '`warden route` lookup CLI over the pointer catalog (WARDEN-WP-0011)'
- packaged ops-warden release with documented OpenBao role bootstrap
- "`warden route` lookup CLI over the pointer catalog (WARDEN-WP-0011)"
consumption_modes:
- library
- CLI
- CLI
- cert_command subprocess
relations:
depends_on: []
supports: []
related_to:
- capability.security.credential-lifecycle
- capability.authorization.policy-evaluate
- capability.ops.tunnel-bridge
- capability.security.structure-construction
related_to: []
consumer_guidance:
recommended_for:
- Operator or automation obtains a bounded SSH certificate and maintains private
connectivity
- issuing short-lived SSH certs for ops-bridge tunnels
- agent or automation access with TTL-bound principals
- checking cert-side compliance before rotation windows
- orienting dev workers on which NetKingdom subsystem owns each credential type
not_recommended_for:
- Direct Tooling contact is a declared gap; certificate reachability is not target
authorization
- storing OpenRouter or Inter-Hub API keys
- replacing OpenBao deployment or host SSH hardening playbooks
- static-key-only legacy access (use ops-bridge static key mode instead)
known_limitations:
- Direct Tooling contact is a declared gap; certificate reachability is not target
authorization
- 2026-10-14 architecture review estimates operational maturity separately; a registry
entry does not authorize execution or establish deployment.
evidence:
documentation:
- SCOPE.md
- README.md
- "VaultCA backend config key remains backend: vault for API compatibility"
- host-side scorecard checks live in railiance-infra
---
# Decision-bound SSH certificates
# SSH Certificate Issuance
warden sign with local and OpenBao backends and adm/agt/atm actors.
ops-warden is the custodian-domain SSH CA tool. It signs short-lived certificates,
maintains the actor inventory, and exposes `warden sign` as the cert_command
contract for ops-bridge and other callers.
## Reuse boundary
Direct Tooling contact is a declared gap; certificate reachability is not target authorization.
## Supported use cases
- U06: Operator or automation obtains a bounded SSH certificate and maintains private connectivity
## Evidence and integration
- [SCOPE.md](../../SCOPE.md)
- [README.md](../../README.md)
Assessment: CUST-WP-0081, 2026-10-14. Relations identify interfaces to compose; they do not certify native integration. Existing external evidence levels are retained where present. New entries use conservative C1/R0 until consumer assessment exists.
Production environments point the vault-compatible backend at OpenBao; labs use
the local ssh-keygen CA backend without platform dependencies.

View file

@ -108,31 +108,6 @@
"security_zone_reason": "workload_resolution_absent"
}
},
{
"id": "ssh-cert:actor/agt-custodian",
"type": "ssh-certificate",
"labels": [
"ssh-signing",
"agt"
],
"owner": "team:platform-security",
"attributes": {
"actor_id": "agt-custodian",
"actor_type": "agt",
"allowed_subjects": [
"agt-custodian",
"iam:agt-custodian"
],
"allowed_principals": [
"agt-custodian"
],
"max_ttl_hours": 12,
"workload_id": "custodian-agents",
"security_zone": "unknown",
"security_zone_admission": "unknown",
"security_zone_reason": "workload_resolution_absent"
}
},
{
"id": "ssh-cert:actor/agt-state-hub-bridge",
"type": "ssh-certificate",
@ -179,10 +154,8 @@
"max_ttl_hours": 8,
"workload_id": "backup-daily",
"security_zone": "unknown",
"security_zone_declared": "z2-protected",
"security_zone_admission": "unsatisfied",
"security_zone_reason": "M1_below_M2_zone_floor",
"security_zone_revision": "sha256:4b937fce814a937a034f63598f4c7f4d8a76cf92e82cada752ae31dbe5f3dc8f"
"security_zone_admission": "unknown",
"security_zone_reason": "workload_resolution_absent"
}
}
],
@ -235,22 +208,6 @@
"actor_type": "agt"
}
},
{
"id": "agt-custodian",
"type": "Agent",
"display_name": "Supervised interactive custodian agent (custodian-agt on Railiance01)",
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
],
"groups": [
"group:ops-warden-agents"
],
"tenant": "tenant:platform",
"metadata": {
"actor_type": "agt"
}
},
{
"id": "agt-state-hub-bridge",
"type": "Agent",
@ -298,7 +255,6 @@
"display_name": "Ops Warden Agents",
"members": [
"agt-codex-interhub-bootstrap",
"agt-custodian",
"agt-state-hub-bridge"
],
"tenant": "tenant:platform"
@ -407,53 +363,6 @@
"access_path": "mediated"
}
},
{
"id": "rel:agt-custodian-sign-agt-custodian",
"system": "ops-warden",
"subject": "group:ops-warden-agents",
"relation": "signer",
"object": "ssh-cert:actor/agt-custodian",
"tenant": "tenant:platform",
"conditions": [
"TimeLimited",
"Logged"
],
"caring": {
"id": "descriptor:ops-warden-agt-signer",
"profile": "caring-0.4.0-rc2",
"subject_type": "Group",
"organization_relation": "ServiceProvider",
"canonical_role": "Operator",
"scope": {
"level": "Resource",
"id": "ssh-cert:actor/agt-custodian",
"tenant": "tenant:platform",
"resource": "ssh-cert:actor/agt-custodian"
},
"planes": [
"Identity",
"Secret",
"Audit"
],
"capabilities": [
"Use",
"Operate",
"Audit"
],
"exposure_modes": [
"Metadata"
],
"conditions": [
"TimeLimited",
"Logged"
],
"restrictions": [
"PrivilegeEscalationBlocked",
"SecretAccessBlocked"
],
"access_path": "mediated"
}
},
{
"id": "rel:agt-state-hub-bridge-sign-agt-state-hub-bridge",
"system": "ops-warden",

View file

@ -1,75 +0,0 @@
{
"changes": {
"added": [
"backup-daily"
],
"baseline": "initial",
"changed": [],
"removed": []
},
"errors": [],
"ok": true,
"profile_errors": [],
"records": [
{
"admission": "unsatisfied",
"admission_reason": "M1_below_M2_zone_floor",
"control_profile": {
"id": "netkingdom-build-v0.1",
"version": "flex-auth@e521e7b+ops-warden@7ce58ae"
},
"controls": [
{
"failure_mode": "fail_open",
"id": "flex-auth/pre-sign",
"pep_owner": "ops-warden",
"policy_owner": "flex-auth",
"policy_ref": "flex-auth@e521e7b:examples/ops-warden/policy_package.md",
"stance": "advisory"
},
{
"failure_mode": "fail_closed",
"id": "ops-warden/agent-high-risk-read",
"pep_owner": "ops-warden",
"policy_owner": "ops-warden",
"policy_ref": "ops-warden@7ce58ae:src/warden/routing/models.py",
"stance": "enforced"
},
{
"failure_mode": "fail_closed",
"id": "ops-warden/plan-zone-rule",
"pep_owner": "ops-warden",
"policy_owner": "ops-warden",
"policy_ref": "security-zones_v0.1@104d5c6:section-5.1",
"stance": "enforced"
}
],
"declared_zone": "z2-protected",
"effective_zone": "unknown",
"guarantees": [
"authoritative-workload-identity",
"explicit-workload-reference",
"explicit-zone-membership",
"non-inferred-resolution",
"source-revision-bound-membership"
],
"identity_bindings": [
"kubernetes-service-account/railiance01/system:serviceaccount:activity-core:default",
"ssh-certificate/ops-warden/atm-backup-daily"
],
"membership_revision": "sha256:4b937fce814a937a034f63598f4c7f4d8a76cf92e82cada752ae31dbe5f3dc8f",
"membership_revision_reason": "source_bound",
"source": "activity-core/declarations/backup-daily/tenancy.yaml",
"source_revision": "activity-core@fd84f2b6518ff17509930499099ec2a6692d088c",
"subject_id": "backup-daily",
"workload_id": "backup-daily",
"workload_ref": {
"applicability": "applicable",
"deployable": null,
"name": "backup-daily",
"rapp_id": null
}
}
],
"standard": "security-zones_v0.1"
}

View file

@ -10,39 +10,26 @@
# declares it, and is null where the field set has not been established --
# null means unknown, never 'one field'.
generated_at: "2026-10-22T06:53:51Z"
generated_at: "2026-08-26T19:56:48Z"
source: ops-warden/registry/routing/catalog.yaml
catalog_revision: "537d551ef28c87def9043e65e316ab4f64f5d9d6"
catalog_revision_date: "2026-10-22T08:53:33+02:00"
catalog_revision: "3a4333d850295d2bed39136bfe53f786564eec03"
catalog_revision_date: "2026-08-26T21:56:44+02:00"
catalog_dirty: false
high_risk_lane_count: 40
concrete_path_count: 17
high_risk_lane_count: 24
concrete_path_count: 14
# 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:
- apps-consumer-handover
- consumer-owner-deployment
- database-dynamic-credentials
- flex-auth-decision-envelope-signing-key
- fluid-telegram-operator
- informed-decision-sitting-requester-login
- inter-hub-bootstrap-ssh
- keycape-attended-case-change
- keycape-client-secret-generate
- kubernetes-railiance01-admin
- net-kingdom-lldap-bind-credential
- net-kingdom-privacyidea-admin-token
- object-storage-sts
- openbao-api-key
- openbao-platform-admin-login
- openbao-shamir-recovery-ceremony
- openbao-structure-survey
- ops-warden-warden-sign-token
- platform-secret-delivery
- platform-supplier-credentials
- secrets-engine-approval-client-login
- secrets-engine-requester-login
- state-hub-forge-derivation-read
paths:
@ -71,11 +58,6 @@ paths:
metadata_path: tenants/metadata/binky/qonto-api
owner_repo: railiance-platform
fields: null # field set not established -- unknown, not one
- id: core-hub-forgejo-source-read
data_path: platform/data/workloads/core-hub/forgejo-source-read
metadata_path: platform/metadata/workloads/core-hub/forgejo-source-read
owner_repo: railiance-platform
fields: [FORGEJO_SOURCE_TOKEN, API_USER, API_BASE_URL, TOKEN_SCOPES, GENERATED_AT]
- id: email-connect-transactional
data_path: platform/data/workloads/email-connect/transactional
metadata_path: platform/metadata/workloads/email-connect/transactional
@ -86,11 +68,6 @@ paths:
metadata_path: platform/metadata/workloads/forgejo/forgejo-admin
owner_repo: railiance-platform
fields: null # field set not established -- unknown, not one
- id: inbound-library-imap-approle
data_path: platform/data/workloads/inbound-library/imap
metadata_path: platform/metadata/workloads/inbound-library/imap
owner_repo: ops-mason
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
@ -101,11 +78,6 @@ paths:
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

View file

@ -1,13 +1,16 @@
version: 1
updated: '2026-10-21'
updated: '2026-06-17'
domain: helix_forge
capabilities:
- id: capability.security.ssh-certificate-issuance
name: Decision-bound SSH certificates
summary: warden sign with local and OpenBao backends and adm/agt/atm actors.
name: SSH Certificate Issuance
summary: Issue short-lived CA-signed SSH certificates for adm, agt, and atm actors
through a stable cert_command CLI interface; steward NetKingdom operational access routing.
vector: D4 / A3 / C3 / R2
domain: helix_forge
status: draft
owner: ops-warden
path: registry/capabilities/capability.security.ssh-certificate-issuance.md
tags:
- ssh
- certificate
@ -15,40 +18,6 @@ capabilities:
- ops-warden
- openbao
- security
- netkingdom
- framework-reuse
vector: D4 / A3 / C3 / R2
path: registry/capabilities/capability.security.ssh-certificate-issuance.md
consumption_modes:
- library
- CLI
- id: capability.security.access-routing
name: Credential routing and contained access
summary: warden route and access catalog; exact lane conduit and secret-exec routing.
domain: infotech
status: draft
owner: ops-warden
tags:
- security
- netkingdom
- framework-reuse
vector: D3 / A3 / C1 / R0
path: registry/capabilities/capability.security.access-routing.md
consumption_modes:
- library
- CLI
- id: capability.security.case-follow-through
name: Security case operational follow-through
summary: KnownSecurityCase route, plan and operational receipts.
domain: infotech
status: draft
owner: ops-warden
tags:
- security
- netkingdom
- framework-reuse
vector: D3 / A2 / C1 / R0
path: registry/capabilities/capability.security.case-follow-through.md
consumption_modes:
- CLI
- source
- cert_command subprocess

View file

@ -40,24 +40,6 @@
version: 1
entries:
- id: netkingdom-layer-declaration
title: NetKingdom layer declaration and PEP stance-map route
risk: standard
workload_ref:
applicability: not-applicable
reason: "Repository conformance guidance, not a workload credential or runtime action."
need_keywords: [how, declare, layer, declaration, netkingdom, security, companion, pep, pep-shaped, stance, conformance, tooling, staff, engine]
owner_repo: ops-warden
subsystem: NetKingdom layer-conformance routing
warden_executes: false
wiki_ref: wiki/playbooks/netkingdom-layer-declaration.md#worker-checklist
canon_ref: net-kingdom/canon/standards/security-layer-model_v0.8.md
reviewed: "2026-10-11"
status: active
delegation:
mode: permanent
reviewed: "2026-09-04"
- id: ssh-cert-host-access
title: Short-lived SSH certificate for host / ops reachability
# Emits a signed certificate — a public artifact. The private key never leaves the caller (WARDEN-WP-0032-T05).
@ -83,30 +65,6 @@ entries:
- "Sign: `warden sign <actor> --pubkey <path>` — cert is written to stdout (the cert_command contract)."
- "TTL is enforced per actor type: adm 48h / agt 24h / atm 8h. No long-lived keys."
- id: agt-custodian-ssh-cert
title: Supervisor-issued SSH certificate for the custodian agent account (agt-custodian)
# Attended issuance pointer (CUST-WP-0073-T02, WARDEN-WP-0045). The cert is a
# public artifact, but issuance must stay outside the agent account: the agent
# never runs warden sign for itself, so this is a ceremony, not an SSH exec lane.
risk: standard
workload_ref:
applicability: not-applicable
reason: "Supervisor act that issues a cert for the custodian-agt host account; no workload credential is retrieved."
need_keywords: [agt-custodian, custodian-agt, custodian-agents, custodian, agent, supervised, interactive, codex, claude, grok, ssh, certificate, cert, railiance01, renew, reissue]
owner_repo: ops-warden
subsystem: ops-warden (supervisor operator account) + railiance-infra principals
warden_executes: false
wiki_ref: wiki/ActorInventoryPatterns.md#supervised-interactive-agent-account-agt-custodian
canon_ref: the-custodian/workplans/CUST-WP-0073-agent-credential-separation.md
reviewed: "2026-10-20"
status: active
delegation:
mode: native
intended_owner: ops-warden
reviewed: "2026-10-20"
verified: unverified
lane: ceremony
- id: ops-warden-warden-sign-token
title: Scoped OpenBao token for ops-warden SSH signing (warden-sign)
# A scoped VAULT_TOKEN is a credential in its own right. Graded on what the value is, not on whether ops-warden currently proxies it (WARDEN-WP-0032-T05).
@ -214,114 +172,6 @@ entries:
exec_capable: true
lane: login
- id: kubernetes-railiance01-admin
title: Attended railiance01 cluster-admin kubeconfig
# Identity pointer for the admin kubeconfig file. The agent default path is
# the scoped ops-mason-builder identity (MASON-IN-0006 choice (b), decision
# 30d29060-8d14-4a8e-9cde-34aa826eb8d1). This lane does not read the file.
risk: high
workload_ref:
applicability: not-applicable
reason: "Attended operator identity for the railiance01 admin kubeconfig file. The agent default path is the scoped ops-mason-builder user."
need_keywords: [kubernetes, kubeconfig, railiance01, cluster-admin, k3s, admin-kubeconfig, ops-mason-builder]
owner_repo: railiance-platform
subsystem: attended kubeconfig file outside the agent default path
warden_executes: false
wiki_ref: wiki/playbooks/kubernetes-railiance01-admin.md#worker-checklist
canon_ref: railiance-platform/argocd/platform-addons/ops-mason-builder
reviewed: "2026-10-22"
status: active
delegation:
mode: native
intended_owner: railiance-platform
reviewed: "2026-10-22"
verified: source-read
auth_method: "attended operator; set KUBECONFIG to /home/worsch/.kube/config-railiance01 for one command. That file is not the agent default path."
path_template: "/home/worsch/.kube/config-railiance01"
fetch_command: "test -f /home/worsch/.kube/config-railiance01"
exec_capable: true
lane: secret
# vends_secret is structural (active secret lane with a path). Renewal is
# re-establishing the file in place, never reading or copying it.
rotation:
method: re-establish
owner: railiance-platform
automatable: false
steps:
- "Re-establish the attended admin kubeconfig at /home/worsch/.kube/config-railiance01, outside the agent default path. Do not copy the admin client key into the builder kubeconfig and do not print the file."
- id: secrets-engine-approval-client-login
title: Attended secrets-engine approval client reader login
risk: high
workload_ref:
applicability: not-applicable
reason: "Attended operator identity for the exact CCR-2026-0019 reader."
need_keywords: [secrets-engine, approval-client, reader, login, ccr-2026-0019]
owner_repo: railiance-platform
subsystem: OpenBao scoped operator OIDC via KeyCape
warden_executes: false
wiki_ref: wiki/playbooks/secrets-engine-approval-client-login.md
canon_ref: railiance-platform/credential-change-requests/CCR-2026-0019-secrets-engine-approval-client-read.yaml
reviewed: "2026-09-14"
status: active
delegation:
mode: native
intended_owner: railiance-platform
reviewed: "2026-09-14"
verified: source-read
auth_method: "attended KeyCape OIDC, netkingdom role secrets-engine-approval-client-workload-kv-read"
fetch_command: "bao login -no-print -method=oidc -path=netkingdom role=secrets-engine-approval-client-workload-kv-read"
exec_capable: true
lane: login
- id: secrets-engine-requester-login
title: Attended T03 create-only requester reader login
risk: high
workload_ref:
applicability: not-applicable
reason: "Attended operator identity for the exact CCR-2026-0025 reader."
need_keywords: [secrets-engine, approval-requester, reader, login, ccr-2026-0025]
owner_repo: railiance-platform
subsystem: OpenBao scoped operator OIDC via KeyCape
warden_executes: false
wiki_ref: wiki/playbooks/secrets-engine-requester-login.md
canon_ref: railiance-platform/credential-change-requests/CCR-2026-0025-t03-requester-reader.yaml
reviewed: "2026-09-14"
status: active
delegation:
mode: native
intended_owner: railiance-platform
reviewed: "2026-09-14"
verified: source-read
auth_method: "attended KeyCape OIDC, netkingdom role secrets-engine-requester-workload-kv-read"
fetch_command: "bao login -no-print -method=oidc -path=netkingdom role=secrets-engine-requester-workload-kv-read"
exec_capable: true
lane: login
- id: informed-decision-sitting-requester-login
title: Attended Informed Decision sitting-requester reader login
risk: high
workload_ref:
applicability: not-applicable
reason: "Attended operator identity for the exact CCR-2026-0027 reader."
need_keywords: [informed-decision, sitting-requester, reader, login, ccr-2026-0027]
owner_repo: railiance-platform
subsystem: OpenBao scoped operator OIDC via KeyCape
warden_executes: false
wiki_ref: wiki/playbooks/informed-decision-sitting-requester-login.md
canon_ref: railiance-platform/credential-change-requests/CCR-2026-0027-informed-decision-sitting-requester-reader.yaml
reviewed: "2026-09-15"
status: active
delegation:
mode: native
intended_owner: railiance-platform
reviewed: "2026-09-15"
verified: source-read
auth_method: "attended KeyCape OIDC, netkingdom role informed-decision-sitting-requester-workload-kv-read"
fetch_command: "bao login -no-print -method=oidc -path=netkingdom role=informed-decision-sitting-requester-workload-kv-read"
exec_capable: true
lane: login
- id: openbao-shamir-recovery-ceremony
title: Attended OpenBao Shamir seal and unseal recovery ceremony
# A ceremony pointer, not a credential-value lane. Approval coordinates
@ -346,19 +196,19 @@ entries:
lane: ceremony
- id: whynot-design-npm-publish
title: whynot-design npm publish token (@whynot/design → coulomb Forgejo registry)
title: whynot-design npm publish token (@whynot/design → coulomb Gitea registry)
# Publish rights to the package registry — a leaked token is a supply-chain write, not a read (WARDEN-WP-0032-T05).
risk: high
workload_ref:
applicability: applicable
unknown_reason: "whynot-design has not published an authoritative workload identity declaration."
need_keywords: [whynot-design, whynot, npm, publish, npm_auth_token, forgejo, registry, coulomb, package]
need_keywords: [whynot-design, whynot, npm, publish, npm_auth_token, gitea, registry, coulomb, package]
owner_repo: railiance-platform
subsystem: OpenBao
warden_executes: false
wiki_ref: wiki/playbooks/whynot-design-npm-publish.md#worker-checklist
canon_ref: net-kingdom/docs/platform-identity-security-architecture.md
reviewed: "2026-09-04"
reviewed: "2026-06-29"
status: active
delegation:
mode: native
@ -373,28 +223,6 @@ entries:
# form is superseded; do not reintroduce it.
auth_method: "bao login -method=oidc -path=netkingdom role=whynot-design-workload-kv-read"
path_template: "platform/workloads/coulomb/whynot-design/npm-publish"
# Field REVERTED to NPM_AUTH_TOKEN on 2026-09-10, and the path CONFIRMED, by the
# custodian of the path (railiance-platform msgs c6841bdb, 79f3e047). An attended,
# read-only, no-value founder session enumerated the field names actually present
# at this path: exactly one, NPM_AUTH_TOKEN. Receipt: railiance-platform
# docs/evidence/2026-09-10-npm-lane-field-resolution.json (commit 9d24086); prior
# basis CCR-2026-0001's two dated receipts (2026-06-28) naming the same field.
#
# The 2026-09-09 change to `npm_token` was adopted from secrets-engine's statement
# (msg 15f0c0ca) and is wrong FOR THIS PATH. That is ADR-0001's failure mode in its
# proper form: a correction taken from a coordination message is still an unverified
# mutation, and this one would have named a field that does not exist — failing at
# the rotation (WARDEN-WP-0037-T03) rather than on a cheap day. Reverting is a
# correction back to the evidenced value, not a new lane change.
#
# OPEN, and not ops-warden's to settle: a legacy location
# secret/coulomb/whynot-design/npm/publish exists (metadata only, v1 created
# 2026-07-03, outside this lane's exact-path policy and outside any CCR).
# railiance-platform's hypothesis is that secrets-engine's lowercase field belongs
# to THAT path and that the native exec front door below may be reading the
# ungoverned duplicate. Tracked as railiance-platform RPF-WP-0035-T07; ops-warden
# has asked secrets-engine which location `secrets-engine exec` reads. Until that
# is answered, this catalog names the governed path and the field evidenced at it.
fetch_command: "bao kv get -field=NPM_AUTH_TOKEN platform/workloads/coulomb/whynot-design/npm-publish"
policy_ref: "flex-auth check secret.read:whynot-design"
exec_capable: true
@ -410,84 +238,9 @@ entries:
owner: railiance-platform
automatable: false
steps:
- "In the coulomb Forgejo registry, revoke the current @whynot/design publish token and generate a new one (scope: package read/write) for the whynot-design publish identity."
- "Write it back: `bao kv put platform/workloads/coulomb/whynot-design/npm-publish NPM_AUTH_TOKEN=@file` (value from a mode-0600 file). NPM_AUTH_TOKEN is the KV field name at this path, evidenced by an attended enumeration on 2026-09-10; it is also the env var the publication-scope policy injects, which is what made the two readings easy to confuse."
- "Verify capabilities-safe, then publish a fresh version and confirm it with Forgejo-supported `npm view <package>@<version>` through the governed execution lane (value used, not printed)."
- id: policy-nexus-forgejo-source-read
title: Policy Nexus Forgejo private-source repository read token
# Read-only at Forgejo, but disclosure grants estate-wide private source access.
# Agent callers therefore remain inside Warden's sanctioned transport boundary.
risk: high
workload_ref:
applicability: applicable
unknown_reason: "policy-nexus-actions has not published an authoritative workload identity declaration."
need_keywords: [policy-nexus, policy, nexus, forgejo, private, source, repository, read, token, actions, FORGEJO_SOURCE_TOKEN]
owner_repo: railiance-platform
subsystem: OpenBao + Forgejo Actions
warden_executes: false
wiki_ref: wiki/playbooks/policy-nexus-forgejo-source-read.md#worker-checklist
canon_ref: railiance-platform/credential-change-requests/CCR-2026-0014-policy-nexus-forgejo-source-read.yaml
reviewed: "2026-09-01"
status: active
delegation:
mode: native
intended_owner: railiance-platform
reviewed: "2026-09-01"
verified: source-read
auth_method: "bao login -method=oidc -path=netkingdom role=policy-nexus-forgejo-source-workload-kv-read"
path_template: "platform/workloads/policy-nexus/forgejo-source-read"
fetch_command: "bao kv get -field=FORGEJO_SOURCE_TOKEN platform/workloads/policy-nexus/forgejo-source-read"
policy_ref: "flex-auth check secret.read:policy-nexus"
exec_capable: true
lane: secret
rotation:
method: rotate
owner: railiance-platform
automatable: false
steps:
- "Mint a replacement PAT for the restricted policy-nexus-source identity with scope exactly read:repository; retain the predecessor until verification passes."
- "Use the attended railiance-platform bootstrap to update OpenBao and the coulomb/policy-nexus FORGEJO_SOURCE_TOKEN Actions secret without exposing the value."
- "Pass one exact-commit candidate workflow, then revoke the predecessor PAT and record bounded non-secret evidence in CCR-2026-0014."
- id: core-hub-forgejo-source-read
title: Core Hub repo-manager publisher Forgejo private-source repository read token
# Read-only at Forgejo, but disclosure grants estate-wide private source read for
# org coulomb. ESO is the only consumer (CCR-2026-0031): no operator fetch lane,
# so exec_capable stays false and Warden never proxies a read of this value.
risk: high
workload_ref:
applicability: applicable
rapp_id: rapp-core-hub
name: core-hub
deployable: repository-classification-publisher
fields: [FORGEJO_SOURCE_TOKEN, API_USER, API_BASE_URL, TOKEN_SCOPES, GENERATED_AT] # CCR-2026-0031
need_keywords: [core-hub, core, hub, repo-manager, publisher, forgejo, private, source, repository, read, token, eso, external-secrets, FORGEJO_SOURCE_TOKEN]
owner_repo: railiance-platform
subsystem: OpenBao + External Secrets + Forgejo
warden_executes: false
wiki_ref: wiki/playbooks/core-hub-forgejo-source-read.md#worker-checklist
canon_ref: railiance-platform/credential-change-requests/CCR-2026-0031-core-hub-forgejo-source-read.yaml
reviewed: "2026-10-03"
status: active
delegation:
mode: native
intended_owner: railiance-platform
reviewed: "2026-10-03"
verified: source-read
auth_method: "Kubernetes auth (mount kubernetes) role=external-secrets-core-hub-forgejo-source, bound to external-secrets/external-secrets; ESO delivery only (ClusterSecretStore openbao-core-hub-forgejo-source → ExternalSecret core-hub/repo-manager-forgejo-source)"
path_template: "platform/workloads/core-hub/forgejo-source-read"
policy_ref: "workload-kv-read-core-hub-forgejo-source"
exec_capable: false
lane: secret
rotation:
method: rotate
owner: railiance-platform
automatable: false
steps:
- "Every 180 days or on owner change: mint a replacement PAT for the restricted core-hub-source identity with scope exactly read:repository; retain the predecessor until verification passes."
- "Write the replacement to platform/workloads/core-hub/forgejo-source-read from stdin through the attended railiance-platform bootstrap, then let ESO refresh core-hub/repo-manager-forgejo-source (5m)."
- "Pass make verify-publisher in rapp-core-hub, then revoke the predecessor PAT and record bounded non-secret evidence in CCR-2026-0031."
- "In the coulomb Gitea/Forgejo registry, revoke the current @whynot/design publish token and generate a new one (scope: package read/write) for the whynot-design publish identity."
- "Write it back: `bao kv put platform/workloads/coulomb/whynot-design/npm-publish NPM_AUTH_TOKEN=@file` (value from a mode-0600 file)."
- "Verify capabilities-safe, then confirm publish works via `secrets-engine exec --catalog whynot-design-npm-publish -- npm whoami` (value used, not printed)."
- id: flex-auth-policy-check
title: Authorization decision — may this actor perform this action
@ -496,19 +249,7 @@ entries:
workload_ref:
applicability: not-applicable
reason: "Generic authorization action; the governed resource supplies workload identity."
need_keywords: [authorization, policy, permission, allow, deny, may, flex-auth, access-engine, topaz, pdp, decision]
# FLEX-WP-0020 rename review, 2026-09-21 (flex-auth msg 0a1956c5; WARDEN-IN-0003).
# `access-engine` added to the keywords so this lane resolves under BOTH names from
# today — the deprecation window ops-warden asked for in WARDEN-IN-0001 and the only
# change the rename needs here to keep routing working.
#
# `owner_repo` is deliberately NOT flipped, and the reason is not inertia. In this
# catalog `owner_repo` is not purely a repository coordinate: src/warden/policy.py
# sends it verbatim as `resource.system` (and as `context.owner_repo`) on every
# /v1/check. FLEX-DEC-2026-013 keeps runtime names as `flex-auth`, so flipping this
# string would rename a policy resource the PDP matches on, under cover of a
# repository rename — a runtime change wearing a coordinate change's clothes. It
# flips when the runtime name does, not when the repository does.
need_keywords: [authorization, policy, permission, allow, deny, may, flex-auth, topaz, pdp, decision]
owner_repo: flex-auth
subsystem: flex-auth
warden_executes: false
@ -540,16 +281,16 @@ entries:
delegation:
mode: interim
intended_owner: key-cape
blocked_on: "ACCEPTED by key-cape 2026-09-08 (msg 12f1bdfa): key-cape owns interactive login, OIDC identity-token issuance and MFA for this lane, confirming the 2026-08-28 correction of intended_owner from secrets-engine (whose 2026-08-21 refusal was right). Lane stays interim because the accepted ownership covers only the identity half: the fetch_command yields an OPENBAO token, and the netkingdom auth mount, the role=<domain> mapping, the token store and its enforcement are OpenBao's, with role/policy definition railiance-platform's. keycape login is not a drop-in — it yields a key-cape issuer JWT written to a private file, a different credential type with a different verifier (KEY-WP-0014-T04). Retires when a key-cape-native front door yields the credential this route's consumers expect, or when the route is deliberately re-contracted to a JWT. Verified against key-cape docs/native-authentication.md, docs/openbao-service-auth-contract.md, config/service-clients.example.yaml, src/internal/server/oidc/discovery.go."
reviewed: "2026-09-08"
verified: owner-confirmed
blocked_on: "REFUSED by secrets-engine 2026-08-21: login, MFA and identity-token issuance are key-cape/Keycloak's; secrets-engine may consume OIDC for OpenBao auth but does not own the login capability. ops-warden agrees — intended_owner corrected from secrets-engine to key-cape. Not yet asked of key-cape."
reviewed: "2026-08-21"
verified: asked-and-waiting
# Login lane (WP-0014 T4) — interactive auth bootstrap, not a secret read. No
# secret-read gate (you have no identity yet) and no caller-auth precheck (the
# point is to obtain one). warden runs it interactively as the caller and never
# captures the resulting token — the owner tool writes it to the caller's store.
lane: login
auth_method: "browser OIDC via key-cape / Keycloak at auth/netkingdom"
fetch_command: "bao login -no-print -method=oidc -path=netkingdom role=<domain>"
auth_method: "browser OIDC via key-cape / Keycloak"
fetch_command: "bao login -method=oidc role=<domain>"
exec_capable: true
- id: ops-bridge-tunnel
@ -723,12 +464,7 @@ entries:
# reuse/reuse-surface-runtime SecretSynced to reuse-surface-env on Railiance01;
# positive + negative access verified. Production consumer is ESO; warden access
# proxies reads as the caller and never holds the value.
# Dedicated caller: CCR-2026-0044 applied. Group reuse-surface-hub-writers,
# assurance aal2, policy workload-kv-read-reuse-surface-runtime only.
# Member and non-member receipts passed 2026-10-14 (ops-mason). The proxy
# still uses the ambient token after this login. platform-admin must not
# receive the scoped policy. ESO stays on reuse-surface-runtime-eso.
auth_method: "bao login -method=oidc -path=netkingdom role=reuse-surface-hub-writer-workload-kv-read"
auth_method: "caller's own OpenBao token (operator OIDC via key-cape, or a token carrying workload-kv-read-reuse-surface-runtime)"
path_template: "platform/workloads/reuse/reuse-surface/runtime-secrets"
fetch_command: "bao kv get -field=REUSE_SURFACE_TOKEN platform/workloads/reuse/reuse-surface/runtime-secrets"
policy_ref: "flex-auth check secret.read:reuse"
@ -838,48 +574,32 @@ entries:
- "Verify capabilities-safe on the data path (`bao token capabilities`); run a restore drill against a re-encrypted artifact."
- "After rotation, clear EXPOSED taint: remove custom_metadata exposed_at/exposed_version (see `warden taint railiance-backup-offsite-lane`)."
- id: flex-auth-decision-envelope-signing-key
title: Ed25519 key that signs flex-auth decision envelopes
risk: high
workload_ref:
applicability: applicable
unknown_reason: "flex-auth has not published an authoritative workload identity declaration for envelope signing."
need_keywords: [flex-auth, decision, envelope, signing, signature, ed25519, responder, authenticity, FLEX-WP-0024]
owner_repo: railiance-platform
subsystem: OpenBao + flex-auth
warden_executes: false
wiki_ref: wiki/playbooks/flex-auth-decision-envelope-signing-key.md#worker-checklist
canon_ref: flex-auth/docs/decision-envelope-signature.md
reviewed: "2026-09-14"
status: draft
delegation:
mode: interim
intended_owner: railiance-platform
blocked_on: "Shape is chosen in flex-auth (FLEX-WP-0024-T02). OpenBao KV path, Kubernetes auth role, and paste_once_provision of the private seed are not built. Testdata key in flex-auth is not this lane."
reviewed: "2026-09-14"
verified: unverified
auth_method: "Kubernetes auth from the flex-auth pin (to be bound); not a static secret in git"
path_template: "platform/workloads/flex-auth/envelope-signing (fields ED25519_PRIVATE_KEY, ED25519_PUBLIC_KEY, kid)"
- id: state-hub-forge-derivation-read
title: Forgejo read-only token for State Hub projection derivation
# Organisation-wide repository read: if the value leaks, every private
# repository is readable. Read-only, no write, no admin, no persistence —
# but the breadth is the grade (approved knowingly 2026-08-26).
risk: high
workload_ref:
applicability: applicable
unknown_reason: "state-hub has not published an authoritative workload identity declaration; Kubernetes auth role state-hub-forge-derivation binds ServiceAccount state-hub/state-hub."
unknown_reason: "The state-hub deployment has no authoritative workload identity declaration yet; the AppRole is the identity until it does."
need_keywords: [state-hub, statehub, forge, forgejo, derivation, derive, projection, private, repository, clone, FORGE_READ_TOKEN, ADR-012, work-records, unreadable, kubernetes-auth]
owner_repo: ops-mason
subsystem: OpenBao + Forgejo
warden_executes: false
wiki_ref: wiki/playbooks/state-hub-forge-derivation-read.md#worker-checklist
canon_ref: ops-mason/plans/state-hub-forge-derivation-read.md
reviewed: "2026-09-14"
status: active
reviewed: "2026-08-26"
status: draft
delegation:
mode: native
intended_owner: ops-mason
reviewed: "2026-09-14"
verified: owner-confirmed
blocked_on: "Policy and Kubernetes auth role built and capability-verified 2026-08-27 (MASON-WP-0003-T02). Still draft: the KV path holds no token until paste_once_provision delivers one, and ServiceAccount state-hub/state-hub does not exist yet (STATE-WP-0084-T02). Promote to active once both land."
reviewed: "2026-08-26"
verified: unverified
# ops-warden routes this and executes nothing: it issues SSH certificates
# only and owns no secret store. ops-mason builds the structure; the forge
# owner mints the value; paste_once_provision delivers it.
auth_method: "Kubernetes auth (role state-hub-forge-derivation, ServiceAccount state-hub/state-hub) from the in-cluster state-hub workload"
path_template: "platform/workloads/state-hub/forge-derivation (field FORGE_READ_TOKEN)"
rotation:
@ -1017,7 +737,7 @@ entries:
- id: rapp-qonto-keycape-client
title: rapp-qonto KeyCape workload client
need_keywords: [rapp-qonto, qonto, keycape, oidc, client-credentials, service-token, workload-identity, binky, client secret, secret, successor, rotate, rotation]
need_keywords: [rapp-qonto, qonto, keycape, oidc, client-credentials, service-token, workload-identity, binky]
owner_repo: key-cape
subsystem: KeyCape + OpenBao
warden_executes: false
@ -1028,9 +748,9 @@ entries:
delegation:
mode: interim
intended_owner: key-cape
blocked_on: "CHARACTER CHANGED 2026-09-10 (key-cape msgs 10cf8fca, 546f76e4), substance unchanged. This is no longer blocked on an OPEN QUESTION: the authority is named, the transport is named, and the rotation CCR is available on request. It is blocked on a DECISION TAKEN — key-cape put the question to the operator rather than answering for him, and the answer is NOT YET, with the offer standing open. ops-warden routed the ask and deliberately did not make it on key-cape's behalf (ADR-0003): a routing errand that implies an owner request manufactures a prepared CCR nobody decided to want. This schema carries no field distinguishing 'no authority exists' from 'authority exists, unexercised by owner decision' — the second is what is true here, and it is recorded in prose because inventing a delegation field to hold it would be a catalog change made to describe one lane. NOT A DEADLINE, and not remediation: key-cape checked and there is no incident behind it. KEY-WP-0011 recovered a real exposure on 2026-08-23, but the rapp-qonto client secret was not in that payload — it is an env: secretRef from a separate Kubernetes Secret, never inline in config.yaml, enforced by config validation. So this is lifecycle hygiene, which may wait for a chosen window; if anyone is carrying it as leftover incident work, it is not. WHAT REOPENS IT (events, never elapsed time): an actual or suspected exposure; the secret's age becoming a stated concern; a consumer requiring proof of rotation; or a decision to prove rotation step 4 before relying on it. Prior state, unchanged in substance: Narrowed again 2026-09-09: the AUTHORITY is answered, the ARTIFACT is not. railiance-platform (msg 7c7228ac) confirmed steps 1-2 are theirs: executed by the platform operator attended, never by ops-warden, secrets-engine autonomously, or any unattended agent; transport is the governed openbao-platform-admin-login lane invoked only through `warden access openbao-platform-admin-login --exec -- <command>` with a unique metadata receipt path (RPF-WP-0017 output containment); authority is founder_required attended OIDC via netkingdom role=platform-admin. What is still missing is a reviewed rotation CCR — a two-custodian CAS rotation with a service restart is a distinct version-guarded operation, not an implementation detail of this lane, and it must name the CAS precondition and expected version on both custodians, sibling-field preservation, the restart window, bounded predecessor retention, and the reconcile-to-same-version failure step. That CCR is railiance-platform to write under RPF-WP-0035 once an owner asks for the rotation; ops-warden has asked key-cape whether to schedule it. Executable precedent for the same two-custodian shape: railiance-platform scripts/keycape_approval_custody.py, with its dated receipt under docs/evidence/ and review packet under docs/credential-lane-designs/. Prior (2026-09-08) narrowing to rotation steps 1-2 only: successor generation and the CAS write to both custodians (platform/workloads/rapp-qonto/keycape-client field client_secret, and sso/keycape-rapp-qonto-client key client-secret) remain custody/deployment acts. The key-cape-native exchange now exists (keycape service-token, 2026-09-05, KEY-WP-0014-T03) and step 3 verification exists as one command (keycape verify-client, 2026-09-08, including predecessor rejection and identical-secret detection); both are documented in key-cape/docs/native-authentication.md. The prior blocker recorded both as absent, which was accurate on 2026-08-28 and is not accurate now — corrected by key-cape (msg 08d42f47). Re-checked against key-cape source 2026-09-08."
reviewed: "2026-09-21"
verified: owner-confirmed
blocked_on: "client_secret_basic exchange is a key-cape protocol procedure, not a KV read; KEY-WP-0008 finished 2026-08-15 without a key-cape-native exchange/rotation command, and the workplan that would add one is not yet opened (key-cape acked interim 2026-08-15)"
reviewed: "2026-08-17"
verified: unverified
risk: high
workload_ref:
applicability: applicable
@ -1045,15 +765,11 @@ entries:
rotation:
method: rotate
owner: key-cape
# Not drivable end to end: steps 1-2 are custody/deployment acts with no
# admitted execution transport, so an executable driver (WARDEN-WP-0027
# Strand B) must not treat this lane as automatable. Step 3 alone is one
# owner command today — the precision lives in the steps, not the flag.
automatable: false
automatable: true
steps:
- "Generate a fresh newline-free secret through an approved execution transport; never print it. NOT automatable — no admitted execution and rollback contract (see delegation.blocked_on)."
- "CAS-write the same value to platform/workloads/rapp-qonto/keycape-client and sso/keycape-rapp-qonto-client preserving siblings, then restart KeyCape in the agreed window. NOT automatable — same blocker; on failure reconcile both custodians to the same version before declaring rollback complete."
- "Verify positive qonto:read exchange plus predecessor-secret and excessive-scope denial without printing tokens: run the owner command keycape verify-client (key-cape/docs/native-authentication.md)."
- "Generate a fresh newline-free secret through an approved execution transport; never print it."
- "Write the same value to OpenBao and sso/keycape-rapp-qonto-client, then restart KeyCape."
- "Verify positive qonto:read exchange plus wrong-secret and excessive-scope denial without printing tokens."
- id: net-kingdom-lldap-bind-credential
title: NetKingdom LLDAP bind credential for identity and privacyIDEA resolver
@ -1290,46 +1006,6 @@ entries:
- "Optionally re-mint the OpenRouter key itself if it rotated independently."
- "Verify AppRole login + field presence (length only); confirm default/other policies still deny sibling paths."
- id: inbound-library-imap-approle
title: inbound-library AppRole for non-interactive library@coulomb.social IMAP read
need_keywords: [inbound-library, approle, library@coulomb.social, mail, imap, unattended]
owner_repo: ops-mason
subsystem: OpenBao AppRole + inbound-library
warden_executes: false
wiki_ref: wiki/playbooks/inbound-library-imap-approle.md#worker-checklist
canon_ref: ops-mason/plans/inbound-library-imap-approle.md
reviewed: '2026-10-05'
status: active
delegation:
mode: interim
intended_owner: ops-mason
blocked_on: AppRole is a host-standing credential; no owner-fronted exec for the
library mailbox read
reviewed: '2026-10-05'
verified: unverified
risk: high
workload_ref:
applicability: applicable
unknown_reason: inbound-library has not published an authoritative workload identity
declaration.
auth_method: AppRole login role=inbound-library (role_id+secret_id in ~/.local/inbound-library/approle,
config/library.json credential.approle_dir)
path_template: platform/workloads/inbound-library/imap
fetch_command: 'bao write -field=token auth/approle/login role_id=$ROLE_ID secret_id=$SECRET_ID #
then bao kv get -field=IMAP_PASSWORD platform/workloads/inbound-library/imap'
exec_capable: false
lane: secret
rotation:
method: rotate
owner: ops-mason
automatable: false
steps:
- bao write -f auth/approle/role/inbound-library/secret-id -> new secret_id; deliver
mode-0600 to ~/.local/inbound-library/approle; shred old secret_id file.
- Optionally re-mint the IMAP password at IONOS and add it as a new version of
platform/workloads/inbound-library/imap (founder, attended).
- Verify via one inbound-library run-due (AppRole login + read-only scan); confirm
a default-policy token still gets deny on the data path.
- id: coulomb-social-runtime-env
title: coulomb.social runtime env Secret (SECRET_KEY, DATABASE_URL, USER_ENGINE_PROXY_SECRET)
need_keywords:
@ -1385,53 +1061,29 @@ entries:
- id: audit-core-senders
title: audit-core sender registry (write and operator-read tokens)
# Vends write and operator-read tokens (WARDEN-WP-0032-T05).
# Activated 2026-10-10 as a pointer on ops-mason's custody check (message
# 5a516911, ops-mason-survey grant, metadata and policy text only):
# - KV v2 at platform/workloads/audit-core/senders, v11, with all five
# custody fields present.
# - Only external-secrets-audit-core grants a data read.
# - agent-high-risk-boundary denies the data path, and the tenant-engine
# projection policy does not name it.
# ESO reads it by token auth; the parent token renewal is due 2026-10-25,
# under railiance-platform RPF-WP-0046. A live-token capability check is
# owner-run and not yet done.
risk: high
workload_ref:
applicability: applicable
unknown_reason: "audit-core has not published an authoritative workload identity declaration."
need_keywords: [audit-core, senders, sender registry, ingest token, AUDIT_CORE_SENDERS, tenant-engine audit-core sender]
need_keywords: [audit-core, senders, sender registry, ingest token, AUDIT_CORE_SENDERS]
owner_repo: ops-mason
subsystem: OpenBao + audit-core
warden_executes: false
wiki_ref: wiki/playbooks/audit-core-senders.md#worker-checklist
canon_ref: audit-core/docs/operator-runbook.md
reviewed: "2026-10-10"
status: active
reviewed: "2026-08-13"
status: draft
delegation:
mode: native
intended_owner: ops-mason
reviewed: "2026-10-10"
verified: source-read
# Per-sender projections. Each is verified on its own and none activates the
# registry lane. tenant-engine: KV platform/workloads/tenant-engine/audit-core-sender
# (v1), read policy workload-kv-read-tenant-engine-audit-core-sender-eso (exact
# read, no registry path), ESO-delivered; verified by ops-mason 2026-10-09
# (MASON-WP-0007-T06, message e7e866bf).
consumers: [tenant-engine]
auth_method: "OpenBao KV v2 custody; audit-core reads via ESO (policy external-secrets-audit-core, token auth). Operators and agents never read the registry; a sender gets its own exact-path projection."
reviewed: "2026-08-15"
verified: unverified
auth_method: "in-cluster generated Secret audit-core-senders; later OpenBao KV via Mason wrap-migrate"
path_template: "platform/workloads/audit-core/senders"
policy_ref: "external-secrets-audit-core"
exec_capable: false
resolvable: false
lane: secret
rotation:
method: rotate
owner: audit-core
automatable: false
steps:
- "Overlap-first per audit-core/docs/operator-runbook.md#sender-credential-rotation: add the replacement token to the sender's tokens list, move the sender to it, then drop the predecessor."
- "Write the updated registry back to platform/workloads/audit-core/senders from a mode-0600 file (never a founder paste, never argv); force the ExternalSecret sync or wait for the 1h refresh, then restart audit-core."
- "Senders with an exact-path projection (tenant-engine) are re-seeded through their own projection lane; verify by metadata and capabilities only."
- id: email-connect-transactional
title: email-connect transactional SMTP and caller ingest token
@ -1514,245 +1166,3 @@ entries:
- "In Scaleway IAM, revoke the bootstrap API key after the scoped bucket key exists."
- "Delete or overwrite platform/workloads/railiance/scaleway/bootstrap; do not copy values into chat."
- "Confirm metadata gone or version bumped; capabilities-safe only."
- id: openbao-structure-survey
title: OpenBao existing-structure survey for a construction plan (section 2)
# Pointer only (WARDEN-WP-0042). Founder decision: ops-warden routes,
# ops-mason surveys. No survey code and no token live in ops-warden; the
# survey runs under ops-mason's read-only ops-mason-survey grant (MASON-IN-0004).
risk: high
workload_ref:
applicability: not-applicable
reason: "Attended read-only survey under a founder-minted grant; reads OpenBao structure metadata, not a workload credential."
need_keywords: [survey, structure survey, openbao structure, existing structure, existing policies, existing lane, auth roles, approle roles, policy list, auth list, secrets list, construction plan, construction plan section 2, plan-survey, ops-mason-survey]
owner_repo: ops-mason
subsystem: OpenBao structure metadata via read-only ops-mason-survey grant
warden_executes: false
wiki_ref: wiki/playbooks/openbao-structure-survey.md#worker-checklist
canon_ref: ops-mason/docs/construction-plan-format.md
reviewed: "2026-10-08"
status: active
delegation:
mode: native
intended_owner: ops-mason
reviewed: "2026-10-08"
verified: source-read
auth_method: "founder-minted scoped grant: ops-mason scripts/bao-session.sh grant <task> (policy ops-mason-survey, 45m, display_name claude-<task>)"
exec_owner: ops-mason
exec_command: "ops-mason/scripts/plan-survey.py --kv-path <PATH> --policy <POLICY> --approle <ROLE> --consumer <REPO> --keyword <WORD>"
exec_capable: false
lane: ceremony
- id: platform-supplier-credentials
title: Supplier account credentials under platform/suppliers/<supplier>/<account>
# Pointer only (WARDEN-WP-0043-T01). Founder decision 2026-10-08: credentials
# we hold with a supplier live on the platform mount under
# suppliers/<supplier>/<account>. railiance-platform owns the KV write and the
# credential-change allowlist; ops-warden routes the shape and holds nothing.
# Workload and rein paths with a live reader stay where they are until each
# reader is retargeted (see the playbook table).
risk: high
workload_ref:
applicability: not-applicable
reason: "Generic supplier-account path pattern; concrete consumers keep their own workload lanes."
need_keywords: [supplier, suppliers, supplier account, supplier credential, vendor, vendor account, platform/suppliers, account credential]
owner_repo: railiance-platform
subsystem: OpenBao platform mount (suppliers/)
warden_executes: false
wiki_ref: wiki/playbooks/platform-supplier-credentials.md#worker-checklist
canon_ref: railiance-platform/scripts/credential-change.py
reviewed: "2026-10-08"
status: active
delegation:
mode: native
intended_owner: railiance-platform
reviewed: "2026-10-08"
verified: source-read
auth_method: "railiance-platform delegated credential change (CCR); founder-attended provision from a mode-0600 file"
path_template: "platform/suppliers/<supplier>/<account>"
exec_capable: false
rotation:
method: rotate
owner: railiance-platform
automatable: false
steps:
- "Re-mint the credential at the supplier, written to a mode-0600 file and never printed."
- "Write it through a railiance-platform delegated credential change to platform/suppliers/<supplier>/<account> (value from the file, never on argv); revoke the predecessor at the supplier."
- "Verify with capabilities and metadata only (`bao token capabilities`, `bao kv metadata get`), then tell readers to re-fetch."
- id: fluid-telegram-operator
title: fluid-telegram Hall of Helix operator KV lane (attended OIDC)
# Pointer only (MASON-WP-0005-T06, proposal 856bd6ba, 2026-10-09). Built by
# ops-mason as CCR-2026-0039; ops-mason verified the policy text and role:
# exact four-path matrix, role bound to fluid-telegram-operators at aal2, and
# 8 agent denies present. ops-mason proposed `applied-pending-verify`; the
# catalog has only draft/active, so it stays draft until the owner-run checks
# pass: member capabilities and non-member refusal
# (ops-mason scripts/verify-fluid-telegram-lane.sh), plus the salt
# create-once probe (FT-WP-0002). Then ops-warden activates it. The adapter
# runtime lane is separate (MASON-IN-0003): bot-token and redaction-salt only.
risk: high
workload_ref:
applicability: applicable
unknown_reason: "fluid-telegram has not published an authoritative workload identity declaration."
need_keywords: [fluid-telegram, hall-of-helix, telegram operator, telegram bot, mtproto, operator-session, bot-token, redaction-salt, fluid-telegram-operators]
owner_repo: ops-mason
subsystem: OpenBao netkingdom OIDC + platform KV (fluid-telegram campaign)
warden_executes: false
wiki_ref: wiki/playbooks/fluid-telegram-operator.md#worker-checklist
canon_ref: ops-mason/plans/fluid-telegram-operator-credential-lane.md
reviewed: "2026-10-09"
status: draft
delegation:
mode: native
intended_owner: ops-mason
reviewed: "2026-10-09"
verified: owner-confirmed
auth_method: "bao login -method=oidc -path=netkingdom role=fluid-telegram-hall-of-helix-workload-kv (members of fluid-telegram-operators, aal2, 15m token)"
path_template: "platform/workloads/coulomb/fluid-telegram/hall-of-helix/{operator-app,operator-session,bot-token,redaction-salt}"
exec_capable: false
resolvable: false
lane: login
- id: keycape-client-secret-generate
title: SecurityCase mint-secret, first KeyCape client secret generated in custody
# Pointer only, draft (WARDEN-WP-0044; railiance-platform RPF-WP-0054,
# message d951386f, 2026-10-09). Lane named by SecurityCaseType
# oidc-browser-relying-party 1.0, step mint-secret
# (value_handling: generated_in_custody). The broker generates the value
# in its own process, writes once with CAS 0, verifies by metadata only and
# returns a receipt with no value. Grant applied (CCR-2026-0040). Not live
# until: the issuer policy is bound to the operator OIDC role (founder),
# key-cape confirms the secret format, and the RPF-WP-0054-T05 disposable
# probe passes. Then railiance-platform confirms and ops-warden activates.
# 2026-10-10: issuer role bound (CCR-2026-0043) and T05 probe passed
# (railiance-platform 21b8af3: generated v1, second run exit 3, grant token
# revoked, value_reads 0). Activated 2026-10-10 on railiance-platform's
# go-ahead (message 20973a9c) after key-cape confirmed the secret format
# (message 4e0213b3); RPF-WP-0054 T01, T03, T05 done.
# How warden case run executes it is WARDEN-WP-0044-T03.
risk: high
workload_ref:
applicability: not-applicable
reason: "Per-case generation lane; each case's consuming service gets its own {consuming_service}-keycape-client entry."
# Case-specific terms only. Broad phrases such as "keycape client secret"
# outranked named per-client lanes (rapp-qonto-keycape-client) on rotation
# needs, and this lane refuses an existing path (first generation only).
need_keywords: [mint-secret, client-secret-generate, keycape/client-secret-generate, generated in custody, generated_in_custody, security case, securitycase, oidc-browser-relying-party]
owner_repo: railiance-platform
subsystem: railiance-platform credential broker + OpenBao platform/workloads/keycape/
warden_executes: false
wiki_ref: wiki/playbooks/keycape-client-secret-generate.md#worker-checklist
canon_ref: railiance-platform/workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md
reviewed: "2026-10-10"
status: active
delegation:
mode: native
intended_owner: railiance-platform
reviewed: "2026-10-10"
verified: owner-confirmed
auth_method: "bao login -method=oidc -path=netkingdom role=credential-broker-keycape-client-secret-generate (net-kingdom-admins, aal2, 15m; token holds only the issuer policy, CCR-2026-0043); broker then mints a 5m create-only grant token (keycape/client-secret-generate). Agents and root refused."
path_template: "platform/workloads/keycape/<client_id>"
exec_owner: railiance-platform
exec_command: "scripts/credential.py generate --grant keycape/client-secret-generate --client-id <client_id> --case-file <case.yaml> --case-ref <case_id>@sha256:<hex64> [--receipt <file>] [--dry-run]"
exec_capable: false
resolvable: false
lane: secret
rotation:
method: rotate
owner: railiance-platform
automatable: false
steps:
- "This lane covers first generation only and refuses an existing path (exit 3). Rotation is a separate railiance-platform task that does not exist yet; route rotation needs to railiance-platform."
# CATALOG-1 (nk48 1.3 owner review, 2026-10-17): four lanes for the 1.3
# SecurityCaseType oidc-browser-relying-party. Pointer only, draft. Each
# stays draft until its owner confirms the pointer
# (wiki/playbooks/catalog-lane-promotion.md); the kernel refuses draft lanes.
- id: keycape-attended-case-change
title: SecurityCase attended KeyCape change (ensure-groups, bind-env, register-client)
# OW-1.3-C1: dedicated attended change authority replacing key-cape-oidc-login
# for kernel-dispatched KeyCape registry, group and Deployment env changes.
# The session may be established by a key-cape OIDC login; that login lane
# stays the identity step, not the change authority.
lane: attended-change
session_lane: key-cape-oidc-login
risk: high
workload_ref:
applicability: not-applicable
reason: "Per-case attended change lane; the consuming service is a case parameter."
need_keywords: [keycape-attended-case-change, ensure-groups, bind-env, register-client, attended keycape change]
owner_repo: key-cape/net-kingdom
subsystem: key-cape registry and Deployment env, net-kingdom groups
warden_executes: false
wiki_ref: wiki/playbooks/catalog-lane-promotion.md
canon_ref: net-kingdom/security-case-types/oidc-browser-relying-party/type.yaml
reviewed: "2026-10-17"
status: draft
delegation:
mode: interim
intended_owner: key-cape
blocked_on: "key-cape and net-kingdom confirm the pointer and the re-frozen 1.3 descriptors (OW-1.3-C1)."
reviewed: "2026-10-17"
verified: source-read
- id: platform-secret-delivery
title: SecurityCase deliver-secret, KeyCape client secret delivered by Platform
risk: high
workload_ref:
applicability: not-applicable
reason: "Per-case delivery lane; each case's consuming service gets its own client entry."
need_keywords: [platform-secret-delivery, deliver-secret, keycape client secret delivery]
owner_repo: railiance-platform
subsystem: railiance-platform External Secrets delivery
warden_executes: false
wiki_ref: wiki/playbooks/catalog-lane-promotion.md
canon_ref: railiance-platform/AGENTS.md
reviewed: "2026-10-17"
status: draft
delegation:
mode: native
intended_owner: railiance-platform
reviewed: "2026-10-17"
- id: consumer-owner-deployment
title: SecurityCase deliver-consumer-secret, consumer deployment by its owner
# Descriptor actor is agent: ADR-0004 read boundary applies. Owner-side
# delivery only; no raw value reaches the agent session. If that cannot be
# shown, the actor changes to human and the railiance-apps digest changes.
lane: attended-change
session_lane: pending-owner-confirmation
risk: high
workload_ref:
applicability: not-applicable
reason: "Per-case consumer deployment; the consumer is a case parameter."
need_keywords: [consumer-owner-deployment, deliver-consumer-secret]
owner_repo: railiance-apps
subsystem: railiance-apps consumer deployment
warden_executes: false
wiki_ref: wiki/playbooks/catalog-lane-promotion.md
canon_ref: railiance-apps/AGENTS.md
reviewed: "2026-10-17"
status: draft
delegation:
mode: native
intended_owner: railiance-apps
reviewed: "2026-10-17"
- id: apps-consumer-handover
title: SecurityCase hand-over to the consuming service owner
lane: attended-change
session_lane: pending-owner-confirmation
risk: high
workload_ref:
applicability: not-applicable
reason: "Per-case hand-over; the consumer owner is a case parameter."
need_keywords: [apps-consumer-handover, hand-over, consumer handover]
owner_repo: railiance-apps
subsystem: railiance-apps consumer hand-over
warden_executes: false
wiki_ref: wiki/playbooks/catalog-lane-promotion.md
canon_ref: railiance-apps/AGENTS.md
reviewed: "2026-10-17"
status: draft
delegation:
mode: native
intended_owner: railiance-apps
reviewed: "2026-10-17"

View file

@ -79,15 +79,8 @@ def denied_data_paths(policy_text: str) -> set[str]:
def to_data_path(path_template: str) -> str | None:
"""Catalog path -> KV v2 data path. `<mount>/rest` -> `<mount>/data/rest`.
An absolute filesystem path is not a KV mount.
"""
if (
PLACEHOLDER.search(path_template)
or " " in path_template
or path_template.startswith("/")
):
"""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

View file

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

View file

@ -1,344 +0,0 @@
#!/usr/bin/env python3
"""Check ops-warden against the NetKingdom security layer model (§5, §11).
Read-only. Two declaration forms are read, and the precedence between them is
`GH-DEC-2026-017` §1 (amendment A11): **INTENT.md's frontmatter governs** and
`layer.yaml` is a derived artifact that must be marked derived, must name
INTENT.md as its source, and must agree with it. The sidecar is read anyway,
because a disagreement between the two is itself a finding and is reported
rather than resolved away by precedence.
Comparison against §3's closed four-token vocabulary ASCII-folds case before
comparing (`GH-DEC-2026-017` §2, amendment A9): two spellings of a token do not
describe two boundaries, and a check that reports findings about capital letters
buries the one real disagreement it exists to find.
Neither form carries a version of the standard or of its companion, in any key
or value (`GH-DEC-2026-017` §5, amendment A12 as refined by A12 r2 /
`GH-DEC-2026-020` §1-§2). The rule reaches content, not a key name: a
`standard_version` key, a `companion_version` key, and a version-bearing path
such as `standard: .../security-layer-model_v0.7.md` are the same pin. Comments
and a file's own `schema_version` are not reached. Stance, claims and
evidence-classification maps (`pep-stance.yaml`) are NOT declarations and this
check does not read them (`GH-DEC-2026-020` §3).
The version belongs to the run (`GH-DEC-2026-020` §4): every run prints
VALIDATED_AGAINST and SCOPE below, including the PASS line, following
kings-guard's pattern. A retained copy of this output is a derived artifact
whose version is owed by whoever retains it.
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"
INTENT = ROOT / "INTENT.md"
DECL = ROOT / "layer.yaml"
VALID_SHAPES = {"5.1", "5.2", "5.3"}
# Accepted v0.8 publication; version provenance belongs to this run, never
# the standing layer declaration (§11). Exact publisher artifact is retained
# in net-kingdom/docs/security-layer-v0.8-acceptance.md.
VALIDATED_AGAINST = (
"net-kingdom/canon/standards/security-layer-model_v0.8.md "
"(accepted; sha256:3d8298560069ef471fba28d2b1245fb2309328f231dad1a667cdf0ce5a5a7c8b)"
)
SCOPE = ("single repository ops-warden (catalogued in §4); "
"INTENT.md frontmatter, layer.yaml, src/warden/**/*.py")
# A12 r2: a version of the standard or companion in any key or value of the
# declaration. Keys: anything naming a standard/companion version. Values: a
# versioned file name or path (`_v0.7`, `-v0.8.md`) or a bare version string on a
# version-named key. `schema_version` is the file's own schema, not reached.
# GH-DEC-2026-021 §1/§3 (A12 r3): a version is reached only as a pin. Any version
# token (`v?N.N`) in the value of an identity-bearing key (`standard:`,
# `companion:`) is a pin; a revision cited in other prose is provenance and is not
# reached. Keys such as `intent_version` name neither and are not flagged.
VERSION_KEY = re.compile(r"(standard|companion).*version|version.*(standard|companion)", re.I)
VERSION_IN_VALUE = re.compile(r"[_\-.]v\d+(\.\d+)*(\.md)?\b|@v?\d+\.\d+", re.I)
NOT_REACHED_KEYS = {"schema_version"}
IDENTITY_KEYS = {"standard", "companion"}
IDENTITY_VERSION = re.compile(r"\bv?\d+\.\d+", re.I)
def find_version_pins(node, where: str = "", identity: bool = False) -> list[str]:
"""Every place in a parsed declaration that carries a standard/companion version.
Walks every key and value (comments are gone after parsing, which is the
A12 r2 exclusion). Returns human-readable locations; empty means clean.
"""
pins: list[str] = []
if isinstance(node, dict):
for k, v in node.items():
here = f"{where}.{k}" if where else str(k)
if str(k) in NOT_REACHED_KEYS:
continue
if VERSION_KEY.search(str(k)):
pins.append(f"{here} (key names a standard/companion version)")
continue
pins.extend(find_version_pins(v, here, str(k).lower() in IDENTITY_KEYS))
elif isinstance(node, list):
for i, v in enumerate(node):
pins.extend(find_version_pins(v, f"{where}[{i}]", identity))
elif isinstance(node, str) and VERSION_IN_VALUE.search(node):
pins.append(f"{where} = {node!r} (value carries a version)")
elif isinstance(node, str) and identity and IDENTITY_VERSION.search(node):
pins.append(f"{where} = {node!r} (identity-bearing value carries a version)")
return pins
def _reject_version_pins(label: str, node) -> None:
pins = find_version_pins(node)
if pins:
print(
f"MALFORMED: {label} carries a standard/companion version — a layer "
"declaration MUST NOT, in any key or value (§11 as amended by A12 r2, "
"GH-DEC-2026-020 §1-§2):"
)
for p in pins:
print(f" {p}")
raise SystemExit(2)
# §3's vocabulary, closed, four tokens (GH-DEC-2026-017 §3, amendment A9). The
# canonical spellings are §4's catalog-column forms; comparison is ASCII
# case-insensitive, so the fold is what is stored and `Taxonomy` is in the set —
# omitting it is the defect A9 records against the estate's other validator.
LAYER_VOCABULARY = {"taxonomy", "tooling", "engine", "staff"}
def _fold(value: str) -> str:
"""ASCII case-fold, per §3 as amended: two spellings of a token are one token."""
return str(value).strip().translate(str.maketrans(
"ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"))
def load_governing_layer() -> str:
"""The declaration is INTENT.md's frontmatter `layer:` key (§11, GH-DEC-2026-017 §1).
layer.yaml is derived and does not govern. Read without a YAML frontmatter
dependency: the file is read by humans first and a parser second.
"""
if not INTENT.exists():
print(f"MISSING: {INTENT} — §11's governing declaration form")
raise SystemExit(2)
lines = INTENT.read_text().splitlines()
if not lines or lines[0].strip() != "---":
print("MALFORMED: INTENT.md has no frontmatter to carry the declaration (§11)")
raise SystemExit(2)
end = next((i for i, ln in enumerate(lines[1:], 1) if ln.strip() == "---"), None)
if end is None:
print("MALFORMED: INTENT.md frontmatter is not terminated")
raise SystemExit(2)
front = yaml.safe_load("\n".join(lines[1:end])) or {}
if "layer" not in front:
print("MALFORMED: INTENT.md frontmatter has no 'layer' key — §11's declaration")
raise SystemExit(2)
_reject_version_pins("INTENT.md frontmatter", front)
layer = front["layer"]
if _fold(layer) not in LAYER_VOCABULARY:
print(
f"MALFORMED: INTENT.md declares layer {layer!r}, outside §3's closed "
f"vocabulary {sorted(LAYER_VOCABULARY)} (case-insensitive)"
)
raise SystemExit(2)
return layer
# 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", "derived", "derived_from", "tooling_contacts"):
if key not in decl:
print(f"MALFORMED: layer.yaml has no {key!r}")
raise SystemExit(2)
# §11's derived-artifact rule: marked as derived, naming what it derives from.
if decl["derived"] is not True:
print("MALFORMED: layer.yaml must be marked 'derived: true' (§11, GH-DEC-2026-017 §1)")
raise SystemExit(2)
if decl["derived_from"] != "INTENT.md":
print(
f"MALFORMED: layer.yaml derives from {decl['derived_from']!r}; §11 names "
"INTENT.md as the governing declaration"
)
raise SystemExit(2)
# A12: the version has no home in a declaration, governing or derived.
_reject_version_pins("layer.yaml", decl)
if _fold(decl["layer"]) not in LAYER_VOCABULARY:
print(
f"MALFORMED: layer.yaml declares layer {decl['layer']!r}, outside §3's "
f"closed vocabulary {sorted(LAYER_VOCABULARY)} (case-insensitive)"
)
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()
# Printed before anything can fail, so even a MALFORMED run states what it
# checked against and over what (GH-DEC-2026-020 §4).
print(f"validated against: {VALIDATED_AGAINST}")
print(f"scope: {SCOPE}")
governing = load_governing_layer()
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: {governing} (declared in INTENT.md; §11 governing form)")
print(f" layer.yaml: derived from {decl['derived_from']}, layer: {decl['layer']}")
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
# §11 as amended (A11): the derived form MUST agree with the governing one,
# and a disagreement is a finding in its own right — reported, not resolved
# away by precedence. Comparison folds case (A9): `Staff` and `staff` agree.
if _fold(decl["layer"]) != _fold(governing):
ok = False
print(
"\nDECLARATION DISAGREEMENT — a finding under §11, not a precedence question:\n"
f" INTENT.md (governs): layer: {governing}\n"
f" layer.yaml (derived): layer: {decl['layer']}\n"
"Precedence says which value is ops-warden's answer. It does not say the\n"
"disagreement did not happen. Case is already folded, so this is a real\n"
"disagreement about a layer, not about orthography."
)
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; "
f"validated against {VALIDATED_AGAINST}"
)
elif ok:
print(
"\nPASS — every direct Tooling contact maps to a declared shape; "
f"validated against {VALIDATED_AGAINST}"
)
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())

View file

@ -118,14 +118,7 @@ def build() -> tuple[str, int]:
def _to_data_path(template: str) -> str | None:
import re
# A leading slash is a filesystem path, not an OpenBao mount. partition()
# would otherwise invent an empty mount and a fake `/data/...` address.
if (
re.search(r"[<>{}*]", template)
or " " in template
or template.startswith("k8s:")
or template.startswith("/")
):
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

View file

@ -19,7 +19,7 @@ set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
INVENTORY="${INVENTORY:-$HOME/.config/warden/inventory.yaml}"
REGISTRY="$ROOT/registry/flex-auth/production_registry_snapshot.json"
POLICY="${FLEX_AUTH_POLICY:-$HOME/access-engine/examples/ops-warden/policy_package.md}"
POLICY="${FLEX_AUTH_POLICY:-$HOME/flex-auth/examples/ops-warden/policy_package.md}"
FLEX_AUTH_BIN="${FLEX_AUTH_BIN:-/tmp/flex-auth}"
ADDR="${FLEX_AUTH_ADDR:-127.0.0.1:18090}"
PUBKEY="${PUBKEY:-$HOME/.ssh/agt-state-hub-bridge_ed25519.pub}"

View file

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

View file

@ -1,283 +0,0 @@
"""Run trusted unit tests with no inherited credentials or external I/O.
This is an accident guard for this suite, not an OS sandbox for hostile Python.
Direct pytest bypasses it. Use make test (already installed dependencies only).
"""
from __future__ import annotations
import hashlib
import json
import os
from pathlib import Path
import runpy
import socket
import subprocess
import sys
import tempfile
ROOT = Path(__file__).resolve().parents[1]
SELF = Path(__file__).resolve()
SHELL_FIXTURES = {
"printf 'the-secret-value'", "printf 'sekret'", "printf 'new'",
"printf '%s' '{\"wrap_info\":{\"token\":\"hvs.WRAP\"}}'",
"printf 'not-json'",
}
JOURNAL_CODE_SHA256 = '69aa18f302fd698104cb2253b4af0c3a077e72a0a427183e3dce1444c4926df9'
class IsolationDenied(PermissionError, subprocess.SubprocessError):
"""A unit test attempted an unreviewed external operation."""
def clean_environment(home: Path, temporary: Path) -> dict[str, str]:
# Construct, never copy/inventory the caller's environment.
return {'HOME': str(home), 'TMPDIR': str(temporary), 'PATH': '/usr/bin:/bin',
'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1',
'PYTEST_DISABLE_PLUGIN_AUTOLOAD': '1'}
def inside(path: Path, roots: tuple[Path, ...]) -> bool:
return any(path == root or root in path.parents for root in roots)
class Guard:
def __init__(self, temporary: Path, read_roots: list[Path], child: bool = False, git_metadata: bool = False):
self.temporary = temporary.resolve()
self.read_roots = tuple(p.resolve() for p in read_roots)
self.child = child
self.git_metadata = git_metadata
self.loopback_test = False
self.endpoints: set[tuple[str, int]] = set()
self.launch = None
self.original_popen = subprocess.Popen
def fixture_path(self, value: str) -> Path:
path = Path(value).resolve()
if not inside(path, (self.temporary,)):
raise IsolationDenied('unit isolation: non-fixture path')
return path
def audit(self, event, args):
if event == 'open' and not isinstance(args[0], int):
path = Path(os.fsdecode(args[0])).resolve()
mode, flags = args[1:3]
write = ((isinstance(mode, str) and any(c in mode for c in 'wax+'))
or (isinstance(flags, int) and flags & (os.O_WRONLY | os.O_RDWR | os.O_CREAT | os.O_TRUNC | os.O_APPEND)))
roots = (self.temporary,) if write else self.read_roots
if path != Path('/dev/null') and not inside(path, roots):
raise IsolationDenied('unit isolation: file outside reviewed roots')
if event == 'subprocess.Popen':
if self.launch is None or tuple(args[1]) != self.launch:
raise IsolationDenied('unit isolation: unexpected process')
if event in {'os.system', 'os.exec', 'os.posix_spawn', 'os.fork', 'os.forkpty'}:
raise IsolationDenied('unit isolation: native execution disabled')
if event.startswith('socket.'):
if not self.loopback_test or self.child:
raise IsolationDenied('unit isolation: sockets disabled')
if event == 'socket.__new__':
if args[1] != socket.AF_INET or args[2] & 15 != socket.SOCK_STREAM:
raise IsolationDenied('unit isolation: socket family/type denied')
elif event == 'socket.bind':
if args[1] != ('127.0.0.1', 0):
raise IsolationDenied('unit isolation: only ephemeral fixture bind')
elif event == 'socket.connect':
if args[1] not in self.endpoints:
raise IsolationDenied('unit isolation: only this fixture endpoint')
elif event not in {'socket.gethostname'}:
raise IsolationDenied('unit isolation: DNS/socket operation denied')
def python_payload(self, argv):
"""Closed subprocess inventory; every allowed Python child installs guard."""
from warden import case_inputs
if (len(argv) == 5 and argv[:3] == [sys.executable, '-I', '-c']
and argv[3] == case_inputs._VALIDATE):
directory = self.fixture_path(argv[4])
if not directory.name.startswith('warden-case-validator-'):
raise IsolationDenied('unit isolation: validator fixture directory differs')
# Reverify copied modules immediately before launch, independently of caller.
for relative, digest in case_inputs.SOURCE_PINS.items():
if hashlib.sha256((directory / relative).read_bytes()).hexdigest() != digest:
raise IsolationDenied('unit isolation: validator source pin differs')
return {'code': argv[3], 'args': [str(directory)]}
if len(argv) == 3 and argv[:2] == [sys.executable, '-c'] and argv[2] == 'print("SYNTHETIC-SECRET")':
return {'code': argv[2], 'args': []}
if (len(argv) == 6 and argv[:3] == [sys.executable, '-B', '-c']
and hashlib.sha256(argv[3].encode()).hexdigest() == JOURNAL_CODE_SHA256):
self.fixture_path(argv[4])
if len(argv[5]) > 192:
raise IsolationDenied('unit isolation: journal source bound')
return {'code': argv[3], 'args': argv[4:]}
if len(argv) >= 2 and argv[0] in (sys.executable, 'python3'):
script = Path(argv[1]).resolve()
args = argv[2:]
if script == ROOT / 'tests/unit_isolation_probe.py' and len(args) == 1 and args[0] in {'state', 'socket', 'provider', 'home', 'nested'}:
return {'script': str(script), 'args': args}
if script == ROOT / 'scripts/check_layer_conformance.py' and not args:
return {'script': str(script), 'args': args}
if script == ROOT / 'scripts/emit_high_risk_paths.py' and args == ['--check']:
return {'script': str(script), 'args': args, 'git_metadata': True}
if script == ROOT / 'scripts/check_principals_drift.py' and len(args) == 4 and args[::2] == ['--inventory', '--infra']:
for value in args[1::2]:
self.fixture_path(value)
return {'script': str(script), 'args': args}
if script == ROOT / 'scripts/build_flex_auth_registry.py':
if (len(args) in (3, 5) and Path(args[0]).resolve() == ROOT / 'examples/inventory.seed.yaml'
and args[-2] == '-o' and (len(args) == 3 or args[1] == '--zone-resolutions')):
self.fixture_path(args[-1])
if len(args) == 5:
self.fixture_path(args[2])
return {'script': str(script), 'args': args}
raise IsolationDenied('unit isolation: command not in reviewed subprocess inventory')
def popen(self, argv, *pos, **kw):
if pos or kw.get('executable') or kw.get('preexec_fn') or kw.get('pass_fds') or kw.get('close_fds') is False:
raise IsolationDenied('unit isolation: unexpected process options')
if kw.pop('shell', False):
if self.child or not isinstance(argv, str) or argv not in SHELL_FIXTURES:
raise IsolationDenied('unit isolation: unreviewed shell command')
return self.spawn(['/bin/bash', '--noprofile', '--norc', '-c', argv], kw)
if not isinstance(argv, (list, tuple)):
raise IsolationDenied('unit isolation: argv list required')
argv = [os.fspath(x) for x in argv]
# Never run a real provider: the only shell fixture is an exact-byte double.
from warden.doubles import _DOUBLES
metadata_commands = (['git', 'log', '-1', '--format=%H %cI', '--', str(ROOT / 'registry/routing/catalog.yaml')],
['git', 'status', '--porcelain', '--', str(ROOT / 'registry/routing/catalog.yaml')])
if self.child and not (self.git_metadata and argv in metadata_commands):
raise IsolationDenied('unit isolation: nested process disabled')
candidate = Path(argv[0])
if not candidate.is_absolute():
import shutil
resolved = shutil.which(argv[0], path=kw.get('env', os.environ).get('PATH', ''))
candidate = Path(resolved) if resolved else candidate
if self.child and self.git_metadata:
if Path(kw.get('cwd', '.')).resolve() != ROOT:
raise IsolationDenied('unit isolation: metadata cwd differs')
command = ['/usr/bin/git', '-c', 'core.fsmonitor=false', '-c', 'core.hooksPath=/dev/null', '--no-pager', *argv[1:]]
elif candidate.is_absolute() and inside(candidate.resolve(), (self.temporary,)) and candidate.name in _DOUBLES:
if candidate.read_text() != _DOUBLES[candidate.name].script:
raise IsolationDenied('unit isolation: double bytes differ')
tail = argv[1:]
if (candidate.name == 'bao' and tail not in (
['kv', 'get', '-field=NPM_AUTH_TOKEN', 'platform/x/y'],
['kv', 'get', '-field=API_KEY', 'platform/x/y/z'],
['login', '-method=oidc'], ['write', 'secret/x'])) or (candidate.name == 'key-cape' and tail != ['login']):
raise IsolationDenied('unit isolation: double arguments differ')
command = ['/bin/bash', '--noprofile', '--norc', str(candidate), *tail]
else:
payload = self.python_payload(argv)
payload.update(temporary=str(self.temporary), read_roots=[str(x) for x in self.read_roots])
command = [sys.executable, '-I', str(SELF), '--child', json.dumps(payload)]
return self.spawn(command, kw)
def spawn(self, command, kw):
cwd = Path(kw.get('cwd') or ROOT).resolve()
if not inside(cwd, (ROOT, self.temporary)):
raise IsolationDenied('unit isolation: non-fixture working directory')
kw['cwd'] = str(cwd)
# env supplied by tests is never propagated: even synthetic token mutations
# remain in the parent. All approved children have only this fresh environment.
kw['env'] = clean_environment(self.temporary / 'home', self.temporary)
kw['env'].update(GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL='/dev/null', GIT_OPTIONAL_LOCKS='0')
self.launch = tuple(command)
try:
return self.original_popen(command, **kw)
finally:
self.launch = None
def install(self):
sys.addaudithook(self.audit)
subprocess.Popen = self.popen
guard = self
original_socket = socket.socket
class FixtureSocket(original_socket):
def bind(self, address):
super().bind(address)
guard.endpoints.add(self.getsockname())
socket.socket = FixtureSocket
def numeric_addresses(host, port, family=0, type=0, proto=0, flags=0):
if not guard.loopback_test or (host, port) not in guard.endpoints:
raise IsolationDenied('unit isolation: DNS/external endpoint denied')
return [(socket.AF_INET, socket.SOCK_STREAM, 6, '', (host, port))]
socket.getaddrinfo = numeric_addresses
def local_name(host=''):
if not guard.loopback_test or host != '127.0.0.1':
raise IsolationDenied('unit isolation: hostname lookup denied')
return 'localhost'
socket.getfqdn = local_name
def pytest_configure(self, config):
config._unit_guard = self
# Exact single synthetic HTTP test only; no other test gets a loopback lease.
def pytest_runtest_setup(self, item):
self.loopback_test = item.nodeid == 'tests/test_desk.py::test_approve_flow_http_dry'
self.endpoints.clear()
def pytest_runtest_teardown(self, item):
self.loopback_test = False
self.endpoints.clear()
def worker(temporary: Path, arguments: list[str], netkingdom: Path | None, phase: Path | None):
sys.dont_write_bytecode = True
tempfile.tempdir = str(temporary)
paths = [ROOT / 'src']
if phase:
paths.append(phase / 'src')
# Explicit trusted source roots only, never ambient PYTHONPATH/plugins.
sys.path[:0] = [str(p) for p in paths]
roots = [ROOT, *paths, Path(sys.prefix), Path(sys.base_prefix), temporary]
if phase:
roots.append(phase / 'tests/fixtures/ops-warden-coordination-profile.json')
if netkingdom:
roots.append(netkingdom)
os.environ['WARDEN_TEST_NETKINGDOM_ROOT'] = str(netkingdom)
guard = Guard(temporary, roots)
guard.install()
# Pytest itself and collection run only after installation.
import pytest
raise SystemExit(pytest.main(['-p', 'no:cacheprovider', '--basetemp', str(temporary / 'pytest'), *arguments], plugins=[guard]))
def main():
if sys.argv[1:2] == ['--child']:
payload = json.loads(sys.argv[2])
temporary = Path(payload['temporary'])
sys.dont_write_bytecode = True
tempfile.tempdir = str(temporary)
sys.path.insert(0, str(ROOT / 'src'))
guard = Guard(temporary, [Path(p) for p in payload['read_roots']], child=True,
git_metadata=payload.get('git_metadata', False))
guard.install()
sys.argv = [payload.get('script', '-c'), *payload['args']]
if 'script' in payload:
runpy.run_path(payload['script'], run_name='__main__')
else:
exec(compile(payload['code'], '<reviewed-unit-child>', 'exec'), {'__name__': '__main__'})
return
if sys.argv[1:2] == ['--worker']:
temporary = Path(sys.argv[2])
netkingdom = Path(sys.argv[3]) if sys.argv[3] else None
phase = Path(sys.argv[4]) if sys.argv[4] else None
worker(temporary, sys.argv[5:], netkingdom, phase)
return
import argparse
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--netkingdom-root', type=Path, default=ROOT.parent / 'net-kingdom')
parser.add_argument('--phase-memory-root', type=Path, default=ROOT.parent / 'phase-memory')
options, arguments = parser.parse_known_args()
netkingdom = options.netkingdom_root.resolve() if options.netkingdom_root.is_dir() else None
phase = options.phase_memory_root.resolve() if (options.phase_memory_root / 'src').is_dir() else None
with tempfile.TemporaryDirectory(prefix='warden-unit-') as directory:
temporary = Path(directory)
home = temporary / 'home'
home.mkdir()
result = subprocess.run([sys.executable, '-I', str(SELF), '--worker', str(temporary),
str(netkingdom) if netkingdom else '', str(phase) if phase else '', *arguments],
cwd=ROOT, env=clean_environment(home, temporary))
raise SystemExit(result.returncode)
if __name__ == '__main__':
main()

View file

@ -12,21 +12,13 @@ reuses `expand_handoff` to build the command it runs as the caller.
"""
from __future__ import annotations
import os
from dataclasses import dataclass
from typing import Mapping, Optional
from typing import Optional
from warden.config import ConfigError, load_config
from warden.routing.models import RouteEntry
# KeyCape KEY-WP-0009-T03 is the issued coding-agent identity available today.
# This is intentionally an exact subject allowlist, not a guess based on every
# ``service:*`` identity. OpenBao validates and enforces the credential; this
# module only recognizes the already-issued subject for the advisory CLI guard.
ISSUED_AGENT_SUBJECTS = frozenset({"service:codex:railiance-platform"})
@dataclass
class ExpandedHandoff:
"""Handoff templates with `<domain>` substituted when a domain is supplied.
@ -42,25 +34,6 @@ class ExpandedHandoff:
exec_capable: bool
def agent_read_boundary_identity(
environ: Mapping[str, str] | None = None,
) -> str | None:
"""Return the issued/fallback agent marker used by the advisory read guard.
``WARDEN_POLICY_SUBJECT`` carries the principal identity used by the policy
request. When it names an issued coding-agent subject, prefer it over the
self-declared legacy marker. This function does not validate a token or
render an authorization decision; OpenBao's agent policy is the enforced
boundary. ``WARDEN_AGENT_ID`` remains a fail-toward-safety fallback.
"""
env = os.environ if environ is None else environ
issued_subject = str(env.get("WARDEN_POLICY_SUBJECT") or "").strip()
if issued_subject in ISSUED_AGENT_SUBJECTS:
return issued_subject
fallback = str(env.get("WARDEN_AGENT_ID") or "").strip()
return fallback or None
def _sub_domain(value: Optional[str], domain: Optional[str]) -> Optional[str]:
if value and domain:
return value.replace("<domain>", domain)
@ -98,10 +71,6 @@ def policy_gate_status() -> str:
cfg = load_config()
except ConfigError:
return "advisory — no warden.yaml (caller identity; gate not enforced)"
fetch_url = cfg.policy.fetch_flex_auth_url
if fetch_url:
return f"zone-aware — fetches gated by flex-auth-ops-warden-fetch at {fetch_url}"
return (
"fail-closed — secret-read evaluator unconfigured "
"(policy.fetch_flex_auth_url unset); fetches are refused"
)
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

@ -4,8 +4,6 @@ Every ops-warden action appends a JSONL event. Secret values are rejected at wri
"""
from __future__ import annotations
from warden.clock_attribution import recording_time
import json
import os
import re
@ -87,7 +85,6 @@ def record_event(
if value is None:
continue
event[key] = value
event.update(recording_time())
_assert_metadata_safe(json.dumps(event, default=str))
state_dir.mkdir(parents=True, exist_ok=True)
path = _audit_path(state_dir)

View file

@ -33,25 +33,6 @@ def _enforce_ttl(spec: CertSpec) -> None:
f"TTL {spec.ttl_hours}h exceeds maximum {max_h}h for actor type "
f"{spec.actor_type.value!r} (AccessManagementDirective §2)"
)
if spec.max_ttl_hours is not None and spec.ttl_hours > spec.max_ttl_hours:
raise CAError(
f"TTL {spec.ttl_hours}h exceeds actor {spec.actor_name!r} profile "
f"maximum {spec.max_ttl_hours}h"
)
def extension_options(spec: CertSpec) -> List[str]:
"""ssh-keygen -O arguments for the actor's extension allow-list.
``None`` keeps ssh-keygen defaults (all permit-* extensions). A list clears
every default extension and permits only the listed ones.
"""
if spec.extensions is None:
return []
opts = ["-O", "clear"]
for ext in spec.extensions:
opts += ["-O", ext]
return opts
def _evict_cert(actor_name: str, state_dir: Path) -> None:
@ -205,7 +186,6 @@ class LocalCA(CABackend):
"-I", spec.identity,
"-n", principals_str,
"-V", f"+{spec.ttl_hours}h",
*extension_options(spec),
str(pubkey_copy),
]
result = subprocess.run(cmd, capture_output=True, text=True)

View file

@ -1,98 +0,0 @@
"""Reviewed browser metadata projection; no native trust or command dispatch.
The producer validator and all reviewed producer modules are pinned below.
Their verified bytes run in a disposable tree, without caller environment or
ambient Python imports. Type parameters must match the exact reviewed version.
Machine descriptor extensions still require Executor's independent admission.
"""
import hashlib
import json
from pathlib import Path
import subprocess
import sys
import tempfile
from .case_receipt_state import Refused
# Reviewed producer: net-kingdom 015694ee414c802dc2ef1fa0120dfed610086e69.
# Review: docs/reviews/2026-10-16-browser-producer-pin-refresh.md
SOURCE_PINS = {
"tools/security-case-types/security_case_evidence.py": "da8efa59c52ad73283397ebe639ae259d0cb9c6348b5cb829135c24230dcdc75",
"tools/security-case-types/security_case_validator.py": "1bcc62e9bc374c711363295549f3cd5bb0b32952671ed4b08fddc7f74742959b",
"tools/security-scenario-composer/security_scenario_composer.py": "9f0cc69930aa8ee2cc3fb92236e6a99728df17b8c0fe36e1250241c38fc01c4f",
"tools/playbook-capability-contract/playbook_contract_validator.py": "2e1e0c809d4bc9d64c2d974cc23fecd1c5636785154c2554edd9191852fd926d",
}
PARAMETER_PINS = {
"1.1": "9ec2d6113bf70dbd35cfc247412363ff187e792bc82e24159a6f78e090aa3e4c",
"1.2": "c04385d176f5f70257fb0fa6fef7e48ab26ed7cf26f6b1c7f14b16237b0ebea6",
}
_VALIDATE = """
import json, sys
from pathlib import Path
sys.path.insert(0, str(Path(sys.argv[1]) / 'tools/security-case-types'))
import security_case_validator as v
case, typ = json.load(sys.stdin)
issues = v.validate_type(typ) + v.validate_case(case, typ, require_implemented=False)
print('valid' if not v.has_errors(issues) else 'invalid')
"""
class BrowserInputs:
"""Explicit owner configuration, never inferred from a case-supplied path."""
def __init__(self, netkingdom_root: Path):
self.root = Path(netkingdom_root)
def target(self, case: dict, typ: dict, target_parameter: str) -> str:
try:
spec = typ["spec"]
parameters = spec["parameters"]
fingerprint = hashlib.sha256(
json.dumps(parameters, sort_keys=True, separators=(",", ":")).encode()
).hexdigest()
if (
spec["id"] != "oidc-browser-relying-party"
or PARAMETER_PINS.get(spec["version"]) != fingerprint
):
raise ValueError
declared = {item["name"]: item for item in parameters}
definition = declared.get(target_parameter, {})
if (
definition.get("sensitivity") not in ("public", "operational")
or definition.get("case_settable") is not True
):
raise ValueError
# No secret references or newly classified metadata are grandfathered.
for name in case["spec"]["parameters"]:
item = declared.get(name, {})
if item.get("case_settable") is not True or item.get(
"sensitivity"
) not in ("public", "operational", "security_sensitive"):
raise ValueError
with tempfile.TemporaryDirectory(
prefix="warden-case-validator-"
) as directory:
for relative, expected in SOURCE_PINS.items():
source = (self.root / relative).read_bytes()
if hashlib.sha256(source).hexdigest() != expected:
raise ValueError
dest = Path(directory) / relative
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_bytes(source)
result = subprocess.run(
[sys.executable, "-I", "-c", _VALIDATE, directory],
input=json.dumps([case, typ]),
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
env={},
timeout=10,
check=True,
)
if result.stdout != "valid\n":
raise ValueError
return case["spec"]["parameters"][target_parameter]
except (OSError, ValueError, TypeError, KeyError, subprocess.SubprocessError):
raise Refused("browser metadata failed exact-version validation") from None

View file

@ -1,226 +0,0 @@
"""Plan a KnownSecurityCase run (WARDEN-WP-0044-T02).
``warden case plan <case.yaml>`` checks that a case is accepted and materialized
(canon ``security-case-types_v0.1``: ``proposed -> accepted -> materialized ->
live``), checks its type with :mod:`warden.case_type`, resolves each
follow-through step against the case's parameters, and prints the run: which
steps ops-warden executes as the caller's proxy, which run in an attended
session, which the owner runs, and what verification follows.
Read-only: it parses YAML and the catalog, executes nothing, and never handles a
secret value. Parameters are rendered into entry points and checks, so a
parameter whose type declares ``sensitivity: secret`` is refused outright.
"""
from __future__ import annotations
import hashlib
import re
import string
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional
import yaml
from warden.case_type import Finding, check_follow_through
from warden.routing.catalog import Catalog
PLANNABLE_CASE_STATUS = "materialized"
PLANNABLE_TYPE_STATUSES = ("acknowledged", "implemented")
_WHEN = re.compile(r"^\s*([a-z_][a-z0-9_]*)\s*==\s*(\S+)\s*$")
class CaseError(ValueError):
"""The case cannot be planned; the message says why."""
@dataclass(frozen=True)
class PlannedStep:
id: str
owner: str
mode: str # "warden" | "attended" | "owner"
entry_point: str
auth: str
lane: Optional[str]
value_handling: str
ready_check: Optional[str] = None
def as_dict(self) -> dict:
return {k: v for k, v in self.__dict__.items() if v is not None}
@dataclass
class CasePlan:
case_id: str
type_ref: str
steps: List[PlannedStep] = field(default_factory=list)
skipped: List[str] = field(default_factory=list)
attended_sessions: List[str] = field(default_factory=list)
verification: List[dict] = field(default_factory=list)
case_ref: Optional[str] = None
def as_dict(self) -> dict:
return {
"case": self.case_id,
"case_ref": self.case_ref,
"type": self.type_ref,
"steps": [s.as_dict() for s in self.steps],
"skipped": self.skipped,
"attended_sessions": self.attended_sessions,
"verification": self.verification,
}
def load_case(path: Path) -> dict:
with open(path) as fh:
doc = yaml.safe_load(fh)
if not isinstance(doc, dict) or doc.get("kind") != "SecurityCase":
raise CaseError(f"{path}: not a SecurityCase document")
return doc
def case_ref(case_id: str, case_bytes: bytes) -> str:
"""``<case_id>@sha256:<hex>`` over the case file's exact bytes.
The reference an owner entry point records for a run (RPF-WP-0054-T01). The
owner re-derives it from the file rather than trusting the caller.
"""
return f"{case_id}@sha256:{hashlib.sha256(case_bytes).hexdigest()}"
def default_type_path(case_path: Path, type_ref: str) -> Path:
"""net-kingdom layout: security-cases/<case>.yaml beside security-case-types/<id>/type.yaml."""
type_id = type_ref.split("@", 1)[0]
return case_path.resolve().parent.parent / "security-case-types" / type_id / "type.yaml"
def _applies(when: Optional[str], params: Dict[str, str]) -> bool:
if not when:
return True
match = _WHEN.match(when)
if not match:
raise CaseError(f"unsupported condition {when!r}; only '<parameter> == <value>' is planned")
name, expected = match.groups()
if name not in params:
raise CaseError(f"condition {when!r} names unknown parameter '{name}'")
return str(params[name]).lower() == expected.lower()
#: Format specs a template may apply, matching net-kingdom's validator
#: (``FORMAT_SPECS``): ``{client_id:env}`` is upper case with ``-`` as ``_``.
FORMAT_SPECS = ("", "env")
class _Param(str):
def __format__(self, spec: str) -> str:
if spec == "env":
return self.upper().replace("-", "_")
return str.__format__(self, spec)
def _render(text: Optional[str], params: Dict[str, str], where: str) -> Optional[str]:
if text is None:
return None
fields = [(f, spec or "", conv) for _, f, spec, conv in string.Formatter().parse(text) if f]
bad = sorted({f"{f}:{spec}" for f, spec, _ in fields if spec not in FORMAT_SPECS})
if bad:
raise CaseError(f"{where}: unknown format spec in {bad}; only {{param:env}} is supported")
if any(conv for _, _, conv in fields):
raise CaseError(f"{where}: conversions such as !r are not supported in templates")
missing = sorted({f for f, _, _ in fields} - set(params))
if missing:
raise CaseError(f"{where}: parameters {missing} are not set by the case")
return text.format_map({k: _Param(v) for k, v in params.items()})
def plan_case(case_doc: dict, type_doc: dict, catalog: Optional[Catalog]) -> CasePlan:
case = case_doc.get("spec") or {}
spec = type_doc.get("spec") or {}
case_id = str(case.get("id", ""))
type_ref = str(case.get("type", ""))
status = case.get("status")
if status != PLANNABLE_CASE_STATUS:
hint = {"proposed": "it is not accepted", "accepted": "ops-mason has not materialized it",
"live": "it is already live", "retired": "it is retired"}.get(status, "unknown status")
raise CaseError(f"case {case_id} is '{status}' ({hint}); planning needs '{PLANNABLE_CASE_STATUS}'")
if not case.get("acceptance"):
raise CaseError(f"case {case_id} carries no acceptance record")
want = f"{spec.get('id')}@{spec.get('version')}"
if type_ref != want:
raise CaseError(f"case names type {type_ref}, type file is {want}")
if spec.get("status") not in PLANNABLE_TYPE_STATUSES:
raise CaseError(f"type {want} is '{spec.get('status')}', not acknowledged")
report = check_follow_through(type_doc, catalog)
if report.findings:
raise CaseError("type is not plannable:\n" + "\n".join(_line(f) for f in report.findings))
declared = {p["name"]: p for p in spec.get("parameters") or [] if isinstance(p, dict) and "name" in p}
params = {k: str(v) for k, v in (case.get("parameters") or {}).items()}
unknown = sorted(set(params) - set(declared))
if unknown:
raise CaseError(f"case sets parameters the type does not declare: {unknown}")
fixed = sorted(k for k in params if declared[k].get("case_settable") is False)
if fixed:
raise CaseError(f"case sets parameters the type fixes: {fixed}")
secret = sorted(k for k in params if declared[k].get("sensitivity") == "secret")
if secret:
raise CaseError(f"case carries secret-sensitivity parameters {secret}; values never enter a plan")
for name, decl in declared.items():
if name not in params and "default" in decl:
params[name] = str(decl["default"])
plan = CasePlan(case_id=case_id, type_ref=type_ref)
for step in spec.get("follow_through") or []:
sid = step["id"]
if not _applies(step.get("when"), params):
plan.skipped.append(sid)
continue
mode = "warden" if step["warden_executes"] else "attended" if step["attended"] else "owner"
where = f"follow_through[{sid}]"
plan.steps.append(PlannedStep(
id=sid, owner=_render(str(step["owner"]), params, where), mode=mode,
entry_point=_render(step["entry_point"], params, where),
auth=_render(step["auth"], params, where),
lane=step.get("lane"), value_handling=step["value_handling"],
ready_check=_render(step.get("ready_check"), params, where),
))
if mode == "attended" and step["lane"] not in plan.attended_sessions:
plan.attended_sessions.append(step["lane"])
for item in spec.get("verification") or []:
if not _applies(item.get("when"), params):
continue
plan.verification.append({
"id": item.get("id"),
"check": _render(item.get("check"), params, f"verification[{item.get('id')}]"),
"expect": item.get("expect"),
})
return plan
def _line(f: Finding) -> str:
return f"[{f.kind}] {f.code} {f.where}: {f.message}"
def format_plan(plan: CasePlan) -> str:
labels = {"warden": "warden (caller proxy)", "attended": "ATTENDED", "owner": "owner"}
lines = [f"case {plan.case_id} ({plan.type_ref}): plan only, nothing executed"]
if plan.case_ref:
lines.append(f"case-ref: {plan.case_ref}")
lines.append("")
for n, s in enumerate(plan.steps, 1):
lane = f" lane={s.lane}" if s.lane else ""
lines.append(f"{n}. {s.id} [{labels[s.mode]}] owner={s.owner}{lane} values={s.value_handling}")
lines.append(f" entry: {s.entry_point}")
lines.append(f" auth: {s.auth}")
if s.ready_check:
lines.append(f" ready: {s.ready_check}")
if plan.skipped:
lines.append(f"skipped (condition false): {', '.join(plan.skipped)}")
lines.append(f"attended sessions: {', '.join(plan.attended_sessions) or '-'}")
lines.append("verification:")
lines.extend(f" - {v['id']}: {v['check']} (expect {v['expect']})" for v in plan.verification)
return "\n".join(lines)

View file

@ -1,146 +0,0 @@
"""Proposed, pure case receipt reducer for WARDEN-WP-0044-T03.
This prototype executes nothing and implements no accepted owner adapter API.
Callers MUST durably persist a returned running receipt before a side effect.
Only metadata identifiers/digests enter this model; no command output is accepted.
"""
from __future__ import annotations
from dataclasses import dataclass, replace
from enum import Enum
import re
_ID = re.compile(r"[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,127}\Z")
_DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z")
class Refused(ValueError):
"""A transition requires an independent owner reconciliation or new review."""
class Outcome(str, Enum):
RUNNING = "running"
SUCCESS = "verified-success"
NO_CHANGE = "verified-no-change"
FAILED_BEFORE = "failed-before-change"
FAILED_AFTER = "failed-after-change"
UNCERTAIN = "uncertain"
ROLLBACK_RUNNING = "rollback-running"
ROLLED_BACK = "verified-rolled-back"
ROLLBACK_UNCERTAIN = "rollback-uncertain"
@dataclass(frozen=True)
class Pins:
"""Exact reviewed case/type/adapter/catalog/acceptance/run-sheet digests."""
case: str
case_type: str
adapter: str
catalog: str
acceptance: str
run_sheet: str
def __post_init__(self) -> None:
if not all(_DIGEST.fullmatch(value) for value in vars(self).values()):
raise Refused("pins require sha256 digests")
@dataclass(frozen=True)
class Receipt:
attempt: int
outcome: Outcome
created: tuple[str, ...] = ()
@dataclass(frozen=True)
class StepState:
step_id: str
owner: str
pins: Pins
allowed_created: tuple[str, ...] = ()
retry_safe: bool = False
receipts: tuple[Receipt, ...] = ()
def __post_init__(self) -> None:
if not all(_ID.fullmatch(v) for v in (self.step_id, self.owner, *self.allowed_created)):
raise Refused("only metadata identifiers are supported")
if len(set(self.allowed_created)) != len(self.allowed_created):
raise Refused("duplicate reviewed object identifier")
@property
def last(self) -> Receipt | None:
return self.receipts[-1] if self.receipts else None
def _same_pins(state: StepState, pins: Pins) -> None:
if state.pins != pins:
raise Refused("pins changed; a newly reviewed sheet is required")
def _append(state: StepState, outcome: Outcome, created: tuple[str, ...] = ()) -> StepState:
receipt = Receipt(len(state.receipts) + 1, outcome, created)
return replace(state, receipts=(*state.receipts, receipt))
def begin(state: StepState, pins: Pins, *, retry: bool = False) -> StepState:
"""Return intent to dispatch. Persist before calling an owner adapter."""
_same_pins(state, pins)
last = state.last
if last is None:
if retry:
raise Refused("no failed attempt to retry")
elif not (last.outcome is Outcome.FAILED_BEFORE and state.retry_safe and retry):
raise Refused("step cannot dispatch or replay without owner reconciliation")
return _append(state, Outcome.RUNNING)
def finish(state: StepState, pins: Pins, outcome: Outcome,
*, created: tuple[str, ...] = ()) -> StepState:
"""Accept only an adapter's verified, closed metadata result, never output."""
_same_pins(state, pins)
if not state.last or state.last.outcome is not Outcome.RUNNING:
raise Refused("no running side effect")
if not isinstance(outcome, Outcome) or outcome not in (Outcome.SUCCESS, Outcome.NO_CHANGE, Outcome.FAILED_BEFORE,
Outcome.FAILED_AFTER, Outcome.UNCERTAIN):
raise Refused("unsupported adapter result")
if len(set(created)) != len(created) or not set(created) <= set(state.allowed_created):
raise Refused("created objects are outside the reviewed case")
if created and outcome not in (Outcome.SUCCESS, Outcome.FAILED_AFTER):
raise Refused("this result cannot claim created objects")
return _append(state, outcome, created)
def recover(state: StepState) -> StepState:
"""Lost result after persisted dispatch is uncertain, never safe to retry."""
if state.last and state.last.outcome is Outcome.RUNNING:
return _append(state, Outcome.UNCERTAIN)
if state.last and state.last.outcome is Outcome.ROLLBACK_RUNNING:
return _append(state, Outcome.ROLLBACK_UNCERTAIN, state.last.created)
return state
def rollback_objects(state: StepState, pins: Pins) -> tuple[str, ...]:
"""Reviewed identifiers only, reverse created order; no shared-object undo."""
_same_pins(state, pins)
last = state.last
if last is None or last.outcome is Outcome.NO_CHANGE:
return ()
if last.outcome is not Outcome.SUCCESS:
raise Refused("rollback needs a verified successful creation receipt")
return tuple(reversed(last.created))
def begin_rollback(state: StepState, pins: Pins) -> StepState:
objects = rollback_objects(state, pins)
if not objects:
raise Refused("no case-created objects to undo")
return _append(state, Outcome.ROLLBACK_RUNNING, objects)
def finish_rollback(state: StepState, pins: Pins, *, verified: bool) -> StepState:
_same_pins(state, pins)
if not state.last or state.last.outcome is not Outcome.ROLLBACK_RUNNING:
raise Refused("no running rollback")
outcome = Outcome.ROLLED_BACK if verified else Outcome.ROLLBACK_UNCERTAIN
return _append(state, outcome, state.last.created)

File diff suppressed because it is too large Load diff

View file

@ -1,173 +0,0 @@
"""SecurityCaseType follow-through check (WARDEN-WP-0044-T01).
Checks the ``follow_through`` section of a NetKingdom SecurityCaseType against
the rules ops-warden needs before it can plan a run (canon
``security-case-types_v0.1`` section on follow_through; ops-warden review
``history/2026-10-08-oidc-rp-follow-through-review.md``, R1-R3, R5-R7).
Two kinds of finding:
* ``schema`` — the type itself is wrong; net-kingdom fixes the type.
* ``catalog`` — the type is right but a lane it names is not yet a reviewed,
active catalog entry; the lane's owner supplies it. ``warden case plan``
(T02) refuses to plan while any catalog finding remains.
Read-only: it parses YAML and the catalog, executes nothing, and never handles a
secret value.
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, List, Optional
import yaml
from warden.routing.catalog import Catalog
from warden.routing.models import attended_lane_issue
VALUE_HANDLING = ("none", "generated_in_custody")
REQUIRED_STEP_FIELDS = ("id", "owner", "entry_point", "auth", "warden_executes", "attended", "value_handling")
@dataclass(frozen=True)
class Finding:
kind: str # "schema" | "catalog"
code: str
where: str
message: str
def as_dict(self) -> dict:
return {"kind": self.kind, "code": self.code, "where": self.where, "message": self.message}
@dataclass(frozen=True)
class FollowThroughReport:
type_id: str
version: str
findings: List[Finding]
attended_lanes: List[str] # one attended session per lane, in step order
warden_steps: List[str]
@property
def schema_ok(self) -> bool:
return not any(f.kind == "schema" for f in self.findings)
@property
def plannable(self) -> bool:
return not self.findings
def as_dict(self) -> dict:
return {
"type": self.type_id,
"version": self.version,
"schema_ok": self.schema_ok,
"plannable": self.plannable,
"warden_steps": self.warden_steps,
"attended_sessions": self.attended_lanes,
"findings": [f.as_dict() for f in self.findings],
}
def load_type(path: Path) -> dict:
with open(path) as fh:
doc = yaml.safe_load(fh)
if not isinstance(doc, dict) or doc.get("kind") != "SecurityCaseType":
raise ValueError(f"{path}: not a SecurityCaseType document")
return doc
def _bool(step: dict, key: str) -> Optional[bool]:
value = step.get(key)
return value if isinstance(value, bool) else None
def check_follow_through(doc: dict, catalog: Optional[Catalog] = None) -> FollowThroughReport:
spec = doc.get("spec") or {}
steps = spec.get("follow_through") or []
findings: List[Finding] = []
attended_lanes: List[str] = []
warden_steps: List[str] = []
def schema(code: str, where: str, message: str) -> None:
findings.append(Finding("schema", code, where, message))
if not steps:
schema("FT-EMPTY", "follow_through", "type declares no follow-through steps")
seen: set = set()
for index, step in enumerate(steps):
sid = step.get("id") if isinstance(step, dict) else None
where = f"follow_through[{sid or index}]"
if not isinstance(step, dict):
schema("FT-SHAPE", where, "step is not a mapping")
continue
for key in REQUIRED_STEP_FIELDS:
if key not in step or step[key] in (None, ""):
schema("FT-FIELD", where, f"missing required field '{key}'")
if sid in seen:
schema("FT-DUP", where, "duplicate step id")
seen.add(sid)
executes = _bool(step, "warden_executes")
attended = _bool(step, "attended")
for key, value in (("warden_executes", executes), ("attended", attended)):
if key in step and value is None:
schema("FT-BOOL", where, f"'{key}' must be a boolean")
# R3: values never pass through the run.
handling = step.get("value_handling")
if handling is not None and handling not in VALUE_HANDLING:
schema("FT-VALUE", where, f"value_handling '{handling}' not in {list(VALUE_HANDLING)}")
if handling == "generated_in_custody" and attended:
schema("FT-VALUE", where, "a generated-in-custody step may not run in an attended session")
# Attended steps: justified, and never executed by warden itself.
if attended:
if not step.get("attended_justification"):
schema("FT-ATTEND", where, "attended step carries no attended_justification")
if executes:
schema("FT-ATTEND", where, "a step is either warden_executes or attended, not both")
# R1: a lane for every step warden executes or groups into a session.
lane = step.get("lane")
if (executes or attended) and not lane:
schema("FT-LANE", where, "warden_executes or attended step names no catalog lane")
if lane and catalog is not None:
entry = catalog.get(lane)
if entry is None:
findings.append(Finding("catalog", "LANE-MISSING", where, f"lane '{lane}' is not in the catalog"))
elif not entry.is_active or not entry.reviewed:
findings.append(Finding("catalog", "LANE-UNREVIEWED", where, f"lane '{lane}' is not active and reviewed"))
elif attended:
issue = attended_lane_issue(entry, catalog.get)
if issue:
findings.append(Finding("catalog", issue[0], where, issue[1]))
if executes:
warden_steps.append(sid)
if attended and lane and lane not in attended_lanes:
attended_lanes.append(lane)
# R7: verification never reads a value.
for index, item in enumerate(spec.get("verification") or []):
if not isinstance(item, dict) or item.get("reads_secret_value") is not False:
vid = item.get("id", index) if isinstance(item, dict) else index
schema("VER-VALUE", f"verification[{vid}]", "reads_secret_value must be explicitly false")
# Every step must be reversible (or explicitly the consumer's to undo).
reversed_ids = {r.get("reverses") for r in spec.get("rollback") or [] if isinstance(r, dict)}
for sid in sorted(s for s in seen if s and s not in reversed_ids):
schema("RB-MISSING", f"follow_through[{sid}]", "no rollback entry reverses this step")
return FollowThroughReport(
type_id=str(spec.get("id", "")),
version=str(spec.get("version", "")),
findings=findings,
attended_lanes=attended_lanes,
warden_steps=warden_steps,
)
def summarize(findings: Iterable[Finding]) -> str:
return "\n".join(f"[{f.kind}] {f.code} {f.where}: {f.message}" for f in findings)

View file

@ -2,12 +2,12 @@
from __future__ import annotations
import json
import os
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Annotated, List, Optional
import typer
import yaml
from rich.console import Console
from rich.table import Table
@ -51,11 +51,6 @@ memory_app = typer.Typer(
no_args_is_help=True,
)
app.add_typer(memory_app, name="memory")
case_app = typer.Typer(
help="SecurityCaseType follow-through (WARDEN-WP-0044; read-only check, no execution)",
no_args_is_help=True,
)
app.add_typer(case_app, name="case")
console = Console()
err = Console(stderr=True)
@ -165,8 +160,6 @@ def sign(
ttl_hours=ttl or entry.ttl_hours,
principals=entry.principals,
identity=actor_name,
extensions=entry.extensions,
max_ttl_hours=entry.max_ttl_hours,
)
ca = _get_ca(cfg)
@ -227,8 +220,6 @@ def issue(
ttl_hours=ttl or entry.ttl_hours,
principals=entry.principals,
identity=actor_name,
extensions=entry.extensions,
max_ttl_hours=entry.max_ttl_hours,
)
try:
_apply_policy_gate(cfg, spec)
@ -712,26 +703,6 @@ def _print_entry_table(
console.print(table)
@route_app.command("scaffold")
def route_scaffold(
input_path: Annotated[Path, typer.Option("--input", help="Non-secret draft request YAML")],
output_dir: Annotated[Path, typer.Option("--output-dir", help="New staging directory")],
) -> None:
"""Generate validated draft catalog/playbook pointers offline."""
from warden.routing.scaffold import ScaffoldError, scaffold
from warden.routing.catalog import CatalogError
try:
paths = scaffold(input_path, output_dir)
except ScaffoldError as exc:
err.print(f"[red]Scaffold refused:[/red] {exc}")
raise typer.Exit(1)
except CatalogError:
err.print("[red]Scaffold refused:[/red] existing routing catalog is invalid")
raise typer.Exit(1)
console.print("Draft staging created: " + ", ".join(path.name for path in paths))
@route_app.command("list")
def route_list(
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
@ -1176,41 +1147,6 @@ def route_find(
# warden access — operator front door (advisory; proxy lands in T3)
# ---------------------------------------------------------------------------
def _openbao_caller_view(entry) -> Optional[dict]:
"""Sanitized data-plane identity. Absent for lanes that do not use it."""
if entry.warden_executes or entry.lane == "login" or not entry.exec_capable:
return None
from warden.openbao_caller import caller_boundary_note, describe_ambient_openbao_caller
described = describe_ambient_openbao_caller()
note = caller_boundary_note(described, high_risk=bool(entry.is_high_risk))
if note:
described = {**described, "boundary_note": note}
return described
def _print_openbao_caller(entry) -> None:
described = _openbao_caller_view(entry)
if described is None:
return
if described.get("error"):
console.print(
f" openbao : [yellow]{described['source']}[/yellow] — {described['error']}"
)
return
policies = ", ".join(described.get("policies") or []) or "(none)"
role = described.get("oidc_role") or "(none)"
console.print(
f" openbao : {described['source']} entity {described.get('entity_id') or '(none)'}"
)
console.print(f" role : {role} policies: {policies}")
if described.get("boundary_note"):
console.print(f" caller : [yellow]{described['boundary_note']}[/yellow]")
console.print(
" gate id : [dim]flex-auth uses policy.caller_auth, not this OpenBao entity[/dim]"
)
def _access_json(entry, expanded, gate: str, domain: Optional[str]) -> dict:
"""Stable, secret-free JSON shape for agentic operators. WP-0014 T2."""
payload = _entry_summary(entry)
@ -1223,9 +1159,6 @@ def _access_json(entry, expanded, gate: str, domain: Optional[str]) -> dict:
"policy_ref": expanded.policy_ref,
"exec_capable": expanded.exec_capable,
}
caller = _openbao_caller_view(entry)
if caller is not None:
payload["openbao_caller"] = caller
if entry.warden_executes:
payload["next_action"] = "ops-warden issues this directly — see cert_command"
payload["cert_command"] = entry.cert_command
@ -1280,7 +1213,6 @@ def _access_proxy(
goes to stderr so --fetch stdout carries only the secret.
"""
from warden.proxy import (
AttendedLoginError,
ProxyError,
build_wrapped_fetch,
caller_auth_present,
@ -1377,9 +1309,7 @@ def _access_proxy(
# T04 — agent identity on a high-risk lane: never stream raw secret data.
# Agents may use sanctioned transports (--out / --exec / --wrap / --fingerprint).
from warden.access import agent_read_boundary_identity
agent_id = agent_read_boundary_identity()
agent_id = os.environ.get("WARDEN_AGENT_ID", "").strip()
raw_value_stream = (
not is_login and not do_exec and not wrap and not out_path and not fingerprint
)
@ -1414,32 +1344,11 @@ def _access_proxy(
)
raise typer.Exit(6)
# After the refusals above, so a denied gate does not look like a fetch.
# The lookup uses the OpenBao CLI and must not run on those paths.
if not is_login:
caller = _openbao_caller_view(entry)
if caller is not None:
if caller.get("error"):
err.print(
f"[yellow]OpenBao caller:[/yellow] {caller['source']} — {caller['error']}"
)
else:
err.print(
"[dim]OpenBao caller[/dim] "
f"{caller['source']} entity {caller.get('entity_id') or '(none)'} "
f"role {caller.get('oidc_role') or '(none)'} "
f"policies {', '.join(caller.get('policies') or []) or '(none)'}"
)
if caller.get("boundary_note"):
err.print(f"[yellow]{caller['boundary_note']}[/yellow]")
action = "login" if is_login else ("exec" if do_exec else "fetch")
err.print(
f"[dim]proxy {action}: {entry.id} → {entry.owner_repo} "
f"(caller identity; value not persisted)[/dim]"
)
rc = 10 if is_login else 5
outcome = "login_failed" if is_login else "error"
try:
if is_login:
rc = proxy_attended_login_exec(resolved, child_argv=child_argv)
@ -1471,12 +1380,9 @@ def _access_proxy(
rc = 0
else:
rc = proxy_fetch(resolved)
outcome = "ok" if rc == 0 else "error"
except ProxyError as e:
if isinstance(e, AttendedLoginError):
rc, outcome = e.exit_code, e.outcome
err.print(f"[red]{e}[/red]")
raise typer.Exit(rc)
raise typer.Exit(5)
finally:
try:
write_audit(
@ -1486,8 +1392,6 @@ def _access_proxy(
domain=domain,
action=action,
decision_id=decision_id,
exit_code=rc,
outcome=outcome,
)
except OSError as e:
err.print(f"[yellow]audit write failed:[/yellow] {e}")
@ -1627,7 +1531,6 @@ def access(
if expanded.auth_method:
console.print(f" auth : {expanded.auth_method}")
_print_openbao_caller(entry)
if expanded.path_template:
console.print(f" path : {expanded.path_template}")
if expanded.fetch_command and entry.lane != "login":
@ -1639,8 +1542,6 @@ def access(
)
if expanded.policy_ref:
console.print(f" policy : {expanded.policy_ref} [dim]({gate})[/dim]")
elif entry.exec_capable:
console.print(f" gate : [dim]{gate}[/dim]")
console.print(f" wiki : {entry.wiki_ref}")
console.print(f" canon : {entry.canon_ref}")
@ -1662,10 +1563,6 @@ def access(
f" {label:<8} : [dim]{proxy} --exec -- <reviewed-command>[/dim] "
"[yellow](contained login + command; output suppressed)[/yellow]"
)
console.print(
" contract : reviewed command must be silent; any stdout/stderr "
"fails closed. Redirect both streams inside the reviewed command."
)
else:
console.print(
f" {label:<8} : [dim]{proxy} --fetch[/dim] "
@ -2280,58 +2177,3 @@ def memory_activate(
print(json.dumps(payload, indent=2))
return
console.print(warden_memory.format_activation_summary(payload))
@case_app.command("check-type")
def case_check_type(
type_file: Annotated[Path, typer.Argument(help="SecurityCaseType type.yaml")],
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
) -> None:
"""Check a type's follow_through against ops-warden's planning rules (exit 1 if not plannable)."""
from warden.case_type import check_follow_through, load_type, summarize
try:
doc = load_type(type_file)
except (OSError, ValueError, yaml.YAMLError) as exc:
err.print(f"[red]{exc}[/red]")
raise typer.Exit(2)
report = check_follow_through(doc, _load_catalog())
if output_json:
print(json.dumps(report.as_dict(), indent=2))
else:
console.print(f"{report.type_id} {report.version}: schema_ok={report.schema_ok} plannable={report.plannable}")
console.print(f"warden executes: {', '.join(report.warden_steps) or '-'}")
console.print(f"attended sessions: {', '.join(report.attended_lanes) or '-'}")
if report.findings:
console.print(summarize(report.findings), markup=False)
raise typer.Exit(0 if report.plannable else 1)
@case_app.command("plan")
def case_plan(
case_file: Annotated[Path, typer.Argument(help="SecurityCase yaml (accepted and materialized)")],
type_file: Annotated[
Optional[Path],
typer.Option("--type", help="type.yaml (default: ../security-case-types/<type>/type.yaml beside the case)"),
] = None,
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
) -> None:
"""Print a case's run (warden, attended, owner steps and verification); executes nothing (exit 1 if refused)."""
from warden.case_plan import CaseError, case_ref, default_type_path, format_plan, load_case, plan_case
from warden.case_type import load_type
try:
case_doc = load_case(case_file)
type_path = type_file or default_type_path(case_file, str((case_doc.get("spec") or {}).get("type", "")))
plan = plan_case(case_doc, load_type(type_path), _load_catalog())
plan.case_ref = case_ref(plan.case_id, case_file.read_bytes())
except CaseError as exc:
err.print(str(exc), markup=False)
raise typer.Exit(1)
except (OSError, ValueError, yaml.YAMLError) as exc:
err.print(f"[red]{exc}[/red]")
raise typer.Exit(2)
if output_json:
print(json.dumps(plan.as_dict(), indent=2))
else:
console.print(format_plan(plan), markup=False, soft_wrap=True)

View file

@ -1,17 +0,0 @@
"""Optional recording-time evidence; original local timestamps remain intact."""
import os
def recording_time():
if not os.environ.get('WARDEN_CLOCK_TRUST_FILE'):
return {}
try:
from railiance_clock.attribution import from_environment
result = from_environment('WARDEN_CLOCK_TRUST_FILE')
# These are time observations, not raw credentials. Keep audit metadata
# within the existing detector's restrictions without weakening it.
if result.get('railiance_time', {}).get('status') == 'usable':
result['railiance_time'].pop('epoch', None)
result['railiance_time'].pop('sample_id', None)
return result
except (ImportError, OSError, ValueError):
return {'railiance_time': {'status': 'unavailable', 'kind': 'recording_time'}}

View file

@ -45,11 +45,7 @@ class CallerAuthConfig:
@dataclass
class PolicyConfig:
# SSH signing pin (flex-auth-ops-warden). Admits only resource.system ops-warden.
flex_auth_url: Optional[str] = None
# Secret-read pin (flex-auth-ops-warden-fetch, CUST-WP-0075). Used only by
# check_fetch_policy; there is no fallback to flex_auth_url.
fetch_flex_auth_url: Optional[str] = None
zone_registry_path: Optional[Path] = None
failure_modes: Dict[str, str] = field(
default_factory=lambda: {
@ -215,12 +211,8 @@ def load_config(path: Optional[Path] = None) -> WardenConfig:
)
zone_registry_path = policy_raw.get("zone_registry_path")
flex_auth_url = str(policy_raw.get("flex_auth_url", "")).strip() or None
fetch_flex_auth_url = (
str(policy_raw.get("fetch_flex_auth_url", "") or "").strip() or None
)
policy_cfg = PolicyConfig(
flex_auth_url=flex_auth_url,
fetch_flex_auth_url=fetch_flex_auth_url,
zone_registry_path=(
Path(os.path.expanduser(str(zone_registry_path)))
if zone_registry_path

View file

@ -3,17 +3,11 @@ from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional
from typing import Dict, List
import yaml
from warden.models import (
ActorType,
DEFAULT_TTL_HOURS,
MAX_TTL_HOURS,
SSH_USER_EXTENSIONS,
validate_actor_name,
)
from warden.models import ActorType, DEFAULT_TTL_HOURS, validate_actor_name
class InventoryError(Exception):
@ -27,9 +21,6 @@ class ActorEntry:
principals: List[str]
ttl_hours: int
description: str = ""
# Optional issuance profile: certificate extension allow-list and TTL ceiling.
extensions: Optional[List[str]] = None
max_ttl_hours: Optional[int] = None
@dataclass
@ -44,37 +35,6 @@ class PrincipalsInventory:
hosts: Dict[str, HostEntry] = field(default_factory=dict)
def _parse_extensions(name: str, raw: object) -> Optional[List[str]]:
if raw is None:
return None
if not isinstance(raw, list):
raise InventoryError(f"Actor {name!r} extensions must be a list")
extensions = [str(e) for e in raw]
unknown = sorted(set(extensions) - SSH_USER_EXTENSIONS)
if unknown:
raise InventoryError(
f"Actor {name!r} has unknown certificate extension(s): {unknown}"
)
return extensions
def _parse_max_ttl(
name: str, actor_type: ActorType, ttl: int, raw: object
) -> Optional[int]:
if raw is None:
return None
max_ttl = int(raw) # type: ignore[arg-type]
if max_ttl <= 0 or max_ttl > MAX_TTL_HOURS[actor_type]:
raise InventoryError(
f"Actor {name!r} max_ttl_hours {max_ttl} must be 1..{MAX_TTL_HOURS[actor_type]}"
)
if ttl > max_ttl:
raise InventoryError(
f"Actor {name!r} ttl_hours {ttl} exceeds its max_ttl_hours {max_ttl}"
)
return max_ttl
def load_inventory(path: Path) -> PrincipalsInventory:
"""Load inventory.yaml. Returns empty inventory if path does not exist."""
if not path.exists():
@ -105,16 +65,12 @@ def load_inventory(path: Path) -> PrincipalsInventory:
ttl = int(data.get("ttl_hours", DEFAULT_TTL_HOURS[actor_type]))
principals = list(data.get("principals") or [name])
extensions = _parse_extensions(name, data.get("extensions"))
max_ttl = _parse_max_ttl(name, actor_type, ttl, data.get("max_ttl_hours"))
actors[name] = ActorEntry(
name=name,
actor_type=actor_type,
principals=principals,
ttl_hours=ttl,
description=str(data.get("description", "")),
extensions=extensions,
max_ttl_hours=max_ttl,
)
hosts: Dict[str, HostEntry] = {}
@ -139,12 +95,6 @@ def save_inventory(inventory: PrincipalsInventory, path: Path) -> None:
"principals": e.principals,
"ttl_hours": e.ttl_hours,
**({"description": e.description} if e.description else {}),
**({"extensions": e.extensions} if e.extensions is not None else {}),
**(
{"max_ttl_hours": e.max_ttl_hours}
if e.max_ttl_hours is not None
else {}
),
}
for name, e in inventory.actors.items()
},

View file

@ -32,18 +32,6 @@ ACTOR_PREFIX: dict[ActorType, str] = {
}
# OpenSSH user-certificate extensions (PROTOCOL.certkeys). An actor profile may
# restrict a certificate to a subset; ``None`` keeps the backend default.
SSH_USER_EXTENSIONS: frozenset[str] = frozenset({
"no-touch-required",
"permit-X11-forwarding",
"permit-agent-forwarding",
"permit-port-forwarding",
"permit-pty",
"permit-user-rc",
})
def validate_actor_name(name: str, actor_type: ActorType) -> None:
"""Raise ValueError if name does not carry the required prefix for actor_type."""
prefix = ACTOR_PREFIX[actor_type]
@ -68,10 +56,6 @@ class CertSpec:
policy_zone: Optional[str] = None
policy_failure_mode: Optional[str] = None
policy_outcome: Optional[str] = None
# Per-actor issuance profile (CUST-WP-0073-T02): explicit extension allow-list
# (None = backend default) and a TTL ceiling tighter than the type maximum.
extensions: Optional[List[str]] = None
max_ttl_hours: Optional[int] = None
def __post_init__(self) -> None:
if not self.identity:

View file

@ -1,137 +0,0 @@
"""Sanitized view of the OpenBao identity a proxied fetch will use.
``warden access`` does not select a caller. The data-plane command inherits the
process environment, and the OpenBao CLI then resolves a token in this order:
``BAO_TOKEN``, ``VAULT_TOKEN``, then ``~/.vault-token``. That identity is
independent of the flex-auth caller (``policy.caller_auth``).
The lookup response contains the token itself. This module keeps only the
fields an operator needs to see which principal will be evaluated.
"""
from __future__ import annotations
import json
import os
import subprocess
from pathlib import Path
from typing import Mapping, Optional, Sequence
_TOKEN_KEYS = ("BAO_TOKEN", "VAULT_TOKEN")
_DROPPED = {"id", "accessor", "client_token", "token", "wrap_info"}
def ambient_token_source(env: Optional[Mapping[str, str]] = None) -> str:
"""Which credential the OpenBao CLI will use. Never reads the value."""
current = os.environ if env is None else env
if current.get("BAO_TOKEN", "").strip():
return "BAO_TOKEN"
if current.get("VAULT_TOKEN", "").strip():
return "VAULT_TOKEN"
if (Path.home() / ".vault-token").is_file():
return "vault-token-file"
return "none"
def describe_ambient_openbao_caller(
env: Optional[Mapping[str, str]] = None,
*,
command: Sequence[str] = ("bao", "token", "lookup", "-format=json"),
timeout: float = 10.0,
) -> dict:
"""Return entity, policies, and auth path for the ambient token.
``id`` and ``accessor`` from the lookup payload are dropped. On failure the
result carries ``error`` and no raw CLI output.
"""
current = os.environ if env is None else env
source = ambient_token_source(current)
described = {
"source": source,
"entity_id": None,
"display_name": None,
"auth_path": None,
"policies": [],
"oidc_role": None,
"ttl": None,
"error": None,
}
if source == "none":
described["error"] = "no ambient OpenBao token"
return described
try:
completed = subprocess.run( # noqa: S603 — fixed argv, caller environment
list(command),
capture_output=True,
text=True,
timeout=timeout,
env=dict(current),
check=False,
)
except FileNotFoundError:
described["error"] = "bao client not found"
return described
except subprocess.TimeoutExpired:
described["error"] = "token lookup timed out"
return described
except OSError:
described["error"] = "token lookup could not start"
return described
if completed.returncode != 0:
described["error"] = "token lookup failed"
return described
try:
payload = json.loads(completed.stdout or "")
except json.JSONDecodeError:
described["error"] = "token lookup returned non-JSON"
return described
data = payload.get("data") if isinstance(payload, dict) else None
if not isinstance(data, dict):
described["error"] = "token lookup returned no data"
return described
meta = data.get("meta") if isinstance(data.get("meta"), dict) else {}
role = meta.get("role")
policies = data.get("policies")
described.update(
{
"entity_id": _text(data.get("entity_id")),
"display_name": _text(data.get("display_name")),
"auth_path": _text(data.get("path")),
"policies": [str(item) for item in policies] if isinstance(policies, list) else [],
"oidc_role": role if isinstance(role, str) and role else None,
"ttl": data.get("ttl") if isinstance(data.get("ttl"), int) else None,
"error": None,
}
)
# Guard the contract even if a future field is copied by mistake.
for key in _DROPPED:
described.pop(key, None)
return described
def caller_boundary_note(described: Mapping[str, object], *, high_risk: bool) -> Optional[str]:
"""One line when this token cannot be the lane's scoped reader."""
policies = described.get("policies")
names = set(policies) if isinstance(policies, list) else set()
if high_risk and "agent-high-risk-boundary" in names:
return (
"this token carries agent-high-risk-boundary, which denies data read "
"on high-risk lanes; a platform-admin capabilities check is a different caller"
)
if "platform-admin" in names:
return (
"this token is the broad platform-admin role; do not attach the lane's "
"scoped read policy to this entity"
)
return None
def _text(value: object) -> Optional[str]:
if isinstance(value, str) and value.strip():
return value
return None

View file

@ -98,48 +98,6 @@ def _candidate_row(entry: RouteEntry, score: int) -> dict:
}
#: Verbs that make a need a request to CHANGE custody rather than to read it
#: (WARDEN-WP-0038). Reported independently by key-cape (2026-09-08) and
#: railiance-platform (2026-09-09): a need saying "generate a successor secret and
#: CAS-write it to two custodians" scored well against the lane that READS that
#: path and inherited its `autonomous` verdict, answered with three read
#: transports.
#:
#: Deliberately not including "issue" or "sign". SSH certificate issuance is a
#: mutating act ops-warden owns outright, and the ownership test below is what
#: separates it from someone else's custody — not the absence of the verb.
MUTATE_TOKENS = frozenset({
"rotate", "rotating", "rotation", "rerotate",
"generate", "regenerate", "mint", "reissue",
"write", "rewrite", "put", "patch", "cas",
"provision", "reprovision", "create", "install",
"revoke", "revoking", "disable", "delete", "remove",
"replace", "successor", "reset", "restart", "update",
})
def _need_intent(need: str) -> str:
"""``mutate`` when the need asks to change a credential, else ``read``.
Token match, not substring: "update" must not fire on "updated docs" any
more than it already would, but "no-update" style hyphenation is normalised
the same way the scorer normalises it.
"""
tokens = {t.strip(".,;:()") for t in need.lower().replace("-", " ").split()}
return "mutate" if tokens & MUTATE_TOKENS else "read"
def _owns_write_authority(entry: RouteEntry) -> bool:
"""Whether ops-warden may itself perform a mutating act on this lane.
True only where ops-warden is the designed owner of the front door — the SSH
signing lane, `delegation.mode: permanent`. Everywhere else ops-warden is a
pointer or a caller-identity proxy (`ADR-0002`, `ADR-0005`), so a write is
another component's custody act and no plan verdict here can authorise it.
"""
return entry.effective_delegation.mode == "permanent"
def _score_for(catalog: Catalog, entry: RouteEntry, need: str) -> int:
if entry.id == need.strip():
return 100
@ -289,8 +247,7 @@ def _ccr_stub(need: str) -> dict:
],
"commands": [
"warden route list --all",
"warden route scaffold --input <warden-request.yaml> --output-dir <new-staging-directory>",
"# platform drafts: use railiance-platform's documented scaffold-lane command",
"# after CCR: edit registry/routing/catalog.yaml + playbook",
],
}
@ -337,7 +294,6 @@ def build_plan(
)
entry, score = matches[0]
intent = _need_intent(need)
# Draft-only top match without active alternatives → unroutable
if entry.status == "draft" and not include_draft:
@ -422,80 +378,6 @@ def build_plan(
domain=domain,
)
# --- mutating need on a lane ops-warden does not own (WARDEN-WP-0038) ------
#
# Must run before any branch that can return `autonomous`. A write is a
# custody act belonging to the lane owner, and `autonomous` is documented as
# the signal to proceed without the founder — so returning it here would let
# this front door authorise a mutation on someone else's custody. It would
# also answer a write with `--out`/`--exec`/`--wrap`, which are reads.
if intent == "mutate" and not _owns_write_authority(entry):
rotation = entry.rotation
owner = (rotation.owner if rotation else None) or entry.owner_repo
reasons = [
"need asks to change a credential, not read one",
f"write authority on this lane belongs to {owner}, not ops-warden",
]
if rotation is not None:
act = FounderAct(
kind="approve",
summary=(
f"Attended owner act required to change {entry.id} — "
f"{owner} holds write authority"
),
details={
"lane_id": entry.id,
"write_owner": owner,
"rotation_method": rotation.method,
"rotation_automatable": rotation.automatable,
"wiki_ref": entry.wiki_ref,
"guidance_command": f"warden rotate-guide {entry.id}",
"desk_hint": f"warden desk --act approve --lane {entry.id}",
},
)
if not rotation.automatable:
reasons.append(
"lane records the rotation as not automatable — an executable "
"driver must not attempt it"
)
return AccessPlan(
need=need,
verdict="founder_required",
organization_posture=org,
policy_gate=gate,
lane_id=entry.id,
lane_title=entry.title,
match_score=score,
# Deliberately empty: every command this lane offers is a READ,
# and offering one against a write need is the reported defect.
commands=[f"warden rotate-guide {entry.id} # guidance, not execution"],
founder_act=act,
catalog=freshness,
candidates=candidates,
reasons=reasons,
actor=actor,
domain=domain,
)
stub = _ccr_stub(need)
stub["owner_hint"] = f"{owner} (write authority for {entry.id})"
reasons.append("lane records no rotation route — the act has no admitted transport")
return AccessPlan(
need=need,
verdict="unroutable",
organization_posture=org,
policy_gate=gate,
lane_id=entry.id,
lane_title=entry.title,
match_score=score,
commands=[],
ccr_stub=stub,
catalog=freshness,
candidates=candidates,
reasons=reasons,
actor=actor,
domain=domain,
)
if _lane_is_autonomous(entry):
return AccessPlan(
need=need,

View file

@ -131,12 +131,6 @@ def check_sign_policy(cfg: PolicyConfig, spec: CertSpec) -> str | None:
response = httpx.post(url, json=request, headers=headers, timeout=10.0)
response.raise_for_status()
except httpx.HTTPStatusError as e:
if e.response.status_code in {401, 403}:
spec.policy_outcome = "deny"
raise CAError(
f"flex-auth refused sign policy check (HTTP {e.response.status_code}); "
"caller authentication or system binding was rejected"
) from e
_evaluator_failure(
f"flex-auth rejected sign policy check (HTTP {e.response.status_code}) "
f"for security zone {zone!r}",
@ -191,21 +185,15 @@ def check_fetch_policy(
The action is ``read`` on a ``secret`` resource owned by another subsystem —
ops-warden is the conduit, not the owner. Unresolved target workload identity
selects the explicit ``unknown`` profile; no secret value enters the request.
The check goes only to ``cfg.fetch_flex_auth_url`` — the secret-read pin
(flex-auth-ops-warden-fetch, CUST-WP-0075). It never falls back to the SSH
signing pin (``cfg.flex_auth_url``), whose caller binding admits only the
``ops-warden`` system. An unset or unreachable fetch evaluator always fails
closed, whatever the zone failure mode.
"""
zone = "unknown"
fail_closed = _is_fail_closed(cfg, zone)
if cfg.fetch_flex_auth_url is None:
raise CAError(
"secret-read evaluator not configured: set policy.fetch_flex_auth_url "
"(flex-auth-ops-warden-fetch pin); fetch checks never use the SSH "
"signing pin policy.flex_auth_url"
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},
@ -219,17 +207,12 @@ def check_fetch_policy(
"context": {"need_id": need_id, "owner_repo": owner_repo, "domain": domain},
}
url = cfg.fetch_flex_auth_url.rstrip("/") + "/v1/check"
headers = _caller_headers(cfg, fail_closed=True)
url = cfg.flex_auth_url.rstrip("/") + "/v1/check"
headers = _caller_headers(cfg, fail_closed=fail_closed)
try:
response = httpx.post(url, json=request, headers=headers, timeout=10.0)
response.raise_for_status()
except httpx.HTTPStatusError as e:
if e.response.status_code in {401, 403}:
raise CAError(
f"flex-auth refused fetch policy check (HTTP {e.response.status_code}); "
"caller authentication or system binding was rejected"
) from e
_evaluator_failure(
f"flex-auth rejected fetch policy check (HTTP {e.response.status_code})",
fail_closed=fail_closed,
@ -237,10 +220,12 @@ def check_fetch_policy(
)
return None
except httpx.RequestError as e:
raise CAError(
"secret-read evaluator (flex-auth-ops-warden-fetch) unreachable at "
f"{cfg.fetch_flex_auth_url!r}; fetch refused (no fallback to the signing pin)"
) 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()

View file

@ -12,10 +12,8 @@ intact. Three guardrails are enforced here in code:
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, fails closed on any
non-allowlisted login output, requires successful persistence to a private token
helper, self-revokes, and cleans up. Audit records
are metadata only.
an isolated helper session, permits no output, 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.
@ -23,8 +21,6 @@ This module shells out but never *interprets* secret bytes in the ``--fetch`` pa
"""
from __future__ import annotations
from warden.clock_attribution import recording_time
import json
import os
import re
@ -41,51 +37,10 @@ from typing import List, Optional
from warden.routing.models import RouteEntry
_PLACEHOLDER = re.compile(r"<[^>]+>")
_OPENBAO_TOKEN = re.compile(rb"\b(?:hvs|hvb|hvr|s)\.[A-Za-z0-9_-]{8,}\b")
_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"
# Attended-login output allowlist (RPF-WP-0017 criterion 4). Structure only: the
# values are never logged, returned, stored, or included in an exception.
#
# stdout: empty, or exactly one JSON object shaped like a ``bao login -format=json``
# response (api.Secret with an ``auth`` block). ``wrap_info`` must be null or
# absent (a wrapping token would survive ``token revoke -self``), ``data`` null,
# absent or empty, and ``auth.client_token`` a non-empty string.
_LOGIN_JSON_TOP_TYPES: dict = {
"request_id": (str,),
"lease_id": (str,),
"lease_duration": (int,),
"renewable": (bool,),
"data": (dict, type(None)),
"warnings": (list, type(None)),
"wrap_info": (type(None),),
"mount_type": (str,),
"auth": (dict,),
}
_LOGIN_JSON_AUTH_TYPES: dict = {
"client_token": (str,),
"accessor": (str,),
"policies": (list, type(None)),
"token_policies": (list, type(None)),
"identity_policies": (list, type(None)),
"metadata": (dict, type(None)),
"orphan": (bool,),
"entity_id": (str,),
"lease_duration": (int,),
"renewable": (bool,),
"mfa_requirement": (dict, type(None)),
"num_uses": (int,),
}
# stderr: empty, or exactly the OpenBao OIDC CLI prompt (browser-launch or
# open-link variant, one https URL, optional wait line). Any other byte fails closed.
_LOGIN_OIDC_STDERR = re.compile(
r"Complete the login via your OIDC provider\. "
r"(?:Launching browser to:|Open the following link in your browser:)"
r"[ \t]*\n\s*https://[^\s]+[ \t]*\n"
r"\s*(?:Waiting for OIDC authentication to complete\.\.\.[ \t]*\n?\s*)?"
)
@dataclass(frozen=True)
class ResolvedFetch:
@ -103,15 +58,6 @@ class ProxyError(Exception):
"""Raised when a proxy fetch cannot be performed safely."""
class AttendedLoginError(ProxyError):
"""Value-free phase result for the attended command envelope."""
def __init__(self, message: str, *, exit_code: int, outcome: str):
super().__init__(message)
self.exit_code = exit_code
self.outcome = outcome
def _has_shell_pipe(cmd: str) -> bool:
"""True when ``cmd`` contains an unquoted shell pipe operator."""
in_single = in_double = False
@ -195,7 +141,6 @@ def write_audit(
action: str,
decision_id: Optional[str],
exit_code: Optional[int] = None,
outcome: Optional[str] = None,
) -> Path:
"""Append a metadata-only audit record. Never contains a secret value (G2)."""
state_dir.mkdir(parents=True, exist_ok=True)
@ -209,9 +154,7 @@ def write_audit(
"subject": os.environ.get("WARDEN_POLICY_SUBJECT", "").strip() or "operator",
"policy_decision_id": decision_id,
"exit_code": exit_code,
"outcome": outcome or ("ok" if exit_code in (None, 0) else "error"),
}
record.update(recording_time())
with log_path.open("a") as f:
f.write(json.dumps(record) + "\n")
try:
@ -224,11 +167,10 @@ def write_audit(
subject=record["subject"],
target=need_id,
decision_id=decision_id,
outcome=record["outcome"],
outcome="ok" if exit_code in (None, 0) else "error",
source="access",
owner_repo=owner_repo,
domain=domain,
exit_code=exit_code,
)
except Exception:
pass
@ -391,71 +333,6 @@ def _output_bytes(completed: subprocess.CompletedProcess) -> bytes:
return stdout + b"\n" + stderr
def _stream_bytes(value: object) -> bytes:
if isinstance(value, str):
return value.encode("utf-8", errors="replace")
return value if isinstance(value, bytes) else b""
def _reject_duplicate_keys(pairs: list) -> dict:
keys = [key for key, _ in pairs]
if len(keys) != len(set(keys)):
raise ValueError("duplicate key")
return dict(pairs)
def _typed_keys_allowed(obj: dict, allowed: dict) -> bool:
for key, value in obj.items():
types = allowed.get(key)
if types is None:
return False
# bool is an int subclass; never accept it where only int is expected.
if isinstance(value, bool) and bool not in types:
return False
if not isinstance(value, types):
return False
return True
def _login_stdout_allowed(raw: bytes) -> bool:
if not raw.strip():
return True
try:
parsed = json.loads(
raw.decode("utf-8"), object_pairs_hook=_reject_duplicate_keys
)
except (UnicodeDecodeError, ValueError, RecursionError):
return False
if not isinstance(parsed, dict) or "auth" not in parsed:
return False
if not _typed_keys_allowed(parsed, _LOGIN_JSON_TOP_TYPES):
return False
if parsed.get("data"):
return False
auth = parsed["auth"]
if not _typed_keys_allowed(auth, _LOGIN_JSON_AUTH_TYPES):
return False
token = auth.get("client_token")
return isinstance(token, str) and token != ""
def _login_stderr_allowed(raw: bytes) -> bool:
if not raw:
return True
try:
text = raw.decode("utf-8")
except UnicodeDecodeError:
return False
return _LOGIN_OIDC_STDERR.fullmatch(text) is not None
def _login_output_allowed(completed: subprocess.CompletedProcess) -> bool:
"""True only for allowlisted login output; checks structure, never values."""
return _login_stdout_allowed(_stream_bytes(completed.stdout)) and (
_login_stderr_allowed(_stream_bytes(completed.stderr))
)
def _revoke_contained(
bao_binary: str,
*,
@ -503,13 +380,9 @@ def proxy_attended_login_exec(
"""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
proceeds only when the private helper is populated and its captured output is
empty or allowlisted (one ``-format=json`` login object on stdout, the OIDC
prompt on stderr); anything else, persistence defects, and non-zero results
fail closed with self-revocation. The reviewed child must
remain silent. Any possibly issued token is revoked before the isolated helper
directory is removed.
child, and revocation output are captured and discarded. Any non-empty output,
persistence defect, or non-zero result fails closed; any possibly issued token
is revoked before the isolated helper directory is removed.
"""
if not child_argv:
raise ProxyError(
@ -527,10 +400,6 @@ def proxy_attended_login_exec(
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)
@ -544,7 +413,7 @@ def proxy_attended_login_exec(
login_argv = list(resolved.argv)
if not any(arg == "-format" or arg.startswith("-format=") for arg in login_argv):
login_argv.insert(2, "-format=json")
login_argv.append("-format=json")
try:
try:
@ -559,38 +428,14 @@ def proxy_attended_login_exec(
except (OSError, ProxyError):
helper_valid = False
output_allowed = _login_output_allowed(login)
if login.returncode != 0 or not helper_valid or not output_allowed:
if login.returncode != 0 or login_output.strip() 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"
reason = (
"" if output_allowed
else " (login client produced non-allowlisted output)"
)
# Fixed categories and booleans only; captured bytes stay discarded.
if login.returncode != 0:
if b"timed out waiting for response from provider" in login_output:
category = "callback_timeout"
elif b"Error handling callback" in login_output:
category = "callback_error"
elif b"Error authenticating:" in login_output:
category = "authentication_error"
else:
category = "unknown_client_failure"
elif not helper_valid:
category = "helper_persistence_missing"
else:
category = "output_shape_rejected"
raise AttendedLoginError(
"attended login failed closed before command handoff"
f"{reason}; category={category}, client_exit={login.returncode}, "
f"helper_present={helper_valid}, "
f"stdout_allowed={_login_stdout_allowed(_stream_bytes(login.stdout))}, "
f"stderr_allowed={_login_stderr_allowed(_stream_bytes(login.stderr))}; "
f"any possible issued session was contained and {status}",
exit_code=10, outcome="login_failed",
raise ProxyError(
"attended login failed closed before command handoff; any possible "
f"issued session was contained and {status}"
)
try:
@ -600,33 +445,23 @@ def proxy_attended_login_exec(
resolved.argv[0], env=env, possible_output=b""
)
status = "revoked" if revoked else "revocation could not be confirmed"
raise AttendedLoginError(
"attended command could not start; the login session was " + status,
exit_code=11, outcome="child_failed",
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
)
# Even whitespace is output. The separator added by _output_bytes is not.
if child.stdout or child.stderr:
if child.returncode != 0 or child_output.strip():
status = "revoked" if revoked else "revocation could not be confirmed"
raise AttendedLoginError(
"attended command produced output; reviewed commands must remain "
f"silent (redirect both streams); the login session was {status}",
exit_code=12, outcome="child_output",
)
if child.returncode != 0:
status = "revoked" if revoked else "revocation could not be confirmed"
raise AttendedLoginError(
f"attended command exited non-zero; the login session was {status}",
exit_code=11, outcome="child_failed",
raise ProxyError(
"attended command failed closed because it returned a failure or "
f"unexpected output; the login session was {status}"
)
if not revoked:
raise AttendedLoginError(
"attended command completed but session revocation could not be confirmed",
exit_code=13, outcome="revoke_unconfirmed",
raise ProxyError(
"attended command completed but session revocation could not be confirmed"
)
return 0
finally:
@ -635,11 +470,7 @@ def proxy_attended_login_exec(
if root_created:
root.rmdir()
except OSError as exc:
raise AttendedLoginError(
"attended login private storage cleanup failed; inspect the prior phase "
"before retrying the reviewed command",
exit_code=14, outcome="cleanup_failed",
) from exc
raise ProxyError("attended login private storage cleanup failed") from exc
def _capture_value(resolved: ResolvedFetch) -> str:

View file

@ -69,7 +69,7 @@ _REQUIRED_FIELDS = (
"workload_ref",
)
_VALID_STATUS = ("active", "draft")
_VALID_LANES = ("secret", "login", "ceremony", "attended-change")
_VALID_LANES = ("secret", "login", "ceremony")
_VALID_ROTATION_METHODS = ("rotate", "re-establish")
# Default review cadence for a catalog pointer — "is this still the right owner
@ -199,36 +199,6 @@ class CatalogFreshness:
}
def _openbao_configuration_need(query: str) -> bool:
"""Separate explicit control-plane configuration from metadata discovery.
This is a routing distinction, never approval. Exact lane IDs are handled
first; reads and surveys continue through ordinary catalog ranking.
"""
text = query.lower().replace("-", " ")
# The attended administration front door is itself explicit intent. Its
# reviewed operation may concern signer custody or KV metadata rather than
# one of the control nouns below; keyword ranking must not turn that into a
# metadata-survey approval. This selects identity, never grants authority.
if (re.search(r"\battended\b", text)
and re.search(r"\bplatform\s+administration\b", text)
and re.search(r"\bopenbao\b", text)):
return True
control = re.search(r"\b(?:polic(?:y|ies)|auth\s+roles?|token\s+roles?|"
r"database\s*(?:/| )\s*config|secrets?\s+engine)\b", text)
change = re.search(r"\b(?:configure|configuring|create|apply|install|update|"
r"administration|administer)\b", text)
return bool(re.search(r"\bopenbao\b", text) and control and change)
def _openbao_provision_need(query: str) -> bool:
"""Value provisioning belongs to custody, never the structure survey."""
text = query.lower().replace("-", " ")
return bool(re.search(r"\bopenbao\b", text)
and re.search(r"\b(?:provision|provisioning|seed|seeding)\b", text)
and re.search(r"\b(?:keys?|credentials?|secrets?|tokens?|values?)\b", text))
@dataclass
class Catalog:
path: Path
@ -262,19 +232,6 @@ class Catalog:
scored = [(e.match_score(tokens), e) for e in pool]
scored = [(s, e) for s, e in scored if s > 0]
scored.sort(key=lambda pair: (-pair[0], pair[1].id))
if _openbao_provision_need(query):
custody = self.get("openbao-api-key")
if custody is None or not custody.is_active or custody.lane != "secret":
return []
return ([custody] + [entry for _, entry in scored if entry.id != custody.id])[:limit]
if _openbao_configuration_need(query):
admin = self.get("openbao-platform-admin-login")
# A missing/inactive administration front door must not fall back to
# a survey ceremony or a secret-read transport.
if admin is None or not admin.is_active or admin.lane != "login":
return []
ranked = [admin] + [entry for _, entry in scored if entry.id != admin.id]
return ranked[:limit]
return [e for _, e in scored[:limit]]
def stale(
@ -697,9 +654,6 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry:
raise CatalogError(
f"entry {entry_id!r} has invalid lane {lane!r} (expected one of {_VALID_LANES})"
)
session_lane = str(raw["session_lane"]).strip() if raw.get("session_lane") else None
if lane == "attended-change" and not session_lane:
raise CatalogError(f"entry {entry_id!r} is an attended-change lane and must name session_lane")
risk_value = raw.get("risk")
risk = str(risk_value).strip() if risk_value is not None else "ungraded"
@ -731,7 +685,6 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry:
exec_capable=exec_capable,
policy_ref=handoff["policy_ref"],
lane=lane,
session_lane=session_lane,
exec_owner=str(raw["exec_owner"]) if raw.get("exec_owner") else None,
exec_command=handoff["exec_command"],
pointer_command=handoff["pointer_command"],

View file

@ -8,7 +8,7 @@ pointers only (the no-double-source rule, enforced in `catalog.py`).
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Callable, List, Optional
from typing import List, Optional
# Risk grade vocabulary (ADR-0007). Grades outside LOW_RISK_GRADES — including
@ -177,7 +177,6 @@ class RouteEntry:
# "ceremony" — attended owner operation such as Shamir seal/unseal. It is a
# pointer plus approval boundary, never an executable access lane.
lane: str = "secret"
session_lane: Optional[str] = None # attended-change: login lane whose session establishes it
# 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
# catalog routes to it and keeps ops-warden's own --fetch/--exec proxy as a transparent
@ -324,20 +323,3 @@ class RouteEntry:
elif any(t in h or h in t for h in haystack):
score += 1
return score
def attended_lane_issue(
entry: RouteEntry, get_entry: Callable[[str], Optional[RouteEntry]]
) -> Optional[tuple[str, str]]:
"""Shared planner/executor boundary, using one parsed catalog snapshot."""
if not entry.is_active or not entry.reviewed:
return ("LANE-UNREVIEWED", f"lane '{entry.id}' is not active and reviewed")
if entry.lane == "attended-change":
session = get_entry(entry.session_lane or "")
if (session is None or not session.is_active or not session.reviewed
or session.lane != "login"):
return ("LANE-KIND", f"attended-change lane '{entry.id}' session_lane "
f"'{entry.session_lane}' does not resolve to an active, reviewed login lane")
elif entry.lane != "login":
return ("LANE-KIND", f"attended step's lane '{entry.id}' is not a login or attended-change lane")
return None

View file

@ -1,198 +0,0 @@
"""Offline, draft-only catalog scaffolding (WARDEN-WP-0041)."""
from __future__ import annotations
from copy import deepcopy
from datetime import date
from pathlib import Path
import re
import shutil
import yaml
from warden.routing.catalog import (
Catalog,
CatalogError,
_assert_no_secret_material,
_parse_entry,
load_catalog,
)
class ScaffoldError(ValueError):
"""Safe diagnostic: field names only, never supplied values."""
FIELDS = frozenset(
{
"schema_version",
"lane_id",
"title",
"need_keywords",
"consumer_repo",
"owner_repo",
"workload_ref",
"risk",
"wiki_ref",
"canon_ref",
"platform_request_ref",
"reviewed",
}
)
WORKLOAD_FIELDS = frozenset(
{
"applicability",
"rapp_id",
"name",
"deployable",
"declaration_ref",
"reason",
"unknown_reason",
}
)
SLUG = re.compile(r"[a-z][a-z0-9]*(?:-[a-z0-9]+)*\Z")
def _screen(value: object, field: str) -> None:
if isinstance(value, dict):
for key, item in value.items():
_screen(item, field)
elif isinstance(value, list):
for item in value:
_screen(item, field)
elif isinstance(value, str):
try:
_assert_no_secret_material("draft", field, value, prose=False)
except CatalogError:
raise ScaffoldError(f"{field}: suspected secret material") from None
def render(request: object, catalog: Catalog) -> dict[str, str]:
"""Validate before any write; preserve explicit owner metadata."""
if not isinstance(request, dict) or set(request) != FIELDS:
raise ScaffoldError(
"request: required fields missing or unknown fields present"
)
if type(request["schema_version"]) is not int or request["schema_version"] != 1:
raise ScaffoldError("schema_version: expected 1")
for field in FIELDS - {"schema_version", "need_keywords", "workload_ref"}:
value = request[field]
if (
not isinstance(value, str)
or not value.strip()
or any(c in value for c in "\r\n\x00")
):
raise ScaffoldError(f"{field}: expected nonempty single-line text")
_screen(value, field)
for field in ("lane_id", "consumer_repo", "owner_repo"):
if not SLUG.fullmatch(request[field]):
raise ScaffoldError(f"{field}: expected repository/lane slug")
lane = request["lane_id"]
if catalog.get(lane) is not None:
raise ScaffoldError("lane_id: already exists in routing catalog")
if request["owner_repo"] != "railiance-platform":
raise ScaffoldError(
"owner_repo: this scaffold supports railiance-platform KV read pointers"
)
if request["risk"] not in ("standard", "high", "ungraded"):
raise ScaffoldError("risk: expected explicit standard, high or ungraded")
if not re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}", request["reviewed"]):
raise ScaffoldError("reviewed: expected ISO date")
try:
date.fromisoformat(request["reviewed"])
except ValueError:
raise ScaffoldError("reviewed: expected ISO date") from None
keywords = request["need_keywords"]
if (
not isinstance(keywords, list)
or not keywords
or any(
not isinstance(k, str) or not k.strip() or any(c in k for c in "\r\n\x00")
for k in keywords
)
):
raise ScaffoldError("need_keywords: expected nonempty text list")
_screen(keywords, "need_keywords")
workload = request["workload_ref"]
if not isinstance(workload, dict) or not set(workload) <= WORKLOAD_FIELDS:
raise ScaffoldError("workload_ref: expected known catalog fields")
if any(
not isinstance(v, str) or not v.strip() or any(c in v for c in "\r\n\x00")
for v in workload.values()
):
raise ScaffoldError("workload_ref: expected nonempty text values")
_screen(workload, "workload_ref")
entry = {
"id": lane,
"title": request["title"],
"need_keywords": deepcopy(keywords),
"owner_repo": request["owner_repo"],
"subsystem": "openbao",
"warden_executes": False,
"wiki_ref": request["wiki_ref"],
"canon_ref": request["canon_ref"],
"reviewed": request["reviewed"],
"status": "draft",
"workload_ref": deepcopy(workload),
"risk": request["risk"],
"lane": "secret",
"exec_capable": False,
}
try:
_parse_entry(entry, 0)
except CatalogError:
raise ScaffoldError(
"workload_ref: invalid catalog workload declaration"
) from None
playbook = f"""# {request["title"]}
Draft lane: `{lane}`. Consumer: `{request["consumer_repo"]}`.
Owner: `{request["owner_repo"]}`.
Platform request: `{request["platform_request_ref"]}`.
Authoritative reference: `{request["canon_ref"]}`.
## Worker checklist
- Obtain platform review and confirmed workload/auth bindings.
- Review delegated frontdoor and whole-path rotation requirements with owners.
- Use the platform owner's CCR lifecycle and evidence procedure.
- Record approved metadata, attended values and deployment receipts separately.
- Obtain positive/negative capability evidence without reading secret data.
- Review catalog-lane-promotion criteria before any activation.
This draft supplies no executable fetch, approval, grant or runtime readiness.
"""
return {
"catalog.yaml": yaml.safe_dump({"entries": [entry]}, sort_keys=False),
"playbook.md": playbook,
"request.yaml": yaml.safe_dump(deepcopy(request), sort_keys=True),
}
def scaffold(
input_path: Path, output_dir: Path, *, catalog: Catalog | None = None
) -> list[Path]:
"""Publish a new staging directory; never replace existing destinations."""
try:
request = yaml.safe_load(input_path.read_text())
except (OSError, yaml.YAMLError):
raise ScaffoldError("input: unreadable or invalid YAML") from None
outputs = render(request, catalog if catalog is not None else load_catalog())
if output_dir.exists() or output_dir.is_symlink():
raise ScaffoldError("output_dir: destination already exists")
if not output_dir.parent.is_dir() or output_dir.parent.is_symlink():
raise ScaffoldError(
"output_dir: parent must be an existing nonsymlink directory"
)
try:
output_dir.mkdir() # exclusive creation; collision races fail before writes
except OSError:
raise ScaffoldError("output_dir: cannot create new destination") from None
try:
for name, content in outputs.items():
with (output_dir / name).open("x") as stream:
stream.write(content)
except OSError:
shutil.rmtree(output_dir)
raise ScaffoldError("output_dir: publication failed") from None
return [output_dir / name for name in outputs]

View file

@ -43,20 +43,16 @@ class VaultCA(CABackend):
)
url = f"{self._cfg.addr}/v1/{self._cfg.mount}/sign/{role}"
payload = {
"public_key": pubkey_text,
"valid_principals": ",".join(spec.principals),
"ttl": f"{spec.ttl_hours}h",
"cert_type": "user",
"key_id": spec.identity,
}
if spec.extensions is not None:
# Explicit allow-list replaces the role's default_extensions.
payload["extensions"] = {ext: "" for ext in spec.extensions}
try:
response = httpx.post(
url,
json=payload,
json={
"public_key": pubkey_text,
"valid_principals": ",".join(spec.principals),
"ttl": f"{spec.ttl_hours}h",
"cert_type": "user",
"key_id": spec.identity,
},
headers={"X-Vault-Token": self._token()},
timeout=10.0,
)

View file

@ -1,20 +0,0 @@
[
{
"repo": "rapp-core-hub",
"source": "retained public snapshot from warden-1.3-kernel-fixes-r1 test preparation",
"source_sha256": "98e81fb1116aa0ac9dc1a3e7aef9dd87c6d2110d98d46fbfca2f76eec8f3a77b",
"projection_sha256": "9ce902da0c77809f1094a634244652835b162f59682e9fa6923d8c8aa4ed4a3c"
},
{
"repo": "rapp-issue-core",
"source": "retained public snapshot from warden-1.3-kernel-fixes-r1 test preparation",
"source_sha256": "0425c9fe84e26b359b2a35fc37ef8fcb781de1aaa6229c59fcd246ee523514a5",
"projection_sha256": "a1956deecf2f0fbca44549e5ea5e8f1c4b74ebf91a9482c64c982491af0d923b"
},
{
"repo": "rapp-qonto",
"source": "retained public snapshot from warden-1.3-kernel-fixes-r1 test preparation",
"source_sha256": "6279dc2f3ab09519dffa1ceeedb2095f1182960e0106109a87aa5c0052446883",
"projection_sha256": "15aa9809e8d6a120c516be5ce9adfb72c00382140c171e791083ad117f503b3f"
}
]

View file

@ -1,15 +0,0 @@
# Public declaration projection for the published coverage snapshot; not live evidence.
rapp_id: rapp-core-hub
workload_identity:
name: core-hub
composition:
member_repos:
- deployables:
- core-hub-api
- core-hub-migrate
- hub-core-candidate
- hub-core-candidate-migrate
- repository-classification-publisher
data_classification: confidential
criticality: high
readiness_state: verified

View file

@ -1,11 +0,0 @@
# Public declaration projection for the published coverage snapshot; not live evidence.
rapp_id: rapp-issue-core
workload_identity:
name: issue-core
composition:
member_repos:
- deployables:
- issue-core
data_classification: confidential
criticality: high
readiness_state: verified

View file

@ -1,11 +0,0 @@
# Public declaration projection for the published coverage snapshot; not live evidence.
rapp_id: rapp-qonto
workload_identity:
name: qonto
composition:
member_repos:
- deployables:
- rapp-qonto
data_classification: restricted
criticality: critical
readiness_state: verified

View file

@ -4,45 +4,15 @@ from __future__ import annotations
import json
from pathlib import Path
import pytest
from typer.testing import CliRunner
from warden.access import agent_read_boundary_identity, expand_handoff, policy_gate_status
from warden.access import expand_handoff, policy_gate_status
from warden.cli import app
from warden.routing.models import RouteEntry
runner = CliRunner()
@pytest.fixture(autouse=True)
def synthetic_openbao_advisory_identity(monkeypatch):
"""Advisory CLI tests must not inspect or use workstation credentials.
Identity-display tests override this stub explicitly. The real helper has
separate subprocess-mocked unit coverage in test_openbao_caller.py.
"""
monkeypatch.setattr(
"warden.openbao_caller.describe_ambient_openbao_caller",
lambda *args, **kwargs: {
"source": "none", "entity_id": None, "display_name": None,
"auth_path": None, "policies": [], "oidc_role": None, "ttl": None,
"error": "synthetic test: no ambient OpenBao token",
},
)
def test_advisory_does_not_invoke_ambient_lookup(monkeypatch):
monkeypatch.setenv("BAO_TOKEN", "fixture-only-never-a-real-token")
def forbidden(*args, **kwargs):
raise AssertionError("advisory unit tests must not launch a token lookup")
monkeypatch.setattr("warden.openbao_caller.subprocess.run", forbidden)
result = runner.invoke(app, ["access", "npm token", "--json"])
assert result.exit_code == 0
assert json.loads(result.stdout)["openbao_caller"]["source"] == "none"
def _repo_catalog() -> Path:
return Path(__file__).resolve().parents[1] / "registry" / "routing" / "catalog.yaml"
@ -94,60 +64,8 @@ def test_policy_gate_status_no_config(monkeypatch, tmp_path):
assert "advisory" in policy_gate_status()
def test_issued_coding_agent_subject_drives_read_boundary_without_legacy_marker():
env = {"WARDEN_POLICY_SUBJECT": "service:codex:railiance-platform"}
assert agent_read_boundary_identity(env) == "service:codex:railiance-platform"
def test_legacy_agent_marker_remains_fail_toward_safety_fallback():
env = {"WARDEN_AGENT_ID": "grok"}
assert agent_read_boundary_identity(env) == "grok"
def test_non_agent_policy_subject_does_not_manufacture_agent_identity():
env = {"WARDEN_POLICY_SUBJECT": "iam:bernd"}
assert agent_read_boundary_identity(env) is None
def test_explicit_empty_environment_does_not_inherit_process_identity(monkeypatch):
monkeypatch.setenv("WARDEN_AGENT_ID", "codex")
monkeypatch.setenv("WARDEN_POLICY_SUBJECT", "service:codex:railiance-platform")
assert agent_read_boundary_identity({}) is None
assert agent_read_boundary_identity() == "service:codex:railiance-platform"
def test_operator_subject_does_not_override_legacy_agent_marker():
env = {"WARDEN_POLICY_SUBJECT": "iam:bernd", "WARDEN_AGENT_ID": "codex"}
assert agent_read_boundary_identity(env) == "codex"
# --- CLI -------------------------------------------------------------------
def test_access_advisory_shows_ambient_openbao_caller(monkeypatch):
monkeypatch.setenv("WARDEN_ROUTING_CATALOG", str(_repo_catalog()))
def described(env=None, **kwargs):
return {
"source": "BAO_TOKEN",
"entity_id": "4a1f663c-0621-0483-edd0-f852274f4b6b",
"display_name": "platform-root",
"auth_path": "auth/netkingdom/oidc/callback",
"policies": ["agent-high-risk-boundary"],
"oidc_role": "coding-agent",
"ttl": 900,
"error": None,
}
monkeypatch.setattr("warden.openbao_caller.describe_ambient_openbao_caller", described)
r = runner.invoke(app, ["access", "reuse-surface hub write token", "--json"])
assert r.exit_code == 0
payload = json.loads(r.stdout)
caller = payload["openbao_caller"]
assert caller["entity_id"] == "4a1f663c-0621-0483-edd0-f852274f4b6b"
assert "agent-high-risk-boundary" in caller["boundary_note"]
assert "id" not in caller
def test_access_advisory_output(monkeypatch):
monkeypatch.setenv("WARDEN_ROUTING_CATALOG", str(_repo_catalog()))
r = runner.invoke(app, ["access", "npm token", "--domain", "coulomb_social"])

View file

@ -81,10 +81,6 @@ class TestToDataPath:
"""`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
def test_absolute_filesystem_path_has_no_kv_address(self):
"""A kubeconfig path must not become an empty-mount `/data/...` address."""
assert mod.to_data_path("/home/worsch/.kube/config-railiance01") is None
class TestAgainstTheRealCatalog:
def test_every_high_risk_lane_resolves_or_is_explicitly_pattern(self):

View file

@ -1,153 +0,0 @@
"""Issuance profile for agt-custodian (CUST-WP-0073-T02, WARDEN-WP-0045).
No real signing: ssh-keygen and Vault are mocked; no CA key is read.
"""
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
import yaml
from warden.ca import CAError, LocalCA, _enforce_ttl, extension_options
from warden.inventory import InventoryError, load_inventory
from warden.models import ActorType, CertSpec, MAX_TTL_HOURS
from warden.routing.catalog import load_catalog
from warden.vault import VaultCA
from warden.config import VaultConfig
ROOT = Path(__file__).resolve().parents[1]
SEED = ROOT / "examples" / "inventory.seed.yaml"
CATALOG = ROOT / "registry" / "routing" / "catalog.yaml"
@pytest.fixture
def entry():
return load_inventory(SEED).actors["agt-custodian"]
def _spec(entry, pubkey: Path, ttl=None) -> CertSpec:
return CertSpec(
actor_name=entry.name,
actor_type=entry.actor_type,
pubkey_path=pubkey,
ttl_hours=ttl or entry.ttl_hours,
principals=entry.principals,
extensions=entry.extensions,
max_ttl_hours=entry.max_ttl_hours,
)
def test_profile_principal_and_type(entry):
assert entry.actor_type is ActorType.AGT
assert entry.principals == ["agt-custodian"]
def test_profile_ttl_bound(entry):
assert entry.ttl_hours == 12
assert entry.max_ttl_hours == 12
assert entry.max_ttl_hours < MAX_TTL_HOURS[ActorType.AGT]
def test_profile_extensions_pty_only(entry):
assert entry.extensions == ["permit-pty"]
for forbidden in (
"permit-agent-forwarding",
"permit-port-forwarding",
"permit-X11-forwarding",
"permit-user-rc",
):
assert forbidden not in entry.extensions
def test_ttl_override_cannot_exceed_profile(entry, tmp_path):
_enforce_ttl(_spec(entry, tmp_path / "k.pub"))
with pytest.raises(CAError, match="profile maximum 12h"):
_enforce_ttl(_spec(entry, tmp_path / "k.pub", ttl=13))
with pytest.raises(CAError):
_enforce_ttl(_spec(entry, tmp_path / "k.pub", ttl=24))
def test_local_ca_clears_default_extensions(entry, tmp_path):
pubkey = tmp_path / "agent.pub"
pubkey.write_text("ssh-ed25519 AAAA agent\n")
ca_key = tmp_path / "ca" # placeholder path; ssh-keygen is mocked
ca_key.write_text("not-a-key")
captured = {}
def fake_run(cmd, **kwargs):
if "-s" in cmd:
captured["cmd"] = cmd
Path(cmd[-1]).with_name("key-cert.pub").write_text("cert\n")
return MagicMock(returncode=0, stdout="", stderr="")
return MagicMock(
returncode=0,
stdout=(
'Key ID: "agt-custodian"\n'
"Valid: from 2026-10-20T08:00:00 to 2026-10-20T20:00:00\n"
"Principals:\n agt-custodian\n"
"Critical Options: (none)\n"
),
stderr="",
)
with patch("warden.ca.subprocess.run", side_effect=fake_run):
record = LocalCA(ca_key, tmp_path / "state").sign(_spec(entry, pubkey))
cmd = captured["cmd"]
assert cmd[cmd.index("-n") + 1] == "agt-custodian"
assert cmd[cmd.index("-V") + 1] == "+12h"
opts = [cmd[i + 1] for i, a in enumerate(cmd) if a == "-O"]
assert opts == ["clear", "permit-pty"]
assert record.principals == ["agt-custodian"]
def test_vault_payload_carries_explicit_extensions(entry, tmp_path):
pubkey = tmp_path / "agent.pub"
pubkey.write_text("ssh-ed25519 AAAA agent\n")
ca = VaultCA(
VaultConfig(addr="http://vault.invalid", role_map={"agt": "agt-role"}),
tmp_path / "state",
)
with patch.dict("os.environ", {"VAULT_TOKEN": "test"}), patch(
"warden.vault.httpx.post", side_effect=RuntimeError("stop")
) as post:
with pytest.raises(RuntimeError):
ca.sign(_spec(entry, pubkey))
payload = post.call_args.kwargs["json"]
assert payload["valid_principals"] == "agt-custodian"
assert payload["ttl"] == "12h"
assert payload["extensions"] == {"permit-pty": ""}
def test_default_actor_keeps_backend_extensions(tmp_path):
spec = CertSpec(
actor_name="agt-state-hub-bridge",
actor_type=ActorType.AGT,
pubkey_path=tmp_path / "k.pub",
ttl_hours=24,
principals=["agt-task-bridge"],
)
assert extension_options(spec) == []
def test_inventory_rejects_unknown_extension(tmp_path):
inv = tmp_path / "inv.yaml"
inv.write_text(yaml.dump({"actors": {"agt-x": {
"type": "agt", "extensions": ["permit-everything"]}}}))
with pytest.raises(InventoryError, match="unknown certificate extension"):
load_inventory(inv)
def test_inventory_rejects_ttl_above_profile_max(tmp_path):
inv = tmp_path / "inv.yaml"
inv.write_text(yaml.dump({"actors": {"agt-x": {
"type": "agt", "ttl_hours": 20, "max_ttl_hours": 12}}}))
with pytest.raises(InventoryError, match="exceeds its max_ttl_hours"):
load_inventory(inv)
def test_catalog_lane_is_attended_not_executable():
lane = load_catalog(CATALOG).get("agt-custodian-ssh-cert")
assert lane is not None
assert lane.lane == "ceremony"
assert lane.warden_executes is False

View file

@ -1,151 +0,0 @@
"""Integration: the real net-kingdom 1.3 candidate against this kernel and catalog.
Reads the actual 1.3 type, its forgejo example and the committed composition
artifact from net-kingdom, plus this branch's real routing catalog. The descriptor
map and metadata come from those committed files. All authority is fixture-only:
we change the proposed type to acknowledged, change the example case to
materialized and replace its acceptance record, promote draft lanes in memory,
and temporarily install the 1.3 parameter pin. Stub adapters replace native
adapters; an unconditional evidence verifier simulates every acceptance and caller
session. WARDEN_AGENT_ID is removed to simulate a human runtime. No adapter is
dispatched. This proves conditional source admission, not authenticated owner
acceptance, native execution or readiness; unresolved Apps sessions stay refused.
"""
from dataclasses import asdict
import hashlib
import json
import os
from pathlib import Path
from unittest import mock
import pytest
import yaml
from warden import case_inputs as inputs
from warden.case_receipt_state import Refused
from warden.case_run import Adapter, Descriptor, Executor, Journal, digest
from warden.case_type import check_follow_through
from warden.routing.catalog import load_catalog
NK = Path(os.environ.get("WARDEN_TEST_NETKINGDOM_ROOT",
Path(__file__).resolve().parents[2] / "net-kingdom"))
TYPE_DIR = NK / "security-case-type-revisions/1.3/oidc-browser-relying-party"
ARTIFACT = NK / "sso-mfa/k8s/keycape/descriptor-composition-1.3.candidate.json"
CATALOG = Path(__file__).resolve().parents[1] / "registry/routing/catalog.yaml"
pytestmark = pytest.mark.skipif(not (TYPE_DIR / "type.yaml").is_file() or not ARTIFACT.is_file(),
reason="net-kingdom 1.3 candidate not available")
DRAFT_LANES = {"keycape-attended-case-change", "platform-secret-delivery",
"consumer-owner-deployment", "apps-consumer-handover"}
def encode(desc): # net-kingdom compose-1.2-descriptors.py encode()
return json.dumps(asdict(desc), sort_keys=True, separators=(",", ":")).encode()
def type_doc(status="acknowledged"):
doc = yaml.safe_load((TYPE_DIR / "type.yaml").read_bytes())
doc["spec"]["status"] = status
return doc
def catalog_bytes(promote):
raw = yaml.safe_load(CATALOG.read_bytes())
if promote:
for entry in raw["entries"]:
if entry["id"] in DRAFT_LANES and entry.get("status") == "draft":
entry["status"] = "active"
return yaml.safe_dump(raw).encode()
def descriptors():
art = json.loads(ARTIFACT.read_text())
out = {}
for row in art["steps"]:
d = dict(row["descriptor"])
for key in ("allowed_created", "revocation_scopes"):
d[key] = tuple(d[key])
out[row["step"]] = Descriptor(**d)
return out
def pin_13():
params = type_doc()["spec"]["parameters"]
fp = hashlib.sha256(json.dumps(params, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
return mock.patch.dict(inputs.PARAMETER_PINS, {"1.3": fp})
def unused(*_):
raise AssertionError("admission must not dispatch")
def admit(tmp_path, desc, *, promote=True, pinned=True, status="acknowledged"):
case = yaml.safe_load((TYPE_DIR / "examples/forgejo.yaml").read_bytes())
case["spec"].update(status="materialized", acceptance={
"at": "2026-10-17", "record": "fixture-only acceptance, not an owner record",
"by": [{"name": "fixture", "role": "owner"}, {"name": "fixture", "role": "founder"}]})
adapter = Adapter(desc.adapter, desc.adapter_source, unused, unused)
executor = Executor(Journal(tmp_path / desc.step), {adapter.reference: adapter},
lambda purpose, source, evidence: True,
browser_inputs=inputs.BrowserInputs(NK))
with mock.patch.dict(os.environ), (pin_13() if pinned else mock.patch.dict({})):
os.environ.pop("WARDEN_AGENT_ID", None)
return executor.admit(yaml.safe_dump(case).encode(), yaml.safe_dump(type_doc(status)).encode(),
encode(desc), catalog_bytes(promote), principal="founder",
actor=desc.actor, lane=desc.lane,
acceptance_evidence="e-acceptance", materialization_evidence="e-mason",
descriptor_evidence="e-descriptor", session_evidence="e-session")
def test_artifact_descriptors_are_the_type_map():
descs = descriptors()
assert len(descs) == 7
assert {s: digest(encode(d)) for s, d in descs.items()} == type_doc()["spec"]["execution_descriptors"]
def test_conditional_admission_keeps_unresolved_apps_sessions_refused(tmp_path):
for step, desc in descriptors().items():
if step in ("deliver-consumer-secret", "hand-over"):
with pytest.raises(Refused, match="does not resolve to an active, reviewed login lane"):
admit(tmp_path, desc)
else:
admitted, invocation, _ = admit(tmp_path, desc)
assert admitted == desc
assert (invocation.step, invocation.actor, invocation.target) == (step, "human", "forgejo")
def test_draft_lane_steps_refused_until_promoted(tmp_path):
for step, desc in descriptors().items():
if desc.lane in DRAFT_LANES:
with pytest.raises(Refused, match="descriptor lane is not active in the catalog"):
admit(tmp_path, desc, promote=False)
else:
admit(tmp_path, desc, promote=False) # mint-secret's lane is already active
def test_unpinned_1_3_refused_by_browser_validation(tmp_path):
with pytest.raises(Refused, match="failed exact-version validation"):
admit(tmp_path, descriptors()["mint-secret"], pinned=False)
def test_proposed_type_refused(tmp_path):
with pytest.raises(Refused, match="type is not acknowledged"):
admit(tmp_path, descriptors()["mint-secret"], status="proposed")
def test_type_check_findings_against_the_real_catalog(tmp_path):
def findings(promote):
path = tmp_path / f"catalog-{promote}.yaml"
path.write_bytes(catalog_bytes(promote))
report = check_follow_through(type_doc("proposed"), load_catalog(path))
return {(f.code, f.where) for f in report.findings}
draft = findings(False)
assert {code for code, _ in draft} == {"LANE-UNREVIEWED"}
assert len(draft) == 6 # six steps use the four draft lanes; mint-secret's lane is active
promoted = findings(True)
# Only the two railiance-apps lanes remain: their session_lane awaits owner confirmation.
assert {code for code, _ in promoted} == {"LANE-KIND"}
assert len(promoted) == 2
assert all("deliver-consumer-secret" in where or "hand-over" in where for _, where in promoted)

View file

@ -1,136 +0,0 @@
"""KERNEL-2 lane resolution and KERNEL-3 mode-aware admission (fixture only)."""
from dataclasses import asdict, replace
import json
import pytest
import yaml
from warden.case_receipt_state import Refused
from warden.case_run import Descriptor, digest
from tests.test_case_run import MINT_STEP, fixture_catalog, lane_entry, make_rig
@pytest.fixture
def rig(tmp_path):
return make_rig(tmp_path)
EVIDENCE = {"acceptance", "materialization", "descriptor", "session"}
def admit(rig, *, step=None, entries=None, catalog=None, desc=None, **over):
executor, _, _, sources, kwargs = rig
case, typ, descriptor, _ = sources
if desc is not None:
base = Descriptor.decode(descriptor)
descriptor = json.dumps(asdict(replace(base, **desc))).encode()
doc = yaml.safe_load(typ)
doc["spec"]["execution_descriptors"] = {"mint-secret": digest(descriptor)}
if step is not None:
doc["spec"]["follow_through"] = step
typ = yaml.safe_dump(doc).encode()
if catalog is None:
catalog = fixture_catalog(*(entries or [lane_entry()]))
executor.evidence_verifier = lambda purpose, source, evidence: evidence in EVIDENCE
return executor.admit(case, typ, descriptor, catalog, **{**kwargs, **over})
def mode_step(executes, attended):
return [dict(MINT_STEP, warden_executes=executes, attended=attended)]
def refused(message, rig, **kw):
with pytest.raises(Refused, match=message):
admit(rig, **kw)
# Positive modes
def test_warden_mode_admits(rig):
assert admit(rig)[1].lane == "issuer-lane"
def test_attended_mode_admits_human_parent_session(rig, monkeypatch):
monkeypatch.delenv("WARDEN_AGENT_ID", raising=False)
admitted = admit(
rig,
step=mode_step(False, True),
entries=[dict(lane_entry(), lane="login")],
desc=dict(effect="change", revocation_scopes=("parent-session",)),
)
assert admitted[1].actor == "human"
def test_owner_mode_admits_native_owner_lane(rig):
admitted = admit(rig, step=mode_step(False, False), desc=dict(effect="change"))
assert admitted[1].owner == "owner"
# KERNEL-2
@pytest.mark.parametrize(
"entries, catalog, message",
[
(None, b"not: [a catalog", "invalid catalog source shape"),
([lane_entry("other-lane")], None, "descriptor lane is not in the catalog"),
([lane_entry(status="draft")], None, "descriptor lane is not active in the catalog"),
([{k: v for k, v in lane_entry().items() if k != "delegation"}], None,
"descriptor lane has no catalog delegation record"),
([{k: v for k, v in lane_entry().items() if k != "risk"}], None,
"descriptor lane carries no risk grade"),
([lane_entry(owner="someone-else")], None,
"descriptor owner does not fit catalog lane owner_repo"),
([lane_entry(risk="standard")], None,
"catalog lane risk does not cover the descriptor effect"),
],
)
def test_catalog_lane_refusals(rig, entries, catalog, message):
refused(message, rig, entries=entries, catalog=catalog)
# KERNEL-3: the step must declare dispatch and agree with the descriptor
@pytest.mark.parametrize(
"step, message",
[
([], "type step does not declare how the kernel dispatches it"),
(mode_step(True, True), "type step does not declare how the kernel dispatches it"),
([{k: v for k, v in MINT_STEP.items() if k != "lane"}],
"type step does not declare how the kernel dispatches it"),
([dict(MINT_STEP, warden_executes="yes")],
"type step does not declare how the kernel dispatches it"),
([dict(MINT_STEP, lane="other-lane")], "type step lane or owner disagrees with descriptor"),
([dict(MINT_STEP, owner="other")], "type step lane or owner disagrees with descriptor"),
],
)
def test_step_declaration_refusals(rig, step, message):
refused(message, rig, step=step)
def test_attended_refuses_agent_session(rig, monkeypatch):
monkeypatch.setenv("WARDEN_AGENT_ID", "agent-1")
refused("attended step requires a human parent session", rig,
step=mode_step(False, True), desc=dict(effect="change"))
def test_attended_refuses_agent_actor(rig):
refused("attended step requires a human parent session", rig,
step=mode_step(False, True), desc=dict(effect="change", actor="agent"),
actor="agent")
def test_attended_refuses_caller_lane_other_than_step_lane(rig):
refused("principal actor or lane mismatch", rig,
step=mode_step(False, True), desc=dict(effect="change"), lane="login-lane")
@pytest.mark.parametrize(
"entry, desc",
[
(lane_entry(delegation={"mode": "interim", "intended_owner": "owner",
"blocked_on": "fixture", "reviewed": "2026-10-17"}),
dict(effect="change")),
(lane_entry(owner="owner/other"), dict(effect="change")),
(lane_entry(warden_executes=True), dict(effect="change")),
(lane_entry(), {}), # owner step may not be a Warden-brokered mint
],
)
def test_owner_mode_refusals(rig, entry, desc):
refused("owner step lane is not the owner's native path", rig,
step=mode_step(False, False), entries=[entry], desc=desc)

View file

@ -1,113 +0,0 @@
"""Synthetic attended admission/dispatch regressions; no owner/native actions."""
from dataclasses import replace
import pytest
from tests.test_case_admission_modes import admit, mode_step
from tests.test_case_run import fixture_catalog, lane_entry, make_rig
from warden.case_receipt_state import Outcome, Refused
from warden.case_type import check_follow_through
from warden.routing.catalog import CatalogError, _parse_entry, load_catalog
def attended(rig, entries):
return admit(rig, step=mode_step(False, True), entries=entries,
desc=dict(effect="change", revocation_scopes=("parent-session",)))
@pytest.mark.parametrize("operation", ["run", "rollback", "reconcile"])
@pytest.mark.parametrize("marker", ["agent-1", "", None])
def test_identity_change_refuses_before_any_write_or_callback(tmp_path, monkeypatch, operation, marker):
monkeypatch.delenv("WARDEN_AGENT_ID", raising=False)
rig = make_rig(tmp_path)
executor = rig[0]
admitted = attended(rig, [dict(lane_entry(), lane="login")])
executor.evidence_verifier = lambda *_: True
if operation == "rollback":
executor.run(*admitted)
elif operation == "reconcile":
old = executor.adapters[admitted[0].adapter]
def lost(_):
raise RuntimeError("synthetic lost reply")
executor.adapters[old.reference] = replace(old, invoke=lost)
executor.run(*admitted)
calls = []
old = executor.adapters[admitted[0].adapter]
callback = getattr(old, "invoke" if operation == "run" else operation)
def observed(*args):
calls.append(args)
return callback(*args)
executor.adapters[old.reference] = replace(old, **{
"invoke" if operation == "run" else operation: observed,
})
def rows():
return tuple(executor.journal.db.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
for table in ("intents", "receipts"))
before = rows()
if marker is None:
result = getattr(executor, operation)(*admitted)
assert result.last.outcome is {"run": Outcome.SUCCESS,
"rollback": Outcome.ROLLED_BACK,
"reconcile": Outcome.NO_CHANGE}[operation]
assert len(calls) == 1
assert rows() != before
return
monkeypatch.setenv("WARDEN_AGENT_ID", marker)
with pytest.raises(Refused, match="attended step requires a human parent session"):
getattr(executor, operation)(*admitted)
assert rows() == before
assert not calls
def entries_for(case):
change = dict(lane_entry(), lane="attended-change", session_lane="login-lane")
login = dict(lane_entry("login-lane"), lane="login")
if case == "missing":
return [change]
if case == "draft-login":
login["status"] = "draft"
elif case == "unreviewed-login":
login["reviewed"] = ""
elif case == "wrong-kind-login":
login["lane"] = "secret"
elif case == "draft-change":
change["status"] = "draft"
elif case == "unreviewed-change":
change["reviewed"] = ""
elif case == "missing-session-field":
del change["session_lane"]
elif case in ("secret", "ceremony"):
change["lane"] = case
del change["session_lane"]
return [change, login]
@pytest.mark.parametrize("case, code", [
("valid", None), ("missing", "LANE-KIND"), ("draft-login", "LANE-KIND"),
("unreviewed-login", "LANE-KIND"), ("wrong-kind-login", "LANE-KIND"),
("draft-change", "LANE-UNREVIEWED"), ("unreviewed-change", "LANE-UNREVIEWED"),
("secret", "LANE-KIND"), ("ceremony", "LANE-KIND"),
])
def test_attended_change_planner_and_executor_agree(tmp_path, monkeypatch, case, code):
monkeypatch.delenv("WARDEN_AGENT_ID", raising=False)
entries = entries_for(case)
path = tmp_path / "catalog.yaml"
path.write_bytes(fixture_catalog(*entries))
catalog = load_catalog(path)
step = dict(mode_step(False, True)[0], attended_justification="synthetic test",
entry_point="owner/test", auth="synthetic session", value_handling="none")
doc = {"spec": {"follow_through": [step], "rollback": [{"reverses": step["id"]}]}}
report = check_follow_through(doc, catalog)
assert [f.code for f in report.findings] == ([] if code is None else [code])
rig = make_rig(tmp_path)
if code:
with pytest.raises(Refused):
attended(rig, entries)
assert rig[0].journal.db.execute("SELECT COUNT(*) FROM receipts").fetchone()[0] == 0
else:
assert attended(rig, entries)[1].lane == "issuer-lane"
def test_attended_change_missing_session_is_parser_rejection():
with pytest.raises(CatalogError, match="must name session_lane"):
_parse_entry(entries_for("missing-session-field")[0], 0)

View file

@ -1,105 +0,0 @@
"""Current caller authentication never replaces historical dispatch provenance."""
from dataclasses import replace
import pytest
from tests.test_case_run import make_rig
from warden.case_receipt_state import Outcome, Refused
from warden.case_run import Executor, Journal
def fresh_verifier(executor, *, allow=True):
original = executor.evidence_verifier
def verify(purpose, source, evidence):
if purpose.startswith("caller-session:") and evidence == "fresh-session":
return allow
return original(purpose, source, evidence)
executor.evidence_verifier = verify
return verify
def rows(executor):
return tuple(executor.journal.db.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
for table in ("receipts", "intents"))
def readmit(executor, sources, kwargs, reference="session"):
return executor.admit(*sources, **{**kwargs, "session_evidence": "fresh-session",
"original_session_reference": reference})
def test_normal_new_session_cannot_rewrite_original_journal_pins(tmp_path):
executor, admitted, _, sources, kwargs = make_rig(tmp_path)
executor.run(*admitted)
fresh_verifier(executor)
before = rows(executor)
changed = executor.admit(*sources, **{**kwargs,"session_evidence":"fresh-session"})
assert changed[1].pins != admitted[1].pins
with pytest.raises(Refused,match="journal pins changed"):
executor.rollback(*changed)
assert rows(executor) == before
def test_fresh_process_inverse_retains_original_pins_without_replay(tmp_path):
executor, admitted, calls, sources, kwargs = make_rig(tmp_path)
assert executor.run(*admitted).last.outcome is Outcome.SUCCESS
before = rows(executor)
fresh = Executor(Journal(executor.journal.directory), executor.adapters,
fresh_verifier(executor))
renewed = readmit(fresh, sources, kwargs)
assert renewed[1].pins == admitted[1].pins
assert rows(fresh) == before
with pytest.raises(Refused):
fresh.run(*renewed)
assert len(calls) == 1
assert fresh.rollback(*renewed).last.outcome is Outcome.ROLLED_BACK
@pytest.mark.parametrize("kind", ["missing-intent", "orphan-intent", "wrong-principal", "wrong-reference", "same-as-current", "invalid-reference", "inactive-current"])
def test_original_reference_is_never_authentication_or_new_intent(tmp_path, kind):
executor, admitted, calls, sources, kwargs = make_rig(tmp_path)
if kind != "missing-intent":
executor.run(*admitted)
if kind == "orphan-intent":
executor.journal.db.execute("DELETE FROM intents")
if kind == "wrong-principal":
kwargs = {**kwargs,"principal":"another-operator"}
fresh_verifier(executor, allow=kind != "inactive-current")
before = rows(executor)
reference = {"wrong-reference": "unrelated-session", "same-as-current": "fresh-session",
"invalid-reference": "not valid"}.get(kind, "session")
with pytest.raises(Refused):
readmit(executor, sources, kwargs, reference)
assert rows(executor) == before
def test_unknown_effect_stays_unknown_after_current_readmission(tmp_path):
executor, admitted, calls, sources, kwargs = make_rig(tmp_path)
old = executor.adapters[admitted[0].adapter]
def interrupted(inv):
calls.append(inv)
raise KeyboardInterrupt
executor.adapters[old.reference] = replace(old, invoke=interrupted)
with pytest.raises(KeyboardInterrupt):
executor.run(*admitted)
fresh_verifier(executor)
before = rows(executor)
renewed = readmit(executor, sources, kwargs)
assert rows(executor) == before
with pytest.raises(Refused):
executor.run(*renewed, retry=True)
state = executor.journal.load("mint-secret:fixture-client", renewed[2])
assert state.last.outcome is Outcome.UNCERTAIN
assert len(calls) == 1
def test_current_session_revalidated_after_readmission_before_journal_write(tmp_path):
executor, admitted, _, sources, kwargs = make_rig(tmp_path)
executor.run(*admitted)
fresh_verifier(executor)
renewed = readmit(executor, sources, kwargs)
executor.evidence_verifier = lambda *_: False
before = rows(executor)
with pytest.raises(Refused, match="caller session no longer verified"):
executor.rollback(*renewed)
assert rows(executor) == before

View file

@ -1,210 +0,0 @@
"""Actual producer validation and Executor; fake owner evidence, no native claims."""
import os
from pathlib import Path
import pytest
import yaml
from warden.case_inputs import BrowserInputs, SOURCE_PINS
from warden.case_run import digest
from warden.case_receipt_state import Outcome, Refused
from tests.test_case_run import rig as base_rig
@pytest.fixture
def rig(tmp_path):
return base_rig.__wrapped__(tmp_path)
NK = Path(
os.environ.get(
"WARDEN_TEST_NETKINGDOM_ROOT",
Path(__file__).resolve().parents[2] / "net-kingdom",
)
)
def browser(rig, version="1.1", mutate=None):
executor, _, calls, sources, kwargs = rig
case = yaml.safe_load(
(NK / "security-cases/forgejo-netkingdom-oidc.yaml").read_bytes()
)
relative = (
"security-case-types"
if version == "1.1"
else "security-case-type-revisions/1.2"
)
typ = yaml.safe_load(
(NK / relative / "oidc-browser-relying-party/type.yaml").read_bytes()
)
case["spec"]["type"] = "oidc-browser-relying-party@" + version
case["spec"]["status"] = "materialized"
if version == "1.2":
case["spec"]["parameters"]["consumer_namespace"] = "forgejo"
# This extension and all native provenance are TEST ONLY, never source assent.
typ["spec"]["execution_descriptors"] = {"mint-secret": digest(sources[2])}
for step in typ["spec"]["follow_through"]:
if step["id"] == "mint-secret": # TEST ONLY: route to the fixture lane
step.update(owner="owner", lane="issuer-lane")
if mutate:
mutate(case, typ)
changed = (
yaml.safe_dump(case).encode(),
yaml.safe_dump(typ).encode(),
*sources[2:],
)
old = executor.evidence_verifier
def verified(purpose, source, evidence):
return old(
purpose,
source.replace(digest(changed[0]), digest(sources[0])).replace(
digest(changed[1]), digest(sources[1])
),
evidence,
)
executor.evidence_verifier = verified
executor.browser_inputs = BrowserInputs(NK)
return executor, calls, changed, kwargs
@pytest.mark.parametrize("version", ["1.1", "1.2"])
def test_actual_browser_executor_and_owner_projection(rig, version):
executor, calls, sources, kwargs = browser(rig, version)
args = executor.admit(*sources, **kwargs)
assert args[1].target == "forgejo"
assert not calls
assert executor.run(*args).last.outcome is Outcome.SUCCESS
assert calls[0].target == "forgejo"
assert not hasattr(calls[0], "parameters")
# Owner callbacks/journal get target and fingerprints, no full metadata.
persisted = (executor.journal.directory / "journal.sqlite").read_bytes()
assert b"https://forgejo.coulomb.social" not in persisted
assert b"forge-admins" not in persisted
@pytest.mark.parametrize(
"field,value",
[
("redirect_uris", "https://*.example.org/callback"),
("redirect_uris", "http://example.org/callback"),
("allowed_scopes", "openid,admin"),
("client_type", "arbitrary"),
("mfa_required", False),
("unknown", "value"),
("issuer", "https://other.example"),
("display_name", "hvs." + "A" * 30),
],
)
def test_case_refusals_even_with_test_evidence(rig, field, value):
executor, calls, sources, kwargs = browser(
rig, mutate=lambda c, t: c["spec"]["parameters"].update({field: value})
)
with pytest.raises(Refused, match="exact-version"):
executor.admit(*sources, **kwargs)
assert not calls
@pytest.mark.parametrize(
"mutation",
[
lambda c, t: t["spec"].update(version="1.3"),
lambda c, t: t["spec"]["parameters"][2].update(sensitivity="public"),
lambda c, t: t["spec"]["parameters"][2].update(constraints={}),
lambda c, t: t["spec"]["parameters"].append(
{
"name": "secret",
"type": "string",
"case_settable": True,
"sensitivity": "secret_reference",
}
),
],
)
def test_type_revision_cannot_widen_reviewed_parameters(rig, mutation):
def change(case, typ):
mutation(case, typ)
case["spec"]["type"] = typ["spec"]["id"] + "@" + typ["spec"]["version"]
executor, calls, sources, kwargs = browser(rig, mutate=change)
with pytest.raises(Refused, match="exact-version"):
executor.admit(*sources, **kwargs)
assert not calls
def test_namespace_and_missing_required_are_validated(rig):
executor, calls, sources, kwargs = browser(
rig, "1.2", lambda c, t: c["spec"]["parameters"].update(consumer_namespace="*")
)
with pytest.raises(Refused, match="exact-version"):
executor.admit(*sources, **kwargs)
assert not calls
@pytest.mark.parametrize("changed_dependency", list(SOURCE_PINS))
def test_missing_producer_and_changed_transitive_source_refuse(
rig, tmp_path, changed_dependency
):
executor, calls, sources, kwargs = browser(rig)
executor.browser_inputs = BrowserInputs(tmp_path)
for relative in SOURCE_PINS:
path = tmp_path / relative
path.parent.mkdir(parents=True, exist_ok=True)
if relative != changed_dependency:
path.write_bytes((NK / relative).read_bytes())
with pytest.raises(Refused, match="exact-version"):
executor.admit(*sources, **kwargs)
(tmp_path / changed_dependency).write_text('raise RuntimeError("must not execute")')
with pytest.raises(Refused, match="exact-version"):
executor.admit(*sources, **kwargs)
assert not calls
def test_browser_without_validator_still_refuses(rig):
executor, calls, sources, kwargs = browser(rig)
executor.browser_inputs = None
with pytest.raises(Refused, match="not configured"):
executor.admit(*sources, **kwargs)
assert not calls
def test_real_unextended_type_stays_plan_only(rig):
executor, calls, sources, kwargs = browser(rig)
typ = yaml.safe_load(sources[1])
del typ["spec"]["execution_descriptors"]
with pytest.raises(Refused, match="plan only"):
executor.admit(sources[0], yaml.safe_dump(typ).encode(), *sources[2:], **kwargs)
assert not calls
def test_required_browser_parameter_is_not_defaulted_away(rig):
executor, calls, sources, kwargs = browser(
rig, mutate=lambda c, t: c["spec"]["parameters"].pop("redirect_uris")
)
with pytest.raises(Refused, match="exact-version"):
executor.admit(*sources, **kwargs)
assert not calls
@pytest.mark.parametrize("version", ["1.1", "1.2"])
@pytest.mark.parametrize(
"pins",
[
None,
[],
{"mint-secret": "sha256:" + "A" * 64},
{"mint-secret": "sha256:" + "a" * 64 + "\n"},
{"undeclared-step": "sha256:" + "a" * 64},
{"mint-secret": ["sha256:" + "a" * 64]},
{"step-" + str(n): "sha256:" + "a" * 64 for n in range(129)},
],
)
def test_actual_producer_refuses_malformed_descriptor_pins(rig, version, pins):
executor, calls, sources, _ = browser(rig, version)
case, typ = (yaml.safe_load(source) for source in sources[:2])
typ["spec"]["execution_descriptors"] = pins
with pytest.raises(Refused, match="exact-version"):
executor.browser_inputs.target(case, typ, "client_id")
assert not calls

View file

@ -1,198 +0,0 @@
"""Original-intent consistency checks with generated metadata only.
Reuse the existing synthetic owner rig. These checks do not provide a native
holder or independently authenticate original creation from local SQL/JSON.
"""
from dataclasses import asdict, replace
import json
from pathlib import Path
import sqlite3
import subprocess
import sys
import pytest
from tests.test_case_run import rig as rig
from warden.case_receipt_state import Outcome, Refused
from warden.case_run import OriginalIntent
def interrupted(rig):
executor, args, calls, sources, kwargs = rig
old = executor.adapters[args[0].adapter]
seen = []
def lost(inv):
calls.append(inv)
raise TimeoutError("generated lost response")
def original(ctx):
seen.append(ctx)
return old.reconcile(ctx.attempt)
executor.adapters[old.reference] = replace(
old, invoke=lost, reconcile=None, reconcile_original=original
)
assert executor.run(*args).last.outcome is Outcome.UNCERTAIN
return executor, args, calls, old, seen
def alter_payload(executor, **changes):
row = executor.journal.db.execute(
"SELECT seq,payload FROM intents ORDER BY seq DESC LIMIT 1"
).fetchone()
obj = json.loads(row[1])
obj.update(changes)
executor.journal.db.execute(
"UPDATE intents SET payload=? WHERE seq=?", (json.dumps(obj), row[0])
)
def test_original_actor_mismatch_is_refused(rig):
executor, args, calls, old, _ = interrupted(rig)
alter_payload(executor, actor="agent")
with pytest.raises(Refused):
executor.journal.original_intent(
"mint-secret:fixture-client", args[1], old.source
)
def test_payload_attempt_id_must_match_sql_column_and_original_receipt(rig):
executor, args, calls, old, _ = interrupted(rig)
actual = calls[0].attempt_id
alter_payload(executor, attempt_id="attempt-unrelated")
with pytest.raises(Refused):
executor.journal.original_intent(
"mint-secret:fixture-client", args[1], old.source
)
assert (
executor.journal.db.execute("SELECT attempt_id FROM intents").fetchone()[0]
== actual
)
def test_orphan_intent_without_original_running_receipt_is_refused(rig):
executor, args, calls, old, _ = interrupted(rig)
executor.journal.db.execute(
"DELETE FROM receipts WHERE seq=(SELECT MIN(seq) FROM receipts)"
)
assert (
executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome
is Outcome.UNCERTAIN
)
with pytest.raises(Refused):
executor.journal.original_intent(
"mint-secret:fixture-client", args[1], old.source
)
def test_serializer_refuses_arbitrary_values_outside_normal_admit(rig):
_, args, _, _, _ = rig
inv = replace(
args[1],
target={"unexpected": "synthetic-owner-payload"},
operation="run",
attempt_id="attempt-fixture",
)
record = OriginalIntent("mint-secret:fixture-client", inv, "sha256:" + "1" * 64)
with pytest.raises(Refused):
OriginalIntent.decode(record.payload())
# This is a direct serialization API gap, not an observed normal-admit leak.
# Owner trust is the existing synthetic fixture; this proves call shape only.
@pytest.mark.parametrize(
"field,value",
[
("principal", "another-person"),
("tenant", "another-tenant"),
("realm", "another-realm"),
],
)
def test_existing_lookup_does_refuse_stored_principal_tenant_realm_mismatch(
rig, field, value
):
executor, args, _, old, _ = interrupted(rig)
alter_payload(executor, **{field: value})
with pytest.raises(Refused):
executor.journal.original_intent(
"mint-secret:fixture-client", args[1], old.source
)
def test_existing_atomic_insert_failure_prevents_dispatch_and_both_records(rig):
executor, args, calls, _, _ = rig
executor.journal.db.execute(
"CREATE TRIGGER fail_receipt BEFORE INSERT ON receipts BEGIN SELECT RAISE(ABORT,'fixture'); END"
)
with pytest.raises(sqlite3.IntegrityError):
executor.run(*args)
assert not calls
assert executor.journal.db.execute("SELECT COUNT(*) FROM intents").fetchone() == (
0,
)
assert executor.journal.db.execute("SELECT COUNT(*) FROM receipts").fetchone() == (
0,
)
def test_existing_missing_original_refuses_before_callback(rig):
executor, args, _, _, seen = interrupted(rig)
executor.journal.db.execute("DELETE FROM intents")
with pytest.raises(Refused):
executor.reconcile(*args)
assert not seen
assert (
executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome
is Outcome.UNCERTAIN
)
def test_existing_current_parent_revocation_remains_mandatory(rig):
executor, args, _, _, _ = interrupted(rig)
old = executor.evidence_verifier
def refuse_session(purpose, source, evidence):
return (
False
if purpose.startswith("session-revocation:")
else old(purpose, source, evidence)
)
executor.evidence_verifier = refuse_session
with pytest.raises(Refused):
executor.reconcile(*args)
assert (
executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome
is Outcome.UNCERTAIN
)
def test_existing_intent_survives_actual_distinct_interpreter(rig):
executor, args, calls, old, _ = interrupted(rig)
code = """import json,sys
from pathlib import Path
from warden.case_run import Journal,Invocation
from warden.case_receipt_state import Pins
obj=json.load(sys.stdin);obj['pins']=Pins(**obj['pins']);j=Journal(Path(sys.argv[1]))
with j.locked():
i=j.original_intent('mint-secret:fixture-client',Invocation(**obj),sys.argv[2])
print(json.dumps({'attempt':i.invocation.attempt_id,'operation':i.invocation.operation}))
"""
result = subprocess.run(
[sys.executable, "-B", "-c", code, str(executor.journal.directory), old.source],
input=json.dumps(asdict(args[1])),
text=True,
capture_output=True,
timeout=15,
env={
"PATH": "/usr/bin:/bin",
"PYTHONPATH": str(Path(__file__).parents[1] / "src"),
"PYTHONDONTWRITEBYTECODE": "1",
},
check=True,
)
assert json.loads(result.stdout) == {
"attempt": calls[0].attempt_id,
"operation": "run",
}

View file

@ -1,205 +0,0 @@
"""WARDEN-WP-0044-T02: plan a KnownSecurityCase run."""
from __future__ import annotations
import copy
import os
from pathlib import Path
import pytest
import yaml
from typer.testing import CliRunner
from warden.case_plan import CaseError, plan_case
from warden.cli import app
from warden.routing.catalog import load_catalog
LIVE_CASE = Path(os.environ.get("WARDEN_TEST_NETKINGDOM_ROOT",
Path(__file__).resolve().parents[2] / "net-kingdom")) / "security-cases/forgejo-netkingdom-oidc.yaml"
TYPE = {
"apiVersion": "netkingdom.io/security-case-type/v0.1",
"kind": "SecurityCaseType",
"spec": {
"id": "t",
"version": "1.0",
"status": "acknowledged",
"parameters": [
{"name": "client_id", "case_settable": True, "sensitivity": "operational"},
{"name": "client_type", "case_settable": True, "sensitivity": "operational"},
{"name": "consumer", "case_settable": True, "sensitivity": "operational"},
{"name": "issuer", "case_settable": False, "default": "https://kc.example"},
],
"follow_through": [
{"id": "proxy", "owner": "o", "entry_point": "ensure {client_id}", "auth": "caller",
"lane": "key-cape-oidc-login", "warden_executes": True, "attended": False,
"value_handling": "none"},
{"id": "mint", "owner": "o", "entry_point": "mint {client_id}", "auth": "caller",
"lane": "key-cape-oidc-login", "warden_executes": True, "attended": False,
"value_handling": "generated_in_custody", "when": "client_type == confidential"},
{"id": "login", "owner": "o", "entry_point": "apply {client_id} env KEYCAPE_{client_id:env}_X",
"auth": "operator",
"lane": "key-cape-oidc-login", "warden_executes": False, "attended": True,
"value_handling": "none", "attended_justification": "live registry is a Secret"},
{"id": "handover", "owner": "{consumer}", "entry_point": "use {issuer}", "auth": "a",
"warden_executes": False, "attended": False, "value_handling": "none",
"ready_check": "{consumer} ready"},
],
"verification": [
{"id": "v", "check": "GET {issuer} for {client_id}", "expect": "present",
"reads_secret_value": False},
{"id": "vc", "check": "custody {client_id}", "expect": "deny",
"reads_secret_value": False, "when": "client_type == confidential"},
],
"rollback": [{"reverses": s} for s in ("proxy", "mint", "login", "handover")],
},
}
CASE = {
"apiVersion": "netkingdom.io/security-case/v0.1",
"kind": "SecurityCase",
"spec": {
"id": "c",
"type": "t@1.0",
"status": "materialized",
"acceptance": {"at": "2026-10-09", "record": "r"},
"parameters": {"client_id": "forgejo", "client_type": "public", "consumer": "railiance-apps"},
},
}
@pytest.fixture(scope="module")
def catalog():
return load_catalog()
def _case(**spec):
doc = copy.deepcopy(CASE)
doc["spec"].update(spec)
return doc
def _type():
return copy.deepcopy(TYPE)
def test_plan_resolves_modes_params_and_defaults(catalog):
plan = plan_case(_case(), _type(), catalog)
modes = {s.id: s.mode for s in plan.steps}
assert modes == {"proxy": "warden", "login": "attended", "handover": "owner"}
assert plan.skipped == ["mint"]
assert plan.attended_sessions == ["key-cape-oidc-login"]
handover = next(s for s in plan.steps if s.id == "handover")
assert handover.owner == "railiance-apps"
assert handover.entry_point == "use https://kc.example"
assert handover.ready_check == "railiance-apps ready"
assert [v["id"] for v in plan.verification] == ["v"]
assert plan.verification[0]["check"] == "GET https://kc.example for forgejo"
def test_condition_true_includes_step_and_check(catalog):
params = dict(CASE["spec"]["parameters"], client_type="confidential")
plan = plan_case(_case(parameters=params), _type(), catalog)
assert "mint" in [s.id for s in plan.steps] and not plan.skipped
assert [v["id"] for v in plan.verification] == ["v", "vc"]
@pytest.mark.parametrize("status,needle", [
("proposed", "not accepted"),
("accepted", "not materialized"),
("live", "already live"),
])
def test_refuse_case_not_materialized(catalog, status, needle):
with pytest.raises(CaseError, match=needle):
plan_case(_case(status=status), _type(), catalog)
def test_refuse_case_without_acceptance(catalog):
with pytest.raises(CaseError, match="acceptance"):
plan_case(_case(acceptance=None), _type(), catalog)
def test_refuse_type_mismatch_and_unacknowledged_type(catalog):
with pytest.raises(CaseError, match="type file is"):
plan_case(_case(type="t@2.0"), _type(), catalog)
doc = _type()
doc["spec"]["status"] = "proposed"
with pytest.raises(CaseError, match="not acknowledged"):
plan_case(_case(), doc, catalog)
def test_refuse_missing_catalog_lane(catalog):
doc = _type()
doc["spec"]["follow_through"][0]["lane"] = "no-such-lane"
with pytest.raises(CaseError, match="LANE-MISSING"):
plan_case(_case(), doc, catalog)
def test_refuse_unreviewed_catalog_lane(catalog):
draft = next((e.id for e in catalog.entries if not (e.is_active and e.reviewed)), None)
if draft is None:
pytest.skip("catalog has no inactive or unreviewed lane")
doc = _type()
doc["spec"]["follow_through"][0]["lane"] = draft
with pytest.raises(CaseError, match="LANE-UNREVIEWED"):
plan_case(_case(), doc, catalog)
def test_refuse_bad_parameters(catalog):
with pytest.raises(CaseError, match="does not declare"):
plan_case(_case(parameters=dict(CASE["spec"]["parameters"], extra="x")), _type(), catalog)
with pytest.raises(CaseError, match="fixes"):
plan_case(_case(parameters=dict(CASE["spec"]["parameters"], issuer="https://evil")), _type(), catalog)
params = {k: v for k, v in CASE["spec"]["parameters"].items() if k != "consumer"}
with pytest.raises(CaseError, match="not set by the case"):
plan_case(_case(parameters=params), _type(), catalog)
def test_refuse_secret_parameter(catalog):
doc = _type()
doc["spec"]["parameters"].append({"name": "pw", "case_settable": True, "sensitivity": "secret"})
with pytest.raises(CaseError, match="secret-sensitivity"):
plan_case(_case(parameters=dict(CASE["spec"]["parameters"], pw="hunter2")), doc, catalog)
def test_env_format_spec_renders_like_keycape_client(catalog):
params = dict(CASE["spec"]["parameters"], client_id="my-app")
plan = plan_case(_case(parameters=params), _type(), catalog)
login = next(s for s in plan.steps if s.id == "login")
assert login.entry_point == "apply my-app env KEYCAPE_MY_APP_X"
@pytest.mark.parametrize("template", ["{client_id:upper}", "{client_id:>9}", "{client_id!r}"])
def test_refuse_unknown_format_spec_or_conversion(catalog, template):
doc = _type()
doc["spec"]["follow_through"][0]["entry_point"] = template
with pytest.raises(CaseError, match="format spec|conversions"):
plan_case(_case(), doc, catalog)
def test_refuse_unsupported_condition(catalog):
doc = _type()
doc["spec"]["follow_through"][0]["when"] = "client_type in (a, b)"
with pytest.raises(CaseError, match="unsupported condition"):
plan_case(_case(), doc, catalog)
def test_cli_plan_shows_attended_and_exit_codes(tmp_path):
case_file, type_file = tmp_path / "case.yaml", tmp_path / "type.yaml"
case_file.write_text(yaml.safe_dump(CASE))
type_file.write_text(yaml.safe_dump(TYPE))
runner = CliRunner()
ok = runner.invoke(app, ["case", "plan", str(case_file), "--type", str(type_file)])
assert ok.exit_code == 0, ok.output
assert "ATTENDED" in ok.output and "nothing executed" in ok.output
import hashlib
digest = hashlib.sha256(case_file.read_bytes()).hexdigest()
assert f"case-ref: c@sha256:{digest}" in ok.output
case_file.write_text(yaml.safe_dump(_case(status="accepted")))
refused = runner.invoke(app, ["case", "plan", str(case_file), "--type", str(type_file)])
assert refused.exit_code == 1
@pytest.mark.skipif(not LIVE_CASE.exists(), reason="net-kingdom checkout absent")
def test_live_forgejo_case_is_refused_until_materialized():
result = CliRunner().invoke(app, ["case", "plan", str(LIVE_CASE)])
assert result.exit_code == 1

View file

@ -1,124 +0,0 @@
"""Offline prototype regression cases; no owner adapter, credential or live use."""
from dataclasses import FrozenInstanceError, replace
import pytest
from warden.case_receipt_state import (
Outcome, Pins, Refused, StepState, begin, begin_rollback, finish,
finish_rollback, recover, rollback_objects,
)
@pytest.fixture
def pins():
return Pins(*["sha256:" + character * 64 for character in "abcdef"])
@pytest.fixture
def state(pins):
return StepState("mint-secret", "railiance-platform", pins, ("case-policy", "case-role"))
def test_records_running_before_dispatch_without_mutating_history(state, pins):
running = begin(state, pins)
assert state.receipts == ()
assert running.last.outcome is Outcome.RUNNING
with pytest.raises(FrozenInstanceError):
running.last.attempt = 8
def test_changed_exact_revision_refuses_dispatch(state, pins):
with pytest.raises(Refused):
begin(state, replace(pins, adapter="sha256:" + "0" * 64))
def test_lost_receipt_becomes_uncertain_and_cannot_retry(state, pins):
resumed = recover(begin(state, pins))
assert resumed.last.outcome is Outcome.UNCERTAIN
assert len(resumed.receipts) == 2
with pytest.raises(Refused):
begin(replace(resumed, retry_safe=True), pins, retry=True)
@pytest.mark.parametrize("outcome", [Outcome.SUCCESS, Outcome.NO_CHANGE])
def test_verified_success_or_no_change_never_replayed(state, pins, outcome):
done = finish(begin(state, pins), pins, outcome)
with pytest.raises(Refused):
begin(done, pins, retry=True)
def test_retry_requires_verified_no_change_failure_and_safe_adapter(state, pins):
failed = finish(begin(state, pins), pins, Outcome.FAILED_BEFORE)
with pytest.raises(Refused):
begin(failed, pins, retry=True)
safe = replace(failed, retry_safe=True)
with pytest.raises(Refused):
begin(safe, pins)
retry = begin(safe, pins, retry=True)
assert retry.last.outcome is Outcome.RUNNING
assert len(retry.receipts) == 3
def test_failure_after_change_does_not_become_safe_retry_or_rollback(state, pins):
failed = finish(begin(state, pins), pins, Outcome.FAILED_AFTER, created=("case-policy",))
with pytest.raises(Refused):
begin(replace(failed, retry_safe=True), pins, retry=True)
with pytest.raises(Refused):
rollback_objects(failed, pins)
def test_rollback_only_created_not_all_reviewed_objects(state, pins):
done = finish(begin(state, pins), pins, Outcome.SUCCESS, created=("case-role",))
assert rollback_objects(done, pins) == ("case-role",)
assert "case-policy" not in rollback_objects(done, pins)
undo = begin_rollback(done, pins)
verified = finish_rollback(undo, pins, verified=True)
assert verified.last.outcome is Outcome.ROLLED_BACK
with pytest.raises(Refused):
begin_rollback(verified, pins)
def test_rollback_reverse_order_and_uncertain_undo_blocks_replay(state, pins):
done = finish(begin(state, pins), pins, Outcome.SUCCESS,
created=("case-policy", "case-role"))
assert rollback_objects(done, pins) == ("case-role", "case-policy")
uncertain = recover(begin_rollback(done, pins))
assert uncertain.last.outcome is Outcome.ROLLBACK_UNCERTAIN
with pytest.raises(Refused):
begin_rollback(uncertain, pins)
def test_unverified_rollback_cannot_claim_success(state, pins):
done = finish(begin(state, pins), pins, Outcome.SUCCESS, created=("case-role",))
failed = finish_rollback(begin_rollback(done, pins), pins, verified=False)
assert failed.last.outcome is Outcome.ROLLBACK_UNCERTAIN
def test_refuses_shared_objects_or_false_no_change_creation(state, pins):
running = begin(state, pins)
for outcome, created in [(Outcome.SUCCESS, ("shared-group",)),
(Outcome.NO_CHANGE, ("case-policy",)),
(Outcome.FAILED_BEFORE, ("case-policy",)),
(Outcome.SUCCESS, ("case-policy", "case-policy"))]:
with pytest.raises(Refused):
finish(running, pins, outcome, created=created)
def test_no_raw_payload_or_output_receipt_interface(state, pins):
with pytest.raises(TypeError):
finish(begin(state, pins), pins, Outcome.SUCCESS, output="sensitive output")
with pytest.raises(Refused):
StepState("step", "owner", pins, ("path with spaces or output",))
with pytest.raises(Refused):
Pins("not-a-digest", pins.case_type, pins.adapter, pins.catalog, pins.acceptance, pins.run_sheet)
def test_out_of_order_result_and_noop_recovery(state, pins):
assert recover(state) is state
with pytest.raises(Refused):
finish(state, pins, Outcome.SUCCESS)
with pytest.raises(Refused):
finish_rollback(state, pins, verified=True)
def test_result_must_be_closed_enum_not_string(state, pins):
with pytest.raises(Refused):
finish(begin(state, pins), pins, "verified-success")

View file

@ -1,745 +0,0 @@
"""Injected owner harness; these receipts are not native owner attestations."""
from dataclasses import asdict, replace
import json
import os
import subprocess
import sys
import sqlite3
import pytest
import yaml
from warden.case_receipt_state import Outcome, Refused
from warden.case_run import Adapter, Descriptor, Executor, Journal, OwnerResult, digest
def lane_entry(lane="issuer-lane", owner="owner", **over):
"""Fixture catalog entry (TEST ONLY; never a real routing pointer)."""
entry = {
"id": lane,
"title": "fixture lane",
"need_keywords": [lane],
"owner_repo": owner,
"subsystem": "fixture",
"warden_executes": False,
"wiki_ref": "wiki/fixture.md",
"canon_ref": "fixture/canon.md",
"reviewed": "2026-10-17",
"status": "active",
"risk": "high",
"workload_ref": {"applicability": "not-applicable", "reason": "fixture"},
"delegation": {
"mode": "native",
"intended_owner": owner,
"reviewed": "2026-10-17",
},
}
entry.update(over)
return entry
def fixture_catalog(*entries):
return yaml.safe_dump({"entries": list(entries) or [lane_entry()]}).encode()
CATALOG = fixture_catalog()
MINT_STEP = {
"id": "mint-secret",
"owner": "owner",
"lane": "issuer-lane",
"warden_executes": True,
"attended": False,
}
def make_rig(tmp_path):
desc = Descriptor(
"warden.case.execution.v1",
"mint-secret",
"owner",
"owner/mint@v1",
"sha256:" + "1" * 64,
"human",
"issuer-lane",
"client_id",
"mint",
("case-policy", "case-role"),
True,
("parent-session", "broker-grant"),
)
descriptor = json.dumps(asdict(desc)).encode()
typ = yaml.safe_dump(
{
"spec": {
"id": "test",
"version": "1",
"status": "acknowledged",
"parameters": [
{
"name": "client_id",
"sensitivity": "operational",
"case_settable": True,
}
],
"execution_descriptors": {"mint-secret": digest(descriptor)},
"follow_through": [dict(MINT_STEP)],
}
}
).encode()
case = yaml.safe_dump(
{
"spec": {
"id": "case-test",
"type": "test@1",
"status": "materialized",
"acceptance": "acceptance",
"parameters": {"client_id": "fixture-client"},
}
}
).encode()
calls = []
def invoke(inv):
calls.append(inv)
return bound(
inv,
OwnerResult(
Outcome.SUCCESS,
"owner-receipt",
("case-policy", "case-role"),
True,
session_revocation_evidence="session-revoked",
grant_revocation_evidence="grant-revoked",
),
)
adapter = Adapter(
"owner/mint@v1",
desc.adapter_source,
invoke,
lambda inv, result: result.evidence == "owner-receipt",
lambda inv, objects: bound(
inv,
OwnerResult(
Outcome.ROLLED_BACK,
"owner-receipt",
(),
True,
session_revocation_evidence="session-revoked",
grant_revocation_evidence="grant-revoked",
),
),
lambda inv: bound(
inv,
OwnerResult(
Outcome.NO_CHANGE,
"owner-receipt",
(),
True,
session_revocation_evidence="session-revoked",
grant_revocation_evidence="grant-revoked",
),
),
)
verified = {
("case-acceptance", digest(case), "acceptance"),
("ops-mason-materialization", digest(case), "materialization"),
("owner-descriptor-acceptance:owner", digest(descriptor), "descriptor"),
("type-machine-seam-acceptance", digest(typ), "descriptor"),
("catalog-routing-acceptance", digest(CATALOG), "descriptor"),
("caller-session:human:operator:issuer-lane", digest(case), "session"),
("session-revocation:issuer-lane", digest(case), "session-revoked"),
("grant-revocation:issuer-lane", digest(case), "grant-revoked"),
}
def verifier(purpose, source, evidence):
if purpose.startswith(("session-revocation:", "grant-revocation:")):
scope, lane, operation, attempt = purpose.split(":")
return (
source == digest(case)
and lane == "issuer-lane"
and operation in ("run", "rollback", "reconcile")
and evidence
== ("session-" if scope == "session-revocation" else "grant-") + attempt
)
return (purpose, source, evidence) in verified
executor = Executor(
Journal(tmp_path / "private"), {"owner/mint@v1": adapter}, verifier
)
kwargs = dict(
principal="operator",
actor="human",
lane="issuer-lane",
acceptance_evidence="acceptance",
materialization_evidence="materialization",
descriptor_evidence="descriptor",
session_evidence="session",
)
admitted = executor.admit(case, typ, descriptor, CATALOG, **kwargs)
return (executor, admitted, calls, (case, typ, descriptor, CATALOG), kwargs)
@pytest.fixture
def rig(tmp_path):
return make_rig(tmp_path)
def test_commit_running_is_visible_from_separate_connection_before_dispatch(rig):
executor, args, calls, _, _ = rig
old = executor.adapters[args[0].adapter]
def inspect(inv):
other = sqlite3.connect(executor.journal.directory / "journal.sqlite")
payload = json.loads(
other.execute(
"SELECT payload FROM receipts ORDER BY seq DESC LIMIT 1"
).fetchone()[0]
)
assert payload["receipts"][-1]["outcome"] == "running"
return old.invoke(inv)
executor.adapters[old.reference] = replace(old, invoke=inspect)
assert executor.run(*args).last.outcome is Outcome.SUCCESS
with pytest.raises(Refused):
executor.run(*args)
assert len(calls) == 1
def test_crash_and_fresh_process_resume_never_duplicates_effect(rig):
executor, args, calls, sources, kwargs = rig
old = executor.adapters[args[0].adapter]
def crash(inv):
calls.append(inv)
raise KeyboardInterrupt
executor.adapters[old.reference] = replace(old, invoke=crash)
with pytest.raises(KeyboardInterrupt):
executor.run(*args)
fresh = Executor(
Journal(executor.journal.directory),
{old.reference: old},
executor.evidence_verifier,
)
admitted = fresh.admit(*sources, **kwargs)
with pytest.raises(Refused):
fresh.run(*admitted, retry=True)
state = fresh.journal.load("mint-secret:fixture-client", admitted[2])
assert state.last.outcome is Outcome.UNCERTAIN
assert len(calls) == 1
def test_lost_result_preserved_and_cas_reconciliation_does_not_mint(rig):
executor, args, calls, _, _ = rig
old = executor.adapters[args[0].adapter]
def lost(inv):
calls.append(inv)
raise TimeoutError("SECRET-HIDDEN")
executor.adapters[old.reference] = replace(old, invoke=lost)
assert executor.run(*args).last.outcome is Outcome.UNCERTAIN
assert executor.reconcile(*args).last.outcome is Outcome.NO_CHANGE
with pytest.raises(Refused):
executor.run(*args, retry=True)
assert len(calls) == 1
assert (
b"SECRET-HIDDEN"
not in (executor.journal.directory / "journal.sqlite").read_bytes()
)
def test_failed_before_change_requires_explicit_retry_and_safe_owner(rig):
executor, args, calls, _, _ = rig
old = executor.adapters[args[0].adapter]
def no_change(inv):
calls.append(inv)
return bound(
inv,
OwnerResult(
Outcome.FAILED_BEFORE,
"owner-receipt",
(),
True,
session_revocation_evidence="session-revoked",
grant_revocation_evidence="grant-revoked",
),
)
executor.adapters[old.reference] = replace(old, invoke=no_change)
assert executor.run(*args).last.outcome is Outcome.FAILED_BEFORE
with pytest.raises(Refused):
executor.run(*args)
assert executor.run(*args, retry=True).last.outcome is Outcome.FAILED_BEFORE
assert len(calls) == 2
@pytest.mark.parametrize(
"result",
[
OwnerResult(
Outcome.SUCCESS,
"owner-receipt",
("shared-policy",),
True,
session_revocation_evidence="session-revoked",
grant_revocation_evidence="grant-revoked",
),
OwnerResult(
Outcome.SUCCESS,
"owner-receipt",
(),
False,
session_revocation_evidence="session-revoked",
grant_revocation_evidence="grant-revoked",
),
OwnerResult(
Outcome.SUCCESS,
"unknown-receipt",
(),
True,
session_revocation_evidence="session-revoked",
grant_revocation_evidence="grant-revoked",
),
{"outcome": "verified-success", "stdout": "SECRET-HIDDEN"},
],
)
def test_unknown_secret_payload_created_scope_or_missing_revocation_never_succeeds(
rig, result, capsys
):
executor, args, _, _, _ = rig
old = executor.adapters[args[0].adapter]
def malicious(inv):
print("SECRET-HIDDEN" * 1000)
return result
executor.adapters[old.reference] = replace(old, invoke=malicious)
assert executor.run(*args).last.outcome is Outcome.UNCERTAIN
assert capsys.readouterr().out == ""
assert (
b"SECRET-HIDDEN"
not in (executor.journal.directory / "journal.sqlite").read_bytes()
)
def test_rollback_reverses_only_verified_case_created_objects(rig):
executor, args, _, _, _ = rig
old = executor.adapters[args[0].adapter]
undos = []
def undo(inv, objects):
undos.append(objects)
return old.rollback(inv, objects)
executor.adapters[old.reference] = replace(old, rollback=undo)
executor.run(*args)
assert executor.rollback(*args).last.outcome is Outcome.ROLLED_BACK
assert undos == [("case-role", "case-policy")]
assert "shared-policy" not in undos[0]
def test_uncertain_rollback_cannot_replay(rig):
executor, args, _, _, _ = rig
old = executor.adapters[args[0].adapter]
executor.run(*args)
executor.adapters[old.reference] = replace(
old,
rollback=lambda inv, objs: bound(
inv,
OwnerResult(
Outcome.ROLLED_BACK,
"owner-receipt",
(),
False,
session_revocation_evidence="session-revoked",
grant_revocation_evidence="grant-revoked",
),
),
)
assert executor.rollback(*args).last.outcome is Outcome.ROLLBACK_UNCERTAIN
with pytest.raises(Refused):
executor.rollback(*args)
@pytest.mark.parametrize(
"change", ["materialization", "type-prose", "actor", "lane", "source", "target"]
)
def test_exact_admission_requires_owner_evidence_and_closed_source_pins(rig, change):
executor, _, _, sources, kwargs = rig
case, typ, descriptor, catalog = sources
kwargs = dict(kwargs)
if change == "materialization":
kwargs["materialization_evidence"] = "bare-json"
if change == "type-prose":
obj = yaml.safe_load(typ)
obj["spec"].pop("execution_descriptors")
typ = yaml.safe_dump(obj).encode()
if change == "actor":
kwargs["actor"] = "agent"
if change == "lane":
kwargs["lane"] = "other-lane"
if change == "source":
obj = json.loads(descriptor)
obj["adapter_source"] = "sha256:" + "2" * 64
descriptor = json.dumps(obj).encode()
if change == "target":
obj = yaml.safe_load(case)
obj["spec"]["parameters"]["client_id"] = "other-target"
case = yaml.safe_dump(obj).encode()
with pytest.raises(Refused):
executor.admit(case, typ, descriptor, catalog, **kwargs)
def test_agent_env_cannot_dispatch_after_human_admission(rig, monkeypatch):
executor, args, calls, _, _ = rig
monkeypatch.setenv("WARDEN_AGENT_ID", "test-agent")
with pytest.raises(Refused):
executor.run(*args)
assert calls == []
def test_forged_invocation_cannot_bypass_admission(rig):
executor, args, calls, _, _ = rig
with pytest.raises(Refused):
executor.run(args[0], replace(args[1], target="other-target"), args[2])
assert calls == []
def test_journal_rejects_unsafe_directory_and_symlink(tmp_path):
unsafe = tmp_path / "unsafe"
unsafe.mkdir(mode=493)
with pytest.raises(Refused):
Journal(unsafe)
private = tmp_path / "private"
private.mkdir(mode=448)
(private / "journal.sqlite").symlink_to(tmp_path / "outside")
with pytest.raises(OSError):
Journal(private)
def test_process_lock_blocks_parallel_dispatch(rig):
executor, args, calls, _, _ = rig
with executor.journal.locked():
with pytest.raises(Refused):
executor.run(*args)
assert calls == []
def test_changed_catalog_pins_cannot_inherit_old_success(rig):
executor, args, calls, sources, kwargs = rig
executor.run(*args)
with pytest.raises(Refused):
executor.admit(*sources[:3], fixture_catalog(lane_entry(title="changed")), **kwargs)
assert len(calls) == 1
def test_private_journal_does_not_retain_owner_payload_or_accept_unknown_schema(rig):
executor, args, _, _, _ = rig
executor.run(*args)
row = executor.journal.db.execute(
"SELECT seq,payload FROM receipts ORDER BY seq DESC LIMIT 1"
).fetchone()
payload = json.loads(row[1])
assert payload["journal_schema"] == "warden.case.step-journal.v1"
assert payload["evidence_digest"] == digest(b"owner-receipt")
assert payload["session_revoked"] is True
assert "owner-receipt" not in row[1]
payload["stdout"] = "UNTRUSTED-OUTPUT"
executor.journal.db.execute(
"UPDATE receipts SET payload=? WHERE seq=?", (json.dumps(payload), row[0])
)
with pytest.raises(Refused):
executor.run(*args)
def test_expired_caller_session_cannot_use_cached_admission(rig):
executor, args, calls, _, _ = rig
old = executor.evidence_verifier
executor.evidence_verifier = lambda purpose, source, evidence: (
False
if purpose.startswith("caller-session:")
else old(purpose, source, evidence)
)
with pytest.raises(Refused):
executor.run(*args)
assert calls == []
@pytest.mark.parametrize("index", [0, 1, 2, 3])
def test_source_sizes_are_bounded_before_parsing(rig, index):
executor, _, _, sources, kwargs = rig
oversized = list(sources)
oversized[index] = b"x" * 262145
with pytest.raises(Refused, match="bounded"):
executor.admit(*oversized, **kwargs)
def test_owner_fd_and_inherited_subprocess_output_is_contained(rig, capfd):
executor, args, _, _, _ = rig
old = executor.adapters[args[0].adapter]
def leaking(inv):
os.write(1, b"SYNTHETIC-SECRET")
os.write(2, b"SYNTHETIC-SECRET")
subprocess.run([sys.executable, "-c", 'print("SYNTHETIC-SECRET")'], check=True)
return old.invoke(inv)
executor.adapters[old.reference] = replace(old, invoke=leaking)
assert executor.run(*args).last.outcome is Outcome.SUCCESS
observed = capfd.readouterr()
assert "SYNTHETIC-SECRET" not in observed.out + observed.err
def test_evidence_provider_output_and_error_never_escape(rig, capfd):
executor, _, calls, sources, kwargs = rig
def broken(*args):
print("SYNTHETIC-SECRET")
os.write(2, b"SYNTHETIC-SECRET")
raise RuntimeError("SYNTHETIC-SECRET")
executor.evidence_verifier = broken
with pytest.raises(Refused) as caught:
executor.admit(*sources, **kwargs)
assert "SYNTHETIC-SECRET" not in str(caught.value)
observed = capfd.readouterr()
assert "SYNTHETIC-SECRET" not in observed.out + observed.err
assert calls == []
@pytest.mark.parametrize("missing", ["session", "grant"])
def test_broker_grant_and_parent_session_have_separate_required_evidence(rig, missing):
executor, args, _, _, _ = rig
old = executor.adapters[args[0].adapter]
result = old.invoke(args[1])
if missing == "session":
result = replace(result, session_revocation_evidence=None)
else:
result = replace(result, grant_revocation_evidence=None)
executor.adapters[old.reference] = replace(old, invoke=lambda inv: result)
assert executor.run(*args).last.outcome is Outcome.UNCERTAIN
def test_mint_descriptor_cannot_omit_broker_grant_scope(rig):
descriptor = json.loads(rig[3][2])
descriptor["revocation_scopes"] = ["parent-session"]
with pytest.raises(Refused):
Descriptor.decode(json.dumps(descriptor).encode())
def test_unsafe_owner_step_cannot_retry_even_after_verified_failed_before(
rig, tmp_path
):
executor, args, _, sources, kwargs = rig
descriptor = replace(args[0], retry_safe=False)
descriptor_source = json.dumps(asdict(descriptor)).encode()
type_doc = yaml.safe_load(sources[1])
type_doc["spec"]["execution_descriptors"][descriptor.step] = digest(
descriptor_source
)
type_source = yaml.safe_dump(type_doc).encode()
old_verifier = executor.evidence_verifier
def verifier(purpose, source, evidence):
if purpose == "owner-descriptor-acceptance:owner":
return source == digest(descriptor_source) and evidence == "descriptor"
if purpose == "type-machine-seam-acceptance":
return source == digest(type_source) and evidence == "descriptor"
return old_verifier(purpose, source, evidence)
adapter = executor.adapters[descriptor.adapter]
def failed(inv):
return bound(
inv,
OwnerResult(
Outcome.FAILED_BEFORE,
"owner-receipt",
(),
True,
"session-revoked",
"grant-revoked",
),
)
fresh = Executor(
Journal(tmp_path / "unsafe-retry"),
{adapter.reference: replace(adapter, invoke=failed)},
verifier,
)
admitted = fresh.admit(
sources[0], type_source, descriptor_source, sources[3], **kwargs
)
assert fresh.run(*admitted).last.outcome is Outcome.FAILED_BEFORE
with pytest.raises(Refused):
fresh.run(*admitted, retry=True)
def bound(inv, result):
return replace(
result,
attempt_id=inv.attempt_id,
operation=inv.operation,
session_revocation_evidence=("session-" + inv.attempt_id)
if result.session_revocation_evidence
else None,
grant_revocation_evidence=("grant-" + inv.attempt_id)
if result.grant_revocation_evidence
else None,
)
def test_old_grant_and_session_proofs_cannot_verify_a_new_retry(rig):
executor, args, _, _, _ = rig
adapter = executor.adapters[args[0].adapter]
retained = []
def attempts(inv):
if not retained:
result = bound(
inv,
OwnerResult(
Outcome.FAILED_BEFORE,
"owner-receipt",
(),
True,
"session-revoked",
"grant-revoked",
),
)
retained.append(result)
return result
# Even a caller rewriting labels cannot authenticate prior revocation
# evidence for the newly persisted attempt identity.
return replace(retained[0], attempt_id=inv.attempt_id, operation=inv.operation)
executor.adapters[adapter.reference] = replace(adapter, invoke=attempts)
assert executor.run(*args).last.outcome is Outcome.FAILED_BEFORE
assert executor.run(*args, retry=True).last.outcome is Outcome.UNCERTAIN
def test_owner_interrupt_payload_is_sanitized_with_running_intent_retained(rig, capfd):
executor, args, _, _, _ = rig
adapter = executor.adapters[args[0].adapter]
def interrupted(inv):
os.write(2, b"SYNTHETIC-SECRET")
raise KeyboardInterrupt("SYNTHETIC-SECRET")
executor.adapters[adapter.reference] = replace(adapter, invoke=interrupted)
with pytest.raises(KeyboardInterrupt) as caught:
executor.run(*args)
assert "SYNTHETIC-SECRET" not in str(caught.value)
assert "SYNTHETIC-SECRET" not in "".join(capfd.readouterr())
assert (
executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome
is Outcome.RUNNING
)
def test_attempt_identity_changes_with_exact_catalog_revision_and_new_journal(
rig, tmp_path
):
executor, args, calls, sources, kwargs = rig
executor.run(*args)
original_attempt = calls[-1].attempt_id
new_catalog = fixture_catalog(lane_entry(title="independently reviewed v2"))
old_verifier = executor.evidence_verifier
def verifier(purpose, source, evidence):
if purpose == "catalog-routing-acceptance":
return source == digest(new_catalog) and evidence == "descriptor"
return old_verifier(purpose, source, evidence)
fresh = Executor(Journal(tmp_path / "new-revision"), executor.adapters, verifier)
revised = fresh.admit(*sources[:3], new_catalog, **kwargs)
assert fresh.run(*revised).last.outcome is Outcome.SUCCESS
assert calls[-1].attempt_id != original_attempt
assert calls[-1].pins.catalog != calls[0].pins.catalog
def test_secret_sensitivity_parameter_cannot_reach_admission_or_journal(rig):
executor, _, _, sources, kwargs = rig
type_doc = yaml.safe_load(sources[1])
type_doc["spec"]["parameters"][0]["sensitivity"] = "secret"
source = yaml.safe_dump(type_doc).encode()
old = executor.evidence_verifier
executor.evidence_verifier = lambda purpose, digest_value, evidence: (
True
if purpose == "type-machine-seam-acceptance"
else old(purpose, digest_value, evidence)
)
with pytest.raises(Refused, match="secret-sensitivity"):
executor.admit(sources[0], source, sources[2], sources[3], **kwargs)
def _lose_result(executor, args, calls):
old = executor.adapters[args[0].adapter]
def lost(inv):
calls.append(inv)
raise TimeoutError("lost")
seen = []
def by_original(ctx):
seen.append(ctx)
return old.reconcile(ctx.attempt)
executor.adapters[old.reference] = replace(
old, invoke=lost, reconcile=None, reconcile_original=by_original
)
assert executor.run(*args).last.outcome is Outcome.UNCERTAIN
return seen
def test_original_intent_survives_restart_separate_from_reconcile_attempt(rig):
executor, args, calls, _, _ = rig
seen = _lose_result(executor, args, calls)
executor.journal = Journal(executor.journal.directory) # restart: fresh handle
assert executor.reconcile(*args).last.outcome is Outcome.NO_CHANGE
(ctx,) = seen
original = ctx.original.invocation
assert original.attempt_id == calls[0].attempt_id
assert original.operation == "run" and ctx.attempt.operation == "reconcile"
assert ctx.attempt.attempt_id != original.attempt_id
assert original.pins == args[1].pins
assert (original.principal, original.tenant, original.realm) == (
args[1].principal,
args[1].tenant,
args[1].realm,
)
assert ctx.original.source == executor.adapters[args[0].adapter].source
def test_missing_original_intent_refuses_and_stays_uncertain(rig):
executor, args, calls, _, _ = rig
seen = _lose_result(executor, args, calls)
executor.journal.db.execute("DELETE FROM intents")
with pytest.raises(Refused, match="original intent record missing"):
executor.reconcile(*args)
assert seen == []
binding = f"{args[1].step}:{args[1].target}"
assert executor.journal.load(binding, args[2]).last.outcome is Outcome.UNCERTAIN
@pytest.mark.parametrize(
"change", [{"tenant": "other-tenant"}, {"realm": "other-realm"}, {"lane": "x"}]
)
def test_original_intent_mismatch_refuses(rig, change):
executor, args, calls, _, _ = rig
seen = _lose_result(executor, args, calls)
with pytest.raises(Refused):
executor.reconcile(args[0], replace(args[1], **change), args[2])
assert seen == []
def test_original_intent_source_mismatch_refuses(rig):
executor, args, calls, _, _ = rig
_lose_result(executor, args, calls)
binding = f"{args[1].step}:{args[1].target}"
with pytest.raises(Refused, match="source mismatch"):
executor.journal.original_intent(binding, calls[0], "other-source")

View file

@ -1,130 +0,0 @@
"""WARDEN-WP-0044-T01: SecurityCaseType follow-through check."""
from __future__ import annotations
import copy
import os
from pathlib import Path
import pytest
import yaml
from typer.testing import CliRunner
from warden.case_type import check_follow_through, load_type
from warden.cli import app
from warden.routing.catalog import load_catalog
LIVE_TYPE = Path(os.environ.get("WARDEN_TEST_NETKINGDOM_ROOT",
Path(__file__).resolve().parents[2] / "net-kingdom")) / "security-case-types/oidc-browser-relying-party/type.yaml"
BASE = {
"apiVersion": "netkingdom.io/security-case-type/v0.1",
"kind": "SecurityCaseType",
"spec": {
"id": "t",
"version": "1.0",
"follow_through": [
{"id": "run", "owner": "o", "entry_point": "e", "auth": "a", "lane": "key-cape-oidc-login",
"warden_executes": True, "attended": False, "value_handling": "none"},
{"id": "login", "owner": "o", "entry_point": "e", "auth": "a", "lane": "key-cape-oidc-login",
"warden_executes": False, "attended": True, "value_handling": "none",
"attended_justification": "live registry is a Secret"},
{"id": "handover", "owner": "o", "entry_point": "e", "auth": "a",
"warden_executes": False, "attended": False, "value_handling": "none"},
],
"verification": [{"id": "v", "check": "c", "expect": "present", "reads_secret_value": False}],
"rollback": [{"reverses": "run"}, {"reverses": "login"}, {"reverses": "handover"}],
},
}
@pytest.fixture(scope="module")
def catalog():
return load_catalog()
def _doc(**step_overrides):
doc = copy.deepcopy(BASE)
doc["spec"]["follow_through"][0].update(step_overrides)
return doc
def _codes(report):
return {f.code for f in report.findings}
def test_conforming_type_is_plannable(catalog):
report = check_follow_through(_doc(), catalog)
assert report.plannable, report.findings
assert report.warden_steps == ["run"]
assert report.attended_lanes == ["key-cape-oidc-login"]
def test_executed_step_without_lane_refused(catalog):
doc = _doc()
del doc["spec"]["follow_through"][0]["lane"]
assert "FT-LANE" in _codes(check_follow_through(doc, catalog))
def test_unknown_lane_is_catalog_finding_not_schema(catalog):
report = check_follow_through(_doc(lane="no-such-lane"), catalog)
assert report.schema_ok and not report.plannable
assert _codes(report) == {"LANE-MISSING"}
def test_value_handling_required_and_enumerated(catalog):
assert "FT-VALUE" in _codes(check_follow_through(_doc(value_handling="returned"), catalog))
doc = _doc()
del doc["spec"]["follow_through"][0]["value_handling"]
assert "FT-FIELD" in _codes(check_follow_through(doc, catalog))
def test_attended_needs_justification_and_excludes_execution(catalog):
doc = copy.deepcopy(BASE)
del doc["spec"]["follow_through"][1]["attended_justification"]
doc["spec"]["follow_through"][1]["warden_executes"] = True
msgs = [f.message for f in check_follow_through(doc, catalog).findings if f.code == "FT-ATTEND"]
assert len(msgs) == 2
def test_attended_step_needs_login_lane(catalog):
doc = copy.deepcopy(BASE)
secret_lane = next(e.id for e in catalog.entries if e.is_active and e.reviewed and e.lane == "secret")
doc["spec"]["follow_through"][1]["lane"] = secret_lane
assert "LANE-KIND" in _codes(check_follow_through(doc, catalog))
def test_generated_value_never_in_attended_session(catalog):
doc = copy.deepcopy(BASE)
doc["spec"]["follow_through"][1]["value_handling"] = "generated_in_custody"
assert "FT-VALUE" in _codes(check_follow_through(doc, catalog))
def test_verification_must_not_read_values(catalog):
doc = _doc()
doc["spec"]["verification"][0]["reads_secret_value"] = True
assert "VER-VALUE" in _codes(check_follow_through(doc, catalog))
def test_every_step_reversible(catalog):
doc = _doc()
doc["spec"]["rollback"] = doc["spec"]["rollback"][1:]
assert "RB-MISSING" in _codes(check_follow_through(doc, catalog))
def test_cli_exit_codes(tmp_path):
good, bad = tmp_path / "good.yaml", tmp_path / "bad.yaml"
good.write_text(yaml.safe_dump(_doc()))
bad.write_text(yaml.safe_dump(_doc(lane="no-such-lane")))
runner = CliRunner()
assert runner.invoke(app, ["case", "check-type", str(good), "--json"]).exit_code == 0
assert runner.invoke(app, ["case", "check-type", str(bad), "--json"]).exit_code == 1
@pytest.mark.skipif(not LIVE_TYPE.exists(), reason="net-kingdom checkout not present")
def test_live_oidc_rp_type_schema_conforms(catalog):
"""The acknowledged 1.0 type is plannable: every lane it names is active (2026-10-10)."""
report = check_follow_through(load_type(LIVE_TYPE), catalog)
assert report.schema_ok, report.findings
assert report.plannable, report.findings
assert report.attended_lanes == ["key-cape-oidc-login"]
assert report.warden_steps == ["mint-secret"]

View file

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

View file

@ -1,355 +0,0 @@
"""Layer-model conformance (security-layer-model_v0.8 §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())
def _intent_frontmatter() -> dict:
lines = (ROOT / "INTENT.md").read_text().splitlines()
assert lines[0].strip() == "---", "INTENT.md must carry frontmatter — it is the declaration"
end = next(i for i, ln in enumerate(lines[1:], 1) if ln.strip() == "---")
return yaml.safe_load("\n".join(lines[1:end]))
def _checker():
import importlib.util
spec = importlib.util.spec_from_file_location(
"check_layer_conformance", ROOT / "scripts" / "check_layer_conformance.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def _fold(value: str) -> str:
return str(value).strip().encode("ascii", "ignore").decode().lower()
class TestDeclaration:
def test_declares_staff_layer_in_its_own_voice(self):
d = _decl()
assert d["repository"] == "ops-warden"
assert _fold(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_intent_md_carries_the_governing_declaration(self):
"""GH-DEC-2026-017 §1 / A11: INTENT.md's frontmatter is the declaration."""
assert _fold(_intent_frontmatter()["layer"]) == "staff"
def test_sidecar_is_marked_derived_and_names_its_source(self):
"""§11's derived-artifact rule, applied to the layer sidecar."""
d = _decl()
assert d["derived"] is True
assert d["derived_from"] == "INTENT.md"
def test_the_two_forms_agree_once_case_is_folded(self):
"""A11: the derived form must agree; A9: comparison folds case.
`Staff` in INTENT.md and `staff` in layer.yaml are the SAME value. This
test is deliberately a fold rather than an equality: the ruling asked
nobody to re-spell anything, and an equality assertion here would be this
repository quietly doing the re-spelling the ruling declined to order.
The next *real* divergence — a different layer — still fails.
"""
assert _fold(_decl()["layer"]) == _fold(_intent_frontmatter()["layer"])
def test_layer_is_in_section_3_closed_vocabulary(self):
"""A9: {Taxonomy, Tooling, Engine, Staff}, closed, case-insensitive."""
vocabulary = {"taxonomy", "tooling", "engine", "staff"}
assert _fold(_intent_frontmatter()["layer"]) in vocabulary
assert _fold(_decl()["layer"]) in vocabulary
def test_no_declaration_carries_a_standard_version(self):
"""GH-DEC-2026-017 §5 / A12 — and the regression guard on its return.
The field was removed from the estate's reference form, not just from
this file. A field that is present will be branched on, so absence is
asserted rather than trusted.
"""
assert "standard_version" not in _decl()
assert "standard_version" not in _intent_frontmatter()
def test_no_version_anywhere_in_either_declaration(self):
"""A12 r2 / GH-DEC-2026-020 §1-§2: content, not a key name.
A versioned `standard:` path or a `companion_version` is the same pin as
`standard_version`, so the guard walks every key and value of both forms.
"""
checker = _checker()
assert checker.find_version_pins(_intent_frontmatter()) == []
assert checker.find_version_pins(_decl()) == []
assert not str(_intent_frontmatter()["standard"]).endswith(".md")
def test_checker_catches_a_versioned_standard_path(self):
checker = _checker()
pins = checker.find_version_pins(
{"layer": "Staff", "standard": "net-kingdom/canon/standards/security-layer-model_v0.7.md"}
)
assert pins and pins[0].startswith("standard")
def test_checker_catches_a_companion_version(self):
checker = _checker()
assert checker.find_version_pins({"layer": "Staff", "companion_version": "0.2"})
assert checker.find_version_pins({"nested": {"standard_version": "0.7"}})
def test_version_token_in_identity_value_is_a_pin(self):
"""GH-DEC-2026-021 §3: any `v?N.N` in a standard:/companion: value is a pin."""
checker = _checker()
pins = checker.find_version_pins({"standard": "security-layer-model v0.7"})
assert pins and pins[0].startswith("standard")
assert checker.find_version_pins({"companion": "SECURITY-COMPANION 0.2"})
assert checker.find_version_pins({"nested": {"standard": ["security-layer-model v0.8"]}})
def test_prose_citation_and_intent_version_are_not_reached(self):
"""GH-DEC-2026-021 §1 (A12 r3): prose provenance and `intent_version` pass."""
checker = _checker()
assert checker.find_version_pins(
{"layer": "Staff", "note": "Outside §5 by the v0.5 scope rule", "intent_version": "0.1.0"}
) == []
def test_schema_version_is_not_reached(self):
assert _checker().find_version_pins({"schema_version": "0.2", "layer": "Staff"}) == []
def test_stance_map_is_outside_the_run(self):
"""GH-DEC-2026-020 §3: a stance map keeps its version; the run must not read it."""
stance = yaml.safe_load((ROOT / "pep-stance.yaml").read_text())
assert "standard_version" in stance, "pep-stance.yaml keeps its clause-scoped version"
assert "pep-stance" not in _checker().SCOPE
def test_every_run_states_version_and_scope(self):
"""GH-DEC-2026-020 §4: the version belongs to the run, printed every time."""
checker = _checker()
out = subprocess.run(
[sys.executable, str(ROOT / "scripts" / "check_layer_conformance.py")],
capture_output=True,
text=True,
).stdout
assert f"validated against: {checker.VALIDATED_AGAINST}" in out
assert f"scope: {checker.SCOPE}" in out
pass_line = next(ln for ln in out.splitlines() if ln.startswith("PASS"))
assert checker.VALIDATED_AGAINST in pass_line
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"
def test_revocation_visibility_deadline_equals_enforced_ttl_policy(self):
"""§9.7.2: a published replay window must not drift from issuance."""
from warden.models import ActorType, MAX_TTL_HOURS
published = self._stance()["revocation_visibility"]
expected = {actor.value: MAX_TTL_HOURS[actor] for actor in ActorType}
assert published["deadline_hours"] == expected
assert published["mechanism"] == "ttl_expiry"
assert published["revocation_channel"] == "none"
def test_attributive_emission_cadence_deferral_carries_measurement(self):
cadence = self._stance()["emission_cadence"]
assert cadence["classification"] == "attributive"
assert cadence["status"] == "deferred"
assert cadence["observed_window"]["signature_records"] == 3
assert cadence["observed_window"]["active_signature_days"] == 2
assert cadence["reason"]
# --- classification coverage (v0.8 §6.4 obligation 3) -------------------------
def test_published_coverage_equals_measured_coverage():
"""The published figure must equal its explicit public declaration snapshot.
This is a deterministic source check, not a claim about current estate state.
Live measurement remains scripts/report_coverage.py with an explicit estate
root; tests never treat the developer HOME as an authority or fixture.
ops-warden asked gate-house for §13.1's Coverage column and its figures are
that column's first entries, so their accuracy is ours to hold. The register
explicitly does not compute anyone's coverage, and a stale number beside a
marked cell is worse than a blank -- a blank at least reads as "not reported".
This is the same property that makes the stance map worth publishing (the map
equals PolicyConfig.failure_modes by test), applied one level up.
"""
import importlib.util
repo = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location(
"report_coverage", repo / "scripts" / "report_coverage.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
published = yaml.safe_load((repo / "pep-stance.yaml").read_text())[
"classification_coverage"
]
measured = module.measure(repo / "tests/fixtures/workload-coverage")
for population in ("signing_targets", "routing_lanes"):
assert published[population] == measured[population], population
def test_the_unknown_cell_is_marked_as_a_declared_gap():
"""A non-conformant cell must say so where it is declared, not only in a review.
§11's marking obligation, which ops-warden argued for in the v0.6 round and
then acquired a marked cell under. If the cell is ever flipped to fail_closed
this test fails, which is the correct time to remove the marking.
"""
repo = Path(__file__).resolve().parents[1]
text = (repo / "pep-stance.yaml").read_text()
stance = yaml.safe_load(text)["stance"]
if stance["unknown"] == "fail_open":
assert "DECLARED GAP" in text
assert "WARDEN-WP-0040" in text
else:
assert stance["unknown"] == "fail_closed"
def test_accepted_v08_run_provenance_and_catalog_scope():
checker = _checker()
assert "security-layer-model_v0.8.md" in checker.VALIDATED_AGAINST
assert "sha256:3d8298560069ef471fba28d2b1245fb2309328f231dad1a667cdf0ce5a5a7c8b" in checker.VALIDATED_AGAINST
assert "single repository ops-warden (catalogued in §4)" in checker.SCOPE
def test_reference_semantics_propagate_identity_through_nested_sequences():
checker = _checker()
for data in (
{"nested": {"standard": ["Security Layer Model v0.8"]}},
{"companion": [["Security Companion 0.3"]]},
{"standard": {"alternate": ["Security Layer Model v0.7"]}},
):
# A dictionary begins a fresh identity key; only sequence descendants
# inherit the immediate standard:/companion: identity in the reference.
if isinstance(data.get("standard"), dict):
assert checker.find_version_pins(data) == []
else:
assert checker.find_version_pins(data), data
assert checker.find_version_pins({"rationale": "Assented at v0.7; migrated at v0.8."}) == []
assert checker.find_version_pins({"intent_version": "0.2.0", "schema_version": "0.1"}) == []
def test_ascii_folding_never_discards_non_ascii_letters():
checker = _checker()
assert checker._fold("Staff") == "staff"
assert checker._fold("Engine") == "engine"
# Deleting non-ASCII bytes would turn this fifth token into Staff.
assert checker._fold("Stαaff") not in checker.LAYER_VOCABULARY

View file

@ -1,125 +0,0 @@
"""Sanitized ambient OpenBao caller identity."""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from warden.openbao_caller import (
ambient_token_source,
caller_boundary_note,
describe_ambient_openbao_caller,
)
def test_source_prefers_bao_token_over_vault_and_file(monkeypatch, tmp_path):
monkeypatch.setenv("BAO_TOKEN", "hvs.bao")
monkeypatch.setenv("VAULT_TOKEN", "hvs.vault")
monkeypatch.setattr(Path, "home", lambda: tmp_path)
(tmp_path / ".vault-token").write_text("file-token")
assert ambient_token_source() == "BAO_TOKEN"
def test_source_uses_vault_token_file_when_env_absent(monkeypatch, tmp_path):
monkeypatch.delenv("BAO_TOKEN", raising=False)
monkeypatch.delenv("VAULT_TOKEN", raising=False)
monkeypatch.setattr(Path, "home", lambda: tmp_path)
(tmp_path / ".vault-token").write_text("file-token")
assert ambient_token_source() == "vault-token-file"
def test_lookup_drops_token_fields(monkeypatch):
secret = "hvs.this-must-not-appear"
monkeypatch.setenv("BAO_TOKEN", secret)
payload = {
"data": {
"id": secret,
"accessor": "also-secret",
"entity_id": "4a1f663c-0621-0483-edd0-f852274f4b6b",
"display_name": "netkingdom-uid=platform-root",
"path": "auth/netkingdom/oidc/callback",
"policies": ["default", "operator-custody", "platform-admin"],
"meta": {"role": "platform-admin", "email": "hidden@example.invalid"},
"ttl": 3001,
}
}
def runner(argv, **kwargs):
class Completed:
returncode = 0
stdout = json.dumps(payload)
stderr = ""
assert kwargs["env"]["BAO_TOKEN"] == secret
return Completed()
monkeypatch.setattr("warden.openbao_caller.subprocess.run", runner)
described = describe_ambient_openbao_caller()
encoded = json.dumps(described)
assert secret not in encoded
assert "accessor" not in described
assert "email" not in encoded
assert described["entity_id"].startswith("4a1f663c-")
assert described["oidc_role"] == "platform-admin"
assert described["policies"] == ["default", "operator-custody", "platform-admin"]
assert described["source"] == "BAO_TOKEN"
assert described["error"] is None
def test_lookup_failure_has_no_raw_output(monkeypatch):
monkeypatch.setenv("VAULT_TOKEN", "hvs.caller")
monkeypatch.delenv("BAO_TOKEN", raising=False)
def runner(argv, **kwargs):
class Completed:
returncode = 2
stdout = ""
stderr = "Error: permission denied hvs.caller"
return Completed()
monkeypatch.setattr("warden.openbao_caller.subprocess.run", runner)
described = describe_ambient_openbao_caller()
assert described["error"] == "token lookup failed"
assert described["entity_id"] is None
assert "hvs.caller" not in json.dumps(described)
def test_missing_client(monkeypatch):
monkeypatch.setenv("BAO_TOKEN", "hvs.bao")
def runner(argv, **kwargs):
raise FileNotFoundError(argv[0])
monkeypatch.setattr("warden.openbao_caller.subprocess.run", runner)
assert describe_ambient_openbao_caller()["error"] == "bao client not found"
def test_no_token(monkeypatch, tmp_path):
monkeypatch.delenv("BAO_TOKEN", raising=False)
monkeypatch.delenv("VAULT_TOKEN", raising=False)
monkeypatch.setattr(Path, "home", lambda: tmp_path)
described = describe_ambient_openbao_caller()
assert described["source"] == "none"
assert described["error"] == "no ambient OpenBao token"
@pytest.mark.parametrize(
("policies", "high_risk", "fragment"),
[
(
["agent-high-risk-boundary", "workload-kv-read-issue-core-runtime"],
True,
"agent-high-risk-boundary",
),
(["default", "platform-admin"], True, "platform-admin"),
(["workload-kv-read-reuse-surface-runtime"], True, None),
],
)
def test_boundary_note(policies, high_risk, fragment):
note = caller_boundary_note({"policies": policies}, high_risk=high_risk)
if fragment is None:
assert note is None
else:
assert fragment in note

View file

@ -1,81 +0,0 @@
"""Configuration intent must never inherit a metadata survey's founder act."""
from dataclasses import replace
from pathlib import Path
import pytest
from warden.plan import build_plan
from warden.routing.catalog import Catalog, load_catalog
ROOT = Path(__file__).resolve().parents[1]
NEED = ('attended OpenBao platform administration to configure read policies and auth roles '
'for Coulomb and email backup delivery')
SIGNER_NEED = ('attended OpenBao platform administration for reviewed flex-auth '
'signer custody CAS0 and fixed KV metadata')
@pytest.fixture
def catalog(monkeypatch):
monkeypatch.setenv('WARDEN_POSTURE_CATALOG', str(ROOT / 'registry/policy/security-posture.yaml'))
return load_catalog(ROOT / 'registry/routing/catalog.yaml')
@pytest.mark.parametrize('need', [NEED,
SIGNER_NEED,
'Attended OpenBao platform administration login',
'configure OpenBao policies and Kubernetes auth roles for backup delivery',
'apply OpenBao read policy for an explicitly reviewed service account'])
def test_configuration_requires_attended_admin_identity(catalog, need):
plan = build_plan(need, catalog=catalog)
assert plan.lane_id == 'openbao-platform-admin-login'
assert plan.verdict == 'founder_required' and plan.founder_act.kind == 'oidc_login'
assert plan.commands == [
'warden access openbao-platform-admin-login --exec -- <reviewed-command>',
'# owner login is contained by warden; do not invoke it separately']
assert all('--fetch' not in command and '--out' not in command for command in plan.commands)
@pytest.mark.parametrize('need', [
'survey existing OpenBao policies and roles for backup delivery',
'OpenBao read-only metadata survey policy approle consumer backup',
'survey OpenBao signer custody CAS0 and fixed KV metadata',
'inspect OpenBao structure metadata policy approle'])
def test_survey_remains_metadata_only(catalog, need):
plan = build_plan(need, catalog=catalog)
assert plan.lane_id == 'openbao-structure-survey'
assert plan.founder_act.kind == 'approve'
@pytest.mark.parametrize('need', [NEED, SIGNER_NEED])
def test_explicit_lane_id_still_wins_and_missing_admin_fails_closed(catalog, need):
assert catalog.find('openbao-structure-survey')[0].id == 'openbao-structure-survey'
unavailable = Catalog(catalog.path, [entry for entry in catalog.entries
if entry.id != 'openbao-platform-admin-login'])
plan = build_plan(need, catalog=unavailable)
assert plan.verdict == 'unroutable' and plan.lane_id is None
assert not plan.commands
@pytest.mark.parametrize('changes', [{'status': 'draft'}, {'lane': 'secret'}])
def test_attended_admin_cannot_use_inactive_or_secret_lane(catalog, changes):
admin = catalog.get('openbao-platform-admin-login')
unavailable = Catalog(catalog.path, [
replace(entry, **changes) if entry.id == admin.id else entry
for entry in catalog.entries])
plan = build_plan(SIGNER_NEED, catalog=unavailable, include_draft=True)
assert plan.verdict == 'unroutable' and plan.lane_id is None
assert not plan.commands
def test_pair_provisioning_routes_to_custody_not_survey(catalog):
need = "provision a Scaleway object storage backup publisher key pair into OpenBao custody"
plan = build_plan(need, catalog=catalog)
assert plan.lane_id == "openbao-api-key"
assert plan.verdict == "founder_required"
assert plan.founder_act.kind == "paste_once_provision"
assert not any("plan-survey" in command for command in plan.commands)
def test_missing_custody_frontdoor_fails_closed(catalog):
unavailable = Catalog(catalog.path, [e for e in catalog.entries if e.id != "openbao-api-key"])
assert build_plan("provision OpenBao backup key pair", catalog=unavailable).verdict == "unroutable"

View file

@ -119,87 +119,3 @@ def test_cli_plan_json():
assert payload["verdict"] == "autonomous"
assert payload["organization_posture"] == "build"
assert payload["lane_id"] == "agent-harness-forgejo-deploy"
# --- mutate intent (WARDEN-WP-0038) -------------------------------------------
#
# `warden plan` scored a need by keyword overlap with no notion of what the
# caller wanted to DO, so "generate a successor secret and CAS-write it to two
# custodians" matched the lane that READS that path and inherited its
# `autonomous` verdict, answered with --out/--exec/--wrap. Reported independently
# by key-cape (2026-09-08) and railiance-platform (2026-09-09), who added: "this
# is the same verdict your warden plan should have returned; until that is fixed,
# do not let a plan result stand in for this answer."
QONTO_ROTATION_NEED = (
"generate a successor client secret for rapp-qonto keycape client and "
"CAS-write it to OpenBao platform/workloads/rapp-qonto/keycape-client "
"and sso/keycape-rapp-qonto-client"
)
def test_reported_qonto_write_no_longer_returns_autonomous():
"""The exact need from the report. Regression, in the manner of WP-0033-T06."""
plan = build_plan(QONTO_ROTATION_NEED)
assert plan.verdict == "founder_required"
assert plan.lane_id == "rapp-qonto-keycape-client"
assert plan.founder_act is not None
assert plan.founder_act.kind == "approve"
assert plan.founder_act.details["write_owner"] == "key-cape"
def test_a_write_need_is_never_answered_with_a_read_transport():
"""The half that made the wrong verdict actionable rather than merely wrong."""
plan = build_plan(QONTO_ROTATION_NEED)
joined = " ".join(plan.commands)
for read_transport in ("--out", "--wrap", "--fetch", "bao kv get", "--exec"):
assert read_transport not in joined, read_transport
def test_mutating_need_names_why_it_escalated():
"""WP-0029's property: a verdict carries the reasons that produced it."""
plan = build_plan(QONTO_ROTATION_NEED)
assert any("change a credential" in r for r in plan.reasons)
assert any("write authority" in r for r in plan.reasons)
# The lane records automatable: false; a driver must be told so.
assert any("not automatable" in r for r in plan.reasons)
def test_ops_warden_still_proceeds_autonomously_on_the_lane_it_owns():
"""Signing IS a mutating act. The test is ownership, not the absence of a verb.
`delegation.mode: permanent` is the whole distinction — if this ever fails,
the guard has started refusing ops-warden's own front door.
"""
for need in (
"sign an ssh certificate for agt-state-hub-bridge",
"issue a short-lived ssh cert for adm",
):
plan = build_plan(need)
assert plan.verdict == "autonomous", need
assert plan.lane_id == "ssh-cert-host-access"
def test_reading_the_same_lane_is_unaffected():
plan = build_plan("I need the npm token to publish whynot-design")
assert plan.verdict == "autonomous"
assert plan.lane_id == "whynot-design-npm-publish"
@pytest.mark.parametrize(
"verb", ["rotate", "revoke", "regenerate", "reset", "replace"]
)
def test_mutating_verbs_escalate_on_a_lane_ops_warden_does_not_own(verb):
plan = build_plan(f"{verb} the forgejo admin api token")
assert plan.verdict == "founder_required"
assert plan.founder_act.details["write_owner"] == "railiance-platform"
def test_intent_classifier_matches_tokens_not_substrings():
from warden.plan import _need_intent
assert _need_intent("rotate the forgejo token") == "mutate"
assert _need_intent("CAS-write to two custodians") == "mutate"
# "created" and "updated" are not the verbs; a need describing state is a read.
assert _need_intent("read the token created for whynot-design") == "read"
assert _need_intent("which subsystem owns the npm token") == "read"

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