[PROPOSAL] governance-mcp: read-only substrate access for the jurist (PENDING-82)

The three-party model asks Claude.app to rule on items it cannot read. Steward
confirmed 2026-07-28 that local MCP servers are exposed to the app's *chat*
surface — and always have been, predating Cowork by about a year. My earlier
framing ("chat, not only Cowork") had the relationship backwards: it is "chat,
always; Cowork, only while its loop still runs locally," and local Cowork is the
mode being phased out as default. The jurist chat is therefore the sturdy target.

Five read-only tools. The one a pasted cache can never provide is
governance_item(id): the verbatim body of any item or ruling, across PENDING.md,
PENDING-archive.md and REVIEWED.md. Four refusals are designed in, each with a
control proving the refusal is detectable — no writes (AST-audited), no path
arguments (keys from a fixed enum, so there is no traversal to defend), no second
parser (item_spans is imported, not reimplemented), and not an agent (tools
return data; an agent would return testimony about the substrate instead).

[FIX] to the shared definition while here: item_spans() is now fence-aware. A
'## ' header inside a fenced block is neither an item nor a boundary. Zero such
headers exist today — 17 open items before and after — but governance drafts are
written as fenced markdown carrying '## REVIEWED-N' headers, which is the
steward's own practice, so the next draft would have created a phantom item and
truncated the item containing it. PENDING-82's own fenced JSON block confirms the
fix within the hour.

