|
| 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 |
| 4 | +:toc: |
| 5 | +:toc-placement: preamble |
| 6 | + |
| 7 | +Localhost multi-instance coordination cartridge for BoJ Server. |
| 8 | + |
| 9 | +== Purpose |
| 10 | + |
| 11 | +Enables multiple AI instances (Claude Code windows, Gemini, Copilot, etc.) |
| 12 | +running on the same machine to discover each other, exchange messages, and |
| 13 | +coordinate work — preventing duplicated effort and enabling intentional |
| 14 | +multi-window workflows. |
| 15 | + |
| 16 | +== Security Model |
| 17 | + |
| 18 | +=== Loopback-Only Binding (Compile-Time Guaranteed) |
| 19 | + |
| 20 | +The Idris2 ABI (`SafeLocalCoord.idr`) provides an `IsLoopback` dependent type |
| 21 | +with exactly two inhabitants: `IsIPv4Loop` ("127.0.0.1") and `IsIPv6Loop` |
| 22 | +("::1"). The `BindConfig` record requires this proof — there is no code path |
| 23 | +that can construct a bind configuration for `0.0.0.0` or any LAN/external |
| 24 | +address. This is a **compile-time guarantee**, not a runtime check. |
| 25 | + |
| 26 | +The Zig FFI and adapter honour this by hardcoding `127.0.0.1:7745`. |
| 27 | + |
| 28 | +=== Session Tokens |
| 29 | + |
| 30 | +Every peer receives a 128-bit CSPRNG token on registration. All subsequent |
| 31 | +calls must carry this token. A message from an unregistered process is rejected |
| 32 | +before dispatch. |
| 33 | + |
| 34 | +=== No Federation |
| 35 | + |
| 36 | +This cartridge explicitly does NOT participate in the Umoja federation network. |
| 37 | +The `FederationPolicy` type has a single constructor (`LocalOnly`) and the |
| 38 | +`IsFederated` type is uninhabited for `LocalOnly` — federation is impossible, |
| 39 | +not just disabled. |
| 40 | + |
| 41 | +== Tools |
| 42 | + |
| 43 | +[cols="1,3"] |
| 44 | +|=== |
| 45 | +| Tool | Description |
| 46 | + |
| 47 | +| `coord_register` |
| 48 | +| Register this instance. Returns a hybrid peer ID (e.g. `claude-7f3a`) and a session token. |
| 49 | + |
| 50 | +| `coord_list_peers` |
| 51 | +| List all active peers on this machine with their IDs, kinds, and states. |
| 52 | + |
| 53 | +| `coord_send` |
| 54 | +| Send a direct message to a peer or broadcast to all (`target: "*"`). |
| 55 | + |
| 56 | +| `coord_receive` |
| 57 | +| Poll this peer's inbox for the next message. |
| 58 | + |
| 59 | +| `coord_claim_task` |
| 60 | +| Mutex-style task claiming. First peer to claim wins; others are told who holds it. |
| 61 | + |
| 62 | +| `coord_status` |
| 63 | +| Set this peer's current work status, visible to other peers. |
| 64 | +|=== |
| 65 | + |
| 66 | +== Architecture |
| 67 | + |
| 68 | +---- |
| 69 | +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ |
| 70 | +│ Claude Code │ │ Claude Code │ │ Gemini │ |
| 71 | +│ Window 1 │ │ Window 2 │ │ Window 3 │ |
| 72 | +└──────┬───────┘ └──────┬───────┘ └──────┬───────┘ |
| 73 | + │ MCP │ MCP │ MCP |
| 74 | + └────────┬────────┴────────┬────────┘ |
| 75 | + │ │ |
| 76 | + ┌────────▼─────────────────▼────────┐ |
| 77 | + │ MCP Bridge (main.js) │ |
| 78 | + │ tools/call → local-coord-mcp │ |
| 79 | + └────────────────┬──────────────────┘ |
| 80 | + │ |
| 81 | + ┌────────────────▼──────────────────┐ |
| 82 | + │ mod.js (Deno adapter) │ |
| 83 | + │ POST http://127.0.0.1:7745/... │ |
| 84 | + └────────────────┬──────────────────┘ |
| 85 | + │ |
| 86 | + ┌────────────────▼──────────────────┐ |
| 87 | + │ local_coord_adapter.zig │ |
| 88 | + │ REST on 127.0.0.1:7745 │ |
| 89 | + │ ↓ calls ↓ │ |
| 90 | + │ local_coord_ffi.zig │ |
| 91 | + │ Peer registry, message fan-out, │ |
| 92 | + │ task claim mutex │ |
| 93 | + └──────────────────────────────────┘ |
| 94 | + Idris2 ABI proofs: |
| 95 | + IsLoopback, ValidPort, |
| 96 | + PeerState transitions, |
| 97 | + ClaimResult semantics |
| 98 | +---- |
| 99 | + |
| 100 | +== Peer Identity (Hybrid Model) |
| 101 | + |
| 102 | +Peers are identified by a human-readable prefix + 4-char hex suffix: |
| 103 | + |
| 104 | +- `claude-7f3a` — Claude Code instance |
| 105 | +- `gemini-b2c1` — Gemini instance |
| 106 | +- `copilot-4e9d` — Copilot instance |
| 107 | +- `custom-a1b2` — Other tool |
| 108 | + |
| 109 | +== Port Assignment |
| 110 | + |
| 111 | +| Port | Service | |
| 112 | +|------|---------| |
| 113 | +| 7745 | local-coord-mcp REST | |
| 114 | + |
| 115 | +== Files |
| 116 | + |
| 117 | +[cols="1,2"] |
| 118 | +|=== |
| 119 | +| Path | Purpose |
| 120 | + |
| 121 | +| `abi/LocalCoord/SafeLocalCoord.idr` |
| 122 | +| Loopback proof, port safety, session tokens, peer identity, federation opt-out |
| 123 | + |
| 124 | +| `abi/LocalCoord/Protocol.idr` |
| 125 | +| Message types, task claiming, peer lifecycle, authenticated message wrapper |
| 126 | + |
| 127 | +| `abi/local-coord-mcp.ipkg` |
| 128 | +| Idris2 package definition |
| 129 | + |
| 130 | +| `ffi/local_coord_ffi.zig` |
| 131 | +| Peer registry, message fan-out, claim mutex, CSPRNG tokens |
| 132 | + |
| 133 | +| `adapter/local_coord_adapter.zig` |
| 134 | +| REST server on 127.0.0.1:7745 |
| 135 | + |
| 136 | +| `cartridge.ncl` |
| 137 | +| Nickel source-of-truth manifest |
| 138 | + |
| 139 | +| `cartridge.json` |
| 140 | +| Generated from Nickel — consumed by MCP bridge |
| 141 | + |
| 142 | +| `mod.js` |
| 143 | +| Deno adapter for MCP tool dispatch |
| 144 | +|=== |
| 145 | + |
| 146 | +== Build |
| 147 | + |
| 148 | +[source,bash] |
| 149 | +---- |
| 150 | +# FFI tests |
| 151 | +cd ffi && zig build test |
| 152 | +
|
| 153 | +# Shared library |
| 154 | +cd ffi && zig build lib |
| 155 | +
|
| 156 | +# Adapter binary |
| 157 | +cd adapter && zig build |
| 158 | +
|
| 159 | +# Regenerate JSON manifest from Nickel |
| 160 | +nickel export --format json < cartridge.ncl > cartridge.json |
| 161 | +---- |
| 162 | + |
| 163 | +== Usage Example |
| 164 | + |
| 165 | +[source] |
| 166 | +---- |
| 167 | +# Window 1 (Claude Code) |
| 168 | +> coord_register(client_kind: "claude") |
| 169 | + → { peer_id: "claude-7f3a", token: "abc123..." } |
| 170 | +
|
| 171 | +> coord_list_peers(token: "abc123...") |
| 172 | + → [{ id: "claude-7f3a", kind: "claude", state: "active" }, |
| 173 | + { id: "gemini-b2c1", kind: "gemini", state: "active" }] |
| 174 | +
|
| 175 | +> coord_claim_task(token: "abc123...", task: "audit-boj-server") |
| 176 | + → { result: "granted" } |
| 177 | +
|
| 178 | +> coord_send(token: "abc123...", target: "*", message: "I'm auditing boj-server, take something else") |
| 179 | + → { sent: 1 } |
| 180 | +
|
| 181 | +# Window 2 (Gemini) |
| 182 | +> coord_receive(token: "def456...") |
| 183 | + → { from: "claude-7f3a", message: "I'm auditing boj-server, take something else" } |
| 184 | +
|
| 185 | +> coord_claim_task(token: "def456...", task: "audit-boj-server") |
| 186 | + → { result: "held", holder: "claude-7f3a" } |
| 187 | +
|
| 188 | +> coord_claim_task(token: "def456...", task: "audit-hypatia") |
| 189 | + → { result: "granted" } |
| 190 | +---- |
0 commit comments