ADR-008 — Execution Packet

Step: 3 of 5 (packet) Date: 2026-05-06 Branch: feature/adr-008-mode-enforcement Worktree: .worktrees/claude/adr-008-mode-enforcement Audit: docs/audits/adr-008-mode-enforcement-audit.md Contract: docs/contracts/adr-008-mode-enforcement-contract.md


1. Files this packet writes

Order File Action Approx LOC
1 docs/architecture/decisions/ADR-008-mode-enforcement.md new ~280 lines markdown
2 docs/architecture/decisions/index.md edit (one bullet add) +1
3 docs/architecture/decisions/README.md edit (one row add) +1
4 docs/verification/adr-008-mode-enforcement-verification.md new ~110 lines markdown

The audit and contract files were written in Steps 1 and 2 and are already on disk.


2. ADR section outline

The ADR file ADR-008-mode-enforcement.md is structured as follows. Each section’s purpose is locked here so the writer cannot drift; the §Decision is also locked to Option A by audit Step 1.

2.1. Frontmatter

---
title: "ADR-008: Mode Enforcement  Capability-Gated Tool Dispatch"
description: ...
tags: [adr, decision, modes, gamma, capabilities, server-lifecycle]
type: adr
round: R84
status: proposed
parent: Architecture Decision Records
updated: 2026-05-06
nav_order: 80
---

2.2. Header block

# ADR-008: Mode Enforcement — Capability-Gated Tool Dispatch

**Status:** Proposed
**Date:** 2026-05-06
**Round:** R84
**Supersedes:** None
**Superseded by:** None

2.3. §Context (~400 words)

State Finding #1. Cite src/server.ts:229, src/modes.ts:174, docs/2-plugin/modes.md:31, docs/reference/mcp-tools-phase-0.md:930, ADR-004:68. Quote the docs that imply enforcement and the code that does not. Refer the reader to docs/audits/adr-008-mode-enforcement-audit.md for the full inventory.

2.4. §Decision (~300 words)

Option A — enforcement via tool config flag. State the choice in one sentence. Then give the mechanism in five bullets:

  • New optional mutates: boolean field on ColibriToolConfig.
  • Default false (a tool that doesn’t declare itself non-mutating is treated as a no-op for capability gating, OR — alternative the ADR will pin — default true to fail safe). Decision: default false for ergonomic non-breakage, but warn at registration if a tool declared mutates: true in READONLY mode.
  • Stage 4 (dispatch) of the middleware reads capabilitiesFor(ctx.mode) once at boot per tool; if mutates && !canWriteDatabase, the wrapper short-circuits with a typed error envelope before the handler is called.
  • New error code ERR_READONLY_MODE joins the standard envelope.
  • Per-tool changes: 14 tool registrations gain a one-keyword declaration each.

2.5. §Alternatives Considered (~600 words)

Three subsections (A, B, C) with the per-axis tradeoff matrix:

Axis A — config flag B — decorator/wrapper C — amend docs
Implementation cost ~50 LOC + per-tool 1-line ~30 LOC + per-tool 1-line ~50 LOC docs only
Runtime cost one branch per dispatch one wrapper per dispatch zero
Test surface mode × tool matrix tool authors must add tests doc-link audit
Schema impact one optional field on ColibriToolConfig none none
Tool author ergonomics declaration is at the registration site opt-in is easy to forget no obligation, but no safety either
Failure mode if author forgets tool defaults to false (read-only enforced safe) tool silently bypasses gating n/a

Subsection A: recommended. Reasons it wins. Subsection B: rejected because the failure mode is silent. A forgotten opt-in is undetectable; a typo in a decorator name does not show up in any test. Subsection C: rejected because it discards the safety story. READONLY becomes a label, not a guarantee. Auditors using COLIBRI_MODE=READONLY for compliance evidence lose the contract.

2.6. §Consequences (~250 words)

Positive. Closes Finding #1. READONLY is honest. New tool authors must explicitly declare mutation intent at the registration site (one keyword). server_health payload becomes useful for clients to skip mutating calls.

Negative. Adds one branch to every dispatch (negligible: a single boolean read against a frozen capability record). Adds a new error code to the canonical envelope. Adds a small test matrix.

Neutral. Does not affect Phase 1 κ Rule Engine — when κ admission ships, mode-gating becomes one input among many. ADR-008 is the floor, not the ceiling.

2.7. §Implementation (~400 words)