Not installed. The mcpServers key edits the steward's desktop-app config; the
snippet is in PENDING-82 and the server is inert until someone loads it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WuMjg3ipEVa3n8CoSzoyvc
This commit is contained in:
David F Glidden
2026-07-28 10:20:17 +02:00
co-authored by Claude Opus 5
parent d12feb5ba9
commit 3df09228c0
4 changed files with 516 additions and 9 deletions
+49
View File
@@ -410,3 +410,52 @@ So the goal is not "make them derived." It is: **keep the cache small, generate
**Files affected:** Claude.app personal preferences (steward edits). Already landed, detection/generation only: `~/dotfiles/scripts/wake-digest.py --brief`.
**Awaiting:** Steward decision on finding #1 (is Cowork a party?), and authorization for the §Standing Context split.
## PENDING-82 — Read-only MCP server: giving the jurist eyes on the substrate
**Date:** 2026-07-28
**Tag:** [PROPOSAL] — new interface between two governing parties. Built and self-tested; **not installed.** Installing it edits the steward's desktop-app config.
**Summary:** `~/dotfiles/scripts/governance-mcp.py` publishes the governance substrate to Claude.app's chat surface as five read-only tools, closing the gap PENDING-81 could only narrow. Supersedes PENDING-81's premise that a generated cache is the best available answer — for chat, it is no longer the only one.
**The gate PENDING-81 left open is answered, and my framing of it was backwards.** Steward-confirmed 2026-07-28: local MCP servers configured in `claude_desktop_config.json` are exposed to the **chat** surface, and have been since roughly a year before Cowork existed — never Cowork-gated. Cowork gets them *conditionally*: local sessions inherit them, remote sessions — now the default execution mode being rolled out — do not run local MCP at all. So the relationship is not "chat, not only Cowork" but **"chat, always; Cowork, only while it still runs locally."** The jurist chat sits on the stable side of that split, which makes this design *less* exposed to product drift than the Cowork-dependent one considered and rejected on 2026-07-28.
**Substrate check:** `claude_desktop_config.json` has **no `mcpServers` key** (top-level keys: `coworkUserFilesPath`, `preferences`). Its `preferences` block is app **UI state** — sidebar mode, pinned panes, Cowork toggles — *not* the jurist's Standing Context prose. Name collision only; PENDING-81's finding that the live preferences are nowhere on disk **stands unrevised.**
**What it exposes (five tools, all read-only):**
- `governance_state` — every open item with its `[TAG]`, recent rulings, drift count, per-repo status. Computed per call.
- `governance_item(id)` — the **verbatim** body of any item or ruling, across `PENDING.md`, `PENDING-archive.md`, `REVIEWED.md`. This is the capability a pasted cache can never have: the jurist can read the thing it is ruling on.
- `governance_read(file, offset, limit)` — verbatim paged read of one of six enumerated documents.
- `drift_report()` — full `governance-drift-check.py` output.
- `repo_activity(repo, count)` — branch, dirty count, recent commits.
**Four refusals designed in, each with a control that proves the refusal detectable:**
1. **Read-only.** No tool writes. Audited by AST, not by text search: 0 filesystem-mutating calls, git subcommands present = `{log, status}` only. A write path would collapse three parties into one.
2. **Not an agent.** Tools return data, verbatim where possible. The rejected alternative — a second Claude with filesystem access reporting back — yields *an agent's testimony about the substrate*, not the substrate.
3. **No second parser.** "An item" is defined once, in `wake-digest.py`'s `item_spans()`, imported here. A private second definition is how twenty items went missing on 2026-07-28.
4. **No path arguments.** Every tool takes a key from a fixed enum. There is no traversal to defend because no path is accepted; the reachable domain is enumerable rather than defined by the instrument.
**Verified:** 27 self-test controls pass, each pairing an absence with a same-run positive control (Q2). Plus a live stdio round-trip — `initialize` → `notifications/initialized` (correctly unanswered) → `tools/list` → two `tools/call` → malformed input survived as a `-32700` rather than a crash; stdout carried only JSON-RPC, stderr empty.
**[FIX] applied to the shared definition while here.** `item_spans()` is now **fence-aware**: a `## ` header inside a fenced code block is neither an item nor an item boundary. Zero such headers exist in the substrate today, so behaviour is unchanged (17 open items before and after) — but governance drafts are written as fenced markdown carrying `## REVIEWED-N` headers, per the steward's own copy-paste-clean practice, so the next such draft would have created a phantom item *and* truncated the real item containing it. Latent defect with a live trigger, not a hypothetical.
**⚑ A false pointer in yesterday's own resumption point.** It stated the two §Your Role edits were "drafted verbatim in the transcript **and in PENDING-81**." They are not in PENDING-81; they existed only in a transcript discarded at the restart. Same wrap that mis-stated the archive. The lesson is the one already in doctrine: *a draft that lives in a transcript is not a record.* Re-drafted below, in the file this time.
**Installation (steward's hand — it edits the app's config, and the app must restart):**
```json
{
"mcpServers": {
"governance": {
"command": "python3",
"args": ["/Users/davidglidden/dotfiles/scripts/governance-mcp.py"]
}
}
}
```
Merge that `mcpServers` key into `~/Library/Application Support/Claude/claude_desktop_config.json` alongside the existing `coworkUserFilesPath` and `preferences` keys, then restart Claude.app. Reversal is deleting the key.
**Cowork retirement — §Your Role replacement text (re-drafted; the steward places it, since the surrounding prose is not readable from here):**
> Three parties hold distinct roles: **steward** (David) authorizes; **jurist** (Claude.app) proposes and governs; **executor** (Claude Code) implements within authorization.
Delete the Cowork party entry and every reference to `COWORK.md`. Grounds, now two: a third executor costs a third doctrine copy of a document that is `CLAUDE.md` with the nouns changed; and Cowork could not have served as the jurist's filesystem eyes even in principle, since `coworkUserFilesPath` points at `~/Claude`, which does not exist, and remote Cowork — the incoming default — runs no local MCP at all.
**Files affected:** new `~/dotfiles/scripts/governance-mcp.py`; `~/dotfiles/scripts/wake-digest.py` (`item_spans()` fence-awareness + 3 controls). Awaiting steward hand: `claude_desktop_config.json`, Claude.app §Your Role and §Standing Context.
**Awaiting:** Steward authorization to install the `mcpServers` key. The server itself is inert until then — nothing loads it.