From 35aff380a33f51a512c1e1b42d52d1dc0d95930f Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 18 Aug 2026 13:35:13 +0200 Subject: [PATCH] Lift ops-warden's binding rules into owned ADRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .claude/rules/architecture.md | 51 +++++++++- .claude/rules/finding-routing.md | 30 ++++-- SCOPE.md | 22 +++++ .../ADR-0001-catalog-is-a-pointer-layer.md | 90 ++++++++++++++++++ docs/adr/ADR-0002-conduit-not-broker.md | 90 ++++++++++++++++++ ...0003-cover-gaps-never-silently-own-them.md | 95 +++++++++++++++++++ ...-agent-read-boundary-on-high-risk-lanes.md | 85 +++++++++++++++++ ...R-0005-implement-narrowly-route-broadly.md | 79 +++++++++++++++ docs/adr/README.md | 69 ++++++++++++++ registry/routing/catalog.yaml | 3 +- 10 files changed, 601 insertions(+), 13 deletions(-) create mode 100644 docs/adr/ADR-0001-catalog-is-a-pointer-layer.md create mode 100644 docs/adr/ADR-0002-conduit-not-broker.md create mode 100644 docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md create mode 100644 docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md create mode 100644 docs/adr/ADR-0005-implement-narrowly-route-broadly.md create mode 100644 docs/adr/README.md diff --git a/.claude/rules/architecture.md b/.claude/rules/architecture.md index 7c2a645..3256afb 100644 --- a/.claude/rules/architecture.md +++ b/.claude/rules/architecture.md @@ -1,7 +1,54 @@ ## Architecture - +### 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 diff --git a/.claude/rules/finding-routing.md b/.claude/rules/finding-routing.md index 06a6ae0..4171644 100644 --- a/.claude/rules/finding-routing.md +++ b/.claude/rules/finding-routing.md @@ -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. diff --git a/SCOPE.md b/SCOPE.md index 6cc4fe8..0311fe4 100644 --- a/SCOPE.md +++ b/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 | diff --git a/docs/adr/ADR-0001-catalog-is-a-pointer-layer.md b/docs/adr/ADR-0001-catalog-is-a-pointer-layer.md new file mode 100644 index 0000000..9d59349 --- /dev/null +++ b/docs/adr/ADR-0001-catalog-is-a-pointer-layer.md @@ -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 diff --git a/docs/adr/ADR-0002-conduit-not-broker.md b/docs/adr/ADR-0002-conduit-not-broker.md new file mode 100644 index 0000000..042c619 --- /dev/null +++ b/docs/adr/ADR-0002-conduit-not-broker.md @@ -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 diff --git a/docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md b/docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md new file mode 100644 index 0000000..5fc474c --- /dev/null +++ b/docs/adr/ADR-0003-cover-gaps-never-silently-own-them.md @@ -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 diff --git a/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md b/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md new file mode 100644 index 0000000..08e5516 --- /dev/null +++ b/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md @@ -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 ` 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 diff --git a/docs/adr/ADR-0005-implement-narrowly-route-broadly.md b/docs/adr/ADR-0005-implement-narrowly-route-broadly.md new file mode 100644 index 0000000..b637aa8 --- /dev/null +++ b/docs/adr/ADR-0005-implement-narrowly-route-broadly.md @@ -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 diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 0000000..73c2053 --- /dev/null +++ b/docs/adr/README.md @@ -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 | diff --git a/registry/routing/catalog.yaml b/registry/routing/catalog.yaml index 58f6f10..b8bf284 100644 --- a/registry/routing/catalog.yaml +++ b/registry/routing/catalog.yaml @@ -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