P2PA

by sanjoydat1

Not rated
GitHub

About

Parallel agents coordinate claims and sync state automatically - no manual merge or data loss.

Details

Author
sanjoydat1
Categories
Developer Tools, Other

Setup

Install P2PA in your MCP client (Claude Desktop, Cursor, Windsurf, and others).

Repository: https://github.com/sanjoydat1/P2PA

Follow the installation instructions in the repository README, then restart your MCP client.

Headless, peer-to-peer context synchronization for local AI agents.

Stop copy-pasting prompts between agents.P2PAis a local-firstMCPtoolkit that lets multiple LLM agents (Cursor, Claude Code, Claude Desktop, or custom scripts) share structured context, pass messages, and merge concurrent edits over a serverless P2P network.

Built for founders and engineering teams who want multiplayer AI without leaving their IDE.

Multi-agent collaboration is still broken in three ways:
- Ephemeral silos— When a local agent finishes a hard task, its working memory dies. Your teammate’s agent starts from zero.
- Token bloat— Many “multi-agent” setups shuttle entire context windows through a central cloud, burning tokens and adding latency.
- Walled gardens— Collaboration often means leaving the terminal for a proprietary dashboard.

P2PAkeeps a shared JSON state buffer on each machine and syncsonly the diffs:

- Hyperswarm— DHT discovery + NAT traversal. No central server.
- Key-based peer authentication— Only allowlisted ed25519 keys can connect. No inbound access from a leaked topic.
- Per-key CRDT merge— Every key carries its own hybrid logical clock. Two agents writing different keys never conflict; two agents writing the same key resolve to the same winner on every replica, with no arbitration step.
- Add-wins sets— Concurrent appends to a list all survive, instead of one agent's entries overwriting the other's.
- Work-claiming leases— An agent claims a task before starting it, so two connected agents don't duplicate work. Leases expire on their own, so a crashed agent cannot block the backlog.
- Agent roster— Each agent publishes its role, capabilities and status, so a swarm can route work to whoever is actually free instead of guessing.
- Addressed messages— Ask one specific agent a question and match its reply by correlation id, rather than broadcasting at everyone.
- Signed operations— Every write is signed by its author's key, so an entry stays attributable after any number of peers relay it. Without this, a swarm of three lets one peer fabricate another's writes.
- Negotiated protocol— Peers agree a version and capability set on connect, so a mixed-version swarm keeps working and an incompatible one says why instead of silently never syncing.
- Event-driven, not polling— An agent can block until the other one actually does something, instead of hoping it remembers to check.
- Messages survive a disconnect— Write to a peer whose agent is offline and it is delivered when they return. Nobody has to resend.
- Human-readable audit log— Every change lands in~/.p2pa/shared_context.md, attributed to the peer that made it.

The wire protocol is specified inSPEC.md— frame grammar, merge rules, signature canonicalization, bounds, and conformance vectors — so P2PA can be implemented in another language and interoperate.

graph TD subgraph MachineA [Machine A] AgentA[Local agent / Cursor] <-->|stdio MCP| MCPA[p2pa mcp] MCPA <--> StoreA[(CRDT document + leases)] MCPA -->|state · claims · audit| LogA["~/.p2pa/shared_context.md"] end subgraph MachineB [Machine B] AgentB[Local agent / Cursor] <-->|stdio MCP| MCPB[p2pa mcp] MCPB <--> StoreB[(CRDT document + leases)] MCPB -->|state · claims · audit| LogB["~/.p2pa/shared_context.md"] end MCPA <-->|Hyperswarm · NDJSON · CRDT ops| MCPB

Run one, not both.Both modes write the same files, so P2PA takes a writer lock at startup and the second one exits with an explanation rather than quietly overwriting the first. Usep2pa mcpwhen an IDE agent is driving; use the daemon when you want background sync without one.

No pairing, no second computer — this just proves the merge engine and the work-leases do what they claim:

git clone https://github.com/SanjoyDat1/P2PA.git cd P2PA npm install npm test # full suite, fully offline npm run smoke:merge # two replicas writing at once, no lost work npm run smoke:claim # two agents racing one backlog, nobody duplicates npm run smoke:outbox # a message left for an agent that is offline

npm testneeds no network: the peer-authentication tests run a realhyperdhttestnet in-process.

npm install && npm run build && npm link

RequiresNode.js 18+. Everything lives in~/.p2pa/.

Pairing ismutual and key-based. Each side allowlists the other's public key, and only allowlisted keys can connect — knowing the topic is not enough.

Send that token toBover a channel you already trust (it carries the pairing topic, so treat it like a password).

p2pa pair <A's token> # allowlists A, adopts A's topic, prints B's token
p2pa pair <B's token> # allowlists B — pairing complete p2pa peers # confirm both directions

Then lock it down (this is the default for new installs, but check):

That prints a ready-made MCP server block. Paste it into:

- Claude Codeclaude mcp add p2pa -- p2pa mcp, or the printed JSON in.mcp.json
- Cursor— Settings → MCP
- Claude Desktopclaude_desktop_config.json

