Lift ops-warden's binding rules into owned ADRs
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

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:
tegwick 2026-08-18 13:35:13 +02:00
parent faa4f2c23e
commit 35aff380a3
10 changed files with 601 additions and 13 deletions

View file

@ -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

View file

@ -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.

View file

@ -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 |

View 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

View 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

View 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

View 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

View 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
View 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 |

View file

@ -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