--- title: "ADR 0022 — Agent writes are an org's choice, and a new org starts with them off" type: decision tags: [signpost, adr, decision] proposed_by: tok_bc09b340db294c709763789154d8113d model: kmem-docs-publisher extracted_at: "2026-10-08T22:41:45.416Z" --- # ADR 0022 — Agent writes are an org's choice, and a new org starts with them off **Status** Accepted · **Date** 2026-09-16 · **Supersedes** nothing · **Amends** ADR 0016's default, ADR 0017's `conflicts` rule for a connection in write mode · **Relates to** ADR 0009 (the permission ladder), ADR 0018 (verification is an org's choice), ADR 0019 (hosted OAuth), `docs/TAXONOMY.md` §7, `packages/core/src/permissions/agent-writes.ts`, `packages/core/src/permissions/capability.ts`, `packages/core/src/permissions/clamp.ts`, `packages/db/migrations/0024_agent_writes.sql` ## Context Before this change, an org had no switch for agent writes: - A connection had two modes in the web app: "Read" and "Read and propose". `POST /api/tokens` also accepted `write`, and a few stored rows have it. - The production write policy is `conflicts` (ADR 0017). Under it, an agent that creates a doc at a free address lands the doc in Docs, and every other agent change waits in Review. - `org_routing.capability_assistants` held `suggest` for every org older than 2026-08-27. Migration `0011` wrote that value, and nobody chose it. A folder can widen past it. The owner asked for three things on 2026-09-16: 1. An org decides whether agents can change docs without a person in Review. 2. The setting is off for a new org and for every existing org. 3. A connection can use a third mode, "Read, propose and write", when the setting is on. Under ADR 0017 as written, "Read, propose and write" would differ from "Read and propose" only for creates. That is not what the owner asked for. ## Decision **Each org has a setting, `org_routing.agent_writes`, with two values, `on` and `off`.** The web app calls it "Agent writes". The three connection modes are "Read", "Read and propose" and "Read, propose and write". - With `off`, every agent change waits in Review. The ceiling for every agent in the org is `suggest`. - With `on`, a connection in write mode adds new docs and edits the current version directly. The ceiling is `write`. A new org gets `off`, unless its creator chooses `on` in the create-org form. The form shows both org switches, "Agent writes" and "Verification", each with a sentence and a small diagram. ### Storage and fold - The column is nullable and has no default. `agentWritesFromStored` reads `on` only for the exact literal `on`. Every other value, NULL included, reads as `off`, so a table rebuild that drops the column fails closed. - `loadStoredCapabilityChain` in `services/permissions.ts` selects the column in its one `org_routing` query. It is the only reader. `agentWritesOf(ctx)` and `effectiveScopeFor(ctx, record)` are the helpers every door calls. - `setOrgAgentWrites` in `tenancy/store.ts` is the only writer. It is an UPDATE of one column, and the write-path allowlist names it. - `effectiveAgentCapability` gains a required `orgCeiling` input, which it folds inside `tightestCapability` with the instance ceiling, the link and the person. It stays the only composition, and the `never` floor stays in one place. - The hard clamp in `capabilityDecisionFor` passes `orgCeiling: "write"`. The switch is a door concern, and Review must not refuse its own accepts. ### Request-time clamp The system never rewrites a stored scope. It reads the switch on each request, with a per-request memo only. The value is not on `ResolvedTenant`, because the 30-second memo there would delay an owner's "off". A stored `write` connection acts as "Read and propose" while the switch is off. It acts as "Read, propose and write" again after an owner turns the switch on. The confirmation dialog for "on" says how many active connections that affects. `effectiveLinkScope` gives the scope that a connection has now. The token list, the briefing, the reindex door and the OAuth consent page read it. It is never an input to the fold. ### Write-mode conflict rule (amends ADR 0017) `conflicts` now looks at the shape of a change, not only at whether its target exists. `changeShapeOf` in `services/commit.ts` is the one implementation. It checks in this order: | Shape | When | Write mode, switch on | | --- | --- | --- | | `delete` | a delete, a retract included | waits in Review | | `taken-address` | a create at a live address | waits in Review | | `replaces-doc` | a create or an update whose Markdown supersedes a live doc | waits in Review | | `new-doc` | a create at a free address | applies | | `stale-edit` | an update or a splice on a doc that is not live | waits in Review | | `stale-edit` | an update with no expected sha, which is a blind overwrite | waits in Review | | `current-edit` | a splice with no expected sha, because its block ids come from the content | applies | | `current-edit` or `stale-edit` | an expected sha equal to the live sha, or not | applies, or waits | A connection in write mode also applies status changes, such as `superseded` and `archived`, when the change has a current base. Undo reverses them. ADR 0017 stays true for every connection that is not in write mode. "Read and propose" queues everything, creates included. An edit above open visibility always queues. The MCP `edit` tool and Quick Connect gain the op `replace_note`, which needs `expected_sha`. The MCP door and Quick Connect no longer refuse a stale `expected_sha`. They send the change with the caller's sha, and the clamp queues it with the stale sentence. The person door, `/api/blocks/edit`, keeps its refusal and does not accept `replace_note`. Every agent write result carries `agent_writes` beside `write_policy` and `because`. A queue that the switch causes names who can change the switch and where. ### Self-hosted reads on A project with no `org_routing` row reads as follows: - A hosted org, that is a project with an `organization` row, reads as `off`. - A self-hosted project reads as `on`. There is no org to switch, and the operator's `SIGNPOST_WRITE_POLICY` is the chosen control. Every self-hosted deployment keeps its behaviour. ### The `capability_assistants` reset Migration `0024` sets `agent_writes` to `off` for every row. It also resets `org_routing.capability_assistants` from `suggest` to NULL. Without the reset, "on" would do nothing for an org older than 2026-08-27, because the old backfill value would still cap it. The reset leaves `none` and `read`, because a person set those values on purpose. The migration widens nothing (`docs/TAXONOMY.md` §7). `off` caps every agent at `suggest`, which is at or below every value an agent had before. No `session_tokens` row changes. ### Mint gates One rule, `refuseWriteMode`, gives the sentence for all mint gates: - `mintToken` refuses scope `write` while the switch is off. `POST /api/tokens` answers 403 with the code `agent-writes-off` and logs `session_link_mint_refused` at `warn`. - The OAuth consent page offers write as a third choice. The page never pre-selects it: the preferred choice is at most "Read and propose". While the switch is off, the write choice is disabled, and the page gives the reason. An owner or an admin also gets a link to Settings. - The consent POST refuses a chosen `write` while the switch is off. It shows the page again with a 400, writes no code row, and logs `oauth_consent_refused` at `warn`. - The OAuth code exchange does not check the switch again. The request-time clamp covers a token that the exchange issues after a flip, and a test proves it. The reindex door needs scope `write`. It reads the effective scope, and its refusal names the switch when the stored scope is `write`. ### Who changes the switch `GET /api/orgs/{orgId}/agent-writes` answers any member. `PATCH` is for an owner or an admin (`mayAdministerOrganization` in `packages/core/src/membership.ts`). A refusal logs `org_agent_writes_refused` at `warn`. A change logs `org_agent_writes_changed` at `warn`, with the value before, the value after and the count of active write connections. The PATCH drops the per-request memo, so the next request reads the new value. ## Controls Write mode can change a person's doc without Review. That is the risk that ADR 0017 exists to prevent. These controls answer it: 1. Two opt-ins: an owner or an admin turns on the switch, and a person makes a connection in write mode. 2. Each edit is a new version in the doc history. 3. "Undo changes" is on the connection's menu in Connect. The sessions screen is behind a flag that is off, so the menu is the place a person finds. 4. Stale edits, blind overwrites, replacing creates and all deletions still wait in Review. 5. `refuseAcceptedClaimChange` still refuses an edit to accepted text. 6. The 25-write session budget still sends further writes to Review. 7. Revocation takes effect on the next request. 8. "Off" takes effect on the next request, and no cache holds the old value. `test/invariants/token-lifecycle.test.ts` proves it. ## The alternatives that were rejected **Use `org_routing.capability_assistants` as the switch.** A folder can widen past that value, so it is not a ceiling. It also held a backfill value that nobody chose. **Rewrite stored `write` connections to `suggest` when the switch goes off.** That loses a person's choice, and the request-time clamp is as safe. A connection gets its mode back when the switch goes on again. **Keep ADR 0017 unchanged for write mode.** Write mode would then mean "creates only", which is what "Read and propose" already did under `conflicts`. The owner asked for direct edits. **A new `SIGNPOST_WRITE_POLICY` value.** That variable is per deployment. The owner asked for a choice per org. ## Consequences - The briefing, `SKILLS.md` and `tools.json` state the effective mode. The Quick Connect cache key includes the effective scope and the mode, and `BRIEFING_CACHE_VERSION` is 6. - `GET /api/tokens` returns `agent_writes: {mode, can_change}` and, for each connection, `effective_scope`. `scope` stays the stored value. - `GET /api/org/{orgId}/defaults` folds the switch into `effective` and explains it in `why`. - The WebMCP manifest returns `agent_writes`. - Settings > Org shows "Agent writes" before "Verification", each with a diagram. - ADR 0016 and ADR 0017 need a status line that points here. The documentation pass adds them, and it updates `docs/MCP.md`, `docs/AGENT-INTEGRATION.md`, `docs/ARCHITECTURE.md`, `docs/SECURITY.md`, `docs/TAXONOMY.md` §7 and `docs/DEPLOYMENT.md`. ## What would make me revisit this - A team asks for write mode on some folders only. The folder grid can then carry the ceiling, and the switch becomes its default. - Undo by session is not enough to recover from a bad write connection. Write mode then needs a per-change review after the fact. - Many agents stop at the 25-write budget in write mode. The budget then needs its own setting.