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
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue