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>
85 lines
3.6 KiB
Markdown
85 lines
3.6 KiB
Markdown
---
|
|
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
|