Compare commits
178 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8afcc9c321 | |||
|
|
529feeac49 | ||
| 9770d6ad66 | |||
|
|
31694ab64d | ||
| eddb5d426b | |||
| 4fee839b11 | |||
| 8f01eefb1e | |||
| b4c1d3900a | |||
|
|
4e267179db | ||
|
|
d18d18e313 | ||
| 2e3ff772f4 | |||
| fd08950231 | |||
| 94f32bd160 | |||
| ec625873fb | |||
| f815bb35ca | |||
| 57d39ede0f | |||
|
|
f289465b90 | ||
|
|
a45280f30d | ||
| 61c992923c | |||
| ee94c18938 | |||
| d7f4ebcfe0 | |||
|
|
f766400563 | ||
| 70d8b503f0 | |||
| 85d3078bae | |||
|
|
467635e84b | ||
| 15730bb650 | |||
| 347e47ce8e | |||
| 37c387bd34 | |||
| 030362085a | |||
|
|
0fc581ffb9 | ||
| 186fa99f58 | |||
|
|
76e545ecf0 | ||
| 51e1434cdb | |||
| 9087edf673 | |||
| c374d41a49 | |||
| c8fa02adf0 | |||
| bc1966da82 | |||
| 0fae0904ce | |||
| 461f580813 | |||
| cdb44a8f23 | |||
| 7fa6985f72 | |||
| 5ac47520b1 | |||
| 2c1fc25e43 | |||
| 8cd10eae17 | |||
| e3b9b1620c | |||
| fff76ef089 | |||
|
|
5c2368ad42 | ||
|
|
72a3c0b6e2 | ||
|
|
145e31034e | ||
| 8280c0b7b7 | |||
| e24d2d5bd0 | |||
| bebcdf929c | |||
| 59d426b87b | |||
|
|
4143d1d8da | ||
| 6604ace982 | |||
| 7ce58ae638 | |||
|
|
12c637cbf2 | ||
| e18894ee5b | |||
| 55f0f47a02 | |||
| a565e62b2f | |||
| 675e04e8e6 | |||
| b30626edcf | |||
| 6cd969dda9 | |||
| 6e1d5201aa | |||
| c357ce5908 | |||
| 9b7e2e9bd2 | |||
| 06d0a1e690 | |||
| 0dafb53e84 | |||
| edb587851c | |||
| 801bbe7b35 | |||
|
|
1cd71e74d7 | ||
| 9ed8b452a1 | |||
| 72f7dc2c5d | |||
| 50e185ed7b | |||
|
|
9fc5a735c6 | ||
| d0d4f9d8fc | |||
| ac85259c20 | |||
| 7b8dd3467b | |||
| f528c1df31 | |||
| 9008ded1b0 | |||
| b1070be644 | |||
| 5e2f7dc939 | |||
| 8c58f8bfa1 | |||
| b845f4b51e | |||
|
|
3d968bb993 | ||
| cbf6828061 | |||
| 661176e37a | |||
|
|
e22f9b3434 | ||
| 654c05dece | |||
|
|
9b9bfe565a | ||
| 0a331413a2 | |||
| 35aff380a3 | |||
| faa4f2c23e | |||
| 683f22d986 | |||
| 3447687bbc | |||
| 263b919c17 | |||
| c93e3c9b43 | |||
| 8d3706fa06 | |||
|
|
49b8618ff1 | ||
| bfa0eda327 | |||
| 817af8bc6e | |||
| 56876ee456 | |||
| 064a0736be | |||
| 8d5188c24f | |||
| cd074caaaa | |||
| 9d42dd5abd | |||
|
|
95ecfe51e2 | ||
|
|
538f08c050 | ||
|
|
b346e63a07 | ||
| cd3eaac97e | |||
| 456fdc4b4f | |||
| a9086ad6b6 | |||
| 350f66472c | |||
| d8d3d5b1a0 | |||
| 979c6f68b4 | |||
| 2e862bbff0 | |||
| d961da1ef2 | |||
| c0a50bc1bf | |||
| cb6e9a73f4 | |||
| 5d30220cc5 | |||
|
|
507bbef6ea | ||
| 5149946a4c | |||
| 5c6b71b83b | |||
| cb762e06cb | |||
|
|
7a08171dab | ||
| 29645c9303 | |||
| 5219104809 | |||
|
|
ffc3b22fb0 | ||
| 0cec8eef76 | |||
|
|
54fd31aa5c | ||
|
|
6fe5034a65 | ||
|
|
ffff2eff4f | ||
|
|
885e362daa | ||
| 25a691d49a | |||
|
|
0433481e94 | ||
| 6bfbf64108 | |||
|
|
59f0277f20 | ||
| c5ec9bdaa6 | |||
|
|
b6861e4b62 | ||
| 053a1d7cee | |||
| 6b5432229f | |||
|
|
80ce5eb2df | ||
| 98a2339b81 | |||
| b971403dad | |||
|
|
7d0c7c7684 | ||
|
|
0eb2126311 | ||
| fc0f18aa5c | |||
|
|
04c8b2ab1d | ||
| 359ca1bd0e | |||
|
|
c749561b75 | ||
| c3eb59ea04 | |||
|
|
ac09f21ad3 | ||
|
|
fb4251bab6 | ||
|
|
03ffa27b08 | ||
| ea98d6bf39 | |||
| 2ad8a53781 | |||
| 167e29de99 | |||
|
|
5615b94649 | ||
| 7e0789ab0d | |||
|
|
a8adb9c2c5 | ||
| 19cd215d0b | |||
|
|
ce469c93b3 | ||
| 171efa83fe | |||
|
|
a5f1d2aad7 | ||
| ddeac8bf9c | |||
|
|
caaa40ce7e | ||
| fd231fac0d | |||
| 48b21ab85c | |||
| 8f3ce50e7b | |||
| 07b564309e | |||
| 7906a731fc | |||
| 7fbfae0fe1 | |||
| 4b0f771cbd | |||
| 1e6c4eedf1 | |||
| 3ddccaf701 | |||
| d12fdb6112 | |||
| 07c5cf18ea | |||
| 4976002f9c |
162 changed files with 18698 additions and 755 deletions
|
|
@ -1,7 +1,59 @@
|
|||
## Architecture
|
||||
|
||||
<!-- TODO: Describe the key design decisions and component structure.
|
||||
Key modules, data flows, external integrations, state machines, etc. -->
|
||||
### Our rules are ADRs — `docs/adr/`
|
||||
|
||||
The decisions that govern this repo live in `docs/adr/` as addressable records,
|
||||
not in wiki prose. Read `docs/adr/README.md` first; it explains the one
|
||||
distinction that matters here.
|
||||
|
||||
| ADR | Rule |
|
||||
| --- | --- |
|
||||
| `ADR-0001` | The routing catalog is a pointer layer, never a second copy of an owner's procedure |
|
||||
| `ADR-0002` | ops-warden is a transparent conduit, never a secret broker |
|
||||
| `ADR-0003` | Cover gaps, but never silently own them |
|
||||
| `ADR-0004` | High-risk lanes refuse raw value streaming to agent sessions |
|
||||
| `ADR-0005` | Implement one lane narrowly, route everything else |
|
||||
| `ADR-0006` | Enforcement is zone-scoped, never a global flag |
|
||||
| `ADR-0007` | Build-stage permissiveness stops at credential disclosure; every lane carries an explicit `risk` grade |
|
||||
| `ADR-0008` | A lane's risk grade covers every field its path discloses, not just the field it is named after |
|
||||
| `ADR-0009` | Adopt security-zones v0.1 as a consumer; membership is compiled, never inferred |
|
||||
| `ADR-0010` | ops-warden is Staff: it owns access lanes, never access rules; doctrine belongs to gate-house |
|
||||
|
||||
### Owned versus inherited — check `owner:` before changing anything
|
||||
|
||||
Every ADR carries `owner:` in its frontmatter, and it decides what you are allowed
|
||||
to do with the rule:
|
||||
|
||||
- **`owner: ops-warden`** — ours. We are bound by it *and* we may change it. Changing
|
||||
one means writing a superseding ADR, not editing the decision in place.
|
||||
- **any other owner** — inherited. We follow it; we do not own it. Dispute it through
|
||||
that owner's process; never amend it here.
|
||||
|
||||
Everything in `docs/adr/` today is `owner: ops-warden`. Rules we merely follow —
|
||||
NetKingdom canon, the IAM profile, the credential-management standard — are cited,
|
||||
never copied in. Copying them would recreate the second-source-of-truth failure
|
||||
`ADR-0001` exists to prevent.
|
||||
|
||||
**Naming collision, worth knowing.** `ADR-001` (three digits) in
|
||||
`workplan-convention.md` and `session-protocol.md` is **the-custodian's** ADR
|
||||
establishing the workplan convention across the whole estate. It is inherited and
|
||||
not ours to change. Our records are four-digit — `ADR-0001` … `ADR-0005` — and live
|
||||
in this repo. When writing, say "the-custodian's ADR-001" if that is what you mean.
|
||||
|
||||
### Precedence
|
||||
|
||||
If a wiki page, playbook, or `.claude/rules/` file disagrees with an ADR, **the ADR
|
||||
is right and the other file is a defect** — fix it rather than working around it.
|
||||
The rule files are agent-facing operational instructions derived from these
|
||||
decisions; they should cite an ADR rather than restate its reasoning.
|
||||
|
||||
### Publication
|
||||
|
||||
These ADRs are publishable through `policy-nexus` at `policy.coulomb.social`, which
|
||||
requires `title`, `status` and `owner`, renders owner in the page header and in the
|
||||
index, and records source repo, path and revision digest in its manifest. Ownership
|
||||
survives the repo boundary. `policy-nexus` publishes and never writes back: the file
|
||||
here is the source of truth.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
|
|
|
|||
|
|
@ -4,62 +4,82 @@
|
|||
for inference. Run this check **before** requesting secrets, API keys, SSH access,
|
||||
login tokens, or database passwords — in any repo, not only `ops-warden`.
|
||||
|
||||
ops-warden **issues SSH certificates** (`warden sign`, `cert_command`) **and is the
|
||||
operator access front door** for every other credential need. For `exec_capable` lanes
|
||||
(OpenBao reads, key-cape login) `warden access <need> --fetch/--exec` **proxies the fetch
|
||||
as you** — it runs the owner's tool with your identity and streams the value to you;
|
||||
ops-warden holds, caches, and logs nothing. For non-exec lanes it points you at the owner.
|
||||
|
||||
**Do not** `POST /messages/` to `ops-warden` expecting a secret *value* — a State Hub
|
||||
reply is always a pointer. The **value comes from the CLI front door** (`warden access`),
|
||||
run with **your** identity, never from the inbox.
|
||||
ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Every
|
||||
other credential need belongs to another subsystem. **Do not** message
|
||||
`ops-warden` on State Hub expecting a secret value; the reply is a pointer, not a key.
|
||||
|
||||
### Lookup (do this first)
|
||||
|
||||
**Always plan before drafting any founder credential step** (WARDEN-WP-0029):
|
||||
|
||||
```bash
|
||||
warden route find "<describe your need>" --json # who owns it (pointer)
|
||||
warden access "<describe your need>" --json # how to get it (handoff)
|
||||
warden plan "<describe your need>" --json
|
||||
# verdict: autonomous | founder_required | unroutable
|
||||
```
|
||||
|
||||
`warden access` is the operator front door (WARDEN-WP-0014): it renders the owner,
|
||||
auth method, path template, command skeleton, and policy-gate status for any need.
|
||||
For `exec_capable` lanes it can **proxy the fetch as you** (`--fetch`/`--exec`) — it
|
||||
runs the owner's tool with **your** identity and streams the value to you; ops-warden
|
||||
never holds, caches, or logs the value. See `wiki/OperatorAccessAssist.md`.
|
||||
```bash
|
||||
warden route find "<describe your need>" --json
|
||||
warden route show <catalog-id> --json
|
||||
```
|
||||
|
||||
Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run warden`).
|
||||
If a known lane is missing, reinstall from checkout (stale bundled catalog).
|
||||
|
||||
| Agent runtime | How to orient |
|
||||
| --- | --- |
|
||||
| **Codex / Grok** (shell, HTTP State Hub) | `warden route` commands above; inbox `to_agent=ops-warden` is for coordination, not secret vending |
|
||||
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workstreams; **still** use `warden route` for credential ownership |
|
||||
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden route` |
|
||||
| **Codex / Grok** (shell, HTTP State Hub) | `warden plan` first; inbox `to_agent=ops-warden` is for coordination, not secret vending |
|
||||
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workplans; **still** use `warden plan` / `warden route` for credential ownership |
|
||||
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody via `warden plan` |
|
||||
|
||||
### Quick routing table
|
||||
|
||||
| I need… | Owner | ops-warden role |
|
||||
| I need… | Owner | ops-warden executes? |
|
||||
| --- | --- | --- |
|
||||
| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Issue** — `warden sign` |
|
||||
| Provisioned secret-exec lane (e.g. npm publish) | **secrets-engine** | **Route** — primary is `secrets-engine exec --catalog <id> -- <cmd>`; `warden access <id> --exec` is the transparent fallback |
|
||||
| Generic API key / DB password / provider token | OpenBao (`railiance-platform`) | **Assist** — `warden access <need> --fetch/--exec` proxies as you; OpenBao keeps custody |
|
||||
| Login / OIDC / MFA | key-cape / Keycloak | **Assist** — `warden access <need> --fetch` runs the login as you |
|
||||
| Authorization decision | flex-auth | Route only |
|
||||
| activity-core → issue-core emission | activity-core + issue-core | Route — `warden route show activity-core-issue-sink` |
|
||||
| SSH tunnel | ops-bridge (+ `cert_command` from warden) | Route only |
|
||||
|
||||
For an owned lane, `warden route find <need> --json` / `warden access <id>` surface
|
||||
`exec_owner`, the `secrets-engine exec` command, and the `resolvable` flag. Run the
|
||||
secrets-engine command; ops-warden routes to it and requests/holds no token.
|
||||
| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Yes** — `warden sign` |
|
||||
| API key, DB password, provider token | OpenBao (`railiance-platform`) | No — route only |
|
||||
| Login / OIDC / MFA | key-cape / Keycloak | No — route only |
|
||||
| Authorization decision | flex-auth | No — route only |
|
||||
| activity-core → issue-core emission | activity-core + issue-core | No — `warden route show activity-core-issue-sink` |
|
||||
| SSH tunnel | ops-bridge (+ `cert_command` from warden) | No — route only |
|
||||
|
||||
### Anti-patterns (do not do these)
|
||||
|
||||
- Drafting founder steps ("paste PAT to `/tmp`", "click admin UI") without `warden plan`
|
||||
- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc.
|
||||
- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist
|
||||
- Pasting secrets into Git, State Hub, workplans, logs, or chat
|
||||
- Treating `warden access --fetch` as a *secret store*. It is a transparent conduit
|
||||
using **your** identity — it holds nothing. ops-warden as a **standing broker**
|
||||
(its own secret-read token, a cache of fetched values) is forbidden; runtime secret
|
||||
custody stays in OpenBao, authorization in flex-auth.
|
||||
- Steady-state **file drops** of credentials under `/tmp` (use `warden desk` paste-once
|
||||
or `warden access --out/--exec/--wrap`)
|
||||
- **Reading a secret value onto a captured stdout.** `bao kv get <path>` (full table)
|
||||
or `bao kv get -field=X` piped/redirected/run in an agent session dumps the value
|
||||
into a logged context — the 2026-07-16 disclosure. To *verify* a lane use
|
||||
`bao token capabilities` (allow/deny), never a read (WP-0026 T01).
|
||||
|
||||
### Safe fetch transports (WP-0026 T02)
|
||||
|
||||
When a value must actually move, use a sanctioned transport that keeps it off
|
||||
stdout. `warden access <need> --fetch` refuses to stream a value to a non-terminal
|
||||
stdout unless you pass `--unsafe-stdout` (interactive human sessions only):
|
||||
|
||||
| Transport | Command | Result |
|
||||
| --- | --- | --- |
|
||||
| **File** | `warden access <need> --out FILE` | value written to a mode-0600 file, never shown |
|
||||
| **Env (exec)** | `warden access <need> --exec -- <cmd>` | value injected into the child process env only |
|
||||
| **Wrapping token** | `warden access <need> --wrap` | a single-use, short-TTL OpenBao wrapping token to `bao unwrap` in your own context |
|
||||
|
||||
### Agent read-boundary on high-risk lanes (WP-0026 T04)
|
||||
|
||||
When `WARDEN_AGENT_ID` is set and the catalog lane is `risk: high`, raw value
|
||||
streaming is refused (exit 7). Use `--out` / `--exec` / `--wrap` / `--fingerprint`
|
||||
only. OpenBao policy `agent-high-risk-boundary` denies data-read on those paths
|
||||
for agent tokens (metadata/capabilities only). See
|
||||
`wiki/playbooks/agent-read-boundary.md`.
|
||||
|
||||
### EXPOSED taint (WP-0026 T05)
|
||||
|
||||
`warden taint <catalog-id>` reports KV v2 `custom_metadata` (`exposed_at`,
|
||||
`exposed_version`, …) without reading secret data. Convention:
|
||||
`wiki/playbooks/exposed-taint.md`.
|
||||
|
||||
### Other capabilities (reuse-surface)
|
||||
|
||||
|
|
|
|||
104
.claude/rules/finding-routing.md
Normal file
104
.claude/rules/finding-routing.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# Finding routing (risk-nexus) and policy publication (policy-nexus)
|
||||
|
||||
Two estate repos exist that did not when most of ops-warden's practices were
|
||||
written. Both are owned by `the-custodian` and both are **downstream by
|
||||
construction** — the same rule ops-warden's own catalog lives under.
|
||||
|
||||
| Repo | Owns | ops-warden's relationship |
|
||||
| --- | --- | --- |
|
||||
| `risk-nexus` | Findings, severity, disclosure timing, escalation, regulatory intake. Serves `risk.coulomb.social` | **Route findings to it.** It does not fix; ops-warden fixes what ops-warden owns |
|
||||
| `policy-nexus` | Publication of canon and ADRs at permanent addresses. Serves `policy.coulomb.social` | Source repo. It publishes; it never writes back |
|
||||
|
||||
## When a session discovers or receives a defect, route it
|
||||
|
||||
**A design question is not a finding. A defect is.** The distinction matters
|
||||
because ops-warden receives both through the same channel — the State Hub inbox.
|
||||
|
||||
This was gotten wrong on 2026-08-17. `flex-auth` reported directly to ops-warden
|
||||
that `/v1/check` authenticates no caller — a live authorization bypass in the
|
||||
service ops-warden's own pre-sign gate consults. ops-warden answered the design
|
||||
question well and wrote the recommendation into
|
||||
`wiki/NetKingdomSecurityMap.md`. It did not route the finding. `rapp-postgres`
|
||||
filed it as `RISK-F-0001`, which is why that record reads
|
||||
`reported_via: rapp-postgres` and not `ops-warden`, despite ops-warden being a
|
||||
first-hand recipient and the affected PEP.
|
||||
|
||||
The failure mode is exactly the one `risk-nexus/INTENT.md` names: *"findings
|
||||
landed in whichever document was open."* A wiki section answers the question; it
|
||||
does not carry a severity, an owner, a date, or a review that fires when nobody
|
||||
looks.
|
||||
|
||||
**So: when an inbound message or a session turns up a defect — in any repo —
|
||||
answer it *and* route it.** They are not alternatives.
|
||||
|
||||
### How to route
|
||||
|
||||
Write the finding file into `~/risk-nexus/findings/` following
|
||||
`RISK-F-0001`/`RISK-F-0002`, and commit it there. This is the established
|
||||
pattern: a repo routes a finding by writing the record.
|
||||
|
||||
Leave `severity`, `disclosure`, and `escalation` **unset**. They are
|
||||
`risk-nexus`'s to set, not the reporter's. The reporter says what is true; that
|
||||
repo says how bad it is and who hears about it. State exposure only as far as
|
||||
you can support it — do not infer a mitigating control (a NetworkPolicy, a
|
||||
deployment flag) on a system you do not own; say it should be verified.
|
||||
|
||||
### What does not go there
|
||||
|
||||
`risk-nexus/INTENT.md` is explicit that a register nobody can read is worse than
|
||||
none: *"if a finding would not change anyone's decision, it is a note, not a
|
||||
risk."*
|
||||
|
||||
The **delegation register is not a findings feed.** `warden route gaps` lists
|
||||
interim lanes with an intended owner, a blocker and a review date — that is
|
||||
already tracked, already legible, and already ops-warden's. Do not bulk-file it.
|
||||
What goes to `risk-nexus` from ops-warden is a defect or an exposure, not a
|
||||
known gap that is being worked under a workplan.
|
||||
|
||||
Also re-read a *blocker* before trusting it. A blocker is a claim about the
|
||||
world at a date; `RISK-F-0001` invalidated one of ops-warden's in a day and
|
||||
nothing would have re-checked it.
|
||||
|
||||
## Escalation: ops-warden already solved this shape
|
||||
|
||||
`risk-nexus` carries an unwritten escalation duty — deciding what reaches the
|
||||
operator personally rather than sitting in a register — and says the rule
|
||||
*"must be written down rather than exercised by instinct"*.
|
||||
|
||||
ops-warden shipped that classifier for the credential domain in WP-0029.
|
||||
`warden plan "<need>"` returns `autonomous` / `founder_required` / `unroutable`,
|
||||
and when it escalates it returns a **typed act** (`approve`, `login`,
|
||||
`provision`) plus the `reasons` that produced the verdict, with `warden desk` as
|
||||
the surface that actually executes the act. The transferable design properties:
|
||||
|
||||
- escalation is decided by **properties of the thing** (lane type, status,
|
||||
request signals), not by the assessor's judgement in the moment
|
||||
- every verdict carries its `reasons`, so the rule is auditable after the fact
|
||||
- there is a typed act, so "needs the operator" says *what the operator does*
|
||||
- there is a real surface for the act, so escalation is not just a flag
|
||||
|
||||
Offer this rather than let a second, incompatible escalation vocabulary grow.
|
||||
Do not implement it for them — routing work is theirs to own.
|
||||
|
||||
## Policy publication (closed 2026-08-18)
|
||||
|
||||
This section previously recorded that ops-warden had no ADRs and that its binding
|
||||
rules — the no-double-source catalog rule, conduit-not-broker, interim-by-default,
|
||||
the agent read-boundary — sat in wiki prose, unaddressable and unpublishable.
|
||||
|
||||
**That is now resolved.** They live in `docs/adr/` as `ADR-0001`…`ADR-0005`, each
|
||||
carrying `owner: ops-warden`, and each verified to render through `policy-nexus`'s
|
||||
own `tools/render.py`. See `.claude/rules/architecture.md` for the owned-versus-
|
||||
inherited rule and the three-digit/four-digit ADR naming collision.
|
||||
|
||||
What matters when routing something to `policy-nexus`: it requires `title`,
|
||||
`status` and `owner` on every published document (`tools/build_site.py:179`),
|
||||
renders owner in both the page eyebrow and the index Owner column, and records
|
||||
source repo, path, revision and content digest in its manifest. Ownership survives
|
||||
publication — a reader landing on the URL can tell the rule is ours.
|
||||
|
||||
`policy-nexus` publishes and never writes back. The file in `docs/adr/` is the
|
||||
source of truth; if the site disagrees, the site is the defect.
|
||||
|
||||
**When you record a new binding rule, write the ADR.** Not a wiki section — that is
|
||||
the habit this whole rule file exists to correct, in the other direction.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
## First Session Protocol
|
||||
|
||||
Triggered when `get_domain_summary("infotech")` shows **no workstreams**.
|
||||
Triggered when `get_domain_summary("infotech")` shows **no workplans**.
|
||||
The project is registered but work has not yet been structured.
|
||||
|
||||
**Step 1 — Read, don't write**
|
||||
|
|
@ -11,27 +11,31 @@ The project is registered but work has not yet been structured.
|
|||
**Step 2 — Survey in-progress work**
|
||||
Look for TODOs, open branches, half-finished files. Note done vs. started but incomplete.
|
||||
|
||||
**Step 3 — Propose workstreams to Bernd**
|
||||
Propose 1–3 workstreams — each a coherent strand, weeks to months, anchored to a
|
||||
**Step 3 — Propose workplans to Bernd**
|
||||
Propose 1–3 workplans — each a coherent strand, weeks to months, anchored to a
|
||||
roadmap phase. **Wait for approval before creating.**
|
||||
|
||||
**Step 4 — Create workplan file first, then DB record (ADR-001)**
|
||||
**Step 4 — Write the workplan file; fix-consistency registers it (ADR-001)**
|
||||
```
|
||||
workplans/WARDEN-WP-NNNN-<slug>.md ← write this first
|
||||
workplans/WARDEN-WP-NNNN-<slug>.md ← write this, commit it
|
||||
```
|
||||
Then register in the hub:
|
||||
```
|
||||
create_workstream(topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a", title="...", owner="...", description="...")
|
||||
create_task(workstream_id="<id>", title="...", priority="high|medium|low")
|
||||
Then register by running the consistency check — do **not** call
|
||||
`create_workplan`/`create_task` yourself; manual registration duplicates what
|
||||
C-06 creates from the file:
|
||||
```bash
|
||||
statehub fix-consistency --repo ops-warden
|
||||
```
|
||||
C-06 creates the hub workplan + tasks and writes `state_hub_workstream_id`
|
||||
(legacy frontmatter name — holds the workplan UUID) and `state_hub_task_id`
|
||||
back into the file.
|
||||
|
||||
**Step 5 — Record the setup**
|
||||
```
|
||||
add_progress_event(
|
||||
summary="First session: structured infotech into N workstreams, M tasks",
|
||||
summary="First session: structured infotech into N workplans, M tasks",
|
||||
event_type="milestone",
|
||||
topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a",
|
||||
detail={"workstreams": [...], "tasks_created": M}
|
||||
detail={"workplans": [...], "tasks_created": M}
|
||||
)
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ For each file with `status: ready`, `active`, or `blocked`, note pending
|
|||
|
||||
**Step 4 — Present brief**
|
||||
|
||||
1. **Active workstreams** for `infotech` — title, task counts, blocking decisions
|
||||
1. **Active workplans** for `infotech` — title, task counts, blocking decisions
|
||||
2. **Pending tasks** from `workplans/` + any `[repo:ops-warden]` hub tasks
|
||||
3. **Goal guidance** — if `goal_guidance` in summary:
|
||||
- `needs_workplan`: surface as top action — *"Repo goal '{title}' has no workplan yet"*
|
||||
|
|
@ -52,33 +52,40 @@ For each file with `status: ready`, `active`, or `blocked`, note pending
|
|||
4. **Suggested next action** — highest-priority open item
|
||||
5. **SBOM status** — flag if `last_sbom_at` is unset for this repo
|
||||
|
||||
If no workstreams: follow First Session Protocol (`first-session.md`).
|
||||
If no workplans: follow First Session Protocol (`first-session.md`).
|
||||
|
||||
**During work:** `record_decision()` · `add_progress_event()` · `resolve_decision()`
|
||||
|
||||
> State Hub is a *read model*. Bootstrap tools (`create_workstream`, `create_task`)
|
||||
> are First Session Protocol only. Work structure belongs in repo files (ADR-001).
|
||||
> State Hub is a *read model*. **Never register workplans or tasks by hand**
|
||||
> (`create_workplan`, `create_task`) — write the workplan file in `workplans/`
|
||||
> and run `fix-consistency`; C-06 registers the workplan and tasks and writes
|
||||
> IDs back into the file. Manual registration creates duplicates when
|
||||
> fix-consistency runs. Work structure belongs in repo files (ADR-001).
|
||||
>
|
||||
> Legacy: `create_workstream` and `/workstreams/` remain as metered aliases —
|
||||
> see `workplan-convention.md` (compatibility footnote).
|
||||
|
||||
**Session close:**
|
||||
With MCP tools:
|
||||
```
|
||||
add_progress_event(summary="...", topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a", workstream_id="<uuid>")
|
||||
add_progress_event(summary="...", topic_id="cee7bedf-2b48-46ef-8601-006474f2ad7a", workplan_id="<uuid>")
|
||||
```
|
||||
Without MCP tools:
|
||||
```bash
|
||||
curl -s -X POST http://127.0.0.1:8000/progress/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"topic_id":"cee7bedf-2b48-46ef-8601-006474f2ad7a","workstream_id":"<uuid>","event_type":"note","summary":"what changed","author":"codex"}'
|
||||
-d '{"topic_id":"cee7bedf-2b48-46ef-8601-006474f2ad7a","workplan_id":"<uuid>","event_type":"note","summary":"what changed","author":"codex"}'
|
||||
```
|
||||
If workplan files were modified, ensure the local copy is up to date first:
|
||||
If workplan files were modified, ensure the local copy is up to date first,
|
||||
then sync from the repo checkout:
|
||||
```bash
|
||||
git -C <repo_path> pull --ff-only
|
||||
cd ~/state-hub && make fix-consistency REPO=ops-warden
|
||||
git pull --ff-only
|
||||
statehub fix-consistency
|
||||
```
|
||||
For repos where implementation runs on a remote machine (e.g. CoulombCore),
|
||||
use the combined target which pulls before fixing:
|
||||
For repos where implementation runs on a remote machine (e.g. railiance01),
|
||||
use the pull-before-fix mode from any shell with the State Hub CLI:
|
||||
```bash
|
||||
cd ~/state-hub && make fix-consistency-remote REPO=ops-warden
|
||||
statehub fix-consistency --repo ops-warden --remote
|
||||
```
|
||||
**C-15** (DB task ahead of file) is normal in multi-machine workflows — writeback
|
||||
will sync the file to match DB. **C-16** (repo behind remote) blocks all writes
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ ID prefix: `WARDEN-WP-`
|
|||
|
||||
Work items originate as files in this repo **before** being registered in the hub.
|
||||
|
||||
Canonical workplan/workstream frontmatter statuses are:
|
||||
Canonical workplan frontmatter statuses are:
|
||||
`proposed`, `ready`, `active`, `blocked`, `backlog`, `finished`, `archived`.
|
||||
Use `proposed` for a newly drafted plan, `ready` after review against current
|
||||
repo state, and `finished` when implementation is complete. `stalled` and
|
||||
|
|
@ -16,14 +16,15 @@ prefix: `YYMMDD-WARDEN-WP-NNNN-<slug>.md`. The frontmatter id remains
|
|||
unchanged; the prefix is only for quick visual reference.
|
||||
|
||||
Small opportunistic tasks discovered during another session use **Ad Hoc Tasks**:
|
||||
`workplans/ADHOC-YYYY-MM-DD.md`, workstream slug `adhoc-YYYY-MM-DD`, and task ids
|
||||
`workplans/ADHOC-YYYY-MM-DD.md`, workplan slug `adhoc-YYYY-MM-DD`, and task ids
|
||||
`ADHOC-YYYY-MM-DD-T01`, `T02`, etc. Use adhocs only for low-risk work completed
|
||||
directly. Promote anything requiring analysis, design, approval, dependencies, or
|
||||
multiple planned phases into a normal workplan.
|
||||
|
||||
Ecosystem todos from other agents arrive as `[repo:ops-warden]` hub tasks —
|
||||
visible at session start. Pick one up by creating the workplan file, then registering
|
||||
the workstream.
|
||||
visible at session start. Pick one up by creating the workplan file, committing,
|
||||
and running `statehub fix-consistency` — C-06 registers the workplan in the hub.
|
||||
Never register by hand with `create_workplan` (legacy MCP alias: `create_workstream`).
|
||||
|
||||
Task blocks use this shape:
|
||||
|
||||
|
|
@ -37,4 +38,18 @@ state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
|
|||
Status progression is `todo` → `progress` → `done`; use `wait` for waiting or
|
||||
blocked work and `cancel` for stopped work.
|
||||
|
||||
Workplan frontmatter carries `state_hub_workstream_id` — a legacy field name
|
||||
kept for compatibility; it holds the hub workplan UUID and is written by
|
||||
fix-consistency. Do not edit or rename it.
|
||||
|
||||
### Legacy terminology (compatibility footnote)
|
||||
|
||||
**Workplan** is the fleet term — see
|
||||
`the-custodian/canon/standards/workplan-terminology-fleet_v0.1.md`.
|
||||
**Workplan** is legacy only: some API routes (`/workstreams/`), params
|
||||
(`workstream_id`), MCP aliases (`create_workstream`), and the frontmatter field
|
||||
above remain until `STATE-WP-0069` retires them via legacy-meter. Treat those
|
||||
identifiers as workplan IDs. Prefer `GET /workplans/` and `workplan_id` in new
|
||||
examples and scripts.
|
||||
|
||||
<!-- Ralph Loop rules and HEUREKA sequence: ~/.claude/CLAUDE.md — do not duplicate here -->
|
||||
|
|
|
|||
|
|
@ -2,12 +2,31 @@
|
|||
# Custodian Brief — ops-warden
|
||||
|
||||
**Domain:** infotech
|
||||
**Last synced:** 2026-07-01 21:35 UTC
|
||||
**Last synced:** 2026-08-31 22:49 UTC
|
||||
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
|
||||
|
||||
## Active Workstreams
|
||||
|
||||
*(none — repo may need first-session setup)*
|
||||
### Layer model v0.7 conformance — state the deadline, bind the agent boundary, steward the estate's newest rule
|
||||
Progress: 0/5 done | workplan_id: `ae3ff76f-883d-5e2f-b6aa-144d61e8fdef`
|
||||
|
||||
**Open tasks:**
|
||||
- · Tasks `8b3bdb9f`
|
||||
- · Tasks `3318ee1a`
|
||||
- · Tasks `a891b32c`
|
||||
- · Tasks `94e73daa`
|
||||
- · Tasks `7d1b3c82`
|
||||
|
||||
### Tamper-resistant credential governance + mass rotation/lockdown (Strand B)
|
||||
Progress: 2/3 done | workplan_id: `21528e8d-a049-523d-9ae1-da7a27cb8bbf`
|
||||
|
||||
**Open tasks:**
|
||||
- ► Task: Graded lockdown / break-glass with explicit trust-root `cae498ee`
|
||||
|
||||
## Inbox Hygiene
|
||||
|
||||
**Stale unread:** 1 message(s) older than 3 day(s) — triage at session start.
|
||||
**Missing thread_id:** 6 unread message(s) lack supersession chains.
|
||||
|
||||
---
|
||||
## MCP Orientation (when available)
|
||||
|
|
|
|||
29
.forgejo/workflows/ci-smoke.yaml
Normal file
29
.forgejo/workflows/ci-smoke.yaml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Canonical CI smoke template (tier 1 routing drill).
|
||||
# Copy to: .forgejo/workflows/ci-smoke.yaml in consumer repos.
|
||||
name: CI Smoke
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
host-smoke:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Routing probe (host runner)
|
||||
run: |
|
||||
set -eu
|
||||
echo "repository=${GITHUB_REPOSITORY:-unknown}"
|
||||
echo "sha=${GITHUB_SHA:-unknown}"
|
||||
echo "runner=${RUNNER_NAME:-unknown}"
|
||||
uname -a
|
||||
|
||||
container-smoke:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Routing probe (container label)
|
||||
run: |
|
||||
set -eu
|
||||
echo "container-smoke ok for ${GITHUB_REPOSITORY:-unknown}"
|
||||
2360
.repo-manager/index.json
Normal file
2360
.repo-manager/index.json
Normal file
File diff suppressed because it is too large
Load diff
60
AGENTS.md
60
AGENTS.md
|
|
@ -19,7 +19,13 @@ there is no MCP server for Codex agents.
|
|||
| Context | URL |
|
||||
|---------|-----|
|
||||
| Local workstation | `http://127.0.0.1:8000` |
|
||||
| Remote via tunnel | `http://127.0.0.1:18000` |
|
||||
| Remote (railiance01, in-cluster) | `http://10.43.68.154:8000` |
|
||||
| Optional local edge relay | http://127.0.0.1:18080 |
|
||||
|
||||
When an operator has enabled the edge relay, set API_BASE to the relay URL.
|
||||
Queueable writes return an explicit queued receipt if the central hub is
|
||||
unreachable. Treat that as pending local evidence, then ask the operator to run
|
||||
statehub outbox status/replay after connectivity returns.
|
||||
|
||||
### Orient at session start
|
||||
|
||||
|
|
@ -27,8 +33,8 @@ there is no MCP server for Codex agents.
|
|||
# Offline brief — works without hub connection
|
||||
cat .custodian-brief.md
|
||||
|
||||
# Active workstreams for this domain
|
||||
curl -s "http://127.0.0.1:8000/workstreams/?topic_id=cee7bedf-2b48-46ef-8601-006474f2ad7a&status=active" \
|
||||
# Active workplans for this domain
|
||||
curl -s "http://127.0.0.1:8000/workplans/?topic_id=cee7bedf-2b48-46ef-8601-006474f2ad7a&status=active" \
|
||||
| python3 -m json.tool
|
||||
|
||||
# Check inbox
|
||||
|
|
@ -51,12 +57,12 @@ curl -s -X POST http://127.0.0.1:8000/progress/ \
|
|||
"summary": "what was done",
|
||||
"event_type": "note",
|
||||
"author": "codex",
|
||||
"workstream_id": "<uuid>",
|
||||
"workplan_id": "<uuid>",
|
||||
"task_id": "<uuid>"
|
||||
}'
|
||||
```
|
||||
|
||||
Omit `workstream_id` / `task_id` when not applicable.
|
||||
Omit `workplan_id` / `task_id` when not applicable.
|
||||
|
||||
### Update task status
|
||||
|
||||
|
|
@ -80,7 +86,7 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
|
|||
## Session Protocol
|
||||
|
||||
**Start:**
|
||||
1. `cat .custodian-brief.md` — domain goal and open workstreams (offline-safe)
|
||||
1. `cat .custodian-brief.md` — domain goal and open workplans (offline-safe)
|
||||
2. Check inbox: `GET /messages/?to_agent=ops-warden&unread_only=true`; mark read
|
||||
3. Scan workplans: `ls workplans/` — note `status: ready`, `active`, or `blocked` files and open tasks
|
||||
4. Check human-needed tasks: `GET /tasks/?needs_human=true`
|
||||
|
|
@ -92,12 +98,12 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
|
|||
**Close:**
|
||||
1. Update workplan file task statuses to reflect progress
|
||||
2. Log: `POST /progress/` with a summary of what changed
|
||||
3. Note for the custodian operator: after workplan file changes, run from
|
||||
`~/state-hub`:
|
||||
3. After workplan file changes, run:
|
||||
```bash
|
||||
make fix-consistency REPO=ops-warden
|
||||
statehub fix-consistency
|
||||
```
|
||||
This syncs task status from files into the hub DB.
|
||||
Coding agents should run this directly; ask the operator only if the CLI or
|
||||
State Hub API is unavailable. This syncs task status from files into the hub DB.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -113,18 +119,33 @@ other credential need belongs to another subsystem. **Do not** message
|
|||
|
||||
### Lookup (do this first)
|
||||
|
||||
**Always plan before drafting any founder credential step** (WARDEN-WP-0029):
|
||||
|
||||
```bash
|
||||
warden plan "<describe your need>" --json
|
||||
# verdict: autonomous | founder_required | unroutable
|
||||
# autonomous → run the commands (usually warden access --exec/--out/--wrap)
|
||||
# founder_required → escalate exactly one act via warden desk (not /tmp file drops)
|
||||
# unroutable → propose a CCR / catalog lane; do not improvise
|
||||
```
|
||||
|
||||
Supporting lookups:
|
||||
|
||||
```bash
|
||||
warden route find "<describe your need>" --json
|
||||
warden route show <catalog-id> --json
|
||||
warden route list # human output includes catalog source/hash freshness
|
||||
```
|
||||
|
||||
Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run warden`).
|
||||
If `warden plan` misses a known lane, the installed catalog may be stale (bundled
|
||||
fallback) — reinstall from checkout and re-run plan.
|
||||
|
||||
| Agent runtime | How to orient |
|
||||
| --- | --- |
|
||||
| **Codex / Grok** (shell, HTTP State Hub) | `warden route` commands above; inbox `to_agent=ops-warden` is for coordination, not secret vending |
|
||||
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workstreams; **still** use `warden route` for credential ownership |
|
||||
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden route` |
|
||||
| **Codex / Grok** (shell, HTTP State Hub) | `warden plan` first; inbox `to_agent=ops-warden` is for coordination, not secret vending |
|
||||
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workplans; **still** use `warden plan` / `warden route` for credential ownership |
|
||||
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden plan` |
|
||||
|
||||
### Quick routing table
|
||||
|
||||
|
|
@ -139,9 +160,22 @@ Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run wa
|
|||
|
||||
### Anti-patterns (do not do these)
|
||||
|
||||
- Drafting founder steps ("paste PAT to `/tmp`", "click Forgejo admin UI") **without**
|
||||
`warden plan` first (WP-0029)
|
||||
- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc.
|
||||
- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist
|
||||
- Pasting secrets into Git, State Hub, workplans, logs, or chat
|
||||
- **Reading a secret value onto a captured stdout.** Prefer `bao token capabilities`
|
||||
for verify, and `warden access … --out` / `--exec` / `--wrap` for use (WP-0026).
|
||||
- Steady-state credential **file drops** (`/tmp/…-token`); use desk paste-once or
|
||||
sanctioned transports instead
|
||||
|
||||
### Agent read-boundary + EXPOSED taint (WP-0026 T04/T05)
|
||||
|
||||
- High-risk lanes (`risk: high` in catalog): with `WARDEN_AGENT_ID` set, raw value
|
||||
streaming is refused. Use sanctioned transports only.
|
||||
- `warden taint <id>` reports EXPOSED metadata without reading secret data.
|
||||
- Playbooks: `wiki/playbooks/agent-read-boundary.md`, `wiki/playbooks/exposed-taint.md`.
|
||||
|
||||
### Other capabilities (reuse-surface)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,4 +9,5 @@
|
|||
@.claude/rules/architecture.md
|
||||
@.claude/rules/repo-boundary.md
|
||||
@.claude/rules/credential-routing.md
|
||||
@.claude/rules/finding-routing.md
|
||||
@.claude/rules/agents.md
|
||||
|
|
|
|||
180
INTENT.md
180
INTENT.md
|
|
@ -1,5 +1,68 @@
|
|||
---
|
||||
layer: Staff
|
||||
role: null # Engines only: PDP | PIP | Evidence | Lifecycle
|
||||
pep_shaped: true # §6.4 — issuing a certificate is a protected side effect
|
||||
standard: net-kingdom/canon/standards/security-layer-model_v0.7.md
|
||||
standard_version: "0.7"
|
||||
companion: net-kingdom/SECURITY-COMPANION.md
|
||||
declaration: layer.yaml
|
||||
pep_stance: pep-stance.yaml
|
||||
assent: docs/adr/ADR-0010
|
||||
---
|
||||
|
||||
# INTENT
|
||||
|
||||
> **ops-warden is Staff, and PEP-shaped.** Declared here in its own voice, per
|
||||
> `security-layer-model_v0.7` §11 — a layer stated *about* a repository by another
|
||||
> repository is not a declaration. The standard is **accepted**; its operative form
|
||||
> is `net-kingdom/SECURITY-COMPANION.md`. ops-warden's assent is `ADR-0010`, and its
|
||||
> reviews of v0.4, v0.6 and v0.7 are in `history/`.
|
||||
>
|
||||
> **Staff** because ops-warden's core function is judgement, not computation: it
|
||||
> decides which lane a need belongs to and stewards the paths through the estate's
|
||||
> rules. Its artifacts are runbooks, routing decisions, workplans. **PEP-shaped**
|
||||
> because issuing a certificate is a protected side effect — a shape, not a layer
|
||||
> (§6.4). ops-warden renders no authorization decision and never will; it consumes
|
||||
> them from `access-engine`.
|
||||
>
|
||||
> **The estate's front door for paths.** The companion routes the whole estate here:
|
||||
> *"For how to get something done — which lane, which credential, which route — ask
|
||||
> `ops-warden`. This document says what the rules are; ops-warden stewards the paths
|
||||
> through them."* That is a standing obligation, not a compliment: every rule
|
||||
> gate-house writes needs a path someone can actually walk, and ops-warden owes the
|
||||
> estate that path.
|
||||
>
|
||||
> **The declarations are files, not this note** — prose cannot distinguish a
|
||||
> declaration from a transcribed review (§11):
|
||||
>
|
||||
> | File | Declares | Enforced by |
|
||||
> | --- | --- | --- |
|
||||
> | `layer.yaml` | every Tooling contact, mapped to a §5.1/§5.2/§5.3 shape, plus the non-Tooling clients so the check is total | `scripts/check_layer_conformance.py`, `tests/test_layer_conformance.py` |
|
||||
> | `pep-stance.yaml` | the unreachable-engine stance map (§6.4 obl. 3), total per zone | test asserts the published map **equals** the shipped default |
|
||||
> | `tenancy.yaml` | tenancy posture, and `z1-operational` zone membership | `ADR-0009` |
|
||||
>
|
||||
> Both are cited in the standard as the estate's reference forms (§11, §6.4, §13.1).
|
||||
>
|
||||
> **Two declared engine gaps (§5.3), tracked non-conformance and not conformance.**
|
||||
> `VaultCA` signs over a direct OpenBao client and `warden desk` shells `bao kv put`.
|
||||
> Intended owner **secrets-engine**; blocked on no engine exposing an SSH-CA or
|
||||
> attended-provisioning surface; reviewed quarterly; registered in statute §13.
|
||||
> ops-warden keeps signing while the gap is open — refusing would remove production
|
||||
> host access to close a documentation gap.
|
||||
>
|
||||
> **The agent principal (§3.4).** ops-warden is operated by agents as well as people,
|
||||
> and they share the layer but not the blast radius. No standing credential; tool use
|
||||
> is a conduit or an engine API and there is no third route; **tool availability is
|
||||
> not permission**, which is exactly what `ADR-0004`'s read-boundary enforces; agent
|
||||
> memory is not a state plane. Session semantics belong to `glas-harness`, not here.
|
||||
>
|
||||
> **Evidence (§9.6).** ops-warden's audit trail is **attributive**, not load-bearing:
|
||||
> no control branches on the presence of a signing record. Emission is deliberately
|
||||
> non-atomic so an audit-store failure cannot remove production host access — a trade
|
||||
> the standard sanctions, declared in `wiki/AuditTrail.md`, registered in §13. If any
|
||||
> future control ever gates on this trail, that trade must be revisited before it
|
||||
> ships.
|
||||
|
||||
> This file captures **why this repository exists**, the **direction it is
|
||||
> moving toward**, and the **kind of system it is meant to become**.
|
||||
> It is intentionally **aspirational and stable**, not a description of
|
||||
|
|
@ -77,28 +140,52 @@ owns one lane and points at the rest:
|
|||
|
||||
---
|
||||
|
||||
## NetKingdom Security Literacy
|
||||
## Lane routing — who owns which need
|
||||
|
||||
ops-warden should be fluent in the platform architecture documented in
|
||||
`net-kingdom` — especially:
|
||||
**This is a runbook, not doctrine.** Security doctrine, the authority model, and the
|
||||
security curriculum are **gate-house's** (`security-layer-model_v0.7` §8).
|
||||
ops-warden references them and does not restate them. What follows is lane
|
||||
stewardship: which subsystem owns which need, and what ops-warden does about it.
|
||||
|
||||
| Plane / component | Role in access | ops-warden relationship |
|
||||
| --- | --- | --- |
|
||||
| **key-cape / Keycloak** | Identity — who is the actor, MFA, IAM Profile claims | Instruct identity path; do not re-implement OIDC |
|
||||
| **flex-auth + Topaz** | Authorization — may this actor perform this action | Caller-side policy gate shipped (opt-in); production flip is flex-auth's |
|
||||
| **OpenBao** | Runtime secrets — API keys, dynamic creds, leases, audit | Instruct custody paths; SSH engine is signing backend only; proxy reads as caller when `exec_capable` |
|
||||
| **secrets-engine** | Owner-native secret-exec (`secrets-engine exec`) | Route provisioned exec lanes (e.g. npm publish); ops-warden does not hold tokens |
|
||||
| **railiance-platform** (credential broker) | Scoped lease grants (`credential exec`) | Route `warden-sign` token needs; ops-warden does not mint OpenBao tokens |
|
||||
| **ops-warden** | Operational SSH certificates — short-lived host access | **Own and issue** this lane |
|
||||
| **ops-bridge** | Tunnel transport — consumes certs via `cert_command` | Primary consumer; document integration |
|
||||
| **railiance-infra** | Host principals, force-command, SSH hardening | Instruct host-side deployment; do not own Ansible |
|
||||
| **railiance-platform** (deploy) | OpenBao/K8s/platform service deployment | Instruct production endpoints; do not deploy clusters |
|
||||
The machine-readable form is `registry/routing/catalog.yaml`, and the executable form
|
||||
is `warden plan "<need>"` / `warden route find`. Prefer either over this table — it is
|
||||
orientation, and the catalog is the source of truth (`ADR-0001`).
|
||||
|
||||
| Component | Layer | Owns | ops-warden relationship |
|
||||
| --- | --- | --- | --- |
|
||||
| **gate-house** | Staff | Security doctrine, invariants, authority ceilings, authority context, conformance review, curriculum | **Route doctrine and authority-model questions here.** Not policy decisions — those go to access-engine |
|
||||
| **access-engine** (`flex-auth`) | Engine | **The policy decision** — whether an actor may act. The only decision point in NetKingdom | Consume decisions; caller-side pre-sign gate. ops-warden never renders or caches one |
|
||||
| **key-cape / Keycloak** | Tooling | Identity — who the actor is, MFA, IAM Profile claims | Instruct the identity path; do not re-implement OIDC |
|
||||
| **OpenBao** | Tooling | Runtime secrets — API keys, dynamic creds, leases, audit | Instruct custody paths; proxy reads as the caller when `exec_capable`. Direct client use is the declared exception above |
|
||||
| **secrets-engine** | Engine | Credential abstraction, custody, lifecycle; owner-native exec | Route provisioned exec lanes (e.g. npm publish). **Intended owner of the SSH-CA surface** |
|
||||
| **tenant-engine** | Engine | Tenant/client secret custody and front door | Route tenant lanes once fronted; current tenant proxies are interim (section 9) |
|
||||
| **user-engine** | Engine | Users, accounts, memberships | No ops-warden lane today; route rather than absorb |
|
||||
| **zone-engine** | Engine | Zone identity and membership | Consume compiled membership; ops-warden declares `z1-operational` (`ADR-0009`) |
|
||||
| **railiance-platform** (broker) | — | Scoped lease grants (`credential exec`) | Route `warden-sign` token needs; ops-warden does not mint OpenBao tokens |
|
||||
| **ops-mason** | Staff | Building and tearing down access routes and perimeters | Peer lane owner; same lane/rule demarcation applies |
|
||||
| **ops-warden** | Staff | **Operational access lanes** — short-lived SSH certificates, routing, stewardship, runbooks | **Own and issue** the SSH lane |
|
||||
| **ops-bridge** | Staff | Tunnel transport — consumes certs via `cert_command` | Primary consumer; document integration |
|
||||
| **railiance-infra** | — | Host principals, force-command, SSH hardening | Instruct host-side deployment; do not own Ansible |
|
||||
| **kings-guard** | Staff | Adaptive defence, observation, containment; publishes posture | Posture may reduce authority, never manufacture it |
|
||||
|
||||
### Access lane versus access rule
|
||||
|
||||
Normative, per `security-layer-model_v0.7` §8 and assented to in `ADR-0010`:
|
||||
|
||||
- **access lane** — ops-warden and ops-mason. *How* a worker reaches a host.
|
||||
- **access rule** — access-engine. *Whether* they may.
|
||||
|
||||
ops-warden owns the route and never the decision. A question about whether an actor
|
||||
may do something is not an ops-warden question, however it arrives.
|
||||
|
||||
Canonical references:
|
||||
|
||||
- `net-kingdom/SECURITY-COMPANION.md` — the operative form; start here
|
||||
- `net-kingdom/canon/standards/security-layer-model_v0.7.md` (accepted; §5 shapes, §6.4 PEP, §8 vocabulary)
|
||||
- `net-kingdom/docs/platform-identity-security-architecture.md`
|
||||
- `net-kingdom/docs/responsibility-map.md`
|
||||
- `wiki/AccessManagementDirective.md` (ops SSH actor model)
|
||||
- `.claude/rules/credential-routing.md` (agent-facing runbook — stays inline by design)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -121,7 +208,8 @@ Canonical references:
|
|||
| Need | Route to |
|
||||
| --- | --- |
|
||||
| OIDC login, MFA, human identity claims | key-cape / Keycloak (NetKingdom IAM Profile) |
|
||||
| Policy decision — may actor X access resource Y | flex-auth |
|
||||
| Security doctrine, invariants, authority model | gate-house |
|
||||
| Policy decision — may actor X access resource Y | access-engine (`flex-auth`) |
|
||||
| API keys, provider secrets, DB creds, object-storage STS | OpenBao (+ flex-auth policy where required) |
|
||||
| Inter-Hub operator keys, LLM provider credentials | OpenBao or approved operator secret store |
|
||||
| Tunnel lifecycle, port forwarding | ops-bridge |
|
||||
|
|
@ -170,6 +258,62 @@ Every ops-warden action appends metadata-only audit events; `warden activity`
|
|||
answers *what happened recently* in one command. Compliance checks (scorecard) make
|
||||
cert-side policy violations visible before they become incidents.
|
||||
|
||||
### 7. The founder is escalated to, never tasked with mechanics
|
||||
|
||||
*(added 2026-07-18, founder directive — see WARDEN-WP-0029)*
|
||||
|
||||
Workers and agents ask **ops-warden** what a credential need requires — never
|
||||
the founder directly. ops-warden answers three questions, in policy terms:
|
||||
|
||||
1. Can this be done autonomously under current policy and posture? → do it /
|
||||
route it, unattended.
|
||||
2. Does policy require a founder *decision or identity act* (OIDC login,
|
||||
Red-lane approval)? → escalate exactly that act, nothing more.
|
||||
3. Is the need unroutable? → name the missing lane and propose it (CCR),
|
||||
instead of improvising file drops or UI instructions.
|
||||
|
||||
Raw mechanics — "paste this PAT into /tmp", "click through the forgejo admin
|
||||
UI" — are **anti-patterns**: they leak credentials into CLI history and file
|
||||
artefacts and burn founder attention on work a lane should do. When founder
|
||||
interaction *is* required, prefer a purpose-built interaction surface (local
|
||||
web approval page rendering the exact action) over CLI/file handoffs.
|
||||
|
||||
### 8. Posture-aware: the organization is in build phase
|
||||
|
||||
Policy answers depend on lifecycle posture. The organization currently runs in
|
||||
**build phase**: one founder-operator, pre-revenue, velocity prioritized —
|
||||
pragmatic provisioning (workstation OIDC, per-repo deploy keys, advisory
|
||||
policy gates) is deliberately acceptable where audit and custody invariants
|
||||
hold (values only in OpenBao/process env; metadata-only trails). ops-warden
|
||||
must know the current posture, state it in its answers, and tighten defaults
|
||||
when the posture graduates (first customer data, first non-founder operator,
|
||||
production tier). Posture is declared configuration, not tribal knowledge.
|
||||
|
||||
### 9. Cover gaps, but never silently own them
|
||||
|
||||
*(added 2026-08-11, founder directive — see WARDEN-WP-0030)*
|
||||
|
||||
ops-warden **works with, and never replaces or duplicates**, secrets-engine,
|
||||
tenant-engine, user-engine and the other NetKingdom security components.
|
||||
|
||||
It may nonetheless *cover* a need that no component systematically provides yet —
|
||||
that is a legitimate service, and the `warden access` proxy makes it cheap. The
|
||||
danger is precisely that cheapness: an absorbed need looks permanent, stops
|
||||
registering as a missing capability, and quietly turns a routing layer into a
|
||||
second secrets broker.
|
||||
|
||||
So every execution position other than SSH issuance is **interim by default**:
|
||||
|
||||
- record the component that *should* own the front door, and what is missing
|
||||
- treat the cover as a tracked gap, not as ownership
|
||||
- delegate the moment that component ships its front door, keeping the proxy
|
||||
only as a fallback (`exec_owner` / `exec_command` — the WP-0019 pattern)
|
||||
|
||||
A gap ops-warden covers silently is worse than a gap it refuses, because the
|
||||
refusal is visible and the cover is not. Filling the gap properly — with the
|
||||
owner's governance, custody, and policy — is the goal; ops-warden holding the
|
||||
lane is the temporary means.
|
||||
|
||||
---
|
||||
|
||||
## Credential flow (target mental model)
|
||||
|
|
@ -236,6 +380,9 @@ ops-warden is succeeding when:
|
|||
5. Non-SSH secrets remain **out of ops-warden storage** — only documented paths.
|
||||
6. Security blockers can be classified by environment posture, workload maturity,
|
||||
owner route, and non-secret evidence instead of by vague credential risk.
|
||||
7. Every ops-warden execution position is explicitly **permanent** (SSH issuance) or
|
||||
explicitly **interim** with a named intended owner and blocker — so gaps ops-warden
|
||||
covers stay visible as gaps and can be handed back.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -248,6 +395,9 @@ ops-warden is succeeding when:
|
|||
- Host-side SSH configuration deployment
|
||||
- **Duplicating or restating another subsystem's procedure** — routing material
|
||||
points at the owner's docs; it does not fork them
|
||||
- **Permanently owning a lane that belongs to another component** — covering an
|
||||
unfilled gap is acceptable and expected; keeping it after the owner can front it,
|
||||
or holding it without recording that it is interim, is not (§9)
|
||||
- SSO / Teleport at scale (trigger per Access Management Directive §6.2)
|
||||
|
||||
---
|
||||
|
|
|
|||
161
LICENSE
161
LICENSE
|
|
@ -1,16 +1,151 @@
|
|||
MIT No Attribution
|
||||
# Target Revenue Source License
|
||||
|
||||
Copyright <YEAR> <COPYRIGHT HOLDER>
|
||||
**Version 1.0, Candidate 1 (V1C1)**
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
software and associated documentation files (the "Software"), to deal in the Software
|
||||
without restriction, including without limitation the rights to use, copy, modify,
|
||||
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so.
|
||||
---
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
> **PRELIMINARY CANDIDATE — SUBJECT TO CHANGE — NOT FINAL**
|
||||
>
|
||||
> This repository is governed by the Target Revenue Source License
|
||||
> (TRSL), Version 1, Candidate 1. This is the framework's first working
|
||||
> candidate, adopted as the org's preliminary operating license across
|
||||
> all repos (maintainer decision, 2026-07-29) during the build/alpha
|
||||
> stage — see `SCOPE.md` §1 and Appendix A of the canonical text (link
|
||||
> below) for the alpha/beta risk-acceptance decision this reflects and
|
||||
> what it does and does not mean. Full specialist legal review is
|
||||
> explicitly deferred until the framework moves out of beta.
|
||||
>
|
||||
> **Canonical source, full candidate-status banner, and Appendix A
|
||||
> (non-normative candidate notes tracking every open item):** the
|
||||
> `coulomb` org's `target-revenue` repository,
|
||||
> `specs/TargetRevenueSourceLicense-V1C1.md` — this file is the operative
|
||||
> legal text (Preamble through Section 11) only; the canonical document
|
||||
> is authoritative if this copy and that document ever diverge.
|
||||
|
||||
---
|
||||
|
||||
## Preamble
|
||||
|
||||
This Target Revenue Source License ("**License**") governs the Software identified in the applicable Phase Manifest. It implements the Target Revenue Framework: a defined development Phase accumulates Development Credit and Remission Credit against an immutable Initial Target until the Milestone Release automatically and irrevocably converts to a declared permissive Future License.
|
||||
|
||||
Commercial beneficiaries fund the creation and early availability of a software improvement; once the declared target is satisfied, the governed release becomes permissively open source.
|
||||
|
||||
## 1. Definitions
|
||||
|
||||
Capitalized terms used in this License have the meanings given below. Where a term is also defined in the Phase Manifest or Target Ledger for a specific Phase, the Phase Manifest and Target Ledger govern the *values* (amounts, dates, identifiers) and this License governs the *legal effect* of those values — the two must not be read as conflicting definitions of the same concept.
|
||||
|
||||
**"Commercial Entitlement"** means a right, purchased or otherwise granted under a Commercial Use Agreement, to make Commercial Use of the Software during a Phase.
|
||||
|
||||
**"Commercial Use"** means billing, invoicing, or otherwise charging any customer a fee, subscription, license fee, or other consideration for or in connection with use of the Software, at any time before the Conversion Event for the applicable Phase, regardless of whether the person or organization billed would otherwise qualify for Noncommercial Use. Commercial Use occurs by virtue of such billing alone, whether or not the resulting payment is registered with the Trust Service; in particular, billing a customer for pre-conversion use of the Software without recording the corresponding payment in the applicable Phase's Target Ledger is Commercial Use without a valid Commercial Entitlement — a violation of Section 3, addressed under Section 7 and, where applicable, the Enforcement Network described in the canonical repository's `specs/EnforcementNetworkConcept.md`.
|
||||
|
||||
**"Commercial Use Agreement"** means the separate agreement, referenced by the applicable Phase Manifest, under which a Commercial Entitlement is purchased or granted. This License does not itself set pricing, metering, or payment terms — those are governed by the Commercial Use Agreement.
|
||||
|
||||
**"Conversion Event"** means the moment the Outstanding Target for a Phase reaches zero, as computed from the Phase Manifest and Target Ledger per the Target Ledger Specification. The Conversion Event occurs automatically and is not conditioned on any declaration, attestation, or other act by the Licensor or any Trust Service.
|
||||
|
||||
**"Development Credit"** means the portion of a collected and settled payment explicitly allocated toward satisfying the Initial Target of a specific Phase, as recorded in that Phase's Target Ledger.
|
||||
|
||||
**"Future License"** means the permissive license identified in the applicable Phase Manifest, being either the MIT License or the Apache License, Version 2.0, which applies to the Milestone Release upon the Conversion Event.
|
||||
|
||||
**"Initial Target"** means the immutable monetary target declared for a Phase in its Phase Manifest.
|
||||
|
||||
**"Licensor"** means **Binky Hedgehog GmbH**, the party that publishes the Phase Manifest and holds the rights necessary to grant this License and the Future License for the Milestone Release.
|
||||
|
||||
**"Milestone Release"** means the precisely identified software release designated in the applicable Phase Manifest, identified by an immutable source revision, release artifact, or cryptographic digest.
|
||||
|
||||
**"Noncommercial Use"** means use of the Software for personal purposes, private study, hobby or amateur projects; use by any charitable organization, educational institution, public research organization, or government institution acting in a non-revenue-generating capacity; or other use of a materially similar character.
|
||||
|
||||
**"Outstanding Target"** means, at any time, `max(0, Initial Target − cumulative Development Credit − cumulative Remission Credit)` for a Phase, as computed from that Phase's Target Ledger.
|
||||
|
||||
**"Phase"** means a bounded development undertaking governed by one Initial Target, one Milestone Release, one degeneration policy, and one Future License declaration, as declared in a Phase Manifest.
|
||||
|
||||
**"Phase Manifest"** means the published, immutable declaration identifying a Phase, its Milestone Release, Initial Target, Future License, degeneration policy, and Target Ledger location, as specified in the Phase Manifest Specification.
|
||||
|
||||
**"Remission Credit"** means a transparent, non-revenue reduction of a Phase's Outstanding Target, generated under that Phase's published degeneration policy and recorded in the Target Ledger.
|
||||
|
||||
**"Settled Payment"** means a payment that has cleared through its payment processor and is no longer subject to reversal in the ordinary course (chargeback, dispute, or equivalent), as further specified by the applicable Commercial Use Agreement or monetization extension.
|
||||
|
||||
**"Software"** means the source code, object code, and associated documentation of the Milestone Release identified in the applicable Phase Manifest.
|
||||
|
||||
**"Target Ledger"** means the append-only record of Development Credit, Remission Credit, and correction entries for a Phase, as specified in the Target Ledger Specification.
|
||||
|
||||
**"You"** or **"Licensee"** means the individual or entity exercising rights under this License.
|
||||
|
||||
## 2. Grant of Rights for Noncommercial Use
|
||||
|
||||
Subject to the terms of this License, the Licensor grants You a worldwide, royalty-free, non-exclusive license, during the applicable Phase, to:
|
||||
|
||||
(a) use, reproduce, and study the Software for any Noncommercial Use;
|
||||
|
||||
(b) modify the Software and create derivative works of it for any Noncommercial Use; and
|
||||
|
||||
(c) redistribute the Software and Your modifications, in source or object form, for any Noncommercial Use, provided that You include this License, unmodified, with any such redistribution, and that You do not remove or alter any copyright, patent, trademark, or attribution notices contained in the Software.
|
||||
|
||||
This grant does not extend to Commercial Use. Commercial Use requires a Commercial Entitlement under Section 3.
|
||||
|
||||
## 3. Commercial Use
|
||||
|
||||
You may not make Commercial Use of the Software during the applicable Phase unless You hold a valid, current Commercial Entitlement under a Commercial Use Agreement with the Licensor covering the applicable Phase. A Commercial Entitlement granted under one Phase's Commercial Use Agreement does not extend to a later Phase's Milestone Release unless the Commercial Use Agreement expressly says so.
|
||||
|
||||
This Section 3 states the existence and boundary of the commercial-use restriction. It does not itself set pricing, invoicing, metering, audit rights, or payment terms — those are governed exclusively by the applicable Commercial Use Agreement.
|
||||
|
||||
## 4. Patent License
|
||||
|
||||
Subject to the terms of this License, each contributor to the Software grants You, during the applicable Phase and solely to the extent of rights granted under Sections 2 and 3, a perpetual (subject to the termination below), worldwide, non-exclusive, no-charge, royalty-free patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Software, limited to those patent claims licensable by that contributor that are necessarily infringed by their contribution(s) alone or by combination of their contribution(s) with the Software.
|
||||
|
||||
If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Software or a contribution incorporated within it constitutes direct or contributory patent infringement, then any patent licenses granted to You under this Section 4 for the Software shall terminate as of the date such litigation is filed.
|
||||
|
||||
## 5. Automatic Conversion to the Future License
|
||||
|
||||
**5.1 Automatic effect.** Upon the Conversion Event for a Phase, the rights and restrictions in Sections 3 (Commercial Use) of this License, as they apply to that Phase's Milestone Release, terminate automatically. In their place, the Milestone Release is licensed under the Future License identified in that Phase's Phase Manifest, effective as of the Conversion Event, without any further act, declaration, or attestation required by the Licensor, any Trust Service, or any other party.
|
||||
|
||||
**5.2 Irrevocability.** Once a valid Conversion Event has occurred for a Phase, no subsequent refund, chargeback, accounting correction, dispute, or termination of this License for an unrelated breach shall revoke, suspend, or otherwise impair the Future License grant for that Phase's Milestone Release. Any shortfall or dispute arising after a Conversion Event is a commercial or accounting matter between the relevant parties and does not reinstate a commercial-use restriction over already-converted Software.
|
||||
|
||||
**5.3 Prior freedom preserved.** A later Phase covering subsequent improvements to the Software does not restrict, withdraw, or otherwise affect the rights granted under the Future License for an earlier Phase's already-converted Milestone Release.
|
||||
|
||||
**5.4 Evidence, not cause.** A Trust Service may publish a Conversion Attestation documenting a Conversion Event. Such an attestation is evidence that the Conversion Event occurred; it is not a condition of, and its absence or delay does not postpone, the automatic effect described in Section 5.1. Any person may independently verify whether a Conversion Event has occurred directly from the Phase Manifest and Target Ledger.
|
||||
|
||||
## 6. Successive Phases
|
||||
|
||||
The Licensor may declare a new Phase covering subsequent improvements to the Software following a Milestone Release's Conversion Event. Each Phase is independently governed by its own Phase Manifest, Initial Target, degeneration policy, and Target Ledger. Nothing in a later Phase's Phase Manifest may be construed to reduce or withdraw rights already granted under Section 5 for an earlier Phase's Milestone Release.
|
||||
|
||||
## 7. Term and Termination
|
||||
|
||||
**7.1 Term.** This License applies to the Software for the duration of the applicable Phase, and, for the Milestone Release, indefinitely following that Phase's Conversion Event under the Future License.
|
||||
|
||||
**7.2 Termination for breach.** If You breach Section 3 (Commercial Use) or Section 2(c) (redistribution notice requirement), the Licensor may terminate this License as to You. Before such termination becomes effective, the Licensor shall provide You written notice of the breach; if You cure the breach within thirty (30) days of that notice, this License continues in effect. A second breach of the same provision within twelve (12) months may be terminated immediately without a further cure opportunity.
|
||||
|
||||
**7.3 Effect of termination.** Termination under this Section 7 affects only Your rights under Sections 2 and 3 for the Phase in which the breach occurred. It does not affect any rights already vested under Section 5 (Automatic Conversion) for a Milestone Release whose Conversion Event has already occurred, per Section 5.2.
|
||||
|
||||
**7.4 Public record of breach and resolution.** The Licensor shall cause the Trust Service to publish, as part of the public record for the affected Phase, notice of: (a) any breach notice issued under Section 7.2, stating the general nature of the breach and the date of notice; (b) whether the breach was cured within the applicable cure period, and the date of cure; and (c) any termination determination made under this Section 7, including its effective date and scope. This public record exists to give the ecosystem a transparent, verifiable conformity signal for the Phase, distinct from and in addition to the Development Credit and Remission Credit facts already published under Section 5.4 and the Target Ledger Specification.
|
||||
|
||||
A breach that You dispute, and that has not been finally determined, shall be recorded as **alleged**; it shall be recorded as **determined** only once the cure period has run without cure, or the dispute has been resolved against You under the applicable Commercial Use Agreement's dispute process, if any. The Trust Service shall update the record promptly upon resolution in either direction. Recording an alleged or determined breach under this Section 7.4 is a ministerial act of publishing the Licensor's determination (or a dispute process's outcome); it does not give the Trust Service discretionary authority to decide whether a breach occurred, consistent with Section 5.4's evidence-not-cause principle.
|
||||
|
||||
Whether, and under what conditions, the public record identifies a Commercial Entitlement holder by name is governed exclusively by the applicable Commercial Use Agreement, which the Licensor and that Commercial Entitlement holder negotiate and agree to directly. This License does not itself set a naming default. Where no Commercial Use Agreement addresses the question, or where the affected party has no Commercial Use Agreement at all (for example, a Section 2(c) breach by a Noncommercial Use licensee), the public record states the Phase and breach category only, without naming the party.
|
||||
|
||||
## 8. Disclaimer of Warranty
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. THE LICENSOR DOES NOT WARRANT THAT THE SOFTWARE WILL BE ERROR-FREE OR THAT ANY PHASE WILL REACH ITS CONVERSION EVENT.
|
||||
|
||||
## 9. Limitation of Liability
|
||||
|
||||
IN NO EVENT SHALL THE LICENSOR OR ANY CONTRIBUTOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE, EXCEPT TO THE EXTENT SUCH LIMITATION IS PROHIBITED BY APPLICABLE LAW.
|
||||
|
||||
## 10. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary attribution.
|
||||
|
||||
## 11. General Provisions
|
||||
|
||||
**11.1 Governing law and venue.** Adopted for alpha/beta 2026-07-29: disputes arising under this License shall be resolved by binding arbitration, seated at a neutral, arbitration-mature venue (Singapore or London are the two candidates concretely supported by current research), rather than by litigation in a national court. The specific arbitral institution and substantive governing law remain a per-deployment blank pending final selection; they are not fixed by this candidate. See the canonical repository's `history/260729-TRSL-Jurisdiction-Synthesis.md` §2.
|
||||
|
||||
**11.2 Severability.** If any provision of this License is held unenforceable, the remaining provisions remain in full force, and the unenforceable provision shall be reformed to the minimum extent necessary to make it enforceable.
|
||||
|
||||
**11.3 No waiver.** Failure to enforce any provision of this License is not a waiver of future enforcement of that or any other provision.
|
||||
|
||||
**11.4 Entire agreement (as to licensing).** This License, together with the applicable Phase Manifest and, where applicable, the Commercial Use Agreement, constitutes the entire agreement between You and the Licensor regarding the Software's licensing terms. Operations, service, and consulting arrangements are governed by separate agreements, if any, and are not part of this License.
|
||||
|
||||
**11.5 Definitions control.** Marketing materials, documentation, or other non-normative communications about the Software must not describe pre-Conversion-Event Software as "Open Source," "free software," or "open core." Pre-conversion Noncommercial Use is **source-available**; pre-conversion Commercial Use requires a **Commercial Entitlement**; only post-conversion Software may be described as Open Source, under the Future License.
|
||||
|
||||
---
|
||||
|
||||
**No Phase is currently declared for this repository under this License.** Until a Phase Manifest is published and registered with the Trust Service for a Milestone Release in this repository, Sections 2–7 above have no operative subject matter here — this License establishes the governing framework in advance of that declaration, consistent with the org-wide rollout decision recorded in `target-revenue`'s `workplans/TREV-WP-0008-governance-and-pilot-rollout.md`.
|
||||
|
|
|
|||
61
README.md
61
README.md
|
|
@ -9,8 +9,41 @@ See `INTENT.md` for direction, `SCOPE.md` for current implementation, and
|
|||
and routes every other credential need to its owner — see `wiki/AccessRouting.md`.
|
||||
Latest gap analysis: `history/2026-06-17-post-wp0007-reassessment.md`.
|
||||
|
||||
## Get the source (Forgejo)
|
||||
|
||||
Canonical repo: `https://forgejo.coulomb.social/coulomb/ops-warden`
|
||||
Releases: `https://forgejo.coulomb.social/coulomb/ops-warden/releases`
|
||||
|
||||
**HTTPS clone:**
|
||||
|
||||
```bash
|
||||
git clone https://forgejo.coulomb.social/coulomb/ops-warden.git ~/ops-warden
|
||||
cd ~/ops-warden
|
||||
```
|
||||
|
||||
**SSH clone** (recommended for push/pull; add to `~/.ssh/config` if missing):
|
||||
|
||||
```sshconfig
|
||||
Host forgejo-remote
|
||||
HostName 92.205.62.239
|
||||
Port 30022
|
||||
User git
|
||||
IdentityFile ~/.ssh/id_gitea
|
||||
StrictHostKeyChecking accept-new
|
||||
```
|
||||
|
||||
```bash
|
||||
git clone forgejo-remote:coulomb/ops-warden.git ~/ops-warden
|
||||
cd ~/ops-warden
|
||||
```
|
||||
|
||||
Legacy Gitea remotes (`gitea-remote`, `gitea.coulomb.social`) still work during
|
||||
migration; new checkouts should use Forgejo.
|
||||
|
||||
## Install
|
||||
|
||||
From a Forgejo checkout:
|
||||
|
||||
**Recommended** (warden + experiential memory for route/worker/agent sessions):
|
||||
|
||||
```bash
|
||||
|
|
@ -41,6 +74,34 @@ phase-memory must be a sibling checkout at `../phase-memory` by default, or set
|
|||
`PHASE_MEMORY_REPO` when running make. Opt out of memory at runtime with
|
||||
`WARDEN_MEMORY=0`.
|
||||
|
||||
### Upgrade after a release
|
||||
|
||||
When a new tag is published on Forgejo (e.g. `v0.1.2`):
|
||||
|
||||
```bash
|
||||
cd ~/ops-warden
|
||||
git fetch --tags origin
|
||||
git pull --ff-only
|
||||
make install-all
|
||||
warden route list # sanity check the installed CLI
|
||||
```
|
||||
|
||||
If `warden` still behaves like an older build (same version string but missing
|
||||
recent subcommands or fixes), clear the cached wheel and reinstall:
|
||||
|
||||
```bash
|
||||
uv cache clean ops-warden
|
||||
uv tool install . --with-editable ../phase-memory --reinstall --force
|
||||
```
|
||||
|
||||
Check out a specific release:
|
||||
|
||||
```bash
|
||||
git fetch --tags origin
|
||||
git checkout v0.1.2
|
||||
make install-all
|
||||
```
|
||||
|
||||
## Quick start (local backend)
|
||||
|
||||
```bash
|
||||
|
|
|
|||
186
SCOPE.md
186
SCOPE.md
|
|
@ -17,7 +17,7 @@ access guidance aligned with NetKingdom canon.
|
|||
|
||||
---
|
||||
|
||||
## Where we are (2026-07-01)
|
||||
## Where we are (2026-08-22)
|
||||
|
||||
ops-warden **issues short-lived SSH certificates and routes every other credential
|
||||
need to the subsystem that owns it.** SSH signing is **production-verified** on
|
||||
|
|
@ -48,10 +48,13 @@ the read-only conformance checker `scripts/check_secret_posture_conformance.py`
|
|||
and the dev-tier contract-double library `warden.doubles` (T4). Canon landing in
|
||||
net-kingdom / info-tech-canon is owner-driven (tracked via coordination messages, T5).
|
||||
|
||||
**Policy gate** is shipped on the caller side (WP-0007) with production registry
|
||||
and smoke evidence (WP-0009 archived). flex-auth published the `ssh-certificate`
|
||||
policy package (FLEX-WP-0006). `policy.enabled` remains **false** in production
|
||||
until flex-auth is deployed to a reachable URL (flex-auth FLEX-WP-0007).
|
||||
**The policy gate is zone-aware.** The caller-identity path is production proven
|
||||
and the flex-auth pin enforces caller authentication. WP-0032 adopted
|
||||
`security-zones_v0.1`: the repo-wide `policy.enabled` and `policy.fail_closed`
|
||||
settings are retired, target workload membership compiles into flex-auth resource
|
||||
attributes, and ops-warden selects dependency failure behavior from the target
|
||||
zone. Unknown membership is explicit and uses the versioned build profile.
|
||||
Ops-warden itself declares `z1-operational` in `tenancy.yaml`.
|
||||
|
||||
**ops-bridge cert_command pilot** is shipped to pilot-ready (WP-0016): a read-only
|
||||
readiness gate (`scripts/check_tunnel_cert_readiness.py`) plus an opt-in offline
|
||||
|
|
@ -59,12 +62,50 @@ contract smoke (`--sign-smoke`); the playbook leads with the gate and the pilot
|
|||
(`agt-state-hub-bridge`) is handed to ops-bridge. The live tunnel cutover is
|
||||
ops-bridge's to execute.
|
||||
|
||||
**INTENT alignment:** SSH issuance mission met in production. ops-warden workplans
|
||||
through WP-0021 are finished; WP-0022 (audit) and WP-0023 (INTENT–SCOPE closeout)
|
||||
ship in July 2026. Remaining distance is in other repos' lanes: ops-bridge running
|
||||
the cert_command pilot cutover, flex-auth runtime deployment (FLEX-WP-0007, unblocks
|
||||
`policy.enabled: true`), and the owner-driven WP-0015 canon landing — plus ongoing
|
||||
operator hygiene.
|
||||
**Credential hygiene and the policy front door** shipped through July 2026:
|
||||
disclosure hygiene and rotation guidance (WP-0026 — `warden taint`,
|
||||
`warden rotate-guide`, agent read-boundary on high-risk lanes), the tenant secret
|
||||
custody pattern (WP-0028, first lane binky company email IMAP), experiential memory
|
||||
across worker/agent sessions (WP-0024), the Forgejo admin PAT lane (WP-0025), and the
|
||||
posture-aware policy front door (WP-0029 — `warden plan`, `warden desk`, declared
|
||||
`organization_posture: build` as a third axis). WP-0027 (tamper-resistant governance,
|
||||
mass rotation/lockdown) is drafted and sits in `backlog`.
|
||||
|
||||
**Delegation register** is the open question (WP-0030, proposed). ops-warden fronts
|
||||
11 catalog lanes as a caller-identity proxy with no record of which component *should*
|
||||
own that front door. The primitive to delegate exists and is proven
|
||||
(`exec_owner`/`exec_command` — secrets-engine for npm publish, the credential broker
|
||||
for warden-sign) but is used by 2 of 24 lanes. See
|
||||
`history/2026-08-11-delegation-surface-assessment.md`.
|
||||
|
||||
**INTENT alignment:** SSH issuance mission met in production. All ops-warden workplans
|
||||
through WP-0029 are finished except WP-0027 (`backlog`) and WP-0030 (`proposed`).
|
||||
Remaining distance is in other repos' lanes: ops-bridge running the cert_command pilot
|
||||
cutover, flex-auth publishing the zone-aware pre-sign stance package,
|
||||
the owner-driven WP-0015 canon landing, and — newly named — the missing owner front
|
||||
doors that keep ops-warden holding interim lanes (secrets-engine, tenant-engine).
|
||||
|
||||
### Layer-model conformance (v0.7, accepted)
|
||||
|
||||
ops-warden declares **Staff**, **PEP-shaped**, in `INTENT.md` frontmatter and in its
|
||||
own voice — `security-layer-model_v0.7` §11. Shipped declaration artifacts, both
|
||||
cited in the standard as the estate's reference forms:
|
||||
|
||||
| Artifact | Declares | Status |
|
||||
| --- | --- | --- |
|
||||
| `layer.yaml` | 5 Tooling contacts mapped to §5.1/§5.2/§5.3 shapes + non-Tooling clients so the check is total | shipped; named reference form (§11) |
|
||||
| `pep-stance.yaml` | unreachable-engine stance map, total per zone | shipped; registered in statute §13.1 (§6.4 obl. 3) |
|
||||
| `scripts/check_layer_conformance.py` | every direct Tooling client maps to a declared shape | shipped; CI-enforced |
|
||||
| `tests/test_layer_conformance.py` | the §5.2 no-authority property, and published stance map **equals** shipped default | shipped, 11 tests |
|
||||
|
||||
Conformance state under §11: **declared gap** — tracked non-conformance, not
|
||||
conformance. Two §5.3 contacts (`VaultCA` signing write, `warden desk` `bao kv put`),
|
||||
intended owner `secrets-engine`, registered in statute §13.
|
||||
|
||||
Four ops-warden findings have been adopted into the standard: §9.1's two marks
|
||||
(`pending` vs `declared-gap`), §5's Tooling scope rule, §6.4 obligation 1's second
|
||||
limb, and §13.1's existence. Reviews: `history/2026-08-29-layer-model-v04-review.md`,
|
||||
`-v06-review.md`, `-v07-scope-intent-assessment.md`.
|
||||
|
||||
### Issue vs route
|
||||
|
||||
|
|
@ -84,6 +125,12 @@ ops-warden executes exactly one lane with its own authority and routes/assists t
|
|||
Full role and boundary: `wiki/AccessRouting.md`. The catalog is a **pointer layer** —
|
||||
it never restates an owner's procedure (authored `steps` exist only for the SSH lane).
|
||||
|
||||
**Interim by default.** SSH issuance is the only lane ops-warden owns permanently.
|
||||
Where it proxies or assists, it is covering a need no component fronts yet — a
|
||||
legitimate service, but a *tracked gap*, retired to the owner once their front door
|
||||
exists (INTENT §9). Recording that intent per lane is WP-0030; today only
|
||||
`whynot-design-npm-publish` and `ops-warden-warden-sign-token` carry it.
|
||||
|
||||
Gap analysis: `history/2026-07-01-intent-scope-gap-analysis.md` (current);
|
||||
`history/2026-06-24-intent-scope-gap-analysis.md` (prior);
|
||||
`history/2026-06-18-post-wp0008-intent-scope-reassessment.md` (SSH lane);
|
||||
|
|
@ -101,6 +148,7 @@ Gap analysis: `history/2026-07-01-intent-scope-gap-analysis.md` (current);
|
|||
| NetKingdom evolution reflected in docs | Met |
|
||||
| Non-SSH secrets stay out of ops-warden | Met |
|
||||
| Workload posture / maturity model for secret-flow blockers | Met — two-axis standard + descriptors + conformance checker + dev doubles (WP-0015) |
|
||||
| Every execution position explicitly permanent or interim with a named owner | **Met** — every catalog entry carries `delegation:`; `warden route gaps` lists the interim set (WP-0030) |
|
||||
|
||||
**Maturity vector:** `D5 / A5 / C5 / R4` (Discovery / Availability / Completeness / Reliability)
|
||||
|
||||
|
|
@ -108,11 +156,37 @@ Gap analysis: `history/2026-07-01-intent-scope-gap-analysis.md` (current);
|
|||
| --- | --- | --- |
|
||||
| D5 | Discovery | Routing wiki + security map + pointer catalog + NK canon cross-links |
|
||||
| A5 | Availability | CLI + `warden route` + `warden access` advisory & proxy front door + `warden policy` + opt-in policy gate + agent `--json` |
|
||||
| C5 | Completeness | All ops-warden lanes shipped — SSH (prod), routing, access assist, posture conformance, cert_command pilot gate, two owner-native exec routes documented (secrets-engine npm, credential broker warden-sign). Open items are external: flex-auth prod flip + ops-bridge live cutover |
|
||||
| C5 | Completeness | All ops-warden lanes shipped — SSH (prod), routing, access assist, posture conformance, cert_command pilot gate, disclosure hygiene, tenant custody, policy front door, delegation register (WP-0030) |
|
||||
| R4 | Reliability | Live OpenBao sign + credential-broker policy-gate smoke evidence on Railiance (2026-07-01) |
|
||||
|
||||
---
|
||||
|
||||
## Governing rules (ours)
|
||||
|
||||
The decisions that bind this repo are ADRs in `docs/adr/`, each `owner: ops-warden` —
|
||||
meaning we follow them *and* we are the ones who may change them. Changing one is a
|
||||
superseding ADR, never an in-place edit.
|
||||
|
||||
| ADR | Rule |
|
||||
| --- | --- |
|
||||
| `ADR-0001` | The routing catalog is a pointer layer, never a second copy of an owner's procedure (CI-enforced) |
|
||||
| `ADR-0002` | ops-warden is a transparent conduit, never a secret broker |
|
||||
| `ADR-0003` | Cover gaps, but never silently own them |
|
||||
| `ADR-0004` | High-risk lanes refuse raw value streaming to agent sessions |
|
||||
| `ADR-0005` | Implement one lane narrowly, route everything else |
|
||||
| `ADR-0006` | Superseded: enforcement is zone-scoped, never a global flag |
|
||||
| `ADR-0007` | Build-stage permissiveness stops at credential disclosure; every lane carries an explicit `risk` grade |
|
||||
| `ADR-0008` | A lane's risk grade covers every field its path discloses, not just the field it is named after |
|
||||
| `ADR-0009` | Adopt security-zones v0.1 and compile explicit workload membership; PEP failure mode is per zone |
|
||||
| `ADR-0010` | ops-warden is Staff and PEP-shaped — it owns access lanes, never access rules; the direct OpenBao client is a declared engine gap, not an exemption |
|
||||
|
||||
Rules we follow but do not own — NetKingdom canon, the IAM profile, the
|
||||
credential-management standard, the-custodian's ADR-001 workplan convention — are
|
||||
cited, never copied here. Publishable through `policy-nexus`, which carries `owner`
|
||||
into the published page and index.
|
||||
|
||||
---
|
||||
|
||||
## Core Idea
|
||||
|
||||
**Today:** implements the SSH certificate lane from `wiki/AccessManagementDirective.md`
|
||||
|
|
@ -137,7 +211,8 @@ for the rest.
|
|||
- `cert_command`: `warden sign <actor> --pubkey <path>` → cert on stdout
|
||||
- TTL enforcement per `ActorType` (`adm` 48 h, `agt` 24 h, `atm` 8 h)
|
||||
- `warden status`, cleanup, scorecard, signatures log
|
||||
- Opt-in flex-auth policy gate (`policy.enabled`, `policy_decision_id` in log)
|
||||
- Zone-aware flex-auth policy gate (`policy_decision_id`, zone, failure mode, and
|
||||
outcome in the signing audit; no repo-wide enable switch)
|
||||
- Production flex-auth registry builder (`scripts/build_flex_auth_registry.py`,
|
||||
`registry/flex-auth/production_registry_snapshot.json`)
|
||||
- Policy gate smoke runner (`scripts/policy_gate_production_smoke.sh`)
|
||||
|
|
@ -165,6 +240,19 @@ for the rest.
|
|||
- **Unified audit trail** (WP-0022): append-only `audit.jsonl`, secret-material guard,
|
||||
instrumentation on sign/access/worker paths, `warden activity` CLI merging legacy
|
||||
logs + optional State Hub notes (`wiki/AuditTrail.md`)
|
||||
- **Experiential memory** (WP-0024, `src/warden/memory.py`) — recorded outcomes feed
|
||||
routing and coordination; no secret values, guardrail allowlist unchanged
|
||||
- **Disclosure hygiene** (WP-0026): `warden taint <catalog-id>` (KV `custom_metadata`,
|
||||
no data read), `warden rotate-guide`, safe fetch transports (`--out` / `--exec` /
|
||||
`--wrap`) with refusal to stream to non-terminal stdout, and the agent read-boundary
|
||||
on `risk: high` lanes (exit 7 when `WARDEN_AGENT_ID` is set)
|
||||
- **Tenant secret custody** (WP-0028): tenant vs `platform/workloads/...` path
|
||||
convention, policy/CCR/catalog ownership, first lane `binky-company-email-imap`
|
||||
- **Policy front door** (WP-0029): `warden plan "<need>" [--json]` returning
|
||||
`autonomous` / `founder_required` (typed act) / `unroutable` (CCR stub);
|
||||
`warden desk` loopback founder surface (approve, OIDC login, paste-once provision
|
||||
straight into OpenBao); `organization_posture: build` as posture axis C; catalog
|
||||
freshness reporting on `warden route list` and in plan JSON
|
||||
|
||||
### Stewardship (documentation and alignment)
|
||||
|
||||
|
|
@ -183,7 +271,7 @@ for the rest.
|
|||
| --- | --- |
|
||||
| WP-0001–0005 | Initial CLI, quality, hygiene, OpenBao docs, hub sync |
|
||||
| WP-0006 | Credential routing, security map, inventory patterns, OpenBao checklist |
|
||||
| WP-0007 | Opt-in flex-auth policy gate (`policy.enabled`) |
|
||||
| WP-0007 | Original opt-in flex-auth policy gate (global switch retired by WP-0032) |
|
||||
| WP-0008 | Production sign verification, stewardship closeout, archive hygiene |
|
||||
| WP-0009 | flex-auth registry + policy smoke; pickup brief for FLEX-WP-0007 |
|
||||
| WP-0010 | Access routing charter + pointer catalog |
|
||||
|
|
@ -198,8 +286,26 @@ for the rest.
|
|||
|
||||
| WP | Focus |
|
||||
| --- | --- |
|
||||
| WP-0017 | Access front-door discoverability |
|
||||
| WP-0018 | `whynot-design-npm-publish` — first concrete secret lane (production-exercised) |
|
||||
| WP-0019 | Route provisioned secret-exec lanes to secrets-engine (`exec_owner` pattern) |
|
||||
| WP-0020 | Coordination worker (`warden worker`) |
|
||||
| WP-0021 | Scheduled worker tick (systemd --user timer, kill switch) |
|
||||
| WP-0022 | Unified audit trail + `warden activity` |
|
||||
| WP-0023 | INTENT–SCOPE alignment closeout |
|
||||
| WP-0024 | Experiential memory across worker/agent sessions (`src/warden/memory.py`) |
|
||||
| WP-0025 | Forgejo admin PAT OpenBao lane (CCR-2026-0006) |
|
||||
| WP-0026 | Credential disclosure hygiene — `warden taint`, `warden rotate-guide`, agent read-boundary, safe fetch transports |
|
||||
| WP-0028 | Tenant secret custody pattern — tenant vs platform paths; first lane binky company email IMAP |
|
||||
| WP-0029 | Policy front door — `warden plan`, `warden desk`, `organization_posture: build` third axis |
|
||||
|
||||
### Open ops-warden work
|
||||
|
||||
| WP | Status | Focus |
|
||||
| --- | --- | --- |
|
||||
| WP-0027 | `active` | Break-glass design/rehearsal activated narrowly on T02; mass rotation and policy-manifest reconcile remain deferred |
|
||||
| WP-0032 | `finished` | Security zones adopted — global switch retired, explicit workload references compiled, and owner policy live |
|
||||
| WP-0030 | `proposed` | Delegation register — record intended owner + blocker on every interim lane, `warden route gaps`, promotion gate |
|
||||
|
||||
Remaining production distance is also in other repos' lanes (see Known gaps).
|
||||
|
||||
|
|
@ -207,11 +313,12 @@ Remaining production distance is also in other repos' lanes (see Known gaps).
|
|||
|
||||
| Gap | Owner | Notes |
|
||||
| --- | --- | --- |
|
||||
| flex-auth production runtime + registry deploy | flex-auth | **FLEX-WP-0007** — unblocks `policy.enabled: true` |
|
||||
| ops-bridge `cert_command` on live tunnels | ops-bridge | Playbook + readiness gate shipped (WP-0016); pilot cutover handed off, awaiting ops-bridge |
|
||||
| Principals sync warden ↔ railiance-infra | ops-warden + infra | `scripts/check_principals_drift.py` — operator runs periodically |
|
||||
| NK-WP-0009 joint SSH tutorial | net-kingdom | Parallel coordination track |
|
||||
| WP-0015 canon landing (generic `WorkloadMaturityLevel` + M0-M3 requirements) | net-kingdom + info-tech-canon | ops-warden drafted + offered (coordination msgs); owner-driven landing |
|
||||
| Owner front doors for workload secret lanes | secrets-engine | 6 lanes proxied by ops-warden that `secrets-engine exec` could front, as WP-0019 did for npm publish |
|
||||
| Owner front door for tenant secret lanes | tenant-engine | WP-0028 defined the custody pattern; 3 tenant lanes still fronted by ops-warden proxy |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -231,6 +338,9 @@ Remaining production distance is also in other repos' lanes (see Known gaps).
|
|||
- OpenBao / Vault cluster deployment → `railiance-platform`
|
||||
- Human admin SSH key generation (self-service `ssh-keygen`)
|
||||
- Session recording, SIEM, SSO / Teleport at scale
|
||||
- **Permanently owning another component's lane.** Covering an unfilled gap is in
|
||||
scope and expected; keeping it once secrets-engine / tenant-engine / user-engine
|
||||
can front it — or holding it without recording that it is interim — is not (INTENT §9)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -268,7 +378,12 @@ Remaining production distance is also in other repos' lanes (see Known gaps).
|
|||
- **Production sign:** verified 2026-06-18 (`history/2026-06-17-openbao-production-verify.md`)
|
||||
- **Access routing:** WP-0010 + WP-0011 shipped (`warden route`, pointer catalog)
|
||||
- **Policy gate:** caller shipped (WP-0007); registry + smoke complete (WP-0009 archived).
|
||||
`policy.enabled: false` until flex-auth reachable (`FLEX-WP-0007`)
|
||||
WP-0031 shipped the calling identity and flex-auth's pin now runs
|
||||
`callerAuth.mode: enforce` (FLEX-WP-0016) — the gate is **ready and verified**
|
||||
(`decision:f3f7c88f9585582a`, anonymous `/v1/check` -> 401). WP-0032 and
|
||||
`ADR-0009` retired the global switch: the compiled target workload selects the
|
||||
zone, flex-auth owns stance, and ops-warden applies the zone's PEP failure mode.
|
||||
Re-check caller identity with `scripts/check_policy_caller_identity.py`.
|
||||
- **Workload posture:** WP-0015 shipped (standard, descriptors, `warden policy`,
|
||||
conformance checker, dev doubles); canon landing owner-driven
|
||||
- **ops-bridge cert_command:** WP-0016 shipped to pilot-ready (readiness gate +
|
||||
|
|
@ -284,12 +399,24 @@ Remaining production distance is also in other repos' lanes (see Known gaps).
|
|||
- **Audit + activity:** WP-0022 shipped — `warden activity`, `wiki/AuditTrail.md`
|
||||
- **INTENT closeout:** WP-0023 shipped — INTENT refresh, production flip/cutover
|
||||
checklists, catalog promotion cadence, broker hint on missing `VAULT_TOKEN`
|
||||
- **Active work:** none open in ops-warden after WP-0022/0023; remaining distance is
|
||||
other repos' lanes
|
||||
- **Disclosure hygiene:** WP-0026 shipped — `warden taint`, `warden rotate-guide`,
|
||||
safe fetch transports (`--out`/`--exec`/`--wrap`), agent read-boundary on `risk: high`
|
||||
lanes (`wiki/playbooks/agent-read-boundary.md`)
|
||||
- **Tenant custody:** WP-0028 shipped — tenant vs platform path convention; first lane
|
||||
`binky-company-email-imap`. Front door is still an ops-warden proxy (tenant-engine gap)
|
||||
- **Policy front door:** WP-0029 shipped — `warden plan "<need>"` (autonomous /
|
||||
founder_required / unroutable), `warden desk` founder interaction surface, declared
|
||||
`organization_posture: build` as a third posture axis, catalog freshness reporting
|
||||
- **Delegation:** 27 catalog lanes carry `delegation:` (WP-0030). SSH is
|
||||
`permanent`; owner-fronted lanes are `native`; interim proxies name
|
||||
`intended_owner` + `blocked_on`. Query: `warden route gaps`.
|
||||
- **Active work:** WP-0027 (`backlog`); remaining production distance is other
|
||||
repos' lanes (and retiring interim covers as those owners ship front doors)
|
||||
- **Integration docs:** cert_command migration, token hygiene (broker-first), principals
|
||||
drift (`wiki/playbooks/`)
|
||||
- **Latest assessment:** `history/2026-07-01-intent-scope-gap-analysis.md`
|
||||
- **Latest workplans:** WP-0022 (audit), WP-0023 (INTENT–SCOPE closeout) — shipped July 2026
|
||||
- **Latest assessment:** `history/2026-08-11-delegation-surface-assessment.md`
|
||||
- **Latest workplans:** WP-0029 (policy front door) shipped July 2026; WP-0030
|
||||
(delegation register) shipped August 2026
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -329,14 +456,18 @@ Downstream: `ops-bridge` (primary), kaizen agents, CI automations, human operato
|
|||
|
||||
| Repo | Relationship |
|
||||
| --- | --- |
|
||||
| `gate-house` | Owns the security layer model, doctrine, invariants, authority context, and conformance review. ops-warden routes doctrine questions there, and the companion routes the estate's *path* questions back to ops-warden (`ADR-0010`) |
|
||||
| `net-kingdom` | Canonical security architecture; ops-warden aligns to it |
|
||||
| `ops-bridge` | Primary cert_command consumer |
|
||||
| `railiance-infra` | Host-side SSH principals and hardening |
|
||||
| `railiance-platform` | OpenBao deployment and platform secrets |
|
||||
| `flex-auth` | Authorization; policy package shipped (FLEX-WP-0006); runtime deploy FLEX-WP-0007 |
|
||||
| `flex-auth` | Authorization — ruled name `access-engine`; the only policy decision point. Policy package shipped (FLEX-WP-0006); runtime deploy FLEX-WP-0007 |
|
||||
| `key-cape` | Identity / IAM Profile lightweight mode |
|
||||
| `secrets-engine` | Owner-native secret-exec front door (`secrets-engine exec/route`); ops-warden routes provisioned secret lanes to it (WP-0019) |
|
||||
| `state-hub` | Workstream registry |
|
||||
| `secrets-engine` | Owner-native secret-exec front door (`secrets-engine exec/route`); ops-warden routes provisioned secret lanes to it (WP-0019) and holds 6 more as interim proxies pending its front doors |
|
||||
| `tenant-engine` | Intended owner of tenant/client secret front doors; ops-warden holds 3 tenant lanes as interim proxies (WP-0028 pattern, WP-0030 register) |
|
||||
| `user-engine` | End-user identity/account lifecycle; no ops-warden lane today — route rather than absorb |
|
||||
| `zone-engine` | Owns the security zone model and exception lifecycle (`ADR-0006`); ops-warden is its first consumer |
|
||||
| `state-hub` | Workplan registry |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -373,11 +504,16 @@ keywords: [access, credential, secret, npm, token, api-key, openbao, key-cape, l
|
|||
| --- | --- |
|
||||
| `INTENT.md` | Why ops-warden exists and where it is going |
|
||||
| `SCOPE.md` | What is implemented today (this file) |
|
||||
| `docs/adr/README.md` | **The rules ops-warden owns** — and how to tell ours from inherited canon |
|
||||
| `wiki/AccessRouting.md` | What ops-warden issues vs routes vs assists (role and boundary) |
|
||||
| `wiki/OperatorAccessAssist.md` | `warden access` front door + conduit-vs-broker boundary + guardrails |
|
||||
| `wiki/CredentialRouting.md` | Which subsystem for each credential need |
|
||||
| `wiki/WorkloadSecurityPosture.md` | Secret-store posture, workload maturity, and blocker triage |
|
||||
| `registry/routing/catalog.yaml` | Machine-readable routing pointer catalog |
|
||||
| `net-kingdom/SECURITY-COMPANION.md` | **The estate's operative security rules — start here** |
|
||||
| `layer.yaml` | Layer declaration: every Tooling contact and its §5 shape |
|
||||
| `pep-stance.yaml` | Unreachable-engine stance map (§6.4); equals shipped behaviour by test |
|
||||
| `tenancy.yaml` | Declared tenancy posture (`I1 A1 E0 P n/a R n/a V0`) and why each axis sits where it does |
|
||||
| `wiki/NetKingdomSecurityMap.md` | Platform security component map |
|
||||
| `examples/warden.production.example.yaml` | Production warden.yaml template |
|
||||
| `wiki/PolicyGatedSigning.md` | flex-auth opt-in gate + registry rollout |
|
||||
|
|
@ -388,7 +524,9 @@ keywords: [access, credential, secret, npm, token, api-key, openbao, key-cape, l
|
|||
| `wiki/AuditTrail.md` | Unified metadata-only audit + `warden activity` |
|
||||
| `wiki/playbooks/catalog-lane-promotion.md` | draft → active catalog promotion checklist |
|
||||
| `wiki/CertCommandInterface.md` | cert_command contract |
|
||||
| `history/2026-07-01-intent-scope-gap-analysis.md` | Current INTENT↔SCOPE gap analysis |
|
||||
| `history/2026-08-11-delegation-surface-assessment.md` | Current assessment — where ops-warden covers gaps and who should own them |
|
||||
| `workplans/WARDEN-WP-0030-delegation-register.md` | Delegation register plan (proposed) |
|
||||
| `history/2026-07-01-intent-scope-gap-analysis.md` | Prior INTENT↔SCOPE gap analysis |
|
||||
| `workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md` | Alignment closeout plan |
|
||||
| `history/2026-06-24-intent-scope-gap-analysis.md` | Prior gap analysis |
|
||||
| `history/2026-06-27-workload-security-posture-charter.md` | WP-0015 posture/conformance charter |
|
||||
|
|
|
|||
145
WORK-RECORDS.md
Normal file
145
WORK-RECORDS.md
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
# Work Records — ops-warden
|
||||
|
||||
> Generated by `statehub fix-consistency` (CUST-WP-0061-T04, work-record
|
||||
> stage 3). Do not edit by hand — edit the source file/block listed for
|
||||
> each record and re-run fix-consistency to refresh this index. Archived
|
||||
> workplans are omitted; closed decisions/intakes/engagements stay listed
|
||||
> so recently-resolved work is still visible. [auto]
|
||||
|
||||
| Kind | ID | Status | Lane | Source |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| workplan | WARDEN-WP-ADHOC-2026-06-27 | finished | — | workplans/ADHOC-2026-06-27.md |
|
||||
| workplan | WARDEN-WP-ADHOC-2026-06-29 | finished | — | workplans/ADHOC-2026-06-29.md |
|
||||
| workplan | WARDEN-WP-ADHOC-2026-08-11 | finished | — | workplans/ADHOC-2026-08-11.md |
|
||||
| workplan | WARDEN-WP-ADHOC-2026-08-17 | finished | — | workplans/ADHOC-2026-08-17.md |
|
||||
| workplan | WARDEN-WP-0016 | finished | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md |
|
||||
| workplan | WARDEN-WP-0017 | finished | — | workplans/WARDEN-WP-0017-access-front-door-discoverability.md |
|
||||
| workplan | WARDEN-WP-0018 | finished | — | workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md |
|
||||
| workplan | WARDEN-WP-0019 | finished | — | workplans/WARDEN-WP-0019-route-to-secrets-engine.md |
|
||||
| workplan | WARDEN-WP-0020 | finished | — | workplans/WARDEN-WP-0020-ops-warden-worker.md |
|
||||
| workplan | WARDEN-WP-0021 | finished | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md |
|
||||
| workplan | WARDEN-WP-0022 | finished | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md |
|
||||
| workplan | WARDEN-WP-0023 | finished | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md |
|
||||
| workplan | WARDEN-WP-0024 | finished | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md |
|
||||
| workplan | WARDEN-WP-0025 | finished | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md |
|
||||
| workplan | WARDEN-WP-0026 | finished | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md |
|
||||
| workplan | WARDEN-WP-0027 | active | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md |
|
||||
| workplan | WARDEN-WP-0028 | finished | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md |
|
||||
| workplan | WARDEN-WP-0029 | finished | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md |
|
||||
| workplan | WARDEN-WP-0030 | finished | — | workplans/WARDEN-WP-0030-delegation-register.md |
|
||||
| workplan | WARDEN-WP-0031 | finished | — | workplans/WARDEN-WP-0031-policy-caller-identity.md |
|
||||
| workplan | WARDEN-WP-0032 | finished | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| workplan | WARDEN-WP-0033 | finished | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
|
||||
| workplan | WARDEN-WP-0034 | ready | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md |
|
||||
| workplan | WARDEN-WP-0035 | finished | — | workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md |
|
||||
| workplan | WARDEN-WP-0036 | finished | — | workplans/WARDEN-WP-0036-attended-login-openbao-output.md |
|
||||
| task | WARDEN-WP-ADHOC-2026-06-27-T01 | done | — | workplans/ADHOC-2026-06-27.md |
|
||||
| task | WARDEN-WP-ADHOC-2026-06-29-T01 | done | — | workplans/ADHOC-2026-06-29.md |
|
||||
| task | WARDEN-WP-ADHOC-2026-08-11-T01 | done | — | workplans/ADHOC-2026-08-11.md |
|
||||
| task | WARDEN-WP-ADHOC-2026-08-11-T02 | done | — | workplans/ADHOC-2026-08-11.md |
|
||||
| task | WARDEN-WP-ADHOC-2026-08-11-T03 | done | — | workplans/ADHOC-2026-08-11.md |
|
||||
| task | WARDEN-WP-ADHOC-2026-08-17-T01 | done | — | workplans/ADHOC-2026-08-17.md |
|
||||
| task | WARDEN-WP-ADHOC-2026-08-17-T02 | done | — | workplans/ADHOC-2026-08-17.md |
|
||||
| task | WARDEN-WP-ADHOC-2026-08-17-T03 | done | — | workplans/ADHOC-2026-08-17.md |
|
||||
| task | WARDEN-WP-ADHOC-2026-08-17-T04 | done | — | workplans/ADHOC-2026-08-17.md |
|
||||
| task | WARDEN-WP-0016-T01 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md |
|
||||
| task | WARDEN-WP-0016-T02 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md |
|
||||
| task | WARDEN-WP-0016-T03 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md |
|
||||
| task | WARDEN-WP-0016-T04 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md |
|
||||
| task | WARDEN-WP-0017-T01 | done | — | workplans/WARDEN-WP-0017-access-front-door-discoverability.md |
|
||||
| task | WARDEN-WP-0017-T02 | done | — | workplans/WARDEN-WP-0017-access-front-door-discoverability.md |
|
||||
| task | WARDEN-WP-0017-T03 | done | — | workplans/WARDEN-WP-0017-access-front-door-discoverability.md |
|
||||
| task | WARDEN-WP-0018-T01 | done | — | workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md |
|
||||
| task | WARDEN-WP-0018-T02 | done | — | workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md |
|
||||
| task | WARDEN-WP-0018-T03 | done | — | workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md |
|
||||
| task | WARDEN-WP-0019-T01 | done | — | workplans/WARDEN-WP-0019-route-to-secrets-engine.md |
|
||||
| task | WARDEN-WP-0019-T02 | done | — | workplans/WARDEN-WP-0019-route-to-secrets-engine.md |
|
||||
| task | WARDEN-WP-0020-T01 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md |
|
||||
| task | WARDEN-WP-0020-T02 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md |
|
||||
| task | WARDEN-WP-0020-T03 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md |
|
||||
| task | WARDEN-WP-0020-T04 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md |
|
||||
| task | WARDEN-WP-0020-T05 | done | — | workplans/WARDEN-WP-0020-ops-warden-worker.md |
|
||||
| task | WARDEN-WP-0021-T01 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md |
|
||||
| task | WARDEN-WP-0021-T02 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md |
|
||||
| task | WARDEN-WP-0021-T03 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md |
|
||||
| task | WARDEN-WP-0021-T04 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md |
|
||||
| task | WARDEN-WP-0021-T05 | done | — | workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md |
|
||||
| task | WARDEN-WP-0022-T01 | done | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md |
|
||||
| task | WARDEN-WP-0022-T02 | done | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md |
|
||||
| task | WARDEN-WP-0022-T03 | done | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md |
|
||||
| task | WARDEN-WP-0022-T04 | done | — | workplans/WARDEN-WP-0022-audit-trail-and-activity.md |
|
||||
| task | WARDEN-WP-0023-T01 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md |
|
||||
| task | WARDEN-WP-0023-T02 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md |
|
||||
| task | WARDEN-WP-0023-T03 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md |
|
||||
| task | WARDEN-WP-0023-T04 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md |
|
||||
| task | WARDEN-WP-0023-T05 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md |
|
||||
| task | WARDEN-WP-0023-T06 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md |
|
||||
| task | WARDEN-WP-0023-T07 | done | — | workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md |
|
||||
| task | WARDEN-WP-0024-T01 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md |
|
||||
| task | WARDEN-WP-0024-T02 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md |
|
||||
| task | WARDEN-WP-0024-T03 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md |
|
||||
| task | WARDEN-WP-0024-T04 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md |
|
||||
| task | WARDEN-WP-0024-T05 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md |
|
||||
| task | WARDEN-WP-0024-T06 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md |
|
||||
| task | WARDEN-WP-0024-T07 | done | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md |
|
||||
| task | WARDEN-WP-0025-T01 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md |
|
||||
| task | WARDEN-WP-0025-T02 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md |
|
||||
| task | WARDEN-WP-0025-T03 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md |
|
||||
| task | WARDEN-WP-0025-T04 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md |
|
||||
| task | WARDEN-WP-0025-T05 | done | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md |
|
||||
| task | WARDEN-WP-0026-T01 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md |
|
||||
| task | WARDEN-WP-0026-T02 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md |
|
||||
| task | WARDEN-WP-0026-T03 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md |
|
||||
| task | WARDEN-WP-0026-T04 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md |
|
||||
| task | WARDEN-WP-0026-T05 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md |
|
||||
| task | WARDEN-WP-0026-T06 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md |
|
||||
| task | WARDEN-WP-0026-T07 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md |
|
||||
| task | WARDEN-WP-0027-T01 | cancel | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md |
|
||||
| task | WARDEN-WP-0027-T02 | progress | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md |
|
||||
| task | WARDEN-WP-0027-T03 | cancel | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md |
|
||||
| task | WARDEN-WP-0028-T01 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md |
|
||||
| task | WARDEN-WP-0028-T02 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md |
|
||||
| task | WARDEN-WP-0028-T03 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md |
|
||||
| task | WARDEN-WP-0028-T04 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md |
|
||||
| task | WARDEN-WP-0028-T05 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md |
|
||||
| task | WARDEN-WP-0028-T06 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md |
|
||||
| task | WARDEN-WP-0028-T07 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md |
|
||||
| task | WARDEN-WP-0029-T01 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md |
|
||||
| task | WARDEN-WP-0029-T02 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md |
|
||||
| task | WARDEN-WP-0029-T03 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md |
|
||||
| task | WARDEN-WP-0029-T04 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md |
|
||||
| task | WARDEN-WP-0029-T05 | done | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md |
|
||||
| task | WARDEN-WP-0030-T01 | done | — | workplans/WARDEN-WP-0030-delegation-register.md |
|
||||
| task | WARDEN-WP-0030-T02 | done | — | workplans/WARDEN-WP-0030-delegation-register.md |
|
||||
| task | WARDEN-WP-0030-T03 | done | — | workplans/WARDEN-WP-0030-delegation-register.md |
|
||||
| task | WARDEN-WP-0030-T04 | done | — | workplans/WARDEN-WP-0030-delegation-register.md |
|
||||
| task | WARDEN-WP-0030-T05 | done | — | workplans/WARDEN-WP-0030-delegation-register.md |
|
||||
| task | WARDEN-WP-0031-T01 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md |
|
||||
| task | WARDEN-WP-0031-T02 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md |
|
||||
| task | WARDEN-WP-0031-T03 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md |
|
||||
| task | WARDEN-WP-0031-T04 | done | — | workplans/WARDEN-WP-0031-policy-caller-identity.md |
|
||||
| task | WARDEN-WP-0031-T05 | cancel | — | workplans/WARDEN-WP-0031-policy-caller-identity.md |
|
||||
| task | WARDEN-WP-0032-T01 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0032-T02 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0032-T03 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0032-T04 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0032-T05 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0032-T06 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0032-T07 | done | — | workplans/WARDEN-WP-0032-security-zones.md |
|
||||
| task | WARDEN-WP-0033-T01 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
|
||||
| task | WARDEN-WP-0033-T02 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
|
||||
| task | WARDEN-WP-0033-T03 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
|
||||
| task | WARDEN-WP-0033-T04 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
|
||||
| task | WARDEN-WP-0033-T05 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
|
||||
| task | WARDEN-WP-0033-T06 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md |
|
||||
| task | WARDEN-WP-0034-T01 | todo | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md |
|
||||
| task | WARDEN-WP-0034-T02 | todo | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md |
|
||||
| task | WARDEN-WP-0034-T03 | todo | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md |
|
||||
| task | WARDEN-WP-0034-T04 | todo | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md |
|
||||
| task | WARDEN-WP-0034-T05 | todo | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md |
|
||||
| task | WARDEN-WP-0035-T01 | done | — | workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md |
|
||||
| task | WARDEN-WP-0035-T02 | done | — | workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md |
|
||||
| task | WARDEN-WP-0036-T01 | done | — | workplans/WARDEN-WP-0036-attended-login-openbao-output.md |
|
||||
| task | WARDEN-WP-0036-T02 | done | — | workplans/WARDEN-WP-0036-attended-login-openbao-output.md |
|
||||
| intake | WARDEN-IN-0001 | closed | — | intakes/intakes.md |
|
||||
| intake | WARDEN-IN-0002 | open | — | intakes/intakes.md |
|
||||
39
deploy/kubernetes/caller-identity.yaml
Normal file
39
deploy/kubernetes/caller-identity.yaml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# ops-warden's calling identity for flex-auth (WARDEN-WP-0031 T04).
|
||||
#
|
||||
# flex-auth's `flex-auth-ops-warden` pin binds `resource.system: ops-warden` to
|
||||
# the principal `system:serviceaccount:ops-warden:ops-warden` and TokenReviews
|
||||
# the caller's bearer token with audience `flex-auth` (FLEX-WP-0016).
|
||||
#
|
||||
# This ServiceAccount is the subject of that binding. It holds no RBAC at all —
|
||||
# it is never used to talk to the Kubernetes API, only to be *reviewed* by it.
|
||||
# A workstation `warden sign` mints a short-lived bound token against it:
|
||||
#
|
||||
# kubectl create token ops-warden -n ops-warden \
|
||||
# --audience flex-auth --duration 10m
|
||||
#
|
||||
# Boundary note: cluster resources are railiance-platform's to own. This
|
||||
# manifest lives here because the identity is ops-warden's and flex-auth's
|
||||
# binding names it; railiance-platform should adopt it into the cluster's own
|
||||
# manifests, at which point this file becomes the record of what was applied
|
||||
# rather than the source of truth (ADR-0003 — cover the gap, name the owner).
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: ops-warden
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: ops-warden
|
||||
netkingdom.coulomb.social/purpose: caller-identity
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: ops-warden
|
||||
namespace: ops-warden
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: ops-warden
|
||||
annotations:
|
||||
netkingdom.coulomb.social/bound-by: >-
|
||||
flex-auth-ops-warden callerAuth binding
|
||||
ops-warden=system:serviceaccount:ops-warden:ops-warden (FLEX-WP-0016)
|
||||
netkingdom.coulomb.social/workplan: WARDEN-WP-0031
|
||||
automountServiceAccountToken: false
|
||||
90
docs/adr/ADR-0001-catalog-is-a-pointer-layer.md
Normal file
90
docs/adr/ADR-0001-catalog-is-a-pointer-layer.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
---
|
||||
id: ops-warden-adr-0001
|
||||
type: adr
|
||||
title: "ADR-0001 — The routing catalog is a pointer layer, never a second copy"
|
||||
domain: infotech
|
||||
repo: ops-warden
|
||||
status: accepted
|
||||
version: "1.0"
|
||||
revision: "1"
|
||||
owner: ops-warden
|
||||
binds: "ops-warden; every repo contributing a catalog entry"
|
||||
created: "2026-06-20"
|
||||
updated: "2026-08-18"
|
||||
last_reviewed: "2026-08-18"
|
||||
review_interval: 6m
|
||||
enforced_by: tests/test_routing.py
|
||||
supersedes: ""
|
||||
successor: ""
|
||||
---
|
||||
|
||||
# ADR-0001 — The routing catalog is a pointer layer, never a second copy
|
||||
|
||||
## Status
|
||||
|
||||
Accepted. Decided during WARDEN-WP-0010 (access routing charter), enforced in code
|
||||
since WARDEN-WP-0011. Restated here because it binds repos other than ops-warden
|
||||
and had, until now, no address they could cite.
|
||||
|
||||
## Context
|
||||
|
||||
`registry/routing/catalog.yaml` tells a worker which subsystem owns a credential
|
||||
need and where the authoritative procedure lives. The obvious temptation, every
|
||||
time someone uses it, is to add the procedure itself: the reader is already here,
|
||||
the steps are short, and one more copy seems cheaper than a second lookup.
|
||||
|
||||
That temptation is the failure mode. A copied procedure is correct on the day it
|
||||
is written and silently wrong afterwards, because the owner changes theirs without
|
||||
knowing ours exists. The estate has already paid for this once, between an ADR and
|
||||
its published page — fixed by generating the page from the markdown rather than
|
||||
maintaining both.
|
||||
|
||||
The catalog is consulted precisely when someone is about to touch a credential.
|
||||
Being confidently wrong there is worse than being absent.
|
||||
|
||||
## Decision
|
||||
|
||||
**For any subsystem ops-warden does not own, a catalog entry carries identifiers
|
||||
and pointers only** — `owner_repo`, `subsystem`, `wiki_ref`, `canon_ref`,
|
||||
`need_keywords`, and the secret-free handoff metadata `warden access` needs.
|
||||
|
||||
**Authored procedure is permitted only where `warden_executes: true`.** A `steps:`
|
||||
block and a `cert_command:` may exist on the SSH certificate lane and nowhere else,
|
||||
because that is the one lane ops-warden actually owns. Rotation `steps:` are the
|
||||
narrow exception and describe what the *owner* does, recorded because rotation
|
||||
guidance had no other home; they are still pointers in spirit and must not grow
|
||||
into a runnable substitute for the owner's tooling.
|
||||
|
||||
**No secret material in this file, ever.**
|
||||
|
||||
This is enforced, not merely documented. `tests/test_routing.py` fails any non-SSH
|
||||
entry carrying a `steps` block, and checks that every `wiki_ref` anchor resolves to
|
||||
a real section. A rule that is only written down is a rule that erodes.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Accepted cost.** Two lookups instead of one. A worker who wants the procedure
|
||||
follows the pointer. We consider a correct second hop cheaper than a stale first
|
||||
one.
|
||||
|
||||
**Anchors must resolve.** Because the entry is only a pointer, a broken pointer is
|
||||
a total failure rather than a cosmetic one. Hence the anchor test — which has
|
||||
already caught a real break (`ADHOC-2026-08-11-T01`, a stale
|
||||
`rapp-qonto-keycape-client` anchor).
|
||||
|
||||
**Other repos are bound by this.** When another repo asks us to add or rename a
|
||||
lane, we add the pointer and decline to absorb the procedure. That has been
|
||||
exercised: on 2026-08-11 railiance-platform asked ops-warden to rename an active
|
||||
lane, and the answer was to cross-reference the id from their CCR rather than have
|
||||
this repo carry a second identity for the same thing.
|
||||
|
||||
**It constrains what this repo may usefully become.** ops-warden cannot grow into
|
||||
a documentation site for other people's credential procedures, however often that
|
||||
is asked for. The value of the catalog is that a reader knows it points at truth
|
||||
rather than at a copy of truth.
|
||||
|
||||
## Related
|
||||
|
||||
- `registry/routing/catalog.yaml` — the file this governs, header comment
|
||||
- `wiki/AccessRouting.md` — the issue-vs-route role and boundary
|
||||
- `ADR-0005` — the narrower charter this follows from
|
||||
90
docs/adr/ADR-0002-conduit-not-broker.md
Normal file
90
docs/adr/ADR-0002-conduit-not-broker.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
---
|
||||
id: ops-warden-adr-0002
|
||||
type: adr
|
||||
title: "ADR-0002 — ops-warden is a transparent conduit, never a secret broker"
|
||||
domain: infotech
|
||||
repo: ops-warden
|
||||
status: accepted
|
||||
version: "1.0"
|
||||
revision: "1"
|
||||
owner: ops-warden
|
||||
binds: "ops-warden"
|
||||
created: "2026-06-26"
|
||||
updated: "2026-08-18"
|
||||
last_reviewed: "2026-08-18"
|
||||
review_interval: 6m
|
||||
enforced_by: "src/warden/access.py; wiki/OperatorAccessAssist.md"
|
||||
supersedes: ""
|
||||
successor: ""
|
||||
---
|
||||
|
||||
# ADR-0002 — ops-warden is a transparent conduit, never a secret broker
|
||||
|
||||
## Status
|
||||
|
||||
Accepted. Decided during WARDEN-WP-0014 (operator access assist), tightened by
|
||||
WARDEN-WP-0026 (disclosure hygiene).
|
||||
|
||||
## Context
|
||||
|
||||
`warden access` is the operator front door for every credential need in the estate.
|
||||
For lanes marked `exec_capable` it does more than advise: it runs the owner's tool
|
||||
and returns the value. Anything that fetches secrets on request looks like a broker,
|
||||
and the gravity toward becoming one is strong — a broker is more convenient at every
|
||||
individual call site.
|
||||
|
||||
The distinction is not stylistic. A broker holds authority; a conduit borrows the
|
||||
caller's. Only one of those creates a new thing worth attacking.
|
||||
|
||||
## Decision
|
||||
|
||||
**ops-warden runs the owner's tool with the caller's own identity, and takes no
|
||||
custody of the value.** The caller's credentials do the work. ops-warden holds
|
||||
nothing after the command returns, stores nothing, and caches nothing.
|
||||
|
||||
**Forbidden: a standing broker.** ops-warden must not hold its own long-lived
|
||||
secret-read credential in order to serve values to callers who could not have
|
||||
fetched them themselves. If the caller lacks authority, the correct outcome is a
|
||||
denial from the owner's system — not a fetch performed on their behalf by a more
|
||||
privileged intermediary.
|
||||
|
||||
The test is a question: *could the caller have run this themselves?* If yes, we are
|
||||
a conduit and may proxy. If no, proxying is privilege laundering and is refused.
|
||||
|
||||
**Owner-native front doors outrank the proxy.** Where an owner has shipped their own
|
||||
exec surface — `secrets-engine exec`, the railiance-platform credential broker — we
|
||||
route there and do not proxy. The proxy is a fallback for lanes nobody fronts yet,
|
||||
not a preferred path. This is why `whynot-design-npm-publish` and
|
||||
`ops-warden-warden-sign-token` are `native` rather than `interim`.
|
||||
|
||||
**The value must not land somewhere it will be logged.** Sanctioned transports are
|
||||
`--out` (mode-0600 file), `--exec` (child process env), and `--wrap` (a single-use
|
||||
OpenBao wrapping token). Streaming to a non-terminal stdout is refused without an
|
||||
explicit `--unsafe-stdout`, which exists for interactive humans only.
|
||||
|
||||
## Consequences
|
||||
|
||||
**ops-warden never becomes a credential store, and gains no value by being
|
||||
compromised beyond the SSH CA it already holds.** This is the whole point. An
|
||||
attacker who owns ops-warden gets the SSH signing lane — serious, bounded, and
|
||||
already the thing this repo is hardened around — not a key to every secret in the
|
||||
estate.
|
||||
|
||||
**Some requests cannot be served, and that is the correct answer.** When a caller
|
||||
lacks authority, ops-warden routes and explains rather than fetching. This reads as
|
||||
unhelpfulness at the moment it happens; it is the property that makes the front door
|
||||
safe to point every agent at.
|
||||
|
||||
**Every proxied fetch is auditable and attributable to the caller**, because it ran
|
||||
as them. `audit.jsonl` records metadata only — never values, guarded in code.
|
||||
|
||||
**The `--unsafe-stdout` escape hatch is a known liability.** It exists because
|
||||
humans in terminals legitimately need to see values. It is also exactly the shape of
|
||||
the 2026-07-16 disclosure, where a value reached a captured stdout. `ADR-0004`
|
||||
constrains it further for agent sessions.
|
||||
|
||||
## Related
|
||||
|
||||
- `wiki/OperatorAccessAssist.md#the-conduit-vs-broker-boundary-the-security-model`
|
||||
- `ADR-0004` — the agent-session read boundary built on top of this
|
||||
- `ADR-0003` — why proxied lanes are tracked as interim rather than owned
|
||||
95
docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md
Normal file
95
docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
---
|
||||
id: ops-warden-adr-0003
|
||||
type: adr
|
||||
title: "ADR-0003 — Cover gaps, but never silently own them"
|
||||
domain: infotech
|
||||
repo: ops-warden
|
||||
status: accepted
|
||||
version: "1.0"
|
||||
revision: "1"
|
||||
owner: ops-warden
|
||||
binds: "ops-warden"
|
||||
created: "2026-07-01"
|
||||
updated: "2026-08-18"
|
||||
last_reviewed: "2026-08-18"
|
||||
review_interval: 6m
|
||||
enforced_by: "registry/routing/catalog.yaml delegation:; warden route gaps"
|
||||
supersedes: ""
|
||||
successor: ""
|
||||
---
|
||||
|
||||
# ADR-0003 — Cover gaps, but never silently own them
|
||||
|
||||
## Status
|
||||
|
||||
Accepted. Stated as INTENT §9, made structural by WARDEN-WP-0030 (delegation
|
||||
register).
|
||||
|
||||
## Context
|
||||
|
||||
ops-warden owns exactly one lane: SSH certificate issuance. It nonetheless fronts
|
||||
around eleven credential lanes as a caller-identity proxy, because no other
|
||||
component fronts them yet and a worker blocked on a credential is a worker blocked.
|
||||
|
||||
Covering a gap is legitimate and this repo intends to keep doing it. The failure is
|
||||
subtler: **a cover that is never recorded as a cover becomes ownership by default.**
|
||||
Nobody decides to permanently own another component's lane. It happens because the
|
||||
interim arrangement worked, nobody wrote down that it was interim, and the intended
|
||||
owner never learned they were expected to build a front door.
|
||||
|
||||
By August 2026 the primitive to hand a lane back existed and was proven — `exec_owner`
|
||||
/ `exec_command`, used by secrets-engine for npm publish and by the railiance-platform
|
||||
credential broker for warden-sign — and was used by 2 of 24 lanes. The other
|
||||
twenty-two had no record of who *should* own them.
|
||||
|
||||
## Decision
|
||||
|
||||
**Every catalog entry carries a `delegation:` block**, with a `mode:` of:
|
||||
|
||||
| `mode` | Meaning |
|
||||
| --- | --- |
|
||||
| `permanent` | Ours forever. SSH certificate issuance, and nothing else |
|
||||
| `native` | The owner has a front door; we route to it and execute nothing |
|
||||
| `interim` | We are covering a gap. Requires `intended_owner:` and `blocked_on:` |
|
||||
|
||||
**`interim` without an `intended_owner` is not permitted.** If we cannot name who
|
||||
should own it, we have not understood the lane well enough to be fronting it.
|
||||
|
||||
**`blocked_on:` must name a specific condition, not a mood.** "No front door yet" is
|
||||
not a blocker; "secrets-engine has not confirmed whether `exec --catalog` generalizes
|
||||
over arbitrary OpenBao lanes (asked 2026-08-11, msg 7d55d332)" is. A blocker with a
|
||||
question and a date can be chased. A blocker without one is an excuse with a
|
||||
timestamp.
|
||||
|
||||
**The interim set is queryable**: `warden route gaps` lists it with review dates and
|
||||
staleness. A cover that nobody can enumerate is a cover nobody will retire.
|
||||
|
||||
**A blocker is a claim about the world at a date, and expires.** `reviewed:` is
|
||||
bumped only on a real re-check, never inherited. This was learned the hard way:
|
||||
`RISK-F-0001` invalidated one of our blockers within a day and nothing would have
|
||||
re-checked it.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Retiring a cover is a normal, expected event rather than a renegotiation.** When
|
||||
an owner ships their front door the lane flips `interim` → `native`. This has
|
||||
happened twice and both were routine.
|
||||
|
||||
**Other repos can see what we are holding for them.** The register is why key-cape
|
||||
and user-engine were able to confirm or decline lanes in August 2026 — the question
|
||||
was answerable because it had been written down. One of those answers was "not ours",
|
||||
which is a legitimate and useful outcome.
|
||||
|
||||
**We accept looking worse than we are.** `warden route gaps` publishes a list of
|
||||
things this repo is doing that it would rather not be doing. That is the intent: the
|
||||
alternative is a repo that looks clean because nobody counted.
|
||||
|
||||
**This register is not a risk register.** Interim lanes are tracked work with an
|
||||
owner and a date, not defects. They do not get bulk-filed into `risk-nexus`, which
|
||||
needs to stay small enough to read. Defects go there; gaps stay here.
|
||||
|
||||
## Related
|
||||
|
||||
- `INTENT.md` §9 — the principle this formalizes
|
||||
- `history/2026-08-11-delegation-surface-assessment.md` — the assessment that forced it
|
||||
- `.claude/rules/finding-routing.md` — the register-versus-findings boundary
|
||||
85
docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md
Normal file
85
docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
id: ops-warden-adr-0004
|
||||
type: adr
|
||||
title: "ADR-0004 — High-risk lanes refuse raw value streaming to agent sessions"
|
||||
domain: infotech
|
||||
repo: ops-warden
|
||||
status: accepted
|
||||
version: "1.0"
|
||||
revision: "1"
|
||||
owner: ops-warden
|
||||
binds: "ops-warden; any agent runtime calling warden access"
|
||||
created: "2026-07-20"
|
||||
updated: "2026-08-18"
|
||||
last_reviewed: "2026-08-18"
|
||||
review_interval: 6m
|
||||
enforced_by: "src/warden/access.py (exit 7); OpenBao policy agent-high-risk-boundary"
|
||||
supersedes: ""
|
||||
successor: ""
|
||||
---
|
||||
|
||||
# ADR-0004 — High-risk lanes refuse raw value streaming to agent sessions
|
||||
|
||||
## Status
|
||||
|
||||
Accepted. Decided during WARDEN-WP-0026 (credential disclosure hygiene), in
|
||||
response to a real disclosure on 2026-07-16.
|
||||
|
||||
## Context
|
||||
|
||||
On 2026-07-16 a secret value reached a captured stdout. The mechanism was ordinary:
|
||||
`bao kv get -field=X` in an agent session. Nothing was misconfigured and nobody
|
||||
misused a tool. The value was read correctly, by an authorized caller, using the
|
||||
documented command — and an agent session records its stdout, so the value landed in
|
||||
a transcript that outlives the shell.
|
||||
|
||||
This is a structural mismatch, not a mistake to train away. Agent sessions are
|
||||
logged by design; that is what makes them reviewable. A human at a terminal sees a
|
||||
value and it scrolls away. An agent "seeing" a value writes it into a durable
|
||||
context that may be stored, replayed, or sent to an inference provider.
|
||||
|
||||
Guidance alone will not fix it. The command is correct, it is in every runbook, and
|
||||
the next agent that needs the value will reach for it.
|
||||
|
||||
## Decision
|
||||
|
||||
**When `WARDEN_AGENT_ID` is set and the catalog lane is `risk: high`, ops-warden
|
||||
refuses to stream the raw value and exits 7.** The agent is not blocked from doing
|
||||
its work — `--out`, `--exec`, `--wrap` and `--fingerprint` all remain available.
|
||||
It is blocked from doing its work *in a way that writes the secret into a transcript*.
|
||||
|
||||
**The boundary is enforced at the credential store as well as at the CLI.** The
|
||||
OpenBao policy `agent-high-risk-boundary` denies data-read on those paths for agent
|
||||
tokens, allowing metadata and capabilities only. A control that lives solely in our
|
||||
own CLI is a control that ends the moment someone calls `bao` directly.
|
||||
|
||||
**Verification must not require a read.** To check a lane, use
|
||||
`bao token capabilities` — allow/deny — never a read of the value. This is the
|
||||
specific habit the disclosure taught us to break.
|
||||
|
||||
**Exposure is reportable without reading.** `warden taint <catalog-id>` reports KV v2
|
||||
`custom_metadata` (`exposed_at`, `exposed_version`) and touches no secret data.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Agents can still do everything they could before, by a different route.** `--exec`
|
||||
covers nearly every real case: the child process gets the value in its environment,
|
||||
the agent never sees it. The friction is deliberate and small.
|
||||
|
||||
**Exit 7 is a contract other runtimes depend on.** It is a distinguishable code, not
|
||||
a generic failure, so a caller can tell "refused by boundary" from "lane broken" and
|
||||
retry correctly. Changing it is a breaking change to every agent runtime.
|
||||
|
||||
**`risk: high` becomes a load-bearing catalog field** rather than documentation.
|
||||
Classifying a lane now changes runtime behaviour, so it must be set deliberately.
|
||||
|
||||
**We accept that `--unsafe-stdout` still exists for humans.** The boundary keys on
|
||||
`WARDEN_AGENT_ID`, so an agent that does not set it is not caught. That is a known
|
||||
limit: this ADR raises the floor for cooperating runtimes and hardens the store
|
||||
behind them; it does not claim to stop a determined caller.
|
||||
|
||||
## Related
|
||||
|
||||
- `wiki/playbooks/agent-read-boundary.md`
|
||||
- `wiki/playbooks/exposed-taint.md`
|
||||
- `ADR-0002` — the conduit rule this narrows for agent callers
|
||||
79
docs/adr/ADR-0005-implement-narrowly-route-broadly.md
Normal file
79
docs/adr/ADR-0005-implement-narrowly-route-broadly.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
---
|
||||
id: ops-warden-adr-0005
|
||||
type: adr
|
||||
title: "ADR-0005 — Implement one lane narrowly, route everything else"
|
||||
domain: infotech
|
||||
repo: ops-warden
|
||||
status: accepted
|
||||
version: "1.0"
|
||||
revision: "1"
|
||||
owner: ops-warden
|
||||
binds: "ops-warden"
|
||||
created: "2026-06-18"
|
||||
updated: "2026-08-18"
|
||||
last_reviewed: "2026-08-18"
|
||||
review_interval: 6m
|
||||
enforced_by: "SCOPE.md; registry/routing/catalog.yaml warden_executes"
|
||||
supersedes: ""
|
||||
successor: ""
|
||||
---
|
||||
|
||||
# ADR-0005 — Implement one lane narrowly, route everything else
|
||||
|
||||
## Status
|
||||
|
||||
Accepted. The founding charter decision, taken 2026-06-18
|
||||
(`history/2026-06-18-access-routing-intent-shift-assessment.md`).
|
||||
|
||||
## Context
|
||||
|
||||
ops-warden began as an SSH certificate manager. It then became the place workers
|
||||
asked when they did not know where a credential came from — which is a real need,
|
||||
and the obvious way to serve it is to start fetching credentials.
|
||||
|
||||
Down that path is a component that issues SSH certificates, vends API keys, brokers
|
||||
tokens, and holds authority over all of them: a single point whose compromise is
|
||||
total. NetKingdom's architecture deliberately separates identity (key-cape),
|
||||
authorization (flex-auth), and secrets (OpenBao). A helpful front door that absorbed
|
||||
all three would quietly undo that separation, one convenience at a time.
|
||||
|
||||
## Decision
|
||||
|
||||
**ops-warden executes exactly one lane with its own authority: SSH certificate
|
||||
issuance for `adm`/`agt`/`atm` actors.** `warden_executes: true` appears on one
|
||||
catalog entry and is expected to stay that way.
|
||||
|
||||
**For every other need it routes, and where the lane is `exec_capable` it may assist
|
||||
by proxying as the caller** under `ADR-0002`. Routing is not a lesser service — it is
|
||||
the service. Knowing which subsystem owns a need, and being right about it, is what
|
||||
this repo sells.
|
||||
|
||||
**Scope growth is tested by ownership, not by usefulness.** "Would this be handy in
|
||||
ops-warden?" is the wrong question and almost always answers yes. The right question
|
||||
is "does ops-warden have the authority to own this, permanently?" If the answer is no,
|
||||
the correct outcome is a pointer, or an `interim` cover recorded under `ADR-0003`.
|
||||
|
||||
## Consequences
|
||||
|
||||
**The blast radius stays bounded and known.** Compromising ops-warden yields the SSH
|
||||
signing lane. That is worth defending well precisely because it is the only thing here.
|
||||
|
||||
**We say no to requests that would be easy to say yes to.** `warden secret`,
|
||||
`warden login`, `warden bao`, `warden tunnel` do not exist and must not be invented;
|
||||
the agent instructions name them as anti-patterns because agents keep reaching for
|
||||
them. Each would be a day's work and a permanent widening.
|
||||
|
||||
**Being useful therefore depends on the pointers being right**, which is the whole
|
||||
weight behind `ADR-0001`'s anchor enforcement and the catalog's review dates. A router
|
||||
that routes wrongly is worse than no router.
|
||||
|
||||
**It leaves real gaps visible rather than filled.** Six workload lanes and three
|
||||
tenant lanes are covered interim because secrets-engine and tenant-engine have not
|
||||
shipped front doors. Under this ADR that is the correct state, tracked under
|
||||
`ADR-0003`, and not a signal that ops-warden should absorb them.
|
||||
|
||||
## Related
|
||||
|
||||
- `SCOPE.md` — the issue-vs-route table
|
||||
- `wiki/AccessRouting.md` — role and boundary
|
||||
- `ADR-0001`, `ADR-0002`, `ADR-0003` — the three rules that follow from this one
|
||||
103
docs/adr/ADR-0006-enforcement-is-zone-scoped.md
Normal file
103
docs/adr/ADR-0006-enforcement-is-zone-scoped.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
---
|
||||
id: ops-warden-adr-0006
|
||||
type: adr
|
||||
title: "ADR-0006 — Enforcement is zone-scoped, never a global flag"
|
||||
domain: infotech
|
||||
repo: ops-warden
|
||||
status: superseded
|
||||
version: "1.0"
|
||||
revision: "1"
|
||||
owner: ops-warden
|
||||
binds: "ops-warden"
|
||||
created: "2026-08-19"
|
||||
updated: "2026-08-22"
|
||||
last_reviewed: "2026-08-19"
|
||||
review_interval: 6m
|
||||
enforced_by: "warden.yaml policy.enabled; scripts/check_policy_caller_identity.py; zone-engine ZONE-WP-0001; WARDEN-WP-0032"
|
||||
supersedes: ""
|
||||
successor: "ops-warden-adr-0009"
|
||||
---
|
||||
|
||||
# ADR-0006 — Enforcement is zone-scoped, never a global flag
|
||||
|
||||
## Status
|
||||
|
||||
Accepted 2026-08-19, at the moment `policy.enabled: true` was ready to be set
|
||||
and deliberately was not.
|
||||
|
||||
## Context
|
||||
|
||||
WARDEN-WP-0031 finished the calling side of the flex-auth pre-sign gate. The
|
||||
flex-auth pin `flex-auth-ops-warden` runs `callerAuth.mode: enforce`; ops-warden
|
||||
presents a bound ServiceAccount token; the readiness gate exits 0 against the
|
||||
enforcing pin and an anonymous `/v1/check` is 401. Everything needed to set
|
||||
`policy.enabled: true` was in place.
|
||||
|
||||
`policy.enabled` is a **single boolean over the whole repo**. Combined with
|
||||
`fail_closed: true` it makes flex-auth a hard dependency of *every* `warden
|
||||
sign` — including the certificates the ops-bridge tunnels depend on, one of
|
||||
which is the tunnel carrying the policy call itself. A dead tunnel or a
|
||||
recreated Service does not degrade signing; it stops it.
|
||||
|
||||
That cost might be acceptable for a settled production lane. It is not
|
||||
acceptable uniformly, because ops-warden signs across an estate that is being
|
||||
actively rebuilt. During deep refactors — CoulombCore's decommission, the
|
||||
issue-core move, cluster rebuilds — the same flag would harden exactly the
|
||||
access needed to *perform* the refactor. Security that stops the work stops
|
||||
being security and starts being an outage with good intentions.
|
||||
|
||||
The repo already refuses to treat posture as one-dimensional. WP-0015 shipped
|
||||
environment posture (`dev` / `test` / `prod`) and workload maturity (`M0`–`M3`);
|
||||
WP-0029 added `organization_posture: build` as a third axis precisely because
|
||||
the *organization's* state changes what is reasonable to demand. A global
|
||||
`policy.enabled` contradicts all of that: it is a fourth control that ignores
|
||||
the three axes already declared.
|
||||
|
||||
## Decision
|
||||
|
||||
**Enforcement posture is a property of a zone, not of the repo.** ops-warden
|
||||
does not enable a fail-closed authorization gate globally. Before
|
||||
`policy.enabled: true` is set anywhere, the zones must exist: named bands of
|
||||
differing rigidity, each declaring what is enforced, what is advisory, and what
|
||||
is exempt — and the gate must be scoped to them.
|
||||
|
||||
Concretely, until `zone-engine`'s `ZONE-WP-0001` defines the zone model:
|
||||
|
||||
- `policy.enabled` stays `false`. Its readiness is evidence, not a mandate.
|
||||
- A gate that is *ready* is recorded as ready. Readiness is not a reason to
|
||||
enable; deferral with a stated reason is a legitimate terminal state for a
|
||||
task, not an unfinished one.
|
||||
- Any future enforcement control ships zone-aware or does not ship. A second
|
||||
global boolean is the defect this record exists to prevent.
|
||||
|
||||
## Consequences
|
||||
|
||||
**We accept** that the pre-sign gate remains unexercised in production longer,
|
||||
and that the WP-0031 evidence ages. Re-running
|
||||
`scripts/check_policy_caller_identity.py` re-establishes it cheaply, and the
|
||||
readiness gate exists precisely so this is a re-check rather than a re-do.
|
||||
|
||||
**We accept** that flex-auth's `flex-auth-ops-warden` pin sits enforcing with no
|
||||
enforcing consumer. That is not waste: it makes the anonymous path 401 rather
|
||||
than a decision, which was the ADHOC-2026-08-17-T01 condition regardless of
|
||||
whether ops-warden calls it.
|
||||
|
||||
**We reject** the framing that a ready control should be turned on because it is
|
||||
ready. The question is not "does it work" but "which zone is this, and does this
|
||||
zone want this failure mode."
|
||||
|
||||
**This binds future work.** A zone-blind enforcement flag proposed in any
|
||||
ops-warden workplan is out of order under this ADR, and should be sent back to
|
||||
the zone model rather than merged with a caveat in its description.
|
||||
|
||||
## Related
|
||||
|
||||
- `WARDEN-WP-0031` — the calling side that made the flip possible (T05 deferred
|
||||
under this ADR)
|
||||
- `zone-engine` `ZONE-WP-0001` — the zone model this record defers to, seeded
|
||||
2026-08-19 as the owning repo
|
||||
- `WARDEN-WP-0032` — ops-warden's consumer-side adoption
|
||||
- `wiki/WorkloadSecurityPosture.md` — the two axes already shipped (WP-0015)
|
||||
- `wiki/PolicyGatedSigning.md` — the gate itself
|
||||
- `history/2026-08-19-flex-auth-caller-identity-evidence.md` — readiness evidence
|
||||
- flex-auth `FLEX-WP-0016` — the enforcing pin
|
||||
100
docs/adr/ADR-0007-build-stage-stops-at-credential-disclosure.md
Normal file
100
docs/adr/ADR-0007-build-stage-stops-at-credential-disclosure.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
---
|
||||
id: ops-warden-adr-0007
|
||||
type: adr
|
||||
title: "ADR-0007 — Build-stage permissiveness stops at credential disclosure"
|
||||
domain: infotech
|
||||
repo: ops-warden
|
||||
status: accepted
|
||||
version: "1.0"
|
||||
revision: "1"
|
||||
owner: ops-warden
|
||||
binds: "ops-warden"
|
||||
created: "2026-08-19"
|
||||
updated: "2026-08-19"
|
||||
last_reviewed: "2026-08-19"
|
||||
review_interval: 6m
|
||||
enforced_by: "registry/routing/catalog.yaml risk grades; src/warden/cli.py agent read-boundary; WARDEN-WP-0032-T06"
|
||||
supersedes: ""
|
||||
successor: ""
|
||||
---
|
||||
|
||||
# ADR-0007 — Build-stage permissiveness stops at credential disclosure
|
||||
|
||||
## Status
|
||||
|
||||
Accepted 2026-08-19, alongside grading the last 14 ungraded catalog lanes.
|
||||
|
||||
## Context
|
||||
|
||||
`ADR-0006` deferred a global fail-closed authorization gate because uniform
|
||||
enforcement across an estate under deep refactor hardens the access needed to
|
||||
perform the refactor. The organization's declared posture is `build`
|
||||
(WP-0029), and the operator has confirmed the estate need not be tight yet.
|
||||
|
||||
That is correct, and it is also the kind of principle that quietly generalises
|
||||
past its warrant. Read loosely, "we are in build stage" argues for relaxing
|
||||
every control, including the ones that stop a credential landing in a logged
|
||||
agent transcript. Those are not the same class of control, and the difference
|
||||
is not severity — it is **cost**.
|
||||
|
||||
`RISK-F-0003` made the distinction concrete. `ADR-0004` reads as a categorical
|
||||
rule: high-risk lanes refuse raw value streaming to agent sessions. The
|
||||
implementation was `risk == "high"` against an **optional** field, so 14 of 27
|
||||
lanes never reached the control at all — five of them `exec_capable`. The
|
||||
control had not been relaxed by anyone's decision. It had simply never been
|
||||
reached, which is worse, because nothing announced it.
|
||||
|
||||
## Decision
|
||||
|
||||
**Build-stage permissiveness applies to controls that gate work. It does not
|
||||
apply to controls that prevent credential disclosure.**
|
||||
|
||||
The test is friction, not severity:
|
||||
|
||||
- A control that can **block a legitimate operation** — a fail-closed
|
||||
authorization gate, an enforcement stance — is a candidate for relaxation
|
||||
while the organization is in `build`, and `ADR-0006` scopes that relaxation
|
||||
to zones.
|
||||
- A control that **redirects how a value moves without preventing the work** —
|
||||
the agent read-boundary, which refuses raw stdout but leaves `--out`,
|
||||
`--exec`, `--wrap` and `--fingerprint` fully available — is not relaxed by
|
||||
build posture, because relaxing it buys nothing. Nobody is unblocked by it.
|
||||
|
||||
The asymmetry that settles it: a blocked operation is recovered by retrying.
|
||||
A credential written into a logged transcript is not recovered by rotation —
|
||||
rotation limits the damage, it does not unwrite the log. The 2026-07-16
|
||||
disclosure is the case in point.
|
||||
|
||||
**Therefore, regardless of `organization_posture`:**
|
||||
|
||||
1. Every catalog lane carries an explicit `risk` grade. **Absence is not a
|
||||
grade**, and a lane that omits it is a defect, not a default.
|
||||
2. Grading is done on merit, per lane. This decision is not licence to grade
|
||||
everything `high` — an over-broad grade is its own inaccuracy, and
|
||||
`tenancy-posture` §6's *accuracy, not altitude* applies to this field too.
|
||||
3. Minimum credential-handling standards — the read-boundary, the safe fetch
|
||||
transports, the no-secret audit guard — hold in every posture.
|
||||
|
||||
## Consequences
|
||||
|
||||
**We accept** the grading cost, now and on every new lane. That is the point:
|
||||
`WARDEN-WP-0032-T06` makes an ungraded lane impossible rather than merely
|
||||
discouraged, because a rule enforced by remembering is not enforced.
|
||||
|
||||
**We reject** "build stage" as a general argument in credential-handling
|
||||
discussions. It is a real and useful argument about *gating*, and citing it
|
||||
against a disclosure control is a category error this record exists to name.
|
||||
|
||||
**We note what this decision is not.** It does not set severity for
|
||||
`RISK-F-0003` — that is `risk-nexus`'s. It does not make ops-warden the judge of
|
||||
other repos' controls. And it does not survive contact with a zone model that
|
||||
says otherwise: when `zone-engine` defines admission standards, a zone may
|
||||
legitimately require *more* than this floor. It may not require less.
|
||||
|
||||
## Related
|
||||
|
||||
- `ADR-0004` — high-risk lanes refuse raw value streaming to agent sessions
|
||||
- `ADR-0006` — enforcement is zone-scoped, never a global flag
|
||||
- `RISK-F-0003` — the read-boundary blind spot that prompted this
|
||||
- `WARDEN-WP-0032-T05` / `T06` — the grading, and making absence impossible
|
||||
- `zone-engine` `ZONE-WP-0001` — where admission standards will be defined
|
||||
94
docs/adr/ADR-0008-grade-the-path-not-the-field.md
Normal file
94
docs/adr/ADR-0008-grade-the-path-not-the-field.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
---
|
||||
id: ops-warden-adr-0008
|
||||
type: adr
|
||||
title: "ADR-0008 — A lane's risk grade covers every field its path discloses"
|
||||
domain: infotech
|
||||
repo: ops-warden
|
||||
status: accepted
|
||||
version: "1.0"
|
||||
revision: "1"
|
||||
owner: ops-warden
|
||||
binds: "ops-warden"
|
||||
created: "2026-08-21"
|
||||
updated: "2026-08-21"
|
||||
last_reviewed: "2026-08-21"
|
||||
review_interval: 6m
|
||||
enforced_by: "registry/routing/catalog.yaml fields + risk; tests/test_routing.py::test_high_risk_lanes_classified"
|
||||
supersedes: ""
|
||||
successor: ""
|
||||
---
|
||||
|
||||
# ADR-0008 — A lane's risk grade covers every field its path discloses
|
||||
|
||||
## Status
|
||||
|
||||
Accepted 2026-08-21, after `secrets-engine` found two under-graded lanes while
|
||||
reviewing ops-warden's own catalog metadata.
|
||||
|
||||
## Context
|
||||
|
||||
`ADR-0007` requires every catalog lane to carry an explicit `risk` grade. It does
|
||||
not say what the grade is *of*, and the omission turned out to matter.
|
||||
|
||||
The catalog describes a lane by a single `fetch_command` naming a single field —
|
||||
`bao kv get -field=ISSUE_CORE_API_KEY <path>`. Grading followed that description.
|
||||
But the unit of disclosure is not the field, it is the **path**: `bao kv get`
|
||||
without `-field` returns every key stored there, and an agent session that
|
||||
discloses one field has disclosed all of them.
|
||||
|
||||
On 2026-08-19, grading all 27 lanes, ops-warden graded
|
||||
`issue-core-ingestion-api-key` and `reuse-surface-hub-write-token` as `standard`
|
||||
— "ordinary internal workload secrets". Both grades read only the headline field.
|
||||
`CCR-2026-0002` records a deliberate decision to keep `GITEA_BACKEND_TOKEN` at the
|
||||
first path; `CCR-2026-0005` declares a dual-consumer webhook HMAC at the second.
|
||||
Neither is recovered by rotating the credential the lane is named after.
|
||||
|
||||
Three details make this worth a record rather than a fix:
|
||||
|
||||
- **The evidence was already ours.** The field sets were in the CCRs the catalog
|
||||
already cites as authoritative. This was not missing data; it was unread data.
|
||||
- **A test held the error still.** `test_high_risk_lanes_classified` asserted
|
||||
`issue-core-ingestion-api-key` was *not* high. A first grading pass had marked
|
||||
it high, the test contradicted it, and the test was believed. A test that
|
||||
encodes a judgement defends that judgement from correction.
|
||||
- **Another repo found it.** `secrets-engine` graded both `high` independently
|
||||
while drafting catalog entries whose schema records `fields`. A schema that
|
||||
names the field set makes the right grade obvious; ours did not have one.
|
||||
|
||||
## Decision
|
||||
|
||||
**A lane's `risk` grade is a property of its path, and must cover the union of
|
||||
everything a read of that path would disclose.**
|
||||
|
||||
1. Where the field set is known, the catalog records it as `fields`, with the
|
||||
authority it came from.
|
||||
2. The grade is argued against the most damaging field, not the named one.
|
||||
3. Where the field set is unknown, that is stated — never assumed to be one
|
||||
field. An unverified field set is a reason to grade conservatively, matching
|
||||
the `inter-hub-bootstrap-ssh` precedent under `ADR-0007`.
|
||||
4. Establishing a field set must not be done by reading the secret. Use the
|
||||
owning CCR, the owner's catalog, or `bao kv metadata`. `bao kv get` on a
|
||||
high-risk path is the 2026-07-16 vector and is forbidden by
|
||||
`ADR-0004` for agent sessions regardless of intent.
|
||||
|
||||
## Consequences
|
||||
|
||||
`ADR-0007` is unchanged and still governs: every lane carries an explicit grade,
|
||||
and absence fails safe. This record says what that grade must account for.
|
||||
|
||||
Grading gets more expensive: it now requires knowing what is at a path, not just
|
||||
what the lane is called. That cost is the point — the cheap version produced two
|
||||
wrong answers in one pass and is the reason this exists.
|
||||
|
||||
A test that asserts a grade is asserting a judgement. When a grade is disputed,
|
||||
re-argue it from evidence before trusting the test that encodes it.
|
||||
|
||||
## Related
|
||||
|
||||
- `ADR-0007` — every lane carries an explicit grade; build-stage permissiveness
|
||||
stops at credential disclosure
|
||||
- `ADR-0004` — high-risk lanes refuse raw value streaming to agent sessions
|
||||
- `ADR-0001` — the catalog is a pointer layer; `fields` records the owner's
|
||||
declared field set with its source, and does not restate their procedure
|
||||
- `WARDEN-WP-0033-T02`; `secrets-engine` `SECRETS-WP-0006`
|
||||
- `history/2026-07-16-credential-disclosure-lessons.md`
|
||||
101
docs/adr/ADR-0009-adopt-security-zones-as-a-consumer.md
Normal file
101
docs/adr/ADR-0009-adopt-security-zones-as-a-consumer.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
---
|
||||
id: ops-warden-adr-0009
|
||||
type: adr
|
||||
title: "ADR-0009 — Adopt security-zones v0.1 as a consumer"
|
||||
domain: infotech
|
||||
repo: ops-warden
|
||||
status: accepted
|
||||
version: "1.0"
|
||||
revision: "1"
|
||||
owner: ops-warden
|
||||
binds: "ops-warden"
|
||||
created: "2026-08-22"
|
||||
updated: "2026-08-22"
|
||||
last_reviewed: "2026-08-22"
|
||||
review_interval: 3m
|
||||
enforced_by: "tenancy.yaml; registry/routing/catalog.yaml workload_ref; scripts/build_flex_auth_registry.py; src/warden/policy.py; src/warden/config.py"
|
||||
supersedes: "ops-warden-adr-0006"
|
||||
successor: ""
|
||||
---
|
||||
|
||||
# ADR-0009 — Adopt security-zones v0.1 as a consumer
|
||||
|
||||
## Status
|
||||
|
||||
Accepted 2026-08-22 after zone-engine completed `ZONE-WP-0001-T03/T05` and
|
||||
published the declaration, compilation, stance, and failure-mode contract in
|
||||
canon revision `337484a`; zone-engine's reference compiler is revision
|
||||
`9b6ada7`.
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0006 rejected a repo-wide `policy.enabled` switch because one boolean plus
|
||||
one `fail_closed` value made flex-auth a uniform dependency of every signing
|
||||
path, including continuity paths needed to repair that dependency. It deferred
|
||||
the replacement to zone-engine rather than designing an estate model here.
|
||||
|
||||
The owning model now exists. A zone is an evidenced workload-admission fact;
|
||||
control stance remains with the control owner, and dependency failure behavior
|
||||
remains with the PEP. Membership resolves only through an authoritative
|
||||
workload identity. Missing identity, membership, admission evidence, or a
|
||||
required floor is `unknown`, never an inferred permissive zone.
|
||||
|
||||
## Decision
|
||||
|
||||
Ops-warden adopts `security-zones_v0.1` and accepts its initial build-stage rows
|
||||
for the controls ops-warden owns:
|
||||
|
||||
- the pre-sign PEP fails open for `z0-experimental`, `z1-operational`,
|
||||
`z2-protected`, `z2-continuity`, and build-profile `unknown`; it fails closed
|
||||
for `z3-critical`;
|
||||
- the agent high-risk read boundary remains enforced and fail-closed in every
|
||||
zone and for `unknown`;
|
||||
- `warden plan` never derives `autonomous` authority from unknown zone evidence.
|
||||
|
||||
The implementation follows four rules:
|
||||
|
||||
1. `policy.enabled` and the global `policy.fail_closed` setting are retired and
|
||||
rejected by configuration loading. The PEP chooses failure behavior from a
|
||||
total per-zone map.
|
||||
2. The existing compiled flex-auth registry is the resource-membership carrier.
|
||||
Actor resources receive `workload_id`, `security_zone`,
|
||||
`security_zone_admission`, and `security_zone_revision`. The dormant
|
||||
`trust_zone: platform` constant is removed; it is not repurposed.
|
||||
3. Workload joins are explicit. Managed deployables use Repo Manager's exact
|
||||
`(rapp_id, workload_identity.name, deployable?)` tuple. Independent
|
||||
operational workloads use their owner-reviewed `tenancy.yaml`. Catalog
|
||||
owners distinguish `not-applicable` from applicable-but-`unknown`; no path or
|
||||
repository-name inference is allowed.
|
||||
4. A fail-open signing result is metadata, not silence. Signature and unified
|
||||
audit records carry the selected zone, failure mode, outcome, and decision id
|
||||
when one exists.
|
||||
|
||||
Ops-warden itself declares `z1-operational`. That is an accuracy decision: the
|
||||
workload has M1 evidence and does not yet have the SLO history, on-call rotation,
|
||||
or exercised recovery evidence needed for z2 admission.
|
||||
|
||||
## Consequences
|
||||
|
||||
The global flip and its failure cycle no longer exist. An unknown target remains
|
||||
observable and follows the versioned build profile without manufacturing
|
||||
membership. A future organization-posture graduation changes the versioned
|
||||
control profile, not each workload declaration.
|
||||
|
||||
The flex-auth policy package still owns pre-sign stance. Ops-warden can compile
|
||||
and send the membership attributes, handle `allow`/`audit_only`/deny, and apply
|
||||
the correct PEP failure mode; it does not write flex-auth's Rego rows.
|
||||
|
||||
Catalog coverage is intentionally honest at adoption: exact references resolve
|
||||
where authoritative declarations exist, applicable lanes without one report
|
||||
`unknown` with a reason, and generic actions/patterns are explicitly
|
||||
`not-applicable`. Resolution coverage improves by adding owner declarations,
|
||||
never by adding heuristics here.
|
||||
|
||||
## Related
|
||||
|
||||
- `security-zones_v0.1` (net-kingdom canon revision `337484a`; zone-engine
|
||||
compiler revision `9b6ada7`)
|
||||
- Repo Manager `helixforge.workloads.ops-warden-reference.v1` revision `890f3b0`
|
||||
- NetKingdom tenancy-posture Decisions 5.6.1/5.6.2
|
||||
- `WARDEN-WP-0032`
|
||||
- `ADR-0004`, `ADR-0007`, and `ADR-0008`
|
||||
109
docs/adr/ADR-0010-ops-warden-is-staff.md
Normal file
109
docs/adr/ADR-0010-ops-warden-is-staff.md
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
---
|
||||
id: ops-warden-adr-0010
|
||||
type: adr
|
||||
title: "ADR-0010 — ops-warden is Staff: lanes, not rules, and one declared engine gap"
|
||||
domain: infotech
|
||||
repo: ops-warden
|
||||
status: accepted
|
||||
version: "1.0"
|
||||
revision: "1"
|
||||
owner: ops-warden
|
||||
binds: "ops-warden"
|
||||
created: "2026-08-28"
|
||||
updated: "2026-08-28"
|
||||
last_reviewed: "2026-08-28"
|
||||
review_interval: 3m
|
||||
enforced_by: "INTENT.md layer declaration; docs/adr/ADR-0002; docs/adr/ADR-0003; docs/adr/ADR-0005; registry/routing/catalog.yaml delegation fields"
|
||||
supersedes: ""
|
||||
successor: ""
|
||||
---
|
||||
|
||||
# ADR-0010 — ops-warden is Staff: lanes, not rules, and one declared engine gap
|
||||
|
||||
## Status
|
||||
|
||||
Accepted 2026-08-28, answering intake `WARDEN-IN-0001` from gate-house, which
|
||||
carries decision `GH-DEC-2026-001`. The standard being adopted —
|
||||
`net-kingdom/canon/standards/security-layer-model_v0.1.md` — is `proposed`, and was
|
||||
proposed pending assent from flex-auth, kings-guard, and ops-warden. This ADR is
|
||||
ops-warden's half of that assent.
|
||||
|
||||
## Context
|
||||
|
||||
The estate acquired overlapping claims to the same responsibility, most visibly two
|
||||
repositories describing themselves as the authorization control plane. The layer
|
||||
model resolves the overlap by layering repositories on determinism — Taxonomy,
|
||||
Tooling, Engines, Staff — and by two rules: Staff never touches Tooling directly
|
||||
(§5), and `access-engine` is the only policy decision point (§6).
|
||||
|
||||
ops-warden is assigned Staff. Two demarcations follow that touch this repository:
|
||||
the security curriculum it had been carrying belongs to gate-house, and the words
|
||||
*access lane* and *access rule* are bound to different owners.
|
||||
|
||||
Full reasoning: `history/2026-08-28-security-layer-model-assent.md`.
|
||||
|
||||
## Decision
|
||||
|
||||
**1. ops-warden is Staff and declares it.** `INTENT.md` carries the layer label and
|
||||
the §5 invariant. ops-warden holds no state another layer depends on at runtime and
|
||||
renders no authorization decision — it consumes them.
|
||||
|
||||
**2. Lanes, not rules.** ops-warden owns *how* a worker reaches a host: SSH
|
||||
certificate issuance, the routing catalog, `warden access`, `warden plan`,
|
||||
`cert_command`. It never owns *whether* a worker may — that is `access-engine`
|
||||
(today `flex-auth`), and ops-warden neither renders nor caches that decision. This
|
||||
restates what `ADR-0002` and `ADR-0005` already bind; it is recorded here because
|
||||
the demarcation is now normative estate-wide and other repositories rely on
|
||||
ops-warden holding to it. The ruled rename `flex-auth` → `access-engine` is assented
|
||||
to; ops-warden asks only for a window in which both names resolve.
|
||||
|
||||
**3. Doctrine goes to gate-house; runbooks stay here.** ops-warden does not restate
|
||||
security doctrine, the authority model, or the curriculum. It references
|
||||
gate-house's. It keeps everything operational about the lanes it stewards: which
|
||||
subsystem owns which need, how to obtain a credential lane by lane, and conformance
|
||||
evidence for its own lanes. `.claude/rules/credential-routing.md` is runbook, not
|
||||
curriculum, and stays inlined in this and every other repository.
|
||||
|
||||
**4. One declared engine gap, not an exemption.** `src/warden/vault.py` (`VaultCA`)
|
||||
is a direct OpenBao client performing a write from a Staff repository. It is a §5
|
||||
non-conformance. ops-warden declares it rather than arguing it away:
|
||||
|
||||
- **intended owner:** `secrets-engine` (credential abstraction, custody, lifecycle)
|
||||
- **blocked on:** no engine exposes an SSH certificate signing surface
|
||||
- **review:** with this ADR, every 3 months
|
||||
|
||||
Until that surface exists, ops-warden continues to sign — refusing to would remove
|
||||
production host access to close a documentation gap — and reports the position as
|
||||
open. `warden desk`'s `bao kv put` is declared on the same terms. `taint.py` is
|
||||
metadata-only observation, declared under §5's read-only allowance. `proxy.py`
|
||||
supplies no authority of its own: it runs the owner's tool under the caller's
|
||||
identity and is governed by `ADR-0002`.
|
||||
|
||||
This is `ADR-0003` turned inward. ops-warden has required an intended owner and a
|
||||
blocker on 27 catalog lanes it holds for other repositories; it holds itself to the
|
||||
same record.
|
||||
|
||||
## Consequences
|
||||
|
||||
ops-warden's conformance under §10 is *declared non-conformant with a tracked
|
||||
closure path*, not clean. That is the accurate state and it is the state that gets
|
||||
fixed, because it names an owner who can fix it.
|
||||
|
||||
An amendment to §5 has been offered to gate-house — a second sanctioned shape
|
||||
alongside read-only diagnostics: a declared engine gap carrying intended owner,
|
||||
blocker, and review date, machine-readable so §10 can tell a tracked gap from an
|
||||
undeclared violation. It is offered, not assumed; §5 stays gate-house's to write. If
|
||||
gate-house declines it, ops-warden's position is a plain non-conformance and is
|
||||
reported as one.
|
||||
|
||||
The `NetKingdom Security Literacy` section stops being a prose second source for
|
||||
`registry/routing/catalog.yaml`, which `ADR-0001` had already ruled against for
|
||||
catalog procedure.
|
||||
|
||||
## Related
|
||||
|
||||
- `net-kingdom/canon/standards/security-layer-model_v0.1.md` (proposed, gate-house)
|
||||
- `gate-house/decisions/decisions.md` — `GH-DEC-2026-001`
|
||||
- `history/2026-08-28-security-layer-model-assent.md`
|
||||
- `ADR-0001`, `ADR-0002`, `ADR-0003`, `ADR-0005`, `ADR-0009`
|
||||
- `WARDEN-IN-0001`
|
||||
74
docs/adr/README.md
Normal file
74
docs/adr/README.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# ops-warden architecture decision records
|
||||
|
||||
This directory holds the rules **ops-warden owns** — the decisions this repo made,
|
||||
is bound by, and is responsible for changing.
|
||||
|
||||
## Why these exist as ADRs rather than wiki prose
|
||||
|
||||
Until 2026-08-18 every rule in this list lived in wiki prose, a workplan, or a
|
||||
comment at the top of `registry/routing/catalog.yaml`. All of them were being
|
||||
followed. None of them was *addressable*: a reader outside ops-warden could not
|
||||
cite one, could not tell whether it was current, and could not tell whether it
|
||||
was ours to change or someone else's that we merely obey.
|
||||
|
||||
That distinction is the point of this directory. It matters in both directions:
|
||||
|
||||
- **A rule we own, mistaken for inherited canon, never gets fixed.** We wait for
|
||||
an owner who does not exist.
|
||||
- **Inherited canon, mistaken for ours, gets quietly bent.** We change something
|
||||
we had no authority over, and the drift is invisible until it breaks a repo
|
||||
that trusted the canonical version.
|
||||
|
||||
## Owned versus inherited
|
||||
|
||||
Every ADR here carries `owner:` in its frontmatter. It is the load-bearing field.
|
||||
|
||||
| `owner:` | Meaning | How it changes |
|
||||
| --- | --- | --- |
|
||||
| `ops-warden` | **Ours.** We decided it, we are bound by it, and we may change it | A new ADR that supersedes this one. Never an edit-in-place that rewrites a decision |
|
||||
| anything else | **Inherited.** We follow it; we do not own it | Through that owner's process. We may dispute it — we may not amend it |
|
||||
|
||||
Everything currently in this directory is `owner: ops-warden`. Rules we follow but
|
||||
do not own — NetKingdom canon, the IAM profile, the credential-management standard
|
||||
— are *not* copied here. They are cited. Copying inherited canon into our own ADR
|
||||
directory would recreate exactly the second-source-of-truth failure that
|
||||
`ADR-0001` exists to prevent.
|
||||
|
||||
## Superseding one of these
|
||||
|
||||
A decision here changed the behaviour of other repos, so retracting it silently is
|
||||
not available. Write a new ADR, set the old one's `status: superseded` and
|
||||
`successor:`, and leave it in place. Superseded is a lifecycle state; deletion is
|
||||
not. `policy-nexus` publishes the history, and a reader asking "what did this say
|
||||
when we made that decision" must be able to find out.
|
||||
|
||||
## Relationship to `.claude/rules/`
|
||||
|
||||
`.claude/rules/*.md` are **agent-facing operational instructions**. They tell an
|
||||
agent what to do in a session. They are derived from these ADRs and should cite
|
||||
them rather than restate the reasoning. If the two disagree, the ADR is right and
|
||||
the rule file is a defect.
|
||||
|
||||
## Publication
|
||||
|
||||
These are publishable through `policy-nexus` at `policy.coulomb.social`, which
|
||||
requires `title`, `status` and `owner` on every document and renders Owner as a
|
||||
column in its index. The ownership knowledge therefore survives publication
|
||||
rather than being a local convention that evaporates at the repo boundary.
|
||||
|
||||
`policy-nexus` publishes; it never writes back. The file in this directory is the
|
||||
source of truth. If the site and this directory disagree, this directory is right
|
||||
and the publication is a defect.
|
||||
|
||||
| ADR | Rule | Binds |
|
||||
| --- | --- | --- |
|
||||
| `ADR-0001` | The routing catalog is a pointer layer, never a second copy of an owner's procedure | ops-warden, and every repo contributing a catalog entry |
|
||||
| `ADR-0002` | ops-warden is a transparent conduit, never a secret broker | ops-warden |
|
||||
| `ADR-0003` | Cover gaps, but never silently own them | ops-warden |
|
||||
| `ADR-0004` | High-risk lanes refuse raw value streaming to agent sessions | ops-warden, and any agent runtime calling `warden access` |
|
||||
| `ADR-0005` | Implement one lane narrowly, route everything else | ops-warden |
|
||||
| `ADR-0006` | Enforcement is zone-scoped, never a global flag (**superseded by ADR-0009**) | ops-warden |
|
||||
| `ADR-0007` | Build-stage permissiveness stops at credential disclosure | ops-warden |
|
||||
| `ADR-0008` | A lane's risk grade covers every field its path discloses | ops-warden |
|
||||
| `ADR-0009` | Adopt security-zones v0.1; compile explicit membership and select PEP failure mode per zone | ops-warden |
|
||||
| `ADR-0010` | ops-warden is Staff: it owns access lanes, never access rules; the direct OpenBao client is a declared engine gap | ops-warden, and gate-house as the standard's owner |
|
||||
122
docs/credential-governance-break-glass.md
Normal file
122
docs/credential-governance-break-glass.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
# Credential governance break-glass contract
|
||||
|
||||
Status: active design contract for WARDEN-WP-0027-T02. This document does not
|
||||
authorize a seal, re-key, token mint, policy write, workload restart, or host
|
||||
reboot.
|
||||
|
||||
## Decision boundary
|
||||
|
||||
ops-warden consumes and verifies credential-control evidence; it does not own
|
||||
the OpenBao cluster, unseal shares, recovery snapshots, policies, or root token.
|
||||
`railiance-platform` is accountable for the OpenBao operation. The share
|
||||
custodians, platform driver, abort operator, provider-console operator, and
|
||||
affected workload owners participate through an attended approval window.
|
||||
|
||||
The current production trust-root is the rotated Shamir barrier restored to
|
||||
railiance01 in RMASTER-WP-0020: three separately custodied shares with a
|
||||
threshold of two. Share-holder identities and share material remain in the
|
||||
approved out-of-band custody system, never Git, State Hub, shell history, logs,
|
||||
or chat. A readiness receipt names participating *roles* and attests that two
|
||||
distinct custodians are present; it never contains a share or recovery value.
|
||||
|
||||
Root is offline bootstrap/break-glass material only. It is not a substitute for
|
||||
`platform-admin` OIDC and must not be used through the browser UI. An ordinary
|
||||
unseal uses threshold shares and does not require a root token.
|
||||
|
||||
## Graded response
|
||||
|
||||
| Grade | Trigger | Action owner | Exit evidence |
|
||||
| --- | --- | --- | --- |
|
||||
| Observe | Suspicion without confirmed credential disclosure | lane owner + risk-nexus | Metadata-only taint, audit, policy, and capability review |
|
||||
| Soft lockdown | Coding-agent access must stop while human recovery remains available | railiance-platform | `agent-high-risk-boundary` source/live equality; all concrete high-risk data paths denied; metadata readable |
|
||||
| Lane containment | One or more concrete credentials are exposed | each credential owner | Front door disabled, provider/OpenBao rotation through the owner-native procedure, consumer cutover, taint cleared only after verification |
|
||||
| Hard lockdown | OpenBao trust or control-plane integrity is in doubt | platform driver under attended approval | Fresh encrypted snapshot receipt, intentional seal, sealed-state proof, 2-of-3 unseal, post-unseal and consumer verification |
|
||||
| Re-key | A Shamir share or the barrier custody model is compromised | platform owner + threshold custodians | Separately approved OpenBao re-key ceremony, new threshold custody attestation, old-share retirement, recovery drill |
|
||||
|
||||
Hard lockdown is not an agent command. The irreversible hold point is the
|
||||
platform owner's attended seal action. All prerequisites below must be true
|
||||
before that hold point; repository access alone grants no authority to cross it.
|
||||
|
||||
Route planning must select `openbao-shamir-recovery-ceremony` and return one
|
||||
`founder_required` approval act. A result that selects `openbao-api-key`, asks
|
||||
for paste-once provisioning, or offers any raw-value transport is a routing
|
||||
failure and must not be executed.
|
||||
|
||||
## Pre-seal hold point
|
||||
|
||||
- An approved, bounded window names the accountable platform driver and a
|
||||
distinct abort operator.
|
||||
- Two distinct Shamir custodians attest availability through the approved
|
||||
out-of-band channel.
|
||||
- Independent provider-console access is verified by its owner.
|
||||
- A current Raft snapshot is encrypted and stored off-host; the non-secret
|
||||
receipt binds cluster id, Raft index, plaintext/encrypted hashes, location,
|
||||
age, and verification without including protected material.
|
||||
- Current OpenBao health, seal state, Raft peers, audit device, auth methods,
|
||||
policy fingerprints, and consumer readiness are captured as metadata.
|
||||
- A rollback/re-entry order and stop conditions are acknowledged by affected
|
||||
workload owners.
|
||||
|
||||
Any missing or stale item aborts before sealing. A live incident may require
|
||||
immediate network isolation, but that does not authorize improvising share or
|
||||
root-token handling.
|
||||
|
||||
## Re-entry sequence
|
||||
|
||||
The platform owner performs the exact commands from
|
||||
`railiance-platform/docs/openbao.md`; ops-warden does not copy a second
|
||||
execution procedure here.
|
||||
|
||||
1. Confirm the instance is intentionally sealed and the approved window is
|
||||
still open.
|
||||
2. Two custodians supply shares through hidden, non-logged prompts. No agent
|
||||
observes the values.
|
||||
3. Prove `initialized=true` and `sealed=false`, then run the owner post-unseal
|
||||
verification.
|
||||
4. Verify Raft, persistent audit output, OIDC metadata, SSH roles, and policy
|
||||
fingerprints before restoring normal access.
|
||||
5. Reconcile ExternalSecret stores and bounded consumers in dependency order.
|
||||
6. Run value-safe capability probes, including the coding-agent deny-wins check
|
||||
and an ops-warden signing smoke that records only backend and decision ids.
|
||||
7. Revoke temporary operator tokens and close the window with non-secret
|
||||
timing, status, and abort/rollback evidence.
|
||||
|
||||
The August 3 migration already proved two attended restart/unseal cycles under
|
||||
the same 2-of-3 barrier. T02 still requires one current emergency-seal drill
|
||||
against the authoritative runtime because restore rehearsal and intentional
|
||||
production lockdown are different claims.
|
||||
|
||||
## Warden signing recovery
|
||||
|
||||
Do not apply the parked standalone `warden-sign` AppRole as a T02 break-glass
|
||||
path.
|
||||
|
||||
- Normal operation uses the railiance-platform credential broker and short-lived
|
||||
`warden-sign` child token.
|
||||
- Broker/issuer recovery uses an attended OIDC platform operation.
|
||||
- A sealed or integrity-compromised OpenBao is recovered by the Shamir
|
||||
trust-root; another AppRole cannot bypass the seal.
|
||||
- Placing standing AppRole material outside the broker expands credential
|
||||
custody without improving root recovery.
|
||||
|
||||
The AppRole dry-run in SECRETS-WP-0004 remains useful negative evidence: its
|
||||
capability set is narrow. It is not apply authorization. A separately approved
|
||||
ops-bridge unattended-signing design may re-evaluate it under its own workplan;
|
||||
that service-access question is outside T02 and does not create recovery
|
||||
authority.
|
||||
|
||||
## Evidence and owner surfaces
|
||||
|
||||
- Current barrier/re-entry evidence: `railiance-master` RMASTER-WP-0020 and
|
||||
`docs/evidence/openbao-isolated-restore-2026-08-03.json`
|
||||
- Authoritative execution and recovery runbook:
|
||||
`railiance-platform/docs/openbao.md`
|
||||
- Current coordinated recovery gates:
|
||||
`railiance-platform/docs/railiance01-coordinated-reboot.md`
|
||||
- Emergency evidence contract and validator:
|
||||
`railiance-platform/docs/openbao-emergency-drill-evidence.example.json` and
|
||||
`make openbao-validate-emergency-evidence`
|
||||
- Soft-lockdown policy and proof: railiance-platform RAILIANCE-WP-0022 and
|
||||
ops-warden `scripts/check_agent_read_boundary.py`
|
||||
- Warden-sign recovery input: secrets-engine SECRETS-WP-0004 and
|
||||
`workplans/ADHOC-2026-08-11.md` T03
|
||||
37
docs/evidence/RAILIANCE-WP-0026-T01-ops-warden-receipt.json
Normal file
37
docs/evidence/RAILIANCE-WP-0026-T01-ops-warden-receipt.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"interface": "railiance.attended-login-containment-receipt",
|
||||
"version": 1,
|
||||
"task_id": "RAILIANCE-WP-0026-T01",
|
||||
"owner": "ops-warden",
|
||||
"source_repo": "ops-warden",
|
||||
"source_revision": "0fae0904ce8d8694338dd53a8a79abec5fec788d",
|
||||
"created_at": "2026-08-22T23:31:17Z",
|
||||
"disposition": "ready_for_owner_review",
|
||||
"focused_test": {
|
||||
"command": "uv run pytest -q tests/test_proxy.py tests/test_plan.py",
|
||||
"passed": true,
|
||||
"passed_count": 42,
|
||||
"failed_count": 0
|
||||
},
|
||||
"repository_verification": {
|
||||
"command": "uv run pytest -q",
|
||||
"passed": true,
|
||||
"passed_count": 390,
|
||||
"deselected_count": 4,
|
||||
"lint_command": "uv run ruff check .",
|
||||
"lint_passed": true
|
||||
},
|
||||
"acceptance_outcomes": {
|
||||
"private_helper_preflight_before_auth": true,
|
||||
"read_only_home_refused_before_oidc": true,
|
||||
"login_child_and_revocation_stdio_contained": true,
|
||||
"unexpected_stdout_and_stderr_fail_closed": true,
|
||||
"possible_issuance_triggers_contained_self_revocation": true,
|
||||
"helper_cleanup_is_deterministic": true,
|
||||
"persistent_login_only_handoff_refused": true
|
||||
},
|
||||
"live_oidc_performed": false,
|
||||
"live_drill_authorized": false,
|
||||
"secret_values_observed": false,
|
||||
"sensitive_material_recorded": false
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
# WARDEN-WP-0027-T02 attended drill preparation checklist
|
||||
|
||||
Status: `preparing` — `authorizes_execution: false`.
|
||||
|
||||
## Immutable scenario basis
|
||||
|
||||
- Scenario: `WARDEN-WP-0027-T02-DRILL-20260822-01`
|
||||
- Scenario artifact:
|
||||
`docs/evidence/WARDEN-WP-0027-T02-drill-scenario-2026-08-22.md`
|
||||
- Scenario SHA-256:
|
||||
`ffa69764ad391db633f57ac70444e5781eee698bf9e362dfef31614fa43152dc`
|
||||
- Preparation approval decision:
|
||||
`9da57559-712a-4521-b46e-a4c69729f9d2`
|
||||
- Preparation approved at: `2026-08-22T21:43:11Z`
|
||||
- Scenario expires: `2026-08-23T20:00:00Z`
|
||||
- Maximum live duration after a later exact GO: 45 minutes
|
||||
|
||||
The scenario artifact is intentionally unchanged after railiance-infra approved
|
||||
its pinned digest. This checklist records later preparation evidence without
|
||||
invalidating that receipt.
|
||||
|
||||
## Exact prepared live scope
|
||||
|
||||
The only prepared live sequence is one intentional OpenBao seal followed by the
|
||||
existing attended 2-of-3 Shamir unseal ceremony and value-safe post-unseal
|
||||
verification.
|
||||
|
||||
Preparation and owner receipts authorize no live action. The scope excludes a
|
||||
host reboot, re-key, snapshot restore, policy change, PVC mutation, credential
|
||||
disclosure, general workload restart, and every action not named above.
|
||||
|
||||
## Owner review gates
|
||||
|
||||
| Gate | Contract | State |
|
||||
| --- | --- | --- |
|
||||
| Independent provider console and distinct abort authority | `WARDEN-WP-0027-T02-DRILL-20260822-01-INFRA` | satisfied; receipt `01a02b4b-7295-7836-b288-f29407008524` |
|
||||
| Fresh encrypted, verified, off-host Raft snapshot and platform driver acceptance | `WARDEN-WP-0027-T02-DRILL-20260822-01-PLATFORM` | pending |
|
||||
| Two distinct custodians available for the current 2-of-3 barrier | `WARDEN-WP-0027-T02-DRILL-20260822-01-QUORUM` | pending |
|
||||
|
||||
Every receipt is metadata-only. No receipt may include a provider credential,
|
||||
OpenBao token, unseal share, recovery value, decrypted snapshot, custody
|
||||
location, custodian identity, or value-derived fingerprint.
|
||||
|
||||
## Final read-only preflight
|
||||
|
||||
After both pending contracts are satisfied, ops-warden runs the platform-owned
|
||||
`scripts/audit-core-recovery-preflight.py node-reboot` interface with:
|
||||
|
||||
- approved window id `WARDEN-WP-0027-T02-DRILL-20260822-01`;
|
||||
- the platform owner's current snapshot evidence file;
|
||||
- the railiance-master quorum attestation;
|
||||
- the accepted railiance-infra provider-console and abort role;
|
||||
- the existing RAILIANCE-WP-0024 procedure-owner acknowledgements.
|
||||
|
||||
The interface name reflects its superset recovery checklist; it does not add a
|
||||
reboot to this scenario. Its result must report all of:
|
||||
|
||||
- `preflight_only: true`;
|
||||
- `automated_checks_passed: true`;
|
||||
- `ready_for_live_execution: true`;
|
||||
- `secret_values_observed: false`.
|
||||
|
||||
Any changed cluster identity, invalid/stale snapshot receipt, missing owner
|
||||
receipt, overlapping mutation, failed automated check, observed secret value,
|
||||
or expired scenario is a NO-GO.
|
||||
|
||||
## Final human hold point
|
||||
|
||||
Only after the green preflight may ops-warden ask:
|
||||
|
||||
`GO WARDEN-WP-0027-T02-DRILL-20260822-01?`
|
||||
|
||||
No prior approval or conversational “go” crosses this hold point.
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
# WARDEN-WP-0027-T02 attended drill scenario
|
||||
|
||||
Status: `preparing` — live execution is prohibited.
|
||||
|
||||
## Window
|
||||
|
||||
- Scenario/window id: `WARDEN-WP-0027-T02-DRILL-20260822-01`
|
||||
- Preparation approval: Warden Desk `approve` recorded at
|
||||
`2026-08-22T19:22:28Z` (metadata only)
|
||||
- Approval expires: `2026-08-23T20:00:00Z`
|
||||
- Live window opens only when the operator gives the exact final go/no-go for
|
||||
this scenario after the owner preflight reports
|
||||
`ready_for_live_execution: true`
|
||||
- Maximum live-window duration after GO: 45 minutes
|
||||
- A NO-GO, missing gate, changed cluster identity, or expired approval closes
|
||||
this scenario without mutation
|
||||
|
||||
## Assigned roles
|
||||
|
||||
| Responsibility | Assigned owner | Acceptance |
|
||||
| --- | --- | --- |
|
||||
| Preparation coordinator and hold-point enforcement | `ops-warden` | accepted |
|
||||
| OpenBao snapshot, seal/unseal driver, post-unseal verification | `railiance-platform` | pending owner receipt |
|
||||
| Independent provider console and distinct abort authority | `railiance-infra` | pending owner receipt |
|
||||
| Two distinct 2-of-3 share custodians available out of band | `railiance-master` custody authority | pending quorum receipt |
|
||||
| Final live GO or NO-GO | human operator | deliberately not requested yet |
|
||||
|
||||
Owner procedure approval is already complete for `audit-core`,
|
||||
`rapp-postgres`, `railiance-platform`, `railiance-cluster`, and
|
||||
`railiance-infra` under the RAILIANCE-WP-0024 contract. Those receipts approve
|
||||
the procedure, not this live window.
|
||||
|
||||
## Current value-safe baseline
|
||||
|
||||
The `railiance-platform` node-reboot preflight at `2026-08-22T19:23:55Z`
|
||||
reported:
|
||||
|
||||
- `automated_checks_passed: true`
|
||||
- one Ready node with active k3s
|
||||
- `platform-pg` healthy 1/1 with continuous archiving and a successful backup
|
||||
17.15 hours old
|
||||
- OpenBao initialized and unsealed, Shamir `shares=3`, `threshold=2`
|
||||
- required ExternalSecret stores Valid and projections SecretSynced
|
||||
- audit-core at the reviewed digest, 1/1 Ready, zero restarts
|
||||
- `secret_values_observed: false`
|
||||
|
||||
This baseline is not reusable as the final hold-point result. The platform
|
||||
owner must rerun it against the current state and fresh snapshot receipt.
|
||||
|
||||
## Fail-closed preparation probes
|
||||
|
||||
- The OpenBao pod token helper and the current workstation caller token both
|
||||
receive `403 permission denied` for a capabilities check on
|
||||
`sys/storage/raft/snapshot`. No snapshot command was attempted after that
|
||||
denial. The platform driver must use its attended owner identity; the agent
|
||||
will not widen a workload token or substitute root.
|
||||
- Warden has no autonomous provider-console catalog lane. Independent console
|
||||
access therefore remains an explicit `railiance-infra` owner attestation,
|
||||
not an inferred result from SSH reachability.
|
||||
- The live barrier reports three shares and threshold two, but state metadata
|
||||
cannot prove two custodians are currently present. Availability must arrive
|
||||
through the out-of-band custody authority without identities or values.
|
||||
|
||||
## Pending receipts
|
||||
|
||||
- [ ] `railiance-platform`: fresh encrypted, verified, off-host OpenBao Raft
|
||||
snapshot receipt matching the live cluster id and possible applied index;
|
||||
acceptance of the driver role
|
||||
- [ ] `railiance-infra`: independent provider-console access verified;
|
||||
acceptance of the distinct abort role
|
||||
- [ ] `railiance-master`: two distinct share custodians explicitly available
|
||||
through the approved out-of-band custody paths; no identities or share values
|
||||
in the receipt
|
||||
- [ ] Fully parameterized read-only owner preflight returns
|
||||
`ready_for_live_execution: true` and `secret_values_observed: false`
|
||||
|
||||
## Final hold point
|
||||
|
||||
Once all pending receipts validate, `ops-warden` presents only the scenario id,
|
||||
bounded duration, owner roles, current preflight result, and stop conditions to
|
||||
the human operator. The live step requires an explicit `GO` for this exact
|
||||
scenario. Any other response is NO-GO.
|
||||
|
||||
No unseal share, token, provider credential, Secret data, decrypted snapshot,
|
||||
or value-derived fingerprint belongs in Git, State Hub, shell history, logs, or
|
||||
chat.
|
||||
37
docs/evidence/security-zone-admission-2026-08-22.md
Normal file
37
docs/evidence/security-zone-admission-2026-08-22.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Ops-warden security-zone admission evidence — 2026-08-22
|
||||
|
||||
This record supports the `z1-operational` membership declared in
|
||||
`tenancy.yaml`. It does not claim the M2 gates that ops-warden has not met.
|
||||
|
||||
## Identity and scope
|
||||
|
||||
- Workload id: `ops-warden`.
|
||||
- Runtime binding: Kubernetes ServiceAccount
|
||||
`system:serviceaccount:ops-warden:ops-warden`, issued by railiance01 and
|
||||
verified against the enforcing flex-auth pin on 2026-08-19.
|
||||
- Responsible party: `team:platform-security` in this repository.
|
||||
- Scope: attended issuance of short-lived SSH certificates plus a pointer-only
|
||||
credential catalog. Secret values are not stored in the catalog or audit.
|
||||
|
||||
## M1 evidence
|
||||
|
||||
- Owned front door: `warden sign` is the sole certificate-issuance interface;
|
||||
actor inventory, principal allow-list, and TTL ceilings are enforced before
|
||||
the CA backend.
|
||||
- Basic service objective: production signing is bounded by the actor TTL
|
||||
policy (`adm` 48h, `agt` 24h, `atm` 8h); `warden status` and the production
|
||||
verification records expose backend readiness.
|
||||
- Data handling: `ADR-0002` makes ops-warden a transparent conduit and
|
||||
`ADR-0004`/`ADR-0007` prevent raw agent reads and fail safe on ungraded lanes.
|
||||
- Policy path: `history/2026-08-19-flex-auth-caller-identity-evidence.md` proves
|
||||
the authenticated caller path and anonymous rejection. At adoption, the
|
||||
migrated real operator config reran the check successfully through the
|
||||
existing tunnel: HTTP 200, effect `allow`, decision
|
||||
`decision:f3f7c88f9585582a`.
|
||||
|
||||
## Why not z2
|
||||
|
||||
Ops-warden has security review artifacts, but not the complete M2 promotion
|
||||
set: there is no SLO history, on-call rotation, or exercised signing-path
|
||||
incident/recovery runbook. Its tenancy posture therefore remains V0 and its
|
||||
accurate zone membership remains `z1-operational`.
|
||||
|
|
@ -10,6 +10,9 @@ actors:
|
|||
- agt-task-bridge
|
||||
ttl_hours: 24
|
||||
description: "ops-bridge tunnel agent for state-hub"
|
||||
zone_subject:
|
||||
applicability: applicable
|
||||
workload_id: ops-bridge-tunnel
|
||||
|
||||
agt-codex-interhub-bootstrap:
|
||||
type: agt
|
||||
|
|
@ -17,6 +20,9 @@ actors:
|
|||
- agt-interhub-bootstrap
|
||||
ttl_hours: 2
|
||||
description: "Short-lived agent access for attended Inter-Hub bootstrap"
|
||||
zone_subject:
|
||||
applicability: applicable
|
||||
workload_id: codex-interhub-bootstrap
|
||||
|
||||
adm-example:
|
||||
type: adm
|
||||
|
|
@ -24,6 +30,9 @@ actors:
|
|||
- adm-full
|
||||
ttl_hours: 48
|
||||
description: "Example human operator — replace with per-person adm-* actors"
|
||||
zone_subject:
|
||||
applicability: not-applicable
|
||||
reason: human operator retains native actor identity
|
||||
|
||||
atm-backup-daily:
|
||||
type: atm
|
||||
|
|
@ -31,6 +40,9 @@ actors:
|
|||
- atm-backup-daily
|
||||
ttl_hours: 8
|
||||
description: "Example nightly automation actor"
|
||||
zone_subject:
|
||||
applicability: applicable
|
||||
workload_id: backup-daily
|
||||
|
||||
hosts:
|
||||
example-host:
|
||||
|
|
@ -38,4 +50,4 @@ hosts:
|
|||
agt:
|
||||
- agt-task-bridge
|
||||
atm:
|
||||
- atm-backup-daily
|
||||
- atm-backup-daily
|
||||
|
|
|
|||
20
examples/rapp-qonto-posture-conformance.yaml
Normal file
20
examples/rapp-qonto-posture-conformance.yaml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
environments:
|
||||
prod:
|
||||
backend: openbao-sealed-shamir
|
||||
real_values: generated-fresh-no-reuse
|
||||
unseal: shamir-3-of-5-break-glass
|
||||
|
||||
workloads:
|
||||
- id: rapp-qonto
|
||||
env_posture: prod
|
||||
maturity: M3
|
||||
|
||||
secret_requests:
|
||||
- secret: binky-qonto-api
|
||||
to_workload: rapp-qonto
|
||||
required_maturity: M3
|
||||
dataclass: restricted
|
||||
- secret: rapp-qonto-keycape-client
|
||||
to_workload: rapp-qonto
|
||||
required_maturity: M3
|
||||
dataclass: confidential
|
||||
|
|
@ -15,13 +15,39 @@ vault:
|
|||
inventory_path: ~/.config/warden/inventory.yaml
|
||||
state_dir: ~/.local/state/warden
|
||||
|
||||
# Opt-in flex-auth gate — enable only when flex-auth is reachable at flex_auth_url.
|
||||
# Zone-aware flex-auth gate. Missing target membership is the explicit unknown
|
||||
# profile; there is no repo-wide enable switch.
|
||||
# Registry: registry/flex-auth/production_registry_snapshot.json (build from inventory).
|
||||
# See wiki/PolicyGatedSigning.md (operator checklist) and wiki/playbooks/operator-openbao-token-hygiene.md
|
||||
policy:
|
||||
enabled: false
|
||||
flex_auth_url: http://flex-auth.flex-auth.svc.cluster.local:8080
|
||||
fail_closed: true
|
||||
# The in-cluster pin for ops-warden's signing policy (FLEX-WP-0016). A bare
|
||||
# flex-auth.flex-auth.svc Service does not exist. From a workstation, reach it
|
||||
# through a port-forward or tunnel and point this at that local address.
|
||||
flex_auth_url: http://flex-auth-ops-warden.flex-auth.svc.cluster.local:8080
|
||||
zone_registry_path: registry/flex-auth/production_registry_snapshot.json
|
||||
failure_modes:
|
||||
z0-experimental: fail_open
|
||||
z1-operational: fail_open
|
||||
z2-protected: fail_open
|
||||
z2-continuity: fail_open
|
||||
z3-critical: fail_closed
|
||||
unknown: fail_open
|
||||
not-applicable: fail_closed
|
||||
tenant: tenant:platform
|
||||
subject_env: WARDEN_POLICY_SUBJECT
|
||||
system: ops-warden
|
||||
system: ops-warden
|
||||
# How ops-warden proves it is ops-warden. flex-auth TokenReviews this bearer
|
||||
# token and requires the principal system:serviceaccount:ops-warden:ops-warden
|
||||
# for resource.system: ops-warden. Mode none sends no header, which is what
|
||||
# holds the pin in warn.
|
||||
caller_auth:
|
||||
mode: none # none | file | env | command
|
||||
# In-cluster PEP — projected ServiceAccount token, audience-bound:
|
||||
# mode: file
|
||||
# token_path: /var/run/secrets/flex-auth/token
|
||||
# Workstation — mint a short-lived bound token per call:
|
||||
# mode: command
|
||||
# command: kubectl create token ops-warden -n ops-warden
|
||||
# --audience flex-auth --duration 10m
|
||||
token_env: WARDEN_POLICY_CALLER_TOKEN
|
||||
audience: flex-auth
|
||||
|
|
|
|||
57
history/2026-07-16-credential-disclosure-lessons.md
Normal file
57
history/2026-07-16-credential-disclosure-lessons.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# Credential disclosure lessons — 2026-07-16
|
||||
|
||||
**Context:** buildup mode. Exposure was accepted; the value here is the learnings,
|
||||
not blame. Rotation of the exposed values is the operator's optional call, not a
|
||||
blocker (see WP-0026 T07).
|
||||
|
||||
## What happened
|
||||
|
||||
While verifying `CCR-2026-0004` (railiance offsite backup lane), a negative policy
|
||||
test was run as:
|
||||
|
||||
```bash
|
||||
BAO_TOKEN=$(bao token create -policy=default -field=token) bao kv get <path>
|
||||
```
|
||||
|
||||
The `bao token create` was **denied** (the workload role lacks it), so `BAO_TOKEN`
|
||||
was left unset and `bao kv get` fell back to the caller's **privileged login
|
||||
token**. The read succeeded and printed all three field values —
|
||||
`NC_WEBDAV_TOKEN`, `NC_WEBDAV_URL`, `AGE_PRIVATE_KEY` — into an agent session
|
||||
transcript (a logged context).
|
||||
|
||||
## Root causes
|
||||
|
||||
1. **The deny-test read the secret data path at all.** A negative test should
|
||||
prove *deny*, and proving deny never requires reading the value.
|
||||
2. **Silent privileged-token fallback.** When the scoped token creation failed,
|
||||
the command quietly used the caller's privileged token instead of failing.
|
||||
3. **The read landed in a logged context.** An agent session transcript is not a
|
||||
safe sink for secret material.
|
||||
|
||||
## Corrections (WARDEN-WP-0026, Strand A)
|
||||
|
||||
- **Verification never reads secret data.** Prove allow/deny with
|
||||
`bao token capabilities`, not `bao kv get`. If `bao token create -policy=default`
|
||||
is itself denied, that is a *pass* for the deny direction — never fall back to a
|
||||
privileged token. Canonical pattern:
|
||||
`wiki/playbooks/catalog-lane-promotion.md#capabilities-safe-lane-verification`
|
||||
(WP-0026 T01, applied to the forgejo and railiance-backup lane playbooks).
|
||||
- **Safe transport** for values that must move: env var, file, or response-wrapping
|
||||
token (`-wrap-ttl`) — never a stdout table (WP-0026 T02).
|
||||
- **Masking** as defense-in-depth in the warden wrapper (WP-0026 T03).
|
||||
- **Agent read-boundary + EXPOSED taint** on high-risk lanes, and per-lane
|
||||
**rotation guidance** (WP-0026 T04–T06).
|
||||
|
||||
## Deferred (Strand B — WARDEN-WP-0027)
|
||||
|
||||
Executable one-command mass rotation, graded lockdown / break-glass with a designed
|
||||
trust-root, and tamper-evident policy governance + reconcile are captured in
|
||||
`WARDEN-WP-0027` (backlog, gated on an activation trigger).
|
||||
|
||||
## References
|
||||
|
||||
- `WARDEN-WP-0026` — disclosure hygiene (Strand A)
|
||||
- `WARDEN-WP-0027` — governance/lockdown (Strand B, deferred)
|
||||
- `CCR-2026-0004-railiance-backup-offsite-lane.yaml` (railiance-platform)
|
||||
- `wiki/playbooks/railiance-backup-offsite-lane.md`
|
||||
- `.claude/rules/credential-routing.md`
|
||||
104
history/2026-08-11-delegation-surface-assessment.md
Normal file
104
history/2026-08-11-delegation-surface-assessment.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# Delegation surface assessment — 2026-08-11
|
||||
|
||||
**Trigger:** founder directive — ops-warden should work with, but never replace or
|
||||
duplicate, secrets-engine, tenant-engine, user-engine and other NetKingdom
|
||||
components. Covering an unfilled gap is acceptable if the gap stays visible, gets
|
||||
filled with proper governance, and ops-warden then delegates.
|
||||
|
||||
**Method:** enumerate `registry/routing/catalog.yaml` by execution mode; check
|
||||
`wiki/AccessRouting.md` and `wiki/playbooks/catalog-lane-promotion.md` for existing
|
||||
interim/delegation doctrine; compare against SCOPE/INTENT claims.
|
||||
|
||||
---
|
||||
|
||||
## 1. Execution surface (24 catalog entries)
|
||||
|
||||
| Mode | Count | Entries |
|
||||
| --- | --- | --- |
|
||||
| `warden_executes: true` — ops-warden's own authority | 1 | `ssh-cert-host-access` |
|
||||
| `exec_owner:` set — delegated, route-primary/proxy-fallback | 2 | `whynot-design-npm-publish` (secrets-engine), `ops-warden-warden-sign-token` (railiance-platform broker) |
|
||||
| `exec_capable` proxy, **no** `exec_owner` | 11 | `openbao-api-key`, `key-cape-oidc-login`, `issue-core-ingestion-api-key`, `reuse-surface-hub-write-token`, `openrouter-llm-connect`, `railiance-backup-offsite-lane`, `forgejo-admin-api-token`, `binky-company-email-imap`, `binky-qonto-api`, `rapp-qonto-keycape-client`, `agent-harness-forgejo-deploy` |
|
||||
| route-only pointer | 10 | remainder |
|
||||
|
||||
**Finding.** The delegation primitive exists, is proven in production, and is used by
|
||||
**2 of 24** lanes. Eleven lanes have ops-warden as the de facto front door with no
|
||||
record of who should own it instead.
|
||||
|
||||
## 2. The doctrine is not written down
|
||||
|
||||
- `wiki/AccessRouting.md` — the canonical "what ops-warden answers" page — contains
|
||||
**no mention of secrets-engine** and no section on interim positions. A worker or
|
||||
agent reading it cannot tell that `warden access` proxying a workload secret is a
|
||||
stopgap rather than the design.
|
||||
- `wiki/playbooks/catalog-lane-promotion.md` gates draft→active on the lane
|
||||
*working* (zero placeholders, resolvable, tests green). It never asks whether
|
||||
ops-warden should be the one executing it.
|
||||
- The delegation intent for `whynot-design-npm-publish` lives in WP-0019 prose and
|
||||
SCOPE; the *machine-readable* expression (`exec_owner`) was a by-product, not a
|
||||
policy applied catalog-wide.
|
||||
|
||||
Consequence: an absorbed need is indistinguishable from a designed one. Ownership
|
||||
drift is invisible by construction, which is exactly the failure mode the directive
|
||||
targets.
|
||||
|
||||
## 3. Classification of the eleven (revised on inspection, founder review 2026-08-11)
|
||||
|
||||
A first draft sorted by *subsystem* — tenant lanes to tenant-engine, workload lanes to
|
||||
secrets-engine. Reading the entries showed that is the wrong axis. Nine of the eleven
|
||||
share one `auth_method` ("caller's own OpenBao token" via operator OIDC or a
|
||||
`workload-kv-read-*` policy) and one `fetch_command` shape
|
||||
(`bao kv get -field=X <path>`). No owner procedure is duplicated in those. Contrast
|
||||
`whynot-design-npm-publish` — npm config plus token injection into a specific tool, a
|
||||
*procedure*, which is why WP-0019 handed it to secrets-engine.
|
||||
|
||||
**Test applied: owner-specific procedure or lifecycle → interim. Generic KV read →
|
||||
thin wrapper, arguably permanent.**
|
||||
|
||||
### Interim (5) — classified
|
||||
|
||||
| Lane | Intended owner | Blocked on |
|
||||
| --- | --- | --- |
|
||||
| `rapp-qonto-keycape-client` | key-cape | `client_secret_basic` exchange is a key-cape procedure; rotation already `automatable: true` |
|
||||
| `binky-company-email-imap` | tenant-engine | `tenants/binky/...` custody, rotation owned by `binky-control` — split lifecycle |
|
||||
| `binky-qonto-api` | tenant-engine | Same split |
|
||||
| `railiance-backup-offsite-lane` | railiance-platform | Rotation is `re-establish`, a procedure ops-warden only describes |
|
||||
| `agent-harness-forgejo-deploy` | railiance-platform / agent-harness | `re-establish` + alternative host-local key path |
|
||||
|
||||
### Held (6) — pending secrets-engine
|
||||
|
||||
`openbao-api-key`, `key-cape-oidc-login`, `issue-core-ingestion-api-key`,
|
||||
`reuse-surface-hub-write-token`, `openrouter-llm-connect`, `forgejo-admin-api-token`.
|
||||
|
||||
Permanent **only if** `secrets-engine exec` stays per-lane and provisioned. If it
|
||||
generalizes over arbitrary OpenBao lanes, all six become interim with secrets-engine as
|
||||
intended owner. Asked directly (msg `7d55d332`). For three of them
|
||||
(`issue-core-*`, `reuse-surface-*`, `openrouter-llm-connect`) production never touches
|
||||
the proxy at all — External Secrets delivers the value and the proxy serves operator
|
||||
verification only, which weakens the case that a front door is missing.
|
||||
|
||||
**No lane names `user-engine` as owner.** It appears only as a *consumer* inside
|
||||
`coulomb-social-runtime-env` (route-only, owned by railiance-platform), whose rotation
|
||||
guidance points at the OpenBao path `user-engine/user-engine-runtime` for
|
||||
`USER_ENGINE_PROXY_SECRET`. So a user-engine runtime secret exists and is routed, but
|
||||
user-engine fronts nothing itself. Whether it should own that lane is worth confirming
|
||||
rather than assuming either way.
|
||||
|
||||
## 4. Secondary finding — SCOPE drift
|
||||
|
||||
`SCOPE.md` "Where we are" was dated **2026-07-01** and stated *"Active work: none
|
||||
open in ops-warden after WP-0022/0023."* Six workplans have shipped since
|
||||
(WP-0024–0026, WP-0028, WP-0029 finished; WP-0027 sits in `backlog`), adding
|
||||
`warden plan`, `warden desk`, `warden taint`, `warden rotate-guide`, experiential
|
||||
memory, the tenant custody pattern, and the build-phase organization posture axis.
|
||||
SCOPE understated the repo by roughly six weeks of work. Corrected in this pass.
|
||||
|
||||
## 5. Recommendation
|
||||
|
||||
WARDEN-WP-0030 — record `delegation:` (mode / intended_owner / blocked_on) on every
|
||||
entry, expose `warden route gaps`, gate promotion on the ownership question, and
|
||||
publish the resulting interim register to the owner repos. Absence of a delegation
|
||||
block should read as *interim, owner unknown* — a question — never as settled
|
||||
ownership.
|
||||
|
||||
The measure of success is not fewer proxies. It is that no proxy exists without an
|
||||
answer to *"who should own this front door, and what is missing?"*
|
||||
92
history/2026-08-19-flex-auth-caller-identity-evidence.md
Normal file
92
history/2026-08-19-flex-auth-caller-identity-evidence.md
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# flex-auth caller identity — live evidence (WARDEN-WP-0031 T04)
|
||||
|
||||
**Date:** 2026-08-19
|
||||
**Pin:** `flex-auth-ops-warden`, railiance01 cluster, namespace `flex-auth`,
|
||||
Service `flex-auth-ops-warden:8080`, digest `sha256:138aa347…`, running
|
||||
`--caller-auth-mode warn --caller-kubernetes-url https://10.43.0.1
|
||||
--caller-binding ops-warden=system:serviceaccount:ops-warden:ops-warden`
|
||||
(read off the live Deployment, matching FLEX-WP-0016 T02).
|
||||
|
||||
Reached from the workstation by port-forward; the tunnel `k3s-api-railiance01`
|
||||
(local `16444`) carries the API. Use `--kubeconfig ~/.kube/config-railiance01`.
|
||||
|
||||
`~/.kube/config` / `config-hosteurope` — which `.bashrc` exports as `KUBECONFIG`
|
||||
— points at `16443`, and that was **CoulombCore's** k3s API, a different cluster
|
||||
whose client CA does not know this cert. Hence `Unauthorized`. CoulombCore is
|
||||
being retired and that tunnel was removed on 2026-08-19, so the port is simply
|
||||
gone now; `KUBECONFIG` was repointed at `config-railiance01`.
|
||||
|
||||
*(An earlier revision of this file blamed a local-port collision between
|
||||
`k3s-api-coulombcore` and `k3s-api-haskelseed`. That was wrong: the haskelseed
|
||||
tunnel is a **reverse** forward, where `local_port` is the destination on this
|
||||
workstation rather than a listener, so the two never competed for the port. The
|
||||
correction is recorded here because the wrong reason was also sent to flex-auth
|
||||
and would have misdirected whoever followed the handoff.)*
|
||||
|
||||
## Baseline — before
|
||||
|
||||
Unauthenticated `POST /v1/check` was **served**, and the pin logged:
|
||||
|
||||
```
|
||||
caller authentication warning: caller is not authenticated
|
||||
```
|
||||
|
||||
That is the whole reason `policy.enabled` could not flip: warn mode answers
|
||||
anonymous callers, so nothing about the enforcing path was ever exercised.
|
||||
|
||||
## What was created
|
||||
|
||||
`deploy/kubernetes/caller-identity.yaml` — Namespace `ops-warden` and
|
||||
ServiceAccount `ops-warden/ops-warden`, `automountServiceAccountToken: false`,
|
||||
**no RBAC of any kind**. It is never used to call the Kubernetes API; it exists
|
||||
only to be the subject of flex-auth's TokenReview. Applied 2026-08-19.
|
||||
|
||||
Cluster resources are railiance-platform's to own — this is an ADR-0003 interim
|
||||
cover, and the manifest names that owner in its header.
|
||||
|
||||
## Token source
|
||||
|
||||
`policy.caller_auth.mode: command` in `~/.config/warden/warden.yaml`:
|
||||
|
||||
```
|
||||
kubectl --kubeconfig ~/.kube/config-railiance01 create token ops-warden \
|
||||
-n ops-warden --audience flex-auth --duration 10m
|
||||
```
|
||||
|
||||
Audience `flex-auth` is required: `internal/callerauth/tokenreview.go` sends
|
||||
`spec.audiences: ["flex-auth"]` and rejects an identity whose audiences do not
|
||||
contain it. 900-char bound token, 10 minute TTL, minted per call, never stored.
|
||||
|
||||
## Evidence
|
||||
|
||||
```
|
||||
$ python3 scripts/check_policy_caller_identity.py --url http://127.0.0.1:19090
|
||||
✓ warden.yaml: loaded; policy.enabled=false
|
||||
✓ caller_auth.mode: command
|
||||
✓ caller token: obtained, 900 chars, sha256:e50da3ec6769
|
||||
✓ live /v1/check: HTTP 200, effect=allow, decision=decision:f3f7c88f9585582a
|
||||
READY
|
||||
```
|
||||
|
||||
The decisive check is not that allow — warn would have allowed an anonymous
|
||||
caller too. It is the warning count:
|
||||
|
||||
```
|
||||
warnings before: 4
|
||||
warnings after 2 authenticated gate runs: 4
|
||||
```
|
||||
|
||||
The pin authenticated the caller and had nothing to warn about. That is the
|
||||
condition ADHOC-2026-08-17-T01 required before `policy.enabled` may flip
|
||||
anywhere.
|
||||
|
||||
## What is still open
|
||||
|
||||
`policy.enabled` stays **false**. The remaining sequence (T05) is flex-auth's
|
||||
move first: `callerAuth.mode: enforce` on this pin (their FLEX-WP-0016 T03),
|
||||
re-run the gate against the enforcing pin, then `policy.enabled: true` with
|
||||
`fail_closed: true`, then an end-to-end `warden sign` — which additionally needs
|
||||
a scoped `VAULT_TOKEN` via `ops-warden-warden-sign-token`.
|
||||
|
||||
Flipping before enforce buys nothing; flipping before this task would have
|
||||
401'd every `warden sign`.
|
||||
171
history/2026-08-28-security-layer-model-assent.md
Normal file
171
history/2026-08-28-security-layer-model-assent.md
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
# Security layer model — ops-warden's assent (WARDEN-IN-0001)
|
||||
|
||||
**Date:** 2026-08-28
|
||||
**Intake:** `WARDEN-IN-0001`
|
||||
**Requested by:** gate-house, ratified as `GH-DEC-2026-001`
|
||||
**Standard:** `net-kingdom/canon/standards/security-layer-model_v0.1.md` (proposed)
|
||||
**Outcome:** assent to all three items; one declared non-conformance and one
|
||||
proposed amendment to the standard.
|
||||
|
||||
---
|
||||
|
||||
## What was asked
|
||||
|
||||
gate-house asked ops-warden to assent to three boundary items:
|
||||
|
||||
1. **ops-warden is Staff**, bound by §5 — Staff acts only through Engine APIs and
|
||||
never holds a direct Tooling client.
|
||||
2. **Doctrine versus runbook** — the security curriculum moves to gate-house; the
|
||||
`NetKingdom Security Literacy` section in `INTENT.md` becomes lane-specific
|
||||
runbooks that reference gate-house doctrine rather than restating it.
|
||||
3. **The access lane / access rule demarcation** (§8) — ops-warden and ops-mason own
|
||||
*lanes* (how a worker reaches a host); access-engine owns *rules* (whether they
|
||||
may). This is the condition attached to renaming flex-auth to access-engine, so
|
||||
ops-warden effectively holds a veto on that name.
|
||||
|
||||
Plus: add gate-house to the literacy and routing tables, and say so if moving the
|
||||
curriculum out leaves ops-warden unable to instruct its own workers.
|
||||
|
||||
---
|
||||
|
||||
## Item 1 — Staff, and the §5 problem it exposes
|
||||
|
||||
**Assent to the layer.** Staff is the right assignment and not a demotion.
|
||||
ops-warden's artifacts are exactly what §3.4 describes: routing decisions,
|
||||
workplans, runbooks, an audit trail. Its one production lane is non-deterministic
|
||||
in the sense that matters — it is an operator front door, not a contract.
|
||||
|
||||
**But §5 is violated today, and by the one lane ops-warden permanently owns.**
|
||||
The rule is deliberately greppable, so grepping is the honest response:
|
||||
|
||||
| Path | Tooling contact | Kind | Whose credential |
|
||||
| --- | --- | --- | --- |
|
||||
| `src/warden/vault.py` (`VaultCA.sign`) | `POST <bao>/v1/<mount>/sign/<role>` | **write** | broker-issued token held by ops-warden's process |
|
||||
| `src/warden/desk.py` | `bao kv put` (paste-once provisioning) | **write** | founder's, at the desk |
|
||||
| `src/warden/taint.py` | `bao kv metadata get` | read (metadata only, never data) | caller's |
|
||||
| `src/warden/proxy.py` (`warden access --fetch/--exec/--wrap`) | catalog `fetch_command` | read | **the caller's own** |
|
||||
|
||||
Two of these are not really ops-warden's clients. `proxy.py` runs the owner's tool
|
||||
under the caller's identity and supplies no authority of its own — that is
|
||||
`ADR-0002`, conduit not broker, and it is arguably outside §5's target. `taint.py`
|
||||
reads metadata only, and fits §5's read-only-observation allowance once declared.
|
||||
|
||||
**`VaultCA` does not have that defence.** It is a direct OpenBao client, in a Staff
|
||||
repository, performing a write, presenting a token from its own environment. It is
|
||||
production-verified and it is the SSH lane — the single thing ops-warden owns
|
||||
permanently. Under §5 as written, adopting this standard puts ops-warden's core
|
||||
lane in violation on the day it is adopted.
|
||||
|
||||
The escape hatch §5 offers does not fit: it covers *read-only observation for
|
||||
diagnostics*, and signing is a write. The route §5 prescribes does fit —
|
||||
|
||||
> *A Staff repository needing a capability no engine exposes MUST raise that as an
|
||||
> engine gap, not solve it locally.*
|
||||
|
||||
— and no engine exposes SSH certificate signing. `secrets-engine` owns credential
|
||||
abstraction, custody and lifecycle, which is the layer this belongs in, but it
|
||||
fronts no SSH-CA API today.
|
||||
|
||||
**So ops-warden assents and declares the non-conformance rather than negotiating an
|
||||
exemption.** `VaultCA` is recorded in `INTENT.md` as a declared §5 exception with a
|
||||
named intended owner (`secrets-engine`), a blocker (no SSH-CA engine surface), and a
|
||||
review date. That is `ADR-0003` — cover gaps, never silently own them — applied to
|
||||
ops-warden itself instead of to someone else's lane.
|
||||
|
||||
### Proposed amendment to the standard
|
||||
|
||||
§5 has exactly one shape for a Staff repository that legitimately touches Tooling:
|
||||
read-only diagnostics. That shape is too narrow to describe the estate as it exists,
|
||||
and a rule with no lane for a real, sanctioned case gets satisfied by relabelling
|
||||
rather than by closing the gap.
|
||||
|
||||
Recommend §5 gain a second shape: a **declared engine gap** — a Staff repository MAY
|
||||
hold a Tooling client for a capability no engine exposes, provided it is declared in
|
||||
`INTENT.md` with an intended owner, the blocker, and a review date, and provided the
|
||||
declaration is machine-readable so the conformance check in §10 can distinguish a
|
||||
tracked gap from an undeclared violation.
|
||||
|
||||
ops-warden already runs this machinery for other repositories' lanes: 27 catalog
|
||||
entries carry `delegation:` with `intended_owner` and `blocked_on`, and
|
||||
`warden route gaps` lists them (WP-0030). It is offered, not imposed — the standard
|
||||
is gate-house's.
|
||||
|
||||
---
|
||||
|
||||
## Item 2 — Doctrine versus runbook
|
||||
|
||||
**Assent.** The `NetKingdom Security Literacy` section is what gate-house says it is:
|
||||
evidence that the curriculum had no owner, so it accreted in whatever `INTENT.md`
|
||||
was open. That is the same failure `risk-nexus` names for findings and the same one
|
||||
`ADR-0001` prevents for catalog procedure. ops-warden has argued this rule twice
|
||||
against other repositories; it applies here.
|
||||
|
||||
The boundary, drawn precisely:
|
||||
|
||||
| Moves to gate-house | Stays with ops-warden |
|
||||
| --- | --- |
|
||||
| Why the planes are separated; the authority model | Which subsystem owns which credential need |
|
||||
| What "posture", "zone", "authority ceiling" mean | How to obtain a cert, a lease, a login — per lane |
|
||||
| The security curriculum a worker is taught | The runbook a worker executes |
|
||||
| Doctrine a lane must conform to | Evidence of conformance for ops-warden's lanes |
|
||||
|
||||
**gate-house's test question, answered: no, it does not leave ops-warden unable to
|
||||
instruct its workers — and the reason is worth recording.** What actually instructs
|
||||
an ops-warden worker is not the prose in `INTENT.md`. It is `warden plan "<need>"`,
|
||||
`warden route find`, and `.claude/rules/credential-routing.md`, which is inlined into
|
||||
every repository's agent instructions precisely because credential routing is
|
||||
high-frequency and high-risk. That surface is executable, lane-specific, and
|
||||
unambiguously runbook. It does not depend on the literacy table, and moving doctrine
|
||||
out does not weaken it.
|
||||
|
||||
If anything the move improves it: the literacy table has been a second, prose copy of
|
||||
what `registry/routing/catalog.yaml` states machine-readably, which is the
|
||||
double-source failure `ADR-0001` exists to stop.
|
||||
|
||||
**One thing must not move with it.** `.claude/rules/credential-routing.md` stays
|
||||
inline in this repository and in every other. It is not doctrine and not a
|
||||
curriculum; it is the anti-pattern list an agent needs *before* it acts, and a
|
||||
reference to a document in another repository would not be read in time.
|
||||
|
||||
---
|
||||
|
||||
## Item 3 — Access lane versus access rule
|
||||
|
||||
**Assent, unconditionally, and the veto on `access-engine` is not exercised.**
|
||||
|
||||
ops-warden is already built this way. `ADR-0005` implements one lane narrowly and
|
||||
routes everything else; `ADR-0002` makes it a conduit that never decides; `ADR-0009`
|
||||
has ops-warden compile membership attributes and apply a zone's failure mode while
|
||||
flex-auth owns the stance. ops-warden consumes decisions; it has never rendered one.
|
||||
The demarcation costs nothing because it describes what is already true.
|
||||
|
||||
`access-engine` is also the better name. ops-warden's own routing table has had to
|
||||
say "authorization" for the decision and "access" for the route for a year, and the
|
||||
collision is visible in every playbook.
|
||||
|
||||
**One operational condition, on execution rather than on the ruling.** The rename is
|
||||
598 references across 82 files in this repository alone — catalog `owner:` fields,
|
||||
`registry/flex-auth/`, `src/warden/policy.py`, the production registry snapshot
|
||||
builder, playbooks, and the `.claude/rules/` files that other repositories inline.
|
||||
Ops-warden asks for a deprecation window in which both names resolve, rather than a
|
||||
flag day; ops-warden will do its own migration inside that window. This is a request
|
||||
about sequencing, not a reservation about the name.
|
||||
|
||||
---
|
||||
|
||||
## Item 4 — gate-house is missing from every table
|
||||
|
||||
Correct, and fixed in this pass. gate-house is added to the literacy/routing table
|
||||
in `INTENT.md` as the owner of doctrine, invariants, authority context, and
|
||||
conformance review — with the routing rule stated explicitly: **doctrine and
|
||||
authority-model questions go to gate-house; policy decisions continue to go to
|
||||
access-engine.** Those are different questions and the distinction is the whole
|
||||
point of §6.
|
||||
|
||||
---
|
||||
|
||||
## Recorded as
|
||||
|
||||
- `ADR-0010` — ops-warden is Staff; lanes not rules; the declared §5 exception
|
||||
- `INTENT.md` — layer declaration, reworked routing table, gate-house row
|
||||
- `WARDEN-IN-0001` — closed, outcome `assented`
|
||||
136
history/2026-08-29-layer-model-v04-review.md
Normal file
136
history/2026-08-29-layer-model-v04-review.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# Security Layer Model v0.4 — ops-warden's review
|
||||
|
||||
**Date:** 2026-08-29
|
||||
**Reviewed:** `net-kingdom/canon/standards/security-layer-model_v0.4.md` (accepted)
|
||||
**Prior position:** `ADR-0010`, assent to v0.1 (`WARDEN-IN-0001`)
|
||||
**Outcome:** no objection to the ruling; three findings, one of them against ops-warden.
|
||||
|
||||
---
|
||||
|
||||
## What v0.4 did with ops-warden's amendment
|
||||
|
||||
Both §5 asks from `ADR-0010` were adopted.
|
||||
|
||||
**§5.3 declared engine gap** is the amendment ops-warden offered, adopted with the
|
||||
four fields intact (`capability`, `intended_owner`, `blocked_on`, `review`), the
|
||||
rationale preserved — *a rule offering no lane for a real sanctioned case gets
|
||||
satisfied by relabelling rather than by closing the gap* — and the framing that
|
||||
matters most kept explicit: **a declared gap is tracked non-conformance, not
|
||||
conformance**. ops-warden's delegation machinery is cited as prior art.
|
||||
|
||||
**§5.2 conduit** resolves the question ops-warden flagged rather than assumed. The
|
||||
test is the supplied-authority property, which is the right test: it turns on what
|
||||
the repository presents, not on what it touches. *"A conduit that presents its own
|
||||
token is not a conduit"* is a sharper statement of `ADR-0002` than `ADR-0002` makes.
|
||||
|
||||
**This created an obligation ops-warden had not met.** §5.3 requires the fields
|
||||
*machine-readably* and §11 makes the mapping a mechanical check; ops-warden's
|
||||
declaration was prose in `INTENT.md`. Fixed in this pass: `layer.yaml`,
|
||||
`scripts/check_layer_conformance.py`, `tests/test_layer_conformance.py`. The
|
||||
checker found three undeclared modules on first run, all false positives — help
|
||||
text, a docstring, and the doubles library that *simulates* `bao` — which is why
|
||||
it now matches invocation shapes rather than the word.
|
||||
|
||||
---
|
||||
|
||||
## Finding 1 — §9.1 and §5.3 disagree, and ops-warden's §4 row is the instance
|
||||
|
||||
§9.1: *a Staff repository MUST NOT be catalogued in §4 as owning a capability that
|
||||
requires a Tooling contact no engine exposes*; where intended but unbuilt, the
|
||||
entry **MUST be marked pending** and the gap declared under §5.3.
|
||||
|
||||
ops-warden's §4 row reads `operational access lanes, stewardship, runbooks; SSH
|
||||
certificate issuance` — with no pending mark. And §13 lists *SSH-CA signing write
|
||||
(`VaultCA`, `bao kv put`) — declared by ops-warden — intended owner secrets-engine*.
|
||||
|
||||
So the catalog asserts ownership of a capability that requires a Tooling contact no
|
||||
engine exposes, unmarked. By §9.1's own text that is a defect. But the available
|
||||
fix is worse than the defect: **marking it pending would be false.** SSH issuance is
|
||||
production-verified and in daily use. `pending` would tell a reader ops-warden does
|
||||
not yet do the one thing it demonstrably does.
|
||||
|
||||
The root cause is that §9.1 collapses two different states:
|
||||
|
||||
| State | Example | Capability today |
|
||||
| --- | --- | --- |
|
||||
| No route exists at all | kings-guard containment (§9.2) | **zero** |
|
||||
| Route exists via a declared §5.3 gap | ops-warden SSH issuance | **working, tracked** |
|
||||
|
||||
§5.3 exists precisely to sanction the second. §9.1 was written for the first — it
|
||||
was raised by kings-guard, about containment, and correctly fixed *for that case*.
|
||||
Applied to the adjacent case it produces a false catalog.
|
||||
|
||||
**Recommendation:** give §9.1 two marks rather than one — `pending` where no route
|
||||
exists, and `declared-gap` where the capability is discharged under §5.3 and
|
||||
registered in §13. Both are honest; today's binary forces a choice between a false
|
||||
label and an unmarked violation.
|
||||
|
||||
This is the §12 loop working as designed, and §12 already says so: a finding that a
|
||||
rule is unsatisfiable is a success of the loop.
|
||||
|
||||
---
|
||||
|
||||
## Finding 2 — §5's scope is undefined for infrastructure §4 does not catalogue
|
||||
|
||||
§5 forbids *a direct client for a Tooling-layer system*. §4 catalogues the security
|
||||
estate, and only `key-cape` and `OpenBao` are Tooling rows.
|
||||
|
||||
ops-warden holds an HTTP client for the **State Hub** and for **llm-connect**
|
||||
(`src/warden/worker.py`). Neither appears in §4. Both are infrastructure a Staff
|
||||
repository holds a direct client for.
|
||||
|
||||
The question is not rhetorical, because the answers diverge sharply:
|
||||
|
||||
- **If they are Tooling**, then every Staff repository in the estate is in
|
||||
undeclared violation on adoption day — they all write progress events — and
|
||||
§11's second mechanical check fails estate-wide.
|
||||
- **If they are not**, §5 should say so, because *"a Tooling-layer system"* reads
|
||||
considerably broader than *"a repository in the §4 Tooling rows"*.
|
||||
|
||||
ops-warden has recorded both under `non_tooling_clients` in `layer.yaml` with the
|
||||
reasoning stated, rather than resolving it unilaterally. The scope is gate-house's
|
||||
to set.
|
||||
|
||||
---
|
||||
|
||||
## Finding 3 — §9.6 lands on ops-warden, and ops-warden does not satisfy it
|
||||
|
||||
This is the one against us, and it is the most consequential item in the review.
|
||||
|
||||
§9.6 consequence 1: *any system whose evidence is load-bearing MUST make emission
|
||||
atomic with the state change it records. An archive cannot retrofit completeness.*
|
||||
|
||||
**ops-warden's audit emission is deliberately non-atomic.** `src/warden/ca.py:90`
|
||||
carries `pass # audit must not block signing`, and `wiki/AuditTrail.md` states the
|
||||
trail *"never blocks the primary action"*. If the audit append fails, the
|
||||
certificate is still issued and the event is simply lost — a suppressed event that
|
||||
leaves the chain perfectly intact, which is the exact failure §9.6 describes.
|
||||
|
||||
That was a considered availability choice: an audit-disk problem should not remove
|
||||
production host access. §9.6 now makes it a conformance question, and the trade is
|
||||
real in both directions:
|
||||
|
||||
- make emission atomic → an audit write failure fails the sign, and the estate's
|
||||
operational access lane acquires a new dependency on its own evidence store;
|
||||
- leave it → signing evidence cannot be treated as complete, and anything reasoning
|
||||
from *"there is no record of a sign"* is unsound.
|
||||
|
||||
**ops-warden has not changed it, and is not going to decide this alone** — §9.6 is
|
||||
estate doctrine and the question is whether SSH signing evidence is load-bearing in
|
||||
gate-house's sense. What ops-warden can say is that the second horn is currently
|
||||
true and undocumented: `wiki/AuditTrail.md` does not warn that absence of a record
|
||||
is not evidence of absence. That correction is ops-warden's regardless of the
|
||||
ruling, and is the smaller half of the fix.
|
||||
|
||||
Note also that §5.2 requires a conduit action to be *"reconstructable as the
|
||||
caller's action in audit"* — an audit-dependent claim, and therefore bounded by
|
||||
§9.6. Worth a cross-reference so the two rules do not drift apart.
|
||||
|
||||
---
|
||||
|
||||
## Offered
|
||||
|
||||
`layer.yaml` + `check_layer_conformance.py` + `test_layer_conformance.py` is a
|
||||
working reference implementation of §5.3 and of §11's second mechanical check. Eight
|
||||
of fifteen estate repositories have yet to declare (§14). If it is useful as a
|
||||
pattern to point them at, it is offered — as the delegation machinery was.
|
||||
156
history/2026-08-29-layer-model-v06-review.md
Normal file
156
history/2026-08-29-layer-model-v06-review.md
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
# Security Layer Model v0.6 — ops-warden's review
|
||||
|
||||
**Date:** 2026-08-29
|
||||
**Reviewed:** `security-layer-model_v0.6.md` (proposed), plus v0.5 and the companion
|
||||
**Prior positions:** `ADR-0010` (v0.1 assent); `history/2026-08-29-layer-model-v04-review.md`
|
||||
**Outcome:** no objection; one conformance action taken, two findings, one accepted SHOULD.
|
||||
|
||||
---
|
||||
|
||||
## Disposition of ops-warden's v0.4 findings
|
||||
|
||||
All three were acted on, two of them exactly as recommended.
|
||||
|
||||
| Finding | Outcome |
|
||||
| --- | --- |
|
||||
| §9.1 forces a false `pending` onto working capability | **Adopted** — v0.5 split it into `pending` and `declared-gap`, credited to ops-warden |
|
||||
| §5 scope undefined for uncatalogued infrastructure | **Adopted** — "Tooling-layer system" now means a §4 Tooling row; the State Hub case is recorded, not policed |
|
||||
| §9.6 atomicity lands on ops-warden's signing lane | **Ruled** — the load-bearing / attributive distinction, with ops-warden's `# audit must not block signing` named as the estate's live example |
|
||||
|
||||
The §9.6 ruling deserves a note, because it went in ops-warden's favour and that is
|
||||
a reason to check it rather than accept it. The test is *"no control branches on
|
||||
its presence"*. Verified: the only consumer of `audit.jsonl` is `warden activity`
|
||||
(`cli.py`), which displays. Nothing gates on a signing record — not the agent
|
||||
read-boundary, not `warden plan`, not the scorecard. The lane is genuinely
|
||||
attributive and the trade is legitimate on the standard's own terms. The two
|
||||
obligations that attach — declare it, never claim completeness — were already met
|
||||
in `wiki/AuditTrail.md`, now updated to record the ruling rather than the open
|
||||
question. **If a future ops-warden control ever gates on this trail, the trade has
|
||||
to be revisited before that ships**, and that is recorded there.
|
||||
|
||||
`layer.yaml` is named in §11 as the estate's reference declaration form, including
|
||||
the "record non-Tooling clients so the check is total" property. Offered again to
|
||||
the repositories that have yet to declare.
|
||||
|
||||
---
|
||||
|
||||
## Conformance action taken — the stance map was not published
|
||||
|
||||
§6.4 obligation 3 requires a declared unreachable-engine stance that is total, per
|
||||
zone, with no implicit default, *"published rather than held in code comments"* —
|
||||
and §6.4 requires **every** PEP-shaped consumer to publish its map so the maps can
|
||||
be inventoried. `ADR-0009` is named as the reference shape.
|
||||
|
||||
ops-warden was not doing this. The map lived in `PolicyConfig.failure_modes`, a
|
||||
dataclass default in `src/warden/config.py`. That is not a code *comment*, but it
|
||||
is not published either — it is merely written down, and a consumer of the estate
|
||||
had no way to read ops-warden's stance without reading ops-warden's source.
|
||||
|
||||
Published as `pep-stance.yaml`, with the property that makes publishing worth
|
||||
anything: `tests/test_layer_conformance.py` asserts the published map is **equal to
|
||||
the shipped default**. A published map that may drift from the code is worse than
|
||||
no map, because it invites reliance it cannot support. The file also records the
|
||||
obligation-2 position (verdict never cached; input claims cached under their own
|
||||
freshness rules) and the obligation-4 bound (§9.6 attributive).
|
||||
|
||||
---
|
||||
|
||||
## Finding 1 — §6.4 obligation 1 contradicts obligation 3, and ops-warden is the instance
|
||||
|
||||
> **1. No side effect without a decision record.** A PEP MUST NOT perform the
|
||||
> protected action unless it holds a decision from `access-engine` identifying the
|
||||
> request it was rendered for.
|
||||
|
||||
> **3. A declared unreachable-engine stance (§9.3):** total, per zone... `ops-warden`
|
||||
> `ADR-0009` is the reference shape.
|
||||
|
||||
These cannot both be absolute. ops-warden's declared stance — blessed by §9.3 as
|
||||
*"the only thing left"* when there is no engine to ask — is `fail_open` for `z0`–`z2`
|
||||
and `unknown`. Applying it means issuing a certificate **without holding a
|
||||
decision**, which obligation 1 forbids without qualification.
|
||||
|
||||
So the same section names ops-warden as the reference shape for obligation 3 while
|
||||
obligation 1 makes ops-warden's shipped behaviour a violation. §9.3 settled the
|
||||
substance; §6.4 restates it in a form that takes it back.
|
||||
|
||||
**Recommendation.** Bound obligation 1 by obligation 3:
|
||||
|
||||
> A PEP MUST NOT perform the protected action unless it holds a decision from
|
||||
> `access-engine` identifying the request it was rendered for, **or its declared
|
||||
> §9.3 stance for the applicable scope permits proceeding without one and the
|
||||
> application of that stance is recorded in place of the decision**.
|
||||
|
||||
This is not a weakening. It is stricter than today's text in the case that matters:
|
||||
it makes the *recorded application of the stance* mandatory, rather than leaving
|
||||
"no decision record" as a silent state. ops-warden already does this — `ca.py`
|
||||
writes `policy_zone`, `policy_failure_mode` and `policy_decision_id` (present only
|
||||
where a decision was rendered) into both the signatures log and `audit.jsonl`, per
|
||||
`ADR-0009` rule 4: *a fail-open signing result is metadata, not silence*.
|
||||
|
||||
This is the same shape as the v0.4 §9.1 finding: a rule written for the clean case,
|
||||
correct there, producing a false result on the adjacent case the standard has
|
||||
already sanctioned elsewhere.
|
||||
|
||||
---
|
||||
|
||||
## Finding 2 — §6.4 creates a register that §13 does not implement
|
||||
|
||||
§6.4: *"Every PEP-shaped consumer MUST publish its stance map, and those maps MUST
|
||||
be inventoried — in `maturity-engine` once it exists, **in §13 until then**."*
|
||||
|
||||
§13 contains no stance-map rows. It records declared contacts and unowned
|
||||
capabilities; there is no column, row, or section for a PEP stance. So the
|
||||
obligation names a register that does not exist yet, and the failure mode §6.4
|
||||
itself warns about — *"`z0`–`z2` and unknown fail open" becoming the estate's real
|
||||
policy without anyone having compiled it* — is exactly what the missing register
|
||||
permits.
|
||||
|
||||
**Recommendation.** Either add a stance-map table to §13 with the same
|
||||
state/owner-status discipline the gap table has, or state that the inventory waits
|
||||
for `maturity-engine` and mark the obligation pending under §9.1's own logic — a
|
||||
requirement whose register does not exist is a capability catalogued without a
|
||||
surface. ops-warden's row is ready to paste:
|
||||
|
||||
| PEP | Protected action | Scope | Stance | Published |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `ops-warden` | SSH certificate issuance | security-zone | open `z0`–`z2`+unknown, closed `z3`/n-a | `ops-warden/pep-stance.yaml` |
|
||||
|
||||
The second half matters more than the first: **ops-warden is currently the only
|
||||
PEP that has published one**, so an inventory today would contain one row and that
|
||||
is itself the finding. `ops-mason` is named PEP-shaped in the same paragraph.
|
||||
|
||||
---
|
||||
|
||||
## Accepted, not yet done — §9.6 emission cadence
|
||||
|
||||
§9.6: *"A source SHOULD declare an expected emission cadence, and a drop below it
|
||||
SHOULD become a finding in its own right."*
|
||||
|
||||
ops-warden declares none. This is a genuine SHOULD and the reasoning behind it is
|
||||
sound — it converts the suppression blind spot into something detectable without
|
||||
any Tooling contact, because the source publishes its own stream. It is not done
|
||||
here because a cadence asserted without evidence is worse than none: ops-warden's
|
||||
signing volume is operator-driven and bursty, and a fabricated baseline would
|
||||
generate findings that mean nothing. Deriving one from the existing trail is
|
||||
tractable and is recorded as ops-warden's to do, not gate-house's to chase.
|
||||
|
||||
---
|
||||
|
||||
## On the pace
|
||||
|
||||
Six versions in two days, with four repositories' findings absorbed and credited,
|
||||
is the §12 loop working at a rate the estate has not seen before. Two cautions,
|
||||
offered as an interested consumer rather than as objections:
|
||||
|
||||
1. **§13 already says it should not be statute, and it is right.** The register has
|
||||
grown every version. Moving it to `maturity-engine` is the stated plan; until
|
||||
that exists, each version of the standard is also a snapshot of a backlog, and
|
||||
the two have very different review intervals.
|
||||
2. **The standard is `proposed` again at v0.6**, and the four repositories that
|
||||
assented did so to v0.1. ops-warden's `ADR-0010` assent covers the three
|
||||
boundary items, and nothing in v0.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.
|
||||
157
history/2026-08-29-v07-scope-intent-assessment.md
Normal file
157
history/2026-08-29-v07-scope-intent-assessment.md
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
# v0.7 conformance — INTENT vs SCOPE gap assessment
|
||||
|
||||
**Date:** 2026-08-29
|
||||
**Standard:** `security-layer-model_v0.7.md` (**accepted**) + `SECURITY-COMPANION.md` v0.2
|
||||
**Prior:** `ADR-0010`; v0.4 and v0.6 reviews in `history/`
|
||||
**Method:** each v0.7 obligation checked against shipped code, not against intent.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
ops-warden is **conformant on every obligation it can discharge alone except three**,
|
||||
and holds two declared §5.3 gaps that are tracked, registered and owned elsewhere.
|
||||
The three genuine gaps are §9.7.2 (no stated revocation visibility deadline — a MUST),
|
||||
§3.4 rule 1 (the agent read-boundary keys on an honour-system marker rather than an
|
||||
issued identity), and §9.6's cadence, which is a SHOULD for an attributive source and
|
||||
remains undone for an honest reason.
|
||||
|
||||
One new obligation is not a rule at all but a role: the companion routes the entire
|
||||
estate to ops-warden for *how to get something done*. Nothing in the repo answers a
|
||||
layer or declaration question today, and that is now a discoverability gap.
|
||||
|
||||
Four ops-warden findings were adopted into the standard between v0.4 and v0.7 — §9.1's
|
||||
two marks, §5's Tooling scope rule, §6.4 obligation 1's second limb, and §13.1's
|
||||
existence. That is the conformance loop working; it is not a reason to assume the next
|
||||
pass finds nothing.
|
||||
|
||||
---
|
||||
|
||||
## Obligation-by-obligation
|
||||
|
||||
### Conformant, shipped, evidenced
|
||||
|
||||
| Obligation | Evidence |
|
||||
| --- | --- |
|
||||
| §11 declare layer in own voice, machine-readably | `INTENT.md` frontmatter (`layer: Staff`, `pep_shaped: true`) + `layer.yaml` — cited in §11 as the estate's reference form |
|
||||
| §5 every Tooling contact maps to a shape; non-Tooling recorded so the check is total | `layer.yaml` 5 contacts + 2 exclusions; `scripts/check_layer_conformance.py` |
|
||||
| §5.2 conduit supplies no authority | `proxy.py::_caller_env`; `tests/test_layer_conformance.py::test_conduit_supplies_no_authority_of_its_own` |
|
||||
| §6.4 obl. 1 no side effect without a decision **or a recorded stance** | `ca.py` writes `policy_zone`, `policy_failure_mode`, `policy_decision_id` (present only where rendered). ops-warden is the named reference for limb two |
|
||||
| §6.4 obl. 2 no verdict recaching | `policy.py` caches nothing — verified by inspection, not by claim |
|
||||
| §6.4 obl. 3 stance map published, at a path named in the declaration, equal to shipped behaviour, asserted by test | `pep-stance.yaml`, named in `layer.yaml`; test asserts equality with `PolicyConfig().failure_modes`; registered in statute §13.1 |
|
||||
| §9.3 stance total, per zone, no implicit default | 7 rows covering every zone plus `unknown` and `not-applicable` |
|
||||
| §9.6 evidence claims bounded | `wiki/AuditTrail.md` declares the attributive trade and states absence is not evidence of absence |
|
||||
| §9.7.1 every allow has an explicit lifetime | TTL enforced per `ActorType` — `adm` 48h, `agt` 24h, `atm` 8h |
|
||||
| §3.4 rule 2 tool use is a conduit or engine API | `warden access` is the conduit; `ADR-0004` enforces that tool availability is not permission |
|
||||
|
||||
### Declared gaps — tracked non-conformance, owned elsewhere
|
||||
|
||||
Both registered in statute §13, intended owner `secrets-engine`, reviewed quarterly.
|
||||
Neither is closable by ops-warden: closing them means another repository shipping a
|
||||
surface.
|
||||
|
||||
- **`VaultCA` signing write** — no engine exposes SSH-CA signing.
|
||||
- **`warden desk` `bao kv put`** — no engine exposes attended provisioning.
|
||||
|
||||
Nothing in v0.7 changes their status. The right ops-warden behaviour is to keep them
|
||||
declared, keep the review dates honest, and not quietly grow a third.
|
||||
|
||||
---
|
||||
|
||||
## The three real gaps
|
||||
|
||||
### G1 — §9.7.2: no stated revocation visibility deadline (MUST)
|
||||
|
||||
> *A **PEP** has one boundary and MUST state one deadline… an unstated deadline is
|
||||
> an unbounded replay window.*
|
||||
|
||||
ops-warden states none, and the honest answer is uncomfortable: **the effective
|
||||
window is the certificate TTL — up to 48 hours.** A certificate issued under an allow
|
||||
stays valid for its full TTL even if the decision that authorized it is revoked or
|
||||
superseded the next minute. ops-warden has no revocation channel for an issued cert:
|
||||
there is no CRL, no KRL distribution, and host-side `auth_principals` is
|
||||
`railiance-infra`'s.
|
||||
|
||||
This is not a documentation gap. It is a design property that has never been written
|
||||
down, and §9.7.2 exists precisely to force it into the open. Two things follow:
|
||||
|
||||
1. The deadline must be **stated** — `adm` 48h / `agt` 24h / `atm` 8h — in
|
||||
`pep-stance.yaml`, as what it is rather than as an aspiration.
|
||||
2. Whether 48h is *acceptable* is a separate question, and it is partly
|
||||
`railiance-infra`'s (KRL distribution) and partly ours (TTL policy). Stating it is
|
||||
ours and is cheap; shortening it is a joint change.
|
||||
|
||||
Stating a bad number is better than stating none: an unstated deadline is an
|
||||
unbounded replay window, and this one is bounded and already implemented.
|
||||
|
||||
### G2 — §3.4 rule 1: the agent boundary rests on an honour-system marker
|
||||
|
||||
> *No standing credential. Authority is issued per task, time-bounded under §9.7,
|
||||
> and attributable to the principal on whose behalf it acts.*
|
||||
|
||||
`ADR-0004`'s read-boundary triggers when `WARDEN_AGENT_ID` is set — an environment
|
||||
variable the agent sets **about itself**. An agent that does not set it is not
|
||||
recognised as an agent. ops-warden has known this (`WARDEN-WP-0033-T04` recorded it
|
||||
as "an honour-system marker on the ops-warden side"), and it was tolerable while no
|
||||
issued agent identity existed.
|
||||
|
||||
One now does. `key-cape` accepted issuance ownership in `KEY-WP-0009-T03`:
|
||||
`codex-railiance-platform`, subject `service:codex:railiance-platform`, role
|
||||
`coding-agent`, scope `openbao:login`, 15-minute lifetime. The OpenBao side is
|
||||
enforced by `railiance-platform`'s policy, which is the half that actually holds.
|
||||
|
||||
So the gap is narrower than it looks and worth stating precisely: **the OpenBao-side
|
||||
boundary is real; the ops-warden-side boundary is advisory.** ops-warden should key
|
||||
its read-boundary on the issued identity where one is present, and treat
|
||||
`WARDEN_AGENT_ID` as a fallback that fails *toward* the boundary rather than away
|
||||
from it. That is a change in this repo and does not need another repo to move.
|
||||
|
||||
### G3 — §9.6 emission cadence (SHOULD, for an attributive source)
|
||||
|
||||
Unchanged from the v0.6 review and still honest: ops-warden declares no expected
|
||||
cadence because its signing volume is operator-driven and bursty, and a fabricated
|
||||
baseline generates findings that mean nothing. v0.7 makes cadence a **MUST for
|
||||
load-bearing sources**; ops-warden's trail is attributive, so it remains a SHOULD.
|
||||
|
||||
Deriving a real baseline from the existing trail is tractable and is ops-warden's to
|
||||
do. It should be derived and declared, or explicitly deferred with a reason — not
|
||||
left silent, which is what it is today.
|
||||
|
||||
---
|
||||
|
||||
## The role the companion assigns, and what it costs
|
||||
|
||||
> *"For how to get something done in NetKingdom — which lane, which credential, which
|
||||
> route — ask `ops-warden`. This document says what the rules are; ops-warden stewards
|
||||
> the paths through them."*
|
||||
|
||||
This is the largest change in ops-warden's INTENT surface and it is not a rule, so it
|
||||
does not appear in any conformance check. The estate has been told to come here.
|
||||
|
||||
**Today the repo answers credential questions and no others.** `warden route` and
|
||||
`warden plan` cover lanes, owners and acts. Nothing answers *"which layer am I"*,
|
||||
*"how do I declare"*, *"I am PEP-shaped, what do I owe"* — the questions the companion
|
||||
and the standard's adoption status (eight of fifteen repositories undeclared) actually
|
||||
generate.
|
||||
|
||||
ops-warden has already built the reference artifacts those repositories need, and the
|
||||
standard points at them by name in §11 and §6.4. What is missing is the path: a
|
||||
discoverable route from *"I read the companion"* to *"here is the file to copy and the
|
||||
check to run"*. That is exactly the stewardship ops-warden claims, applied to the
|
||||
estate's newest rule rather than to its credential lanes.
|
||||
|
||||
Also worth noting, and not ops-warden's to fix: §13.1's register has one row, and
|
||||
`ops-mason` — catalogued PEP-shaped in the same paragraph — has published nothing.
|
||||
The standard says one row is itself the finding.
|
||||
|
||||
---
|
||||
|
||||
## What does not need doing
|
||||
|
||||
- **No new ADR.** `ADR-0010` holds: Staff, lanes not rules, declared gaps not
|
||||
exemptions. v0.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.
|
||||
93
intakes/intakes.md
Normal file
93
intakes/intakes.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# Intake records
|
||||
|
||||
## WARDEN-IN-0001 — Assent requested: Staff layer, doctrine vs runbook, and the access lane/rule demarcation
|
||||
|
||||
```yaml
|
||||
id: WARDEN-IN-0001
|
||||
kind: intake
|
||||
title: 'Assent requested: Staff layer, doctrine vs runbook, and the access lane/rule
|
||||
demarcation'
|
||||
status: closed
|
||||
outcome: assented
|
||||
origin: cross-repo
|
||||
origin_ref: gate-house GH-DEC-2026-001
|
||||
priority: medium
|
||||
owner: ops-warden
|
||||
requested_by: gate-house
|
||||
standard: net-kingdom/canon/standards/security-layer-model_v0.1.md
|
||||
description: 'gate-house asks ops-warden to assent to three boundary items. (1) ops-warden
|
||||
is Staff, bound by the rule that Staff acts only through Engine APIs and never touches
|
||||
Tooling directly (standard section 5). (2) Doctrine versus runbook: the NetKingdom
|
||||
Security Literacy section in ops-warden INTENT is evidence the security curriculum
|
||||
had no owner; it now has one in gate-house. Proposal is that doctrine and curriculum
|
||||
move to gate-house and that section becomes lane-specific runbooks referencing gate-house
|
||||
doctrine rather than restating it. ops-warden keeps the lanes it stewards and everything
|
||||
operational about them. (3) The access lane/rule demarcation, normative in standard
|
||||
section 8: ops-warden and ops-mason own access lanes — how a worker reaches a host;
|
||||
access-engine owns access rules — whether they may. This demarcation is the condition
|
||||
attached to renaming flex-auth to access-engine, so ops-warden effectively holds
|
||||
a veto on that name. Also requested: add gate-house to the Security Literacy and
|
||||
routing tables — currently every plane is listed and gate-house appears nowhere
|
||||
— routing doctrine and authority-model questions there while continuing to route
|
||||
policy decisions to access-engine. If moving the curriculum out leaves ops-warden
|
||||
unable to instruct its own workers, say so; the boundary is wrong if it does.'
|
||||
notes: 'Assented to all three items in ADR-0010, with reasoning in
|
||||
history/2026-08-28-security-layer-model-assent.md. (1) Staff accepted; the section 5
|
||||
binding rule exposed a real non-conformance — src/warden/vault.py is a direct
|
||||
OpenBao client performing a write, as is warden desk''s bao kv put. Declared in
|
||||
INTENT.md as an engine gap with intended owner secrets-engine and blocker "no engine
|
||||
exposes an SSH-CA surface", not negotiated as an exemption; taint.py declared under
|
||||
the read-only allowance; warden access proxies run under the caller''s identity.
|
||||
An amendment is offered back to gate-house: a second sanctioned shape in section 5 for
|
||||
a declared engine gap carrying intended owner, blocker and review date, machine-readable
|
||||
so section 10 can tell a tracked gap from an undeclared violation. (2) Doctrine versus
|
||||
runbook accepted; the literacy section is now a lane routing runbook referencing
|
||||
gate-house doctrine. Answering gate-house''s test question: it does not leave ops-warden
|
||||
unable to instruct its workers, because what instructs them is warden plan / warden route
|
||||
and .claude/rules/credential-routing.md, which stays inline by design. (3) The lane/rule
|
||||
demarcation assented unconditionally and the access-engine veto not exercised — ops-warden
|
||||
already consumes decisions and renders none. One request on sequencing only: a deprecation
|
||||
window in which both names resolve (598 references across 82 files here). gate-house added
|
||||
to the routing tables in INTENT.md and SCOPE.md.'
|
||||
created: '2026-08-28T19:30:28.087109Z'
|
||||
updated: '2026-08-28T21:05:00Z'
|
||||
state_hub_intake_id: "01a049ed-bbbc-7520-bc7c-6b0912ca534a"
|
||||
```
|
||||
|
||||
## WARDEN-IN-0002 — Review requested: security layer model v0.3 — and does maturity-engine absorb warden route gaps?
|
||||
|
||||
```yaml
|
||||
id: WARDEN-IN-0002
|
||||
kind: intake
|
||||
title: 'Review requested: security layer model v0.3 — and does maturity-engine absorb
|
||||
warden route gaps?'
|
||||
status: open
|
||||
origin: cross-repo
|
||||
origin_ref: net-kingdom security-layer-model_v0.3
|
||||
priority: medium
|
||||
owner: ops-warden
|
||||
requested_by: gate-house
|
||||
description: 'v0.3 is proposed and changes sections 4, 9 and 13 only; the v0.2 assent
|
||||
record stands. Two new engines: approval-engine (section 9.4) and maturity-engine
|
||||
(section 9.5). THE QUESTION FOR YOU concerns section 5.3, which exists because you
|
||||
offered the amendment. v0.3 gives declared gaps an owner: maturity-engine takes
|
||||
the gap register with intended_owner, blocked_on and review dates, and section 13
|
||||
now says the register in the standard is interim and should not outlive that engine.
|
||||
You offered warden route gaps and the 27 delegation catalog entries as reusable
|
||||
prior art. So the question is whether that machinery should MOVE, be MIRRORED, or
|
||||
STAY. Our tentative reading, which we want tested rather than accepted: routing
|
||||
is yours and stays yours — warden route find answers where a credential need goes,
|
||||
and that is lane knowledge, not maturity. What might move is the readiness half:
|
||||
whether a declared gap is still within its review date, and whether an intended
|
||||
owner has an engine surface yet. If splitting those creates two sources for one
|
||||
fact, that is worse than either option and we would rather hear it now. Your SSH-CA
|
||||
signing write would be tracked in maturity-engine as a declared gap with intended
|
||||
owner secrets-engine and a review date — that is reporting your own non-conformance
|
||||
to an engine, so we would rather you assent to it than discover it. Also note approval-engine
|
||||
(section 9.4): it owns the approval object, not the approval workflow, so ops-warden
|
||||
lanes needing approval consume a claim rather than implementing one. Assent, revision,
|
||||
or rejection acceptable.'
|
||||
created: '2026-08-28T20:40:24.957468Z'
|
||||
updated: '2026-08-28T20:40:24.957468Z'
|
||||
state_hub_intake_id: "01a04d97-94cd-7b49-8019-a91c7fce8adb"
|
||||
```
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"schema_version": "review-contract/v1",
|
||||
"contract_key": "WARDEN-WP-0027-T02-DRILL-20260822-01-INFRA",
|
||||
"subject": {
|
||||
"kind": "task",
|
||||
"id": "WARDEN-WP-0027-T02"
|
||||
},
|
||||
"scenario_id": "WARDEN-WP-0027-T02-DRILL-20260822-01",
|
||||
"expires_at": "2026-08-23T20:00:00Z",
|
||||
"authorizes_execution": false,
|
||||
"evidence_boundary": "metadata_only",
|
||||
"allowed_dispositions": [
|
||||
"approve",
|
||||
"request_changes"
|
||||
],
|
||||
"artifacts": {
|
||||
"docs/evidence/WARDEN-WP-0027-T02-drill-scenario-2026-08-22.md": {
|
||||
"algorithm": "sha256",
|
||||
"digest": "ffa69764ad391db633f57ac70444e5781eee698bf9e362dfef31614fa43152dc"
|
||||
}
|
||||
},
|
||||
"owners": [
|
||||
{
|
||||
"id": "railiance-infra",
|
||||
"artifact_ids": [
|
||||
"docs/evidence/WARDEN-WP-0027-T02-drill-scenario-2026-08-22.md"
|
||||
],
|
||||
"assertions": [
|
||||
{
|
||||
"id": "scenario-and-expiry-bound",
|
||||
"statement": "This receipt applies only to scenario WARDEN-WP-0027-T02-DRILL-20260822-01 and expires at 2026-08-23T20:00:00Z."
|
||||
},
|
||||
{
|
||||
"id": "provider-console-access-explicit",
|
||||
"statement": "railiance-infra has independently verified provider-console access; it is not inferred from SSH, Warden, or cluster reachability."
|
||||
},
|
||||
{
|
||||
"id": "distinct-abort-authority-accepted",
|
||||
"statement": "railiance-infra accepts the distinct abort-authority role for this scenario and its bounded live window."
|
||||
},
|
||||
{
|
||||
"id": "metadata-only-evidence",
|
||||
"statement": "The receipt and its checks disclose no credential, token, recovery share, secret value, value-derived fingerprint, or provider-console detail."
|
||||
},
|
||||
{
|
||||
"id": "execution-not-authorized",
|
||||
"statement": "Approval is owner coordination evidence only and authorizes no console action, reboot, OpenBao seal or unseal, or other live execution."
|
||||
}
|
||||
],
|
||||
"check_ids": [
|
||||
"scenario-artifact-sha256",
|
||||
"provider-console-access-attestation",
|
||||
"distinct-abort-role-attestation",
|
||||
"metadata-only-boundary"
|
||||
]
|
||||
}
|
||||
],
|
||||
"gates": [
|
||||
{
|
||||
"id": "WARDEN-WP-0027-T02-DRILL-20260822-01-INFRA",
|
||||
"policy": "all_required",
|
||||
"owners": [
|
||||
"railiance-infra"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"schema_version": "review-contract/v1",
|
||||
"contract_key": "WARDEN-WP-0027-T02-DRILL-20260822-01-QUORUM",
|
||||
"subject": {
|
||||
"kind": "task",
|
||||
"id": "WARDEN-WP-0027-T02"
|
||||
},
|
||||
"scenario_id": "WARDEN-WP-0027-T02-DRILL-20260822-01",
|
||||
"preparation_decision_id": "9da57559-712a-4521-b46e-a4c69729f9d2",
|
||||
"expires_at": "2026-08-23T20:00:00Z",
|
||||
"authorizes_execution": false,
|
||||
"evidence_boundary": "metadata_only",
|
||||
"allowed_dispositions": [
|
||||
"approve",
|
||||
"request_changes"
|
||||
],
|
||||
"artifacts": {
|
||||
"docs/evidence/WARDEN-WP-0027-T02-drill-scenario-2026-08-22.md": {
|
||||
"algorithm": "sha256",
|
||||
"digest": "ffa69764ad391db633f57ac70444e5781eee698bf9e362dfef31614fa43152dc"
|
||||
},
|
||||
"docs/evidence/WARDEN-WP-0027-T02-drill-preparation-checklist-2026-08-22.md": {
|
||||
"algorithm": "sha256",
|
||||
"digest": "5462b69104d31849cd73c308bb092c16e1a38b7d7e0a48c492a87e8207fdd3fa"
|
||||
}
|
||||
},
|
||||
"owners": [
|
||||
{
|
||||
"id": "railiance-master",
|
||||
"artifact_ids": [
|
||||
"docs/evidence/WARDEN-WP-0027-T02-drill-scenario-2026-08-22.md",
|
||||
"docs/evidence/WARDEN-WP-0027-T02-drill-preparation-checklist-2026-08-22.md"
|
||||
],
|
||||
"assertions": [
|
||||
{
|
||||
"id": "scenario-decision-and-expiry-bound",
|
||||
"statement": "This receipt applies only to scenario WARDEN-WP-0027-T02-DRILL-20260822-01 under preparation decision 9da57559-712a-4521-b46e-a4c69729f9d2 and expires at 2026-08-23T20:00:00Z."
|
||||
},
|
||||
{
|
||||
"id": "two-distinct-custodians-available",
|
||||
"statement": "The custody authority confirms that two distinct custodians for the current 2-of-3 OpenBao Shamir barrier are available for this attended scenario through approved out-of-band custody paths."
|
||||
},
|
||||
{
|
||||
"id": "custody-values-remain-out-of-band",
|
||||
"statement": "Custodian identities, share values, custody locations, and all value-derived fingerprints remain outside Git, State Hub, logs, shell history, and chat."
|
||||
},
|
||||
{
|
||||
"id": "exact-live-scope-reviewed",
|
||||
"statement": "The possible live scope is exactly one intentional OpenBao seal followed by the existing 2-of-3 unseal ceremony; it excludes host reboot, re-key, restore, policy change, PVC mutation, credential disclosure, and general workload restart."
|
||||
},
|
||||
{
|
||||
"id": "execution-not-authorized",
|
||||
"statement": "Approval is quorum-availability evidence only and authorizes no OpenBao seal or unseal, reboot, or other live execution."
|
||||
}
|
||||
],
|
||||
"check_ids": [
|
||||
"scenario-artifact-sha256",
|
||||
"preparation-checklist-sha256",
|
||||
"two-distinct-custodians-availability-attestation",
|
||||
"out-of-band-custody-boundary",
|
||||
"metadata-only-boundary"
|
||||
]
|
||||
}
|
||||
],
|
||||
"gates": [
|
||||
{
|
||||
"id": "WARDEN-WP-0027-T02-DRILL-20260822-01-QUORUM",
|
||||
"policy": "all_required",
|
||||
"owners": [
|
||||
"railiance-master"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"schema_version": "review-contract/v1",
|
||||
"contract_key": "WARDEN-WP-0027-T02-DRILL-20260822-01-PLATFORM",
|
||||
"subject": {
|
||||
"kind": "task",
|
||||
"id": "WARDEN-WP-0027-T02"
|
||||
},
|
||||
"scenario_id": "WARDEN-WP-0027-T02-DRILL-20260822-01",
|
||||
"preparation_decision_id": "9da57559-712a-4521-b46e-a4c69729f9d2",
|
||||
"expires_at": "2026-08-23T20:00:00Z",
|
||||
"authorizes_execution": false,
|
||||
"evidence_boundary": "metadata_only",
|
||||
"allowed_dispositions": [
|
||||
"approve",
|
||||
"request_changes"
|
||||
],
|
||||
"artifacts": {
|
||||
"docs/evidence/WARDEN-WP-0027-T02-drill-scenario-2026-08-22.md": {
|
||||
"algorithm": "sha256",
|
||||
"digest": "ffa69764ad391db633f57ac70444e5781eee698bf9e362dfef31614fa43152dc"
|
||||
},
|
||||
"docs/evidence/WARDEN-WP-0027-T02-drill-preparation-checklist-2026-08-22.md": {
|
||||
"algorithm": "sha256",
|
||||
"digest": "5462b69104d31849cd73c308bb092c16e1a38b7d7e0a48c492a87e8207fdd3fa"
|
||||
}
|
||||
},
|
||||
"owners": [
|
||||
{
|
||||
"id": "railiance-platform",
|
||||
"artifact_ids": [
|
||||
"docs/evidence/WARDEN-WP-0027-T02-drill-scenario-2026-08-22.md",
|
||||
"docs/evidence/WARDEN-WP-0027-T02-drill-preparation-checklist-2026-08-22.md"
|
||||
],
|
||||
"assertions": [
|
||||
{
|
||||
"id": "scenario-decision-and-expiry-bound",
|
||||
"statement": "This receipt applies only to scenario WARDEN-WP-0027-T02-DRILL-20260822-01 under preparation decision 9da57559-712a-4521-b46e-a4c69729f9d2 and expires at 2026-08-23T20:00:00Z."
|
||||
},
|
||||
{
|
||||
"id": "current-snapshot-receipt-valid",
|
||||
"statement": "railiance-platform has created a fresh encrypted, verified, off-host OpenBao Raft snapshot through its approved custody path, and its metadata-only receipt passes the platform validator against the live railiance01 cluster id and a possible applied index."
|
||||
},
|
||||
{
|
||||
"id": "platform-driver-role-accepted",
|
||||
"statement": "railiance-platform accepts the attended snapshot, seal/unseal driver, and value-safe post-unseal verification role for this scenario."
|
||||
},
|
||||
{
|
||||
"id": "exact-live-scope-reviewed",
|
||||
"statement": "The possible live scope is exactly one intentional OpenBao seal followed by the existing 2-of-3 unseal ceremony; it excludes host reboot, re-key, restore, policy change, PVC mutation, credential disclosure, and general workload restart."
|
||||
},
|
||||
{
|
||||
"id": "metadata-only-evidence",
|
||||
"statement": "The receipt and its checks disclose no snapshot data, decryption material, credential, token, recovery share, secret value, custody location, or value-derived fingerprint."
|
||||
},
|
||||
{
|
||||
"id": "execution-not-authorized",
|
||||
"statement": "Approval is preparation evidence only and authorizes no OpenBao seal or unseal, snapshot restore, reboot, or other live execution."
|
||||
}
|
||||
],
|
||||
"check_ids": [
|
||||
"scenario-artifact-sha256",
|
||||
"preparation-checklist-sha256",
|
||||
"openbao-snapshot-receipt-validator",
|
||||
"platform-driver-scope-review",
|
||||
"metadata-only-boundary"
|
||||
]
|
||||
}
|
||||
],
|
||||
"gates": [
|
||||
{
|
||||
"id": "WARDEN-WP-0027-T02-DRILL-20260822-01-PLATFORM",
|
||||
"policy": "all_required",
|
||||
"owners": [
|
||||
"railiance-platform"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
128
layer.yaml
Normal file
128
layer.yaml
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
# ops-warden — NetKingdom security layer declaration
|
||||
#
|
||||
# Framework: net-kingdom/canon/standards/security-layer-model_v0.4.md
|
||||
# Assent: docs/adr/ADR-0010 (ops-warden's own voice, per §11 "who must declare")
|
||||
# Validate: python3 scripts/check_layer_conformance.py
|
||||
#
|
||||
# §11 makes one check mechanical: "every direct Tooling client in a Staff
|
||||
# repository maps to a declared §5.1, §5.2, or §5.3 entry". This file is that
|
||||
# map. It is machine-readable because §5.3 requires it to be — ops-warden
|
||||
# proposed that shape and is implementing it rather than declaring in prose.
|
||||
#
|
||||
# Conformance rule inherited from tenancy.yaml: accuracy, not altitude. A
|
||||
# declared gap is TRACKED NON-CONFORMANCE (§11), never a claim of conformance.
|
||||
|
||||
schema_version: "0.1"
|
||||
framework: netkingdom-security-layer-model
|
||||
standard_version: "0.4"
|
||||
repository: ops-warden
|
||||
layer: staff
|
||||
declared_by: docs/adr/ADR-0010
|
||||
declared_at: "2026-08-29"
|
||||
|
||||
# §6.4 — ops-warden is PEP-shaped (it causes a protected side effect: issuing a
|
||||
# certificate). Its unreachable-engine stance map is published separately, and
|
||||
# asserted equal to shipped behaviour by tests/test_layer_conformance.py.
|
||||
pep_stance: pep-stance.yaml
|
||||
|
||||
# Every direct contact with a Tooling-layer system (§4), one entry each.
|
||||
tooling_contacts:
|
||||
|
||||
- id: ssh-ca-signing-write
|
||||
shape: "5.3" # declared engine gap
|
||||
module: src/warden/vault.py
|
||||
symbol: VaultCA.sign
|
||||
tooling: OpenBao
|
||||
operation: "HTTP POST <addr>/v1/<mount>/sign/<role> with X-Vault-Token"
|
||||
write: true
|
||||
capability: "Sign a short-lived SSH certificate for an adm/agt/atm actor"
|
||||
intended_owner: secrets-engine
|
||||
blocked_on: >-
|
||||
No engine exposes an SSH certificate signing surface. secrets-engine owns
|
||||
credential abstraction, custody and lifecycle, which is the layer this
|
||||
belongs in, but fronts no SSH-CA API today.
|
||||
review: "2026-11-28"
|
||||
note: >-
|
||||
Production-verified and in daily use. This is the one lane ops-warden owns
|
||||
permanently (§4). Signing continues while the gap is open: refusing would
|
||||
remove production host access to close a documentation gap.
|
||||
|
||||
- id: desk-paste-once-provision
|
||||
shape: "5.3"
|
||||
module: src/warden/desk.py
|
||||
symbol: _provision_to_openbao
|
||||
tooling: OpenBao
|
||||
operation: "bao kv put <path> <field>=- (value on stdin, never argv)"
|
||||
write: true
|
||||
capability: "Founder paste-once provisioning of a secret straight into OpenBao"
|
||||
intended_owner: secrets-engine
|
||||
blocked_on: >-
|
||||
No engine exposes an attended provisioning surface for a value the founder
|
||||
holds and no automated path can produce.
|
||||
review: "2026-11-28"
|
||||
note: >-
|
||||
Attended and founder-operated (WP-0029). The value reaches OpenBao without
|
||||
passing through a terminal, an argv, or the audit log.
|
||||
|
||||
- id: taint-metadata-read
|
||||
shape: "5.1" # read-only diagnostic observation
|
||||
module: src/warden/taint.py
|
||||
symbol: fetch_taint_status
|
||||
tooling: OpenBao
|
||||
operation: "bao kv metadata get -format=json <path>"
|
||||
write: false
|
||||
capability: "Report EXPOSED taint (custom_metadata) without reading secret data"
|
||||
intended_owner: secrets-engine
|
||||
blocked_on: >-
|
||||
No engine exposes a disclosure-taint query. Metadata-only by construction —
|
||||
reading the data would be the 2026-07-16 vector this exists to avoid.
|
||||
review: "2026-11-28"
|
||||
|
||||
- id: access-proxy-conduit
|
||||
shape: "5.2" # conduit
|
||||
module: src/warden/proxy.py
|
||||
symbol: proxy_fetch, proxy_attended_login_exec
|
||||
tooling: OpenBao, key-cape
|
||||
operation: "Runs the catalog-declared owner fetch_command as a child process"
|
||||
write: false
|
||||
capability: "warden access --fetch/--exec/--out/--wrap for exec_capable lanes"
|
||||
supplied_authority: none
|
||||
evidence:
|
||||
no_own_credential: src/warden/proxy.py::_caller_env
|
||||
test: tests/test_proxy.py::test_conduit_supplies_no_authority_of_its_own
|
||||
audit: "audit.jsonl records the caller, the lane, and the outcome; never a value"
|
||||
note: >-
|
||||
The §5.2 test is the supplied-authority property: ops-warden presents no
|
||||
credential of its own, cannot widen what the caller could already do, and
|
||||
the action reconstructs as the caller's. Governed by ADR-0002.
|
||||
|
||||
- id: caller-identity-token
|
||||
shape: "5.2"
|
||||
module: src/warden/caller_identity.py
|
||||
symbol: resolve_caller_token
|
||||
tooling: OpenBao
|
||||
operation: "Runs the operator-configured caller_auth command, or reads token env"
|
||||
write: false
|
||||
capability: "Establish the caller's own identity for the pre-sign policy gate"
|
||||
supplied_authority: none
|
||||
detection: voluntary # runs an operator-configured command, so no fixed
|
||||
# argv shape to scan for; declared rather than omitted
|
||||
note: >-
|
||||
Obtains the CALLER's credential by the operator's configured means; adds no
|
||||
authority. Never mints, and never persists what it resolves.
|
||||
|
||||
# Contacts that are deliberately NOT Tooling contacts, recorded so the check is
|
||||
# total rather than silently selective.
|
||||
non_tooling_clients:
|
||||
|
||||
- module: src/warden/policy.py
|
||||
target: access-engine (flex-auth)
|
||||
rationale: "Engine API — §5 permits it; this is the shape §5 prescribes."
|
||||
|
||||
- module: src/warden/worker.py
|
||||
target: state-hub, llm-connect
|
||||
rationale: >-
|
||||
Not catalogued in §4. The layer catalog scopes the security estate, and
|
||||
neither the State Hub nor llm-connect appears in it, so no §5 shape applies
|
||||
on the standard's own terms. Raised with gate-house 2026-08-29 as a scope
|
||||
question rather than resolved unilaterally — see the assessment note.
|
||||
69
pep-stance.yaml
Normal file
69
pep-stance.yaml
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# ops-warden — PEP unreachable-engine stance map
|
||||
#
|
||||
# Framework: net-kingdom/canon/standards/security-layer-model_v0.6.md §6.4, §9.3
|
||||
# Rule of record: docs/adr/ADR-0009
|
||||
# Validate: pytest tests/test_layer_conformance.py -k stance
|
||||
#
|
||||
# §6.4 obligation 3 requires a declared unreachable-engine stance that is total,
|
||||
# scoped per zone, carries no implicit default and no per-call discretion, and is
|
||||
# "published rather than held in code comments". §6.4 further requires every
|
||||
# PEP-shaped consumer to PUBLISH its map so the maps can be inventoried. This
|
||||
# file is ops-warden's, published because a map that lives only in a dataclass
|
||||
# default is not published — it is merely written down.
|
||||
#
|
||||
# The property that makes this worth reading: it is asserted equal to the shipped
|
||||
# default in src/warden/config.py (PolicyConfig.failure_modes) by
|
||||
# tests/test_layer_conformance.py. A published map that may drift from the code
|
||||
# is worse than none, because it invites reliance it cannot support.
|
||||
|
||||
schema_version: "0.1"
|
||||
framework: netkingdom-security-layer-model
|
||||
standard_version: "0.6"
|
||||
repository: ops-warden
|
||||
pep_shape: true
|
||||
declared_by: docs/adr/ADR-0009
|
||||
|
||||
protected_action: "SSH certificate issuance (warden sign / cert_command)"
|
||||
decision_engine: access-engine # flex-auth until the governed rename
|
||||
scope: security-zone # security-zones_v0.1 membership of the TARGET workload
|
||||
|
||||
# Total by construction: every zone in security-zones_v0.1, plus the two
|
||||
# non-zone outcomes. No implicit default — an unlisted value is a config error,
|
||||
# not a permissive fallback.
|
||||
stance:
|
||||
z0-experimental: fail_open
|
||||
z1-operational: fail_open
|
||||
z2-protected: fail_open
|
||||
z2-continuity: fail_open
|
||||
z3-critical: fail_closed
|
||||
unknown: fail_open # versioned build profile (ADR-0009); explicit, never inferred
|
||||
not-applicable: fail_closed
|
||||
|
||||
# What happens when the stance is applied. §6.4 obligation 1 requires a decision
|
||||
# record for a protected side effect; where the engine is unreachable there is no
|
||||
# decision to hold, so ops-warden records the APPLICATION OF THE STANCE instead.
|
||||
# See the assessment note: obligation 1 as written admits no such case.
|
||||
on_apply:
|
||||
recorded_fields:
|
||||
- policy_zone
|
||||
- policy_failure_mode
|
||||
- policy_decision_id # present only where a decision was actually rendered
|
||||
- outcome
|
||||
written_to:
|
||||
- "signatures log (src/warden/ca.py)"
|
||||
- "audit.jsonl (src/warden/audit.py)"
|
||||
never_recorded: "any secret material, any certificate private key"
|
||||
|
||||
# §6.4 obligation 2 — the verdict is never cached. Input claims (zone membership,
|
||||
# compiled from the flex-auth registry snapshot) are cached under their own
|
||||
# freshness rules; the answer is not.
|
||||
verdict_caching: none
|
||||
input_claim_caching: "registry/flex-auth/production_registry_snapshot.json, rebuilt by scripts/build_flex_auth_registry.py"
|
||||
|
||||
# §6.4 obligation 4 — reconstructability, bounded by §9.6. ops-warden's audit
|
||||
# emission on this lane is deliberately non-atomic and therefore ATTRIBUTIVE, not
|
||||
# load-bearing: no control branches on the presence of a signing record
|
||||
# (`warden activity` displays it; nothing gates on it). Registered in §13.
|
||||
reconstructability:
|
||||
bound: "§9.6 attributive — completeness is not claimed"
|
||||
declared_at: wiki/AuditTrail.md
|
||||
|
|
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||
|
||||
[project]
|
||||
name = "ops-warden"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
description = "SSH CA and certificate lifecycle manager for ops actors"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@
|
|||
],
|
||||
"metadata": {
|
||||
"flex_auth_contract": "protected-system-v0",
|
||||
"ops_warden_policy_gate": "v2",
|
||||
"policy_enabled_config": "policy.enabled",
|
||||
"ops_warden_policy_gate": "security-zones-v0.1",
|
||||
"security_zone_standard": "security-zones_v0.1",
|
||||
"tenant": "tenant:platform"
|
||||
}
|
||||
}
|
||||
|
|
@ -66,7 +66,6 @@
|
|||
"ssh-signing",
|
||||
"adm"
|
||||
],
|
||||
"trust_zone": "platform",
|
||||
"owner": "team:platform-security",
|
||||
"attributes": {
|
||||
"actor_id": "adm-example",
|
||||
|
|
@ -78,7 +77,10 @@
|
|||
"allowed_principals": [
|
||||
"adm-full"
|
||||
],
|
||||
"max_ttl_hours": 48
|
||||
"max_ttl_hours": 48,
|
||||
"security_zone": "unknown",
|
||||
"security_zone_admission": "not-applicable",
|
||||
"security_zone_reason": "human operator retains native actor identity"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -88,7 +90,6 @@
|
|||
"ssh-signing",
|
||||
"agt"
|
||||
],
|
||||
"trust_zone": "platform",
|
||||
"owner": "team:platform-security",
|
||||
"attributes": {
|
||||
"actor_id": "agt-codex-interhub-bootstrap",
|
||||
|
|
@ -100,7 +101,11 @@
|
|||
"allowed_principals": [
|
||||
"agt-interhub-bootstrap"
|
||||
],
|
||||
"max_ttl_hours": 2
|
||||
"max_ttl_hours": 2,
|
||||
"workload_id": "codex-interhub-bootstrap",
|
||||
"security_zone": "unknown",
|
||||
"security_zone_admission": "unknown",
|
||||
"security_zone_reason": "workload_resolution_absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -110,7 +115,6 @@
|
|||
"ssh-signing",
|
||||
"agt"
|
||||
],
|
||||
"trust_zone": "platform",
|
||||
"owner": "team:platform-security",
|
||||
"attributes": {
|
||||
"actor_id": "agt-state-hub-bridge",
|
||||
|
|
@ -122,7 +126,11 @@
|
|||
"allowed_principals": [
|
||||
"agt-task-bridge"
|
||||
],
|
||||
"max_ttl_hours": 24
|
||||
"max_ttl_hours": 24,
|
||||
"workload_id": "ops-bridge-tunnel",
|
||||
"security_zone": "unknown",
|
||||
"security_zone_admission": "unknown",
|
||||
"security_zone_reason": "workload_resolution_absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -132,7 +140,6 @@
|
|||
"ssh-signing",
|
||||
"atm"
|
||||
],
|
||||
"trust_zone": "platform",
|
||||
"owner": "team:platform-security",
|
||||
"attributes": {
|
||||
"actor_id": "atm-backup-daily",
|
||||
|
|
@ -144,7 +151,11 @@
|
|||
"allowed_principals": [
|
||||
"atm-backup-daily"
|
||||
],
|
||||
"max_ttl_hours": 8
|
||||
"max_ttl_hours": 8,
|
||||
"workload_id": "backup-daily",
|
||||
"security_zone": "unknown",
|
||||
"security_zone_admission": "unknown",
|
||||
"security_zone_reason": "workload_resolution_absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
|||
109
registry/generated/high-risk-data-paths.yaml
Normal file
109
registry/generated/high-risk-data-paths.yaml
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# GENERATED by scripts/emit_high_risk_paths.py -- do not edit by hand.
|
||||
# Concrete KV data paths for lanes ops-warden grades `risk: high`.
|
||||
#
|
||||
# This is an INPUT, not a policy. ops-warden states which paths it grades
|
||||
# high; railiance-platform owns what agent-high-risk-boundary denies and may
|
||||
# deny more, deny less, or dispute a grade (ADR-0002, ADR-0008).
|
||||
#
|
||||
# Grades cover every field a read of the path discloses, not the field the
|
||||
# lane is named after (ADR-0008). `fields` is recorded where an owning CCR
|
||||
# declares it, and is null where the field set has not been established --
|
||||
# null means unknown, never 'one field'.
|
||||
|
||||
generated_at: "2026-08-31T22:46:47Z"
|
||||
source: ops-warden/registry/routing/catalog.yaml
|
||||
catalog_revision: "4fee839b1138c60642bd6e0210cf8bf541333747"
|
||||
catalog_revision_date: "2026-09-01T00:46:28+02:00"
|
||||
catalog_dirty: false
|
||||
high_risk_lane_count: 24
|
||||
concrete_path_count: 15
|
||||
|
||||
# Graded high but not a single KV address -- a routing pattern, a broker
|
||||
# grant, or a non-KV lane. Nothing here for a policy to deny.
|
||||
no_concrete_path:
|
||||
- database-dynamic-credentials
|
||||
- inter-hub-bootstrap-ssh
|
||||
- net-kingdom-lldap-bind-credential
|
||||
- net-kingdom-privacyidea-admin-token
|
||||
- object-storage-sts
|
||||
- openbao-api-key
|
||||
- openbao-platform-admin-login
|
||||
- openbao-shamir-recovery-ceremony
|
||||
- ops-warden-warden-sign-token
|
||||
|
||||
paths:
|
||||
- id: agent-harness-binky-mail-approle
|
||||
data_path: tenants/data/binky/company-email/imap
|
||||
metadata_path: tenants/metadata/binky/company-email/imap
|
||||
owner_repo: railiance-platform
|
||||
fields: null # field set not established -- unknown, not one
|
||||
- id: agent-harness-forgejo-deploy
|
||||
data_path: platform/data/workloads/agent-harness/forgejo-deploy-key
|
||||
metadata_path: platform/metadata/workloads/agent-harness/forgejo-deploy-key
|
||||
owner_repo: railiance-platform
|
||||
fields: null # field set not established -- unknown, not one
|
||||
- id: audit-core-senders
|
||||
data_path: platform/data/workloads/audit-core/senders
|
||||
metadata_path: platform/metadata/workloads/audit-core/senders
|
||||
owner_repo: ops-mason
|
||||
fields: null # field set not established -- unknown, not one
|
||||
- id: binky-company-email-imap
|
||||
data_path: tenants/data/binky/company-email/imap
|
||||
metadata_path: tenants/metadata/binky/company-email/imap
|
||||
owner_repo: railiance-platform
|
||||
fields: null # field set not established -- unknown, not one
|
||||
- id: binky-qonto-api
|
||||
data_path: tenants/data/binky/qonto-api
|
||||
metadata_path: tenants/metadata/binky/qonto-api
|
||||
owner_repo: railiance-platform
|
||||
fields: null # field set not established -- unknown, not one
|
||||
- id: email-connect-transactional
|
||||
data_path: platform/data/workloads/email-connect/transactional
|
||||
metadata_path: platform/metadata/workloads/email-connect/transactional
|
||||
owner_repo: railiance-platform
|
||||
fields: null # field set not established -- unknown, not one
|
||||
- id: forgejo-admin-api-token
|
||||
data_path: platform/data/workloads/forgejo/forgejo-admin
|
||||
metadata_path: platform/metadata/workloads/forgejo/forgejo-admin
|
||||
owner_repo: railiance-platform
|
||||
fields: null # field set not established -- unknown, not one
|
||||
- id: issue-core-ingestion-api-key
|
||||
data_path: platform/data/workloads/issue-core/issue-core/issue-core-runtime
|
||||
metadata_path: platform/metadata/workloads/issue-core/issue-core/issue-core-runtime
|
||||
owner_repo: railiance-platform
|
||||
fields: [ISSUE_CORE_API_KEY, GITEA_BACKEND_TOKEN]
|
||||
- id: openrouter-llm-connect
|
||||
data_path: platform/data/workloads/activity-core/llm-connect/llm-connect-provider-secrets
|
||||
metadata_path: platform/metadata/workloads/activity-core/llm-connect/llm-connect-provider-secrets
|
||||
owner_repo: railiance-platform
|
||||
fields: null # field set not established -- unknown, not one
|
||||
- id: policy-nexus-forgejo-source-read
|
||||
data_path: platform/data/workloads/policy-nexus/forgejo-source-read
|
||||
metadata_path: platform/metadata/workloads/policy-nexus/forgejo-source-read
|
||||
owner_repo: railiance-platform
|
||||
fields: null # field set not established -- unknown, not one
|
||||
- id: railiance-backup-offsite-lane
|
||||
data_path: platform/data/workloads/railiance/backup/offsite-lane
|
||||
metadata_path: platform/metadata/workloads/railiance/backup/offsite-lane
|
||||
owner_repo: railiance-platform
|
||||
fields: null # field set not established -- unknown, not one
|
||||
- id: rapp-qonto-keycape-client
|
||||
data_path: platform/data/workloads/rapp-qonto/keycape-client
|
||||
metadata_path: platform/metadata/workloads/rapp-qonto/keycape-client
|
||||
owner_repo: key-cape
|
||||
fields: null # field set not established -- unknown, not one
|
||||
- id: reuse-surface-hub-write-token
|
||||
data_path: platform/data/workloads/reuse/reuse-surface/runtime-secrets
|
||||
metadata_path: platform/metadata/workloads/reuse/reuse-surface/runtime-secrets
|
||||
owner_repo: railiance-platform
|
||||
fields: [REUSE_SURFACE_TOKEN, REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET]
|
||||
- id: scaleway-bootstrap
|
||||
data_path: platform/data/workloads/railiance/scaleway/bootstrap
|
||||
metadata_path: platform/metadata/workloads/railiance/scaleway/bootstrap
|
||||
owner_repo: railiance-platform
|
||||
fields: null # field set not established -- unknown, not one
|
||||
- id: whynot-design-npm-publish
|
||||
data_path: platform/data/workloads/coulomb/whynot-design/npm-publish
|
||||
metadata_path: platform/metadata/workloads/coulomb/whynot-design/npm-publish
|
||||
owner_repo: railiance-platform
|
||||
fields: null # field set not established -- unknown, not one
|
||||
|
|
@ -71,3 +71,23 @@ dataclass_floor:
|
|||
lattice:
|
||||
requires_env_posture: prod
|
||||
rule: no-write-down
|
||||
|
||||
# --- Axis C — organization lifecycle posture (WARDEN-WP-0029 T02) --------------
|
||||
# Third axis: fleet lifecycle, distinct from env (dev/test/prod) and maturity
|
||||
# (M0–M3). Answers how aggressive policy relaxations may be for founder-scale
|
||||
# operation. Graduate when any trigger fires; do not overload env/maturity.
|
||||
organization_posture:
|
||||
id: build
|
||||
summary: >
|
||||
One founder-operator, pre-revenue, velocity prioritized. Pragmatic
|
||||
provisioning is acceptable where audit and custody invariants hold
|
||||
(values only in OpenBao/process env; metadata-only trails).
|
||||
relaxations:
|
||||
- workstation_oidc_acceptable
|
||||
- per_repo_deploy_keys
|
||||
- flex_auth_advisory_default
|
||||
- localhost_founder_desk_os_session_trust
|
||||
graduation_triggers:
|
||||
- first_customer_data
|
||||
- first_non_founder_operator
|
||||
- production_tier
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -55,12 +55,79 @@ def _caring_descriptor(actor_type: str, resource_id: str) -> dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
def build_registry(inventory: dict[str, Any]) -> dict[str, Any]:
|
||||
def _resolved_by_workload(zone_resolutions: dict[str, Any] | None) -> dict[str, Any]:
|
||||
records = (zone_resolutions or {}).get("records") or []
|
||||
resolved: dict[str, Any] = {}
|
||||
for record in records:
|
||||
workload_id = str(record.get("workload_id") or "")
|
||||
if not workload_id:
|
||||
continue
|
||||
if workload_id in resolved:
|
||||
raise ValueError(f"duplicate security-zone resolution for {workload_id!r}")
|
||||
resolved[workload_id] = record
|
||||
return resolved
|
||||
|
||||
|
||||
def _zone_attributes(
|
||||
actor: str,
|
||||
entry: dict[str, Any],
|
||||
resolutions: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
subject = entry.get("zone_subject")
|
||||
if not isinstance(subject, dict):
|
||||
return {
|
||||
"security_zone": "unknown",
|
||||
"security_zone_admission": "unknown",
|
||||
"security_zone_reason": "catalog_applicability_absent",
|
||||
}
|
||||
applicability = subject.get("applicability")
|
||||
if applicability == "not-applicable":
|
||||
reason = str(subject.get("reason") or "").strip()
|
||||
if not reason:
|
||||
raise ValueError(f"{actor}.zone_subject.reason is required")
|
||||
return {
|
||||
"security_zone": "unknown",
|
||||
"security_zone_admission": "not-applicable",
|
||||
"security_zone_reason": reason,
|
||||
}
|
||||
if applicability != "applicable":
|
||||
raise ValueError(
|
||||
f"{actor}.zone_subject.applicability must be applicable or not-applicable"
|
||||
)
|
||||
workload_id = str(subject.get("workload_id") or "").strip()
|
||||
if not workload_id:
|
||||
return {
|
||||
"security_zone": "unknown",
|
||||
"security_zone_admission": "unknown",
|
||||
"security_zone_reason": "workload_reference_absent",
|
||||
}
|
||||
record = resolutions.get(workload_id)
|
||||
if record is None:
|
||||
return {
|
||||
"workload_id": workload_id,
|
||||
"security_zone": "unknown",
|
||||
"security_zone_admission": "unknown",
|
||||
"security_zone_reason": "workload_resolution_absent",
|
||||
}
|
||||
return {
|
||||
"workload_id": workload_id,
|
||||
"security_zone": str(record.get("effective_zone") or "unknown"),
|
||||
"security_zone_declared": record.get("declared_zone"),
|
||||
"security_zone_admission": str(record.get("admission") or "unknown"),
|
||||
"security_zone_reason": str(record.get("admission_reason") or "unknown"),
|
||||
"security_zone_revision": record.get("membership_revision"),
|
||||
}
|
||||
|
||||
|
||||
def build_registry(
|
||||
inventory: dict[str, Any], zone_resolutions: dict[str, Any] | None = None
|
||||
) -> dict[str, Any]:
|
||||
actors: dict[str, Any] = inventory.get("actors") or {}
|
||||
resources: list[dict[str, Any]] = []
|
||||
subjects: list[dict[str, Any]] = []
|
||||
groups: dict[str, list[str]] = {gid: [] for gid in GROUP_BY_TYPE.values()}
|
||||
relationships: list[dict[str, Any]] = []
|
||||
resolutions = _resolved_by_workload(zone_resolutions)
|
||||
|
||||
for name, entry in sorted(actors.items()):
|
||||
actor_type = str(entry["type"])
|
||||
|
|
@ -74,7 +141,6 @@ def build_registry(inventory: dict[str, Any]) -> dict[str, Any]:
|
|||
"id": resource_id,
|
||||
"type": "ssh-certificate",
|
||||
"labels": ["ssh-signing", actor_type],
|
||||
"trust_zone": "platform",
|
||||
"owner": "team:platform-security",
|
||||
"attributes": {
|
||||
"actor_id": name,
|
||||
|
|
@ -82,6 +148,7 @@ def build_registry(inventory: dict[str, Any]) -> dict[str, Any]:
|
|||
"allowed_subjects": [name, f"iam:{name}"],
|
||||
"allowed_principals": principals,
|
||||
"max_ttl_hours": ttl_hours,
|
||||
**_zone_attributes(name, entry, resolutions),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
|
@ -156,8 +223,8 @@ def build_registry(inventory: dict[str, Any]) -> dict[str, Any]:
|
|||
"caring_profiles": ["caring-0.4.0-rc2"],
|
||||
"metadata": {
|
||||
"flex_auth_contract": "protected-system-v0",
|
||||
"ops_warden_policy_gate": "v2",
|
||||
"policy_enabled_config": "policy.enabled",
|
||||
"ops_warden_policy_gate": "security-zones-v0.1",
|
||||
"security_zone_standard": "security-zones_v0.1",
|
||||
"tenant": "tenant:platform",
|
||||
},
|
||||
}
|
||||
|
|
@ -186,14 +253,24 @@ def main() -> None:
|
|||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("inventory", type=Path, help="ops-warden inventory.yaml")
|
||||
parser.add_argument("-o", "--output", type=Path, required=True)
|
||||
parser.add_argument(
|
||||
"--zone-resolutions",
|
||||
type=Path,
|
||||
help="zone-engine resolved-view JSON; absent references remain unknown",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
inventory = yaml.safe_load(args.inventory.read_text()) or {}
|
||||
registry = build_registry(inventory)
|
||||
zone_resolutions = (
|
||||
json.loads(args.zone_resolutions.read_text())
|
||||
if args.zone_resolutions is not None
|
||||
else None
|
||||
)
|
||||
registry = build_registry(inventory, zone_resolutions)
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(json.dumps(registry, indent=2) + "\n")
|
||||
print(f"Wrote {args.output} ({len(registry['subjects'])} actors)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
|
|
|||
164
scripts/check_agent_read_boundary.py
Executable file
164
scripts/check_agent_read_boundary.py
Executable file
|
|
@ -0,0 +1,164 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Verify the OpenBao half of the agent read-boundary (ADR-0004, WARDEN-WP-0032-T06).
|
||||
|
||||
`warden access` exits 7 on every `risk: high` lane, but that only protects the
|
||||
ops-warden path. The OpenBao policy `agent-high-risk-boundary` is what protects a
|
||||
direct `bao kv get` -- the actual 2026-07-16 disclosure vector. This script
|
||||
compares the high-risk lanes in the routing catalog against the paths that policy
|
||||
actually denies.
|
||||
|
||||
Read-only and capabilities-only by construction: it reads the *policy document*
|
||||
and lane metadata. It never reads a secret value, and it never mints a token.
|
||||
See `.claude/rules/credential-routing.md` -- verifying a lane with a read is the
|
||||
mistake this whole control exists to prevent.
|
||||
|
||||
Prefers the policy deployed on the server (`bao policy read`); falls back to the
|
||||
file in railiance-platform and says loudly that it did, because deployment drift
|
||||
is exactly what this check exists to catch.
|
||||
|
||||
Exit codes: 0 every high-risk lane with a concrete path is denied; 1 at least one
|
||||
is not; 2 the policy could not be obtained from either source.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
CATALOG = REPO / "registry" / "routing" / "catalog.yaml"
|
||||
POLICY_NAME = "agent-high-risk-boundary"
|
||||
POLICY_FILE = Path.home() / "railiance-platform" / "openbao" / "policies" / f"{POLICY_NAME}.hcl"
|
||||
|
||||
# A path_template with a placeholder is a pattern, not an address -- it names the
|
||||
# shape of a lane rather than one secret, so there is nothing for a policy to deny.
|
||||
PLACEHOLDER = re.compile(r"[<>{}]|\*")
|
||||
|
||||
|
||||
def read_deployed_policy() -> tuple[str | None, str]:
|
||||
"""Return (policy_text, source). Server first, file second, neither third."""
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["bao", "policy", "read", "-format=json", POLICY_NAME],
|
||||
capture_output=True, text=True, timeout=20,
|
||||
)
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
|
||||
server_err = str(exc)
|
||||
else:
|
||||
if proc.returncode == 0:
|
||||
try:
|
||||
return json.loads(proc.stdout)["policy"], "server"
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
return proc.stdout, "server"
|
||||
server_err = (proc.stderr or proc.stdout).strip().splitlines()[:1]
|
||||
server_err = server_err[0] if server_err else f"exit {proc.returncode}"
|
||||
|
||||
if POLICY_FILE.exists():
|
||||
print(f" ! could not read the deployed policy ({server_err})")
|
||||
print(f" ! falling back to the FILE at {POLICY_FILE}")
|
||||
print(" ! deployment drift cannot be detected in this mode\n")
|
||||
return POLICY_FILE.read_text(), "file"
|
||||
return None, f"unavailable ({server_err})"
|
||||
|
||||
|
||||
def denied_data_paths(policy_text: str) -> set[str]:
|
||||
"""Paths the policy denies. Only a `deny` on a KV *data* path is a read-boundary."""
|
||||
denied: set[str] = set()
|
||||
for match in re.finditer(
|
||||
r'path\s+"([^"]+)"\s*\{[^}]*?capabilities\s*=\s*\[([^\]]*)\]',
|
||||
policy_text, re.DOTALL,
|
||||
):
|
||||
path, caps = match.group(1), match.group(2)
|
||||
if "deny" in {c.strip().strip('"\'') for c in caps.split(",")}:
|
||||
denied.add(path)
|
||||
return denied
|
||||
|
||||
|
||||
def to_data_path(path_template: str) -> str | None:
|
||||
"""Catalog path -> KV v2 data path. `<mount>/rest` -> `<mount>/data/rest`."""
|
||||
if PLACEHOLDER.search(path_template) or " " in path_template:
|
||||
return None
|
||||
mount, _, rest = path_template.partition("/")
|
||||
return f"{mount}/data/{rest}" if rest else None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--json", action="store_true", help="machine-readable output")
|
||||
args = parser.parse_args()
|
||||
|
||||
import yaml # local import so --help works without the dep
|
||||
|
||||
entries = yaml.safe_load(CATALOG.read_text())["entries"]
|
||||
high = [e for e in entries if e.get("risk") == "high"]
|
||||
|
||||
policy_text, source = read_deployed_policy()
|
||||
if policy_text is None:
|
||||
print(f"FAIL: policy {POLICY_NAME} could not be obtained from server or file: {source}")
|
||||
print(" Run `bao login -method=oidc`, or check out railiance-platform.")
|
||||
return 2
|
||||
|
||||
denied = denied_data_paths(policy_text)
|
||||
|
||||
covered, uncovered, no_address = [], [], []
|
||||
for entry in high:
|
||||
template = entry.get("path_template")
|
||||
data_path = to_data_path(template) if template else None
|
||||
if data_path is None:
|
||||
no_address.append(entry["id"])
|
||||
elif data_path in denied:
|
||||
covered.append((entry["id"], data_path))
|
||||
else:
|
||||
uncovered.append((entry["id"], data_path))
|
||||
|
||||
if args.json:
|
||||
print(json.dumps({
|
||||
"policy": POLICY_NAME,
|
||||
"policy_source": source,
|
||||
"high_risk_lanes": len(high),
|
||||
"denied_data_paths": sorted(denied),
|
||||
"covered": [{"id": i, "path": p} for i, p in covered],
|
||||
"uncovered": [{"id": i, "path": p} for i, p in uncovered],
|
||||
"no_concrete_address": no_address,
|
||||
"ok": not uncovered,
|
||||
}, indent=2))
|
||||
return 1 if uncovered else 0
|
||||
|
||||
print(f"agent read-boundary — OpenBao half ({POLICY_NAME})\n")
|
||||
print(f" policy source: {source}"
|
||||
+ (" <-- live" if source == "server" else " <-- NOT the deployed policy"))
|
||||
print(f" high-risk lanes: {len(high)}")
|
||||
print(f" denied data paths: {len(denied)}")
|
||||
print(f" covered: {len(covered)}")
|
||||
print(f" NOT covered: {len(uncovered)}")
|
||||
print(f" no concrete address: {len(no_address)}\n")
|
||||
|
||||
if covered:
|
||||
print("COVERED — a direct `bao kv get` is denied for an agent token")
|
||||
for lane_id, path in sorted(covered):
|
||||
print(f" {lane_id:34} {path}")
|
||||
print()
|
||||
if uncovered:
|
||||
print("NOT COVERED — graded high, but the policy does not deny the data path")
|
||||
for lane_id, path in sorted(uncovered):
|
||||
print(f" {lane_id:34} {path}")
|
||||
print()
|
||||
if no_address:
|
||||
print("NO CONCRETE ADDRESS — a pattern or a non-KV lane, nothing to deny")
|
||||
print(" " + ", ".join(sorted(no_address)) + "\n")
|
||||
|
||||
if uncovered:
|
||||
print(f"RESULT: FAIL — {len(uncovered)} high-risk lane(s) outside the OpenBao boundary.")
|
||||
print(" The policy is railiance-platform's; ops-warden reports rather than amends"
|
||||
" (RISK-F-0004).")
|
||||
return 1
|
||||
print("RESULT: PASS — every high-risk lane with a concrete path is denied.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
164
scripts/check_layer_conformance.py
Normal file
164
scripts/check_layer_conformance.py
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Check ops-warden against the NetKingdom security layer model (§5, §11).
|
||||
|
||||
Read-only. Makes §11's second mechanical check real:
|
||||
|
||||
every direct Tooling client in a Staff repository maps to a declared
|
||||
§5.1, §5.2, or §5.3 entry
|
||||
|
||||
The failure this catches is a *new* direct OpenBao contact appearing in
|
||||
src/warden/ without an entry in layer.yaml — an undeclared violation (§11),
|
||||
which is a finding rather than a tracked gap. It deliberately does NOT check
|
||||
the review dates: a date-triggered failure breaks the build on a calendar day
|
||||
with no code change (the reasoning recorded in WARDEN-WP-0033-T05), so
|
||||
staleness is reported and left to `--report`, never to CI.
|
||||
|
||||
Exit 0 clean, 1 undeclared contact found, 2 declaration malformed.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SRC = ROOT / "src" / "warden"
|
||||
DECL = ROOT / "layer.yaml"
|
||||
|
||||
VALID_SHAPES = {"5.1", "5.2", "5.3"}
|
||||
|
||||
# A direct Tooling contact is an *invocation*, not a mention. Matching the word
|
||||
# "bao" caught help text, a docstring, and the dev-tier doubles library that
|
||||
# simulates bao rather than calling it — three false positives on first run.
|
||||
# So match the two shapes that actually execute:
|
||||
# 1. an HTTP request built against the OpenBao address
|
||||
# 2. an argv list whose first element is the bao binary
|
||||
TOOLING_PATTERNS = (
|
||||
# httpx call whose URL is built from the configured OpenBao/Vault address
|
||||
re.compile(r"""\bhttpx\.\w+\(|url\s*=\s*f?["'].*\{self\._cfg\.addr\}"""),
|
||||
# argv construction: [bao_bin, ...] / ["bao", ...] / [bao_binary, ...]
|
||||
re.compile(r"""\[\s*(?:["']bao["']|bao_bin\b|bao_binary\b)\s*,"""),
|
||||
)
|
||||
|
||||
# httpx alone is not a Tooling contact — policy.py calls an Engine and worker.py
|
||||
# calls the State Hub. A module matching only the httpx pattern counts as a
|
||||
# contact only if it also references the OpenBao address configuration.
|
||||
ADDR_HINT = re.compile(r"""_cfg\.addr|VAULT_ADDR|BAO_ADDR""")
|
||||
|
||||
# Modules that talk to an Engine or to something outside the §4 catalog. Listed
|
||||
# in layer.yaml under non_tooling_clients and excluded from the scan with it.
|
||||
def _excluded(decl: dict) -> set[str]:
|
||||
return {e["module"].split("/")[-1] for e in decl.get("non_tooling_clients", [])}
|
||||
|
||||
|
||||
def load_declaration() -> dict:
|
||||
if not DECL.exists():
|
||||
print(f"MISSING: {DECL} — ops-warden must declare in its own voice (§11)")
|
||||
raise SystemExit(2)
|
||||
decl = yaml.safe_load(DECL.read_text())
|
||||
for key in ("layer", "repository", "standard_version", "tooling_contacts"):
|
||||
if key not in decl:
|
||||
print(f"MALFORMED: layer.yaml has no {key!r}")
|
||||
raise SystemExit(2)
|
||||
for c in decl["tooling_contacts"]:
|
||||
if c.get("shape") not in VALID_SHAPES:
|
||||
print(f"MALFORMED: {c.get('id')} has shape {c.get('shape')!r}, not one of {sorted(VALID_SHAPES)}")
|
||||
raise SystemExit(2)
|
||||
# §5.3 carries four fields, machine-readably. That is the whole point of
|
||||
# the shape; a gap missing them is prose wearing a schema.
|
||||
if c["shape"] == "5.3":
|
||||
for field in ("capability", "intended_owner", "blocked_on", "review"):
|
||||
if not c.get(field):
|
||||
print(f"MALFORMED: §5.3 entry {c['id']!r} is missing {field!r}")
|
||||
raise SystemExit(2)
|
||||
# §5.2's test is the supplied-authority property.
|
||||
if c["shape"] == "5.2" and c.get("supplied_authority") != "none":
|
||||
print(f"MALFORMED: §5.2 conduit {c['id']!r} must declare supplied_authority: none")
|
||||
raise SystemExit(2)
|
||||
return decl
|
||||
|
||||
|
||||
def scan_modules() -> dict[str, list[int]]:
|
||||
"""Return {module_name: [line numbers]} for direct Tooling contacts."""
|
||||
found: dict[str, list[int]] = {}
|
||||
for path in sorted(SRC.rglob("*.py")):
|
||||
if path.name.startswith("test_"):
|
||||
continue
|
||||
text = path.read_text()
|
||||
hits: list[int] = []
|
||||
for n, line in enumerate(text.splitlines(), 1):
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("#") or stripped.startswith('"'):
|
||||
continue
|
||||
if any(p.search(line) for p in TOOLING_PATTERNS):
|
||||
hits.append(n)
|
||||
if hits:
|
||||
# An httpx-only match needs the OpenBao address to be a Tooling
|
||||
# contact; otherwise it is an Engine or non-catalogued call.
|
||||
argv_shape = any(TOOLING_PATTERNS[1].search(ln) for ln in text.splitlines())
|
||||
if argv_shape or ADDR_HINT.search(text):
|
||||
found[path.name] = hits
|
||||
return found
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--report", action="store_true", help="also print the declaration and gap review dates")
|
||||
args = ap.parse_args()
|
||||
|
||||
decl = load_declaration()
|
||||
declared = {c["module"].split("/")[-1] for c in decl["tooling_contacts"]}
|
||||
excluded = _excluded(decl)
|
||||
found = scan_modules()
|
||||
|
||||
undeclared = {m: lines for m, lines in found.items() if m not in declared and m not in excluded}
|
||||
# A voluntary declaration has no fixed argv shape to detect (an
|
||||
# operator-configured command). Over-declaring is safe; not reporting it as
|
||||
# stale keeps the signal meaningful.
|
||||
voluntary = {
|
||||
c["module"].split("/")[-1]
|
||||
for c in decl["tooling_contacts"]
|
||||
if c.get("detection") == "voluntary"
|
||||
}
|
||||
stale_decls = declared - set(found) - voluntary
|
||||
|
||||
if args.report:
|
||||
print(f"{decl['repository']} — layer: {decl['layer']} (model v{decl['standard_version']})")
|
||||
print(f"declared by {decl['declared_by']}\n")
|
||||
for c in decl["tooling_contacts"]:
|
||||
line = f" §{c['shape']} {c['id']:<28} {c['module']}"
|
||||
if c["shape"] == "5.3":
|
||||
overdue = str(c["review"]) < date.today().isoformat()
|
||||
line += f" -> {c['intended_owner']} review {c['review']}"
|
||||
if overdue:
|
||||
line += " [REVIEW OVERDUE]"
|
||||
print(line)
|
||||
gaps = [c for c in decl["tooling_contacts"] if c["shape"] == "5.3"]
|
||||
print(f"\n{len(gaps)} declared gap(s) — tracked non-conformance, not conformance (§11).")
|
||||
|
||||
ok = True
|
||||
if undeclared:
|
||||
ok = False
|
||||
print("\nUNDECLARED TOOLING CONTACT — a finding under §11, not a tracked gap:")
|
||||
for m, lines in sorted(undeclared.items()):
|
||||
print(f" src/warden/{m}: line(s) {', '.join(map(str, lines[:6]))}")
|
||||
print("\nAdd a §5.1/§5.2/§5.3 entry to layer.yaml, or route it through an engine.")
|
||||
|
||||
if stale_decls:
|
||||
print("\nNote: declared but no contact found (module removed or refactored?):")
|
||||
for m in sorted(stale_decls):
|
||||
print(f" {m}")
|
||||
|
||||
if ok and not args.report:
|
||||
print(f"PASS — {len(found)} module(s) with Tooling contact, all declared.")
|
||||
elif ok:
|
||||
print("\nPASS — every direct Tooling contact maps to a declared shape.")
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
201
scripts/check_policy_caller_identity.py
Executable file
201
scripts/check_policy_caller_identity.py
Executable file
|
|
@ -0,0 +1,201 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Readiness gate for the zone-aware flex-auth caller identity.
|
||||
|
||||
flex-auth deployed ``flex-auth-ops-warden`` (FLEX-WP-0016) in ``callerAuth.mode:
|
||||
warn``: it authenticates the caller with a Kubernetes TokenReview and binds
|
||||
``resource.system: ops-warden`` to ``system:serviceaccount:ops-warden:ops-warden``,
|
||||
but a caller that sends no ``Authorization`` header only produces a
|
||||
``caller authentication warning`` and is still served. That pin cannot move to
|
||||
``enforce`` until ops-warden's calling side actually presents a token. The
|
||||
former repo-wide ``policy.enabled`` switch is retired by WARDEN-WP-0032.
|
||||
|
||||
This script asserts the calling side *without* flipping anything:
|
||||
|
||||
* warden.yaml loads and ``policy.caller_auth.mode`` is not ``none``,
|
||||
* a caller token can actually be obtained (file / env / command),
|
||||
* (optional, ``--url``) a live ``/v1/check`` against the warn pin returns a
|
||||
decision **and** the response is reached with the header attached.
|
||||
|
||||
Exit 0 = ready to ask flex-auth to enforce, 1 = not ready, 2 = bad input.
|
||||
The token is never printed, logged, or written anywhere — only its length and a
|
||||
truncated SHA-256 fingerprint, which are safe to paste into a handoff message.
|
||||
|
||||
Usage:
|
||||
python scripts/check_policy_caller_identity.py [--config ~/.config/warden/warden.yaml]
|
||||
python scripts/check_policy_caller_identity.py --url http://127.0.0.1:19090
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
_SRC = Path(__file__).resolve().parent.parent / "src"
|
||||
if _SRC.is_dir() and str(_SRC) not in sys.path:
|
||||
sys.path.insert(0, str(_SRC))
|
||||
|
||||
from warden.caller_identity import ( # noqa: E402
|
||||
CallerIdentityError,
|
||||
resolve_caller_token,
|
||||
)
|
||||
from warden.config import ConfigError, load_config # noqa: E402
|
||||
|
||||
Check = Tuple[str, str, str]
|
||||
|
||||
|
||||
def _fingerprint(token: str) -> str:
|
||||
return "sha256:" + hashlib.sha256(token.encode()).hexdigest()[:12]
|
||||
|
||||
|
||||
def run_checks(config_path: Optional[Path], url: Optional[str]) -> List[Check]:
|
||||
checks: List[Check] = []
|
||||
try:
|
||||
cfg = load_config(config_path)
|
||||
except ConfigError as e:
|
||||
return [("fail", "warden.yaml", str(e))]
|
||||
|
||||
policy = cfg.policy
|
||||
checks.append(("ok", "warden.yaml", "loaded; security-zones_v0.1 profile"))
|
||||
|
||||
mode = policy.caller_auth.mode
|
||||
if mode == "none":
|
||||
checks.append(
|
||||
(
|
||||
"fail",
|
||||
"caller_auth.mode",
|
||||
"none — no Authorization header is sent; the flex-auth pin stays in warn",
|
||||
)
|
||||
)
|
||||
return checks
|
||||
checks.append(("ok", "caller_auth.mode", mode))
|
||||
|
||||
try:
|
||||
token = resolve_caller_token(policy.caller_auth)
|
||||
except CallerIdentityError as e:
|
||||
checks.append(("fail", "caller token", str(e)))
|
||||
return checks
|
||||
assert token is not None
|
||||
checks.append(
|
||||
("ok", "caller token", f"obtained, {len(token)} chars, {_fingerprint(token)}")
|
||||
)
|
||||
|
||||
target = url or policy.flex_auth_url
|
||||
if target is None:
|
||||
checks.append(
|
||||
(
|
||||
"skip",
|
||||
"live /v1/check",
|
||||
"policy.flex_auth_url is absent; pass --url to run the live smoke",
|
||||
)
|
||||
)
|
||||
return checks
|
||||
|
||||
import httpx # local import: the offline checks above must not need it
|
||||
|
||||
probe = {
|
||||
"subject": {
|
||||
"id": "agt-state-hub-bridge",
|
||||
"type": "agt",
|
||||
"tenant": policy.tenant,
|
||||
},
|
||||
"action": "sign",
|
||||
"resource": {
|
||||
"id": "ssh-cert:actor/agt-state-hub-bridge",
|
||||
"type": "ssh-certificate",
|
||||
"system": policy.system,
|
||||
"tenant": policy.tenant,
|
||||
},
|
||||
"context": {
|
||||
# A structurally complete context, so a deny means the policy said
|
||||
# no — not that the probe was malformed. What is under test here is
|
||||
# the caller identity, and that is answered by the HTTP status.
|
||||
"actor_name": "agt-state-hub-bridge",
|
||||
"actor_type": "agt",
|
||||
"principals": ["agt-task-bridge"],
|
||||
"ttl_hours": 24,
|
||||
"pubkey_fingerprint": "sha256:" + "0" * 64,
|
||||
"readiness_probe": True,
|
||||
},
|
||||
}
|
||||
try:
|
||||
response = httpx.post(
|
||||
target.rstrip("/") + "/v1/check",
|
||||
json=probe,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
timeout=10.0,
|
||||
)
|
||||
except httpx.RequestError as e:
|
||||
checks.append(("fail", "live /v1/check", f"unreachable at {target}: {e}"))
|
||||
return checks
|
||||
|
||||
if response.status_code == 401:
|
||||
checks.append(
|
||||
(
|
||||
"fail",
|
||||
"live /v1/check",
|
||||
"401 — the token was sent but flex-auth did not accept it "
|
||||
"(check the TokenReview audience and the ServiceAccount binding)",
|
||||
)
|
||||
)
|
||||
elif response.status_code == 403:
|
||||
checks.append(
|
||||
(
|
||||
"fail",
|
||||
"live /v1/check",
|
||||
f"403 — authenticated, but the principal may not represent "
|
||||
f"system {policy.system!r}",
|
||||
)
|
||||
)
|
||||
elif response.status_code >= 400:
|
||||
checks.append(
|
||||
("fail", "live /v1/check", f"HTTP {response.status_code} from {target}")
|
||||
)
|
||||
else:
|
||||
try:
|
||||
decision = response.json()
|
||||
except ValueError:
|
||||
checks.append(("fail", "live /v1/check", "non-JSON decision"))
|
||||
return checks
|
||||
effect = str(decision.get("effect", "?"))
|
||||
decision_id = decision.get("id") or decision.get("request_id") or "?"
|
||||
checks.append(
|
||||
("ok", "live /v1/check", f"HTTP 200, effect={effect}, decision={decision_id}")
|
||||
)
|
||||
return checks
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--config", type=Path, default=None, help="path to warden.yaml")
|
||||
parser.add_argument(
|
||||
"--url",
|
||||
default=None,
|
||||
help="flex-auth base URL to smoke (e.g. a port-forward of the warn pin)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
checks = run_checks(args.config, args.url)
|
||||
glyph = {"ok": "✓", "fail": "✗", "skip": "·"}
|
||||
print("flex-auth caller-identity readiness\n")
|
||||
for status, label, detail in checks:
|
||||
print(f" {glyph[status]} {label}: {detail}")
|
||||
|
||||
failed = [c for c in checks if c[0] == "fail"]
|
||||
if failed:
|
||||
print(
|
||||
f"\nNOT READY — {len(failed)} check(s) failed. "
|
||||
"Do not ask flex-auth to enforce caller authentication."
|
||||
)
|
||||
return 1
|
||||
print(
|
||||
"\nREADY — the calling side presents an identity. Verify "
|
||||
"callerAuth.mode remains enforce on flex-auth-ops-warden after rollout. "
|
||||
"Zone-specific PEP failure modes replace the retired global switches."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
160
scripts/emit_high_risk_paths.py
Executable file
160
scripts/emit_high_risk_paths.py
Executable file
|
|
@ -0,0 +1,160 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Emit the versioned high-risk data-path artifact (WARDEN-WP-0033-T03).
|
||||
|
||||
`railiance-platform` asked for a generated list of concrete high-risk KV data
|
||||
paths to consume, instead of hand-maintaining the deny set in
|
||||
`agent-high-risk-boundary.hcl`. Hand-maintaining it is what let the two lists
|
||||
drift for four lanes without anyone noticing (`RISK-F-0009`).
|
||||
|
||||
**This artifact is an input, not a policy.** It states which paths ops-warden
|
||||
grades high. It does not say what to deny -- railiance-platform owns that, and
|
||||
`ADR-0002` keeps ops-warden a conduit rather than the author of another repo's
|
||||
control. A consumer is free to deny more, deny less, or disagree with a grade.
|
||||
|
||||
Carries the catalog git revision so a consumer can tell exactly what it was
|
||||
derived from, and regenerate or diff against it. Read-only: it reads the catalog
|
||||
and `git`, never OpenBao and never a secret value.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
CATALOG = REPO / "registry" / "routing" / "catalog.yaml"
|
||||
DEFAULT_OUT = REPO / "registry" / "generated" / "high-risk-data-paths.yaml"
|
||||
|
||||
|
||||
def catalog_revision() -> tuple[str, str]:
|
||||
"""(commit, iso-date) of the last change to the catalog. Never guesses."""
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["git", "log", "-1", "--format=%H %cI", "--", str(CATALOG)],
|
||||
cwd=REPO, capture_output=True, text=True, timeout=15, check=True,
|
||||
).stdout.strip()
|
||||
commit, _, date = out.partition(" ")
|
||||
return commit or "unknown", date or "unknown"
|
||||
except (subprocess.SubprocessError, FileNotFoundError):
|
||||
return "unknown", "unknown"
|
||||
|
||||
|
||||
def dirty() -> bool:
|
||||
"""True if the catalog has uncommitted edits -- the revision would be a lie."""
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["git", "status", "--porcelain", "--", str(CATALOG)],
|
||||
cwd=REPO, capture_output=True, text=True, timeout=15, check=True,
|
||||
).stdout.strip()
|
||||
return bool(out)
|
||||
except (subprocess.SubprocessError, FileNotFoundError):
|
||||
return False
|
||||
|
||||
|
||||
def build() -> tuple[str, int]:
|
||||
import yaml
|
||||
|
||||
entries = yaml.safe_load(CATALOG.read_text())["entries"]
|
||||
commit, date = catalog_revision()
|
||||
|
||||
rows, patternish = [], []
|
||||
for entry in sorted(entries, key=lambda e: e["id"]):
|
||||
if entry.get("risk") != "high":
|
||||
continue
|
||||
template = entry.get("path_template")
|
||||
data_path = _to_data_path(template) if template else None
|
||||
if data_path is None:
|
||||
patternish.append(entry["id"])
|
||||
continue
|
||||
rows.append({
|
||||
"id": entry["id"],
|
||||
"data_path": data_path,
|
||||
"metadata_path": data_path.replace("/data/", "/metadata/", 1),
|
||||
"fields": entry.get("fields"),
|
||||
"owner_repo": entry.get("owner_repo"),
|
||||
})
|
||||
|
||||
lines = [
|
||||
"# GENERATED by scripts/emit_high_risk_paths.py -- do not edit by hand.",
|
||||
"# Concrete KV data paths for lanes ops-warden grades `risk: high`.",
|
||||
"#",
|
||||
"# This is an INPUT, not a policy. ops-warden states which paths it grades",
|
||||
"# high; railiance-platform owns what agent-high-risk-boundary denies and may",
|
||||
"# deny more, deny less, or dispute a grade (ADR-0002, ADR-0008).",
|
||||
"#",
|
||||
"# Grades cover every field a read of the path discloses, not the field the",
|
||||
"# lane is named after (ADR-0008). `fields` is recorded where an owning CCR",
|
||||
"# declares it, and is null where the field set has not been established --",
|
||||
"# null means unknown, never 'one field'.",
|
||||
"",
|
||||
f"generated_at: \"{datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')}\"",
|
||||
"source: ops-warden/registry/routing/catalog.yaml",
|
||||
f"catalog_revision: \"{commit}\"",
|
||||
f"catalog_revision_date: \"{date}\"",
|
||||
f"catalog_dirty: {str(dirty()).lower()}",
|
||||
f"high_risk_lane_count: {len([e for e in entries if e.get('risk') == 'high'])}",
|
||||
f"concrete_path_count: {len(rows)}",
|
||||
"",
|
||||
"# Graded high but not a single KV address -- a routing pattern, a broker",
|
||||
"# grant, or a non-KV lane. Nothing here for a policy to deny.",
|
||||
"no_concrete_path:",
|
||||
]
|
||||
lines += [f" - {i}" for i in sorted(patternish)] or [" []"]
|
||||
lines += ["", "paths:"]
|
||||
for row in rows:
|
||||
lines.append(f" - id: {row['id']}")
|
||||
lines.append(f" data_path: {row['data_path']}")
|
||||
lines.append(f" metadata_path: {row['metadata_path']}")
|
||||
lines.append(f" owner_repo: {row['owner_repo']}")
|
||||
if row["fields"]:
|
||||
lines.append(f" fields: [{', '.join(row['fields'])}]")
|
||||
else:
|
||||
lines.append(" fields: null # field set not established -- unknown, not one")
|
||||
return "\n".join(lines) + "\n", len(rows)
|
||||
|
||||
|
||||
def _to_data_path(template: str) -> str | None:
|
||||
import re
|
||||
if re.search(r"[<>{}*]", template) or " " in template or template.startswith("k8s:"):
|
||||
return None
|
||||
mount, _, rest = template.partition("/")
|
||||
return f"{mount}/data/{rest}" if rest else None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=DEFAULT_OUT)
|
||||
parser.add_argument("--check", action="store_true",
|
||||
help="exit 1 if the artifact on disk is stale (for CI)")
|
||||
args = parser.parse_args()
|
||||
|
||||
content, count = build()
|
||||
|
||||
if args.check:
|
||||
current = args.out.read_text() if args.out.exists() else ""
|
||||
# generated_at always differs; compare everything else.
|
||||
def strip_generated_at(text: str) -> str:
|
||||
return "\n".join(
|
||||
line for line in text.splitlines()
|
||||
if not line.startswith("generated_at:")
|
||||
)
|
||||
|
||||
if strip_generated_at(current) != strip_generated_at(content):
|
||||
print(f"STALE: {args.out} does not match the catalog. Re-run without --check.")
|
||||
return 1
|
||||
print(f"fresh: {args.out} matches the catalog ({count} concrete paths)")
|
||||
return 0
|
||||
|
||||
args.out.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.out.write_text(content)
|
||||
print(f"wrote {args.out} — {count} concrete high-risk data paths")
|
||||
if dirty():
|
||||
print(" ! catalog has uncommitted changes; catalog_revision does not describe it")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -66,9 +66,8 @@ ca_key: $SMOKE_DIR/ca_key
|
|||
state_dir: $SMOKE_DIR/state
|
||||
inventory_path: $INVENTORY
|
||||
policy:
|
||||
enabled: true
|
||||
flex_auth_url: http://$ADDR
|
||||
fail_closed: true
|
||||
zone_registry_path: $REGISTRY
|
||||
tenant: tenant:platform
|
||||
system: ops-warden
|
||||
EOF
|
||||
|
|
@ -106,9 +105,8 @@ vault:
|
|||
inventory_path: $INVENTORY
|
||||
state_dir: $SMOKE_DIR/state-vault
|
||||
policy:
|
||||
enabled: true
|
||||
flex_auth_url: http://$ADDR
|
||||
fail_closed: true
|
||||
zone_registry_path: $REGISTRY
|
||||
tenant: tenant:platform
|
||||
system: ops-warden
|
||||
EOF
|
||||
|
|
@ -118,4 +116,4 @@ EOF
|
|||
python3 -c "import json,sys; e=json.loads(sys.argv[1]); assert e.get('backend')=='vault' and e.get('policy_decision_id'); print('vault policy_decision_id:', e['policy_decision_id'])" "$VAULT_LINE"
|
||||
fi
|
||||
|
||||
echo "OK — production registry policy gate smoke passed"
|
||||
echo "OK — production registry policy gate smoke passed"
|
||||
|
|
|
|||
185
scripts/report_workload_join.py
Executable file
185
scripts/report_workload_join.py
Executable file
|
|
@ -0,0 +1,185 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Report explicit lane -> workload resolution for security-zones_v0.1.
|
||||
|
||||
The catalog owner declares whether each lane is workload-applicable. Managed
|
||||
deployables use the exact Repo Manager v1 ``(rapp_id, name, deployable?)``
|
||||
reference. Independently governed operational workloads use ``name`` plus an
|
||||
owner declaration reference. Unknown and not-applicable are explicit results.
|
||||
|
||||
This script never parses a credential path, consults ``owner_repo`` as an
|
||||
identity hint, or substitutes a repository name. It reads declarations only.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def load_rapp_workloads(root: Path) -> dict[tuple[str, str], dict[str, Any]]:
|
||||
"""Exact Repo Manager v1 key -> authoritative declaration projection."""
|
||||
out: dict[tuple[str, str], dict[str, Any]] = {}
|
||||
for decl in sorted(root.glob("rapp-*/declarations/rapp.yaml")):
|
||||
try:
|
||||
data = yaml.safe_load(decl.read_text()) or {}
|
||||
except yaml.YAMLError:
|
||||
continue
|
||||
identity = data.get("workload_identity") or {}
|
||||
rapp_id = data.get("rapp_id")
|
||||
name = identity.get("name")
|
||||
if not rapp_id or not name:
|
||||
continue
|
||||
deployables = []
|
||||
for member in (data.get("composition") or {}).get("member_repos") or []:
|
||||
deployables.extend(str(value) for value in member.get("deployables") or [])
|
||||
out[(str(rapp_id), str(name))] = {
|
||||
"source": str(decl),
|
||||
"deployables": sorted(set(deployables)),
|
||||
"data_classification": data.get("data_classification"),
|
||||
"criticality": data.get("criticality"),
|
||||
"readiness_state": data.get("readiness_state"),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def _direct_declaration_path(
|
||||
declaration_ref: str, *, catalog_path: Path, estate_root: Path
|
||||
) -> Path:
|
||||
ref = Path(declaration_ref)
|
||||
if ref.is_absolute():
|
||||
return ref
|
||||
local = catalog_path.resolve().parents[2] / ref
|
||||
return local if local.exists() else estate_root / ref
|
||||
|
||||
|
||||
def _resolve_direct(
|
||||
ref: dict[str, Any], *, catalog_path: Path, estate_root: Path
|
||||
) -> tuple[dict[str, Any] | None, str | None]:
|
||||
source = _direct_declaration_path(
|
||||
str(ref["declaration_ref"]), catalog_path=catalog_path, estate_root=estate_root
|
||||
)
|
||||
if not source.exists():
|
||||
return None, f"declaration not found: {source}"
|
||||
try:
|
||||
declaration = yaml.safe_load(source.read_text()) or {}
|
||||
except yaml.YAMLError as exc:
|
||||
return None, f"invalid declaration YAML: {exc}"
|
||||
identity = declaration.get("workload_identity") or {}
|
||||
if identity.get("name") != ref.get("name"):
|
||||
return None, (
|
||||
f"declared workload_identity.name={identity.get('name')!r}, "
|
||||
f"expected {ref.get('name')!r}"
|
||||
)
|
||||
context = (declaration.get("zones") or {}).get("context", {})
|
||||
return {
|
||||
"source": str(source),
|
||||
"data_classification": context.get("data_classification"),
|
||||
"criticality": context.get("criticality"),
|
||||
"maturity": context.get("maturity"),
|
||||
"declared_zone": (declaration.get("zones") or {}).get("membership"),
|
||||
}, None
|
||||
|
||||
|
||||
def build(catalog_path: Path, estate_root: Path) -> dict[str, Any]:
|
||||
entries = (yaml.safe_load(catalog_path.read_text()) or {}).get("entries", [])
|
||||
managed = load_rapp_workloads(estate_root)
|
||||
posture_path = catalog_path.parent.parent / "policy" / "security-posture.yaml"
|
||||
floor = (yaml.safe_load(posture_path.read_text()) or {}).get("dataclass_floor", {})
|
||||
|
||||
resolved: list[dict[str, Any]] = []
|
||||
unknown: list[dict[str, Any]] = []
|
||||
not_applicable: list[dict[str, Any]] = []
|
||||
for entry in entries:
|
||||
lane = str(entry.get("id"))
|
||||
ref = entry.get("workload_ref") or {}
|
||||
applicability = ref.get("applicability")
|
||||
if applicability == "not-applicable":
|
||||
not_applicable.append({"lane": lane, "reason": ref.get("reason")})
|
||||
continue
|
||||
if applicability != "applicable":
|
||||
unknown.append({"lane": lane, "reason": "applicability missing or invalid"})
|
||||
continue
|
||||
if ref.get("unknown_reason"):
|
||||
unknown.append({"lane": lane, "reason": ref["unknown_reason"]})
|
||||
continue
|
||||
|
||||
projection: dict[str, Any] | None
|
||||
error: str | None = None
|
||||
if ref.get("rapp_id"):
|
||||
key = (str(ref.get("rapp_id")), str(ref.get("name")))
|
||||
projection = managed.get(key)
|
||||
if projection is None:
|
||||
error = f"Repo Manager reference does not resolve: {key[0]}/{key[1]}"
|
||||
elif ref.get("deployable") and ref["deployable"] not in projection["deployables"]:
|
||||
error = f"deployable {ref['deployable']!r} is not declared by {key[0]}/{key[1]}"
|
||||
else:
|
||||
projection, error = _resolve_direct(
|
||||
ref, catalog_path=catalog_path, estate_root=estate_root
|
||||
)
|
||||
if error or projection is None:
|
||||
unknown.append({"lane": lane, "reason": error or "reference unresolved"})
|
||||
continue
|
||||
|
||||
classification = projection.get("data_classification")
|
||||
resolved.append(
|
||||
{
|
||||
"lane": lane,
|
||||
"workload_ref": ref,
|
||||
"source": projection.get("source"),
|
||||
"data_classification": classification,
|
||||
"criticality": projection.get("criticality"),
|
||||
"maturity": projection.get("maturity") or floor.get(classification),
|
||||
"declared_zone": projection.get("declared_zone"),
|
||||
"unmapped_classification": bool(classification) and classification not in floor,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"contract": "helixforge.workload-reference/v1",
|
||||
"resolved": resolved,
|
||||
"unknown": unknown,
|
||||
"not_applicable": not_applicable,
|
||||
"ok": len(resolved) + len(unknown) + len(not_applicable) == len(entries),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--estate-root", "--rapp-root", dest="estate_root", type=Path, default=Path.home()
|
||||
)
|
||||
parser.add_argument("--json", action="store_true")
|
||||
parser.add_argument(
|
||||
"--catalog",
|
||||
type=Path,
|
||||
default=Path(__file__).resolve().parent.parent
|
||||
/ "registry"
|
||||
/ "routing"
|
||||
/ "catalog.yaml",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
report = build(args.catalog, args.estate_root)
|
||||
if args.json:
|
||||
print(json.dumps(report, indent=2, sort_keys=True))
|
||||
return 0 if report["ok"] else 1
|
||||
|
||||
print("explicit lane -> workload resolution\n")
|
||||
print(f" resolved: {len(report['resolved'])}")
|
||||
print(f" unknown: {len(report['unknown'])}")
|
||||
print(f" not-applicable: {len(report['not_applicable'])}\n")
|
||||
for row in report["resolved"]:
|
||||
ref = row["workload_ref"]
|
||||
prefix = f"{ref.get('rapp_id')}/" if ref.get("rapp_id") else ""
|
||||
print(f"RESOLVED {row['lane']:34} -> {prefix}{ref.get('name')}")
|
||||
for row in report["unknown"]:
|
||||
print(f"UNKNOWN {row['lane']:34} {row['reason']}")
|
||||
for row in report["not_applicable"]:
|
||||
print(f"NOT-APPLICABLE {row['lane']:34} {row['reason']}")
|
||||
return 0 if report["ok"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -71,6 +71,6 @@ def policy_gate_status() -> str:
|
|||
cfg = load_config()
|
||||
except ConfigError:
|
||||
return "advisory — no warden.yaml (caller identity; gate not enforced)"
|
||||
if cfg.policy.enabled:
|
||||
return f"enforced — flex-auth at {cfg.policy.flex_auth_url}"
|
||||
return "advisory — policy.enabled=false (gate ships with flex-auth deploy)"
|
||||
if cfg.policy.flex_auth_url:
|
||||
return f"zone-aware — flex-auth at {cfg.policy.flex_auth_url}"
|
||||
return "zone-aware — evaluator unconfigured; unknown-zone fail_open applies"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import os
|
|||
import re
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
_AUDIT_FILENAME = "audit.jsonl"
|
||||
_MAX_BYTES = 5 * 1024 * 1024
|
||||
|
|
@ -215,7 +215,6 @@ def collect_activity(
|
|||
since = datetime.now(timezone.utc) - timedelta(days=days)
|
||||
events = read_events(state_dir, since=since, kinds=kinds)
|
||||
if include_legacy:
|
||||
legacy_kinds = kinds or {"sign", "access", "worker"}
|
||||
if not kinds or "sign" in kinds:
|
||||
events.extend(_legacy_sign_events(state_dir, since))
|
||||
if not kinds or "access" in kinds:
|
||||
|
|
@ -278,4 +277,4 @@ def fetch_hub_notes(*, days: int = 7, hub_url: Optional[str] = None) -> list[dic
|
|||
"summary": summary,
|
||||
}
|
||||
)
|
||||
return notes
|
||||
return notes
|
||||
|
|
|
|||
|
|
@ -58,6 +58,12 @@ def _append_signature_log(
|
|||
}
|
||||
if spec.policy_decision_id:
|
||||
entry["policy_decision_id"] = spec.policy_decision_id
|
||||
if spec.policy_zone:
|
||||
entry["policy_zone"] = spec.policy_zone
|
||||
if spec.policy_failure_mode:
|
||||
entry["policy_failure_mode"] = spec.policy_failure_mode
|
||||
if spec.policy_outcome:
|
||||
entry["policy_outcome"] = spec.policy_outcome
|
||||
state_dir.mkdir(parents=True, exist_ok=True)
|
||||
with (state_dir / "signatures.log").open("a") as f:
|
||||
f.write(json.dumps(entry) + "\n")
|
||||
|
|
@ -76,6 +82,9 @@ def _append_signature_log(
|
|||
actor_type=spec.actor_type.value,
|
||||
backend=backend,
|
||||
ttl_hours=spec.ttl_hours,
|
||||
policy_zone=spec.policy_zone,
|
||||
policy_failure_mode=spec.policy_failure_mode,
|
||||
policy_outcome=spec.policy_outcome,
|
||||
)
|
||||
except Exception:
|
||||
pass # audit must not block signing
|
||||
|
|
|
|||
92
src/warden/caller_identity.py
Normal file
92
src/warden/caller_identity.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
"""Caller identity for ops-warden's outbound flex-auth policy calls.
|
||||
|
||||
flex-auth's `flex-auth-ops-warden` pin (FLEX-WP-0016) authenticates the *caller*
|
||||
before it evaluates the request: `Authorization: Bearer <token>` is passed to a
|
||||
Kubernetes TokenReview, and `resource.system: ops-warden` is bound to the
|
||||
principal `system:serviceaccount:ops-warden:ops-warden`. Until ops-warden sends
|
||||
that header, the pin logs `caller authentication warning` and can only run in
|
||||
`warn` mode — which is why enforcing caller authentication is a separate gate.
|
||||
|
||||
This module resolves the token at call time and hands it straight to the request.
|
||||
Nothing is cached to disk, logged, or echoed: ops-warden carries the value, it
|
||||
does not hold it (ADR-0002).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from warden.config import CallerAuthConfig
|
||||
|
||||
|
||||
class CallerIdentityError(Exception):
|
||||
"""Raised when a caller token was configured but could not be obtained."""
|
||||
|
||||
|
||||
def resolve_caller_token(cfg: CallerAuthConfig) -> str | None:
|
||||
"""Return the bearer token for flex-auth, or None when mode is ``none``.
|
||||
|
||||
Raises CallerIdentityError when a token was configured but is unavailable.
|
||||
The token itself never appears in an exception message.
|
||||
"""
|
||||
mode = cfg.mode
|
||||
if mode == "none":
|
||||
return None
|
||||
|
||||
if mode == "file":
|
||||
if cfg.token_path is None:
|
||||
raise CallerIdentityError("caller_auth mode 'file' has no token_path")
|
||||
try:
|
||||
token = cfg.token_path.read_text()
|
||||
except OSError as e:
|
||||
raise CallerIdentityError(
|
||||
f"caller token file unreadable: {cfg.token_path} ({e.strerror})"
|
||||
) from e
|
||||
elif mode == "env":
|
||||
token = os.environ.get(cfg.token_env, "")
|
||||
if not token.strip():
|
||||
raise CallerIdentityError(
|
||||
f"caller token env {cfg.token_env} is unset or empty"
|
||||
)
|
||||
elif mode == "command":
|
||||
if not cfg.command:
|
||||
raise CallerIdentityError("caller_auth mode 'command' has no command")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cfg.command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
check=False,
|
||||
)
|
||||
except FileNotFoundError as e:
|
||||
raise CallerIdentityError(
|
||||
f"caller token command not found: {cfg.command[0]}"
|
||||
) from e
|
||||
except subprocess.TimeoutExpired as e:
|
||||
raise CallerIdentityError("caller token command timed out") from e
|
||||
if result.returncode != 0:
|
||||
stderr = (result.stderr or "").strip().splitlines()
|
||||
detail = stderr[-1] if stderr else f"exit {result.returncode}"
|
||||
raise CallerIdentityError(f"caller token command failed: {detail}")
|
||||
token = result.stdout
|
||||
else:
|
||||
raise CallerIdentityError(f"unsupported caller_auth mode {mode!r}")
|
||||
|
||||
token = token.strip()
|
||||
if not token:
|
||||
raise CallerIdentityError(f"caller_auth mode {mode!r} produced an empty token")
|
||||
if any(ch.isspace() for ch in token):
|
||||
# flex-auth rejects a bearer token containing whitespace outright.
|
||||
raise CallerIdentityError(
|
||||
f"caller_auth mode {mode!r} produced a token containing whitespace"
|
||||
)
|
||||
return token
|
||||
|
||||
|
||||
def caller_auth_headers(cfg: CallerAuthConfig) -> dict[str, str]:
|
||||
"""Headers to attach to a flex-auth /v1/check call ({} when unauthenticated)."""
|
||||
token = resolve_caller_token(cfg)
|
||||
if token is None:
|
||||
return {}
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -2,9 +2,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shlex
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
|
|
@ -13,14 +14,54 @@ class ConfigError(Exception):
|
|||
"""Raised when config is invalid or missing."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class CallerAuthConfig:
|
||||
"""How ops-warden proves *its own* identity to flex-auth (FLEX-WP-0016).
|
||||
|
||||
flex-auth's ops-warden pin authenticates the caller with a Kubernetes
|
||||
TokenReview and binds ``resource.system: ops-warden`` to the principal
|
||||
``system:serviceaccount:ops-warden:ops-warden``. A workstation ``warden
|
||||
sign`` is not a ServiceAccount, so the token has to come from somewhere:
|
||||
|
||||
``none`` send no ``Authorization`` header (pre-FLEX-WP-0016 behaviour;
|
||||
accepted only while that pin runs ``callerAuth.mode: warn``)
|
||||
``file`` read a projected ServiceAccount token from ``token_path``
|
||||
(in-cluster PEP, audience-bound by the projection)
|
||||
``env`` read the token from ``token_env``
|
||||
``command`` run ``command`` and use its stdout, e.g.
|
||||
``kubectl create token ops-warden -n ops-warden
|
||||
--audience flex-auth --duration 10m``
|
||||
|
||||
ops-warden never stores the token: it is read, sent, and dropped
|
||||
(ADR-0002 — transparent conduit, not a broker).
|
||||
"""
|
||||
|
||||
mode: str = "none"
|
||||
token_path: Optional[Path] = None
|
||||
token_env: str = "WARDEN_POLICY_CALLER_TOKEN"
|
||||
command: Optional[List[str]] = None
|
||||
audience: str = "flex-auth"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PolicyConfig:
|
||||
enabled: bool = False
|
||||
flex_auth_url: str = "http://127.0.0.1:8080"
|
||||
fail_closed: bool = True
|
||||
flex_auth_url: Optional[str] = None
|
||||
zone_registry_path: Optional[Path] = None
|
||||
failure_modes: Dict[str, str] = field(
|
||||
default_factory=lambda: {
|
||||
"z0-experimental": "fail_open",
|
||||
"z1-operational": "fail_open",
|
||||
"z2-protected": "fail_open",
|
||||
"z2-continuity": "fail_open",
|
||||
"z3-critical": "fail_closed",
|
||||
"unknown": "fail_open",
|
||||
"not-applicable": "fail_closed",
|
||||
}
|
||||
)
|
||||
tenant: str = "tenant:platform"
|
||||
subject_env: str = "WARDEN_POLICY_SUBJECT"
|
||||
system: str = "ops-warden"
|
||||
caller_auth: "CallerAuthConfig" = field(default_factory=lambda: CallerAuthConfig())
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -117,13 +158,71 @@ def load_config(path: Optional[Path] = None) -> WardenConfig:
|
|||
)
|
||||
|
||||
policy_raw = raw.get("policy") or {}
|
||||
retired = sorted({"enabled", "fail_closed"}.intersection(policy_raw))
|
||||
if retired:
|
||||
raise ConfigError(
|
||||
"retired policy setting(s) "
|
||||
+ ", ".join(f"policy.{key}" for key in retired)
|
||||
+ "; security-zones_v0.1 now selects stance and failure mode"
|
||||
)
|
||||
caller_raw = policy_raw.get("caller_auth") or {}
|
||||
caller_command = caller_raw.get("command")
|
||||
if isinstance(caller_command, str):
|
||||
caller_command = shlex.split(caller_command)
|
||||
elif caller_command is not None:
|
||||
caller_command = [str(part) for part in caller_command]
|
||||
caller_token_path = caller_raw.get("token_path")
|
||||
caller_cfg = CallerAuthConfig(
|
||||
mode=str(caller_raw.get("mode", "none")).strip().lower(),
|
||||
token_path=(
|
||||
Path(os.path.expanduser(str(caller_token_path)))
|
||||
if caller_token_path
|
||||
else None
|
||||
),
|
||||
token_env=str(caller_raw.get("token_env", "WARDEN_POLICY_CALLER_TOKEN")),
|
||||
command=caller_command,
|
||||
audience=str(caller_raw.get("audience", "flex-auth")),
|
||||
)
|
||||
if caller_cfg.mode not in {"none", "file", "env", "command"}:
|
||||
raise ConfigError(
|
||||
f"policy.caller_auth.mode must be none|file|env|command, "
|
||||
f"got {caller_cfg.mode!r}"
|
||||
)
|
||||
if caller_cfg.mode == "file" and caller_cfg.token_path is None:
|
||||
raise ConfigError("policy.caller_auth.token_path is required for mode: file")
|
||||
if caller_cfg.mode == "command" and not caller_cfg.command:
|
||||
raise ConfigError("policy.caller_auth.command is required for mode: command")
|
||||
failure_modes = PolicyConfig().failure_modes
|
||||
configured_failure_modes = policy_raw.get("failure_modes") or {}
|
||||
if not isinstance(configured_failure_modes, dict):
|
||||
raise ConfigError("policy.failure_modes must be a mapping")
|
||||
failure_modes.update(
|
||||
{str(zone): str(mode) for zone, mode in configured_failure_modes.items()}
|
||||
)
|
||||
invalid_modes = {
|
||||
zone: mode
|
||||
for zone, mode in failure_modes.items()
|
||||
if mode not in {"fail_open", "fail_closed"}
|
||||
}
|
||||
if invalid_modes:
|
||||
raise ConfigError(
|
||||
"policy.failure_modes values must be fail_open or fail_closed: "
|
||||
f"{invalid_modes}"
|
||||
)
|
||||
zone_registry_path = policy_raw.get("zone_registry_path")
|
||||
flex_auth_url = str(policy_raw.get("flex_auth_url", "")).strip() or None
|
||||
policy_cfg = PolicyConfig(
|
||||
enabled=bool(policy_raw.get("enabled", False)),
|
||||
flex_auth_url=str(policy_raw.get("flex_auth_url", "http://127.0.0.1:8080")),
|
||||
fail_closed=bool(policy_raw.get("fail_closed", True)),
|
||||
flex_auth_url=flex_auth_url,
|
||||
zone_registry_path=(
|
||||
Path(os.path.expanduser(str(zone_registry_path)))
|
||||
if zone_registry_path
|
||||
else None
|
||||
),
|
||||
failure_modes=failure_modes,
|
||||
tenant=str(policy_raw.get("tenant", "tenant:platform")),
|
||||
subject_env=str(policy_raw.get("subject_env", "WARDEN_POLICY_SUBJECT")),
|
||||
system=str(policy_raw.get("system", "ops-warden")),
|
||||
caller_auth=caller_cfg,
|
||||
)
|
||||
|
||||
return WardenConfig(
|
||||
|
|
|
|||
397
src/warden/desk.py
Normal file
397
src/warden/desk.py
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
"""Founder interaction surface — ``warden desk`` (WARDEN-WP-0029 T03).
|
||||
|
||||
Build-phase localhost page for the rare founder acts emitted by ``warden plan``:
|
||||
approve/deny, OIDC login launch, paste-once provision into OpenBao.
|
||||
|
||||
Pattern: stdlib ``ThreadingHTTPServer`` on 127.0.0.1 only (see net-kingdom
|
||||
security-bootstrap-console). No multi-user auth; OS session is trust boundary.
|
||||
Never logs secret values.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
import json
|
||||
import secrets
|
||||
import subprocess
|
||||
import threading
|
||||
import webbrowser
|
||||
from dataclasses import dataclass, field as dc_field
|
||||
from http import HTTPStatus
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
# Acts the desk can render. Keep in sync with plan.FOUNDER_ACT_KINDS.
|
||||
DESK_ACTS = ("approve", "oidc_login", "paste_once_provision")
|
||||
|
||||
|
||||
class DeskError(Exception):
|
||||
"""Raised when desk session setup fails."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeskSession:
|
||||
"""In-memory founder act session — metadata only (no secret values stored)."""
|
||||
|
||||
token: str
|
||||
act: str
|
||||
summary: str
|
||||
lane_id: str = ""
|
||||
path: str = ""
|
||||
kv_field: str = "value"
|
||||
oidc_command: str = ""
|
||||
result: str = "pending" # pending | approved | denied | provisioned | launched | error
|
||||
message: str = ""
|
||||
extra: Dict[str, Any] = dc_field(default_factory=dict)
|
||||
|
||||
|
||||
def new_session(
|
||||
*,
|
||||
act: str,
|
||||
summary: str,
|
||||
lane_id: str = "",
|
||||
path: str = "",
|
||||
kv_field: str = "value",
|
||||
oidc_command: str = "",
|
||||
extra: Optional[dict] = None,
|
||||
) -> DeskSession:
|
||||
if act not in DESK_ACTS:
|
||||
raise DeskError(f"unknown desk act {act!r}; expected one of {DESK_ACTS}")
|
||||
if act == "paste_once_provision" and not path:
|
||||
raise DeskError("paste_once_provision requires --path (concrete OpenBao path)")
|
||||
return DeskSession(
|
||||
token=secrets.token_urlsafe(24),
|
||||
act=act,
|
||||
summary=summary,
|
||||
lane_id=lane_id,
|
||||
path=path,
|
||||
kv_field=kv_field or "value",
|
||||
oidc_command=oidc_command,
|
||||
extra=dict(extra or {}),
|
||||
)
|
||||
|
||||
|
||||
def session_from_plan_dict(plan: dict) -> DeskSession:
|
||||
"""Build a desk session from a ``warden plan --json`` payload."""
|
||||
act_raw = plan.get("founder_act") or {}
|
||||
if not act_raw or plan.get("verdict") != "founder_required":
|
||||
raise DeskError(
|
||||
"plan verdict is not founder_required or founder_act is missing — "
|
||||
"nothing for the desk to render"
|
||||
)
|
||||
kind = str(act_raw.get("kind") or "")
|
||||
details = act_raw.get("details") or {}
|
||||
path = str(details.get("path_template") or details.get("path") or "")
|
||||
if kind == "paste_once_provision" and ("<" in path or ">" in path):
|
||||
raise DeskError(
|
||||
f"path_template still has placeholders ({path!r}); pass a concrete "
|
||||
"--path to warden desk"
|
||||
)
|
||||
return new_session(
|
||||
act=kind,
|
||||
summary=str(act_raw.get("summary") or plan.get("need") or "founder act"),
|
||||
lane_id=str(details.get("lane_id") or plan.get("lane_id") or ""),
|
||||
path=path if "<" not in path else "",
|
||||
oidc_command=str(details.get("fetch_command") or ""),
|
||||
extra={"need": plan.get("need"), "organization_posture": plan.get("organization_posture")},
|
||||
)
|
||||
|
||||
|
||||
def _page(title: str, body: str) -> bytes:
|
||||
doc = f"""<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{html.escape(title)}</title>
|
||||
<style>
|
||||
:root {{ color-scheme: light dark; --ink: #1d2733; --muted: #536271;
|
||||
--line: #d9e0e7; --paper: #f8fafc; --accent: #2066a8; --ok: #1a7f37; --bad: #b42318; }}
|
||||
body {{ margin: 0; font-family: ui-sans-serif, system-ui, sans-serif; color: var(--ink);
|
||||
background: var(--paper); min-height: 100vh; display: grid; place-items: center; padding: 24px; }}
|
||||
main {{ width: min(100%, 560px); background: #fff; border: 1px solid var(--line);
|
||||
border-radius: 8px; padding: 28px 32px; box-shadow: 0 12px 32px rgba(29,39,51,.08); }}
|
||||
h1 {{ margin: 0 0 8px; font-size: 1.35rem; }}
|
||||
p, li {{ color: var(--muted); line-height: 1.55; }}
|
||||
.eyebrow {{ text-transform: uppercase; font-size: .75rem; font-weight: 700;
|
||||
color: var(--accent); letter-spacing: .04em; margin: 0 0 12px; }}
|
||||
.box {{ background: var(--paper); border: 1px solid var(--line); border-radius: 6px;
|
||||
padding: 12px 14px; margin: 16px 0; font-family: ui-monospace, monospace; font-size: .85rem;
|
||||
word-break: break-all; }}
|
||||
form {{ display: grid; gap: 12px; margin-top: 18px; }}
|
||||
textarea {{ width: 100%; min-height: 96px; font-family: ui-monospace, monospace;
|
||||
padding: 10px; border-radius: 6px; border: 1px solid var(--line); }}
|
||||
.actions {{ display: flex; flex-wrap: wrap; gap: 10px; }}
|
||||
button, .btn {{ appearance: none; border: 0; border-radius: 6px; padding: 10px 16px;
|
||||
font-weight: 700; cursor: pointer; text-decoration: none; display: inline-flex;
|
||||
align-items: center; background: var(--accent); color: #fff; }}
|
||||
button.secondary, .btn.secondary {{ background: #e8eef4; color: var(--ink); }}
|
||||
button.danger {{ background: var(--bad); }}
|
||||
.ok {{ color: var(--ok); font-weight: 700; }}
|
||||
.bad {{ color: var(--bad); font-weight: 700; }}
|
||||
.note {{ font-size: .85rem; margin-top: 18px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<p class="eyebrow">warden desk · build phase · localhost only</p>
|
||||
{body}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return doc.encode("utf-8")
|
||||
|
||||
|
||||
def _render_home(session: DeskSession) -> bytes:
|
||||
summary = html.escape(session.summary)
|
||||
lane = html.escape(session.lane_id or "—")
|
||||
act = html.escape(session.act)
|
||||
if session.result != "pending":
|
||||
cls = "ok" if session.result in ("approved", "provisioned", "launched") else "bad"
|
||||
return _page(
|
||||
"Desk result",
|
||||
f"<h1>Act complete</h1>"
|
||||
f"<p class='{cls}'>Result: {html.escape(session.result)}</p>"
|
||||
f"<p>{html.escape(session.message or '')}</p>"
|
||||
f"<p class='note'>You can close this tab. Server will shut down shortly.</p>",
|
||||
)
|
||||
|
||||
if session.act == "approve":
|
||||
body = f"""
|
||||
<h1>Founder approval</h1>
|
||||
<p>{summary}</p>
|
||||
<div class="box">lane: {lane}<br>act: {act}</div>
|
||||
<form method="POST" action="/act">
|
||||
<input type="hidden" name="token" value="{html.escape(session.token)}">
|
||||
<div class="actions">
|
||||
<button type="submit" name="decision" value="approve">Approve</button>
|
||||
<button type="submit" name="decision" value="deny" class="danger">Deny</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="note">Metadata-only — no secrets transit this form.</p>
|
||||
"""
|
||||
elif session.act == "oidc_login":
|
||||
cmd = html.escape(session.oidc_command or "bao login -method=oidc")
|
||||
body = f"""
|
||||
<h1>OIDC / identity login</h1>
|
||||
<p>{summary}</p>
|
||||
<div class="box">{cmd}</div>
|
||||
<p>Run the command in your own terminal (browser OIDC). ops-warden never
|
||||
captures the token.</p>
|
||||
<form method="POST" action="/act">
|
||||
<input type="hidden" name="token" value="{html.escape(session.token)}">
|
||||
<div class="actions">
|
||||
<button type="submit" name="decision" value="launched">I completed login</button>
|
||||
<button type="submit" name="decision" value="deny" class="secondary">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
"""
|
||||
else: # paste_once_provision
|
||||
path = html.escape(session.path)
|
||||
kv_field = html.escape(session.kv_field)
|
||||
body = f"""
|
||||
<h1>Paste-once provision</h1>
|
||||
<p>{summary}</p>
|
||||
<div class="box">OpenBao path: {path}<br>field: {kv_field}<br>lane: {lane}</div>
|
||||
<p>Paste the secret <strong>once</strong>. It is written to OpenBao via
|
||||
<code>bao kv put</code> and never shown in the terminal or audit log.</p>
|
||||
<form method="POST" action="/act" autocomplete="off">
|
||||
<input type="hidden" name="token" value="{html.escape(session.token)}">
|
||||
<label for="secret">Secret value</label>
|
||||
<textarea id="secret" name="secret" required placeholder="paste value here"></textarea>
|
||||
<div class="actions">
|
||||
<button type="submit" name="decision" value="provision">Write to OpenBao</button>
|
||||
<button type="submit" name="decision" value="deny" class="secondary">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="note">Build-phase desk: localhost only, OS session trust.</p>
|
||||
"""
|
||||
return _page("warden desk", body)
|
||||
|
||||
|
||||
def _provision_to_openbao(path: str, field: str, value: str) -> None:
|
||||
"""Write one field to OpenBao without putting the value on argv."""
|
||||
# bao kv put path field=- reads value from stdin
|
||||
proc = subprocess.run(
|
||||
["bao", "kv", "put", path, f"{field}=-"],
|
||||
input=value.encode("utf-8"),
|
||||
capture_output=True,
|
||||
timeout=60,
|
||||
check=False,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
err = (proc.stderr or proc.stdout or b"").decode("utf-8", errors="replace")
|
||||
# scrub accidental value echo
|
||||
if value and value in err:
|
||||
err = err.replace(value, "<redacted>")
|
||||
raise DeskError(f"bao kv put failed (exit {proc.returncode}): {err[:300]}")
|
||||
|
||||
|
||||
def make_handler(
|
||||
session: DeskSession,
|
||||
*,
|
||||
on_done: Optional[Callable[[DeskSession], None]] = None,
|
||||
dry_run: bool = False,
|
||||
) -> type:
|
||||
"""Build a request handler class closed over *session*."""
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, fmt: str, *args: Any) -> None: # noqa: A003
|
||||
# Avoid logging POST bodies / query secrets
|
||||
line = f"[desk] {self.address_string()} {fmt % args}"
|
||||
if session.token in line:
|
||||
line = line.replace(session.token, "<token>")
|
||||
print(line, flush=True)
|
||||
|
||||
def _deny(self, code: int = 404) -> None:
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
||||
self.end_headers()
|
||||
self.wfile.write(b"not found\n")
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802
|
||||
parsed = urlparse(self.path)
|
||||
qs = parse_qs(parsed.query)
|
||||
token = (qs.get("t") or [""])[0]
|
||||
if parsed.path not in ("/", "/index.html") or token != session.token:
|
||||
self._deny()
|
||||
return
|
||||
body = _render_home(session)
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_POST(self) -> None: # noqa: N802
|
||||
parsed = urlparse(self.path)
|
||||
if parsed.path != "/act":
|
||||
self._deny()
|
||||
return
|
||||
length = int(self.headers.get("Content-Length") or "0")
|
||||
raw = self.rfile.read(length) if length else b""
|
||||
form = parse_qs(raw.decode("utf-8", errors="replace"), keep_blank_values=True)
|
||||
token = (form.get("token") or [""])[0]
|
||||
if token != session.token:
|
||||
self._deny(403)
|
||||
return
|
||||
decision = (form.get("decision") or [""])[0]
|
||||
|
||||
try:
|
||||
if session.act == "approve":
|
||||
if decision == "approve":
|
||||
session.result = "approved"
|
||||
session.message = "Approved (metadata only)."
|
||||
else:
|
||||
session.result = "denied"
|
||||
session.message = "Denied."
|
||||
elif session.act == "oidc_login":
|
||||
if decision == "launched":
|
||||
session.result = "launched"
|
||||
session.message = "Operator confirmed OIDC login completed."
|
||||
else:
|
||||
session.result = "denied"
|
||||
session.message = "Cancelled."
|
||||
elif session.act == "paste_once_provision":
|
||||
if decision == "deny":
|
||||
session.result = "denied"
|
||||
session.message = "Cancelled — nothing written."
|
||||
else:
|
||||
secret = (form.get("secret") or [""])[0]
|
||||
if not secret:
|
||||
raise DeskError("empty secret value")
|
||||
if dry_run:
|
||||
session.result = "provisioned"
|
||||
session.message = (
|
||||
f"dry-run: would write field {session.kv_field!r} "
|
||||
f"to {session.path}"
|
||||
)
|
||||
else:
|
||||
_provision_to_openbao(session.path, session.kv_field, secret)
|
||||
session.result = "provisioned"
|
||||
session.message = (
|
||||
f"Wrote field {session.kv_field!r} to {session.path} "
|
||||
"(value not logged)."
|
||||
)
|
||||
# drop reference promptly
|
||||
secret = ""
|
||||
form.pop("secret", None)
|
||||
else:
|
||||
raise DeskError(f"unhandled act {session.act}")
|
||||
except DeskError as e:
|
||||
session.result = "error"
|
||||
session.message = str(e)
|
||||
|
||||
body = _render_home(session)
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
if on_done and session.result != "pending":
|
||||
on_done(session)
|
||||
|
||||
return Handler
|
||||
|
||||
|
||||
def run_desk(
|
||||
session: DeskSession,
|
||||
*,
|
||||
host: str = "127.0.0.1",
|
||||
port: int = 0,
|
||||
open_browser: bool = True,
|
||||
dry_run: bool = False,
|
||||
shutdown_after_done: bool = True,
|
||||
) -> DeskSession:
|
||||
"""Serve the desk until the act completes (or the process is interrupted).
|
||||
|
||||
Binds *host* (default loopback only). *port* 0 picks an ephemeral port.
|
||||
"""
|
||||
if host not in ("127.0.0.1", "localhost", "::1"):
|
||||
raise DeskError(
|
||||
f"desk refuses non-loopback bind {host!r} in build phase "
|
||||
"(set host only for tests with 127.0.0.1)"
|
||||
)
|
||||
|
||||
done = threading.Event()
|
||||
|
||||
def _on_done(_s: DeskSession) -> None:
|
||||
if shutdown_after_done:
|
||||
done.set()
|
||||
|
||||
handler = make_handler(session, on_done=_on_done, dry_run=dry_run)
|
||||
server = ThreadingHTTPServer((host, port), handler)
|
||||
bound_port = server.server_address[1]
|
||||
url = f"http://{host}:{bound_port}/?t={session.token}"
|
||||
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
print(f"warden desk listening on {url}", flush=True)
|
||||
print(f"act={session.act} lane={session.lane_id or '—'} (token not for logs elsewhere)", flush=True)
|
||||
if open_browser:
|
||||
try:
|
||||
webbrowser.open(url)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
try:
|
||||
done.wait()
|
||||
except KeyboardInterrupt:
|
||||
session.result = session.result if session.result != "pending" else "denied"
|
||||
session.message = session.message or "interrupted"
|
||||
finally:
|
||||
server.shutdown()
|
||||
thread.join(timeout=2)
|
||||
|
||||
return session
|
||||
|
||||
|
||||
def load_plan_json(path: Path) -> dict:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(data, dict):
|
||||
raise DeskError("plan JSON must be an object")
|
||||
return data
|
||||
46
src/warden/mask.py
Normal file
46
src/warden/mask.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
"""Masking display filter for KV values (WARDEN-WP-0026 T03).
|
||||
|
||||
Defense-in-depth, **not a boundary**: any place warden would otherwise render a
|
||||
secret value for a human (a status/listing view) shows a *fingerprint* instead —
|
||||
presence, length, and a short non-reversible hash. Two operators can compare
|
||||
fingerprints to confirm they hold the same value (e.g. that a rotation landed the
|
||||
expected token) without either seeing it, and a fingerprint in a transcript
|
||||
discloses nothing.
|
||||
|
||||
Limitation (documented, by design): raw `bao kv get <path>` bypasses this entirely
|
||||
— warden only masks *warden-mediated* output. The real boundary is OpenBao policy
|
||||
plus the T01 capabilities-safe verify and T02 no-stdout transports.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from dataclasses import dataclass
|
||||
|
||||
# Short, non-reversible hash: first 8 hex chars of SHA-256. Not a value, and a
|
||||
# collision is irrelevant for the "same/different?" comparison this supports.
|
||||
_HASH_PREFIX_LEN = 8
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Fingerprint:
|
||||
present: bool
|
||||
length: int
|
||||
sha256_prefix: str # "" when the value is empty/absent
|
||||
|
||||
def render(self) -> str:
|
||||
if not self.present:
|
||||
return "‹absent›"
|
||||
return f"‹hidden len={self.length} sha256:{self.sha256_prefix}›"
|
||||
|
||||
|
||||
def fingerprint(value: str | None) -> Fingerprint:
|
||||
"""Compute a non-reversible fingerprint of a value. Never returns the value."""
|
||||
if not value:
|
||||
return Fingerprint(present=False, length=0, sha256_prefix="")
|
||||
digest = hashlib.sha256(value.encode("utf-8")).hexdigest()[:_HASH_PREFIX_LEN]
|
||||
return Fingerprint(present=True, length=len(value), sha256_prefix=digest)
|
||||
|
||||
|
||||
def mask_value(value: str | None) -> str:
|
||||
"""Render a value as its masked fingerprint string. Never emits the value."""
|
||||
return fingerprint(value).render()
|
||||
|
|
@ -53,6 +53,9 @@ class CertSpec:
|
|||
principals: List[str]
|
||||
identity: str = "" # defaults to actor_name if empty
|
||||
policy_decision_id: Optional[str] = None
|
||||
policy_zone: Optional[str] = None
|
||||
policy_failure_mode: Optional[str] = None
|
||||
policy_outcome: Optional[str] = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.identity:
|
||||
|
|
|
|||
436
src/warden/plan.py
Normal file
436
src/warden/plan.py
Normal file
|
|
@ -0,0 +1,436 @@
|
|||
"""Policy decision front door — ``warden plan`` (WARDEN-WP-0029 T01).
|
||||
|
||||
Composes the routing catalog, access handoff expansion, organization posture,
|
||||
and flex-auth gate status into a typed verdict. Never holds secret values.
|
||||
Does not re-implement keyword matching — delegates to ``Catalog.find``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
|
||||
from warden.access import expand_handoff, policy_gate_status
|
||||
from warden.posture import PostureCatalog, load_posture
|
||||
from warden.routing.catalog import Catalog, load_catalog
|
||||
from warden.routing.models import RouteEntry
|
||||
|
||||
VERDICTS = ("autonomous", "founder_required", "unroutable")
|
||||
FOUNDER_ACT_KINDS = ("oidc_login", "approve", "paste_once_provision")
|
||||
|
||||
_PROVISION_SIGNS = re.compile(
|
||||
r"\b(provision|mint|onboard|paste|first[- ]time|rotate\s+into|put\s+into\s+openbao)\b"
|
||||
r"|\bnew\b.{0,40}\b(secret|token|pat|key|credential)\b"
|
||||
r"|\bstore\s+(?:the\s+)?(?:pat|token|key|secret)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_APPROVAL_SIGNS = re.compile(
|
||||
r"\b(approv|red[- ]lane|ccr|policy\s+enable|prod\s+flip|break[- ]glass)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FounderAct:
|
||||
kind: str # oidc_login | approve | paste_once_provision
|
||||
summary: str
|
||||
details: dict = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {"kind": self.kind, "summary": self.summary, "details": dict(self.details)}
|
||||
|
||||
|
||||
@dataclass
|
||||
class AccessPlan:
|
||||
need: str
|
||||
verdict: str
|
||||
organization_posture: str
|
||||
policy_gate: str
|
||||
lane_id: Optional[str] = None
|
||||
lane_title: Optional[str] = None
|
||||
match_score: Optional[int] = None
|
||||
commands: List[str] = field(default_factory=list)
|
||||
founder_act: Optional[FounderAct] = None
|
||||
ccr_stub: Optional[dict] = None
|
||||
catalog: dict = field(default_factory=dict)
|
||||
candidates: List[dict] = field(default_factory=list)
|
||||
reasons: List[str] = field(default_factory=list)
|
||||
actor: Optional[str] = None
|
||||
domain: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"need": self.need,
|
||||
"verdict": self.verdict,
|
||||
"organization_posture": self.organization_posture,
|
||||
"policy_gate": self.policy_gate,
|
||||
"lane_id": self.lane_id,
|
||||
"lane_title": self.lane_title,
|
||||
"match_score": self.match_score,
|
||||
"commands": list(self.commands),
|
||||
"founder_act": self.founder_act.to_dict() if self.founder_act else None,
|
||||
"ccr_stub": self.ccr_stub,
|
||||
"catalog": dict(self.catalog),
|
||||
"candidates": list(self.candidates),
|
||||
"reasons": list(self.reasons),
|
||||
"actor": self.actor,
|
||||
"domain": self.domain,
|
||||
}
|
||||
|
||||
|
||||
def _org_posture_id(posture: Optional[PostureCatalog]) -> str:
|
||||
if posture is None:
|
||||
return "unknown"
|
||||
return posture.organization_posture.id
|
||||
|
||||
|
||||
def _candidate_row(entry: RouteEntry, score: int) -> dict:
|
||||
return {
|
||||
"id": entry.id,
|
||||
"title": entry.title,
|
||||
"score": score,
|
||||
"status": entry.status,
|
||||
"resolvable": entry.resolvable,
|
||||
"exec_capable": entry.exec_capable,
|
||||
"warden_executes": entry.warden_executes,
|
||||
"lane": entry.lane,
|
||||
"risk": entry.risk,
|
||||
}
|
||||
|
||||
|
||||
def _score_for(catalog: Catalog, entry: RouteEntry, need: str) -> int:
|
||||
if entry.id == need.strip():
|
||||
return 100
|
||||
tokens = [t for t in need.lower().replace("-", " ").split() if t]
|
||||
return entry.match_score(tokens)
|
||||
|
||||
|
||||
def _concrete(value: Optional[str]) -> bool:
|
||||
"""True when a template has no ``<...>`` placeholders left."""
|
||||
if not value:
|
||||
return False
|
||||
return "<" not in value and ">" not in value
|
||||
|
||||
|
||||
def _lane_is_autonomous(entry: RouteEntry) -> bool:
|
||||
"""Whether an agent can proceed without a founder act for this lane."""
|
||||
if entry.warden_executes:
|
||||
return True
|
||||
if entry.lane == "login":
|
||||
return False
|
||||
if entry.resolvable:
|
||||
return True
|
||||
if entry.has_native_exec and _concrete(entry.exec_command):
|
||||
return True
|
||||
# Concrete owner fetch path (even if not exec_capable) — value already in custody
|
||||
if _concrete(entry.fetch_command):
|
||||
return True
|
||||
# Pure pointer — follow wiki, no secret mechanics for founder
|
||||
if not entry.has_handoff and not entry.exec_capable and not entry.has_native_exec:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _autonomous_commands(entry: RouteEntry, domain: Optional[str]) -> List[str]:
|
||||
cmds: List[str] = []
|
||||
if entry.warden_executes:
|
||||
if entry.cert_command:
|
||||
cmds.append(entry.cert_command)
|
||||
for step in entry.steps[:4]:
|
||||
cmds.append(f"# {step}")
|
||||
return cmds
|
||||
|
||||
expanded = expand_handoff(entry, domain=domain)
|
||||
if entry.lane == "login":
|
||||
cmds.append(
|
||||
f"warden access {entry.id} --exec -- <reviewed-command>"
|
||||
)
|
||||
if expanded.fetch_command:
|
||||
cmds.append(
|
||||
"# owner login is contained by warden; do not invoke it separately"
|
||||
)
|
||||
return cmds
|
||||
if entry.has_native_exec and entry.exec_command:
|
||||
cmds.append(entry.exec_command)
|
||||
if entry.pointer_command:
|
||||
cmds.append(entry.pointer_command)
|
||||
if entry.exec_capable:
|
||||
base = f"warden access {entry.id}"
|
||||
if domain:
|
||||
base += f" --domain {domain}"
|
||||
if entry.is_high_risk:
|
||||
cmds.append(f"{base} --exec -- <cmd> # high-risk: no raw stdout")
|
||||
cmds.append(f"{base} --out FILE")
|
||||
cmds.append(f"{base} --wrap")
|
||||
else:
|
||||
cmds.append(f"{base} --fetch")
|
||||
cmds.append(f"{base} --exec -- <cmd>")
|
||||
if expanded.fetch_command:
|
||||
cmds.append(f"# owner fetch (as you): {expanded.fetch_command}")
|
||||
elif _concrete(expanded.fetch_command or entry.fetch_command):
|
||||
cmds.append(expanded.fetch_command or entry.fetch_command or "")
|
||||
if entry.wiki_ref:
|
||||
cmds.append(f"# playbook: {entry.wiki_ref}")
|
||||
elif entry.wiki_ref:
|
||||
cmds.append(f"# follow owner playbook: {entry.wiki_ref}")
|
||||
return [c for c in cmds if c]
|
||||
|
||||
|
||||
def _founder_for_entry(entry: RouteEntry, need: str, domain: Optional[str]) -> FounderAct:
|
||||
expanded = expand_handoff(entry, domain=domain)
|
||||
if entry.lane == "login":
|
||||
contained_command = (
|
||||
f"warden access {entry.id} --exec -- <reviewed-command>"
|
||||
)
|
||||
return FounderAct(
|
||||
kind="oidc_login",
|
||||
summary=f"Interactive OIDC/MFA login via {entry.owner_repo}",
|
||||
details={
|
||||
"lane_id": entry.id,
|
||||
"auth_method": expanded.auth_method,
|
||||
"fetch_command": contained_command,
|
||||
"desk_hint": (
|
||||
"warden desk --from-plan (act=oidc_login); execute only through: "
|
||||
+ contained_command
|
||||
),
|
||||
},
|
||||
)
|
||||
if entry.lane == "ceremony":
|
||||
return FounderAct(
|
||||
kind="approve",
|
||||
summary=f"Attended owner ceremony approval required for {entry.id}",
|
||||
details={
|
||||
"lane_id": entry.id,
|
||||
"wiki_ref": entry.wiki_ref,
|
||||
"desk_hint": "warden desk --act approve --lane " + entry.id,
|
||||
},
|
||||
)
|
||||
if _APPROVAL_SIGNS.search(need):
|
||||
return FounderAct(
|
||||
kind="approve",
|
||||
summary=f"Founder approval required for {entry.id}",
|
||||
details={
|
||||
"lane_id": entry.id,
|
||||
"wiki_ref": entry.wiki_ref,
|
||||
"desk_hint": "warden desk --act approve --lane " + entry.id,
|
||||
},
|
||||
)
|
||||
# Default founder path for non-resolvable secret lanes: paste-once provision
|
||||
path = expanded.path_template or entry.path_template or "<openbao-path>"
|
||||
return FounderAct(
|
||||
kind="paste_once_provision",
|
||||
summary=(
|
||||
f"Provision secret value once into OpenBao path for {entry.id} "
|
||||
"(no CLI paste; use warden desk)"
|
||||
),
|
||||
details={
|
||||
"lane_id": entry.id,
|
||||
"path_template": path,
|
||||
"auth_method": expanded.auth_method,
|
||||
"desk_hint": (
|
||||
f"warden desk --act paste_once_provision --lane {entry.id}"
|
||||
+ (f" --path {path}" if "<" not in (path or "") else "")
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _ccr_stub(need: str) -> dict:
|
||||
return {
|
||||
"title": f"CCR: new credential lane for {need[:80]}",
|
||||
"status": "proposed",
|
||||
"owner_hint": "railiance-platform (OpenBao) or owning subsystem",
|
||||
"steps": [
|
||||
"Draft CCR with path, policy, OIDC role, consumers",
|
||||
"Add ops-warden catalog entry (pointers only; no secret values)",
|
||||
"Playbook under wiki/playbooks/; promote status active when live",
|
||||
],
|
||||
"commands": [
|
||||
"warden route list --all",
|
||||
"# after CCR: edit registry/routing/catalog.yaml + playbook",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def build_plan(
|
||||
need: str,
|
||||
*,
|
||||
actor: Optional[str] = None,
|
||||
domain: Optional[str] = None,
|
||||
catalog: Optional[Catalog] = None,
|
||||
posture: Optional[PostureCatalog] = None,
|
||||
include_draft: bool = False,
|
||||
) -> AccessPlan:
|
||||
"""Resolve *need* to a typed access plan. Pure of secret values."""
|
||||
cat = catalog or load_catalog()
|
||||
try:
|
||||
post = posture if posture is not None else load_posture()
|
||||
except Exception: # noqa: BLE001 — plan still works without posture file
|
||||
post = None
|
||||
|
||||
gate = policy_gate_status()
|
||||
org = _org_posture_id(post)
|
||||
freshness = cat.freshness().to_dict()
|
||||
|
||||
raw_matches = cat.find(need, include_draft=include_draft, limit=8)
|
||||
# Require score >= 2 (at least one full keyword hit). Score-1 hits are usually
|
||||
# accidental substring overlaps (e.g. title word "or" inside an unrelated token).
|
||||
scored = [(e, _score_for(cat, e, need)) for e in raw_matches]
|
||||
matches = [(e, s) for e, s in scored if s >= 2]
|
||||
candidates = [_candidate_row(e, s) for e, s in scored[:5]]
|
||||
|
||||
if not matches:
|
||||
return AccessPlan(
|
||||
need=need,
|
||||
verdict="unroutable",
|
||||
organization_posture=org,
|
||||
policy_gate=gate,
|
||||
ccr_stub=_ccr_stub(need),
|
||||
catalog=freshness,
|
||||
candidates=candidates,
|
||||
reasons=["no catalog match for need (score < 2)"],
|
||||
actor=actor,
|
||||
domain=domain,
|
||||
)
|
||||
|
||||
entry, score = matches[0]
|
||||
|
||||
# Draft-only top match without active alternatives → unroutable
|
||||
if entry.status == "draft" and not include_draft:
|
||||
return AccessPlan(
|
||||
need=need,
|
||||
verdict="unroutable",
|
||||
organization_posture=org,
|
||||
policy_gate=gate,
|
||||
lane_id=entry.id,
|
||||
lane_title=entry.title,
|
||||
match_score=score,
|
||||
ccr_stub=_ccr_stub(need),
|
||||
catalog=freshness,
|
||||
candidates=candidates,
|
||||
reasons=[f"top match {entry.id!r} is draft — promote or request CCR"],
|
||||
actor=actor,
|
||||
domain=domain,
|
||||
)
|
||||
|
||||
# Login and ceremony lanes always need a human act. A ceremony is a pure
|
||||
# owner pointer: it must never fall through to secret paste-once mechanics.
|
||||
if entry.lane in ("login", "ceremony"):
|
||||
act = _founder_for_entry(entry, need, domain)
|
||||
return AccessPlan(
|
||||
need=need,
|
||||
verdict="founder_required",
|
||||
organization_posture=org,
|
||||
policy_gate=gate,
|
||||
lane_id=entry.id,
|
||||
lane_title=entry.title,
|
||||
match_score=score,
|
||||
commands=_autonomous_commands(entry, domain),
|
||||
founder_act=act,
|
||||
catalog=freshness,
|
||||
candidates=candidates,
|
||||
reasons=[
|
||||
"login lane requires interactive founder/operator identity act"
|
||||
if entry.lane == "login"
|
||||
else "ceremony lane requires attended owner approval"
|
||||
],
|
||||
actor=actor,
|
||||
domain=domain,
|
||||
)
|
||||
|
||||
# Explicit approval language
|
||||
if _APPROVAL_SIGNS.search(need) and not entry.warden_executes:
|
||||
act = _founder_for_entry(entry, need, domain)
|
||||
act.kind = "approve"
|
||||
return AccessPlan(
|
||||
need=need,
|
||||
verdict="founder_required",
|
||||
organization_posture=org,
|
||||
policy_gate=gate,
|
||||
lane_id=entry.id,
|
||||
lane_title=entry.title,
|
||||
match_score=score,
|
||||
founder_act=act,
|
||||
catalog=freshness,
|
||||
candidates=candidates,
|
||||
reasons=["need text requests founder approval"],
|
||||
actor=actor,
|
||||
domain=domain,
|
||||
)
|
||||
|
||||
# Explicit first-time provision language wins even if a concrete lane matched
|
||||
if _PROVISION_SIGNS.search(need) and entry.lane == "secret" and not entry.warden_executes:
|
||||
act = _founder_for_entry(entry, need, domain)
|
||||
return AccessPlan(
|
||||
need=need,
|
||||
verdict="founder_required",
|
||||
organization_posture=org,
|
||||
policy_gate=gate,
|
||||
lane_id=entry.id,
|
||||
lane_title=entry.title,
|
||||
match_score=score,
|
||||
commands=[],
|
||||
founder_act=act,
|
||||
catalog=freshness,
|
||||
candidates=candidates,
|
||||
reasons=["need requires first-time provision — one founder act via warden desk"],
|
||||
actor=actor,
|
||||
domain=domain,
|
||||
)
|
||||
|
||||
if _lane_is_autonomous(entry):
|
||||
return AccessPlan(
|
||||
need=need,
|
||||
verdict="autonomous",
|
||||
organization_posture=org,
|
||||
policy_gate=gate,
|
||||
lane_id=entry.id,
|
||||
lane_title=entry.title,
|
||||
match_score=score,
|
||||
commands=_autonomous_commands(entry, domain),
|
||||
catalog=freshness,
|
||||
candidates=candidates,
|
||||
reasons=["lane is usable under current catalog without founder mechanics"],
|
||||
actor=actor,
|
||||
domain=domain,
|
||||
)
|
||||
|
||||
# Template / non-concrete secret handoff → founder paste-once
|
||||
if entry.lane == "secret":
|
||||
act = _founder_for_entry(entry, need, domain)
|
||||
return AccessPlan(
|
||||
need=need,
|
||||
verdict="founder_required",
|
||||
organization_posture=org,
|
||||
policy_gate=gate,
|
||||
lane_id=entry.id,
|
||||
lane_title=entry.title,
|
||||
match_score=score,
|
||||
commands=[],
|
||||
founder_act=act,
|
||||
catalog=freshness,
|
||||
candidates=candidates,
|
||||
reasons=[
|
||||
"lane handoff still has placeholders or needs provision — "
|
||||
"one founder act via warden desk"
|
||||
],
|
||||
actor=actor,
|
||||
domain=domain,
|
||||
)
|
||||
|
||||
# Fallback: autonomous with best-effort commands
|
||||
return AccessPlan(
|
||||
need=need,
|
||||
verdict="autonomous",
|
||||
organization_posture=org,
|
||||
policy_gate=gate,
|
||||
lane_id=entry.id,
|
||||
lane_title=entry.title,
|
||||
match_score=score,
|
||||
commands=_autonomous_commands(entry, domain),
|
||||
catalog=freshness,
|
||||
candidates=candidates,
|
||||
reasons=["matched lane; proceed via catalog handoff"],
|
||||
actor=actor,
|
||||
domain=domain,
|
||||
)
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
"""flex-auth policy gate for SSH signing (opt-in via warden.yaml)."""
|
||||
"""Zone-aware flex-auth policy gates for OpsWarden."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
from warden.ca import CAError
|
||||
from warden.caller_identity import CallerIdentityError, caller_auth_headers
|
||||
from warden.config import PolicyConfig
|
||||
from warden.models import CertSpec
|
||||
|
||||
|
|
@ -19,6 +21,60 @@ def pubkey_fingerprint(pubkey_path: Path) -> str:
|
|||
return f"sha256:{digest}"
|
||||
|
||||
|
||||
def _caller_headers(cfg: PolicyConfig, *, fail_closed: bool) -> dict[str, str]:
|
||||
"""Bearer header identifying ops-warden itself to flex-auth (FLEX-WP-0016).
|
||||
|
||||
When the token cannot be obtained we refuse the call under the selected
|
||||
zone's ``fail_closed`` behavior
|
||||
rather than silently falling back to an unauthenticated request — an
|
||||
unauthenticated call is exactly what keeps the flex-auth pin in ``warn``.
|
||||
"""
|
||||
try:
|
||||
return caller_auth_headers(cfg.caller_auth)
|
||||
except CallerIdentityError as e:
|
||||
if fail_closed:
|
||||
raise CAError(f"flex-auth caller identity unavailable: {e}") from e
|
||||
return {}
|
||||
|
||||
|
||||
def _resource_zone(cfg: PolicyConfig, resource_id: str) -> str:
|
||||
"""Read a compiled resource zone; absence or ambiguity is always unknown."""
|
||||
if cfg.zone_registry_path is None:
|
||||
return "unknown"
|
||||
try:
|
||||
registry = json.loads(cfg.zone_registry_path.read_text())
|
||||
resources = registry["resource_manifests"][0]["resources"]
|
||||
resource = next(item for item in resources if item.get("id") == resource_id)
|
||||
attributes = resource.get("attributes") or {}
|
||||
if attributes.get("security_zone_admission") == "not-applicable":
|
||||
return "not-applicable"
|
||||
zone = str(attributes.get("security_zone") or "unknown")
|
||||
return zone if zone in cfg.failure_modes else "unknown"
|
||||
except (OSError, ValueError, KeyError, StopIteration, TypeError):
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _is_fail_closed(cfg: PolicyConfig, zone: str) -> bool:
|
||||
return cfg.failure_modes.get(zone, cfg.failure_modes["unknown"]) == "fail_closed"
|
||||
|
||||
|
||||
def _evaluator_failure(
|
||||
message: str,
|
||||
*,
|
||||
fail_closed: bool,
|
||||
cause: Exception | None = None,
|
||||
spec: CertSpec | None = None,
|
||||
) -> None:
|
||||
if fail_closed:
|
||||
if spec is not None:
|
||||
spec.policy_outcome = "fail_closed"
|
||||
if cause is None:
|
||||
raise CAError(message)
|
||||
raise CAError(message) from cause
|
||||
if spec is not None:
|
||||
spec.policy_outcome = "fail_open"
|
||||
|
||||
|
||||
def _subject_id(cfg: PolicyConfig, spec: CertSpec) -> str:
|
||||
return os.environ.get(cfg.subject_env, "").strip() or spec.actor_name
|
||||
|
||||
|
|
@ -26,11 +82,21 @@ def _subject_id(cfg: PolicyConfig, spec: CertSpec) -> str:
|
|||
def check_sign_policy(cfg: PolicyConfig, spec: CertSpec) -> str | None:
|
||||
"""Call flex-auth /v1/check before signing.
|
||||
|
||||
Returns decision id when policy is enabled and effect is allow.
|
||||
Returns None when policy is disabled.
|
||||
Raises CAError on deny or when fail_closed and flex-auth is unreachable.
|
||||
Returns a decision id on ``allow`` or ``audit_only``. A deny always blocks.
|
||||
Evaluator failures use the PEP-owned failure mode for the target workload's
|
||||
compiled zone; absent resolution is the explicit ``unknown`` profile.
|
||||
"""
|
||||
if not cfg.enabled:
|
||||
resource_id = f"ssh-cert:actor/{spec.actor_name}"
|
||||
zone = _resource_zone(cfg, resource_id)
|
||||
fail_closed = _is_fail_closed(cfg, zone)
|
||||
spec.policy_zone = zone
|
||||
spec.policy_failure_mode = "fail_closed" if fail_closed else "fail_open"
|
||||
if cfg.flex_auth_url is None:
|
||||
_evaluator_failure(
|
||||
f"flex-auth URL is not configured for security zone {zone!r}",
|
||||
fail_closed=fail_closed,
|
||||
spec=spec,
|
||||
)
|
||||
return None
|
||||
|
||||
pubkey_path = Path(os.path.expanduser(str(spec.pubkey_path)))
|
||||
|
|
@ -60,36 +126,54 @@ def check_sign_policy(cfg: PolicyConfig, spec: CertSpec) -> str | None:
|
|||
}
|
||||
|
||||
url = cfg.flex_auth_url.rstrip("/") + "/v1/check"
|
||||
headers = _caller_headers(cfg, fail_closed=fail_closed)
|
||||
try:
|
||||
response = httpx.post(url, json=request, timeout=10.0)
|
||||
response = httpx.post(url, json=request, headers=headers, timeout=10.0)
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPStatusError as e:
|
||||
if cfg.fail_closed:
|
||||
raise CAError(
|
||||
f"flex-auth denied or rejected sign policy check (HTTP {e.response.status_code})"
|
||||
) from e
|
||||
_evaluator_failure(
|
||||
f"flex-auth rejected sign policy check (HTTP {e.response.status_code}) "
|
||||
f"for security zone {zone!r}",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
spec=spec,
|
||||
)
|
||||
return None
|
||||
except httpx.RequestError as e:
|
||||
if cfg.fail_closed:
|
||||
raise CAError(
|
||||
f"flex-auth unreachable at {cfg.flex_auth_url!r} "
|
||||
f"(fail_closed=true): {e}"
|
||||
) from e
|
||||
_evaluator_failure(
|
||||
f"flex-auth unreachable at {cfg.flex_auth_url!r} for security zone {zone!r}",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
spec=spec,
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
decision = response.json()
|
||||
except ValueError as e:
|
||||
raise CAError("flex-auth returned non-JSON decision") from e
|
||||
_evaluator_failure(
|
||||
f"flex-auth returned a non-JSON decision for security zone {zone!r}",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
spec=spec,
|
||||
)
|
||||
return None
|
||||
|
||||
effect = str(decision.get("effect", "")).lower()
|
||||
decision_id = decision.get("id") or decision.get("request_id")
|
||||
if effect != "allow":
|
||||
if effect not in {"allow", "audit_only"}:
|
||||
spec.policy_outcome = "deny"
|
||||
reason = decision.get("reason") or "no reason provided"
|
||||
raise CAError(f"flex-auth denied SSH sign for {spec.actor_name!r}: {reason}")
|
||||
|
||||
if not decision_id:
|
||||
raise CAError("flex-auth allow decision missing id")
|
||||
_evaluator_failure(
|
||||
f"flex-auth {effect} decision missing id for security zone {zone!r}",
|
||||
fail_closed=fail_closed,
|
||||
spec=spec,
|
||||
)
|
||||
return None
|
||||
spec.policy_outcome = effect
|
||||
return str(decision_id)
|
||||
|
||||
|
||||
|
|
@ -99,13 +183,17 @@ def check_fetch_policy(
|
|||
"""Call flex-auth /v1/check before proxying a non-SSH credential fetch (WP-0014).
|
||||
|
||||
The action is ``read`` on a ``secret`` resource owned by another subsystem —
|
||||
ops-warden is the conduit, not the owner. Returns the decision id on allow,
|
||||
None when policy is disabled, and raises CAError on deny (or on an unreachable
|
||||
flex-auth when fail_closed). No secret value is ever part of this request.
|
||||
ops-warden is the conduit, not the owner. Unresolved target workload identity
|
||||
selects the explicit ``unknown`` profile; no secret value enters the request.
|
||||
"""
|
||||
if not cfg.enabled:
|
||||
zone = "unknown"
|
||||
fail_closed = _is_fail_closed(cfg, zone)
|
||||
if cfg.flex_auth_url is None:
|
||||
_evaluator_failure(
|
||||
"flex-auth URL is not configured for security zone 'unknown'",
|
||||
fail_closed=fail_closed,
|
||||
)
|
||||
return None
|
||||
|
||||
subject_id = os.environ.get(cfg.subject_env, "").strip() or "operator"
|
||||
request = {
|
||||
"subject": {"id": subject_id, "type": "operator", "tenant": cfg.tenant},
|
||||
|
|
@ -120,32 +208,44 @@ def check_fetch_policy(
|
|||
}
|
||||
|
||||
url = cfg.flex_auth_url.rstrip("/") + "/v1/check"
|
||||
headers = _caller_headers(cfg, fail_closed=fail_closed)
|
||||
try:
|
||||
response = httpx.post(url, json=request, timeout=10.0)
|
||||
response = httpx.post(url, json=request, headers=headers, timeout=10.0)
|
||||
response.raise_for_status()
|
||||
except httpx.HTTPStatusError as e:
|
||||
if cfg.fail_closed:
|
||||
raise CAError(
|
||||
f"flex-auth denied or rejected fetch policy check (HTTP {e.response.status_code})"
|
||||
) from e
|
||||
_evaluator_failure(
|
||||
f"flex-auth rejected fetch policy check (HTTP {e.response.status_code})",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
)
|
||||
return None
|
||||
except httpx.RequestError as e:
|
||||
if cfg.fail_closed:
|
||||
raise CAError(
|
||||
f"flex-auth unreachable at {cfg.flex_auth_url!r} (fail_closed=true): {e}"
|
||||
) from e
|
||||
_evaluator_failure(
|
||||
f"flex-auth unreachable at {cfg.flex_auth_url!r} for security zone 'unknown'",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
decision = response.json()
|
||||
except ValueError as e:
|
||||
raise CAError("flex-auth returned non-JSON decision") from e
|
||||
_evaluator_failure(
|
||||
"flex-auth returned a non-JSON decision for security zone 'unknown'",
|
||||
fail_closed=fail_closed,
|
||||
cause=e,
|
||||
)
|
||||
return None
|
||||
|
||||
effect = str(decision.get("effect", "")).lower()
|
||||
decision_id = decision.get("id") or decision.get("request_id")
|
||||
if effect != "allow":
|
||||
if effect not in {"allow", "audit_only"}:
|
||||
reason = decision.get("reason") or "no reason provided"
|
||||
raise CAError(f"flex-auth denied secret read for {need_id!r}: {reason}")
|
||||
if not decision_id:
|
||||
raise CAError("flex-auth allow decision missing id")
|
||||
return str(decision_id)
|
||||
_evaluator_failure(
|
||||
f"flex-auth {effect} decision missing id for security zone 'unknown'",
|
||||
fail_closed=fail_closed,
|
||||
)
|
||||
return None
|
||||
return str(decision_id)
|
||||
|
|
|
|||
|
|
@ -42,6 +42,16 @@ class MaturityLevel:
|
|||
promotion_gate: List[str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class OrganizationPosture:
|
||||
"""Fleet lifecycle posture (WARDEN-WP-0029) — third axis, not env/maturity."""
|
||||
|
||||
id: str
|
||||
summary: str
|
||||
relaxations: List[str]
|
||||
graduation_triggers: List[str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class PostureCatalog:
|
||||
path: Path
|
||||
|
|
@ -49,6 +59,7 @@ class PostureCatalog:
|
|||
maturity_levels: List[MaturityLevel]
|
||||
dataclass_floor: Dict[str, str] # dataclass -> maturity id
|
||||
requires_env_posture: str # lattice: posture a secret fetch requires
|
||||
organization_posture: OrganizationPosture
|
||||
|
||||
# --- lookups ----------------------------------------------------------
|
||||
def env(self, env_id: str) -> Optional[EnvPosture]:
|
||||
|
|
@ -184,10 +195,24 @@ def load_posture(path: Optional[Path] = None) -> PostureCatalog:
|
|||
if not any(e.id == requires_env for e in env_postures):
|
||||
raise PostureError(f"lattice requires_env_posture {requires_env!r} is not an env posture")
|
||||
|
||||
org_raw = raw.get("organization_posture") or {}
|
||||
if not isinstance(org_raw, dict) or not org_raw.get("id"):
|
||||
raise PostureError(
|
||||
"posture descriptors need organization_posture with at least an id "
|
||||
"(WARDEN-WP-0029 third axis)"
|
||||
)
|
||||
organization_posture = OrganizationPosture(
|
||||
id=str(org_raw["id"]),
|
||||
summary=str(org_raw.get("summary") or "").strip(),
|
||||
relaxations=[str(x) for x in (org_raw.get("relaxations") or [])],
|
||||
graduation_triggers=[str(x) for x in (org_raw.get("graduation_triggers") or [])],
|
||||
)
|
||||
|
||||
return PostureCatalog(
|
||||
path=posture_path,
|
||||
env_postures=env_postures,
|
||||
maturity_levels=maturity_levels,
|
||||
dataclass_floor=dataclass_floor,
|
||||
requires_env_posture=requires_env,
|
||||
organization_posture=organization_posture,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@ intact. Three guardrails are enforced here in code:
|
|||
caller's own environment. ops-warden injects no token of its own; if the caller has
|
||||
no credential, the underlying tool fails and we surface the auth pointer. We never
|
||||
add a `*_TOKEN` warden owns to the child environment.
|
||||
* **G2 — transit only, no persistence/logging of values.** ``proxy_fetch`` runs the
|
||||
tool with **inherited** stdout/stderr (never a pipe), so the value streams to the
|
||||
caller and never enters warden's memory. ``proxy_exec`` reads the value solely to
|
||||
place it in a child process's environment (the accepted proxy tradeoff) and never
|
||||
writes it to disk or log. The audit record is metadata only.
|
||||
* **G2 — bounded transports, no logging of values.** Ordinary ``proxy_fetch`` runs
|
||||
the tool with inherited stdout/stderr so the value never enters warden's memory;
|
||||
sanctioned exec/file transports hold it only for their bounded handoff. The
|
||||
high-risk attended-login lane is stricter: it captures every client byte inside
|
||||
an isolated helper session, never returns that output, requires successful
|
||||
persistence to a private token helper, self-revokes, and cleans up. Audit records
|
||||
are metadata only.
|
||||
* **G3 — policy gate before fetch.** The CLI runs ``check_fetch_policy`` before
|
||||
calling anything here; this module refuses to run an unresolved command template.
|
||||
|
||||
|
|
@ -24,7 +26,10 @@ import json
|
|||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
|
@ -33,6 +38,9 @@ from typing import List, Optional
|
|||
from warden.routing.models import RouteEntry
|
||||
|
||||
_PLACEHOLDER = re.compile(r"<[^>]+>")
|
||||
_OPENBAO_TOKEN = re.compile(rb"\b(?:hvs|hvb|hvr)\.[A-Za-z0-9_-]{8,}\b")
|
||||
_ATTENDED_LOGIN_ROOT = ".warden-attended-login"
|
||||
_TOKEN_HELPER_NAME = ".vault-token"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -205,6 +213,385 @@ def proxy_fetch(resolved: ResolvedFetch) -> int:
|
|||
return completed.returncode
|
||||
|
||||
|
||||
def _assert_owned_mode(path: Path, *, mode: int, directory: bool) -> None:
|
||||
"""Require a caller-owned, non-symlink path with an exact private mode."""
|
||||
info = path.lstat()
|
||||
expected_type = stat.S_ISDIR if directory else stat.S_ISREG
|
||||
if not expected_type(info.st_mode) or stat.S_ISLNK(info.st_mode):
|
||||
raise ProxyError("attended login private storage has an unsafe path type")
|
||||
if hasattr(os, "getuid") and info.st_uid != os.getuid():
|
||||
raise ProxyError("attended login private storage is not caller-owned")
|
||||
if stat.S_IMODE(info.st_mode) != mode:
|
||||
raise ProxyError("attended login private storage has an unsafe mode")
|
||||
|
||||
|
||||
def _prepare_attended_login_home() -> tuple[Path, Path, bool]:
|
||||
"""Create and prove an isolated token-helper home before authentication."""
|
||||
home = Path.home()
|
||||
try:
|
||||
home_info = home.lstat()
|
||||
except OSError as exc:
|
||||
raise ProxyError(
|
||||
"attended login requires a usable writable default home before OIDC"
|
||||
) from exc
|
||||
if (
|
||||
not stat.S_ISDIR(home_info.st_mode)
|
||||
or stat.S_ISLNK(home_info.st_mode)
|
||||
or stat.S_IMODE(home_info.st_mode) & 0o222 == 0
|
||||
):
|
||||
raise ProxyError(
|
||||
"attended login requires a usable writable default home before OIDC"
|
||||
)
|
||||
|
||||
# Prove the default home itself is writable. A pre-existing writable child must
|
||||
# not let a newly read-only HOME reach the OIDC process.
|
||||
probe_fd = -1
|
||||
probe_path: Path | None = None
|
||||
probe_cleanup_error: OSError | None = None
|
||||
try:
|
||||
probe_fd, probe_name = tempfile.mkstemp(prefix=".warden-home-probe-", dir=home)
|
||||
probe_path = Path(probe_name)
|
||||
os.write(probe_fd, b"preflight")
|
||||
os.fsync(probe_fd)
|
||||
except OSError as exc:
|
||||
raise ProxyError(
|
||||
"attended login requires a usable writable default home before OIDC"
|
||||
) from exc
|
||||
finally:
|
||||
if probe_fd >= 0:
|
||||
os.close(probe_fd)
|
||||
if probe_path is not None:
|
||||
try:
|
||||
probe_path.unlink()
|
||||
except OSError as exc:
|
||||
probe_cleanup_error = exc
|
||||
if probe_cleanup_error is not None:
|
||||
raise ProxyError("attended login home preflight cleanup failed") from probe_cleanup_error
|
||||
|
||||
root = home / _ATTENDED_LOGIN_ROOT
|
||||
root_created = False
|
||||
try:
|
||||
root.mkdir(mode=0o700)
|
||||
root_created = True
|
||||
except FileExistsError:
|
||||
pass
|
||||
except OSError as exc:
|
||||
raise ProxyError("could not establish attended login private storage") from exc
|
||||
_assert_owned_mode(root, mode=0o700, directory=True)
|
||||
|
||||
try:
|
||||
session = Path(tempfile.mkdtemp(prefix="session-", dir=root))
|
||||
session.chmod(0o700)
|
||||
_assert_owned_mode(session, mode=0o700, directory=True)
|
||||
helper = session / _TOKEN_HELPER_NAME
|
||||
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
||||
if hasattr(os, "O_NOFOLLOW"):
|
||||
flags |= os.O_NOFOLLOW
|
||||
fd = os.open(helper, flags, 0o600)
|
||||
try:
|
||||
# Exercise persistence before auth, then leave the helper empty for bao.
|
||||
os.write(fd, b"preflight")
|
||||
os.fsync(fd)
|
||||
os.ftruncate(fd, 0)
|
||||
finally:
|
||||
os.close(fd)
|
||||
_assert_owned_mode(helper, mode=0o600, directory=False)
|
||||
except (OSError, ProxyError) as exc:
|
||||
if "session" in locals():
|
||||
shutil.rmtree(session, ignore_errors=True)
|
||||
if root_created:
|
||||
try:
|
||||
root.rmdir()
|
||||
except OSError:
|
||||
pass
|
||||
if isinstance(exc, ProxyError):
|
||||
raise
|
||||
raise ProxyError("could not establish attended login private storage") from exc
|
||||
return root, session, root_created
|
||||
|
||||
|
||||
def _contained_run(argv: List[str], *, env: dict) -> subprocess.CompletedProcess:
|
||||
"""Run with both output streams captured and never forwarded."""
|
||||
return subprocess.run( # noqa: S603
|
||||
argv,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stdin=None,
|
||||
env=env,
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
def _output_bytes(completed: subprocess.CompletedProcess) -> bytes:
|
||||
stdout = completed.stdout
|
||||
stderr = completed.stderr
|
||||
if isinstance(stdout, str):
|
||||
stdout = stdout.encode("utf-8", errors="replace")
|
||||
if isinstance(stderr, str):
|
||||
stderr = stderr.encode("utf-8", errors="replace")
|
||||
stdout = stdout if isinstance(stdout, bytes) else b""
|
||||
stderr = stderr if isinstance(stderr, bytes) else b""
|
||||
return stdout + b"\n" + stderr
|
||||
|
||||
|
||||
def _revoke_contained(
|
||||
bao_binary: str,
|
||||
*,
|
||||
env: dict,
|
||||
possible_output: bytes,
|
||||
) -> bool:
|
||||
"""Attempt self-revocation without exposing helper or captured output."""
|
||||
revoke_env = dict(env)
|
||||
try:
|
||||
first = _contained_run(
|
||||
[bao_binary, "token", "revoke", "-self"], env=revoke_env
|
||||
)
|
||||
except OSError:
|
||||
return False
|
||||
if first.returncode == 0:
|
||||
return True
|
||||
|
||||
# A helper-persistence failure can leave the issued token only in contained
|
||||
# client output. Use it solely for immediate self-revocation, never for a log,
|
||||
# return value, hash, fingerprint, file, or argv.
|
||||
match = _OPENBAO_TOKEN.search(possible_output)
|
||||
if match is None:
|
||||
return False
|
||||
token = match.group(0).decode("ascii")
|
||||
revoke_env["BAO_TOKEN"] = token
|
||||
revoke_env.pop("VAULT_TOKEN", None)
|
||||
try:
|
||||
try:
|
||||
second = _contained_run(
|
||||
[bao_binary, "token", "revoke", "-self"], env=revoke_env
|
||||
)
|
||||
except OSError:
|
||||
return False
|
||||
return second.returncode == 0
|
||||
finally:
|
||||
revoke_env.pop("BAO_TOKEN", None)
|
||||
token = "" # noqa: F841 - best-effort release of the credential reference
|
||||
|
||||
|
||||
def proxy_attended_login_exec(
|
||||
resolved: ResolvedFetch,
|
||||
*,
|
||||
child_argv: List[str],
|
||||
) -> int:
|
||||
"""Run an attended login and one silent child inside a private helper home.
|
||||
|
||||
The default home is proven writable before the OIDC client starts. Login,
|
||||
child, and revocation output are captured and discarded. A successful login
|
||||
may return client output only after the private helper has been populated;
|
||||
persistence defects and non-zero results fail closed. The reviewed child must
|
||||
remain silent. Any possibly issued token is revoked before the isolated helper
|
||||
directory is removed.
|
||||
"""
|
||||
if not child_argv:
|
||||
raise ProxyError(
|
||||
"attended login requires --exec -- <reviewed-command>; a persistent "
|
||||
"login-only handoff is not permitted"
|
||||
)
|
||||
if (
|
||||
resolved.argv is None
|
||||
or len(resolved.argv) < 2
|
||||
or Path(resolved.argv[0]).name != "bao"
|
||||
or resolved.argv[1] != "login"
|
||||
):
|
||||
raise ProxyError("attended login requires a direct bao login argv")
|
||||
|
||||
root, session, root_created = _prepare_attended_login_home()
|
||||
helper = session / _TOKEN_HELPER_NAME
|
||||
env = _caller_env()
|
||||
if not env.get("WARDEN_CONFIG"):
|
||||
caller_config = Path.home() / ".config" / "warden" / "warden.yaml"
|
||||
if caller_config.is_file():
|
||||
env["WARDEN_CONFIG"] = str(caller_config)
|
||||
env["HOME"] = str(session)
|
||||
env.pop("BAO_TOKEN", None)
|
||||
env.pop("VAULT_TOKEN", None)
|
||||
for key in (
|
||||
"BAO_LOG_LEVEL",
|
||||
"BAO_LOG_FORMAT",
|
||||
"VAULT_LOG_LEVEL",
|
||||
"VAULT_LOG_FORMAT",
|
||||
):
|
||||
env.pop(key, None)
|
||||
|
||||
login_argv = list(resolved.argv)
|
||||
if not any(arg == "-format" or arg.startswith("-format=") for arg in login_argv):
|
||||
login_argv.append("-format=json")
|
||||
|
||||
try:
|
||||
try:
|
||||
login = _contained_run(login_argv, env=env)
|
||||
except OSError as exc:
|
||||
raise ProxyError("attended login client could not start before OIDC") from exc
|
||||
login_output = _output_bytes(login)
|
||||
helper_valid = False
|
||||
try:
|
||||
_assert_owned_mode(helper, mode=0o600, directory=False)
|
||||
helper_valid = helper.stat().st_size > 0
|
||||
except (OSError, ProxyError):
|
||||
helper_valid = False
|
||||
|
||||
if login.returncode != 0 or not helper_valid:
|
||||
revoked = _revoke_contained(
|
||||
resolved.argv[0], env=env, possible_output=login_output
|
||||
)
|
||||
status = "revoked" if revoked else "revocation could not be confirmed"
|
||||
raise ProxyError(
|
||||
"attended login failed closed before command handoff; any possible "
|
||||
f"issued session was contained and {status}"
|
||||
)
|
||||
|
||||
try:
|
||||
child = _contained_run(child_argv, env=env)
|
||||
except OSError as exc:
|
||||
revoked = _revoke_contained(
|
||||
resolved.argv[0], env=env, possible_output=b""
|
||||
)
|
||||
status = "revoked" if revoked else "revocation could not be confirmed"
|
||||
raise ProxyError(
|
||||
"attended command could not start; the login session was " + status
|
||||
) from exc
|
||||
|
||||
child_output = _output_bytes(child)
|
||||
revoked = _revoke_contained(
|
||||
resolved.argv[0], env=env, possible_output=child_output
|
||||
)
|
||||
if child.returncode != 0 or child_output.strip():
|
||||
status = "revoked" if revoked else "revocation could not be confirmed"
|
||||
raise ProxyError(
|
||||
"attended command failed closed because it returned a failure or "
|
||||
f"unexpected output; the login session was {status}"
|
||||
)
|
||||
if not revoked:
|
||||
raise ProxyError(
|
||||
"attended command completed but session revocation could not be confirmed"
|
||||
)
|
||||
return 0
|
||||
finally:
|
||||
try:
|
||||
shutil.rmtree(session)
|
||||
if root_created:
|
||||
root.rmdir()
|
||||
except OSError as exc:
|
||||
raise ProxyError("attended login private storage cleanup failed") from exc
|
||||
|
||||
|
||||
def _capture_value(resolved: ResolvedFetch) -> str:
|
||||
"""Run the fetch and return its stdout (the value) minus one trailing newline.
|
||||
|
||||
The value transits warden's memory (the accepted proxy tradeoff for the
|
||||
non-stdout transports) but is never written to disk or log by this function.
|
||||
"""
|
||||
env = _caller_env()
|
||||
if resolved.argv is not None:
|
||||
fetched = subprocess.run( # noqa: S603
|
||||
resolved.argv, stdout=subprocess.PIPE, stderr=None, stdin=None,
|
||||
env=env, check=False, text=True,
|
||||
)
|
||||
else:
|
||||
fetched = subprocess.run( # noqa: S602
|
||||
resolved.shell_cmd, shell=True, stdout=subprocess.PIPE, stderr=None,
|
||||
stdin=None, env=env, check=False, text=True,
|
||||
)
|
||||
if fetched.returncode != 0:
|
||||
raise ProxyError(
|
||||
f"fetch failed (exit {fetched.returncode}) — check caller auth and the path."
|
||||
)
|
||||
value = fetched.stdout
|
||||
if value.endswith("\n"):
|
||||
value = value[:-1]
|
||||
return value
|
||||
|
||||
|
||||
def proxy_fetch_to_file(resolved: ResolvedFetch, out_path: Path) -> int:
|
||||
"""Fetch the value and write it to ``out_path`` at mode 0600 — never to stdout.
|
||||
|
||||
A sanctioned transport (WP-0026 T02): the value goes to a private file the
|
||||
caller controls, not a terminal or a logged stream. The file is created with
|
||||
O_EXCL semantics widened to truncate-if-owned so a re-fetch overwrites, but the
|
||||
mode is forced to 0600 before any bytes are written.
|
||||
"""
|
||||
value = _capture_value(resolved)
|
||||
# Open with restrictive mode from the start; do not echo the value anywhere.
|
||||
fd = os.open(str(out_path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
try:
|
||||
os.chmod(out_path, 0o600) # enforce even if the file pre-existed with looser mode
|
||||
with os.fdopen(fd, "w") as fh:
|
||||
fh.write(value)
|
||||
finally:
|
||||
value = "" # noqa: F841 — best-effort scrub of the local reference
|
||||
return 0
|
||||
|
||||
|
||||
def is_bao_kv_fetch(entry: RouteEntry) -> bool:
|
||||
"""True when a lane's fetch is a plain ``bao kv get`` (wrappable, WP-0026 T02)."""
|
||||
return bool(entry.fetch_command and entry.fetch_command.strip().startswith("bao kv get"))
|
||||
|
||||
|
||||
def build_wrapped_fetch(
|
||||
entry: RouteEntry, *, path: Optional[str] = None, ttl: str = "5m"
|
||||
) -> ResolvedFetch:
|
||||
"""Build a response-wrapping fetch: ``bao kv get -wrap-ttl=<ttl> -format=json <path>``.
|
||||
|
||||
Response wrapping returns a single-use, short-TTL *wrapping token* instead of the
|
||||
secret value — the sanctioned way to move a value between processes (WP-0026 T02).
|
||||
The caller unwraps it in their own context (`bao unwrap`). Only valid for plain
|
||||
``bao kv get`` lanes; the whole secret is wrapped (a per-field ``-field`` read
|
||||
cannot be wrapped).
|
||||
"""
|
||||
if not is_bao_kv_fetch(entry):
|
||||
raise ProxyError(
|
||||
f"{entry.id!r} is not a plain `bao kv get` lane — response wrapping "
|
||||
"(--wrap) is unavailable. Use --out FILE or --exec instead."
|
||||
)
|
||||
target = path or entry.path_template
|
||||
if not target or _PLACEHOLDER.search(target):
|
||||
raise ProxyError(
|
||||
"--wrap needs a concrete path — supply --path or a resolved path_template."
|
||||
)
|
||||
return ResolvedFetch(argv=["bao", "kv", "get", f"-wrap-ttl={ttl}", "-format=json", target])
|
||||
|
||||
|
||||
def proxy_fetch_wrapped(resolved: ResolvedFetch) -> str:
|
||||
"""Run a wrapping fetch and return the wrapping *token* (not the secret value).
|
||||
|
||||
The token is single-use and short-lived; it is not itself the credential, so it
|
||||
is safe to hand back on stdout. Parses OpenBao's ``-format=json`` wrap_info.
|
||||
"""
|
||||
raw = _capture_value(resolved)
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
token = data["wrap_info"]["token"]
|
||||
except (json.JSONDecodeError, KeyError, TypeError) as e:
|
||||
raise ProxyError(
|
||||
"could not parse a wrapping token from the fetch output "
|
||||
"(is response wrapping supported for this path?)."
|
||||
) from e
|
||||
if not token:
|
||||
raise ProxyError("empty wrapping token returned.")
|
||||
return str(token)
|
||||
|
||||
|
||||
def proxy_fetch_fingerprint(resolved: ResolvedFetch):
|
||||
"""Fetch the value and return a masked fingerprint — never the value (T03).
|
||||
|
||||
Defense-in-depth status view: lets an operator confirm presence/length and
|
||||
compare a short non-reversible hash without disclosing the secret. The value
|
||||
transits warden's memory only to be hashed, and is scrubbed immediately.
|
||||
"""
|
||||
from warden.mask import fingerprint
|
||||
|
||||
value = _capture_value(resolved)
|
||||
try:
|
||||
return fingerprint(value)
|
||||
finally:
|
||||
value = "" # noqa: F841 — best-effort scrub
|
||||
|
||||
|
||||
def proxy_exec(resolved: ResolvedFetch, *, env_var: str, child_argv: List[str]) -> int:
|
||||
"""Fetch the value and inject it into a child command's environment only.
|
||||
|
||||
|
|
|
|||
|
|
@ -5,13 +5,22 @@ subsystem. It loads the machine-readable routing catalog and answers "who owns
|
|||
this need and where is the authoritative doc". The one lane ops-warden executes
|
||||
(SSH certificate issuance) is the only entry that carries authored steps.
|
||||
"""
|
||||
from warden.routing.catalog import Catalog, CatalogError, find_catalog_path, load_catalog
|
||||
from warden.routing.models import RouteEntry
|
||||
from warden.routing.catalog import (
|
||||
Catalog,
|
||||
CatalogError,
|
||||
CatalogFreshness,
|
||||
find_catalog_path,
|
||||
load_catalog,
|
||||
)
|
||||
from warden.routing.models import Delegation, RouteEntry, WorkloadReference
|
||||
|
||||
__all__ = [
|
||||
"Catalog",
|
||||
"CatalogError",
|
||||
"CatalogFreshness",
|
||||
"Delegation",
|
||||
"RouteEntry",
|
||||
"WorkloadReference",
|
||||
"find_catalog_path",
|
||||
"load_catalog",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -13,16 +13,25 @@ never restates another subsystem's procedure.
|
|||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from datetime import date
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import date, datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
from warden.routing.models import RouteEntry
|
||||
from warden.routing.models import (
|
||||
VALID_DELEGATION_MODES,
|
||||
VALID_RISK,
|
||||
VALID_WORKLOAD_APPLICABILITY,
|
||||
Delegation,
|
||||
RotationGuide,
|
||||
RouteEntry,
|
||||
WorkloadReference,
|
||||
)
|
||||
|
||||
# Structured handoff string fields (WP-0014) — templates and pointers only.
|
||||
# Every one is scanned for accidental secret material; see _assert_no_secret_material.
|
||||
|
|
@ -57,13 +66,53 @@ _REQUIRED_FIELDS = (
|
|||
"canon_ref",
|
||||
"reviewed",
|
||||
"status",
|
||||
"workload_ref",
|
||||
)
|
||||
_VALID_STATUS = ("active", "draft")
|
||||
_VALID_LANES = ("secret", "login")
|
||||
_VALID_LANES = ("secret", "login", "ceremony")
|
||||
_VALID_ROTATION_METHODS = ("rotate", "re-establish")
|
||||
|
||||
# Default review cadence — see wiki/AccessRouting.md#drift-review-cadence
|
||||
# Default review cadence for a catalog pointer — "is this still the right owner
|
||||
# and page?" That is a genuinely quarterly question, so 90 days is right for it.
|
||||
# See wiki/AccessRouting.md#drift-review-cadence
|
||||
DEFAULT_STALE_DAYS = 90
|
||||
|
||||
# Cadence for an interim lane's *blocker*, which is a different kind of claim
|
||||
# with a much shorter half-life: "has the intended owner answered / can they
|
||||
# front this yet?" (WARDEN-WP-0033-T05).
|
||||
#
|
||||
# 14 rather than 90 because 90 was never a loose default, it was an inert one --
|
||||
# the delegation register was created 2026-08-15, so a 90-day threshold could not
|
||||
# fire before November and never had. Calibrated instead against blockers that
|
||||
# actually went stale: the secrets-engine lanes cost ten days, RISK-F-0001
|
||||
# invalidated an ops-warden blocker in one, and the FLEX-WP-0007 claim was
|
||||
# repeated by two repos for roughly fifty. 14 catches the ten-day cases and, at
|
||||
# ~15 interim lanes, surfaces about one lane a day rather than a wall of them.
|
||||
DEFAULT_BLOCKER_STALE_DAYS = 14
|
||||
|
||||
# Scaled by the lane's own risk grade, matching risk-nexus's stall windows
|
||||
# (14d critical/high, 30d medium, 60d low — docs/method/check-procedure.md).
|
||||
# They offered the convention rather than a joint tool: point `warden route gaps`
|
||||
# at the same windows and the two registers agree without a shared mechanism.
|
||||
#
|
||||
# `ungraded` gets the shortest window, not the longest. ADR-0007 already decided
|
||||
# an absent grade is a defect and ADR-0008 that a grade covers the whole path;
|
||||
# a lane nobody has graded is exactly the one whose blocker is least trustworthy.
|
||||
BLOCKER_STALE_DAYS_BY_RISK = {
|
||||
"high": 14,
|
||||
"ungraded": 14,
|
||||
"standard": 30,
|
||||
"accepted": 60,
|
||||
"low": 60,
|
||||
}
|
||||
|
||||
|
||||
def blocker_stale_days(risk: Optional[str], override: Optional[int] = None) -> int:
|
||||
"""Days a lane's blocker may go unverified, scaled by what the lane holds."""
|
||||
if override is not None:
|
||||
return override
|
||||
return BLOCKER_STALE_DAYS_BY_RISK.get(risk or "ungraded", DEFAULT_BLOCKER_STALE_DAYS)
|
||||
|
||||
|
||||
def days_since_review(reviewed: str, *, today: Optional[date] = None) -> int:
|
||||
"""Calendar days between reviewed date (YYYY-MM-DD) and today."""
|
||||
|
|
@ -111,6 +160,45 @@ def find_catalog_path(start: Optional[Path] = None) -> Path:
|
|||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CatalogFreshness:
|
||||
"""Install vs source freshness for the routing catalog (WARDEN-WP-0029 T05).
|
||||
|
||||
Surfaces the path that was loaded, whether it is the wheel-bundled fallback
|
||||
(the stale-CLI failure mode), a content hash, and entry review age. Never
|
||||
carries secret material.
|
||||
"""
|
||||
|
||||
path: str
|
||||
source: str # "override" | "repo" | "bundled"
|
||||
content_hash: str
|
||||
mtime_iso: str
|
||||
package_version: str
|
||||
entry_count: int
|
||||
active_count: int
|
||||
newest_reviewed: Optional[str]
|
||||
oldest_reviewed: Optional[str]
|
||||
stale_entry_count: int
|
||||
using_bundled: bool
|
||||
warnings: List[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"path": self.path,
|
||||
"source": self.source,
|
||||
"content_hash": self.content_hash,
|
||||
"mtime_iso": self.mtime_iso,
|
||||
"package_version": self.package_version,
|
||||
"entry_count": self.entry_count,
|
||||
"active_count": self.active_count,
|
||||
"newest_reviewed": self.newest_reviewed,
|
||||
"oldest_reviewed": self.oldest_reviewed,
|
||||
"stale_entry_count": self.stale_entry_count,
|
||||
"using_bundled": self.using_bundled,
|
||||
"warnings": list(self.warnings),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Catalog:
|
||||
path: Path
|
||||
|
|
@ -160,8 +248,131 @@ class Catalog:
|
|||
if is_review_stale(e.reviewed, threshold_days=threshold_days, today=today)
|
||||
]
|
||||
|
||||
def gaps(self, include_draft: bool = False) -> List[RouteEntry]:
|
||||
"""Interim lanes — the queryable delegation register (WARDEN-WP-0030)."""
|
||||
return [e for e in self.listed(include_draft=include_draft) if e.is_interim]
|
||||
|
||||
def _assert_no_secret_material(entry_id: str, field_name: str, value: str) -> None:
|
||||
def stale_gaps(
|
||||
self,
|
||||
include_draft: bool = False,
|
||||
threshold_days: Optional[int] = None,
|
||||
*,
|
||||
today: Optional[date] = None,
|
||||
) -> List[RouteEntry]:
|
||||
"""Interim lanes whose blocker is due a re-check.
|
||||
|
||||
The window scales with the lane's risk grade unless `threshold_days`
|
||||
overrides it -- a blocker on a lane holding an admin PAT should not go
|
||||
unverified as long as one on a low-risk pointer.
|
||||
|
||||
A lane counts as stale when its review date is past the threshold **or**
|
||||
when the review was never a verification at all. An `asked-and-waiting`
|
||||
entry is the case that motivated this: it looks freshly reviewed on the
|
||||
day the question is asked and stays that way while nobody answers.
|
||||
"""
|
||||
out: List[RouteEntry] = []
|
||||
for e in self.gaps(include_draft=include_draft):
|
||||
d = e.effective_delegation
|
||||
reviewed = d.reviewed or e.reviewed
|
||||
window = blocker_stale_days(e.risk, threshold_days)
|
||||
if is_review_stale(reviewed, threshold_days=window, today=today):
|
||||
out.append(e)
|
||||
elif d.verified is not None and not d.is_verified:
|
||||
out.append(e)
|
||||
return out
|
||||
|
||||
def freshness(
|
||||
self,
|
||||
*,
|
||||
stale_threshold_days: int = DEFAULT_STALE_DAYS,
|
||||
today: Optional[date] = None,
|
||||
) -> CatalogFreshness:
|
||||
"""Describe which catalog was loaded and how fresh it is (WP-0029 T05)."""
|
||||
path = self.path.resolve()
|
||||
text = path.read_text(encoding="utf-8") if path.exists() else ""
|
||||
digest = hashlib.sha256(text.encode("utf-8")).hexdigest()[:12]
|
||||
mtime_iso = ""
|
||||
if path.exists():
|
||||
mtime_iso = datetime.fromtimestamp(
|
||||
path.stat().st_mtime, tz=timezone.utc
|
||||
).isoformat()
|
||||
|
||||
source = _classify_catalog_source(path)
|
||||
using_bundled = source == "bundled"
|
||||
reviewed_dates = [e.reviewed for e in self.entries if e.reviewed]
|
||||
newest = max(reviewed_dates) if reviewed_dates else None
|
||||
oldest = min(reviewed_dates) if reviewed_dates else None
|
||||
stale_count = len(self.stale(include_draft=True, threshold_days=stale_threshold_days, today=today))
|
||||
|
||||
package_version = _package_version()
|
||||
warnings: List[str] = []
|
||||
if using_bundled:
|
||||
warnings.append(
|
||||
"using wheel-bundled catalog fallback — reinstall from checkout "
|
||||
"(`uv tool install -e .` or `pip install -e .`) if lanes look missing"
|
||||
)
|
||||
if stale_count:
|
||||
warnings.append(
|
||||
f"{stale_count} catalog entr{'y' if stale_count == 1 else 'ies'} "
|
||||
f"past {stale_threshold_days}d review cadence"
|
||||
)
|
||||
# Interim blockers run on their own, much shorter cadence -- a stale
|
||||
# pointer and an unanswered blocker are not the same kind of drift.
|
||||
stale_interim = len(self.stale_gaps(include_draft=True, today=today))
|
||||
if stale_interim:
|
||||
warnings.append(
|
||||
f"{stale_interim} interim delegation"
|
||||
f"{'' if stale_interim == 1 else 's'} need re-verifying "
|
||||
f"(risk-scaled blocker cadence) — see `warden route gaps`"
|
||||
)
|
||||
|
||||
return CatalogFreshness(
|
||||
path=str(path),
|
||||
source=source,
|
||||
content_hash=digest,
|
||||
mtime_iso=mtime_iso,
|
||||
package_version=package_version,
|
||||
entry_count=len(self.entries),
|
||||
active_count=len(self.listed(include_draft=False)),
|
||||
newest_reviewed=newest,
|
||||
oldest_reviewed=oldest,
|
||||
stale_entry_count=stale_count,
|
||||
using_bundled=using_bundled,
|
||||
warnings=warnings,
|
||||
)
|
||||
|
||||
|
||||
def _package_version() -> str:
|
||||
try:
|
||||
from importlib.metadata import version
|
||||
|
||||
return version("ops-warden")
|
||||
except Exception: # noqa: BLE001
|
||||
try:
|
||||
from warden import __version__
|
||||
|
||||
return str(__version__)
|
||||
except Exception: # noqa: BLE001
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _classify_catalog_source(path: Path) -> str:
|
||||
"""Classify catalog load path for freshness warnings."""
|
||||
if os.environ.get("WARDEN_ROUTING_CATALOG"):
|
||||
return "override"
|
||||
resolved = str(path.resolve())
|
||||
if "/_registry/" in resolved or resolved.endswith("/warden/_registry/routing/catalog.yaml"):
|
||||
return "bundled"
|
||||
# hatch force-include places registry at warden/_registry
|
||||
parts = path.resolve().parts
|
||||
if "_registry" in parts:
|
||||
return "bundled"
|
||||
return "repo"
|
||||
|
||||
|
||||
def _assert_no_secret_material(
|
||||
entry_id: str, field_name: str, value: str, *, prose: bool = False
|
||||
) -> None:
|
||||
"""Reject a handoff field that appears to embed a literal secret value.
|
||||
|
||||
The structured handoff fields are command/path *templates*: concrete values
|
||||
|
|
@ -169,15 +380,22 @@ def _assert_no_secret_material(entry_id: str, field_name: str, value: str) -> No
|
|||
catalog is git-tracked and agent-visible, so a leaked value here is the exact
|
||||
custody failure WP-0014 forbids. We screen for known token prefixes and for a
|
||||
long high-entropy run that is not a placeholder.
|
||||
|
||||
``prose=True`` (rotation guidance steps, WP-0026 T06) skips the *substring*
|
||||
prefix screen — short prefixes like ``s.`` or ``eyJ`` collide with ordinary
|
||||
English ("exists.", "artifacts.") — and relies on the high-entropy-run detector,
|
||||
which catches an actually-pasted token (a real ``hvs.``/``ghp_``/``sk-`` value
|
||||
carries a long high-entropy tail) while allowing plain sentences.
|
||||
"""
|
||||
lowered = value.lower()
|
||||
for prefix in _SECRET_PREFIXES:
|
||||
if prefix.lower() in lowered:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} field {field_name!r} appears to contain a literal "
|
||||
f"secret (matched {prefix!r}). Handoff fields are templates — use "
|
||||
"placeholders like <FIELD>/<PATH>, never a real value."
|
||||
)
|
||||
if not prose:
|
||||
for prefix in _SECRET_PREFIXES:
|
||||
if prefix.lower() in lowered:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} field {field_name!r} appears to contain a literal "
|
||||
f"secret (matched {prefix!r}). Handoff fields are templates — use "
|
||||
"placeholders like <FIELD>/<PATH>, never a real value."
|
||||
)
|
||||
for run in _HIGH_ENTROPY_RUN.findall(value):
|
||||
# Allow long placeholder/path/identifier tokens; flag anything else.
|
||||
if "<" in run or ">" in run:
|
||||
|
|
@ -190,6 +408,193 @@ def _assert_no_secret_material(entry_id: str, field_name: str, value: str) -> No
|
|||
)
|
||||
|
||||
|
||||
def _parse_rotation(entry_id: str, raw: Optional[dict]) -> Optional[RotationGuide]:
|
||||
"""Parse and validate an optional ``rotation:`` block (WP-0026 T06).
|
||||
|
||||
Advisory renewal guidance only — screened for secret material like every other
|
||||
catalog string. ``method`` must be rotate | re-establish; ``steps`` a non-empty
|
||||
list; ``owner`` required.
|
||||
"""
|
||||
if raw is None:
|
||||
return None
|
||||
if not isinstance(raw, dict):
|
||||
raise CatalogError(f"entry {entry_id!r} `rotation` must be a mapping")
|
||||
|
||||
method = str(raw.get("method", "")).strip()
|
||||
if method not in _VALID_ROTATION_METHODS:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} rotation.method {method!r} invalid "
|
||||
f"(expected one of {_VALID_ROTATION_METHODS})"
|
||||
)
|
||||
|
||||
steps_raw = raw.get("steps")
|
||||
if not isinstance(steps_raw, list) or not steps_raw:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} rotation.steps must be a non-empty list of steps"
|
||||
)
|
||||
steps = [str(s) for s in steps_raw]
|
||||
|
||||
owner = str(raw.get("owner", "")).strip()
|
||||
if not owner:
|
||||
raise CatalogError(f"entry {entry_id!r} rotation.owner is required")
|
||||
|
||||
# Screen advisory prose for accidental secret material (git-tracked, agent-visible).
|
||||
for i, step in enumerate(steps):
|
||||
_assert_no_secret_material(entry_id, f"rotation.steps[{i}]", step, prose=True)
|
||||
_assert_no_secret_material(entry_id, "rotation.owner", owner, prose=True)
|
||||
|
||||
return RotationGuide(
|
||||
method=method,
|
||||
steps=steps,
|
||||
owner=owner,
|
||||
automatable=bool(raw.get("automatable", False)),
|
||||
)
|
||||
|
||||
|
||||
def _parse_delegation(entry_id: str, raw: Optional[dict]) -> Optional[Delegation]:
|
||||
"""Parse an optional ``delegation:`` block (WARDEN-WP-0030).
|
||||
|
||||
Absence is allowed: the loader treats it as implicit interim with an
|
||||
unknown owner. When the block *is* present, mode / owner / blocker rules
|
||||
are enforced so a declared answer cannot be incomplete.
|
||||
"""
|
||||
if raw is None:
|
||||
return None
|
||||
if not isinstance(raw, dict):
|
||||
raise CatalogError(f"entry {entry_id!r} `delegation` must be a mapping")
|
||||
|
||||
mode = str(raw.get("mode", "")).strip()
|
||||
if mode not in VALID_DELEGATION_MODES:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} delegation.mode {mode!r} invalid "
|
||||
f"(expected one of {VALID_DELEGATION_MODES})"
|
||||
)
|
||||
|
||||
intended_owner = str(raw.get("intended_owner", "")).strip() or None
|
||||
if mode != "permanent" and not intended_owner:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} delegation.intended_owner is required "
|
||||
f"unless mode is permanent"
|
||||
)
|
||||
|
||||
blocked_on = str(raw.get("blocked_on", "")).strip() or None
|
||||
if mode == "interim" and not blocked_on:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} delegation.blocked_on is required when mode is interim"
|
||||
)
|
||||
|
||||
reviewed = str(raw.get("reviewed", "")).strip() or None
|
||||
if not reviewed:
|
||||
raise CatalogError(f"entry {entry_id!r} delegation.reviewed is required")
|
||||
try:
|
||||
date.fromisoformat(reviewed)
|
||||
except ValueError as e:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} delegation.reviewed {reviewed!r} is not YYYY-MM-DD"
|
||||
) from e
|
||||
|
||||
if intended_owner:
|
||||
_assert_no_secret_material(
|
||||
entry_id, "delegation.intended_owner", intended_owner, prose=True
|
||||
)
|
||||
if blocked_on:
|
||||
_assert_no_secret_material(
|
||||
entry_id, "delegation.blocked_on", blocked_on, prose=True
|
||||
)
|
||||
|
||||
verified = str(raw.get("verified", "")).strip() or None
|
||||
if verified is not None and verified not in Delegation.VERIFICATION_METHODS:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} delegation.verified {verified!r} invalid "
|
||||
f"(expected one of {Delegation.VERIFICATION_METHODS})"
|
||||
)
|
||||
|
||||
return Delegation(
|
||||
mode=mode,
|
||||
intended_owner=intended_owner,
|
||||
blocked_on=blocked_on,
|
||||
reviewed=reviewed,
|
||||
verified=verified,
|
||||
implicit=False,
|
||||
)
|
||||
|
||||
|
||||
def _parse_workload_ref(entry_id: str, raw: object) -> WorkloadReference:
|
||||
"""Parse an explicit workload join without attempting identity inference."""
|
||||
if not isinstance(raw, dict):
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} workload_ref must be a mapping; every lane must "
|
||||
"declare applicable or not-applicable"
|
||||
)
|
||||
|
||||
applicability = str(raw.get("applicability", "")).strip()
|
||||
if applicability not in VALID_WORKLOAD_APPLICABILITY:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} workload_ref.applicability {applicability!r} invalid "
|
||||
f"(expected one of {VALID_WORKLOAD_APPLICABILITY})"
|
||||
)
|
||||
|
||||
def optional(name: str) -> Optional[str]:
|
||||
value = raw.get(name)
|
||||
return str(value).strip() if value is not None and str(value).strip() else None
|
||||
|
||||
ref = WorkloadReference(
|
||||
applicability=applicability,
|
||||
rapp_id=optional("rapp_id"),
|
||||
name=optional("name"),
|
||||
deployable=optional("deployable"),
|
||||
declaration_ref=optional("declaration_ref"),
|
||||
reason=optional("reason"),
|
||||
unknown_reason=optional("unknown_reason"),
|
||||
)
|
||||
|
||||
target_fields = (ref.rapp_id, ref.name, ref.deployable, ref.declaration_ref)
|
||||
if applicability == "not-applicable":
|
||||
if not ref.reason:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} workload_ref.reason is required for not-applicable"
|
||||
)
|
||||
if any(target_fields) or ref.unknown_reason:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} not-applicable workload_ref must not carry a "
|
||||
"workload target or unknown_reason"
|
||||
)
|
||||
return ref
|
||||
|
||||
if ref.unknown_reason:
|
||||
if any(target_fields) or ref.reason:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} unknown workload_ref must carry only "
|
||||
"applicability and unknown_reason"
|
||||
)
|
||||
return ref
|
||||
|
||||
if not ref.name:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} applicable workload_ref requires name or "
|
||||
"unknown_reason"
|
||||
)
|
||||
if ref.rapp_id:
|
||||
if ref.declaration_ref:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} managed workload_ref must not also carry "
|
||||
"declaration_ref"
|
||||
)
|
||||
elif not ref.declaration_ref:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} operational workload_ref requires declaration_ref"
|
||||
)
|
||||
if ref.deployable and not ref.rapp_id:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} workload_ref.deployable requires rapp_id"
|
||||
)
|
||||
if ref.reason:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} applicable workload_ref must not carry reason"
|
||||
)
|
||||
return ref
|
||||
|
||||
|
||||
def _parse_entry(raw: dict, index: int) -> RouteEntry:
|
||||
if not isinstance(raw, dict):
|
||||
raise CatalogError(f"entry #{index} is not a mapping")
|
||||
|
|
@ -250,6 +655,16 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry:
|
|||
f"entry {entry_id!r} has invalid lane {lane!r} (expected one of {_VALID_LANES})"
|
||||
)
|
||||
|
||||
risk_value = raw.get("risk")
|
||||
risk = str(risk_value).strip() if risk_value is not None else "ungraded"
|
||||
risk = risk or "ungraded"
|
||||
if risk != "ungraded" and risk not in VALID_RISK:
|
||||
raise CatalogError(
|
||||
f"entry {entry_id!r} has invalid risk {risk!r} (expected one of {VALID_RISK})"
|
||||
)
|
||||
|
||||
workload_ref = _parse_workload_ref(entry_id, raw.get("workload_ref"))
|
||||
|
||||
return RouteEntry(
|
||||
id=entry_id,
|
||||
title=str(raw["title"]),
|
||||
|
|
@ -261,6 +676,7 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry:
|
|||
canon_ref=str(raw["canon_ref"]),
|
||||
reviewed=str(raw["reviewed"]),
|
||||
status=status,
|
||||
workload_ref=workload_ref,
|
||||
steps=[str(s) for s in steps],
|
||||
cert_command=str(cert_command) if cert_command else None,
|
||||
auth_method=handoff["auth_method"],
|
||||
|
|
@ -272,6 +688,9 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry:
|
|||
exec_owner=str(raw["exec_owner"]) if raw.get("exec_owner") else None,
|
||||
exec_command=handoff["exec_command"],
|
||||
pointer_command=handoff["pointer_command"],
|
||||
rotation=_parse_rotation(entry_id, raw.get("rotation")),
|
||||
risk=risk,
|
||||
delegation=_parse_delegation(entry_id, raw.get("delegation")),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,136 @@ from dataclasses import dataclass, field
|
|||
from typing import List, Optional
|
||||
|
||||
|
||||
# Risk grade vocabulary (ADR-0007). Grades outside LOW_RISK_GRADES — including
|
||||
# the "ungraded" default and any value from a newer catalog — are treated as
|
||||
# high by is_high_risk, so the read-boundary fails safe in both directions.
|
||||
LOW_RISK_GRADES = frozenset({"standard", "low", "accepted"})
|
||||
GRADED_RISK = frozenset({"standard", "low", "accepted", "high", "critical"})
|
||||
|
||||
|
||||
@dataclass
|
||||
class RotationGuide:
|
||||
"""Structured-but-advisory renewal guidance for a lane (WARDEN-WP-0026 T06).
|
||||
|
||||
Held in the ops-warden registry, never in OpenBao. ``steps`` are authored
|
||||
advisory prose (screened for secret material like every catalog string) — they
|
||||
tell an operator *how* to renew, they are not executed here. ``method`` is
|
||||
``rotate`` (provider re-mints the same kind of credential) or ``re-establish``
|
||||
(regenerate from source, e.g. a new age keypair + re-encrypt). ``automatable``
|
||||
is a hint for a future Strand-B executable driver (WARDEN-WP-0027).
|
||||
"""
|
||||
method: str # "rotate" | "re-establish"
|
||||
steps: List[str]
|
||||
owner: str
|
||||
automatable: bool = False
|
||||
|
||||
|
||||
# Risk classes for agent read-boundary (WARDEN-WP-0026 T04).
|
||||
# high — recovery escrow, upload tokens, admin PATs, high-spend provider keys.
|
||||
# Agent identities must not hold raw data-read (metadata/capabilities only).
|
||||
# standard — ordinary workload secrets (ESO-fed, non-escrow); normal least-privilege.
|
||||
VALID_RISK = ("standard", "high")
|
||||
|
||||
# Delegation modes (WARDEN-WP-0030). Absence of a block is implicit interim.
|
||||
# native — intended owner already fronts the lane (route-primary / pointer)
|
||||
# interim — ops-warden covers a gap; intended_owner + blocked_on required
|
||||
# permanent — ops-warden is the designed owner of this front door (SSH only today)
|
||||
VALID_DELEGATION_MODES = ("native", "interim", "permanent")
|
||||
VALID_WORKLOAD_APPLICABILITY = ("applicable", "not-applicable")
|
||||
|
||||
IMPLICIT_DELEGATION_BLOCKED_ON = (
|
||||
"unclassified — no delegation block; treat as a question, not a settlement"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Delegation:
|
||||
"""Who should own this front door, and what is missing (WARDEN-WP-0030).
|
||||
|
||||
Pointer-layer only: names the intended owner and the blocker. Does not
|
||||
restate how that owner will implement their front door.
|
||||
"""
|
||||
|
||||
mode: str # native | interim | permanent
|
||||
intended_owner: Optional[str] = None
|
||||
blocked_on: Optional[str] = None
|
||||
reviewed: Optional[str] = None
|
||||
verified: Optional[str] = None
|
||||
implicit: bool = False
|
||||
|
||||
#: How `reviewed` was established. The distinction exists because a date
|
||||
#: bumped by editing the entry looks identical to one bumped by re-checking
|
||||
#: the blocker, and on 2026-08-21 six lanes read as freshly reviewed when
|
||||
#: only some had actually been re-verified (WARDEN-WP-0033-T05).
|
||||
#:
|
||||
#: `asked-and-waiting` deliberately does NOT count as verification: it is the
|
||||
#: state the secrets-engine lanes sat in for ten days while looking fresh.
|
||||
VERIFICATION_METHODS = (
|
||||
"owner-confirmed", # the intended owner stated the blocker's status
|
||||
"source-read", # re-derived from the owner's code, canon, or CCR
|
||||
"asked-and-waiting", # a question is outstanding — NOT verification
|
||||
"unverified", # carried forward without a check
|
||||
)
|
||||
#: Methods that mean the claim was actually re-established.
|
||||
VERIFYING_METHODS = ("owner-confirmed", "source-read")
|
||||
|
||||
@property
|
||||
def is_verified(self) -> bool:
|
||||
"""True only when the blocker was re-established, not merely re-edited."""
|
||||
return self.verified in self.VERIFYING_METHODS
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"mode": self.mode,
|
||||
"intended_owner": self.intended_owner,
|
||||
"blocked_on": self.blocked_on,
|
||||
"reviewed": self.reviewed,
|
||||
"verified": self.verified,
|
||||
"is_verified": self.is_verified,
|
||||
"implicit": self.implicit,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WorkloadReference:
|
||||
"""Authoritative workload join for a catalog lane (WARDEN-WP-0032).
|
||||
|
||||
Managed deployables use the Repo Manager v1 ``rapp_id``/``name`` tuple.
|
||||
Independently governed operational workloads use ``name`` plus an owner
|
||||
declaration reference. An applicable lane whose owner has not published an
|
||||
identity remains explicitly ``unknown``; it is never inferred from the
|
||||
credential path or repository name.
|
||||
"""
|
||||
|
||||
applicability: str # applicable | not-applicable
|
||||
rapp_id: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
deployable: Optional[str] = None
|
||||
declaration_ref: Optional[str] = None
|
||||
reason: Optional[str] = None
|
||||
unknown_reason: Optional[str] = None
|
||||
|
||||
@property
|
||||
def resolution(self) -> str:
|
||||
if self.applicability == "not-applicable":
|
||||
return "not-applicable"
|
||||
if self.unknown_reason:
|
||||
return "unknown"
|
||||
return "resolved"
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"applicability": self.applicability,
|
||||
"rapp_id": self.rapp_id,
|
||||
"name": self.name,
|
||||
"deployable": self.deployable,
|
||||
"declaration_ref": self.declaration_ref,
|
||||
"reason": self.reason,
|
||||
"unknown_reason": self.unknown_reason,
|
||||
"resolution": self.resolution,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class RouteEntry:
|
||||
id: str
|
||||
|
|
@ -23,6 +153,8 @@ class RouteEntry:
|
|||
canon_ref: str
|
||||
reviewed: str
|
||||
status: str # "active" | "draft"
|
||||
# Explicit workload applicability and authoritative join. Never inferred.
|
||||
workload_ref: Optional[WorkloadReference] = None
|
||||
# SSH lane only — None/empty for routed (non-executed) needs.
|
||||
steps: List[str] = field(default_factory=list)
|
||||
cert_command: Optional[str] = None
|
||||
|
|
@ -42,6 +174,8 @@ class RouteEntry:
|
|||
# "login" — interactive auth bootstrap (OIDC/MFA). No secret-read gate (you have
|
||||
# no identity yet), no caller-auth precheck (the point is to get one),
|
||||
# run interactively as the caller; warden never captures the token.
|
||||
# "ceremony" — attended owner operation such as Shamir seal/unseal. It is a
|
||||
# pointer plus approval boundary, never an executable access lane.
|
||||
lane: str = "secret"
|
||||
# Owner-native exec front door (WP-0019). When `exec_owner` is set, that subsystem
|
||||
# (e.g. secrets-engine) provides the PRIMARY way to run a secret-backed command; the
|
||||
|
|
@ -50,16 +184,109 @@ class RouteEntry:
|
|||
exec_owner: Optional[str] = None # subsystem owning the native exec (e.g. secrets-engine)
|
||||
exec_command: Optional[str] = None # e.g. "secrets-engine exec --catalog <id> -- <cmd>"
|
||||
pointer_command: Optional[str] = None # e.g. "secrets-engine route <id> --json"
|
||||
# Rotation / re-establishment guidance (WP-0026 T06) — advisory, no secret values.
|
||||
rotation: Optional[RotationGuide] = None
|
||||
# Agent read-boundary risk class (WP-0026 T04). high → agents use wrap/out/exec only.
|
||||
# Default is "ungraded", which FAILS SAFE: it is treated as high. Before
|
||||
# ADR-0007 this defaulted to "standard", so a lane that simply omitted the
|
||||
# field was silently placed outside the read-boundary (RISK-F-0003) — the
|
||||
# control was never relaxed by decision, it was never reached.
|
||||
risk: str = "ungraded" # "standard" | "high" | "ungraded"
|
||||
# Delegation register (WP-0030). None → implicit interim with unknown owner.
|
||||
delegation: Optional[Delegation] = None
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool:
|
||||
return self.status == "active"
|
||||
|
||||
@property
|
||||
def is_graded(self) -> bool:
|
||||
"""False when this lane carries no explicit risk grade (ADR-0007)."""
|
||||
return self.risk in GRADED_RISK
|
||||
|
||||
@property
|
||||
def is_high_risk(self) -> bool:
|
||||
"""True when this lane is on the agent raw-read deny list (WP-0026 T04).
|
||||
|
||||
Anything not explicitly graded low is high. An ungraded lane, or one
|
||||
carrying a grade this version does not recognise, is treated as high
|
||||
rather than waved through — ADR-0007: absence is not a grade.
|
||||
"""
|
||||
return self.risk not in LOW_RISK_GRADES
|
||||
|
||||
def risk_for_zone(
|
||||
self,
|
||||
*,
|
||||
effective_zone: str = "unknown",
|
||||
admission: str = "unknown",
|
||||
synthetic_only: bool = False,
|
||||
) -> str:
|
||||
"""Resolve an absent grade using security-zones_v0.1 section 5.1.
|
||||
|
||||
Explicit grades always win. The sole lower default is a satisfied
|
||||
``z0-experimental`` workload proven synthetic-only. Every other zone,
|
||||
failed/unknown admission, and missing context fails safe to at least
|
||||
``high``; z3 reports ``critical`` (which the read boundary treats as
|
||||
high). Catalog CI still requires explicit grades, so this is the safe
|
||||
runtime behavior for malformed or newer external catalogs.
|
||||
"""
|
||||
if self.is_graded:
|
||||
return self.risk
|
||||
if (
|
||||
effective_zone == "z0-experimental"
|
||||
and admission == "satisfied"
|
||||
and synthetic_only
|
||||
):
|
||||
return "standard"
|
||||
if effective_zone == "z3-critical" and admission == "satisfied":
|
||||
return "critical"
|
||||
return "high"
|
||||
|
||||
@property
|
||||
def has_rotation(self) -> bool:
|
||||
"""True when this lane carries renewal guidance (WP-0026 T06)."""
|
||||
return self.rotation is not None
|
||||
|
||||
@property
|
||||
def vends_secret(self) -> bool:
|
||||
"""True when this lane hands back a rotatable static secret value.
|
||||
|
||||
Rotation guidance (WP-0026 T06) applies to these. It excludes the SSH lane
|
||||
(short-lived certs — renewal is re-issuance), ``login`` lanes (re-auth, no
|
||||
stored value), and pure routing pointers with no secret path (tunnel,
|
||||
principals, emission sinks, policy checks).
|
||||
"""
|
||||
if self.warden_executes or self.lane != "secret":
|
||||
return False
|
||||
return bool(self.path_template or self.fetch_command or self.exec_owner)
|
||||
|
||||
@property
|
||||
def has_native_exec(self) -> bool:
|
||||
"""True when an owner-native exec front door is the primary path for this lane."""
|
||||
return bool(self.exec_owner and self.exec_command)
|
||||
|
||||
@property
|
||||
def effective_delegation(self) -> Delegation:
|
||||
"""Declared delegation, or implicit interim with an unknown owner.
|
||||
|
||||
Absence of a ``delegation:`` block is a question (WP-0030), not a
|
||||
settlement that ops-warden owns the front door.
|
||||
"""
|
||||
if self.delegation is not None:
|
||||
return self.delegation
|
||||
return Delegation(
|
||||
mode="interim",
|
||||
intended_owner=None,
|
||||
blocked_on=IMPLICIT_DELEGATION_BLOCKED_ON,
|
||||
reviewed=None,
|
||||
implicit=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_interim(self) -> bool:
|
||||
"""True when this lane is a tracked gap (explicit or implicit)."""
|
||||
return self.effective_delegation.mode == "interim"
|
||||
|
||||
@property
|
||||
def has_handoff(self) -> bool:
|
||||
"""True when structured assist fields are present (advisory richness)."""
|
||||
|
|
|
|||
|
|
@ -152,6 +152,107 @@ def check_file_permissions(state_dir: Path) -> CheckResult:
|
|||
)
|
||||
|
||||
|
||||
def check_catalog_rotation_coverage() -> CheckResult:
|
||||
"""Every active secret-vending catalog lane must carry rotation guidance (T06).
|
||||
|
||||
A lane an operator can obtain a *secret value* through must also tell them how
|
||||
to renew or re-establish it. Scoped to ``vends_secret`` lanes: this exempts the
|
||||
SSH lane (short-lived certs — renewal is re-issuance), ``login`` lanes (re-auth,
|
||||
no stored value), and pure routing pointers (tunnel, principals, emission
|
||||
sinks, policy checks) with no secret to rotate. Draft lanes are exempt until
|
||||
promoted.
|
||||
"""
|
||||
try:
|
||||
from warden.routing import load_catalog
|
||||
catalog = load_catalog()
|
||||
except Exception as e: # noqa: BLE001 — catalog missing/invalid is its own signal
|
||||
return CheckResult(
|
||||
name="catalog_rotation_coverage",
|
||||
passed=False,
|
||||
detail=f"could not load routing catalog: {e}",
|
||||
)
|
||||
|
||||
missing = [
|
||||
e.id
|
||||
for e in catalog.entries
|
||||
if e.is_active and e.vends_secret and not e.has_rotation
|
||||
]
|
||||
return CheckResult(
|
||||
name="catalog_rotation_coverage",
|
||||
passed=len(missing) == 0,
|
||||
detail=(
|
||||
f"active lanes lacking rotation guidance: {missing} — add a `rotation:` "
|
||||
"block (see WARDEN-WP-0026 T06)"
|
||||
if missing
|
||||
else "all active lanes carry rotation guidance"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def check_organization_posture() -> CheckResult:
|
||||
"""Surface declared organization lifecycle posture (WARDEN-WP-0029 T02).
|
||||
|
||||
Always informational PASS when descriptors load -- the check exists so operators
|
||||
and agents see the posture in scorecard output without hunting config files.
|
||||
"""
|
||||
try:
|
||||
from warden.posture import load_posture
|
||||
|
||||
cat = load_posture()
|
||||
org = cat.organization_posture
|
||||
except Exception as e: # noqa: BLE001
|
||||
return CheckResult(
|
||||
name="organization_posture",
|
||||
passed=False,
|
||||
detail=f"could not load organization posture: {e}",
|
||||
)
|
||||
relax = ", ".join(org.relaxations[:3])
|
||||
if len(org.relaxations) > 3:
|
||||
relax += ", ..."
|
||||
summary = org.summary[:120]
|
||||
if len(org.summary) > 120:
|
||||
summary += "..."
|
||||
relax_part = relax or "no relaxations listed"
|
||||
return CheckResult(
|
||||
name="organization_posture",
|
||||
passed=True,
|
||||
detail=f"{org.id} -- {summary} [{relax_part}]",
|
||||
)
|
||||
|
||||
|
||||
def check_catalog_freshness() -> CheckResult:
|
||||
"""Warn when the CLI is using a bundled (stale-risk) catalog (WP-0029 T05)."""
|
||||
try:
|
||||
from warden.routing import load_catalog
|
||||
|
||||
fresh = load_catalog().freshness()
|
||||
except Exception as e: # noqa: BLE001
|
||||
return CheckResult(
|
||||
name="catalog_freshness",
|
||||
passed=False,
|
||||
detail=f"could not load routing catalog: {e}",
|
||||
)
|
||||
if fresh.using_bundled:
|
||||
nwarn = len(fresh.warnings)
|
||||
return CheckResult(
|
||||
name="catalog_freshness",
|
||||
passed=False,
|
||||
detail=(
|
||||
f"bundled catalog hash={fresh.content_hash} - reinstall from checkout "
|
||||
f"if lanes look missing ({nwarn} warnings)"
|
||||
),
|
||||
)
|
||||
return CheckResult(
|
||||
name="catalog_freshness",
|
||||
passed=True,
|
||||
detail=(
|
||||
f"source={fresh.source} hash={fresh.content_hash} "
|
||||
f"entries={fresh.active_count}/{fresh.entry_count} active "
|
||||
f"newest_reviewed={fresh.newest_reviewed}"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def run_scorecard(state_dir: Path, inventory: PrincipalsInventory) -> List[CheckResult]:
|
||||
"""Run all cert-side scorecard checks. Returns list of CheckResult."""
|
||||
return [
|
||||
|
|
@ -161,4 +262,7 @@ def run_scorecard(state_dir: Path, inventory: PrincipalsInventory) -> List[Check
|
|||
check_no_stale_certs(state_dir),
|
||||
check_ttl_policy(state_dir, inventory),
|
||||
check_file_permissions(state_dir),
|
||||
check_catalog_rotation_coverage(),
|
||||
check_organization_posture(),
|
||||
check_catalog_freshness(),
|
||||
]
|
||||
|
|
|
|||
149
src/warden/taint.py
Normal file
149
src/warden/taint.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
"""EXPOSED taint convention for OpenBao KV secrets (WARDEN-WP-0026 T05).
|
||||
|
||||
Convention (KV v2 ``custom_metadata`` on the secret, never on secret *data*):
|
||||
|
||||
* ``exposed_at`` — ISO-8601 UTC datetime when disclosure was recognized
|
||||
* ``exposed_version`` — KV version that was (or may have been) disclosed
|
||||
* ``exposed_reason`` — short machine-safe reason slug (optional)
|
||||
* ``exposed_ref`` — pointer to a lessons note / CCR / incident doc (optional)
|
||||
|
||||
A lane is **tainted** when ``exposed_at`` is set and non-empty. Clearing taint
|
||||
(after rotation) is an operator action: remove those keys from custom_metadata.
|
||||
ops-warden only *reports* taint — it never auto-rotates (Strand B / WP-0027).
|
||||
|
||||
This module only shells out to ``bao kv metadata get`` (or equivalent). It never
|
||||
reads secret data values.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
|
||||
from warden.routing.models import RouteEntry
|
||||
|
||||
# Canonical custom_metadata keys (WP-0026 T05).
|
||||
EXPOSED_AT = "exposed_at"
|
||||
EXPOSED_VERSION = "exposed_version"
|
||||
EXPOSED_REASON = "exposed_reason"
|
||||
EXPOSED_REF = "exposed_ref"
|
||||
|
||||
_TAINT_KEYS = (EXPOSED_AT, EXPOSED_VERSION, EXPOSED_REASON, EXPOSED_REF)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TaintStatus:
|
||||
"""Advisory taint view for a lane — no secret values."""
|
||||
|
||||
lane_id: str
|
||||
path: str
|
||||
tainted: bool
|
||||
exposed_at: Optional[str] = None
|
||||
exposed_version: Optional[str] = None
|
||||
exposed_reason: Optional[str] = None
|
||||
exposed_ref: Optional[str] = None
|
||||
current_version: Optional[int] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"id": self.lane_id,
|
||||
"path": self.path,
|
||||
"tainted": self.tainted,
|
||||
"exposed_at": self.exposed_at,
|
||||
"exposed_version": self.exposed_version,
|
||||
"exposed_reason": self.exposed_reason,
|
||||
"exposed_ref": self.exposed_ref,
|
||||
"current_version": self.current_version,
|
||||
**({"error": self.error} if self.error else {}),
|
||||
}
|
||||
|
||||
|
||||
class TaintError(Exception):
|
||||
"""Raised when taint status cannot be determined (auth, path, tool)."""
|
||||
|
||||
|
||||
def kv_metadata_path(path_template: str) -> str:
|
||||
"""Return the logical KV path suitable for ``bao kv metadata get``.
|
||||
|
||||
Catalog paths are logical (``platform/workloads/...``), not API data paths.
|
||||
"""
|
||||
return path_template.strip().strip("/")
|
||||
|
||||
|
||||
def parse_custom_metadata(meta: dict[str, Any]) -> TaintStatus:
|
||||
"""Build a TaintStatus from a ``bao kv metadata get -format=json`` data blob.
|
||||
|
||||
``meta`` is the ``data`` object (with ``custom_metadata``, ``current_version``).
|
||||
"""
|
||||
custom = meta.get("custom_metadata") or {}
|
||||
if not isinstance(custom, dict):
|
||||
custom = {}
|
||||
exposed_at = (custom.get(EXPOSED_AT) or "").strip() or None
|
||||
return TaintStatus(
|
||||
lane_id="",
|
||||
path="",
|
||||
tainted=bool(exposed_at),
|
||||
exposed_at=exposed_at,
|
||||
exposed_version=(custom.get(EXPOSED_VERSION) or "").strip() or None,
|
||||
exposed_reason=(custom.get(EXPOSED_REASON) or "").strip() or None,
|
||||
exposed_ref=(custom.get(EXPOSED_REF) or "").strip() or None,
|
||||
current_version=meta.get("current_version"),
|
||||
)
|
||||
|
||||
|
||||
def fetch_taint_status(entry: RouteEntry, *, bao_bin: str = "bao") -> TaintStatus:
|
||||
"""Query OpenBao metadata for a catalog entry (never reads secret data).
|
||||
|
||||
Uses the caller's ``BAO_TOKEN`` / ``VAULT_TOKEN`` / ``~/.vault-token`` — same
|
||||
G1 rule as the access proxy. Requires ``path_template`` on the entry.
|
||||
"""
|
||||
if not entry.path_template or "<" in entry.path_template:
|
||||
raise TaintError(
|
||||
f"{entry.id!r} has no concrete path_template — cannot query taint metadata."
|
||||
)
|
||||
path = kv_metadata_path(entry.path_template)
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[bao_bin, "kv", "metadata", "get", "-format=json", path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=os.environ.copy(),
|
||||
check=False,
|
||||
)
|
||||
except FileNotFoundError as e:
|
||||
raise TaintError(f"{bao_bin!r} not found on PATH") from e
|
||||
|
||||
if proc.returncode != 0:
|
||||
err = (proc.stderr or proc.stdout or "metadata get failed").strip().splitlines()
|
||||
# Never echo tokens if somehow present.
|
||||
safe = " ".join(err[:3])[:300]
|
||||
return TaintStatus(
|
||||
lane_id=entry.id,
|
||||
path=path,
|
||||
tainted=False,
|
||||
error=safe or f"bao exit {proc.returncode}",
|
||||
)
|
||||
|
||||
try:
|
||||
payload = json.loads(proc.stdout)
|
||||
except json.JSONDecodeError as e:
|
||||
raise TaintError(f"invalid JSON from bao metadata get: {e}") from e
|
||||
|
||||
data = payload.get("data") if isinstance(payload, dict) else None
|
||||
if not isinstance(data, dict):
|
||||
raise TaintError("bao metadata response missing data object")
|
||||
|
||||
status = parse_custom_metadata(data)
|
||||
return TaintStatus(
|
||||
lane_id=entry.id,
|
||||
path=path,
|
||||
tainted=status.tainted,
|
||||
exposed_at=status.exposed_at,
|
||||
exposed_version=status.exposed_version,
|
||||
exposed_reason=status.exposed_reason,
|
||||
exposed_ref=status.exposed_ref,
|
||||
current_version=status.current_version,
|
||||
)
|
||||
|
|
@ -588,7 +588,13 @@ def draft_route_answer(query: str) -> str:
|
|||
elif e.has_native_exec:
|
||||
parts.append(f"Primary: {e.exec_command}.")
|
||||
elif e.exec_capable:
|
||||
parts.append(f"Proxy: warden access {e.id} --fetch (as the caller).")
|
||||
if e.lane == "login":
|
||||
parts.append(
|
||||
f"Contained login: warden access {e.id} --exec -- "
|
||||
"<reviewed-command>."
|
||||
)
|
||||
else:
|
||||
parts.append(f"Proxy: warden access {e.id} --fetch (as the caller).")
|
||||
parts.append(f"See {e.wiki_ref}.")
|
||||
return " ".join(parts)
|
||||
|
||||
|
|
|
|||
172
tenancy.yaml
Normal file
172
tenancy.yaml
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
# ops-warden tenancy posture declaration
|
||||
# Framework: net-kingdom/canon/standards/tenancy-posture_v0.1.md draft-9
|
||||
# Conformance rule (§6): accuracy, not altitude. Nothing here is aspirational.
|
||||
# Validate: python3 ~/net-kingdom/tools/tenancy-posture/validate.py tenancy.yaml
|
||||
|
||||
schema_version: "0.1"
|
||||
framework: netkingdom-tenancy-posture
|
||||
service: ops-warden
|
||||
role: ssh-certificate-authority
|
||||
|
||||
workload_identity:
|
||||
name: ops-warden
|
||||
kind: operational-control-plane
|
||||
responsible_repo: ops-warden
|
||||
identity_bindings:
|
||||
- scheme: kubernetes-service-account
|
||||
authority: railiance01
|
||||
subject: system:serviceaccount:ops-warden:ops-warden
|
||||
principal_type: service
|
||||
environment: prod
|
||||
|
||||
tenancy:
|
||||
current:
|
||||
I: 1
|
||||
A: 1
|
||||
E: 0
|
||||
P: "n/a"
|
||||
R: "n/a"
|
||||
V: 0
|
||||
implemented:
|
||||
A: 3
|
||||
target:
|
||||
I: 1
|
||||
A: 3
|
||||
E: 0
|
||||
P: "n/a"
|
||||
R: "n/a"
|
||||
V: 1
|
||||
reviewed: "2026-08-19"
|
||||
review_due: "2027-02-18"
|
||||
service_class: interactive
|
||||
permanent: [P, R]
|
||||
|
||||
gap:
|
||||
I: >-
|
||||
ops-warden has a tenant notion — `policy.tenant` in warden.yaml, and the
|
||||
tenant/platform path split introduced by WP-0028 — but it is a static
|
||||
configuration constant, not a claim verified on an inbound call. `warden`
|
||||
is a CLI invoked by an operator or agent; its caller is an OS user and no
|
||||
token is presented to it. Subject id comes from `WARDEN_POLICY_SUBJECT`
|
||||
or falls back to the actor name, which is §4.1's "taken from the request"
|
||||
case exactly. I1.
|
||||
|
||||
Not declared permanent. `warden desk` (WP-0029) already performs an OIDC
|
||||
login against key-cape, so a verified inbound identity is reachable
|
||||
rather than structurally excluded. It is simply not built, and I2 would
|
||||
require it on the signing path, not just the desk.
|
||||
A: >-
|
||||
There is a single choke point on the signing path — inventory membership,
|
||||
actor type, and the TTL ceiling — but it binds an *actor* to principals,
|
||||
not a request to the tenants it may act for. Tenant context is a
|
||||
constant, so "bound once, centrally" (§4.2 A2) would be true only in a
|
||||
trivial sense that overclaims. A1.
|
||||
|
||||
`implemented: A3` is the honest separate fact: delegation to flex-auth as
|
||||
PDP is built (`src/warden/policy.py`, `check_sign_policy`) and was
|
||||
verified live on 2026-08-19 against the enforcing `flex-auth-ops-warden`
|
||||
pin — `decision:f3f7c88f9585582a`, with an anonymous `/v1/check`
|
||||
returning 401. It is not `current` because `policy.enabled` is false, and
|
||||
it is false by decision rather than by blocker: `ADR-0006` scopes
|
||||
enforcement to security zones, which `zone-engine` is defining
|
||||
(`ZONE-WP-0001`). Evidence:
|
||||
`history/2026-08-19-flex-auth-caller-identity-evidence.md`.
|
||||
|
||||
A4 is not a target here. The AuthZEN interface question belongs to
|
||||
flex-auth as the decision point; ops-warden would follow it, not lead it.
|
||||
E: >-
|
||||
E0 is accurate and is not a defect to remediate. ops-warden holds no
|
||||
tenant-partitioned data: `registry/routing/catalog.yaml` is a pointer
|
||||
layer carrying no secret values (`ADR-0001`, CI-enforced) and no `tenant`
|
||||
field on any entry; local state (`signatures.log`, `audit.jsonl`,
|
||||
`access-audit.log`) is operator-scoped and keyed by actor, not tenant.
|
||||
|
||||
The tenant boundaries ops-warden *routes* to — `tenants/binky/...` versus
|
||||
`platform/workloads/...` — are enforced by OpenBao policy, which is the
|
||||
owner's control, not ops-warden's. Claiming E1 on the strength of someone
|
||||
else's enforcement is the overclaim §6 prohibits. Under `ADR-0002`
|
||||
ops-warden is a transparent conduit and takes no custody, so it has no
|
||||
tenant data to key. Raising E would mean acquiring data it is out of
|
||||
scope to hold.
|
||||
P: >-
|
||||
n/a and permanent. No primary datastore: state is files under
|
||||
`~/.local/state/warden` on the invoking operator's machine, and
|
||||
§3.3 scopes P to the primary datastore. Custody of secrets is explicitly
|
||||
out of scope (`SCOPE.md`, `ADR-0002`), so ops-warden will not acquire a
|
||||
tenant-bearing substrate.
|
||||
R: >-
|
||||
n/a and permanent, on the same ground as P: no tenant data at rest.
|
||||
|
||||
Named honestly rather than hidden behind the n/a: ops-warden *does* keep
|
||||
local operator records indefinitely — `audit.jsonl` is append-only by
|
||||
design (WP-0022) and `signatures.log` has no retention position. They are
|
||||
metadata-only and guarded against secret material, and they are not
|
||||
tenant data, so they do not move this axis. But "no declared retention
|
||||
for local audit" is a real gap on a different axis than this file grades,
|
||||
and is recorded here so it is not lost.
|
||||
V: >-
|
||||
V0. `warden sign` depends synchronously on OpenBao at
|
||||
`https://bao.coulomb.social` (railiance01) and, once enabled, on the
|
||||
flex-auth pin reached through the ops-bridge tunnel
|
||||
`flex-auth-ops-warden-railiance01`. §4.6.1 takes the minimum across that
|
||||
path, and none of it has an exercised recovery objective.
|
||||
|
||||
A `backend: local` CA exists in the code and is covered by tests, but it
|
||||
has never been exercised as a production degraded mode, and §13 does not
|
||||
accept "the code path exists" as V evidence. Target V1 means documenting
|
||||
and actually rehearsing recovery of the signing path — not adding
|
||||
redundancy, which the substrate cannot currently support: `reef-railiance`
|
||||
is single-node with a shared control plane, and under Decision 4.6.1 that
|
||||
caps V for everything bound to it (see NK-WP-0027).
|
||||
|
||||
zones:
|
||||
standard: security-zones_v0.1
|
||||
membership: z1-operational
|
||||
responsible_party: team:platform-security
|
||||
justification: >-
|
||||
The attended platform signing service has a bounded operational scope and
|
||||
internal metadata exposure. It has M1 evidence, but no SLO history, on-call
|
||||
rotation, or incident exercise that would support z2-protected.
|
||||
context:
|
||||
maturity: M1
|
||||
criticality: medium
|
||||
data_classification: internal
|
||||
evidence:
|
||||
- ref: docs/evidence/security-zone-admission-2026-08-22.md
|
||||
supports: [M1, platform-only-scope, basic-slo, data-handling-note]
|
||||
- ref: history/2026-08-19-flex-auth-caller-identity-evidence.md
|
||||
supports: [production-policy-path, authenticated-caller]
|
||||
reviewed: "2026-08-22"
|
||||
review_due: "2026-11-22"
|
||||
|
||||
evidence:
|
||||
A: >-
|
||||
Built and verified live 2026-08-19 against the enforcing
|
||||
`flex-auth-ops-warden` pin: `decision:f3f7c88f9585582a`, anonymous
|
||||
`/v1/check` 401. Record:
|
||||
`history/2026-08-19-flex-auth-caller-identity-evidence.md`. Re-establish on
|
||||
demand with `scripts/check_policy_caller_identity.py`. Why it is implemented
|
||||
and not current: `docs/adr/ADR-0006-enforcement-is-zone-scoped.md`.
|
||||
E: >-
|
||||
`docs/adr/ADR-0002-conduit-not-broker.md` and `ADR-0001` (catalog is a
|
||||
pointer layer, CI-enforced) — why no tenant-keyed data exists to enforce on.
|
||||
P: >-
|
||||
No datastore. State is operator-local files under `~/.local/state/warden`.
|
||||
V: >-
|
||||
Production signing path verified 2026-06-17
|
||||
(`history/2026-06-17-openbao-production-verify.md`). No recovery exercise
|
||||
exists for that path, which is why this is V0 and not V1.
|
||||
|
||||
notes:
|
||||
- >-
|
||||
ops-warden issues short-lived SSH certificates and routes every other
|
||||
credential need to its owner.
|
||||
- >-
|
||||
Five of six axes are low because it deliberately holds nothing. The low
|
||||
grades on E, P and R are the intended consequence of ADR-0002 and ADR-0005,
|
||||
not deferred work, and raising them would mean acquiring data ops-warden is
|
||||
out of scope to hold.
|
||||
- >-
|
||||
The two axes with real movement are A — built, deferred by ADR-0006 pending
|
||||
the zone model — and V, which needs a rehearsed recovery and is capped by
|
||||
the substrate until NK-WP-0027 lands.
|
||||
131
tests/test_agent_read_boundary_check.py
Normal file
131
tests/test_agent_read_boundary_check.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
"""Tests for scripts/check_agent_read_boundary.py (WARDEN-WP-0032-T06).
|
||||
|
||||
This script is a control, not a report: it is the invariant RISK-F-0009 asked for
|
||||
("a check that fails when a high-risk lane has no corresponding deny"). So the
|
||||
parsing has to be right about the two things that would make it lie -- treating a
|
||||
non-deny grant as a deny, and treating a path pattern as a concrete address.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"check_agent_read_boundary", REPO / "scripts" / "check_agent_read_boundary.py"
|
||||
)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
|
||||
class TestDeniedDataPaths:
|
||||
def test_extracts_denied_paths(self):
|
||||
policy = """
|
||||
path "platform/data/workloads/forgejo/forgejo-admin" {
|
||||
capabilities = ["deny"]
|
||||
}
|
||||
"""
|
||||
assert mod.denied_data_paths(policy) == {"platform/data/workloads/forgejo/forgejo-admin"}
|
||||
|
||||
def test_metadata_read_is_not_a_deny(self):
|
||||
"""The policy permits metadata read alongside every data deny.
|
||||
|
||||
Counting those as denies would double the apparent coverage.
|
||||
"""
|
||||
policy = """
|
||||
path "platform/metadata/workloads/forgejo/forgejo-admin" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
"""
|
||||
assert mod.denied_data_paths(policy) == set()
|
||||
|
||||
def test_deny_is_matched_exactly_not_by_substring(self):
|
||||
"""A capability merely containing 'deny' must not register as a deny."""
|
||||
policy = """
|
||||
path "platform/data/workloads/x/y" {
|
||||
capabilities = ["denylist-read"]
|
||||
}
|
||||
"""
|
||||
assert mod.denied_data_paths(policy) == set()
|
||||
|
||||
def test_multiple_blocks(self):
|
||||
policy = """
|
||||
path "a/data/one" { capabilities = ["deny"] }
|
||||
path "a/metadata/one" { capabilities = ["read"] }
|
||||
path "b/data/two" { capabilities = ["deny"] }
|
||||
"""
|
||||
assert mod.denied_data_paths(policy) == {"a/data/one", "b/data/two"}
|
||||
|
||||
|
||||
class TestToDataPath:
|
||||
def test_inserts_kv_v2_data_segment(self):
|
||||
assert (
|
||||
mod.to_data_path("platform/workloads/forgejo/forgejo-admin")
|
||||
== "platform/data/workloads/forgejo/forgejo-admin"
|
||||
)
|
||||
|
||||
def test_tenant_mount(self):
|
||||
assert (
|
||||
mod.to_data_path("tenants/binky/company-email/imap")
|
||||
== "tenants/data/binky/company-email/imap"
|
||||
)
|
||||
|
||||
def test_placeholder_pattern_has_no_address(self):
|
||||
"""`openbao-api-key` is a routing pattern, not one secret.
|
||||
|
||||
RISK-F-0009 counted it among the uncovered lanes; there is nothing for a
|
||||
policy to deny, and reporting it as a gap overstates the exposure.
|
||||
"""
|
||||
assert mod.to_data_path("platform/workloads/<domain>/<workload>/<bundle>") is None
|
||||
|
||||
def test_non_kv_lane_has_no_address(self):
|
||||
"""`ops-warden-warden-sign-token` is a broker grant, not a KV path."""
|
||||
assert mod.to_data_path("credential-grants/catalog.yaml grant ops-warden/warden-sign") is None
|
||||
|
||||
|
||||
class TestAgainstTheRealCatalog:
|
||||
def test_every_high_risk_lane_resolves_or_is_explicitly_pattern(self):
|
||||
"""No high-risk lane may fall through the classifier silently.
|
||||
|
||||
Each is either a concrete data path the policy can deny, or a pattern --
|
||||
never an unhandled third case, which is the ADR-0007 failure mode.
|
||||
"""
|
||||
import yaml
|
||||
|
||||
entries = yaml.safe_load((REPO / "registry" / "routing" / "catalog.yaml").read_text())["entries"]
|
||||
for entry in (e for e in entries if e.get("risk") == "high"):
|
||||
template = entry.get("path_template")
|
||||
if not template:
|
||||
continue
|
||||
resolved = mod.to_data_path(template)
|
||||
assert resolved is None or resolved.count("/data/") == 1, entry["id"]
|
||||
|
||||
|
||||
class TestGeneratedArtifact:
|
||||
"""The artifact railiance-platform consumes (WARDEN-WP-0033-T03).
|
||||
|
||||
A consumer applies this to a live deny set, so staleness is the failure that
|
||||
matters -- a path graded high after the last emit would silently not reach them.
|
||||
"""
|
||||
|
||||
def test_artifact_is_current(self):
|
||||
import subprocess
|
||||
|
||||
result = subprocess.run(
|
||||
["python3", str(REPO / "scripts" / "emit_high_risk_paths.py"), "--check"],
|
||||
capture_output=True, text=True, timeout=60,
|
||||
)
|
||||
assert result.returncode == 0, (
|
||||
f"{result.stdout}{result.stderr}\n"
|
||||
"Re-run scripts/emit_high_risk_paths.py and commit the result."
|
||||
)
|
||||
|
||||
def test_every_concrete_high_risk_lane_is_in_the_artifact(self):
|
||||
import yaml
|
||||
|
||||
catalog = yaml.safe_load((REPO / "registry" / "routing" / "catalog.yaml").read_text())
|
||||
artifact = yaml.safe_load(
|
||||
(REPO / "registry" / "generated" / "high-risk-data-paths.yaml").read_text()
|
||||
)
|
||||
emitted = {row["id"] for row in artifact["paths"]} | set(artifact["no_concrete_path"] or [])
|
||||
graded_high = {e["id"] for e in catalog["entries"] if e.get("risk") == "high"}
|
||||
assert graded_high == emitted, "a high-risk lane is missing from the generated artifact"
|
||||
|
|
@ -84,13 +84,13 @@ def test_default_vault_token_env(tmp_path):
|
|||
assert cfg.vault.token_env == "VAULT_TOKEN"
|
||||
|
||||
|
||||
def test_policy_defaults_disabled(tmp_path):
|
||||
def test_policy_defaults_to_unknown_zone_profile(tmp_path):
|
||||
cfg_path = tmp_path / "warden.yaml"
|
||||
write_yaml(cfg_path, {"backend": "local", "ca_key": str(tmp_path / "ca")})
|
||||
cfg = load_config(cfg_path)
|
||||
assert cfg.policy.enabled is False
|
||||
assert cfg.policy.flex_auth_url == "http://127.0.0.1:8080"
|
||||
assert cfg.policy.fail_closed is True
|
||||
assert cfg.policy.flex_auth_url is None
|
||||
assert cfg.policy.failure_modes["unknown"] == "fail_open"
|
||||
assert cfg.policy.failure_modes["z3-critical"] == "fail_closed"
|
||||
|
||||
|
||||
def test_policy_block_parsed(tmp_path):
|
||||
|
|
@ -99,18 +99,30 @@ def test_policy_block_parsed(tmp_path):
|
|||
"backend": "local",
|
||||
"ca_key": str(tmp_path / "ca"),
|
||||
"policy": {
|
||||
"enabled": True,
|
||||
"flex_auth_url": "http://flex-auth:8080",
|
||||
"fail_closed": False,
|
||||
"zone_registry_path": str(tmp_path / "zones.json"),
|
||||
"failure_modes": {"z2-protected": "fail_closed"},
|
||||
"tenant": "tenant:coulomb",
|
||||
"subject_env": "MY_SUBJECT",
|
||||
"system": "warden-test",
|
||||
},
|
||||
})
|
||||
cfg = load_config(cfg_path)
|
||||
assert cfg.policy.enabled is True
|
||||
assert cfg.policy.flex_auth_url == "http://flex-auth:8080"
|
||||
assert cfg.policy.fail_closed is False
|
||||
assert cfg.policy.zone_registry_path == tmp_path / "zones.json"
|
||||
assert cfg.policy.failure_modes["z2-protected"] == "fail_closed"
|
||||
assert cfg.policy.tenant == "tenant:coulomb"
|
||||
assert cfg.policy.subject_env == "MY_SUBJECT"
|
||||
assert cfg.policy.system == "warden-test"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("retired", ["enabled", "fail_closed"])
|
||||
def test_retired_global_policy_switches_are_rejected(tmp_path, retired):
|
||||
cfg_path = tmp_path / "warden.yaml"
|
||||
write_yaml(cfg_path, {
|
||||
"backend": "local",
|
||||
"ca_key": str(tmp_path / "ca"),
|
||||
"policy": {retired: True},
|
||||
})
|
||||
with pytest.raises(ConfigError, match=f"policy.{retired}"):
|
||||
load_config(cfg_path)
|
||||
|
|
|
|||
96
tests/test_desk.py
Normal file
96
tests/test_desk.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
"""Tests for warden desk (WARDEN-WP-0029 T03)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from http.server import ThreadingHTTPServer
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from warden.cli import app
|
||||
from warden.desk import (
|
||||
DeskError,
|
||||
make_handler,
|
||||
new_session,
|
||||
session_from_plan_dict,
|
||||
)
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def test_new_session_rejects_unknown_act():
|
||||
with pytest.raises(DeskError, match="unknown desk act"):
|
||||
new_session(act="teleport", summary="nope")
|
||||
|
||||
|
||||
def test_paste_once_requires_path():
|
||||
with pytest.raises(DeskError, match="requires --path"):
|
||||
new_session(act="paste_once_provision", summary="mint")
|
||||
|
||||
|
||||
def test_session_from_plan_dict():
|
||||
plan = {
|
||||
"verdict": "founder_required",
|
||||
"need": "provision token",
|
||||
"lane_id": "openbao-api-key",
|
||||
"organization_posture": "build",
|
||||
"founder_act": {
|
||||
"kind": "approve",
|
||||
"summary": "Approve red-lane change",
|
||||
"details": {"lane_id": "openbao-api-key"},
|
||||
},
|
||||
}
|
||||
s = session_from_plan_dict(plan)
|
||||
assert s.act == "approve"
|
||||
assert s.lane_id == "openbao-api-key"
|
||||
|
||||
|
||||
def test_session_from_plan_rejects_autonomous():
|
||||
with pytest.raises(DeskError, match="founder_required"):
|
||||
session_from_plan_dict({"verdict": "autonomous", "founder_act": None})
|
||||
|
||||
|
||||
def test_approve_flow_http_dry():
|
||||
session = new_session(act="approve", summary="Enable something", lane_id="demo")
|
||||
done = threading.Event()
|
||||
|
||||
def on_done(s):
|
||||
done.set()
|
||||
|
||||
handler = make_handler(session, on_done=on_done, dry_run=True)
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
||||
port = server.server_address[1]
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
url = f"http://127.0.0.1:{port}/?t={session.token}"
|
||||
with urllib.request.urlopen(url, timeout=5) as resp:
|
||||
body = resp.read().decode()
|
||||
assert "Founder approval" in body
|
||||
assert session.token not in body or True # token is in form; ok
|
||||
|
||||
data = urllib.parse.urlencode(
|
||||
{"token": session.token, "decision": "approve"}
|
||||
).encode()
|
||||
req = urllib.request.Request(
|
||||
f"http://127.0.0.1:{port}/act", data=data, method="POST"
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
result_body = resp.read().decode()
|
||||
assert "approved" in result_body.lower() or session.result == "approved"
|
||||
assert session.result == "approved"
|
||||
assert done.wait(timeout=2)
|
||||
finally:
|
||||
server.shutdown()
|
||||
thread.join(timeout=2)
|
||||
|
||||
|
||||
def test_cli_desk_approve_dry_run():
|
||||
# Exercise CLI wiring without waiting forever: dry-run still serves until act.
|
||||
# Use a short-circuit by importing run path via invoke would hang — skip full CLI
|
||||
# server test; unit coverage above is enough. Smoke that --help works.
|
||||
r = runner.invoke(app, ["desk", "--help"])
|
||||
assert r.exit_code == 0
|
||||
assert "paste_once" in r.stdout or "founder" in r.stdout.lower() or "--act" in r.stdout
|
||||
|
|
@ -31,4 +31,53 @@ def test_build_registry_from_inventory_seed(tmp_path):
|
|||
)
|
||||
assert bridge["attributes"]["actor_type"] == "agt"
|
||||
assert bridge["attributes"]["max_ttl_hours"] == 24
|
||||
assert "agt-task-bridge" in bridge["attributes"]["allowed_principals"]
|
||||
assert "agt-task-bridge" in bridge["attributes"]["allowed_principals"]
|
||||
assert "trust_zone" not in bridge
|
||||
assert bridge["attributes"]["security_zone"] == "unknown"
|
||||
assert bridge["attributes"]["security_zone_admission"] == "unknown"
|
||||
assert bridge["attributes"]["workload_id"] == "ops-bridge-tunnel"
|
||||
|
||||
human = next(
|
||||
r
|
||||
for r in registry["resource_manifests"][0]["resources"]
|
||||
if r["id"] == "ssh-cert:actor/adm-example"
|
||||
)
|
||||
assert human["attributes"]["security_zone_admission"] == "not-applicable"
|
||||
|
||||
|
||||
def test_compiler_joins_explicit_workload_reference_to_resolved_zone(tmp_path):
|
||||
zones = tmp_path / "zones.json"
|
||||
zones.write_text(json.dumps({
|
||||
"records": [{
|
||||
"workload_id": "ops-bridge-tunnel",
|
||||
"declared_zone": "z2-continuity",
|
||||
"admission": "satisfied",
|
||||
"admission_reason": "admission_floor_met",
|
||||
"effective_zone": "z2-continuity",
|
||||
"membership_revision": "sha256:zone-revision",
|
||||
}]
|
||||
}))
|
||||
out = tmp_path / "registry.json"
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(SCRIPT),
|
||||
str(INVENTORY),
|
||||
"--zone-resolutions",
|
||||
str(zones),
|
||||
"-o",
|
||||
str(out),
|
||||
],
|
||||
check=True,
|
||||
cwd=ROOT,
|
||||
)
|
||||
registry = json.loads(out.read_text())
|
||||
bridge = next(
|
||||
r
|
||||
for r in registry["resource_manifests"][0]["resources"]
|
||||
if r["id"] == "ssh-cert:actor/agt-state-hub-bridge"
|
||||
)
|
||||
attrs = bridge["attributes"]
|
||||
assert attrs["security_zone"] == "z2-continuity"
|
||||
assert attrs["security_zone_admission"] == "satisfied"
|
||||
assert attrs["security_zone_revision"] == "sha256:zone-revision"
|
||||
|
|
|
|||
133
tests/test_layer_conformance.py
Normal file
133
tests/test_layer_conformance.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
"""Layer-model conformance (security-layer-model_v0.4 §5, §11).
|
||||
|
||||
Two things are checked here. §11 makes one of them mechanical: every direct
|
||||
Tooling client maps to a declared shape. §5.2 asks for the other: the conduit's
|
||||
supplied-authority property covered by a test.
|
||||
|
||||
Deliberately absent: any assertion on a §5.3 review date. A date-triggered
|
||||
failure breaks the build on a calendar day with no code change, punishing
|
||||
whoever commits next rather than whoever owns the gap — the same reasoning
|
||||
recorded in WARDEN-WP-0033-T05 for blocker staleness.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _decl() -> dict:
|
||||
return yaml.safe_load((ROOT / "layer.yaml").read_text())
|
||||
|
||||
|
||||
class TestDeclaration:
|
||||
def test_declares_staff_layer_in_its_own_voice(self):
|
||||
d = _decl()
|
||||
assert d["repository"] == "ops-warden"
|
||||
assert d["layer"] == "staff"
|
||||
# §11: "only the repository's own file, in its own voice, conforms."
|
||||
assert d["declared_by"] == "docs/adr/ADR-0010"
|
||||
|
||||
def test_every_tooling_contact_maps_to_a_declared_shape(self):
|
||||
"""§11 mechanical check — the guard against a new undeclared client."""
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(ROOT / "scripts" / "check_layer_conformance.py")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 0, (
|
||||
f"undeclared Tooling contact — a finding under §11, not a tracked gap:\n"
|
||||
f"{result.stdout}{result.stderr}"
|
||||
)
|
||||
|
||||
def test_declared_gaps_carry_all_four_fields(self):
|
||||
"""§5.3 is machine-readable or it is prose wearing a schema."""
|
||||
for c in _decl()["tooling_contacts"]:
|
||||
if c["shape"] == "5.3":
|
||||
for field in ("capability", "intended_owner", "blocked_on", "review"):
|
||||
assert c.get(field), f"{c['id']} missing {field}"
|
||||
|
||||
def test_gaps_are_not_counted_as_conformance(self):
|
||||
"""§11: a declared gap is tracked non-conformance. Keep that visible."""
|
||||
text = (ROOT / "layer.yaml").read_text()
|
||||
assert "TRACKED NON-CONFORMANCE" in text.upper()
|
||||
|
||||
|
||||
class TestConduitSuppliesNoAuthority:
|
||||
"""§5.2: 'MUST NOT present its own credential, MUST NOT widen what the
|
||||
caller could already do.' The standard says this SHOULD be covered by a
|
||||
test; this is that test."""
|
||||
|
||||
def test_conduit_supplies_no_authority_of_its_own(self, monkeypatch):
|
||||
from warden import proxy
|
||||
|
||||
monkeypatch.setenv("VAULT_TOKEN", "caller-own-token")
|
||||
monkeypatch.setenv("HOME", "/home/nobody")
|
||||
before = dict(os.environ)
|
||||
|
||||
env = proxy._caller_env()
|
||||
|
||||
# The child environment IS the caller's environment — nothing added,
|
||||
# nothing removed, no ops-warden credential injected.
|
||||
assert env == before, (
|
||||
"conduit altered the caller's environment; §5.2 requires it to "
|
||||
"supply no authority of its own"
|
||||
)
|
||||
assert env["VAULT_TOKEN"] == "caller-own-token"
|
||||
|
||||
def test_conduit_declares_supplied_authority_none(self):
|
||||
conduits = [c for c in _decl()["tooling_contacts"] if c["shape"] == "5.2"]
|
||||
assert conduits, "no §5.2 conduit declared — proxy.py is one"
|
||||
for c in conduits:
|
||||
assert c["supplied_authority"] == "none"
|
||||
|
||||
def test_proxy_holds_no_credential_constant(self):
|
||||
"""A conduit that presents its own token is not a conduit (§5.2)."""
|
||||
src = (ROOT / "src" / "warden" / "proxy.py").read_text()
|
||||
# It may name token ENV VARS to detect caller auth; it must not carry a
|
||||
# token value or mint one.
|
||||
for forbidden in ("X-Vault-Token", "auth/approle/login", "token create"):
|
||||
assert forbidden not in src, (
|
||||
f"proxy.py references {forbidden!r} — that is presenting or "
|
||||
f"minting authority, not conducting the caller's"
|
||||
)
|
||||
|
||||
|
||||
class TestPepStanceMap:
|
||||
"""§6.4: every PEP-shaped consumer MUST publish its unreachable-engine
|
||||
stance map, total and per zone, 'published rather than held in code'.
|
||||
ADR-0009 is named as the reference shape, so it should actually hold."""
|
||||
|
||||
def _stance(self) -> dict:
|
||||
return yaml.safe_load((ROOT / "pep-stance.yaml").read_text())
|
||||
|
||||
def test_published_map_equals_shipped_behaviour(self):
|
||||
"""The whole point. A published map that may drift from the code is
|
||||
worse than none, because it invites reliance it cannot support."""
|
||||
from warden.config import PolicyConfig
|
||||
|
||||
assert self._stance()["stance"] == PolicyConfig().failure_modes
|
||||
|
||||
def test_stance_is_total_over_the_zone_model(self):
|
||||
"""§6.4 obligation 3: total, no implicit default."""
|
||||
stance = self._stance()["stance"]
|
||||
required = {
|
||||
"z0-experimental", "z1-operational", "z2-protected",
|
||||
"z2-continuity", "z3-critical", "unknown", "not-applicable",
|
||||
}
|
||||
assert required <= set(stance), f"stance not total; missing {required - set(stance)}"
|
||||
assert set(stance.values()) <= {"fail_open", "fail_closed"}
|
||||
|
||||
def test_critical_zone_fails_closed(self):
|
||||
"""ADR-0009's one non-negotiable row."""
|
||||
assert self._stance()["stance"]["z3-critical"] == "fail_closed"
|
||||
|
||||
def test_verdict_is_never_cached(self):
|
||||
"""§6.4 obligation 2: caching an input claim is permitted; caching the
|
||||
answer is a second decision point deciding early (§6.1)."""
|
||||
assert self._stance()["verdict_caching"] == "none"
|
||||
33
tests/test_mask.py
Normal file
33
tests/test_mask.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
"""Tests for the masking display filter (WARDEN-WP-0026 T03)."""
|
||||
from warden.mask import fingerprint, mask_value
|
||||
from warden.proxy import ResolvedFetch, proxy_fetch_fingerprint
|
||||
|
||||
|
||||
def test_mask_never_contains_the_value():
|
||||
secret = "ghp_realtokenvalue1234567890abcdef"
|
||||
masked = mask_value(secret)
|
||||
assert secret not in masked
|
||||
assert "hidden" in masked and "len=" in masked and "sha256:" in masked
|
||||
|
||||
|
||||
def test_fingerprint_reports_presence_and_length():
|
||||
fp = fingerprint("abcd")
|
||||
assert fp.present is True and fp.length == 4
|
||||
assert len(fp.sha256_prefix) == 8
|
||||
|
||||
|
||||
def test_absent_value_renders_absent():
|
||||
assert mask_value("") == "‹absent›"
|
||||
assert mask_value(None) == "‹absent›"
|
||||
assert fingerprint(None).present is False
|
||||
|
||||
|
||||
def test_fingerprint_is_stable_and_discriminating():
|
||||
assert fingerprint("token-A").sha256_prefix == fingerprint("token-A").sha256_prefix
|
||||
assert fingerprint("token-A").sha256_prefix != fingerprint("token-B").sha256_prefix
|
||||
|
||||
|
||||
def test_proxy_fingerprint_returns_mask_not_value():
|
||||
fp = proxy_fetch_fingerprint(ResolvedFetch(shell_cmd="printf 'the-secret-value'"))
|
||||
assert fp.present and fp.length == len("the-secret-value")
|
||||
assert "the-secret-value" not in fp.render()
|
||||
|
|
@ -7,7 +7,7 @@ import json
|
|||
from typer.testing import CliRunner
|
||||
|
||||
from warden.cli import app
|
||||
from warden.memory import activate, enabled, record_command_episode, status, store_path
|
||||
from warden.memory import activate, record_command_episode, status, store_path
|
||||
from warden.worker import RuleBrain, _plan_with_memory, build_plans
|
||||
|
||||
runner = CliRunner()
|
||||
|
|
@ -140,4 +140,4 @@ def test_route_find_implicitly_activates_memory_without_explicit_command(tmp_pat
|
|||
activation = ensure_memory_context(need="ssh tunnel", implicit=True)
|
||||
assert activation is not None
|
||||
assert activation.get("implicit") is True
|
||||
assert status()["episode_count"] >= 1
|
||||
assert status()["episode_count"] >= 1
|
||||
|
|
|
|||
121
tests/test_plan.py
Normal file
121
tests/test_plan.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
"""Tests for warden plan (WARDEN-WP-0029 T01)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from warden.cli import app
|
||||
from warden.plan import build_plan
|
||||
from warden.posture import load_posture
|
||||
from warden.routing.catalog import load_catalog
|
||||
|
||||
runner = CliRunner()
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _catalog_env(monkeypatch):
|
||||
monkeypatch.setenv("WARDEN_ROUTING_CATALOG", str(REPO / "registry/routing/catalog.yaml"))
|
||||
monkeypatch.setenv("WARDEN_POSTURE_CATALOG", str(REPO / "registry/policy/security-posture.yaml"))
|
||||
|
||||
|
||||
def test_plan_forgejo_deploy_key_autonomous():
|
||||
plan = build_plan("forgejo deploy key for binky-control")
|
||||
assert plan.verdict == "autonomous"
|
||||
assert plan.organization_posture == "build"
|
||||
assert plan.lane_id == "agent-harness-forgejo-deploy"
|
||||
assert plan.commands
|
||||
assert plan.founder_act is None
|
||||
assert plan.catalog.get("content_hash")
|
||||
|
||||
|
||||
def test_plan_forgejo_admin_autonomous():
|
||||
plan = build_plan("forgejo admin api token")
|
||||
assert plan.verdict == "autonomous"
|
||||
assert plan.lane_id == "forgejo-admin-api-token"
|
||||
assert any("warden access forgejo-admin-api-token" in c for c in plan.commands)
|
||||
|
||||
|
||||
def test_plan_new_secret_founder_required():
|
||||
plan = build_plan("provision a new secret token for a tenant workload")
|
||||
assert plan.verdict == "founder_required"
|
||||
assert plan.founder_act is not None
|
||||
assert plan.founder_act.kind in ("paste_once_provision", "approve", "oidc_login")
|
||||
|
||||
|
||||
def test_plan_login_founder_required():
|
||||
plan = build_plan("oidc login mfa key-cape")
|
||||
assert plan.verdict == "founder_required"
|
||||
assert plan.founder_act is not None
|
||||
assert plan.founder_act.kind == "oidc_login"
|
||||
|
||||
|
||||
def test_plan_first_time_openbao_database_admin_uses_platform_admin_login():
|
||||
plan = build_plan(
|
||||
"first-time OpenBao database engine administration for "
|
||||
"database/config/platform-pg-2 dynamic roles policies and token roles; "
|
||||
"requires attended platform-admin handoff"
|
||||
)
|
||||
assert plan.verdict == "founder_required"
|
||||
assert plan.lane_id == "openbao-platform-admin-login"
|
||||
assert plan.founder_act is not None
|
||||
assert plan.founder_act.kind == "oidc_login"
|
||||
command = plan.founder_act.details["fetch_command"]
|
||||
assert command == (
|
||||
"warden access openbao-platform-admin-login --exec -- <reviewed-command>"
|
||||
)
|
||||
assert "financials" not in command
|
||||
assert "paste_once" not in plan.founder_act.details["desk_hint"]
|
||||
assert any(
|
||||
item
|
||||
== "warden access openbao-platform-admin-login --exec -- <reviewed-command>"
|
||||
for item in plan.commands
|
||||
)
|
||||
assert not any("--fetch" in item for item in plan.commands)
|
||||
assert not any("--out" in item or "--wrap" in item for item in plan.commands)
|
||||
|
||||
|
||||
def test_plan_openbao_shamir_recovery_uses_approval_ceremony_not_secret_provision():
|
||||
plan = build_plan(
|
||||
"coordinate one attended production OpenBao emergency seal/unseal drill "
|
||||
"with a fresh encrypted off-host Raft snapshot receipt, verified "
|
||||
"provider-console access, two-of-three Shamir custodian quorum, named "
|
||||
"driver and abort operator, without exposing credential values"
|
||||
)
|
||||
assert plan.verdict == "founder_required"
|
||||
assert plan.lane_id == "openbao-shamir-recovery-ceremony"
|
||||
assert plan.founder_act is not None
|
||||
assert plan.founder_act.kind == "approve"
|
||||
assert "openbao-shamir-recovery-ceremony" in plan.founder_act.details["desk_hint"]
|
||||
assert "paste_once" not in plan.founder_act.details["desk_hint"]
|
||||
assert all("warden access" not in item for item in plan.commands)
|
||||
assert any("openbao-shamir-recovery-ceremony" in item for item in plan.commands)
|
||||
|
||||
|
||||
def test_plan_unroutable():
|
||||
# Zero keyword overlap with catalog (avoid tokens like secret/key/token)
|
||||
plan = build_plan("xyzzy-plugh-fnord-qqq-zzzz")
|
||||
assert plan.verdict == "unroutable"
|
||||
assert plan.ccr_stub is not None
|
||||
assert plan.lane_id is None
|
||||
|
||||
|
||||
def test_plan_composes_catalog_find():
|
||||
"""Plan must use Catalog.find — exact id match wins."""
|
||||
cat = load_catalog()
|
||||
plan = build_plan("ssh-cert-host-access", catalog=cat, posture=load_posture())
|
||||
assert plan.verdict == "autonomous"
|
||||
assert plan.lane_id == "ssh-cert-host-access"
|
||||
assert any("warden sign" in c for c in plan.commands)
|
||||
|
||||
|
||||
def test_cli_plan_json():
|
||||
r = runner.invoke(app, ["plan", "forgejo deploy key for binky-control", "--json"])
|
||||
assert r.exit_code == 0, r.stdout + r.stderr
|
||||
payload = json.loads(r.stdout)
|
||||
assert payload["verdict"] == "autonomous"
|
||||
assert payload["organization_posture"] == "build"
|
||||
assert payload["lane_id"] == "agent-harness-forgejo-deploy"
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
"""Tests for warden.policy — flex-auth gate."""
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
|
@ -6,7 +7,12 @@ import httpx
|
|||
import pytest
|
||||
|
||||
from warden.ca import CAError
|
||||
from warden.config import PolicyConfig
|
||||
from warden.caller_identity import (
|
||||
CallerIdentityError,
|
||||
caller_auth_headers,
|
||||
resolve_caller_token,
|
||||
)
|
||||
from warden.config import CallerAuthConfig, PolicyConfig
|
||||
from warden.models import ActorType, CertSpec
|
||||
from warden.policy import check_sign_policy, pubkey_fingerprint
|
||||
|
||||
|
|
@ -21,6 +27,17 @@ def _spec(pubkey_path: Path) -> CertSpec:
|
|||
)
|
||||
|
||||
|
||||
def _zone_registry(tmp_path: Path, zone: str) -> Path:
|
||||
path = tmp_path / "registry.json"
|
||||
path.write_text(
|
||||
'{"resource_manifests":[{"resources":[{"id":'
|
||||
'"ssh-cert:actor/agt-state-hub-bridge","attributes":{'
|
||||
f'"security_zone":"{zone}","security_zone_admission":"satisfied"'
|
||||
'}}]}]}'
|
||||
)
|
||||
return path
|
||||
|
||||
|
||||
def test_pubkey_fingerprint(tmp_path):
|
||||
pubkey = tmp_path / "key.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA test\n")
|
||||
|
|
@ -29,26 +46,32 @@ def test_pubkey_fingerprint(tmp_path):
|
|||
assert len(fp) == 7 + 64
|
||||
|
||||
|
||||
def test_disabled_returns_none(tmp_path):
|
||||
def test_unconfigured_evaluator_uses_unknown_fail_open_profile(tmp_path):
|
||||
pubkey = tmp_path / "key.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA\n")
|
||||
cfg = PolicyConfig(enabled=False)
|
||||
assert check_sign_policy(cfg, _spec(pubkey)) is None
|
||||
cfg = PolicyConfig()
|
||||
spec = _spec(pubkey)
|
||||
assert check_sign_policy(cfg, spec) is None
|
||||
assert spec.policy_zone == "unknown"
|
||||
assert spec.policy_failure_mode == "fail_open"
|
||||
assert spec.policy_outcome == "fail_open"
|
||||
|
||||
|
||||
def test_allow_returns_decision_id(tmp_path):
|
||||
pubkey = tmp_path / "key.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA\n")
|
||||
cfg = PolicyConfig(enabled=True, flex_auth_url="http://flex-auth.test")
|
||||
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {"effect": "allow", "id": "dec-123"}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
|
||||
spec = _spec(pubkey)
|
||||
with patch("warden.policy.httpx.post", return_value=mock_response) as post:
|
||||
result = check_sign_policy(cfg, _spec(pubkey))
|
||||
result = check_sign_policy(cfg, spec)
|
||||
|
||||
assert result == "dec-123"
|
||||
assert spec.policy_outcome == "allow"
|
||||
post.assert_called_once()
|
||||
call_kwargs = post.call_args
|
||||
assert call_kwargs[0][0] == "http://flex-auth.test/v1/check"
|
||||
|
|
@ -61,7 +84,7 @@ def test_allow_returns_decision_id(tmp_path):
|
|||
def test_deny_raises_ca_error(tmp_path):
|
||||
pubkey = tmp_path / "key.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA\n")
|
||||
cfg = PolicyConfig(enabled=True)
|
||||
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
|
|
@ -78,7 +101,10 @@ def test_deny_raises_ca_error(tmp_path):
|
|||
def test_unreachable_fail_closed_raises(tmp_path):
|
||||
pubkey = tmp_path / "key.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA\n")
|
||||
cfg = PolicyConfig(enabled=True, fail_closed=True)
|
||||
cfg = PolicyConfig(
|
||||
flex_auth_url="http://flex-auth.test",
|
||||
zone_registry_path=_zone_registry(tmp_path, "z3-critical"),
|
||||
)
|
||||
|
||||
with patch(
|
||||
"warden.policy.httpx.post",
|
||||
|
|
@ -91,7 +117,7 @@ def test_unreachable_fail_closed_raises(tmp_path):
|
|||
def test_unreachable_fail_open_returns_none(tmp_path):
|
||||
pubkey = tmp_path / "key.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA\n")
|
||||
cfg = PolicyConfig(enabled=True, fail_closed=False)
|
||||
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
|
||||
|
||||
with patch(
|
||||
"warden.policy.httpx.post",
|
||||
|
|
@ -103,7 +129,10 @@ def test_unreachable_fail_open_returns_none(tmp_path):
|
|||
def test_http_error_fail_closed_raises(tmp_path):
|
||||
pubkey = tmp_path / "key.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA\n")
|
||||
cfg = PolicyConfig(enabled=True, fail_closed=True)
|
||||
cfg = PolicyConfig(
|
||||
flex_auth_url="http://flex-auth.test",
|
||||
zone_registry_path=_zone_registry(tmp_path, "z3-critical"),
|
||||
)
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 403
|
||||
|
|
@ -117,7 +146,7 @@ def test_http_error_fail_closed_raises(tmp_path):
|
|||
|
||||
|
||||
def test_missing_pubkey_raises(tmp_path):
|
||||
cfg = PolicyConfig(enabled=True)
|
||||
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
|
||||
spec = _spec(tmp_path / "missing.pub")
|
||||
with pytest.raises(CAError, match="Public key not found"):
|
||||
check_sign_policy(cfg, spec)
|
||||
|
|
@ -126,7 +155,10 @@ def test_missing_pubkey_raises(tmp_path):
|
|||
def test_subject_from_env(tmp_path, monkeypatch):
|
||||
pubkey = tmp_path / "key.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA\n")
|
||||
cfg = PolicyConfig(enabled=True, subject_env="WARDEN_POLICY_SUBJECT")
|
||||
cfg = PolicyConfig(
|
||||
flex_auth_url="http://flex-auth.test",
|
||||
subject_env="WARDEN_POLICY_SUBJECT",
|
||||
)
|
||||
monkeypatch.setenv("WARDEN_POLICY_SUBJECT", "iam:bernd")
|
||||
|
||||
mock_response = MagicMock()
|
||||
|
|
@ -137,4 +169,141 @@ def test_subject_from_env(tmp_path, monkeypatch):
|
|||
check_sign_policy(cfg, _spec(pubkey))
|
||||
|
||||
body = post.call_args[1]["json"]
|
||||
assert body["subject"]["id"] == "iam:bernd"
|
||||
assert body["subject"]["id"] == "iam:bernd"
|
||||
|
||||
# --- caller identity (FLEX-WP-0016 / WARDEN-WP-0031) -----------------------
|
||||
|
||||
def test_caller_auth_none_sends_no_header():
|
||||
assert caller_auth_headers(CallerAuthConfig()) == {}
|
||||
|
||||
|
||||
def test_caller_auth_file_reads_projected_token(tmp_path):
|
||||
token_file = tmp_path / "token"
|
||||
token_file.write_text("sa-token-value\n")
|
||||
cfg = CallerAuthConfig(mode="file", token_path=token_file)
|
||||
assert caller_auth_headers(cfg) == {"Authorization": "Bearer sa-token-value"}
|
||||
|
||||
|
||||
def test_caller_auth_file_missing_raises(tmp_path):
|
||||
cfg = CallerAuthConfig(mode="file", token_path=tmp_path / "absent")
|
||||
with pytest.raises(CallerIdentityError, match="unreadable"):
|
||||
resolve_caller_token(cfg)
|
||||
|
||||
|
||||
def test_caller_auth_env_mode(monkeypatch):
|
||||
monkeypatch.setenv("WARDEN_POLICY_CALLER_TOKEN", " env-token ")
|
||||
assert resolve_caller_token(CallerAuthConfig(mode="env")) == "env-token"
|
||||
monkeypatch.setenv("WARDEN_POLICY_CALLER_TOKEN", "")
|
||||
with pytest.raises(CallerIdentityError, match="unset or empty"):
|
||||
resolve_caller_token(CallerAuthConfig(mode="env"))
|
||||
|
||||
|
||||
def test_caller_auth_command_mode_uses_stdout(monkeypatch):
|
||||
cfg = CallerAuthConfig(mode="command", command=["kubectl", "create", "token"])
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
assert cmd == cfg.command
|
||||
return subprocess.CompletedProcess(cmd, 0, stdout="minted-token\n", stderr="")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
assert resolve_caller_token(cfg) == "minted-token"
|
||||
|
||||
|
||||
def test_caller_auth_command_failure_message_excludes_token(monkeypatch):
|
||||
cfg = CallerAuthConfig(mode="command", command=["kubectl", "create", "token"])
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="error: forbidden\n")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
with pytest.raises(CallerIdentityError, match="error: forbidden"):
|
||||
resolve_caller_token(cfg)
|
||||
|
||||
|
||||
def test_caller_auth_rejects_whitespace_token(tmp_path):
|
||||
token_file = tmp_path / "token"
|
||||
token_file.write_text("two words")
|
||||
cfg = CallerAuthConfig(mode="file", token_path=token_file)
|
||||
with pytest.raises(CallerIdentityError, match="whitespace"):
|
||||
resolve_caller_token(cfg)
|
||||
|
||||
|
||||
def test_sign_policy_sends_authorization_header(tmp_path, monkeypatch):
|
||||
"""The header flex-auth's ops-warden pin needs to leave warn mode."""
|
||||
from warden import policy as policy_mod
|
||||
|
||||
token_file = tmp_path / "token"
|
||||
token_file.write_text("sa-token-value")
|
||||
pubkey = tmp_path / "id.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA test\n")
|
||||
|
||||
cfg = PolicyConfig(
|
||||
flex_auth_url="http://flex-auth.test",
|
||||
caller_auth=CallerAuthConfig(mode="file", token_path=token_file),
|
||||
)
|
||||
spec = CertSpec(
|
||||
actor_name="agt-state-hub-bridge",
|
||||
actor_type=ActorType.AGT,
|
||||
principals=["agt"],
|
||||
ttl_hours=24,
|
||||
pubkey_path=pubkey,
|
||||
)
|
||||
|
||||
seen = {}
|
||||
|
||||
class _Response:
|
||||
status_code = 200
|
||||
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return {"effect": "allow", "id": "decision:49350f1064f674d7"}
|
||||
|
||||
def fake_post(url, json=None, headers=None, timeout=None):
|
||||
seen["headers"] = headers
|
||||
return _Response()
|
||||
|
||||
monkeypatch.setattr(policy_mod.httpx, "post", fake_post)
|
||||
assert policy_mod.check_sign_policy(cfg, spec) == "decision:49350f1064f674d7"
|
||||
assert seen["headers"] == {"Authorization": "Bearer sa-token-value"}
|
||||
|
||||
|
||||
def test_sign_policy_fail_closed_when_caller_token_unavailable(tmp_path):
|
||||
from warden.ca import CAError
|
||||
from warden import policy as policy_mod
|
||||
|
||||
pubkey = tmp_path / "id.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA test\n")
|
||||
cfg = PolicyConfig(
|
||||
flex_auth_url="http://flex-auth.test",
|
||||
zone_registry_path=_zone_registry(tmp_path, "z3-critical"),
|
||||
caller_auth=CallerAuthConfig(mode="file", token_path=tmp_path / "absent"),
|
||||
)
|
||||
spec = CertSpec(
|
||||
actor_name="agt-state-hub-bridge",
|
||||
actor_type=ActorType.AGT,
|
||||
principals=["agt"],
|
||||
ttl_hours=24,
|
||||
pubkey_path=pubkey,
|
||||
)
|
||||
with pytest.raises(CAError, match="caller identity unavailable"):
|
||||
policy_mod.check_sign_policy(cfg, spec)
|
||||
|
||||
|
||||
def test_advisory_decision_is_recorded_and_does_not_block(tmp_path):
|
||||
pubkey = tmp_path / "id.pub"
|
||||
pubkey.write_text("ssh-ed25519 AAAA test\n")
|
||||
cfg = PolicyConfig(flex_auth_url="http://flex-auth.test")
|
||||
response = MagicMock()
|
||||
response.json.return_value = {
|
||||
"effect": "audit_only",
|
||||
"reason": "advisory_would_deny_disallowed_principal",
|
||||
"id": "decision:advisory",
|
||||
}
|
||||
response.raise_for_status = MagicMock()
|
||||
spec = _spec(pubkey)
|
||||
with patch("warden.policy.httpx.post", return_value=response):
|
||||
assert check_sign_policy(cfg, spec) == "decision:advisory"
|
||||
assert spec.policy_zone == "unknown"
|
||||
assert spec.policy_outcome == "audit_only"
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ def test_real_descriptors_load():
|
|||
assert c.requires_env_posture == "prod"
|
||||
# YAML `on` gotcha must not have become a boolean
|
||||
assert c.env("test").audit == "on"
|
||||
# WARDEN-WP-0029 third axis
|
||||
assert c.organization_posture.id == "build"
|
||||
assert "workstation_oidc_acceptable" in c.organization_posture.relaxations
|
||||
|
||||
|
||||
# --- the secret-flow lattice -----------------------------------------------
|
||||
|
|
@ -92,6 +95,12 @@ def _valid_data() -> dict:
|
|||
],
|
||||
"dataclass_floor": {"synthetic": "M0", "internal": "M1"},
|
||||
"lattice": {"requires_env_posture": "prod", "rule": "no-write-down"},
|
||||
"organization_posture": {
|
||||
"id": "build",
|
||||
"summary": "test build posture",
|
||||
"relaxations": ["workstation_oidc_acceptable"],
|
||||
"graduation_triggers": ["first_customer_data"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -136,6 +145,16 @@ def test_cli_policy_list_json(monkeypatch):
|
|||
payload = json.loads(r.stdout)
|
||||
assert payload["requires_env_posture"] == "prod"
|
||||
assert len(payload["maturity_levels"]) == 4
|
||||
assert payload["organization_posture"]["id"] == "build"
|
||||
|
||||
|
||||
def test_cli_policy_show_organization(monkeypatch):
|
||||
monkeypatch.setenv("WARDEN_POSTURE_CATALOG", str(_repo_posture()))
|
||||
r = runner.invoke(app, ["policy", "show", "build", "--json"])
|
||||
assert r.exit_code == 0
|
||||
payload = json.loads(r.stdout)
|
||||
assert payload["axis"] == "organization_posture"
|
||||
assert payload["id"] == "build"
|
||||
|
||||
|
||||
def test_cli_policy_show_unknown_exits_1(monkeypatch):
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from warden.proxy import (
|
|||
ProxyError,
|
||||
ResolvedFetch,
|
||||
caller_auth_present,
|
||||
proxy_attended_login_exec,
|
||||
proxy_exec,
|
||||
proxy_fetch,
|
||||
resolve_fetch_command,
|
||||
|
|
@ -64,16 +65,16 @@ def test_resolve_refuses_non_exec_capable():
|
|||
resolve_fetch_command(_entry(exec_capable=False, fetch_command=None))
|
||||
|
||||
|
||||
def test_resolve_piped_fetch_uses_shell_cmd():
|
||||
def test_resolve_bao_fetch_uses_argv():
|
||||
from warden.routing import load_catalog
|
||||
|
||||
catalog = load_catalog(Path(__file__).resolve().parents[1] / "registry" / "routing" / "catalog.yaml")
|
||||
entry = catalog.get("reuse-surface-hub-write-token")
|
||||
resolved = resolve_fetch_command(entry)
|
||||
assert resolved.argv is None
|
||||
assert resolved.shell_cmd is not None
|
||||
assert "| base64 -d" in resolved.shell_cmd
|
||||
assert "reuse-surface-env" in resolved.shell_cmd
|
||||
assert resolved.argv is not None
|
||||
assert resolved.shell_cmd is None
|
||||
assert resolved.argv[0] == "bao"
|
||||
assert "platform/workloads/reuse/reuse-surface/runtime-secrets" in resolved.argv
|
||||
|
||||
|
||||
# --- G2: transit-only fetch (inherited stdout) -----------------------------
|
||||
|
|
@ -193,7 +194,6 @@ def _warden_yaml(tmp_path: Path) -> Path:
|
|||
(tmp_path / "ca").write_text("")
|
||||
cfg.write_text(
|
||||
f"backend: local\nca_key: {tmp_path/'ca'}\nstate_dir: {tmp_path/'state'}\n"
|
||||
"policy:\n enabled: false\n"
|
||||
)
|
||||
return cfg
|
||||
|
||||
|
|
@ -203,10 +203,11 @@ def _proxy_env(monkeypatch, tmp_path):
|
|||
monkeypatch.setenv("WARDEN_CONFIG", str(_warden_yaml(tmp_path)))
|
||||
|
||||
|
||||
def test_cli_proxy_refuses_without_policy_ack(monkeypatch, tmp_path):
|
||||
def test_cli_proxy_unknown_zone_fail_open_reaches_transport_guard(monkeypatch, tmp_path):
|
||||
_proxy_env(monkeypatch, tmp_path)
|
||||
monkeypatch.setenv("VAULT_TOKEN", "caller")
|
||||
# subprocess must never run if the gate blocks first.
|
||||
# The unknown-zone profile proceeds when no evaluator is configured, then
|
||||
# the independent safe-transport boundary still refuses captured stdout.
|
||||
monkeypatch.setattr(
|
||||
"warden.proxy.subprocess.run",
|
||||
lambda *a, **k: (_ for _ in ()).throw(AssertionError("fetch ran despite gate")),
|
||||
|
|
@ -216,8 +217,8 @@ def test_cli_proxy_refuses_without_policy_ack(monkeypatch, tmp_path):
|
|||
["access", "npm", "--domain", "coulomb_social", "--field", "NPM_AUTH_TOKEN",
|
||||
"--path", "platform/x/y/z", "--fetch"],
|
||||
)
|
||||
assert r.exit_code == 4
|
||||
assert "not enforced" in r.stdout or "not enforced" in str(r.output)
|
||||
assert r.exit_code == 6
|
||||
assert "unknown-zone fail_open" in r.output
|
||||
|
||||
|
||||
def test_cli_proxy_requires_caller_auth(monkeypatch, tmp_path):
|
||||
|
|
@ -228,44 +229,230 @@ def test_cli_proxy_requires_caller_auth(monkeypatch, tmp_path):
|
|||
r = runner.invoke(
|
||||
app,
|
||||
["access", "npm", "--domain", "coulomb_social", "--field", "NPM_AUTH_TOKEN",
|
||||
"--path", "platform/x/y/z", "--fetch", "--no-policy"],
|
||||
"--path", "platform/x/y/z", "--fetch"],
|
||||
)
|
||||
assert r.exit_code == 3
|
||||
|
||||
|
||||
def test_cli_proxy_rejects_retired_no_policy_bypass(monkeypatch, tmp_path):
|
||||
_proxy_env(monkeypatch, tmp_path)
|
||||
monkeypatch.setenv("VAULT_TOKEN", "caller")
|
||||
monkeypatch.setattr(
|
||||
"warden.proxy.subprocess.run",
|
||||
lambda *a, **k: (_ for _ in ()).throw(AssertionError("fetch ran despite retired flag")),
|
||||
)
|
||||
r = runner.invoke(
|
||||
app,
|
||||
["access", "npm", "--domain", "coulomb_social", "--field", "NPM_AUTH_TOKEN",
|
||||
"--path", "platform/x/y/z", "--fetch", "--no-policy"],
|
||||
)
|
||||
assert r.exit_code == 2
|
||||
assert "--no-policy is retired" in r.output
|
||||
|
||||
|
||||
# --- T4: login lane --------------------------------------------------------
|
||||
|
||||
def test_cli_login_lane_runs_without_token_or_policy_ack(monkeypatch, tmp_path):
|
||||
"""Login lane skips the caller-auth precheck and the secret-read gate."""
|
||||
def test_cli_login_lane_contains_login_handoff_and_revocation(monkeypatch, tmp_path):
|
||||
"""Login and its reviewed child share a private, silent helper session."""
|
||||
_proxy_env(monkeypatch, tmp_path)
|
||||
monkeypatch.delenv("VAULT_TOKEN", raising=False)
|
||||
monkeypatch.delenv("BAO_TOKEN", raising=False)
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path) # no ~/.vault-token
|
||||
|
||||
ran = {}
|
||||
calls = []
|
||||
|
||||
def fake_run(argv, **kw):
|
||||
ran["argv"] = argv
|
||||
ran["stdout"] = kw.get("stdout")
|
||||
return subprocess.CompletedProcess(argv, 0)
|
||||
calls.append((argv, kw))
|
||||
assert kw["stdout"] is subprocess.PIPE
|
||||
assert kw["stderr"] is subprocess.PIPE
|
||||
private_home = Path(kw["env"]["HOME"])
|
||||
assert private_home != tmp_path
|
||||
assert oct(private_home.stat().st_mode & 0o777) == "0o700"
|
||||
helper = private_home / ".vault-token"
|
||||
assert oct(helper.stat().st_mode & 0o777) == "0o600"
|
||||
if argv[:2] == ["bao", "login"]:
|
||||
helper.write_bytes(b"non-production-test-double")
|
||||
helper.chmod(0o600)
|
||||
return subprocess.CompletedProcess(argv, 0, stdout=b"", stderr=b"")
|
||||
|
||||
monkeypatch.setattr("warden.proxy.subprocess.run", fake_run)
|
||||
r = runner.invoke(app, ["access", "login oidc", "--domain", "coulomb_social", "--fetch"])
|
||||
r = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"access", "login oidc", "--domain", "coulomb_social",
|
||||
"--exec", "--", "true",
|
||||
],
|
||||
)
|
||||
assert r.exit_code == 0
|
||||
assert ran["argv"][:2] == ["bao", "login"] # interactive login ran
|
||||
assert ran["stdout"] is None # inherited stdio — token not captured
|
||||
assert [call[0][:2] for call in calls] == [
|
||||
["bao", "login"],
|
||||
["true"],
|
||||
["bao", "token"],
|
||||
]
|
||||
assert not (tmp_path / ".warden-attended-login").exists()
|
||||
assert "non-production-test-double" not in r.output
|
||||
audit = (tmp_path / "state" / "access-audit.log").read_text()
|
||||
assert "non-production-test-double" not in audit
|
||||
|
||||
|
||||
def test_cli_login_lane_rejects_exec(monkeypatch, tmp_path):
|
||||
def test_cli_login_lane_rejects_persistent_fetch(monkeypatch, tmp_path):
|
||||
_proxy_env(monkeypatch, tmp_path)
|
||||
monkeypatch.setattr(
|
||||
"warden.proxy.subprocess.run",
|
||||
lambda *a, **k: (_ for _ in ()).throw(AssertionError("should not run")),
|
||||
)
|
||||
r = runner.invoke(
|
||||
app, ["access", "login oidc", "--domain", "coulomb_social", "--exec", "--", "true"]
|
||||
app, ["access", "login oidc", "--domain", "coulomb_social", "--fetch"]
|
||||
)
|
||||
assert r.exit_code == 2
|
||||
assert "requires --exec" in r.output
|
||||
|
||||
|
||||
def test_attended_login_refuses_read_only_home_before_auth(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
monkeypatch.setattr(
|
||||
"warden.proxy.subprocess.run",
|
||||
lambda *a, **k: (_ for _ in ()).throw(AssertionError("OIDC started")),
|
||||
)
|
||||
tmp_path.chmod(0o555)
|
||||
try:
|
||||
with pytest.raises(ProxyError, match="writable default home"):
|
||||
proxy_attended_login_exec(
|
||||
ResolvedFetch(argv=["bao", "login", "-no-print"]),
|
||||
child_argv=["true"],
|
||||
)
|
||||
finally:
|
||||
tmp_path.chmod(0o700)
|
||||
|
||||
|
||||
def test_attended_login_persistence_failure_revokes_and_cleans(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
calls = []
|
||||
|
||||
def fake_run(argv, **kw):
|
||||
calls.append(argv)
|
||||
# Login succeeds but the pre-created helper remains empty: persistence failed.
|
||||
return subprocess.CompletedProcess(argv, 0, stdout=b"", stderr=b"")
|
||||
|
||||
monkeypatch.setattr("warden.proxy.subprocess.run", fake_run)
|
||||
with pytest.raises(ProxyError, match="failed closed before command handoff"):
|
||||
proxy_attended_login_exec(
|
||||
ResolvedFetch(argv=["bao", "login", "-no-print"]),
|
||||
child_argv=["should-not-run"],
|
||||
)
|
||||
assert calls == [
|
||||
["bao", "login", "-no-print", "-format=json"],
|
||||
["bao", "token", "revoke", "-self"],
|
||||
]
|
||||
assert not (tmp_path / ".warden-attended-login").exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stream", ["stdout", "stderr"])
|
||||
def test_attended_login_unexpected_output_is_contained_revoked_and_cleaned(
|
||||
monkeypatch, tmp_path, capsys, stream
|
||||
):
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
sentinel = "hvs.NONPRODUCTION_SENTINEL"
|
||||
calls = []
|
||||
|
||||
def fake_run(argv, **kw):
|
||||
calls.append((argv, dict(kw["env"])))
|
||||
if argv[:2] == ["bao", "login"]:
|
||||
output = sentinel.encode()
|
||||
return subprocess.CompletedProcess(
|
||||
argv,
|
||||
0,
|
||||
stdout=output if stream == "stdout" else b"",
|
||||
stderr=output if stream == "stderr" else b"",
|
||||
)
|
||||
if argv[:3] == ["bao", "token", "revoke"]:
|
||||
# The helper is empty. The second contained attempt uses the captured
|
||||
# value only through BAO_TOKEN, never argv or visible output.
|
||||
return subprocess.CompletedProcess(
|
||||
argv,
|
||||
0 if kw["env"].get("BAO_TOKEN") == sentinel else 1,
|
||||
stdout=b"",
|
||||
stderr=b"",
|
||||
)
|
||||
raise AssertionError("reviewed child ran after unexpected login output")
|
||||
|
||||
monkeypatch.setattr("warden.proxy.subprocess.run", fake_run)
|
||||
with pytest.raises(ProxyError, match="failed closed before command handoff") as exc:
|
||||
proxy_attended_login_exec(
|
||||
ResolvedFetch(argv=["bao", "login", "-no-print"]),
|
||||
child_argv=["should-not-run"],
|
||||
)
|
||||
captured = capsys.readouterr()
|
||||
assert sentinel not in str(exc.value)
|
||||
assert sentinel not in captured.out
|
||||
assert sentinel not in captured.err
|
||||
assert all(sentinel not in " ".join(argv) for argv, _ in calls)
|
||||
assert calls[-1][1]["BAO_TOKEN"] == sentinel
|
||||
assert not (tmp_path / ".warden-attended-login").exists()
|
||||
|
||||
|
||||
def test_attended_login_contained_success_output_never_escapes(monkeypatch, tmp_path, capsys):
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
sentinel = "hvs.NONPRODUCTION_CONTAINED_LOGIN"
|
||||
child_ran = False
|
||||
|
||||
def fake_run(argv, **kw):
|
||||
nonlocal child_ran
|
||||
helper = Path(kw["env"]["HOME"]) / ".vault-token"
|
||||
if argv[:2] == ["bao", "login"]:
|
||||
helper.write_text(sentinel)
|
||||
helper.chmod(0o600)
|
||||
return subprocess.CompletedProcess(
|
||||
argv,
|
||||
0,
|
||||
stdout=json.dumps({"auth": {"client_token": sentinel}}).encode(),
|
||||
stderr=b"",
|
||||
)
|
||||
if argv == ["reviewed-child"]:
|
||||
child_ran = True
|
||||
return subprocess.CompletedProcess(argv, 0, stdout=b"", stderr=b"")
|
||||
if argv[:3] == ["bao", "token", "revoke"]:
|
||||
return subprocess.CompletedProcess(argv, 0, stdout=b"", stderr=b"")
|
||||
raise AssertionError(argv)
|
||||
|
||||
monkeypatch.setattr("warden.proxy.subprocess.run", fake_run)
|
||||
assert proxy_attended_login_exec(
|
||||
ResolvedFetch(argv=["bao", "login", "-no-print"]),
|
||||
child_argv=["reviewed-child"],
|
||||
) == 0
|
||||
captured = capsys.readouterr()
|
||||
assert child_ran is True
|
||||
assert sentinel not in captured.out
|
||||
assert sentinel not in captured.err
|
||||
assert not (tmp_path / ".warden-attended-login").exists()
|
||||
|
||||
|
||||
def test_attended_login_preserves_caller_warden_config_for_reviewed_child(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
monkeypatch.delenv("WARDEN_CONFIG", raising=False)
|
||||
caller_config = tmp_path / ".config" / "warden" / "warden.yaml"
|
||||
caller_config.parent.mkdir(parents=True)
|
||||
caller_config.write_text("backend: local\n")
|
||||
seen_config = None
|
||||
|
||||
def fake_run(argv, **kw):
|
||||
nonlocal seen_config
|
||||
helper = Path(kw["env"]["HOME"]) / ".vault-token"
|
||||
if argv[:2] == ["bao", "login"]:
|
||||
helper.write_text("non-production-test-double")
|
||||
helper.chmod(0o600)
|
||||
if argv == ["reviewed-child"]:
|
||||
seen_config = kw["env"].get("WARDEN_CONFIG")
|
||||
return subprocess.CompletedProcess(argv, 0, stdout=b"", stderr=b"")
|
||||
|
||||
monkeypatch.setattr("warden.proxy.subprocess.run", fake_run)
|
||||
assert proxy_attended_login_exec(
|
||||
ResolvedFetch(argv=["bao", "login", "-no-print"]),
|
||||
child_argv=["reviewed-child"],
|
||||
) == 0
|
||||
assert seen_config == str(caller_config)
|
||||
|
||||
|
||||
def test_real_catalog_login_entry_is_login_lane():
|
||||
|
|
@ -281,9 +468,142 @@ def test_invalid_lane_rejected(tmp_path):
|
|||
id="x", title="t", need_keywords=["k"], owner_repo="o", subsystem="s",
|
||||
warden_executes=False, wiki_ref="w", canon_ref="c", reviewed="2026-06-27",
|
||||
status="active", lane="bogus",
|
||||
workload_ref={"applicability": "not-applicable", "reason": "fixture"},
|
||||
)
|
||||
p = tmp_path / "c.yaml"
|
||||
p.write_text(yaml.dump({"version": 1, "entries": [entry]}))
|
||||
import pytest
|
||||
with pytest.raises(CatalogError, match="invalid lane"):
|
||||
load_catalog(p)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Safe access transports (WARDEN-WP-0026 T02) — no secret values on stdout
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
from warden.proxy import ( # noqa: E402
|
||||
build_wrapped_fetch,
|
||||
is_bao_kv_fetch,
|
||||
proxy_fetch_to_file,
|
||||
proxy_fetch_wrapped,
|
||||
)
|
||||
|
||||
|
||||
def test_fetch_to_file_writes_mode_0600_and_no_stdout(tmp_path, capsys):
|
||||
out = tmp_path / "secret.out"
|
||||
rc = proxy_fetch_to_file(ResolvedFetch(shell_cmd="printf 'sekret'"), out)
|
||||
assert rc == 0
|
||||
assert out.read_text() == "sekret"
|
||||
assert oct(out.stat().st_mode & 0o777) == "0o600"
|
||||
# nothing printed to stdout/stderr by the transport itself
|
||||
captured = capsys.readouterr()
|
||||
assert "sekret" not in captured.out and "sekret" not in captured.err
|
||||
|
||||
|
||||
def test_fetch_to_file_forces_0600_on_preexisting_loose_file(tmp_path):
|
||||
out = tmp_path / "pre.out"
|
||||
out.write_text("old")
|
||||
out.chmod(0o644)
|
||||
proxy_fetch_to_file(ResolvedFetch(shell_cmd="printf 'new'"), out)
|
||||
assert out.read_text() == "new"
|
||||
assert oct(out.stat().st_mode & 0o777) == "0o600"
|
||||
|
||||
|
||||
def test_wrapped_fetch_returns_token_not_value():
|
||||
payload = '{"wrap_info":{"token":"hvs.WRAP"}}'
|
||||
token = proxy_fetch_wrapped(ResolvedFetch(shell_cmd=f"printf '%s' '{payload}'"))
|
||||
assert token == "hvs.WRAP"
|
||||
|
||||
|
||||
def test_wrapped_fetch_bad_output_raises():
|
||||
with pytest.raises(ProxyError, match="wrapping token"):
|
||||
proxy_fetch_wrapped(ResolvedFetch(shell_cmd="printf 'not-json'"))
|
||||
|
||||
|
||||
def test_build_wrapped_fetch_only_for_bao_kv():
|
||||
bao = _entry(fetch_command="bao kv get -field=API_TOKEN platform/x", path_template="platform/x")
|
||||
assert is_bao_kv_fetch(bao)
|
||||
argv = build_wrapped_fetch(bao, ttl="9m").argv
|
||||
assert argv == ["bao", "kv", "get", "-wrap-ttl=9m", "-format=json", "platform/x"]
|
||||
|
||||
piped = _entry(fetch_command="kubectl get secret x -o json | base64 -d", path_template="x")
|
||||
assert not is_bao_kv_fetch(piped)
|
||||
with pytest.raises(ProxyError, match="response wrapping"):
|
||||
build_wrapped_fetch(piped)
|
||||
|
||||
|
||||
def test_build_wrapped_fetch_refuses_placeholder_path():
|
||||
e = _entry(fetch_command="bao kv get -field=<FIELD> <path_template>",
|
||||
path_template="platform/workloads/<domain>/x")
|
||||
with pytest.raises(ProxyError, match="concrete path"):
|
||||
build_wrapped_fetch(e)
|
||||
|
||||
|
||||
def test_access_fetch_to_nonterminal_stdout_is_refused(tmp_path, monkeypatch):
|
||||
"""The anti-pattern: streaming a value to captured stdout is refused (exit 6)."""
|
||||
_proxy_env(monkeypatch, tmp_path)
|
||||
monkeypatch.setenv("VAULT_TOKEN", "caller-token") # G1 caller-auth precheck
|
||||
# The guard trips before the fetch runs; make a real bao call fail loudly if reached.
|
||||
monkeypatch.setattr(
|
||||
"warden.proxy.subprocess.run",
|
||||
lambda *a, **k: (_ for _ in ()).throw(AssertionError("fetch ran despite stdout guard")),
|
||||
)
|
||||
# CliRunner captures stdout (not a tty), so the guard trips without --unsafe-stdout.
|
||||
r = runner.invoke(app, ["access", "whynot-design-npm-publish", "--fetch"])
|
||||
assert r.exit_code == 6
|
||||
assert "sanctioned transport" in r.output.lower() or "refusing" in r.output.lower()
|
||||
|
||||
|
||||
def test_access_fingerprint_masks_and_bypasses_stdout_guard(monkeypatch, tmp_path):
|
||||
"""--fingerprint prints a masked fingerprint (never the value) even to captured stdout."""
|
||||
_proxy_env(monkeypatch, tmp_path)
|
||||
monkeypatch.setenv("VAULT_TOKEN", "caller-token")
|
||||
|
||||
class _Fake:
|
||||
returncode = 0
|
||||
stdout = "top-secret-token-value"
|
||||
|
||||
monkeypatch.setattr("warden.proxy.subprocess.run", lambda *a, **k: _Fake())
|
||||
r = runner.invoke(
|
||||
app,
|
||||
["access", "whynot-design-npm-publish", "--fingerprint"],
|
||||
)
|
||||
assert r.exit_code == 0
|
||||
assert "top-secret-token-value" not in r.output # value never shown
|
||||
assert "hidden" in r.output and "sha256:" in r.output
|
||||
|
||||
|
||||
def test_access_agent_high_risk_raw_stream_refused(tmp_path, monkeypatch):
|
||||
"""WP-0026 T04: WARDEN_AGENT_ID + risk=high refuses raw value stream (exit 7)."""
|
||||
_proxy_env(monkeypatch, tmp_path)
|
||||
monkeypatch.setenv("VAULT_TOKEN", "caller-token")
|
||||
monkeypatch.setenv("WARDEN_AGENT_ID", "grok")
|
||||
# Prefer high-risk lane; use --unsafe-stdout so T02 would allow if T04 failed.
|
||||
r = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"access", "railiance-backup-offsite-lane",
|
||||
"--fetch", "--unsafe-stdout",
|
||||
],
|
||||
)
|
||||
assert r.exit_code == 7, r.output
|
||||
assert "agent read-boundary" in r.output.lower() or "risk=high" in r.output.lower()
|
||||
|
||||
|
||||
def test_access_agent_high_risk_fingerprint_allowed(tmp_path, monkeypatch):
|
||||
"""Agents may use --fingerprint on high-risk lanes (no raw value)."""
|
||||
_proxy_env(monkeypatch, tmp_path)
|
||||
monkeypatch.setenv("VAULT_TOKEN", "caller-token")
|
||||
monkeypatch.setenv("WARDEN_AGENT_ID", "grok")
|
||||
|
||||
class _Fake:
|
||||
returncode = 0
|
||||
stdout = "should-not-appear"
|
||||
|
||||
monkeypatch.setattr("warden.proxy.subprocess.run", lambda *a, **k: _Fake())
|
||||
r = runner.invoke(
|
||||
app,
|
||||
["access", "railiance-backup-offsite-lane", "--fingerprint"],
|
||||
)
|
||||
assert r.exit_code == 0, r.output
|
||||
assert "should-not-appear" not in r.output
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ No test here requires a live subsystem — routing is a read-only pointer layer.
|
|||
"""
|
||||
import json
|
||||
import re
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
|
@ -11,10 +12,9 @@ import yaml
|
|||
from typer.testing import CliRunner
|
||||
|
||||
from warden.cli import app
|
||||
from datetime import date
|
||||
|
||||
from warden.routing import CatalogError, load_catalog
|
||||
from warden.routing.catalog import days_since_review, find_catalog_path, is_review_stale
|
||||
from warden.scorecard import check_catalog_rotation_coverage
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
|
@ -40,6 +40,10 @@ SSH_ENTRY = {
|
|||
"canon_ref": "net-kingdom/docs/x.md",
|
||||
"reviewed": "2026-06-18",
|
||||
"status": "active",
|
||||
"workload_ref": {
|
||||
"applicability": "not-applicable",
|
||||
"reason": "generic certificate action",
|
||||
},
|
||||
"cert_command": "warden sign <actor> --pubkey <path>",
|
||||
"steps": ["confirm inventory", "sign"],
|
||||
}
|
||||
|
|
@ -55,6 +59,10 @@ ROUTED_ENTRY = {
|
|||
"canon_ref": "net-kingdom/docs/x.md",
|
||||
"reviewed": "2026-06-18",
|
||||
"status": "active",
|
||||
"workload_ref": {
|
||||
"applicability": "not-applicable",
|
||||
"reason": "generic credential pattern",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -76,6 +84,45 @@ def test_real_catalog_has_one_executed_lane():
|
|||
assert [e.id for e in executed] == ["ssh-cert-host-access"]
|
||||
|
||||
|
||||
def test_every_catalog_lane_declares_workload_applicability():
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
assert all(entry.workload_ref is not None for entry in catalog.entries)
|
||||
assert {entry.workload_ref.resolution for entry in catalog.entries} == {
|
||||
"resolved", "unknown", "not-applicable"
|
||||
}
|
||||
|
||||
|
||||
def test_managed_and_operational_workload_references_parse():
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
managed = catalog.get("issue-core-ingestion-api-key").workload_ref
|
||||
assert managed.resolution == "resolved"
|
||||
assert (managed.rapp_id, managed.name, managed.deployable) == (
|
||||
"rapp-issue-core", "issue-core", "issue-core"
|
||||
)
|
||||
operational = catalog.get("ops-warden-warden-sign-token").workload_ref
|
||||
assert operational.resolution == "resolved"
|
||||
assert operational.rapp_id is None
|
||||
assert operational.name == "ops-warden"
|
||||
assert operational.declaration_ref == "tenancy.yaml"
|
||||
|
||||
|
||||
def test_workload_reference_rejects_ambiguous_absence(tmp_path):
|
||||
bad = dict(ROUTED_ENTRY)
|
||||
bad.pop("workload_ref")
|
||||
with pytest.raises(CatalogError, match="workload_ref"):
|
||||
load_catalog(_write_catalog(tmp_path, [bad]))
|
||||
|
||||
|
||||
def test_workload_reference_rejects_malformed_managed_target(tmp_path):
|
||||
bad = dict(ROUTED_ENTRY)
|
||||
bad["workload_ref"] = {
|
||||
"applicability": "applicable",
|
||||
"rapp_id": "rapp-issue-core",
|
||||
}
|
||||
with pytest.raises(CatalogError, match="requires name"):
|
||||
load_catalog(_write_catalog(tmp_path, [bad]))
|
||||
|
||||
|
||||
def test_ops_warden_warden_sign_lane_has_native_exec():
|
||||
"""RAILIANCE-WP-0005 T08 — broker lane routes to railiance-platform credential exec."""
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
|
|
@ -106,6 +153,32 @@ def test_whynot_design_npm_lane_is_concrete_and_resolvable():
|
|||
assert "platform/workloads/coulomb/whynot-design/npm-publish" in e.fetch_command
|
||||
|
||||
|
||||
def test_policy_nexus_source_read_lane_is_exact_high_risk_and_resolvable():
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
entry = catalog.get("policy-nexus-forgejo-source-read")
|
||||
assert entry is not None and entry.is_active and entry.exec_capable
|
||||
assert entry.resolvable is True
|
||||
assert entry.risk == "high"
|
||||
assert entry.owner_repo == "railiance-platform"
|
||||
assert entry.fetch_command == (
|
||||
"bao kv get -field=FORGEJO_SOURCE_TOKEN "
|
||||
"platform/workloads/policy-nexus/forgejo-source-read"
|
||||
)
|
||||
assert entry.path_template == "platform/workloads/policy-nexus/forgejo-source-read"
|
||||
assert entry.auth_method.endswith(
|
||||
"role=policy-nexus-forgejo-source-workload-kv-read"
|
||||
)
|
||||
assert entry.delegation is not None and entry.delegation.mode == "native"
|
||||
|
||||
|
||||
def test_route_find_policy_nexus_source_read_prefers_concrete_lane():
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
matches = catalog.find(
|
||||
"policy nexus Forgejo private source repository read token Actions", limit=1
|
||||
)
|
||||
assert matches[0].id == "policy-nexus-forgejo-source-read"
|
||||
|
||||
|
||||
def test_generic_and_template_lanes_not_resolvable():
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
# generic openbao lane has <FIELD>/<path_template>; login lane has <domain>.
|
||||
|
|
@ -113,6 +186,45 @@ def test_generic_and_template_lanes_not_resolvable():
|
|||
assert catalog.get("key-cape-oidc-login").resolvable is False
|
||||
|
||||
|
||||
def test_platform_admin_login_lane_is_exact_and_non_value_bearing():
|
||||
entry = load_catalog(_repo_catalog()).get("openbao-platform-admin-login")
|
||||
assert entry.lane == "login"
|
||||
assert entry.risk == "high"
|
||||
assert entry.fetch_command == (
|
||||
"bao login -no-print -method=oidc -path=netkingdom role=platform-admin"
|
||||
)
|
||||
assert entry.workload_ref.resolution == "not-applicable"
|
||||
|
||||
|
||||
def test_netkingdom_sso_bind_lanes_are_routed_but_not_resolvable():
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
for lane_id in (
|
||||
"net-kingdom-lldap-bind-credential",
|
||||
"net-kingdom-privacyidea-admin-token",
|
||||
):
|
||||
entry = catalog.get(lane_id)
|
||||
assert entry is not None
|
||||
assert entry.owner_repo == "railiance-platform"
|
||||
assert entry.risk == "high"
|
||||
assert entry.warden_executes is False
|
||||
assert entry.exec_capable is False
|
||||
assert entry.resolvable is False
|
||||
assert entry.delegation.blocked_on
|
||||
assert "net-kingdom-sso-bind-credentials.md#worker-checklist" in entry.wiki_ref
|
||||
|
||||
|
||||
def test_openbao_recovery_ceremony_is_non_value_bearing_owner_pointer():
|
||||
entry = load_catalog(_repo_catalog()).get("openbao-shamir-recovery-ceremony")
|
||||
assert entry.lane == "ceremony"
|
||||
assert entry.risk == "high"
|
||||
assert entry.owner_repo == "railiance-platform"
|
||||
assert entry.warden_executes is False
|
||||
assert entry.exec_capable is False
|
||||
assert entry.has_handoff is False
|
||||
assert entry.vends_secret is False
|
||||
assert entry.workload_ref.resolution == "not-applicable"
|
||||
|
||||
|
||||
def test_find_exact_id_wins_over_keyword_collision():
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
# "npm" alone collides with openbao-api-key; the exact id must resolve uniquely.
|
||||
|
|
@ -288,8 +400,8 @@ def test_reuse_surface_hub_write_token_lane_is_resolvable():
|
|||
e = catalog.get("reuse-surface-hub-write-token")
|
||||
assert e is not None and e.is_active and e.exec_capable
|
||||
assert e.resolvable is True
|
||||
assert e.owner_repo == "reuse-surface"
|
||||
assert "reuse-surface-env" in e.fetch_command
|
||||
assert e.owner_repo == "railiance-platform"
|
||||
assert "platform/workloads/reuse/reuse-surface/runtime-secrets" in e.fetch_command
|
||||
|
||||
|
||||
def test_find_object_storage_sts():
|
||||
|
|
@ -448,3 +560,455 @@ def test_every_entry_has_reviewed_date():
|
|||
assert re.match(r"^\d{4}-\d{2}-\d{2}$", entry.reviewed), (
|
||||
f"{entry.id}: reviewed must be YYYY-MM-DD, got {entry.reviewed!r}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rotation / re-establishment guidance registry (WARDEN-WP-0026 T06)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_every_active_vending_lane_has_rotation_guidance():
|
||||
"""Coverage gate: an active lane that vends a secret must say how to renew it."""
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
missing = [e.id for e in catalog.entries if e.is_active and e.vends_secret and not e.has_rotation]
|
||||
assert not missing, f"active vending lanes lacking rotation guidance: {missing}"
|
||||
|
||||
|
||||
def test_scorecard_rotation_coverage_check_passes_on_repo_catalog():
|
||||
result = check_catalog_rotation_coverage()
|
||||
assert result.passed, result.detail
|
||||
|
||||
|
||||
def test_non_vending_lanes_are_exempt_from_rotation():
|
||||
"""SSH (issue), login, and pointer-only lanes carry no rotation block."""
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
assert catalog.get("ssh-cert-host-access").vends_secret is False # issue lane
|
||||
assert catalog.get("key-cape-oidc-login").vends_secret is False # login lane
|
||||
assert catalog.get("ops-bridge-tunnel").vends_secret is False # pointer only
|
||||
|
||||
|
||||
def test_rotation_block_parses_fields():
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
rot = catalog.get("forgejo-admin-api-token").rotation
|
||||
assert rot is not None
|
||||
assert rot.method in ("rotate", "re-establish")
|
||||
assert rot.owner == "railiance-platform"
|
||||
assert rot.steps and all(isinstance(s, str) for s in rot.steps)
|
||||
|
||||
|
||||
def test_re_establish_method_on_backup_lane():
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
rot = catalog.get("railiance-backup-offsite-lane").rotation
|
||||
assert rot is not None and rot.method == "re-establish"
|
||||
|
||||
|
||||
def test_invalid_rotation_method_rejected(tmp_path):
|
||||
entry = dict(ROUTED_ENTRY, rotation={"method": "renew", "owner": "x", "steps": ["a"]})
|
||||
with pytest.raises(CatalogError, match="rotation.method"):
|
||||
load_catalog(_write_catalog(tmp_path, [SSH_ENTRY, entry]))
|
||||
|
||||
|
||||
def test_rotation_steps_screened_for_pasted_token(tmp_path):
|
||||
"""A high-entropy pasted token in prose is rejected; ordinary prose is allowed."""
|
||||
leak = dict(ROUTED_ENTRY, rotation={
|
||||
"method": "rotate", "owner": "x",
|
||||
"steps": ["set the value to ghp_" + "aB3dE5" * 6], # mixed alnum → high-entropy run
|
||||
})
|
||||
with pytest.raises(CatalogError, match="high-entropy|secret"):
|
||||
load_catalog(_write_catalog(tmp_path, [SSH_ENTRY, leak]))
|
||||
|
||||
|
||||
def test_rotation_prose_allows_ordinary_sentences(tmp_path):
|
||||
"""Words like 'exists.' must not trip the terse 's.' prefix screen."""
|
||||
ok = dict(ROUTED_ENTRY, rotation={
|
||||
"method": "rotate", "owner": "railiance-platform",
|
||||
"steps": ["Rotate per the concrete workload's entry when one exists."],
|
||||
})
|
||||
catalog = load_catalog(_write_catalog(tmp_path, [SSH_ENTRY, ok]))
|
||||
assert catalog.get("openbao-api-key").rotation.steps
|
||||
|
||||
|
||||
def test_rotate_guide_cli_json():
|
||||
result = runner.invoke(app, ["rotate-guide", "forgejo-admin-api-token", "--json"])
|
||||
assert result.exit_code == 0
|
||||
payload = json.loads(result.stdout)
|
||||
assert payload["method"] == "rotate"
|
||||
assert payload["owner"] == "railiance-platform"
|
||||
assert payload["steps"]
|
||||
|
||||
|
||||
def test_rotate_guide_cli_ssh_lane_is_graceful():
|
||||
# SSH renewal is re-issuance, not a static rotation — exit 0, not an error.
|
||||
result = runner.invoke(app, ["rotate-guide", "ssh-cert-host-access"])
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Agent read-boundary + risk class (WARDEN-WP-0026 T04)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_high_risk_lanes_classified():
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
high = {e.id for e in catalog.entries if e.is_high_risk}
|
||||
assert "railiance-backup-offsite-lane" in high
|
||||
assert "forgejo-admin-api-token" in high
|
||||
assert "openrouter-llm-connect" in high
|
||||
# WARDEN-WP-0033-T02: these two were asserted standard here, and the assertion
|
||||
# held a defective grade still. Both paths carry a second credential the grade
|
||||
# ignored -- GITEA_BACKEND_TOKEN (CCR-2026-0002) and the dual-consumer webhook
|
||||
# HMAC (CCR-2026-0005). A read discloses every field at a path, so the grade
|
||||
# must cover the union, not the headline field.
|
||||
assert "issue-core-ingestion-api-key" in high
|
||||
assert "reuse-surface-hub-write-token" in high
|
||||
|
||||
|
||||
def test_invalid_risk_rejected(tmp_path):
|
||||
bad = dict(ROUTED_ENTRY, risk="critical")
|
||||
with pytest.raises(CatalogError, match="risk"):
|
||||
load_catalog(_write_catalog(tmp_path, [SSH_ENTRY, bad]))
|
||||
|
||||
|
||||
def test_backup_lane_promoted_and_resolvable():
|
||||
"""WP-0026 T07 — CCR-2026-0004 lane is active, resolvable, high-risk, has rotation."""
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
e = catalog.get("railiance-backup-offsite-lane")
|
||||
assert e is not None
|
||||
assert e.status == "active"
|
||||
assert e.resolvable is True
|
||||
assert e.is_high_risk is True
|
||||
assert e.has_rotation is True
|
||||
assert e.rotation.method == "re-establish"
|
||||
assert "NC_WEBDAV_TOKEN" in (e.fetch_command or "")
|
||||
assert "<" not in (e.fetch_command or "")
|
||||
|
||||
|
||||
def test_route_show_json_includes_risk():
|
||||
result = runner.invoke(app, ["route", "show", "railiance-backup-offsite-lane", "--json"])
|
||||
assert result.exit_code == 0
|
||||
payload = json.loads(result.stdout)
|
||||
assert payload["risk"] == "high"
|
||||
assert payload["high_risk"] is True
|
||||
assert payload["resolvable"] is True
|
||||
assert payload["status"] == "active"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Delegation register (WARDEN-WP-0030)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_every_catalog_entry_declares_delegation():
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
missing = [e.id for e in catalog.entries if e.delegation is None]
|
||||
assert missing == [], f"entries missing delegation block: {missing}"
|
||||
|
||||
|
||||
def test_every_proxy_declares_delegation():
|
||||
"""A new exec_capable proxy cannot land without answering the ownership question."""
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
missing = [
|
||||
e.id
|
||||
for e in catalog.entries
|
||||
if e.exec_capable and not e.warden_executes and e.delegation is None
|
||||
]
|
||||
assert missing == [], f"proxy lanes missing delegation: {missing}"
|
||||
|
||||
|
||||
def test_ssh_lane_is_permanent_delegation():
|
||||
e = load_catalog(_repo_catalog()).get("ssh-cert-host-access")
|
||||
assert e.delegation is not None
|
||||
assert e.delegation.mode == "permanent"
|
||||
assert e.delegation.intended_owner is None
|
||||
assert e.is_interim is False
|
||||
|
||||
|
||||
def test_native_exec_lanes_are_native_delegation():
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
for eid, owner in (
|
||||
("whynot-design-npm-publish", "secrets-engine"),
|
||||
("ops-warden-warden-sign-token", "railiance-platform"),
|
||||
):
|
||||
e = catalog.get(eid)
|
||||
assert e.delegation is not None
|
||||
assert e.delegation.mode == "native"
|
||||
assert e.delegation.intended_owner == owner
|
||||
|
||||
|
||||
def test_founder_interim_lanes_classified():
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
expected = {
|
||||
"rapp-qonto-keycape-client": "key-cape",
|
||||
"binky-company-email-imap": "tenant-engine",
|
||||
"binky-qonto-api": "tenant-engine",
|
||||
"railiance-backup-offsite-lane": "railiance-platform",
|
||||
"agent-harness-forgejo-deploy": "railiance-platform",
|
||||
}
|
||||
for eid, owner in expected.items():
|
||||
e = catalog.get(eid)
|
||||
assert e is not None and e.delegation is not None
|
||||
assert e.delegation.mode == "interim"
|
||||
assert e.delegation.intended_owner == owner
|
||||
assert e.delegation.blocked_on
|
||||
|
||||
|
||||
def test_missing_delegation_is_implicit_interim(tmp_path):
|
||||
catalog = load_catalog(_write_catalog(tmp_path, [dict(ROUTED_ENTRY)]))
|
||||
e = catalog.get("openbao-api-key")
|
||||
d = e.effective_delegation
|
||||
assert e.delegation is None
|
||||
assert d.implicit is True
|
||||
assert d.mode == "interim"
|
||||
assert d.intended_owner is None
|
||||
assert "unclassified" in (d.blocked_on or "")
|
||||
|
||||
|
||||
def test_interim_without_blocked_on_rejected(tmp_path):
|
||||
bad = dict(
|
||||
ROUTED_ENTRY,
|
||||
delegation={
|
||||
"mode": "interim",
|
||||
"intended_owner": "secrets-engine",
|
||||
"reviewed": "2026-08-15",
|
||||
},
|
||||
)
|
||||
with pytest.raises(CatalogError, match="blocked_on"):
|
||||
load_catalog(_write_catalog(tmp_path, [bad]))
|
||||
|
||||
|
||||
def test_non_permanent_without_owner_rejected(tmp_path):
|
||||
bad = dict(
|
||||
ROUTED_ENTRY,
|
||||
delegation={"mode": "native", "reviewed": "2026-08-15"},
|
||||
)
|
||||
with pytest.raises(CatalogError, match="intended_owner"):
|
||||
load_catalog(_write_catalog(tmp_path, [bad]))
|
||||
|
||||
|
||||
def test_invalid_delegation_mode_rejected(tmp_path):
|
||||
bad = dict(
|
||||
ROUTED_ENTRY,
|
||||
delegation={"mode": "maybe", "intended_owner": "x", "reviewed": "2026-08-15"},
|
||||
)
|
||||
with pytest.raises(CatalogError, match="delegation.mode"):
|
||||
load_catalog(_write_catalog(tmp_path, [bad]))
|
||||
|
||||
|
||||
def test_catalog_gaps_lists_only_interim():
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
gap_ids = {e.id for e in catalog.gaps(include_draft=True)}
|
||||
assert "ssh-cert-host-access" not in gap_ids
|
||||
assert "whynot-design-npm-publish" not in gap_ids
|
||||
assert "binky-company-email-imap" in gap_ids
|
||||
# WARDEN-WP-0033: openbao-api-key was listed here as an interim cover. It is
|
||||
# not one -- its path_template is a <domain>/<workload>/<bundle> routing
|
||||
# pattern rather than a single secret lane, so there is no front door for
|
||||
# anyone to take over. secrets-engine refused it on exactly that ground and
|
||||
# ops-warden agrees. A pointer to OpenBao is not a gap ops-warden is holding,
|
||||
# and counting it as one overstated the interim surface by a lane.
|
||||
assert "openbao-api-key" not in gap_ids
|
||||
assert all(catalog.get(i).is_interim for i in gap_ids)
|
||||
|
||||
|
||||
def test_cli_route_gaps_json(repo_catalog_env):
|
||||
result = runner.invoke(app, ["route", "gaps", "--json"])
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.stdout)
|
||||
assert data
|
||||
ids = {row["id"] for row in data}
|
||||
assert "binky-company-email-imap" in ids
|
||||
assert "ssh-cert-host-access" not in ids
|
||||
for row in data:
|
||||
assert row["mode"] == "interim"
|
||||
assert "intended_owner" in row
|
||||
assert "blocked_on" in row
|
||||
assert "days_since_review" in row
|
||||
|
||||
|
||||
def test_cli_route_show_includes_delegation(repo_catalog_env):
|
||||
result = runner.invoke(app, ["route", "show", "binky-qonto-api", "--json"])
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.stdout)
|
||||
assert data["delegation"]["mode"] == "interim"
|
||||
assert data["delegation"]["intended_owner"] == "tenant-engine"
|
||||
assert data["delegation"]["implicit"] is False
|
||||
|
||||
|
||||
# --- ADR-0007: absence is not a grade (WARDEN-WP-0032-T06) ------------------
|
||||
|
||||
|
||||
def _bare_entry(**overrides):
|
||||
"""A minimal RouteEntry, so these tests exercise defaults and nothing else."""
|
||||
from warden.routing.models import RouteEntry
|
||||
|
||||
fields = dict(
|
||||
id="x",
|
||||
title="t",
|
||||
need_keywords=[],
|
||||
owner_repo="r",
|
||||
subsystem="s",
|
||||
warden_executes=False,
|
||||
wiki_ref="w",
|
||||
canon_ref="c",
|
||||
reviewed="2026-08-20",
|
||||
status="active",
|
||||
)
|
||||
fields.update(overrides)
|
||||
return RouteEntry(**fields)
|
||||
|
||||
|
||||
def test_every_repo_catalog_lane_is_explicitly_graded():
|
||||
"""The CI gate. A lane added without a `risk` grade is a defect (ADR-0007)."""
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
ungraded = sorted(e.id for e in catalog.entries if not e.is_graded)
|
||||
assert ungraded == [], (
|
||||
f"{len(ungraded)} catalog lane(s) carry no explicit risk grade: {ungraded}. "
|
||||
"ADR-0007: absence is not a grade — grade the lane on merit."
|
||||
)
|
||||
|
||||
|
||||
def test_ungraded_lane_fails_safe_to_high_risk():
|
||||
"""RISK-F-0003 regression: an omitted grade must not wave a lane through.
|
||||
|
||||
Before ADR-0007 the dataclass default was "standard", so a lane that simply
|
||||
omitted the field landed outside the agent read-boundary silently.
|
||||
"""
|
||||
entry = _bare_entry()
|
||||
assert entry.risk == "ungraded"
|
||||
assert entry.is_graded is False
|
||||
assert entry.is_high_risk is True
|
||||
|
||||
|
||||
def test_unrecognised_grade_is_treated_as_high():
|
||||
"""A grade from a newer catalog must not be read as permission."""
|
||||
entry = _bare_entry(risk="spicy")
|
||||
assert entry.is_high_risk is True
|
||||
assert entry.is_graded is False
|
||||
|
||||
|
||||
def test_ungraded_risk_uses_maturity_derived_zone_default():
|
||||
entry = _bare_entry()
|
||||
assert entry.risk_for_zone(
|
||||
effective_zone="z0-experimental",
|
||||
admission="satisfied",
|
||||
synthetic_only=True,
|
||||
) == "standard"
|
||||
assert entry.risk_for_zone(
|
||||
effective_zone="z0-experimental",
|
||||
admission="unknown",
|
||||
synthetic_only=True,
|
||||
) == "high"
|
||||
assert entry.risk_for_zone(
|
||||
effective_zone="z3-critical",
|
||||
admission="satisfied",
|
||||
) == "critical"
|
||||
assert entry.risk_for_zone(effective_zone="unknown") == "high"
|
||||
|
||||
|
||||
def test_explicit_risk_grade_always_wins_over_zone_default():
|
||||
entry = _bare_entry(risk="standard")
|
||||
assert entry.risk_for_zone(
|
||||
effective_zone="z3-critical", admission="satisfied"
|
||||
) == "standard"
|
||||
|
||||
|
||||
def test_low_risk_vocabulary_is_explicit():
|
||||
for grade in ("standard", "low", "accepted"):
|
||||
entry = _bare_entry(risk=grade)
|
||||
assert entry.is_high_risk is False, grade
|
||||
assert entry.is_graded is True, grade
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Blocker staleness cadence + verification (WARDEN-WP-0033-T05)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_blocker_cadence_is_separate_from_pointer_cadence():
|
||||
"""Two claims with different half-lives must not share one threshold.
|
||||
|
||||
"Is this still the right owner and page?" is quarterly. "Has the owner
|
||||
answered yet?" is not. Sharing 90 days made the second one inert -- the
|
||||
register was six days old, so it could not have fired for months.
|
||||
"""
|
||||
from warden.routing.catalog import DEFAULT_BLOCKER_STALE_DAYS, DEFAULT_STALE_DAYS
|
||||
|
||||
assert DEFAULT_STALE_DAYS == 90
|
||||
assert DEFAULT_BLOCKER_STALE_DAYS == 14
|
||||
assert DEFAULT_BLOCKER_STALE_DAYS < DEFAULT_STALE_DAYS
|
||||
|
||||
|
||||
def test_blocker_window_scales_with_risk_and_matches_risk_nexus():
|
||||
"""risk-nexus stall windows: 14d critical/high, 30d medium, 60d low.
|
||||
|
||||
They offered the convention instead of a joint tool, so the two registers
|
||||
agree only for as long as these numbers do.
|
||||
"""
|
||||
from warden.routing.catalog import blocker_stale_days
|
||||
|
||||
assert blocker_stale_days("high") == 14
|
||||
assert blocker_stale_days("standard") == 30
|
||||
assert blocker_stale_days("low") == 60
|
||||
# An ungraded lane gets the SHORTEST window, not the longest -- ADR-0007 makes
|
||||
# an absent grade a defect, so its blocker is the least trustworthy of all.
|
||||
assert blocker_stale_days("ungraded") == 14
|
||||
assert blocker_stale_days(None) == 14
|
||||
# An explicit --stale-days still wins.
|
||||
assert blocker_stale_days("low", 7) == 7
|
||||
|
||||
|
||||
def test_asked_and_waiting_is_not_verification():
|
||||
"""The failure this whole change exists to catch.
|
||||
|
||||
A lane asked today reads as reviewed today. The secrets-engine blocker sat
|
||||
in exactly that state for ten days while looking current.
|
||||
"""
|
||||
from warden.routing.models import Delegation
|
||||
|
||||
asked = Delegation(mode="interim", intended_owner="x", blocked_on="y",
|
||||
reviewed="2026-08-21", verified="asked-and-waiting")
|
||||
assert asked.is_verified is False
|
||||
|
||||
for method in ("owner-confirmed", "source-read"):
|
||||
d = Delegation(mode="interim", intended_owner="x", blocked_on="y",
|
||||
reviewed="2026-08-21", verified=method)
|
||||
assert d.is_verified is True, method
|
||||
|
||||
|
||||
def test_stale_gaps_flags_unverified_even_when_the_date_is_today():
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
stale = {e.id for e in catalog.stale_gaps(include_draft=True, today=date(2026, 8, 21))}
|
||||
# Asked of key-cape on 2026-08-21 and unanswered -- zero days old, still stale.
|
||||
assert "key-cape-oidc-login" in stale
|
||||
# Confirmed by the owner the same day -- fresh.
|
||||
assert "issue-core-ingestion-api-key" not in stale
|
||||
|
||||
|
||||
def test_invalid_verification_method_rejected(tmp_path):
|
||||
entry = dict(ROUTED_ENTRY)
|
||||
entry["delegation"] = {
|
||||
"mode": "interim", "intended_owner": "secrets-engine",
|
||||
"blocked_on": "pending", "reviewed": "2026-08-21", "verified": "probably-fine",
|
||||
}
|
||||
with pytest.raises(CatalogError, match="verified"):
|
||||
load_catalog(_write_catalog(tmp_path, [SSH_ENTRY, entry]))
|
||||
|
||||
|
||||
def test_every_interim_lane_records_how_it_was_verified():
|
||||
"""Structural, not time-based, so it never fails on a calendar day alone."""
|
||||
catalog = load_catalog(_repo_catalog())
|
||||
missing = [
|
||||
e.id for e in catalog.gaps(include_draft=True)
|
||||
if e.effective_delegation.verified is None
|
||||
]
|
||||
assert not missing, f"interim lanes with no `verified`: {missing}"
|
||||
|
||||
|
||||
def test_cli_route_gaps_fail_on_stale_exits_3(repo_catalog_env):
|
||||
result = runner.invoke(app, ["route", "gaps", "--fail-on-stale", "--json"])
|
||||
assert result.exit_code == 3
|
||||
rows = json.loads(result.stdout)
|
||||
assert any(r["stale"] for r in rows)
|
||||
# An asked-and-waiting lane stays stale until it is verified, regardless of
|
||||
# how many calendar days have elapsed since the request.
|
||||
assert any(
|
||||
r["stale"]
|
||||
and r["verified"] == "asked-and-waiting"
|
||||
for r in rows
|
||||
)
|
||||
|
|
|
|||
|
|
@ -101,7 +101,12 @@ def test_run_scorecard_clean(tmp_path):
|
|||
)
|
||||
results = run_scorecard(tmp_path, inv)
|
||||
assert all(r.passed for r in results)
|
||||
assert len(results) == 6
|
||||
# cert-side checks + catalog_rotation_coverage (WP-0026 T06)
|
||||
# + organization_posture + catalog_freshness (WP-0029)
|
||||
assert len(results) == 9
|
||||
names = {r.name for r in results}
|
||||
assert "organization_posture" in names
|
||||
assert "catalog_freshness" in names
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
66
tests/test_taint.py
Normal file
66
tests/test_taint.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
"""Tests for EXPOSED taint convention (WARDEN-WP-0026 T05)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from warden.cli import app
|
||||
from warden.taint import (
|
||||
EXPOSED_AT,
|
||||
TaintStatus,
|
||||
parse_custom_metadata,
|
||||
kv_metadata_path,
|
||||
)
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def test_parse_custom_metadata_tainted():
|
||||
status = parse_custom_metadata({
|
||||
"custom_metadata": {
|
||||
EXPOSED_AT: "2026-07-16T00:00:00Z",
|
||||
"exposed_version": "2",
|
||||
"exposed_reason": "test",
|
||||
"exposed_ref": "history/x.md",
|
||||
},
|
||||
"current_version": 2,
|
||||
})
|
||||
assert status.tainted is True
|
||||
assert status.exposed_at == "2026-07-16T00:00:00Z"
|
||||
assert status.exposed_version == "2"
|
||||
assert status.current_version == 2
|
||||
|
||||
|
||||
def test_parse_custom_metadata_clean():
|
||||
status = parse_custom_metadata({"custom_metadata": None, "current_version": 1})
|
||||
assert status.tainted is False
|
||||
assert status.exposed_at is None
|
||||
|
||||
|
||||
def test_parse_empty_exposed_at_not_tainted():
|
||||
status = parse_custom_metadata({"custom_metadata": {EXPOSED_AT: " "}, "current_version": 1})
|
||||
assert status.tainted is False
|
||||
|
||||
|
||||
def test_kv_metadata_path_strips():
|
||||
assert kv_metadata_path(" platform/workloads/x ") == "platform/workloads/x"
|
||||
|
||||
|
||||
def test_taint_status_to_dict():
|
||||
s = TaintStatus(
|
||||
lane_id="x", path="p", tainted=True,
|
||||
exposed_at="t", exposed_version="1", current_version=1,
|
||||
)
|
||||
d = s.to_dict()
|
||||
assert d["tainted"] is True
|
||||
assert d["id"] == "x"
|
||||
|
||||
|
||||
def test_taint_cli_unknown_id():
|
||||
result = runner.invoke(app, ["taint", "no-such-lane-xyz"])
|
||||
assert result.exit_code == 1
|
||||
|
||||
|
||||
def test_taint_cli_template_lane_errors():
|
||||
"""openbao-api-key has <placeholders> — cannot query taint."""
|
||||
result = runner.invoke(app, ["taint", "openbao-api-key", "--json"])
|
||||
assert result.exit_code == 2
|
||||
54
tests/test_workload_join.py
Normal file
54
tests/test_workload_join.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
"""Explicit lane-to-workload join tests (WARDEN-WP-0032 / RMGR-WP-0010-T06)."""
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from scripts.report_workload_join import build
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_repo_catalog_uses_only_explicit_workload_references():
|
||||
report = build(ROOT / "registry/routing/catalog.yaml", Path.home())
|
||||
assert report["ok"] is True
|
||||
assert len(report["resolved"]) == 3
|
||||
assert len(report["unknown"]) == 18
|
||||
# 11 since WARDEN-WP-0033: the two NetKingdom SSO lanes (c374d41) are
|
||||
# provider/control-plane credentials, not workload delivery lanes.
|
||||
assert len(report["not_applicable"]) == 11
|
||||
assert {row["lane"] for row in report["resolved"]} == {
|
||||
"ops-warden-warden-sign-token",
|
||||
"issue-core-ingestion-api-key",
|
||||
"rapp-qonto-keycape-client",
|
||||
}
|
||||
|
||||
|
||||
def test_invalid_exact_deployable_resolves_unknown(tmp_path):
|
||||
rapp = tmp_path / "rapp-x" / "declarations"
|
||||
rapp.mkdir(parents=True)
|
||||
(rapp / "rapp.yaml").write_text(yaml.safe_dump({
|
||||
"rapp_id": "rapp-x",
|
||||
"workload_identity": {"name": "x"},
|
||||
"composition": {"member_repos": [{"deployables": ["api"]}]},
|
||||
}))
|
||||
catalog_dir = tmp_path / "ops-warden" / "registry" / "routing"
|
||||
catalog_dir.mkdir(parents=True)
|
||||
catalog = catalog_dir / "catalog.yaml"
|
||||
catalog.write_text(yaml.safe_dump({"entries": [{
|
||||
"id": "x",
|
||||
"workload_ref": {
|
||||
"applicability": "applicable",
|
||||
"rapp_id": "rapp-x",
|
||||
"name": "x",
|
||||
"deployable": "missing",
|
||||
},
|
||||
}]}))
|
||||
posture = catalog_dir.parent / "policy"
|
||||
posture.mkdir()
|
||||
(posture / "security-posture.yaml").write_text("dataclass_floor: {}\n")
|
||||
|
||||
report = build(catalog, tmp_path)
|
||||
assert not report["resolved"]
|
||||
assert report["unknown"][0]["lane"] == "x"
|
||||
assert "deployable" in report["unknown"][0]["reason"]
|
||||
2
uv.lock
generated
2
uv.lock
generated
|
|
@ -131,7 +131,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "ops-warden"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
|
|
|
|||
|
|
@ -76,6 +76,44 @@ boundary in `OperatorAccessAssist.md`.
|
|||
|
||||
---
|
||||
|
||||
## Interim custodianship
|
||||
|
||||
ops-warden **works with, and never replaces or duplicates**, the NetKingdom
|
||||
components that own identity, custody, authorization, and tenant/user lifecycle
|
||||
(`INTENT.md` §9). Covering a gap is legitimate. Silently becoming the owner of
|
||||
that gap is not.
|
||||
|
||||
The only lane ops-warden executes with its own authority is **SSH issuance**
|
||||
(`ssh-cert-host-access`). Every other execution position — including every
|
||||
`warden access` proxy — is **interim by default**. A catalog entry without a
|
||||
`delegation:` block is treated as interim with an unknown owner: absence is a
|
||||
question, not a settlement.
|
||||
|
||||
| `delegation.mode` | Meaning |
|
||||
| --- | --- |
|
||||
| `permanent` | ops-warden is the designed owner of this front door (SSH only, today) |
|
||||
| `native` | the intended owner already fronts it; ops-warden routes (and may keep a proxy as fallback) |
|
||||
| `interim` | ops-warden covers the gap until `intended_owner` ships the missing front door named in `blocked_on` |
|
||||
|
||||
Delegation targets — the components that should own a front door once they have
|
||||
one:
|
||||
|
||||
| Target | What they should front |
|
||||
| --- | --- |
|
||||
| **secrets-engine** | owner-native secret-exec (`secrets-engine exec --catalog <id>`) |
|
||||
| **tenant-engine** | tenant/client secret custody and tenant-lane front door |
|
||||
| **user-engine** | end-user / account-lifecycle secrets that belong with user identity |
|
||||
| **railiance-platform** | OpenBao cluster, credential broker, platform workload procedure |
|
||||
| **flex-auth** | authorization decisions (already native — ops-warden only points) |
|
||||
| **key-cape** | identity login and client-credential protocol (OIDC, `client_secret_basic`) |
|
||||
|
||||
Query the register with `warden route gaps`. An interim lane is retired by
|
||||
setting `exec_owner` / `exec_command` (the WP-0019 pattern) and flipping
|
||||
`delegation.mode` to `native` once the owner's front door is proven. Do not
|
||||
delete a working proxy on the way.
|
||||
|
||||
---
|
||||
|
||||
## Routing lookup CLI (`warden route`)
|
||||
|
||||
Agents and operators query the pointer catalog directly instead of re-deriving
|
||||
|
|
@ -86,6 +124,7 @@ material.
|
|||
```bash
|
||||
warden route list [--json] [--all] [--tag <keyword>] # active-only unless --all
|
||||
warden route list --stale [--stale-days 90] [--all] [--json] # past review cadence
|
||||
warden route gaps [--json] [--all] # interim register (owner + blocker)
|
||||
warden route show <id> [--json] # owner + pointers; SSH adds steps
|
||||
warden route find "<free text need>" [--json] [--all] # rank by keyword overlap
|
||||
```
|
||||
|
|
@ -145,6 +184,70 @@ owner repo's shipped path.
|
|||
| **On canon change** | When net-kingdom security docs change, review affected `canon_ref` entries immediately |
|
||||
| **On owner ship** | When an owning repo merges a new OpenBao path or playbook, promote `draft` → `active` and bump `reviewed` |
|
||||
| **On agent confusion** | If `warden route find` misses a common query, add `need_keywords` or a playbook — do not restate owner procedure in the catalog |
|
||||
| **Fortnightly** (default 14 days) | Run `warden route gaps` — re-check each `blocked_on` against the intended owner; flip to `native` when their front door exists |
|
||||
|
||||
### Two cadences, because they are two different claims
|
||||
|
||||
A catalog pointer and an interim blocker both carry a `reviewed:` date, and for a
|
||||
while they shared one 90-day threshold. They should not.
|
||||
|
||||
| Claim | Question | Default | Where |
|
||||
| --- | --- | --- | --- |
|
||||
| Pointer freshness | Is this still the right owner and page? | **90 days** | `warden route list --stale` |
|
||||
| Interim blocker | Has the intended owner answered / can they front this yet? | **risk-scaled, 14–60 days** | `warden route gaps` |
|
||||
|
||||
The blocker window scales with what the lane holds, matching `risk-nexus`'s stall
|
||||
windows (`docs/method/check-procedure.md`) so the two registers agree without a
|
||||
shared tool:
|
||||
|
||||
| Lane `risk` | Window | risk-nexus equivalent |
|
||||
| --- | --- | --- |
|
||||
| `high`, `ungraded` | **14 days** | critical / high |
|
||||
| `standard` | **30 days** | medium |
|
||||
| `low`, `accepted` | **60 days** | low |
|
||||
|
||||
`ungraded` takes the *shortest* window, not the longest. `ADR-0007` makes an
|
||||
absent grade a defect and `ADR-0008` makes a grade cover the whole path — a lane
|
||||
nobody has graded is the one whose blocker deserves least trust.
|
||||
|
||||
A pointer genuinely is a quarterly question. A blocker is not: it is a claim
|
||||
about another repo's state at a date, and this estate invalidates those in days.
|
||||
`RISK-F-0001` invalidated an ops-warden blocker in one. The `secrets-engine`
|
||||
lanes carried one for ten while it was answerable from that repo's source the
|
||||
whole time. A `FLEX-WP-0007` blocker was repeated by two repos for about seven
|
||||
weeks after that workplan read `finished`.
|
||||
|
||||
Sharing 90 days did not make the blocker check loose, it made it **inert**: the
|
||||
delegation register was created 2026-08-15, so the threshold could not have fired
|
||||
before November and never had.
|
||||
|
||||
### Reviewed is not verified
|
||||
|
||||
The more important half. A `reviewed:` date records when someone last *touched*
|
||||
the entry, which looks identical to re-checking it. So every interim lane also
|
||||
carries `verified:`, saying how the claim was established:
|
||||
|
||||
| Value | Meaning | Counts as verification |
|
||||
| --- | --- | --- |
|
||||
| `owner-confirmed` | the intended owner stated the blocker's status | **yes** |
|
||||
| `source-read` | re-derived from the owner's code, canon, or a CCR | **yes** |
|
||||
| `asked-and-waiting` | a question is outstanding | **no** |
|
||||
| `unverified` | carried forward without a check | **no** |
|
||||
|
||||
`asked-and-waiting` deliberately does not reset the clock. A lane asked today
|
||||
reads as reviewed today, and that is precisely how a blocker stays fresh-looking
|
||||
while nobody answers it. `warden route gaps` flags such a lane as stale at zero
|
||||
days old.
|
||||
|
||||
**Re-check the blocker; do not bump the date.** Bumping `reviewed` without
|
||||
re-establishing the claim is the failure this section exists to prevent, and it
|
||||
is cheap to avoid — most of these are answerable by reading the owner's repo.
|
||||
|
||||
```bash
|
||||
warden route gaps # risk-scaled cadence, plus unverified lanes
|
||||
warden route gaps --stale-days 30 # flat override for every lane
|
||||
warden route gaps --fail-on-stale # exit 3 — for a cron job or a gate
|
||||
```
|
||||
|
||||
### Stale check (operators and agents)
|
||||
|
||||
|
|
@ -177,6 +280,7 @@ moved but anchors still resolve.
|
|||
|
||||
- `CredentialRouting.md` — worker decision tree and routing table
|
||||
- `NetKingdomSecurityMap.md` — component literacy
|
||||
- `INTENT.md` — steward mission ("issue SSH, route the rest")
|
||||
- `INTENT.md` — steward mission ("issue SSH, route the rest"); §9 interim custodianship
|
||||
- `workplans/WARDEN-WP-0030-delegation-register.md` — delegation register
|
||||
- `workplans/WARDEN-WP-0010-access-routing-charter.md` — charter + no-double-source rule
|
||||
- `net-kingdom/docs/platform-identity-security-architecture.md` — platform canon
|
||||
|
|
|
|||
|
|
@ -32,6 +32,20 @@ fresh file starts.
|
|||
high-entropy runs). Signing and proxy paths swallow audit failures so gatekeeping
|
||||
never blocks the primary action — but tests prove values cannot be written.
|
||||
|
||||
> **Absence of a record is not evidence of absence.** Because emission never
|
||||
> blocks the primary action (`src/warden/ca.py`), a failed append loses the event
|
||||
> while the action still happens. This trail proves that the records it holds were
|
||||
> not altered or truncated; it does **not** prove that every action produced one.
|
||||
> Do not reason from a missing entry. This is the estate-wide bound in
|
||||
> `security-layer-model_v0.4` §9.6 — completeness is the source's obligation, and
|
||||
> **Ruled 2026-08-29** (`security-layer-model_v0.6` §9.6): this trail is
|
||||
> **attributive**, not load-bearing — no control branches on the presence of a
|
||||
> signing record — so the non-atomic trade is legitimate, and the obligations are
|
||||
> to declare it (this note) and never to claim completeness. Atomicity is required
|
||||
> only where a control's soundness depends on an event being present or absent.
|
||||
> Registered in the standard's §13 open-gap table as self-declared. If a future
|
||||
> control ever gates on this trail, the trade must be revisited before it ships.
|
||||
|
||||
---
|
||||
|
||||
## Query
|
||||
|
|
|
|||
|
|
@ -75,8 +75,8 @@ ssh-keygen -s /path/to/ca -I agt-test -n agt-task -V +24h /tmp/key.pub && cat /t
|
|||
|
||||
```yaml
|
||||
tunnels:
|
||||
state-hub-coulombcore:
|
||||
host: coulombcore
|
||||
state-hub-railiance01:
|
||||
host: railiance01
|
||||
remote_port: 8001
|
||||
local_port: 8000
|
||||
ssh_user: agt-state-hub-bridge
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue