Skip to content

Commit f264764

Browse files
hyperpolymathclaude
andcommitted
docs(local-coord-mcp): add PanLL panels manifest and technical report
RSR compliance: panels/manifest.json (3 panels: peers, claims, status) and LOCAL-COORD-MCP-REPORT.adoc (security architecture, formal verification summary, test coverage, integration roadmap). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 887f556 commit f264764

2 files changed

Lines changed: 363 additions & 0 deletions

File tree

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
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.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
{
2+
"$schema": "https://panll.dev/schemas/panel-manifest/v1.json",
3+
"spdx": "PMPL-1.0-or-later",
4+
"cartridge": "local-coord-mcp",
5+
"domain": "Local Coordination",
6+
"version": "0.1.0",
7+
"panels": [
8+
{
9+
"id": "coord-peers",
10+
"title": "Active Peers",
11+
"description": "AI instances registered on this machine — their IDs, kinds, and states",
12+
"type": "metric",
13+
"data_source": {
14+
"endpoint": "/cartridge/local-coord-mcp/invoke",
15+
"method": "POST",
16+
"body": { "tool": "coord_list_peers" },
17+
"refresh_interval_ms": 3000
18+
},
19+
"widgets": [
20+
{ "type": "counter", "field": "active_peers", "label": "Active Peers", "icon": "users" },
21+
{
22+
"type": "table",
23+
"field": "peers",
24+
"columns": [
25+
{ "key": "id", "label": "Peer ID" },
26+
{ "key": "kind", "label": "Client" },
27+
{ "key": "state", "label": "State" },
28+
{ "key": "status", "label": "Current Work" }
29+
]
30+
}
31+
]
32+
},
33+
{
34+
"id": "coord-claims",
35+
"title": "Task Claims",
36+
"description": "Mutex-style task assignments — which peer holds which task",
37+
"type": "metric",
38+
"data_source": {
39+
"endpoint": "/cartridge/local-coord-mcp/invoke",
40+
"method": "POST",
41+
"body": { "tool": "coord_list_claims" },
42+
"refresh_interval_ms": 5000
43+
},
44+
"widgets": [
45+
{ "type": "counter", "field": "active_claims", "label": "Active Claims", "icon": "lock" },
46+
{
47+
"type": "table",
48+
"field": "claims",
49+
"columns": [
50+
{ "key": "task", "label": "Task" },
51+
{ "key": "holder", "label": "Held By" }
52+
]
53+
}
54+
]
55+
},
56+
{
57+
"id": "coord-status",
58+
"title": "Coordination Status",
59+
"description": "Service health and bind configuration",
60+
"type": "status-indicator",
61+
"data_source": {
62+
"endpoint": "/cartridge/local-coord-mcp/invoke",
63+
"method": "POST",
64+
"body": { "tool": "status" },
65+
"refresh_interval_ms": 10000
66+
},
67+
"widgets": [
68+
{
69+
"type": "state-badge",
70+
"field": "state",
71+
"states": {
72+
"ready": { "color": "#2ecc71", "icon": "shield" },
73+
"degraded": { "color": "#f39c12", "icon": "alert-circle" },
74+
"error": { "color": "#e74c3c", "icon": "alert-triangle" }
75+
}
76+
},
77+
{ "type": "text", "field": "bind_address", "label": "Bind" },
78+
{ "type": "text", "field": "federation", "label": "Federation" }
79+
]
80+
}
81+
]
82+
}

0 commit comments

Comments
 (0)