The config runsp2pa mcpover stdio, so the tools appear inside the agent.

Both write the same files, so P2PA takes awriter lockat startup: whichever starts second exits with an explanation instead of silently overwriting the first. If you get that message,p2pa stopthe daemon and retry.

p2pa status # identity, topic, auth mode, peer count p2pa log # live tail of the audit trail p2pa peers # who you are paired with p2pa stop # stop the daemon

Two developers, two machines, one backlog. Nothing here is manual bookkeeping — the agents do it through the tools.

claim_task("refactor-auth", note: "splitting the token module") push_context("status", "auth refactor started")

Agent B, on the other machine, checks before starting anything:

list_claims() → refactor-auth is held by a3f9c1b2 claim_task("write-tests") → granted, different task

Agent Bfinishes and goes idle, rather than polling:

release_task("write-tests") await_peer_event() → blocks… ← { kind: "claim", taskId: "update-docs", peer: "a3f9c1b2" }

If B's machine is asleep when A sends a message, A does not need to resend — the message is queued and delivered when B comes back.

Everything above is also written to~/.p2pa/shared_context.mdin plain Markdown, attributed to the peer that did it, so a human can read the whole session without any tooling.

Config and state live under~/.p2pa/(mode0700):

Override the config directory withP2PA_CONFIG_DIR(must stay under your home directory).

A Hyperswarm topic is adiscovery identifier, not a secret.sha256(topic)is the DHT key your node announces under, and the DHT nodes nearest that key in keyspace necessarily learn it. Anyone who obtains it — by being in that neighbourhood, or because the topic leaked from a shell history, apslisting, or a chat log — could previously connect and get full read/write on your shared context.

P2PA now treats the topic asdiscovery onlyand authenticates peers by public key.

Each install generates a stable ed25519 keypair on first run, derived from a 32-byte seed in~/.p2pa/identity.json(0600). That public key is your node's permanent address on the swarm.

Hyperswarm's Noise handshake already proves a peer holds the secret key for the public key it presents. P2PA hooks thefirewallcallback — which runs onboth inbound and outboundconnection attempts — and refuses any key that is not on your allowlist. An unauthorized peer is dropped before a single byte of application data is exchanged, so it can neither read your state via the handshake snapshot nor write it via a patch.

That covers eachhop. It does not cover relay: a handshake snapshot carries operations authored byotherpeers — that is how a joining node learns what everyone else has done — so "the sender proved who it is" says nothing about who wrote the entries inside. With two nodes that costs nothing; with three or more it lets one peer fabricate another's writes. Every operation is therefore signed by its author's key, and stays verifiable however many peers relay it. SeeSPEC.md §6, and turn on enforcement withp2pa auth require-signaturesonce every node runs 0.8+.

Upgrading will not sever a working pairing: a config written before this feature has noauthfield and resolves toopen, with a warning on every start until you runp2pa auth strict.

p2pa peers # who can connect, and in which mode p2pa auth strict # lock it down (takes effect on next restart)

Changing theallowlistis picked up live by running nodes — pairing a peer connects it without a restart, and revoking one drops its open connection immediately. Changing theauth moderequires a restart.

Every peer-sourced entry in the audit trail now records which peer acted, keyed by the Noise-authenticated public key:

### [2026-07-25 10:14:02] - [SOURCE: Peer a3f9c1b2 (sanjoy-laptop)] - [ACTION: State Update]

Labels are peer-supplied and sanitized (control characters and Markdown structure stripped) so a peer cannot forge audit entries through its own name. The fingerprint is the identity; the label is a convenience.

- A peer's keyp2pa peers remove <pubkey>, then re-pair.
- Your own key— delete~/.p2pa/identity.jsonand restart. Every peer must re-pair with your new key.
- The topicp2pa start --topic <new>on each machine, or re-pair with--adopt-topic.

Agents sync machine state over P2P;humans steer in a shared Google Docanyone with the link can edit.

Humans edit "## HUMAN directives" → poller → Active State key steering Agents call doc_publish → Status / Plan / Agent log sections

- Create a Google Cloud project; enableGoogle Docs APIandGoogle Drive API.
- Create aservice account, download its JSON key.
- Export the path (never commit the key; never put it in shared context):

export P2PA_GOOGLE_SA_JSON="$HOME/.p2pa/google-sa.json" # chmod 600 the key file — path only (never paste the JSON into env / MCP config)
p2pa doc create --title "Auth refactor war room" # or: p2pa doc link "https://docs.google.com/document/d/…/edit" p2pa doc status

- PutP2PA_GOOGLE_SA_JSONin your MCP env (p2pa connectcopies it if already set in your shell), then restart MCP.

Agents keep running while you edit. They read steering withdoc_read_steeringorpull_contextkeysteering.

Optional:P2PA_DOC_POLL_MS(default4000).

A lease is a lock over a task id, and until v0.9 a task id referred to nothing — two agents describing the same work differently each took a lease and both did the job. The backlog is that missing vocabulary: work becomes an object with a shared id, a result, and a lifecycle other agents can wait on.

Agent A: create_task(title: "Port the auth module to the new token API", needs: ["typescript"], priority: 7) → "port-the-auth-module-to-the-new-toke-4f8c2a" Agent A: create_task(title: "Write migration notes", deps: ["port-the-auth-module-to-the-new-toke-4f8c2a"]) → blocked until the first is done Agent A: await_peer_event() Agent B: next_task() → the auth task, leased to B until 18:07:19Z … work happens … Agent B: complete_task(task_id: "port-the-auth-…", result: {files: 6}) Agent A: ← wakes with { kind: "task_done", taskId: "port-the-auth-…" } { kind: "task_ready", taskId: "write-migration-notes-…" }

next_taskselectsandleases in one call, so there is no window in which an agent has decided to do work it does not hold. It never offers a task whose dependencies are unfinished, one needing a capability the agent has not announced, or one a peer already holds — and when there is nothing for you it says so, with the counts, rather than returning an error.

A tasknever records who is working on it.@task/<id>holds the work and@claim/<id>holds the lease; they share an id and are joined when you read the board. Aholderfield on the task would be a second answer to a question the lease already answers, and the two would disagree the first time a holder crashed.

fail_taskputs the work back rather than losing it — after three attempts it is dead-lettered with the reason on the board. An agent that crashes mid-task simply lets its lease lapse; the task staysopenand is reported to the swarm as abandoned the next time anyone asks for work.

The board is also written into~/.p2pa/shared_context.mdunder## Backlog, so a human can read what the swarm is doing without asking it.

The backlog holds 500 tasks. Settled ones are collected after seven days, and a board that is already full makes room by dropping the longest-settled task rather than refusing new work — so the cap is a queue depth, not a lifetime limit. Open tasks are never dropped: if all 500 are open,create_taskrefuses and says so.

State sync stops two agents overwriting each other; it does not stop them doing the same job twice. A lease fixes that:

Agent A: claim_task("refactor-auth") → holds it until 14:32 Agent B: claim_task("refactor-auth") → already held by a3f9c1b2, picks another task

- Exactly one holder.Two agents racing for the same task converge on one winner, in any delivery order, without asking each other.
- First come, first served.Within a lease generation the earliest claim wins, so an honest agent cannot take a task by simply writing again.
- Leases expire.A crashed agent stops blocking the task once its TTL runs out; whoever claims next takes the following generation, so a stale op from the dead lease can never reinstate it.
- Release is final.Handing a task back cannot be undone by a claim that was still in flight.

claim_taskwaits one propagation window before answering, so an agent is never told it owns work it has already lost.

- Twopartitionednodes can both believe they hold the same lease until they can talk again. No protocol without a quorum can avoid that, and P2PA has no quorum by design. The lease narrows duplicate work to the propagation delay — it is not a distributed mutex.
- A lease protects against races, not against a hostile peer. An allowlisted peer can bid a higher generation and take a live lease, just as it can overwrite any state key. Displaced leases surface incheck_conflictsand the audit trail. Pair with peers you trust.

Messages used to go straight to whatever sockets were open, so anything written while the other agent was asleep, restarting, or on a train was simply lost.

A message is now queued first and sent second:

Agent A: send_peer_message("auth refactor is done") → nobody online, queued … Agent B starts up … Agent B: ← receives it automatically on connect

- Queued before sending, so a socket that drops mid-flight loses nothing.
- Retried until confirmed.A message is only dropped once the recipient acknowledges it, so "written to the socket" is never mistaken for "received".
- Delivered exactly once as far as the agent can tell.Replay is at-least-once; the receiver dedupes by message id, so a replay is not logged or surfaced twice.
- Survives a restart of either side— the queue and the seen-ids are on disk.

Bounded, since a peer that never returns must not grow the file forever: 500 messages, given up after 7 days, replayed 100 at a time. Anything given up on is counted inoutbox_statusrather than vanishing quietly.

A message is addressed to the peers you were paired with when you sent it, and replayed only to peers you have actually paired with — a node that joins later does not receive the earlier conversation.

Every other tool is pull-only, which means an agent learns a peer did something only if it happens to call one — and an LLM does not do that unprompted. So one agent talks and the other never hears it.

Agent B: await_peer_event() → blocks Agent A: claim_task("refactor-auth") Agent B: ← wakes with { kind: "claim", taskId: "refactor-auth", … }

Events carry aseq. Pass the highest one you have seen back assince_seqand nothing is missed between calls, even if you were busy when it happened. A timeout returns an empty list rather than an error — "nothing happened" is an ordinary answer.

Clients that support MCP resource subscriptions can instead watchp2pa://eventsand get nudged on each peer action, without parking a tool call.

With more than two agents, "who should do this?" matters as much as "has someone already done it?". Each agent publishes a card saying what it is for:

Agent A: announce_self(role="planner", capabilities=["architecture"]) Agent B: announce_self(role="builder", capabilities=["typescript","tests"]) Agent C: announce_self(role="reviewer", capabilities=["security"])
No reviews yet — be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.