Concrete file deltas for the follow-up task:

  • src/server.tsColibriToolConfig gets readonly mutates?: boolean. registerColibriTool reads ctx.mode, computes caps = capabilitiesFor(ctx.mode) once, and binds enforcedMutates = (toolConfig.mutates ?? false) && !caps.canWriteDatabase. The wrapped handler checks enforcedMutates before stage 4 dispatch and returns the ERR_READONLY_MODE envelope if true. The existing void capabilitiesFor(mode) call in createServer is removed (no longer needed — the read moves to per-registration).
  • src/server.ts (continued) — the server_ping registration in bootstrap and the mutates declarations for the 5 mutating tools.
  • src/tools/health.tsmutates: false on the server_health registration.
  • src/tools/merkle.tsmutates: true on audit_session_start, merkle_finalize; mutates: false on merkle_root.
  • src/domains/tasks/repository.tsmutates: true on task_create, task_update; mutates: false on task_get, task_list, task_next_actions.
  • src/domains/skills/repository.tsmutates: false on skill_list.
  • src/domains/trail/repository.tsmutates: true on thought_record; mutates: false on thought_record_list.
  • src/domains/trail/verifier.tsmutates: false on audit_verify_chain.
  • src/__tests__/middleware/mode-enforcement.test.ts — new test suite. One test per (mode, tool) admission pair (4 modes × 14 tools = 56 cases minus the read-only fast-path = 5 mutating tools rejected in READONLY and MINIMAL = 10 explicit refuse-cases, plus 9 read-only allow-cases × 4 modes = 36 explicit allow-cases). The exact count is the implementer’s call; the floor is “every mutating tool refused in READONLY and MINIMAL, every read-only tool admitted in every mode”.
  • docs/2-plugin/modes.md — line 31 updated to use the term ERR_READONLY_MODE and Stage 4 (dispatch) instead of Stage 1 (tool-lock). The ADR-008 §Implementation explicitly says “ADR-008 supersedes the modes.md line 31 prose about Stage 1; enforcement happens at Stage 4”.
  • docs/reference/mcp-tools-phase-0.mdERR_READONLY_MODE added to the canonical error code list (line 988 area).

The follow-up task is one cohesive PR. Estimate: ~120 LOC code + ~250 LOC tests + ~30 LOC docs.

2.8. §Verification (~150 words)

Sigma confirms ADR-008 is implemented if and only if:

  • src/server.ts has the mutates field on ColibriToolConfig and Stage 4 reads capabilitiesFor.
  • All 5 mutating tools declare mutates: true.
  • A test asserts task_update returns ERR_READONLY_MODE when COLIBRI_MODE=READONLY.
  • npm run build && npm run lint && npm test is green.
  • docs/2-plugin/modes.md:31 and the error-code list in mcp-tools-phase-0.md are updated to match the runtime.

2.9. §References

  • Audit, contract, packet, verification of ADR-008
  • src/modes.ts, src/server.ts
  • docs/2-plugin/modes.md, docs/reference/mcp-tools-phase-0.md, docs/architecture/decisions/ADR-004-tool-surface.md
  • ADR-007 (independent sibling, η session lifecycle)

3. Index update mechanics

3.1. docs/architecture/decisions/index.md

Add one bullet between ADR-006 and the existing ## See also heading:

- [ADR-008 — Mode Enforcement: Capability-Gated Tool Dispatch](ADR-008-mode-enforcement.md)

ADR-007 is being drafted by a sibling worktree; do not add an ADR-007 bullet here. The merge will resolve naturally because both ADRs are line-additions in different positions.

3.2. docs/architecture/decisions/README.md

Add one row to the existing status table (currently has rows for ADR-001..003):

| [ADR-008](ADR-008-mode-enforcement.md) | Mode Enforcement — Capability-Gated Tool Dispatch | PROPOSED | γ Server Lifecycle |

Leave ADR-004, ADR-005, ADR-006, and ADR-007 rows alone — they are the responsibility of those ADRs’ worktrees. (Note: README.md’s table is currently incomplete; it stops at ADR-003. ADR-008 only adds itself; a separate hygiene pass will fill the gaps.)


4. Step 5 (verification) outline

The verification document records:

  • Frontmatter validation pass
  • Section-presence checklist (8 sections)
  • Code citation accuracy check (each line cited in the ADR exists at the worktree base)
  • Index update presence check
  • npm run build && npm run lint && npm test exit code 0 and stable test count
  • Commit SHAs for the three implementation commits (one per file group: ADR + index + verification)

The verification doc is written after the ADR and the gate runs successfully.


5. Gate sequence

1. Write ADR-008-mode-enforcement.md
2. Edit index.md (one bullet)
3. Edit README.md (one row)
4. npm run build       (must be green)
5. npm run lint        (must be green)
6. npm test            (must be green; same count as base)
7. Write verification doc
8. Commit each step:
   - audit
   - contract
   - packet
   - feat (ADR + index + README)
   - verify
9. Push branch (no PR; the dispatching agent handles PR creation)

6. Rollback plan

No source code is touched. Rollback is git reset --hard origin/main on the feature branch, which deletes all five docs files and both index edits. Safe.


7. Risks and mitigations

Risk Mitigation
Test gate fails despite no source changes Fall back to npm test --runInBand to rule out parallel-test flake (the known startup — subprocess smoke flake is the most likely culprit per memory). Re-run once before declaring failure.
Word count drifts toward 4,000 in the ADR Stick to the section budgets in §2; cut from §Alternatives first.
ADR-007 worktree commits its index update first and ours conflicts Pull origin/main right before commit; resolve by accepting both line additions. The ADRs are independent.
Frontmatter date format diverges from peer ADRs Use 2026-05-06 (ISO-8601, no quotes). ADR-005 uses 2026-04-09. ADR-006 uses 2026-04-09. Match style.
Reader thinks ADR-008 changes runtime today §Verification opens with “Implementation lands in a follow-up task; this ADR is the architectural decision, not the patch.”

8. References

  • Audit: docs/audits/adr-008-mode-enforcement-audit.md
  • Contract: docs/contracts/adr-008-mode-enforcement-contract.md
  • ADR format: docs/architecture/decisions/README.md
  • Code: src/modes.ts:174, src/server.ts:229
  • Docs: docs/2-plugin/modes.md:31

Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.