Compare commits
3 commits
main
...
mason/stat
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e66a56653 | |||
| 6294e4b521 | |||
| 3a4333d850 |
157 changed files with 379 additions and 18353 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
34
.custodian-brief.md
Normal 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.
|
||||
|
|
@ -1 +0,0 @@
|
|||
{"version": 1, "repo_slug": "ops-warden"}
|
||||
|
|
@ -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
3
.gitignore
vendored
|
|
@ -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
62
AGENTS.md
62
AGENTS.md
|
|
@ -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 -->
|
||||
|
|
|
|||
22
CLAUDE.md
22
CLAUDE.md
|
|
@ -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
148
INTENT.md
|
|
@ -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,
|
||||
|
|
|
|||
16
Makefile
16
Makefile
|
|
@ -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 .
|
||||
|
|
@ -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
|
||||
|
|
|
|||
64
SCOPE.md
64
SCOPE.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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"
|
||||
```
|
||||
156
layer.yaml
156
layer.yaml
|
|
@ -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.
|
||||
160
pep-stance.yaml
160
pep-stance.yaml
|
|
@ -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"
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
@ -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())
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
@ -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()
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'}}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
20
tests/fixtures/workload-coverage/provenance.json
vendored
20
tests/fixtures/workload-coverage/provenance.json
vendored
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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",
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue