Lift ops-warden's binding rules into owned ADRs
Five rules that governed this repo lived in wiki prose, a workplan, and a comment at the top of catalog.yaml. All were followed; none was addressable. A reader outside ops-warden could not cite one, could not tell whether it was current, and — the point of this change — could not tell whether it was ours to change or someone else's that we merely obey. ADR-0001 The routing catalog is a pointer layer, never a second copy ADR-0002 ops-warden is a transparent conduit, never a secret broker ADR-0003 Cover gaps, but never silently own them ADR-0004 High-risk lanes refuse raw value streaming to agent sessions ADR-0005 Implement one lane narrowly, route everything else Each carries owner: ops-warden, which is the load-bearing field. It says we follow the rule AND we are responsible for changing it — by superseding ADR, never an in-place edit. The failure this prevents runs both ways: a rule we own mistaken for inherited canon never gets fixed, because we wait for an owner who does not exist; inherited canon mistaken for ours gets quietly bent, and the drift is invisible until it breaks a repo that trusted the canonical version. Rules we follow but do not own — NetKingdom canon, the IAM profile, the credential-management standard, the-custodian's ADR-001 workplan convention — are cited, never copied into docs/adr/. Copying them would recreate exactly the second-source-of-truth failure ADR-0001 exists to prevent. architecture.md also now flags the three-digit/four-digit ADR-001 vs ADR-0001 collision, which is itself an ours-versus-inherited confusion waiting to happen. Publication verified rather than assumed: all five render through policy-nexus tools/render.py, and owner reaches the reader in three places — the page eyebrow (render.py:346), the index Owner column (build_site.py:123,137), and the publication manifest. build_site.py:179 makes title/status/owner required, so ownership cannot be dropped on the way out. policy-nexus publishes and never writes back; docs/adr/ stays the source of truth. Documentation adapted: SCOPE.md gains a Governing rules section and an orientation entry; .claude/rules/architecture.md replaced its stub with the ADR index, the owned-versus-inherited rule, and ADR-over-wiki precedence; finding-routing.md's ADR-gap section closed; catalog.yaml's no-double-source header now cites ADR-0001 rather than the originating workplan. uv run pytest -q → 338 passed, 4 deselected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
faa4f2c23e
commit
35aff380a3
10 changed files with 601 additions and 13 deletions
|
|
@ -1,7 +1,54 @@
|
|||
## 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 |
|
||||
|
||||
### 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
|
||||
|
||||
|
|
|
|||
|
|
@ -80,15 +80,25 @@ the surface that actually executes the act. The transferable design properties:
|
|||
Offer this rather than let a second, incompatible escalation vocabulary grow.
|
||||
Do not implement it for them — routing work is theirs to own.
|
||||
|
||||
## ADR gap (open, not yet resolved)
|
||||
## Policy publication (closed 2026-08-18)
|
||||
|
||||
`policy-nexus` publishes canon and ADRs — roughly 68 ADRs across 18 repos.
|
||||
**ops-warden has none**, and it carries binding rules that govern other repos'
|
||||
behaviour: the no-double-source catalog rule (CI-enforced), conduit-not-broker,
|
||||
interim-by-default with a named owner, the agent read-boundary on `risk: high`
|
||||
lanes. These live in wiki prose and workplan files, so they are unaddressable
|
||||
and unpublishable — a reader outside ops-warden cannot cite them or tell whether
|
||||
they are current.
|
||||
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.
|
||||
|
||||
Do not create an ADR corpus unilaterally; it is a structural decision for the
|
||||
operator. Raise it when ops-warden next records a rule of that kind.
|
||||
**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.
|
||||
|
|
|
|||
22
SCOPE.md
22
SCOPE.md
|
|
@ -136,6 +136,27 @@ Gap analysis: `history/2026-07-01-intent-scope-gap-analysis.md` (current);
|
|||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
|
||||
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`
|
||||
|
|
@ -445,6 +466,7 @@ 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 |
|
||||
|
|
|
|||
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
|
||||
69
docs/adr/README.md
Normal file
69
docs/adr/README.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# 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 |
|
||||
|
|
@ -4,7 +4,8 @@
|
|||
# worker WHICH subsystem owns a need and WHERE the authoritative doc is. It is NOT
|
||||
# a second copy of any subsystem's procedure.
|
||||
#
|
||||
# No-double-source rule (binding — see workplans/WARDEN-WP-0010-access-routing-charter.md):
|
||||
# No-double-source rule (binding — docs/adr/ADR-0001-catalog-is-a-pointer-layer.md,
|
||||
# owner: ops-warden; origin workplans/WARDEN-WP-0010-access-routing-charter.md):
|
||||
# - For any subsystem ops-warden does not own, an entry carries identifiers +
|
||||
# pointers ONLY: owner_repo, subsystem, wiki_ref, canon_ref, need_keywords.
|
||||
# - Authored procedure (a `steps:` block and `cert_command:`) is allowed ONLY on
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue