Skip to content

Commit 2dff04d

Browse files
Claude/coord postmerge cleanup (#145)
<!-- SPDX-License-Identifier: MPL-2.0 --> ## Summary <!-- Briefly describe what this PR does and why. Link to related issues with "Closes #N". --> ## Changes <!-- List the key changes introduced by this PR. --> - ## RSR Quality Checklist <!-- Check all that apply. PRs that fail required checks will not be merged. --> ### Required - [ ] Tests pass (`just test` or equivalent) - [ ] Code is formatted (`just fmt` or equivalent) - [ ] Linter is clean (no new warnings or errors) - [ ] No banned language patterns (no TypeScript, no npm/bun, no Go/Python) - [ ] No `unsafe` blocks without `// SAFETY:` comments - [ ] No banned functions (`believe_me`, `unsafeCoerce`, `Obj.magic`, `Admitted`, `sorry`) - [ ] SPDX license headers present on all new/modified source files - [ ] No secrets, credentials, or `.env` files included ### As Applicable - [ ] `.machine_readable/STATE.a2ml` updated (if project state changed) - [ ] `.machine_readable/ECOSYSTEM.a2ml` updated (if integrations changed) - [ ] `.machine_readable/META.a2ml` updated (if architectural decisions changed) - [ ] Documentation updated for user-facing changes - [ ] `TOPOLOGY.md` updated (if architecture changed) - [ ] `CHANGELOG` or release notes updated - [ ] New dependencies reviewed for license compatibility (MPL-2.0 / MPL-2.0) - [ ] ABI/FFI changes validated (`src/abi/` and `ffi/zig/` consistent) ## Testing <!-- Describe how you tested these changes. --> ## Screenshots <!-- If applicable, add screenshots or terminal output demonstrating the change. --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 56b0c15 commit 2dff04d

2 files changed

Lines changed: 269 additions & 0 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<!-- SPDX-License-Identifier: MPL-2.0 -->
2+
<!-- Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> -->
3+
4+
# 15. Backend-enforced file-lock primitive — spike
5+
6+
Date: 2026-05-24
7+
8+
## Status
9+
10+
Deferred (2026-05-24) — ADR-0016 (cross-host federation stop-gap) was
11+
chosen as the next build over this one because it closes a more
12+
user-visible survey gap without altering the verified backend core.
13+
The bridge-layer advisory path-claims shipped in PR #142/#143 remain
14+
the current answer for in-flight conflict signalling.
15+
16+
This ADR stays on file as the design-of-record for a backend-enforced
17+
lock primitive should "advisory warning is not enough" become a stated
18+
requirement. Reopen by flipping to "Proposed" and scheduling alongside
19+
the next P-0x proof-obligation cycle.
20+
21+
## Context
22+
23+
The multi-agent MCP survey identified that two comparator servers
24+
(`rinadelph/Agent-MCP`, `AndrewDavidRivers/multi-agent-coordination-mcp`)
25+
ship **hard file locks** at claim time, whereas `local-coord-mcp` has
26+
only the bridge-layer **advisory** path-claims added in PR #142 / PR #143.
27+
The survey marked file-level locks as a clear gap relative to those two.
28+
29+
This spike evaluates promoting path-claims from "bridge-only advisory
30+
warning" to a **backend-enforced lock primitive** in the verified Idris2
31+
ABI + Zig FFI.
32+
33+
## What the change does
34+
35+
1. Extend the `LocalCoord` Idris2 ABI with a new tool surface:
36+
`coord_lock_paths(token, task, paths[])` and
37+
`coord_unlock_paths(token, task)`. Paths are interned, normalised, and
38+
the backend maintains an authoritative `task → segment[][]` map
39+
alongside the existing claim map.
40+
2. The lock check runs **inside** `coord_claim_task` when `paths` is
41+
present: if any declared path segment-overlaps an existing locked
42+
path held by another peer, the claim is **rejected** (not annotated).
43+
Today's bridge-layer overlap scan becomes a projection of the
44+
backend's authoritative state.
45+
3. New proof obligation **P-08: LockSoundness** in
46+
`cartridges/local-coord-mcp/abi/LocalCoord/Locks.idr`, discharged by
47+
construction:
48+
- **Mutual exclusion** — no two distinct tasks simultaneously hold
49+
overlapping paths (segment-prefix-disjoint).
50+
- **Lock-claim composition** — a granted claim's path-locks survive
51+
until `coord_unlock_paths` or watchdog expiry; never silently
52+
released by a different peer.
53+
- **Watchdog interaction** — when the claim's role-based TTL fires
54+
(P-03 WatchdogTermination), the lock-set is released atomically
55+
with the claim.
56+
4. Cartridge schema bump: `cartridge.json` adds the two new tools and
57+
widens `coord_claim_task` to declare `path_locked` as a rejection
58+
reason in its output schema. Coherence test (`dispatch_test.js`)
59+
already enforces bridge↔cartridge sync, so the bridge tool list and
60+
dispatcher must update in lockstep.
61+
62+
## Cost
63+
64+
| Surface | Work |
65+
|---|---|
66+
| Idris2 ABI | New `Locks.idr` module + P-08 discharge. Roughly 200-300 lines of Idris2; the segment-prefix proof reduces to list-prefix induction on `List String`, which is constructive (no new `believe_me`). |
67+
| Zig FFI | `boj_coord_lock_paths` / `boj_coord_unlock_paths` exports + an internal `PathLockTable` (radix tree or sorted-array; flat array is fine at expected scale). ~150 LOC. |
68+
| Bridge | Demote `path-claims.js` to a stateless projection — query the backend's lock table on demand rather than maintaining its own. Or delete it. |
69+
| Cartridge schema | Two new tool entries; one output-shape widening on `coord_claim_task`. |
70+
| Tests | New Idris2 totality proofs (P-08), Zig unit tests, bridge integration tests, bench update. |
71+
72+
Estimated: 2-4 working days end-to-end.
73+
74+
## Benefit
75+
76+
- Closes the survey gap **completely**: backend-enforced locks rather
77+
than an advisory layer. Strongest answer.
78+
- Idris2 proof gives a guarantee neither Agent-MCP nor multi-agent-coord
79+
has — they ship runtime-checked locks, we'd ship a *proved-correct*
80+
lock primitive. Aligns with the AAA Formal posture (P-01..P-07).
81+
- The bridge-layer code shrinks (path-claims.js largely becomes a
82+
pass-through projection), which is a healthy direction.
83+
84+
## Risks / open questions
85+
86+
- **Architectural regression risk.** Today's design is explicit: the
87+
verified backend stays minimal and the bridge layers advisory
88+
features. Adding state to the backend (path-locks) is the first time
89+
a non-task-id concept enters the proved core. P-08 must not weaken
90+
the existing proofs — needs careful composition argument.
91+
- **Schema-evolution lock-in.** Once `coord_lock_paths` is in the
92+
cartridge manifest, removing it is a breaking change. Path-semantics
93+
(segment-prefix matching) gets baked into the wire contract.
94+
- **Defines vs. enforces.** What does the backend do when a *non-path*
95+
claim conflicts with a locked path? The current advisory layer is
96+
silent in that case; an enforcer must take a position.
97+
- **Failure modes change.** Today a clashing path is a *warning*; under
98+
this proposal it's a *claim rejection*. Existing rate-limit (5
99+
rejections / 10 min → 30s cooldown) applies; either way, agents need
100+
retry logic.
101+
102+
## Verdict
103+
104+
Strongest technical answer to the survey gap, but the largest
105+
architectural commitment in this branch of work. The proof load
106+
(P-08) is tractable — segment-prefix mutual exclusion is structurally
107+
inductive — but the *design* commitment (locks as a first-class backend
108+
concept) deserves the user's explicit sign-off, not an implementer's
109+
judgement call.
110+
111+
**Recommendation:** Choose this only if "advisory warning is not
112+
enough" is a stated requirement. Otherwise, prefer ADR-0016
113+
(cross-host federation stop-gap), which closes a different and arguably
114+
more user-visible survey gap (Ruflo being the only comparator with real
115+
cross-host story) without altering the verified core.
116+
117+
## Out of scope
118+
119+
- File-range locks (byte ranges within a file). Not addressed — paths
120+
are still whole-file. The Perforce-style range lock would be a
121+
separate, larger ADR.
122+
- Lock fairness / queueing on contention. This spike rejects on
123+
overlap; a queue would be a P-09 extension.
124+
- Cross-host locks. Locks here are still localhost-bound; combining
125+
with ADR-0010 or ADR-0016 is future work.
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
<!-- SPDX-License-Identifier: MPL-2.0 -->
2+
<!-- Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> -->
3+
4+
# 16. mTLS + ed25519 federation stop-gap — spike
5+
6+
Date: 2026-05-24
7+
8+
## Status
9+
10+
Accepted (2026-05-24) — chosen over ADR-0015 because it closes the
11+
more user-visible survey gap (Ruflo as the only comparator with a
12+
cross-host story) and is additive: loopback bus and Idris2-verified
13+
intra-host ABI stay untouched. Positioned as the v1 transport;
14+
ADR-0010 (DID + ML-DSA-87 + ML-KEM-1024 + federated quarantine)
15+
remains the v2 upgrade path.
16+
17+
**Implementation plan:** phased rollout, each phase its own PR off
18+
`main` after this ADR lands.
19+
20+
| Phase | Scope | Estimated |
21+
|---|---|---|
22+
| 1 | Identity foundation — ed25519 keypair, pubkey export, `known_peers.toml` parser, `coord-tui --print-pubkey` flag. No transport. | ~1 day |
23+
| 2 | Envelope sign/verify on the loopback bus (round-trip validation before any wire work). | ~1 day |
24+
| 3 | TLS listener on `:7746` + mTLS handshake gated by `known_peers.toml`. | ~1-2 days |
25+
| 4 | New `coord_add_peer` / `coord_remove_peer` / `coord_list_remote_peers` tools — bridge + cartridge.json sync. | ~0.5 day |
26+
| 5 | Idris2 `Federation.idr` — envelope-boundary signature soundness obligation. | ~1 day |
27+
| 6 | Tests — two-instance round-trip, signature tampering, replay protection. | ~0.5 day |
28+
29+
## Context
30+
31+
The multi-agent MCP survey identified `ruvnet/ruflo` as the only
32+
comparator shipping a real cross-host story (mTLS + ed25519 federation
33+
across machines). `local-coord-mcp` is currently localhost-only
34+
(`127.0.0.1:7745`) — closing this gap is one of the two outstanding
35+
items from PR #143's spike.
36+
37+
ADR-0010 already proposes the *ambitious* answer: DID-based identity,
38+
ML-DSA-87 signing, ML-KEM-1024 key exchange, federated quarantine. It
39+
sits at "Proposed (RFC)" status, tracked under epic #87 item 3, and is
40+
a substantial cross-cartridge build effort.
41+
42+
This spike evaluates a deliberately **smaller** stop-gap: ship
43+
Ruflo-equivalent cross-host federation using mTLS + ed25519 *now*,
44+
without blocking on the post-quantum work in ADR-0010. The two are
45+
compatible: this becomes the v1 transport, ADR-0010 becomes the v2
46+
upgrade path.
47+
48+
## What the change does
49+
50+
1. **Transport.** Add a TLS-terminating endpoint alongside the loopback
51+
bus — `https://<host>:7746/coord/federated/inbox` for inbound
52+
federated envelopes. Loopback bus (`:7745`) stays the default for
53+
intra-machine traffic and is untouched.
54+
2. **Identity.** Each peer generates an ed25519 keypair on first run
55+
(stored under `~/.cache/coord-tui/peer.key`). The public key is the
56+
peer's federated identity; the private key signs outbound envelopes.
57+
No DID method, no PKI hierarchy — leaf-only trust.
58+
3. **Peer trust.** A new file `~/.config/coord-tui/known_peers.toml`
59+
maps peer-id → `{host, port, pubkey}`. Federation only works between
60+
peers that have explicitly added each other (manual key exchange via
61+
`coord-tui --print-pubkey` and a shared channel). No discovery; no
62+
registry.
63+
4. **mTLS.** Both ends of the federated link present client certs
64+
derived from their ed25519 identity keys (SPKI-pinned, not
65+
CA-signed). Connection fails closed if the presented pubkey doesn't
66+
match `known_peers.toml`.
67+
5. **Envelope wrapping.** Outbound coord messages destined for a remote
68+
peer are signed (ed25519 over the canonical envelope bytes), wrapped
69+
in `{from, to, signature, payload}`, sent over the mTLS connection.
70+
Receiver verifies signature against `known_peers.toml`, then injects
71+
into the local bus as if it had arrived locally.
72+
6. **New tools.** `coord_add_peer(peer_id, host, port, pubkey)`,
73+
`coord_remove_peer(peer_id)`, `coord_list_remote_peers()`. No tool
74+
schema change to the existing `coord_send` / `coord_claim_task`
75+
surface — federation is transport-layer.
76+
77+
## Cost
78+
79+
| Surface | Work |
80+
|---|---|
81+
| Zig adapter | TLS listener (use `std.crypto.tls`), ed25519 sign/verify (`std.crypto.sign.Ed25519` — already available), known_peers parser, envelope wrap/unwrap. ~400-600 LOC. |
82+
| Idris2 ABI | New `Federation.idr` — but only proof obligations on the *envelope* boundary (signature verification soundness), not on the transport. ~150 lines; reuses existing `SafeHTTP` patterns. Class (J) believe_me may be unavoidable for `std.crypto.tls` opaque primitives — same axiom posture as the existing `Char`/`String` primitives. |
83+
| Bridge | New 3 `coord_*` tool entries in `tools.js` + cartridge.json; dispatcher routes them to the backend. No envelope-shape changes. |
84+
| coord-tui | `--print-pubkey` flag, `~/.config/coord-tui/known_peers.toml` reader, optional `coord-add-peer` shell helper. |
85+
| Tests | Round-trip test on two backend instances on different ports; signature-tampering rejection test; replay-protection (envelope timestamp + nonce). |
86+
87+
Estimated: 4-6 working days end-to-end.
88+
89+
## Benefit
90+
91+
- Closes the survey's "cross-host transport" gap directly: matches
92+
Ruflo's posture, doesn't claim more.
93+
- Additive — the existing loopback bus and Idris2-verified intra-host
94+
ABI are untouched. No proof regression.
95+
- Sets up ADR-0010 as a clean v2 upgrade (swap ed25519→ML-DSA-87,
96+
add ML-KEM, add DID resolution) once that work is prioritised.
97+
Wire format is the same shape; only the cryptographic primitives
98+
change.
99+
- Real user-visible feature — agents on Jonathan's workstation can
100+
coordinate with agents on a build server, a sandbox VM, or a
101+
teammate's machine.
102+
103+
## Risks / open questions
104+
105+
- **Trust bootstrap is manual.** Exchanging pubkeys via a shared
106+
channel is the right answer for a v1 (it's how SSH known_hosts
107+
works), but it does mean federation has a per-peer setup cost. ADR-
108+
0010's DID + cartridge-index resolution removes this; v1 doesn't.
109+
- **No federated quarantine.** Tier-2+ envelopes from a remote peer
110+
hit the *local* master for review — no upstream supervision model.
111+
This is a feature gap relative to ADR-0010, not a bug, but worth
112+
naming.
113+
- **Crypto agility.** ed25519 in a post-quantum world has a finite
114+
shelf life. Estate standards already commit to ML-DSA-87. This
115+
spike's stop-gap framing is honest about that: v1 lasts until ADR-
116+
0010 lands, not forever. CHANGELOG and README should say so.
117+
- **Idris2 transport-layer proofs.** TLS internals are opaque to the
118+
Idris2 backend-assurance harness — same axiomatisation posture as
119+
the existing 5 `believe_me` sites. Net new principled assumption,
120+
not a regression.
121+
122+
## Verdict
123+
124+
Smaller scope than ADR-0015, lower architectural risk than ADR-0010,
125+
and directly closes the only survey gap where `local-coord-mcp`
126+
clearly trails a comparator. The "this is explicitly a stop-gap"
127+
framing keeps the door open for ADR-0010 without making it a
128+
blocker.
129+
130+
**Recommendation:** Prefer this over ADR-0015 if the goal is "ship a
131+
visible new capability that the survey identified as missing".
132+
Combine with ADR-0015 only if file-locks are a stated user need;
133+
otherwise ADR-0015 is gold-plating relative to the survey's actual
134+
findings.
135+
136+
## Out of scope
137+
138+
- DID identity (ADR-0010, v2 upgrade).
139+
- Post-quantum crypto (ADR-0010, v2 upgrade).
140+
- Discovery / registry (deliberate — manual known_peers keeps the
141+
trust story legible).
142+
- Federated quarantine semantics (would land alongside DID work).
143+
- Wire format negotiation for v1↔v2 transition — addressed when
144+
ADR-0010 implementation starts.

0 commit comments

Comments
 (0)