P2PA
Agentes paralelos coordenam reivindicações e sincronizam estado automaticamente - sem mesclagem manual ou perda de dados.
Documentação
P2PA
Sincronização de contexto peer-to-peer headless para agentes de IA locais.
Pare de copiar e colar prompts entre agentes. P2PA é um kit de ferramentas MCP local-first que permite que vários agentes de LLM (Cursor, Claude Code, Claude Desktop ou scripts personalizados) compartilhem contexto estruturado, troquem mensagens e mesclem edições concorrentes em uma rede P2P sem servidor.
Feito para fundadores e equipes de engenharia que querem IA multiplayer sem sair do IDE.
O problema
A colaboração multi-agente ainda é problemática de três maneiras:
- Silos efêmeros — Quando um agente local termina uma tarefa difícil, sua memória de trabalho morre. O agente do seu colega começa do zero.
- Inchaço de tokens — Muitas configurações "multi-agente" transportam janelas de contexto inteiras por uma nuvem central, queimando tokens e adicionando latência.
- Jardins murados — A colaboração muitas vezes significa sair do terminal para um painel proprietário.
A solução
P2PA mantém um buffer de estado JSON compartilhado em cada máquina e sincroniza apenas os diffs:
- Hyperswarm — Descoberta DHT + travessia NAT. Sem servidor central.
- Autenticação de pares baseada em chave — Somente chaves ed25519 na lista de permissões podem se conectar. Sem acesso de entrada a partir de um tópico vazado.
- Mesclagem CRDT por chave — Cada chave carrega seu próprio relógio lógico híbrido. Dois agentes escrevendo chaves diferentes nunca entram em conflito; dois agentes escrevendo a mesma chave resolvem para o mesmo vencedor em cada réplica, sem etapa de arbitragem.
- Conjuntos add-wins — Anexos concorrentes a uma lista sobrevivem, em vez de as entradas de um agente sobrescreverem as do outro.
- Leases de reivindicação de trabalho — Um agente reivindica uma tarefa antes de iniciá-la, para que dois agentes conectados não dupliquem o trabalho. As leases expiram sozinhas, então um agente que travou não pode bloquear o backlog.
- Lista de agentes — Cada agente publica seu papel, capacidades e status, para que um enxame possa rotear o trabalho para quem está realmente livre, em vez de adivinhar.
- Mensagens endereçadas — Pergunte a um agente específico e combine sua resposta pelo id de correlação, em vez de transmitir para todos.
- Operações assinadas — Cada escrita é assinada pela chave do autor, para que uma entrada permaneça atribuível após qualquer número de pares retransmiti-la. Sem isso, um enxame de três permite que um par fabrique escritas de outro.
- Protocolo negociado — Os pares concordam com uma versão e um conjunto de capacidades na conexão, para que um enxame de versões mistas continue funcionando e um incompatível diga o porquê, em vez de silenciosamente nunca sincronizar.
- Orientado a eventos, não polling — Um agente pode bloquear até que o outro realmente faça algo, em vez de esperar que ele se lembre de verificar.
- Mensagens sobrevivem a uma desconexão — Escreva para um par cujo agente está offline e a mensagem é entregue quando ele voltar. Ninguém precisa reenviar.
- Log de auditoria legível por humanos — Cada mudança cai em
~/.p2pa/shared_context.md, atribuída ao par que a fez.
O protocolo de rede é especificado em SPEC.md — gramática de quadros, regras de mesclagem, canonicalização de assinaturas, limites e vetores de conformidade — para que o P2PA possa ser implementado em outra linguagem e interoperar.
Arquitetura
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
Dois modos de processo (mesmo estado em disco):
| Modo | Comando | Use quando |
|---|---|---|
| MCP (primeiro plano) | p2pa mcp | Conectando Cursor / Claude — possui stdio limpo |
| Daemon (segundo plano) | p2pa start | Sincronização Hyperswarm opcional via PM2 (sem MCP stdio) |
Execute um, não ambos. Ambos os modos escrevem os mesmos arquivos, então o P2PA obtém um bloqueio de escrita na inicialização e o segundo sai com uma explicação em vez de sobrescrever silenciosamente o primeiro. Use p2pa mcp quando um agente de IDE estiver dirigindo; use o daemon quando quiser sincronização em segundo plano sem um.
Experimente em dois minutos (uma máquina)
Sem pareamento, sem segundo computador — isso apenas prova que o mecanismo de mesclagem e as leases de trabalho fazem o que afirmam:
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 test não precisa de rede: os testes de autenticação de pares executam uma testnet real hyperdht em processo.
Executando de verdade (duas máquinas)
1. Instalação
npm install -g p2pa
Ou a partir de um clone:
npm install && npm run build && npm link
Requer Node.js 18+. Tudo fica em ~/.p2pa/.
2. Pareie as duas máquinas
O pareamento é mútuo e baseado em chave. Cada lado coloca a chave pública do outro na lista de permissões, e somente chaves na lista podem se conectar — saber o tópico não é suficiente.
Na máquina A:
p2pa pair # prints an invite token
Envie esse token para B por um canal que você já confia (ele carrega o tópico de pareamento, então trate-o como uma senha).
Na máquina B:
p2pa pair <A's token> # allowlists A, adopts A's topic, prints B's token
De volta em A:
p2pa pair <B's token> # allowlists B — pairing complete
p2pa peers # confirm both directions
Então restrinja o acesso (este é o padrão para novas instalações, mas verifique):
p2pa auth strict
3. Aponte seu agente para ele
p2pa connect
Isso imprime um bloco de servidor MCP pronto. Cole em:
- Claude Code —
claude mcp add p2pa -- p2pa mcp, ou o JSON impresso em.mcp.json - Cursor — Configurações → MCP
- Claude Desktop —
claude_desktop_config.json
A configuração executa p2pa mcp via stdio, então as ferramentas aparecem dentro do agente.
4. Escolha um processo, não dois
| Você quer | Execute | Notas |
|---|---|---|
| Um agente de IDE dirigindo | p2pa mcp (via a configuração MCP acima) | Iniciado para você pelo cliente |
| Sincronização em segundo plano, sem IDE | p2pa start | Daemon PM2 |
Ambos escrevem os mesmos arquivos, então o P2PA obtém um bloqueio de escrita na inicialização: o que iniciar em segundo lugar sai com uma explicação em vez de sobrescrever silenciosamente o primeiro. Se você receber essa mensagem, p2pa stop o daemon e tente novamente.
5. Veja funcionar
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
Como uma sessão realmente se parece
Dois desenvolvedores, duas máquinas, um backlog. Nada aqui é contabilidade manual — os agentes fazem isso através das ferramentas.
Agente A pega o trabalho e avisa:
claim_task("refactor-auth", note: "splitting the token module")
push_context("status", "auth refactor started")
Agente B, na outra máquina, verifica antes de começar qualquer coisa:
list_claims() → refactor-auth is held by a3f9c1b2
claim_task("write-tests") → granted, different task
Agente B termina e fica ocioso, em vez de fazer polling:
release_task("write-tests")
await_peer_event() → blocks…
← { kind: "claim", taskId: "update-docs", peer: "a3f9c1b2" }
Se a máquina de B estiver dormindo quando A enviar uma mensagem, A não precisa reenviar — a mensagem é enfileirada e entregue quando B voltar.
Tudo acima também é escrito em ~/.p2pa/shared_context.md em Markdown simples, atribuído ao par que o fez, para que um humano possa ler toda a sessão sem nenhuma ferramenta.
Referência da CLI
| Comando | Descrição |
|---|---|
p2pa start [--topic <code>] | Iniciar daemon Hyperswarm em segundo plano (PM2) |
p2pa stop | Parar o daemon |
p2pa status | Status do daemon, identidade, impressão digital do tópico, modo de autenticação |
p2pa log | Seguir ~/.p2pa/shared_context.md |
p2pa connect | Imprimir JSON MCP para Cursor / Claude Desktop |
p2pa mcp | Executar MCP em primeiro plano + servidor P2P (stdio) |
p2pa pair [--label <name>] | Imprimir seu token de convite |
p2pa pair <token> [--adopt-topic] | Colocar um par na lista de permissões a partir do token dele |
p2pa peers | Listar pares na lista de permissões |
p2pa peers remove <pubkey|label> | Revogar um par |
p2pa auth <strict|open> | Definir a política de conexão |
p2pa auth require-signatures | Recusar operações retransmitidas que não são assinadas (recomendado para 3+ pares) |
p2pa doc create [--title] | Criar uma sala de guerra no Google Docs + edição por qualquer pessoa com link |
p2pa doc link <url> | Vincular um Google Doc existente |
p2pa doc unlink | Limpar o vínculo do documento |
p2pa doc status | Mostrar documento vinculado + se as credenciais SA estão definidas |
Configuração e estado ficam em ~/.p2pa/ (modo 0700):
| Caminho | Propósito |
|---|---|
config.json | Tópico de pareamento, modo de autenticação, lista de permissões de pares, link de documento opcional (0600) |
identity.json | Semente de identidade de 32 bytes deste nó (0600) — nunca compartilhe |
shared_context.md | Estado Ativo + Estado de Réplica + Reivindicações + Atualizações Concorrentes + Trilha de Auditoria |
shared_context.archive.md | Entradas de auditoria mais antigas, removidas do arquivo ativo |
outbox.json | Mensagens aguardando confirmação (0600) |
state-writer.lock | Mantido por qualquer processo que esteja escrevendo |
daemon-error.log | Diagnósticos do daemon (não misturados no stdout do MCP) |
Substitua o diretório de configuração com P2PA_CONFIG_DIR (deve permanecer sob seu diretório pessoal).
Autenticação de pares
Por que o tópico sozinho não é suficiente
Um tópico Hyperswarm é um identificador de descoberta, não um segredo. sha256(topic) é a chave DHT sob a qual seu nó anuncia, e os nós DHT mais próximos dessa chave no espaço de chaves necessariamente a aprendem. Qualquer pessoa que a obtenha — por estar nessa vizinhança, ou porque o tópico vazou de um histórico de shell, uma listagem ps ou um log de chat — poderia anteriormente se conectar e obter leitura/escrita completa no seu contexto compartilhado.
O P2PA agora trata o tópico como apenas descoberta e autentica pares por chave pública.
Como funciona
Cada instalação gera um par de chaves ed25519 estável na primeira execução, derivado de uma semente de 32 bytes em ~/.p2pa/identity.json (0600). Essa chave pública é o endereço permanente do seu nó no enxame.
O handshake Noise do Hyperswarm já prova que um par possui a chave secreta para a chave pública que apresenta. O P2PA conecta o callback firewall — que roda em tentativas de conexão tanto de entrada quanto de saída — e recusa qualquer chave que não esteja na sua lista de permissões. Um par não autorizado é descartado antes que um único byte de dados de aplicação seja trocado, então ele não pode ler seu estado via snapshot do handshake nem escrevê-lo via patch.
Isso cobre cada salto. Não cobre retransmissão: um snapshot de handshake carrega operações escritas por outros pares — é assim que um nó que entra aprende o que todos os outros fizeram — então "o remetente provou quem é" não diz nada sobre quem escreveu as entradas dentro. Com dois nós isso não custa nada; com três ou mais, permite que um par fabrique escritas de outro. Cada operação é, portanto, assinada pela chave do autor e permanece verificável independentemente de quantos pares a retransmitam. Veja SPEC.md §6 e ative a aplicação com
p2pa auth require-signaturesassim que todos os nós executarem 0.8+.
Modos de autenticação
| Modo | Comportamento |
|---|---|
strict | Somente chaves públicas na lista de permissões podem se conectar. Padrão para novas instalações. |
open | Qualquer pessoa que conheça o tópico pode se conectar (comportamento pré-0.7). |
A atualização não interromperá um pareamento em funcionamento: uma configuração escrita antes deste recurso não tem o campo auth e resolve para open, com um aviso a cada início até você executar p2pa auth strict.
p2pa peers # who can connect, and in which mode
p2pa auth strict # lock it down (takes effect on next restart)
Alterar a lista de permissões é capturado ao vivo pelos nós em execução — parear um par o conecta sem reiniciar, e revogar um derruba sua conexão aberta imediatamente. Alterar o modo de autenticação requer reinicialização.
Atribuição
Cada entrada originada de par na trilha de auditoria agora registra qual par agiu, identificado pela chave pública autenticada por Noise:
### [2026-07-25 10:14:02] - [SOURCE: Peer a3f9c1b2 (sanjoy-laptop)] - [ACTION: State Update]
Os rótulos são fornecidos pelos pares e sanitizados (caracteres de controle e estrutura Markdown removidos) para que um par não possa forjar entradas de auditoria através do próprio nome. A impressão digital é a identidade; o rótulo é uma conveniência.
Rotação
- Chave de um par —
p2pa peers remove <pubkey>, depois repareie. - Sua própria chave — exclua
~/.p2pa/identity.jsone reinicie. Cada par deve reparear com sua nova chave. - O tópico —
p2pa start --topic <new>em cada máquina, ou repareie com--adopt-topic.
Documento vivo (direção via Google Docs)
Agentes sincronizam estado da máquina via P2P; humanos dirigem em um Google Doc compartilhado que qualquer pessoa com o link pode editar.
Humans edit "## HUMAN directives" → poller → Active State key `steering`
Agents call doc_publish → Status / Plan / Agent log sections
Configuração única do Google
- Crie um projeto no Google Cloud; habilite Google Docs API e Google Drive API.
- Crie uma conta de serviço, baixe sua chave JSON.
- Exporte o caminho (nunca faça commit da chave; nunca a coloque em contexto compartilhado):
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)
- Crie ou vincule um documento:
p2pa doc create --title "Auth refactor war room"
# or: p2pa doc link "https://docs.google.com/document/d/…/edit"
p2pa doc status
- Coloque
P2PA_GOOGLE_SA_JSONno seu ambiente MCP (p2pa connecto copia se já estiver definido no seu shell) e reinicie o MCP.
Seções do documento (cabeçalhos exatos):
| Seção | Quem escreve |
|---|---|
## Status | Agentes (doc_publish seção=status) |
## Plan | Agentes (doc_publish seção=plan) |
## HUMAN directives | Humanos (anexam orientação; coletado em steering) |
## Agent log | Agentes (somente anexação via doc_publish seção=agent_log) |
Os agentes continuam em execução enquanto você edita. Eles leem a orientação com doc_read_steering ou pull_context chave steering.
Opcional: P2PA_DOC_POLL_MS (padrão 4000).
Ferramentas do MCP
Uma vez conectados, os agentes podem chamar:
Estado compartilhado
| Ferramenta | O que faz |
|---|---|
push_context | Define uma chave de nível superior e a transmite |
pull_context | Lê uma chave, ou o documento compartilhado inteiro |
delete_context | Marca uma chave como removida para que uma réplica obsoleta não possa ressuscitá-la |
set_add / set_remove | Operações de conjunto add-wins, para listas às quais dois agentes anexam |
override_context | Impõe seus próprios valores quando o vencedor automático está errado por intenção |
check_conflicts | Atualizações concorrentes recentes — já resolvidas, informativas |
Dividindo o trabalho
| Ferramenta | O que faz |
|---|---|
create_task | Coloca uma unidade de trabalho no backlog compartilhado para qualquer agente qualificado |
next_task | Pergunta ao backlog por trabalho que você pode executar — e assume a concessão na mesma chamada |
complete_task | Registra o resultado, devolve o resultado, libera a concessão |
fail_task | Desiste de uma tentativa: reenfileira, coloca em dead-letter ou cancela |
list_tasks | O quadro: o que existe, o que está bloqueado, quem tem o quê |
claim_task | Assume uma concessão diretamente, para trabalho que não está no backlog |
release_task | Devolve uma tarefa antes de sua concessão expirar |
list_claims | Vê quais tarefas estão em andamento e quem as possui |
Falando com o outro agente
| Ferramenta | O que faz |
|---|---|
send_peer_message | Envia mensagem para todos os pares; enfileirado e reenviado se estiverem offline |
ask_peer | Pergunta a um agente uma questão, obtém um id de correlação para combinar a resposta |
reply_to_peer | Responde a uma pergunta que outro agente lhe fez |
await_peer_event | Bloqueia até que um par aja, então retorna o que fez |
recent_peer_events | Atualiza-se sobre a atividade dos pares sem bloquear |
outbox_status | Mensagens ainda aguardando confirmação |
Sabendo quem está no enxame
| Ferramenta | O que faz |
|---|---|
announce_self | Publica seu papel, capacidades e status para que os pares roteiem trabalho para você |
list_agents | A lista de membros: quem está aqui, o que fazem, quem está livre |
Introspecção
| Ferramenta | O que faz |
|---|---|
sync_health | Id de réplica, hash de conteúdo, contagem de pares, versão de protocolo negociada por par |
read_context_history | Lê as últimas N linhas do log markdown local |
Documento vivo (opcional, veja abaixo)
| Ferramenta | O que faz |
|---|---|
doc_publish | Envia status / plano / agent_log para o Google Doc vinculado |
doc_read_steering | Lê diretivas HUMAN (poll opcional forçado) |
doc_status | Link do documento vivo + saúde do poll (sem segredos) |
Delegando trabalho
Uma concessão é um bloqueio sobre um id de tarefa, e até v0.9 um id de tarefa não se referia a nada — dois agentes descrevendo o mesmo trabalho de maneiras diferentes cada um assumia uma concessão e ambos faziam o trabalho. O backlog é esse vocabulário ausente: o trabalho se torna um objeto com um id compartilhado, um resultado e um ciclo de vida no qual outros agentes podem esperar.
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_task seleciona e concede em uma única chamada, então não há janela em que um
agente tenha decidido fazer trabalho que não possui. Ele nunca oferece uma tarefa cujas
dependências não estejam concluídas, que exija uma capacidade que o agente não anunciou,
ou que um par já possua — e quando não há nada para você, ele diz isso, com
as contagens, em vez de retornar um erro.
Uma tarefa nunca registra quem está trabalhando nela. @task/<id> mantém o trabalho e
@claim/<id> mantém a concessão; eles compartilham um id e são unidos quando você lê o
quadro. Um campo holder na tarefa seria uma segunda resposta a uma pergunta que a
concessão já responde, e os dois discordariam na primeira vez que um detentor falhasse.
fail_task coloca o trabalho de volta em vez de perdê-lo — após três tentativas, ele é
colocado em dead-letter com o motivo no quadro. Um agente que falha no meio da tarefa simplesmente
deixa sua concessão expirar; a tarefa permanece open e é reportada ao enxame como
abandonada na próxima vez que alguém pedir trabalho.
O quadro também é escrito em ~/.p2pa/shared_context.md sob ## Backlog, para que
um humano possa ler o que o enxame está fazendo sem perguntar.
O backlog comporta 500 tarefas. As resolvidas são coletadas após sete dias, e um
quadro já cheio abre espaço descartando a tarefa resolvida há mais tempo em vez
de recusar novo trabalho — então o limite é uma profundidade de fila, não um limite de vida. As tarefas
em aberto nunca são descartadas: se todas as 500 estiverem abertas, create_task recusa e diz isso.
Reivindicando trabalho
A sincronização de estado impede que dois agentes se sobrescrevam; ela não impede que eles façam o mesmo trabalho duas vezes. Uma concessão corrige isso:
Agent A: claim_task("refactor-auth") → holds it until 14:32
Agent B: claim_task("refactor-auth") → already held by a3f9c1b2, picks another task
- Exatamente um detentor. Dois agentes correndo pela mesma tarefa convergem para um vencedor, em qualquer ordem de entrega, sem se perguntar um ao outro.
- Quem chega primeiro, é atendido primeiro. Dentro de uma geração de concessão, a primeira reivindicação vence, então um agente honesto não pode tomar uma tarefa simplesmente escrevendo novamente.
- As concessões expiram. Um agente que falhou para de bloquear a tarefa quando seu TTL expira; quem reivindica a seguir assume a próxima geração, então uma operação obsoleta da concessão morta nunca pode reinstaurá-la.
- A liberação é final. Devolver uma tarefa não pode ser desfeito por uma reivindicação que ainda estava em andamento.
claim_task espera uma janela de propagação antes de responder, então um agente nunca é
informado de que possui trabalho que já perdeu.
Duas limitações honestas:
- Dois nós particionados podem ambos acreditar que possuem a mesma concessão até poderem conversar novamente. Nenhum protocolo sem quorum pode evitar isso, e o P2PA não tem quorum por design. A concessão reduz o trabalho duplicado ao atraso de propagação — não é um mutex distribuído.
- Uma concessão protege contra corridas, não contra um par hostil. Um par
na lista de permissões pode dar um lance de geração mais alta e tomar uma concessão ativa, assim como pode
sobrescrever qualquer chave de estado. Concessões deslocadas aparecem em
check_conflictse no trilha de auditoria. Associe-se a pares em quem você confia.
Deixando recado para um par offline
As mensagens costumavam ir direto para os sockets abertos, então qualquer coisa escrita enquanto o outro agente estava dormindo, reiniciando ou em um trem era simplesmente perdida.
Uma mensagem agora é enfileirada primeiro e enviada depois:
Agent A: send_peer_message("auth refactor is done") → nobody online, queued
… Agent B starts up …
Agent B: ← receives it automatically on connect
- Enfileirada antes do envio, para que um socket que caia no meio da transmissão não perca nada.
- Reenviada até ser confirmada. Uma mensagem só é descartada quando o destinatário a reconhece, então "escrita no socket" nunca é confundida com "recebida".
- Entregue exatamente uma vez, até onde o agente pode saber. Repetição é no mínimo uma vez; o receptor deduplica pelo id da mensagem, então uma repetição não é registrada ou exibida duas vezes.
- Sobrevive a um reinício de qualquer lado — a fila e os ids já vistos estão no disco.
Limitada, já que um par que nunca retorna não deve crescer o arquivo para sempre: 500
mensagens, desistidas após 7 dias, repetidas 100 por vez. Qualquer coisa abandonada é
contabilizada em outbox_status em vez de desaparecer silenciosamente.
Uma mensagem é endereçada aos pares com os quais você estava emparelhado quando a enviou, e repetida apenas para pares com os quais você realmente se emparelhou — um nó que se junta depois não recebe a conversa anterior.
Esperando o outro agente
Todas as outras ferramentas são apenas pull, o que significa que um agente aprende que um par fez algo apenas se acontecer de chamar uma — e um LLM não faz isso sem ser estimulado. Então um agente fala e o outro nunca ouve.
Agent B: await_peer_event() → blocks
Agent A: claim_task("refactor-auth")
Agent B: ← wakes with { kind: "claim", taskId: "refactor-auth", … }
Eventos carregam um seq. Passe o maior que você viu de volta como since_seq e
nada é perdido entre chamadas, mesmo se você estivesse ocupado quando aconteceu. Um
timeout retorna uma lista vazia em vez de um erro — "nada aconteceu" é uma
resposta ordinária.
Clientes que suportam assinaturas de recursos do MCP podem, em vez disso, observar
p2pa://events e ser notificados a cada ação de par, sem estacionar uma chamada de ferramenta.
Trabalhando como um enxame
Com mais de dois agentes, "quem deveria fazer isso?" importa tanto quanto "alguém já fez isso?". Cada agente publica um cartão dizendo para que serve:
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"])
Qualquer agente pode então ler a lista de membros e rotear trabalho:
Agent A: list_agents(capability="typescript", idle_only=true)
→ [{ nodeId: "b4f9…", role: "builder", status: "idle", live: true }]
Agent A: ask_peer(node_id="b4f9…", question="can you take refactor-auth?")
→ { corr: "7c1d94a2ef0b3355" }
Agent B: ← await_peer_event() wakes with { kind:"message", intent:"ask", corr:"7c1d94a2ef0b3355", from:"b4f9…" }
Agent B: reply_to_peer(to="…", corr="7c1d94a2ef0b3355", answer="taking it now")
ask_peer é endereçado: ele cai somente no feed daquele agente, então uma pergunta
destinada ao revisor não interrompe todos os outros. Respostas carregam o mesmo
corr, então um agente lidando com várias threads abertas sabe qual resposta pertence a
qual pergunta. Ambos são enfileirados se o destinatário estiver offline.
Um cartão é válido somente na única posição que seu autor possui (@agent/<nodeId>), então nenhum
par pode anunciar em nome de outro — a mesma regra que protege concessões.
A vivacidade vem do timestamp do próprio cartão: re-anuncie a cada 30s ou mais, e um
agente que para é reportado live: false em vez de permanecer como disponível.
Como o merge funciona
- Cada escrita local carimba sua chave com um relógio lógico híbrido: tempo de parede, um contador e o id deste nó.
- Pares mesclam cada chave independentemente. Escritas em chaves diferentes sempre mesclam — não há versão de documento inteira para se contender.
- Escritas na mesma chave resolvem por ordem de carimbo. A comparação é total e idêntica em cada réplica, então todos os pares escolhem o mesmo vencedor sem conversar entre si.
- A escrita perdedora é registrada sob
## Concurrent Updatese exposta porcheck_conflicts. Nada é bloqueado esperando por ela. - Se o vencedor automático está errado por intenção,
override_contextescreve o valor que você deseja; ele supera o carimbo do que substitui e propaga normalmente.
Carimbos são limitados: uma entrada que reivindica um relógio mais de 24h à frente do tempo local é recusada e registrada, então um par não pode saturar o relógio de uma réplica ou usar um snapshot de handshake para sobrescrever estado que nunca possuiu.
Cada atualização, handshake de snapshot, mensagem de par e recusa é escrito em um arquivo markdown legível por humanos em ~/.p2pa/shared_context.md.
O arquivo tem cinco seções:
- Estado Ativo — JSON compartilhado atual, simples e legível por humanos
- Estado da Réplica — o mesmo documento mais carimbos por chave (gerenciado por máquina)
- Reivindicações — quais tarefas estão em andamento e quem as possui (omitido quando vazio)
- Atualizações Concorrentes — contenção resolvida recente (omitido quando vazio)
- Trilha de Auditoria — histórico recente de atualizações, mensagens, reivindicações e recusas
A trilha de auditoria é limitada para que o arquivo ativo permaneça pequeno e cada escrita permaneça
barata — o documento inteiro é re-renderizado a cada mutação, então um histórico
ilimitado deixaria o nó cada vez mais lento sem motivo visível. Entradas mais antigas
rolam para shared_context.archive.md em vez de serem descartadas, para que
o registro permaneça completo.
Acompanhe durante o desenvolvimento:
p2pa log
Notas de segurança
- No modo
strict(o padrão), a lista de permissões de pares é a fronteira do controle de acesso — um vazamento de tópico sozinho não concede mais acesso. Veja Autenticação de pares. - O tópico ainda é material de descoberta, não um segredo — ele é anunciado no DHT público. Prefira tópicos longos e aleatórios (códigos gerados automaticamente têm 22 caracteres) e evite
--topicna linha de comando, onde ele fica no histórico do shell e na saída dops. Usep2pa pairouP2PA_TOPICem vez disso. - No modo
opennão há autenticação alguma: qualquer pessoa que souber o tópico pode ler e escrever seu estado compartilhado. Use-o apenas para manter um pareamento pré-0.7 ativo enquanto você migra. identity.jsoné o material de chave privada do seu nó. Nunca o copie entre máquinas — dois nós compartilhando um par de chaves não podem ser distinguidos nem revogados de forma independente. Ele também é a chave de assinatura para toda operação que este nó autora.- Em um enxame de três ou mais, ative a verificação de assinaturas. Um snapshot de handshake legitima operações autoradas por outros pares, então a autenticação hop a hop não pode atestar o conteúdo delas. Assinaturas resolvem isso: uma entrada permanece verificável após qualquer número de relays. A verificação está desligada por padrão apenas porque exclui pares v3. Veja SPEC.md §6.
- Texto fornecido por pares são dados, nunca instruções. Corpos de mensagens, papéis de agentes, capacidades e notas são todos escritos pelo par. Agentes devem tratá-los como alegações a avaliar, não comandos a seguir.
- O link do Google Doc também é uma capacidade — com "qualquer pessoa com o link = editor", qualquer um que tiver a URL pode direcionar agentes por meio das diretivas HUMAN. Rode girando criando um novo doc +
p2pa doc unlink. - O JSON da conta de serviço (
P2PA_GOOGLE_SA_JSON) deve permanecer no disco / no ambiente MCP apenas — nunca no Active State, no Doc ou em patches P2P. - Um par na lista de permissões é confiável. Ele pode escrever em qualquer chave de estado, tomar posse de uma concessão que você detém e ler tudo o que você sincroniza. A lista de permissões é a fronteira — pareie com pessoas, não com tópicos que você encontrou em algum lugar.
- O backlog é o único namespace que não é vinculado ao proprietário. Qualquer par na lista de permissões pode criar, concluir, reenfileirar ou cancelar qualquer tarefa — é isso que um backlog compartilhado é, e um par que não pudesse concluir trabalho que não criou não poderia ser delegado. Portanto, um par na lista de permissões pode marcar seu trabalho como
donecom umresultfabricado, ou comocancelledpara que ninguém o pegue. Isso amplia o que um par dentro da lista de permissões pode fazer, não quem está nela.createdByé escolhido pelo par e não é uma identidade; a assinatura na operação é. A junção é monotônica, então a direção destrutiva é unilateral: um par pode encerrar uma tarefa, mas não reabri-la silenciosamente. - Títulos, detalhes e resultados de tarefas são instruções escritas por pares. Este é o caso de maior risco de "texto de par é dado": o payload literalmente é uma instrução — de outro agente, não do seu operador. Cada resultado de ferramenta que carrega uma tarefa diz isso, e toda string que chega ao quadro Markdown é sanitizada para que um título não possa forjar uma linha de tabela ou um cabeçalho de seção.
outbox.jsonarmazena o texto das mensagens no disco (0600, dentro do diretório de configuração 0700). O histórico de mensagens só é reproduzido para pares com os quais você pareou.- Pares fazem relay uns para os outros, dentro do snapshot de handshake. Toda operação é assinada pelo seu autor, então uma escrita com relay permanece atribuível. A verificação (
p2pa auth require-signatures) está desligada por padrão apenas porque um par com protocolo v3 não pode assinar — ative-a assim que todos os nós rodarem 0.8+, e certamente antes de executar um enxame de três ou mais. - O diretório de configuração tem como padrão
0700;config.jsoné escrito como0600. - Não coloque credenciais ou segredos de produção no contexto compartilhado.
- Os logs do daemon em segundo plano vão para
daemon-error.logpara que o stdout do MCP permaneça um fluxo JSON-RPC limpo.
Desenvolvimento
git clone <your-repo-url>
cd P2PA
npm install
npm run build
npm test # unit + integration + conformance, fully offline
npm run smoke # Hyperswarm two-node sync (needs internet)
npm run smoke:merge # concurrent merge, same-key resolution, set adds
npm run smoke:claim # two agents racing the same backlog
npm run smoke:outbox # a message left for an offline peer
npm run smoke:doc # living-doc bridge (mock Google Docs, no keys)
npm test roda inteiramente offline — os testes de integração de autenticação de pares iniciam uma testnet hyperdht em processo, então eles exercitam o firewall real contra conexões reais sem tocar no DHT público.
npm run typecheck # tsc over src, scripts and test
npm run build # compile to dist/
Ponto de entrada do pacote: p2pa → dist/cli.js.
Layout
| Caminho | O que vive lá |
|---|---|
src/crdt.ts, src/hlc.ts | Motor de junção: registros por chave, relógios lógicos híbridos |
src/claim.ts | Concessões de trabalho |
src/events.ts | Barramento de atividade de pares por trás de await_peer_event |
src/outbox.ts | Mensagens duráveis |
src/sync.ts | Amarra o acima ao transporte e ao trilho de auditoria |
src/p2p.ts | Transporte Hyperswarm, firewall de pares |
src/mcp-server.ts | A superfície de ferramentas voltada ao agente |
src/markdown-log.ts | O arquivo legível por humanos |
Contribuições são bem-vindas. A suíte de testes é a especificação — todo comportamento acima tem um teste que falha se você remover a proteção que o fornece.
Ideias de roadmap
- Adaptadores para Notion e outros documentos vivos
- Transporte HTTP Streamable opcional junto com stdio
- Sincronização delta por chave (pares já pulam o snapshot inteiro quando seus digests coincidem, mas uma incompatibilidade parcial ainda envia a réplica inteira)
- Tornar a verificação de assinaturas o padrão, quando pares com protocolo v3 forem raros o suficiente para que excluí-los não custe nada
Licença
Construído para a próxima geração de desenvolvimento multi-agente.