Architecture decision records
An ADR records a decision that was hard to make and would be expensive to reverse, along with the context that made it the right call and the consequences accepted in taking it.
The point is not to document what the code does — the rest of docs/ does that, and it stays current as the code changes. The point is to answer “why is it like this?” a year later, so a future change is made knowingly rather than by accident.
| # | Decision | Status |
|---|---|---|
| 0001 | Record architecture decisions in ADRs | Accepted |
| 0002 | A workspace with a dependency-free core crate | Accepted |
| 0003 | Policy is an ordered, short-circuiting chain carrying evidence | Accepted |
| 0004 | Default-deny lives in config, not in code | Accepted |
| 0005 | rustls, and a deliberately split HTTP stack | Accepted |
| 0006 | CEL for the rules layer, not a general scripting language | Accepted |
| 0007 | Bodies stream by default; buffering must be declared | Accepted |
| 0008 | TLS interception is mandatory, not a fallback | Accepted |
| 0009 | Identity is derived from the connection, never asserted | Accepted |
| 0010 | Resolve once, check every address, connect to the checked one | Accepted |
| 0011 | Secrets are injected at the boundary, not held by the agent | Accepted; placeholder model superseded by 0027 |
| 0012 | The judge sees a reduced request as data, never as instruction | Partially superseded by 0042 |
| 0013 | MCP denials are protocol errors, and tools/list is filtered | Accepted |
| 0014 | Network-namespace isolation without CAP_NET_ADMIN | Accepted |
| 0015 | The cgroup naming convention is the identity registration | Accepted (amended by 0037) |
| 0016 | Config splits by fixed directory convention, not include globs | Accepted |
| 0017 | The fallback profile is embedded, unnamed, and unreferenceable | Accepted |
| 0018 | Profiles do not inherit | Accepted |
| 0019 | Log detail, sink and format are three independent axes | Accepted |
| 0020 | Reload builds everything before swapping; a failure changes nothing | Accepted |
| 0021 | “Identity”, not “session” | Accepted |
| 0022 | Remove transparent (nftables REDIRECT) capture | Accepted |
| 0023 | Multi-port explicit listeners, to restore listener_port identity | Accepted |
| 0024 | netns isolation binds an explicit allowlist, not the whole host root | Accepted |
| 0025 | Secret injection understands Basic auth, with no new config | Superseded by 0027 |
| 0026 | Blind credential injection, for a client that presents nothing | Accepted |
| 0027 | Secret injection is unconditional injection only | Accepted |
| 0028 | SigV4 injection buffers the body, capped, as a declared exception | Accepted |
| 0029 | The redaction set is learned at runtime, not sealed at startup | Accepted |
| 0030 | OAuth2 is a secret source, not an injection kind | Accepted |
| 0031 | A responder may answer a request instead of forwarding it | Accepted |
| 0032 | Marshal owns the PKCE verifier and terminates the authorization flow | Accepted |
| 0033 | The env file is an overlay, not the environment | Accepted |
| 0034 | Bootstrap capture reads the token exchange, and trusts the session | Accepted |
| 0035 | Release versioning uses cocogitto, not release-plz | Accepted |
| 0036 | Bind groups are named and shared, the same way bundles are | Accepted |
| 0037 | Identity and profile are independent axes of the scope name | Accepted |
| 0038 | A second source can read another swap’s ID token | Accepted |
| 0039 | OAuth2 can exchange a token before caching it | Accepted |
| 0040 | Header filtering is allow or deny, never both — and never touches wire framing | Accepted |
| 0041 | The LLM router rewrites destination after allow; intercept may connect to a mapped origin | Accepted |
| 0042 | Native decisions may replace judge tool calls; native decision routing stays within its family | Accepted |
Writing a new one
Section titled “Writing a new one”Copy template.md to NNNN-a-short-imperative-title.md, taking the next free
number. Add a row to the index above.
Write one when: a choice constrains future work, closes off an obvious alternative, trades one desirable property for another, or is likely to look wrong to someone who wasn’t there.
Don’t write one for: anything the code or the rest of the docs already states plainly, a choice with an obvious default and no trade-off, or an implementation detail that could change next week without anyone noticing.
Keeping them current
Section titled “Keeping them current”ADRs are immutable once accepted. Do not rewrite the reasoning of a decision to match what you now believe — that erases exactly the history the record exists to preserve.
When a decision changes:
- Write a new ADR describing the new decision, with a
Supersedes: ADR-NNNNline. - Edit the old one’s Status to
Superseded by ADR-MMMM, linking to it. That status line is the only part of an accepted ADR that may change. - Update the index table.
Correcting a typo or a broken link is fine. Changing the Context, Decision or Consequences of an accepted ADR is not.
If a decision is under discussion but not settled, write it with Status: Proposed and say
what would decide it. If it is abandoned before implementation, mark it Rejected and keep
it — knowing an option was considered and refused is worth as much as knowing one was taken.
See AGENTS.md for when a change needs an ADR alongside its code.