|
| 1 | +// SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> |
| 3 | += LOCAL-COORD-MCP Technical Report |
| 4 | +Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> |
| 5 | +v0.1.0, 2026-04-12 |
| 6 | +:toc: |
| 7 | +:toc-placement: preamble |
| 8 | +:sectnums: |
| 9 | + |
| 10 | +Technical report for the `local-coord-mcp` cartridge — BoJ Server cartridge |
| 11 | +#97 (numbered at design time; registered count is 99+ after recent additions). |
| 12 | + |
| 13 | +== Overview |
| 14 | + |
| 15 | +[cols="1,3"] |
| 16 | +|=== |
| 17 | +| *Cartridge* | `local-coord-mcp` |
| 18 | +| *Version* | 0.1.0 |
| 19 | +| *Domain* | Agent (ai) |
| 20 | +| *Tier* | Ayo |
| 21 | +| *Protocols* | MCP, Agentic |
| 22 | +| *Port* | 7745 (REST, loopback only) |
| 23 | +| *Federation* | Disabled (LocalOnly — uninhabited IsFederated type) |
| 24 | +| *Max peers* | 16 concurrent instances |
| 25 | +| *Max claims* | 64 concurrent task locks |
| 26 | +| *Manifest format* | Nickel (.ncl) source-of-truth → JSON export (first BoJ cartridge to use this) |
| 27 | +|=== |
| 28 | + |
| 29 | +== Problem Statement |
| 30 | + |
| 31 | +When running multiple AI instances on one machine (e.g. three Claude Code |
| 32 | +windows splitting a repo audit), there is no mechanism for the instances to: |
| 33 | + |
| 34 | +1. Know each other exist |
| 35 | +2. Avoid duplicating work |
| 36 | +3. Share findings or coordinate strategy |
| 37 | + |
| 38 | +This leads to wasted tokens, conflicting edits, and redundant effort. The |
| 39 | +user must manually orchestrate by switching windows and copy-pasting context. |
| 40 | + |
| 41 | +== Design Decisions |
| 42 | + |
| 43 | +=== DD-1: Manifest Format (CLOSED 2026-04-12) |
| 44 | + |
| 45 | +*Decision:* Nickel. |
| 46 | + |
| 47 | +`cartridge.ncl` is the source-of-truth. `nickel export --format json` |
| 48 | +produces `cartridge.json` for the MCP bridge. This aligns with the estate |
| 49 | +rule "tools emit A2ML (data) or Nickel (schemas), not JSON" — manifests are |
| 50 | +config/schema (Nickel), not data (A2ML). |
| 51 | + |
| 52 | +=== DD-2: Port (CLOSED 2026-04-12) |
| 53 | + |
| 54 | +*Decision:* 7745. |
| 55 | + |
| 56 | +Ports 7700–7744 are allocated. 7745 is the next sequential assignment. |
| 57 | + |
| 58 | +=== DD-3: Client Identity (CLOSED 2026-04-12) |
| 59 | + |
| 60 | +*Decision:* Hybrid — human-readable prefix + 4-char hex suffix. |
| 61 | + |
| 62 | +Examples: `claude-7f3a`, `gemini-b2c1`, `copilot-4e9d`. Readable in logs |
| 63 | +and tool output, unique enough to prevent collisions on a single machine. |
| 64 | +Supports the 4-LLM workflow split (Claude, Gemini, Copilot, custom). |
| 65 | + |
| 66 | +== Security Architecture |
| 67 | + |
| 68 | +=== Loopback-Only Binding (Compile-Time) |
| 69 | + |
| 70 | +The Idris2 ABI defines `IsLoopback` as a dependent type with exactly two |
| 71 | +constructors: |
| 72 | + |
| 73 | +[source,idris] |
| 74 | +---- |
| 75 | +data IsLoopback : String -> Type where |
| 76 | + IsIPv4Loop : IsLoopback "127.0.0.1" |
| 77 | + IsIPv6Loop : IsLoopback "::1" |
| 78 | +---- |
| 79 | + |
| 80 | +The `BindConfig` record requires an `IsLoopback` proof. The type |
| 81 | +`IsLoopback "0.0.0.0"` is uninhabited — `wildcardNotLoopback` proves this. |
| 82 | +No code path exists that can bind to a non-loopback address. |
| 83 | + |
| 84 | +The Zig FFI and adapter hardcode `127.0.0.1` as a defence-in-depth measure, |
| 85 | +but the primary guarantee is the Idris2 proof. |
| 86 | + |
| 87 | +=== Session Tokens |
| 88 | + |
| 89 | +Each peer receives a 128-bit CSPRNG token (from `std.crypto.random`) on |
| 90 | +registration. All 6 tool calls require this token. Invalid tokens are |
| 91 | +rejected before dispatch. Tokens are not stored on disk — they exist only in |
| 92 | +the Zig FFI's in-memory peer registry. |
| 93 | + |
| 94 | +=== Federation Opt-Out |
| 95 | + |
| 96 | +[source,idris] |
| 97 | +---- |
| 98 | +data FederationPolicy : Type where |
| 99 | + LocalOnly : FederationPolicy |
| 100 | +
|
| 101 | +data IsFederated : FederationPolicy -> Type where |
| 102 | + -- Intentionally empty |
| 103 | +
|
| 104 | +localOnlyNotFederated : IsFederated LocalOnly -> Void |
| 105 | +localOnlyNotFederated _ impossible |
| 106 | +---- |
| 107 | + |
| 108 | +The `IsFederated` type has zero constructors for `LocalOnly`. Any code |
| 109 | +requiring federation participation would need to construct this proof, which |
| 110 | +is impossible. This is not a feature flag — it is a type-level impossibility. |
| 111 | + |
| 112 | +=== Attack Surface |
| 113 | + |
| 114 | +[cols="1,2,2"] |
| 115 | +|=== |
| 116 | +| Vector | Mitigation | Proof level |
| 117 | + |
| 118 | +| Network exposure |
| 119 | +| Loopback-only bind |
| 120 | +| Compile-time (Idris2 `IsLoopback`) |
| 121 | + |
| 122 | +| Rogue local processes |
| 123 | +| Per-session CSPRNG token |
| 124 | +| Runtime (Zig FFI) |
| 125 | + |
| 126 | +| Federation leak |
| 127 | +| Uninhabited `IsFederated` type |
| 128 | +| Compile-time (Idris2) |
| 129 | + |
| 130 | +| Inbox flood |
| 131 | +| 256-message ring buffer per peer |
| 132 | +| Runtime (Zig constant) |
| 133 | + |
| 134 | +| Peer exhaustion |
| 135 | +| 16-peer hard cap |
| 136 | +| Runtime (Zig constant) |
| 137 | + |
| 138 | +| CRLF / null-byte injection |
| 139 | +| Inherited from BoJ SafeHTTP/SafeWebSocket |
| 140 | +| Compile-time (Idris2) |
| 141 | +|=== |
| 142 | + |
| 143 | +== Formal Verification Summary |
| 144 | + |
| 145 | +[cols="1,1,2"] |
| 146 | +|=== |
| 147 | +| Module | Proofs | Status |
| 148 | + |
| 149 | +| SafeLocalCoord.idr |
| 150 | +| `IsLoopback`, `wildcardNotLoopback`, `emptyNotLoopback`, `loopbackRoundtrip`, `ValidPort`, `coordPortValid`, `localOnlyNotFederated` |
| 151 | +| 0 believe_me, 0 postulates |
| 152 | + |
| 153 | +| Protocol.idr |
| 154 | +| `grantedAllows`, `heldDenies`, `ValidPeerTransition` (5 constructors), `canTransitionPeer` |
| 155 | +| 0 believe_me, 0 postulates |
| 156 | +|=== |
| 157 | + |
| 158 | +Total: *0 believe_me*, *0 postulates* in the local-coord-mcp ABI. |
| 159 | + |
| 160 | +(The shared `SafetyLemmas.idr` has 3 axiomatic believe_me for Char/String |
| 161 | +primitives — these are not introduced by this cartridge.) |
| 162 | + |
| 163 | +== Tool Reference |
| 164 | + |
| 165 | +=== coord_register |
| 166 | + |
| 167 | +Register this instance. Returns a hybrid peer ID and a session token. |
| 168 | + |
| 169 | +*Input:* `{ client_kind: "claude" | "gemini" | "copilot" | "custom" }` |
| 170 | + |
| 171 | +*Output:* `{ peer_id: "claude-7f3a", token: "abc123..." }` |
| 172 | + |
| 173 | +*Side effects:* Allocates a peer slot (1 of 16). |
| 174 | + |
| 175 | +=== coord_list_peers |
| 176 | + |
| 177 | +List all active peers. |
| 178 | + |
| 179 | +*Input:* `{ token: "..." }` |
| 180 | + |
| 181 | +*Output:* `[{ id: "claude-7f3a", kind: "claude", state: "active", status: "auditing boj-server" }, ...]` |
| 182 | + |
| 183 | +*Side effects:* None (read-only). |
| 184 | + |
| 185 | +=== coord_send |
| 186 | + |
| 187 | +Send a message to a specific peer or broadcast to all. |
| 188 | + |
| 189 | +*Input:* `{ token: "...", target: "gemini-b2c1" | "*", message: "..." }` |
| 190 | + |
| 191 | +*Output:* `{ sent: 1 }` |
| 192 | + |
| 193 | +*Side effects:* Enqueues message(s) in recipient inbox(es). |
| 194 | + |
| 195 | +=== coord_receive |
| 196 | + |
| 197 | +Poll for the next inbound message. |
| 198 | + |
| 199 | +*Input:* `{ token: "..." }` |
| 200 | + |
| 201 | +*Output:* `{ from: "claude-7f3a", message: "..." }` or `{ empty: true }` |
| 202 | + |
| 203 | +*Side effects:* Dequeues one message from this peer's inbox. |
| 204 | + |
| 205 | +=== coord_claim_task |
| 206 | + |
| 207 | +Attempt to claim a task (mutex acquisition). |
| 208 | + |
| 209 | +*Input:* `{ token: "...", task: "audit-boj-server" }` |
| 210 | + |
| 211 | +*Output:* `{ result: "granted" }` or `{ result: "held", holder: "claude-7f3a" }` |
| 212 | + |
| 213 | +*Side effects:* Acquires or rejects a claim slot. Idempotent if already held |
| 214 | +by the same peer. |
| 215 | + |
| 216 | +=== coord_status |
| 217 | + |
| 218 | +Set this peer's current work status. |
| 219 | + |
| 220 | +*Input:* `{ token: "...", status: "auditing boj-server — src/abi/" }` |
| 221 | + |
| 222 | +*Output:* `{ success: true }` |
| 223 | + |
| 224 | +*Side effects:* Updates the peer's status field, visible via coord_list_peers. |
| 225 | + |
| 226 | +== Test Coverage |
| 227 | + |
| 228 | +=== Zig FFI Tests (8 tests, all passing) |
| 229 | + |
| 230 | +[cols="1,2"] |
| 231 | +|=== |
| 232 | +| Test | Validates |
| 233 | + |
| 234 | +| register and deregister peer | Basic lifecycle |
| 235 | +| register fills up | MAX_PEERS=16 hard cap |
| 236 | +| bad token rejected | Token validation gate |
| 237 | +| claim mutex semantics | Grant → deny → release → re-grant |
| 238 | +| idempotent claim | Same peer re-claims without error |
| 239 | +| send and receive direct message | Point-to-point messaging |
| 240 | +| broadcast message | Fan-out to all except sender |
| 241 | +| deregister releases claims | Auto-cleanup on disconnect |
| 242 | +|=== |
| 243 | + |
| 244 | +== Integration Roadmap |
| 245 | + |
| 246 | +[cols="1,2,1"] |
| 247 | +|=== |
| 248 | +| Phase | Work | Status |
| 249 | + |
| 250 | +| 1. ABI |
| 251 | +| SafeLocalCoord.idr, Protocol.idr, ipkg |
| 252 | +| DONE |
| 253 | + |
| 254 | +| 2. FFI |
| 255 | +| local_coord_ffi.zig (peer registry, claims, messages) |
| 256 | +| DONE |
| 257 | + |
| 258 | +| 3. Manifest + handler |
| 259 | +| cartridge.ncl → cartridge.json, mod.js |
| 260 | +| DONE |
| 261 | + |
| 262 | +| 4. Integration |
| 263 | +| Register in Catalogue.idr, TOPOLOGY.md port map, MCP bridge wiring, PanLL panels |
| 264 | +| Panels DONE, catalogue/topology PENDING |
| 265 | + |
| 266 | +| 5. End-to-end test |
| 267 | +| Two Claude Code instances register, message, and claim |
| 268 | +| PENDING |
| 269 | +|=== |
| 270 | + |
| 271 | +== Dependencies |
| 272 | + |
| 273 | +This cartridge depends on: |
| 274 | + |
| 275 | +- Idris2 (ABI compilation) |
| 276 | +- Zig 0.15+ (FFI and adapter) |
| 277 | +- Deno (mod.js runtime) |
| 278 | +- Nickel CLI (manifest generation) |
| 279 | +- BoJ MCP bridge (tool discovery and dispatch) |
| 280 | + |
| 281 | +No external network dependencies. No database. No federation. |
0 commit comments