P2PA
Los agentes paralelos coordinan reclamos y sincronizan el estado automáticamente, sin fusión manual ni pérdida de datos.
Documentación
P2PA
Sincronización de contexto peer-to-peer sin interfaz, para agentes de IA locales.
Deja de copiar y pegar prompts entre agentes. P2PA es un kit de herramientas MCP local-first que permite que múltiples agentes LLM (Cursor, Claude Code, Claude Desktop o scripts personalizados) compartan contexto estructurado, pasen mensajes y fusionen ediciones concurrentes a través de una red P2P sin servidor.
Creado para fundadores y equipos de ingeniería que quieren IA multijugador sin salir de su IDE.
El problema
La colaboración multi-agente sigue rota en tres aspectos:
- Silos efímeros — Cuando un agente local termina una tarea difícil, su memoria de trabajo muere. El agente de tu compañero empieza desde cero.
- Inflado de tokens — Muchas configuraciones "multi-agente" transportan ventanas de contexto completas a través de una nube central, quemando tokens y añadiendo latencia.
- Jardines amurallados — La colaboración a menudo significa dejar la terminal por un panel propietario.
La solución
P2PA mantiene un buffer de estado JSON compartido en cada máquina y sincroniza solo los diffs:
- Hyperswarm — Descubrimiento DHT + traversal NAT. Sin servidor central.
- Autenticación de pares basada en claves — Solo las claves ed25519 en la lista blanca pueden conectarse. Sin acceso entrante desde un tema filtrado.
- Fusión CRDT por clave — Cada clave lleva su propio reloj lógico híbrido. Dos agentes escribiendo claves diferentes nunca entran en conflicto; dos agentes escribiendo la misma clave resuelven al mismo ganador en cada réplica, sin paso de arbitraje.
- Conjuntos add-wins — Las adiciones concurrentes a una lista sobreviven todas, en lugar de que las entradas de un agente sobrescriban las del otro.
- Arrendamientos de reclamo de trabajo — Un agente reclama una tarea antes de comenzarla, para que dos agentes conectados no dupliquen trabajo. Los arrendamientos expiran solos, por lo que un agente bloqueado no puede bloquear el backlog.
- Roster de agentes — Cada agente publica su rol, capacidades y estado, para que un enjambre pueda enrutar trabajo a quien esté realmente libre en lugar de adivinar.
- Mensajes direccionados — Pregunta a un agente específico y empareja su respuesta por id de correlación, en lugar de transmitir a todos.
- Operaciones firmadas — Cada escritura está firmada por la clave de su autor, por lo que una entrada sigue siendo atribuible después de que cualquier número de pares la retransmita. Sin esto, un enjambre de tres permite que un par fabrique escrituras de otro.
- Protocolo negociado — Los pares acuerdan una versión y un conjunto de capacidades al conectarse, para que un enjambre de versiones mixtas siga funcionando y uno incompatible explique por qué en lugar de no sincronizarse silenciosamente.
- Basado en eventos, no en polling — Un agente puede bloquearse hasta que el otro realmente haga algo, en lugar de esperar que recuerde comprobar.
- Los mensajes sobreviven a una desconexión — Escribe a un par cuyo agente está offline y se entrega cuando regresa. Nadie tiene que reenviar.
- Registro de auditoría legible por humanos — Cada cambio aterriza en
~/.p2pa/shared_context.md, atribuido al par que lo hizo.
El protocolo de red está especificado en SPEC.md — gramática de tramas, reglas de fusión, canonicalización de firmas, límites y vectores de conformidad — para que P2PA pueda implementarse en otro lenguaje e interoperar.
Arquitectura
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
Dos modos de proceso (mismo estado en disco):
| Modo | Comando | Úsalo cuando |
|---|---|---|
| MCP (primer plano) | p2pa mcp | Conectando Cursor / Claude — posee stdio limpio |
| Daemon (fondo) | p2pa start | Sincronización Hyperswarm opcional vía PM2 (sin MCP stdio) |
Ejecuta uno, no ambos. Ambos modos escriben los mismos archivos, por lo que P2PA toma un bloqueo de escritura al inicio y el segundo sale con una explicación en lugar de sobrescribir silenciosamente al primero. Usa p2pa mcp cuando un agente IDE está conduciendo; usa el daemon cuando quieras sincronización en segundo plano sin uno.
Pruébalo en dos minutos (una máquina)
Sin emparejamiento, sin segunda computadora — esto solo demuestra que el motor de fusión y los arrendamientos de trabajo hacen lo que afirman:
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 no necesita red: las pruebas de autenticación de pares ejecutan una testnet real de hyperdht en proceso.
Ejecutándolo de verdad (dos máquinas)
1. Instalación
npm install -g p2pa
O desde un clon:
npm install && npm run build && npm link
Requiere Node.js 18+. Todo vive en ~/.p2pa/.
2. Empareja las dos máquinas
El emparejamiento es mutuo y basado en claves. Cada lado pone en lista blanca la clave pública del otro, y solo las claves en lista blanca pueden conectarse — conocer el tema no es suficiente.
En la máquina A:
p2pa pair # prints an invite token
Envía ese token a B por un canal que ya confíes (lleva el tema de emparejamiento, así que trátalo como una contraseña).
En la máquina B:
p2pa pair <A's token> # allowlists A, adopts A's topic, prints B's token
De vuelta en A:
p2pa pair <B's token> # allowlists B — pairing complete
p2pa peers # confirm both directions
Luego asegúralo (esto es el predeterminado para nuevas instalaciones, pero verifica):
p2pa auth strict
3. Apunta tu agente a ello
p2pa connect
Eso imprime un bloque de servidor MCP listo. Pégalo en:
- Claude Code —
claude mcp add p2pa -- p2pa mcp, o el JSON impreso en.mcp.json - Cursor — Configuración → MCP
- Claude Desktop —
claude_desktop_config.json
La configuración ejecuta p2pa mcp sobre stdio, por lo que las herramientas aparecen dentro del agente.
4. Elige un proceso, no dos
| Quieres | Ejecuta | Notas |
|---|---|---|
| Un agente IDE conduciéndolo | p2pa mcp (vía la configuración MCP anterior) | Iniciado por el cliente |
| Sincronización en segundo plano, sin IDE | p2pa start | Daemon PM2 |
Ambos escriben los mismos archivos, por lo que P2PA toma un bloqueo de escritura al inicio: el que comience segundo sale con una explicación en lugar de sobrescribir silenciosamente al primero. Si recibes ese mensaje, p2pa stop el daemon y reintenta.
5. Obsérvalo 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
Cómo se ve una sesión real
Dos desarrolladores, dos máquinas, un backlog. Nada aquí es contabilidad manual — los agentes lo hacen a través de las herramientas.
Agente A toma trabajo y lo dice:
claim_task("refactor-auth", note: "splitting the token module")
push_context("status", "auth refactor started")
Agente B, en la otra máquina, verifica antes de comenzar algo:
list_claims() → refactor-auth is held by a3f9c1b2
claim_task("write-tests") → granted, different task
Agente B termina y se pone inactivo, en lugar de hacer polling:
release_task("write-tests")
await_peer_event() → blocks…
← { kind: "claim", taskId: "update-docs", peer: "a3f9c1b2" }
Si la máquina de B está dormida cuando A envía un mensaje, A no necesita reenviar — el mensaje se pone en cola y se entrega cuando B regresa.
Todo lo anterior también se escribe en ~/.p2pa/shared_context.md en Markdown plano, atribuido al par que lo hizo, para que un humano pueda leer toda la sesión sin ninguna herramienta.
Referencia CLI
| Comando | Descripción |
|---|---|
p2pa start [--topic <code>] | Iniciar daemon Hyperswarm en segundo plano (PM2) |
p2pa stop | Detener el daemon |
p2pa status | Estado del daemon, identidad, huella del tema, modo de autenticación |
p2pa log | Seguir ~/.p2pa/shared_context.md |
p2pa connect | Imprimir JSON MCP para Cursor / Claude Desktop |
p2pa mcp | Ejecutar MCP en primer plano + servidor P2P (stdio) |
p2pa pair [--label <name>] | Imprimir tu token de invitación |
p2pa pair <token> [--adopt-topic] | Poner en lista blanca un par desde su token |
p2pa peers | Listar pares en lista blanca |
p2pa peers remove <pubkey|label> | Revocar un par |
p2pa auth <strict|open> | Establecer la política de conexión |
p2pa auth require-signatures | Rechazar operaciones retransmitidas que no estén firmadas (recomendado para 3+ pares) |
p2pa doc create [--title] | Crear una sala de guerra de Google Doc + edición con enlace |
p2pa doc link <url> | Vincular un Google Doc existente |
p2pa doc unlink | Limpiar el vínculo del doc |
p2pa doc status | Mostrar doc vinculado + si las credenciales SA están establecidas |
La configuración y el estado viven bajo ~/.p2pa/ (modo 0700):
| Ruta | Propósito |
|---|---|
config.json | Tema de emparejamiento, modo de autenticación, lista blanca de pares, vínculo de doc opcional (0600) |
identity.json | Semilla de identidad de 32 bytes de este nodo (0600) — nunca compartir |
shared_context.md | Estado Activo + Estado Réplica + Reclamos + Actualizaciones Concurrentes + Rastro de Auditoría |
shared_context.archive.md | Entradas de auditoría antiguas, rotadas del archivo vivo |
outbox.json | Mensajes esperando confirmación (0600) |
state-writer.lock | Retenido por el proceso que esté escribiendo |
daemon-error.log | Diagnósticos del daemon (no mezclados en stdout de MCP) |
Anula el directorio de configuración con P2PA_CONFIG_DIR (debe permanecer bajo tu directorio de inicio).
Autenticación de pares
Por qué el tema solo no es suficiente
Un tema de Hyperswarm es un identificador de descubrimiento, no un secreto. sha256(topic) es la clave DHT bajo la que tu nodo se anuncia, y los nodos DHT más cercanos a esa clave en el espacio de claves necesariamente la aprenden. Cualquiera que la obtenga — por estar en ese vecindario, o porque el tema se filtró de un historial de shell, un listado de ps o un registro de chat — podría conectarse y obtener lectura/escritura completa en tu contexto compartido.
P2PA ahora trata el tema como solo descubrimiento y autentica pares por clave pública.
Cómo funciona
Cada instalación genera un par de claves ed25519 estable en la primera ejecución, derivado de una semilla de 32 bytes en ~/.p2pa/identity.json (0600). Esa clave pública es la dirección permanente de tu nodo en el enjambre.
El handshake Noise de Hyperswarm ya demuestra que un par posee la clave secreta para la clave pública que presenta. P2PA engancha el callback de firewall — que se ejecuta en intentos de conexión tanto entrantes como salientes — y rechaza cualquier clave que no esté en tu lista blanca. Un par no autorizado se elimina antes de que se intercambie un solo byte de datos de aplicación, por lo que no puede leer tu estado vía la instantánea del handshake ni escribirlo vía un parche.
Eso cubre cada salto. No cubre la retransmisión: una instantánea de handshake lleva operaciones escritas por otros pares — así es como un nodo que se une aprende lo que todos los demás han hecho — por lo que "el remitente demostró quién es" no dice nada sobre quién escribió las entradas dentro. Con dos nodos eso no cuesta nada; con tres o más permite que un par fabrique escrituras de otro. Cada operación está por lo tanto firmada por la clave de su autor, y sigue siendo verificable sin importar cuántos pares la retransmitan. Ver SPEC.md §6, y activa la aplicación con
p2pa auth require-signaturesuna vez que cada nodo ejecute 0.8+.
Modos de autenticación
| Modo | Comportamiento |
|---|---|
strict | Solo las claves públicas en lista blanca pueden conectarse. Predeterminado para nuevas instalaciones. |
open | Cualquiera que conozca el tema puede conectarse (comportamiento pre-0.7). |
Actualizar no cortará un emparejamiento funcional: una configuración escrita antes de esta característica no tiene campo auth y resuelve a open, con una advertencia en cada inicio hasta que ejecutes p2pa auth strict.
p2pa peers # who can connect, and in which mode
p2pa auth strict # lock it down (takes effect on next restart)
Cambiar la lista blanca se recoge en vivo por los nodos en ejecución — emparejar un par lo conecta sin reiniciar, y revocar uno corta su conexión abierta inmediatamente. Cambiar el modo de autenticación requiere un reinicio.
Atribución
Cada entrada de origen par en el rastro de auditoría ahora registra qué par actuó, clave por la clave pública autenticada por Noise:
### [2026-07-25 10:14:02] - [SOURCE: Peer a3f9c1b2 (sanjoy-laptop)] - [ACTION: State Update]
Las etiquetas son proporcionadas por el par y saneadas (caracteres de control y estructura Markdown eliminados) para que un par no pueda forjar entradas de auditoría a través de su propio nombre. La huella es la identidad; la etiqueta es una conveniencia.
Rotación
- La clave de un par —
p2pa peers remove <pubkey>, luego re-empareja. - Tu propia clave — elimina
~/.p2pa/identity.jsony reinicia. Cada par debe re-emparejarse con tu nueva clave. - El tema —
p2pa start --topic <new>en cada máquina, o re-empareja con--adopt-topic.
Documento vivo (dirección de Google Docs)
Los agentes sincronizan el estado de la máquina sobre P2P; los humanos dirigen en un Google Doc compartido que cualquiera con el enlace puede editar.
Humans edit "## HUMAN directives" → poller → Active State key `steering`
Agents call doc_publish → Status / Plan / Agent log sections
Configuración única de Google
- Crea un proyecto de Google Cloud; habilita Google Docs API y Google Drive API.
- Crea una cuenta de servicio, descarga su clave JSON.
- Exporta la ruta (nunca comprometas la clave; nunca la pongas en contexto compartido):
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)
- Crea o vincula un doc:
p2pa doc create --title "Auth refactor war room"
# or: p2pa doc link "https://docs.google.com/document/d/…/edit"
p2pa doc status
- Pon
P2PA_GOOGLE_SA_JSONen tu entorno MCP (p2pa connectlo copia si ya está establecido en tu shell), luego reinicia MCP.
Secciones del doc (encabezados exactos):
| Sección | Quién escribe |
|---|---|
## Status | Agentes (doc_publish sección=status) |
## Plan | Agentes (doc_publish sección=plan) |
## HUMAN directives | Humanos (añaden dirección; se consulta en steering) |
## Agent log | Agentes (solo añadir vía doc_publish sección=agent_log) |
Los agentes siguen ejecutándose mientras editas. Leen la dirección con doc_read_steering o pull_context clave steering.
Opcional: P2PA_DOC_POLL_MS (por defecto 4000).
Herramientas MCP
Una vez conectados, los agentes pueden llamar:
Estado compartido
| Herramienta | Qué hace |
|---|---|
push_context | Establece una clave de nivel superior y la difunde |
pull_context | Lee una clave, o todo el documento compartido |
delete_context | Marca una clave como eliminada para que una réplica obsoleta no pueda resucitarla |
set_add / set_remove | Operaciones de conjunto con adición ganadora, para listas a las que dos agentes añaden |
override_context | Impone tus propios valores cuando el ganador automático es incorrecto por intención |
check_conflicts | Actualizaciones concurrentes recientes — ya resueltas, informativo |
Dividir el trabajo
| Herramienta | Qué hace |
|---|---|
create_task | Pone una unidad de trabajo en el backlog compartido para cualquier agente cualificado |
next_task | Pide trabajo al backlog que puedas ejecutar — y toma el arrendamiento en la misma llamada |
complete_task | Registra el resultado, devuelve el resultado, libera el arrendamiento |
fail_task | Abandona un intento: lo reencola, lo envía a carta muerta o lo cancela |
list_tasks | El tablero: qué existe, qué está bloqueado, quién tiene qué |
claim_task | Toma un arrendamiento directamente, para trabajo que no está en el backlog |
release_task | Devuelve una tarea antes de que expire su arrendamiento |
list_claims | Ve qué tareas están en curso y quién las tiene |
Hablar con el otro agente
| Herramienta | Qué hace |
|---|---|
send_peer_message | Envía un mensaje a todos los pares; se pone en cola y se reintenta si están desconectados |
ask_peer | Pregunta a un agente, obtén un id de correlación para emparejar la respuesta |
reply_to_peer | Responde a una pregunta que otro agente te hizo |
await_peer_event | Bloquea hasta que un par actúa, luego devuelve lo que hizo |
recent_peer_events | Ponte al día de la actividad de los pares sin bloquear |
outbox_status | Mensajes aún pendientes de confirmación |
Saber quién está en el enjambre
| Herramienta | Qué hace |
|---|---|
announce_self | Publica tu rol, capacidades y estado para que los pares te enruten trabajo |
list_agents | El registro: quién está aquí, qué hace, quién está libre |
Introspección
| Herramienta | Qué hace |
|---|---|
sync_health | Id de réplica, hash de contenido, número de pares, versión de protocolo negociada por par |
read_context_history | Lee las últimas N líneas del registro markdown local |
Documento vivo (opcional, ver abajo)
| Herramienta | Qué hace |
|---|---|
doc_publish | Empuja estado / plan / agent_log al Google Doc vinculado |
doc_read_steering | Lee directivas HUMAN (sondeo opcional forzado) |
doc_status | Enlace del documento vivo + salud del sondeo (sin secretos) |
Delegar trabajo
Un arrendamiento es un bloqueo sobre un id de tarea, y hasta v0.9 un id de tarea no refería a nada — dos agentes que describen el mismo trabajo de forma diferente tomaban cada uno un arrendamiento y ambos hacían el trabajo. El backlog es ese vocabulario que faltaba: el trabajo se convierte en un objeto con un id compartido, un resultado y un ciclo de vida en el que otros agentes pueden 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 selecciona y arrienda en una sola llamada, así que no hay ventana en la que un
agente haya decidido hacer trabajo que no tiene. Nunca ofrece una tarea cuyas
dependencias estén sin terminar, una que requiera una capacidad que el agente no haya anunciado,
o una que un par ya tenga — y cuando no hay nada para ti lo dice, con
los recuentos, en lugar de devolver un error.
Una tarea nunca registra quién está trabajando en ella. @task/<id> contiene el trabajo y
@claim/<id> contiene el arrendamiento; comparten un id y se unen cuando lees el
tablero. Un campo holder en la tarea sería una segunda respuesta a una pregunta que el
arrendamiento ya responde, y los dos discreparían la primera vez que un titular se estrellara.
fail_task devuelve el trabajo en lugar de perderlo — después de tres intentos se envía a
carta muerta con el motivo en el tablero. Un agente que se estrella a mitad de tarea simplemente
deja que su arrendamiento expire; la tarea permanece open y se informa al enjambre como
abandonada la próxima vez que alguien pida trabajo.
El tablero también se escribe en ~/.p2pa/shared_context.md bajo ## Backlog, así
un humano puede leer lo que el enjambre está haciendo sin preguntarle.
El backlog contiene 500 tareas. Las resueltas se recogen después de siete días, y un
tablero que ya está lleno hace sitio eliminando la tarea resuelta más antigua en lugar
de rechazar trabajo nuevo — así que el límite es una profundidad de cola, no un límite de vida. Las tareas
abiertas nunca se eliminan: si las 500 están abiertas, create_task se niega y lo dice.
Reclamar trabajo
La sincronización de estado evita que dos agentes se sobrescriban; no evita que hagan el mismo trabajo dos veces. Un arrendamiento arregla eso:
Agent A: claim_task("refactor-auth") → holds it until 14:32
Agent B: claim_task("refactor-auth") → already held by a3f9c1b2, picks another task
- Exactamente un titular. Dos agentes compitiendo por la misma tarea convergen en un ganador, en cualquier orden de entrega, sin preguntarse entre sí.
- Por orden de llegada. Dentro de una generación de arrendamiento, la reclamación más temprana gana, así que un agente honesto no puede tomar una tarea simplemente escribiendo de nuevo.
- Los arrendamientos expiran. Un agente estrellado deja de bloquear la tarea una vez que su TTL se agota; quien reclame a continuación toma la siguiente generación, así que una operación obsoleta del arrendamiento muerto nunca puede reinstaurarla.
- La liberación es definitiva. Devolver una tarea no puede deshacerse por una reclamación que aún estaba en vuelo.
claim_task espera una ventana de propagación antes de responder, así que un agente nunca
recibe la indicación de que posee trabajo que ya ha perdido.
Dos limitaciones honestas:
- Dos nodos particionados pueden creer ambos que tienen el mismo arrendamiento hasta que puedan hablar de nuevo. Ningún protocolo sin quórum puede evitar eso, y P2PA no tiene quórum por diseño. El arrendamiento reduce el trabajo duplicado al retardo de propagación — no es un mutex distribuido.
- Un arrendamiento protege contra carreras, no contra un par hostil. Un par en
lista blanca puede pujar una generación más alta y tomar un arrendamiento vivo, igual que puede
sobrescribir cualquier clave de estado. Los arrendamientos desplazados aparecen en
check_conflictsy en el rastro de auditoría. Empareja con pares en los que confíes.
Dejar un mensaje para un par desconectado
Los mensajes solían ir directamente a los sockets abiertos, así que cualquier cosa escrita mientras el otro agente dormía, se reiniciaba o estaba en un tren simplemente se perdía.
Un mensaje ahora se pone en cola primero y se envía después:
Agent A: send_peer_message("auth refactor is done") → nobody online, queued
… Agent B starts up …
Agent B: ← receives it automatically on connect
- En cola antes de enviar, así que un socket que se cae a mitad de vuelo no pierde nada.
- Reintentado hasta confirmación. Un mensaje solo se elimina cuando el destinatario lo reconoce, así que "escrito en el socket" nunca se confunde con "recibido".
- Entregado exactamente una vez en la medida que el agente puede saber. La reproducción es al menos una vez; el receptor deduplica por id de mensaje, así que una reproducción no se registra ni se muestra dos veces.
- Sobrevive a un reinicio de cualquiera de los lados — la cola y los ids vistos están en disco.
Acotado, ya que un par que nunca vuelve no debe hacer crecer el archivo para siempre: 500
mensajes, abandonados después de 7 días, reproducidos de 100 en 100. Cualquier cosa abandonada se
cuenta en outbox_status en lugar de desaparecer silenciosamente.
Un mensaje se dirige a los pares con los que estabas emparejado cuando lo enviaste, y se reproduce solo a pares con los que realmente te has emparejado — un nodo que se une más tarde no recibe la conversación anterior.
Esperar al otro agente
Toda otra herramienta es solo de extracción, lo que significa que un agente aprende que un par hizo algo solo si casualmente llama a una — y un LLM no hace eso sin que se le pida. Así que un agente habla y el otro nunca lo oye.
Agent B: await_peer_event() → blocks
Agent A: claim_task("refactor-auth")
Agent B: ← wakes with { kind: "claim", taskId: "refactor-auth", … }
Los eventos llevan un seq. Pasa el más alto que hayas visto de vuelta como since_seq y
nada se pierde entre llamadas, incluso si estabas ocupado cuando ocurrió. Un
tiempo de espera devuelve una lista vacía en lugar de un error — "no pasó nada" es una
respuesta ordinaria.
Los clientes que soportan suscripciones de recursos MCP pueden en su lugar observar
p2pa://events y recibir un aviso en cada acción de par, sin aparcar una llamada de herramienta.
Trabajar como enjambre
Con más de dos agentes, "¿quién debería hacer esto?" importa tanto como "¿alguien ya lo ha hecho?". Cada agente publica una tarjeta diciendo para qué sirve:
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"])
Cualquier agente puede entonces leer el registro y enrutar trabajo:
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 está dirigido: aterriza solo en el feed de ese agente, así que una pregunta
destinada al revisor no interrumpe a todos los demás. Las respuestas llevan el mismo
corr, así que un agente que maneja varios hilos abiertos sabe qué respuesta pertenece a
qué pregunta. Ambos se ponen en cola si el destinatario está desconectado.
Una tarjeta solo es válida en el único hueco que su autor posee (@agent/<nodeId>), así que ningún
par puede anunciar en nombre de otro — la misma regla que protege los arrendamientos.
La vivacidad viene de la propia marca de tiempo de la tarjeta: vuelve a anunciar cada 30s más o menos, y un
agente que se detiene se informa como live: false en lugar de permanecer como disponible.
Cómo funciona la fusión
- Cada escritura local sella su clave con un reloj lógico híbrido: tiempo de pared, un contador y el id de este nodo.
- Los pares fusionan cada clave de forma independiente. Las escrituras a claves diferentes siempre se fusionan — no hay versión a nivel de documento por la que contender.
- Las escrituras a la misma clave se resuelven por orden de sello. La comparación es total e idéntica en cada réplica, así que todos los pares eligen el mismo ganador sin hablar entre sí.
- La escritura perdedora se registra bajo
## Concurrent Updatesy se muestra mediantecheck_conflicts. Nada se bloquea esperándola. - Si el ganador automático es incorrecto por intención,
override_contextescribe el valor que quieres; supera en sello a lo que reemplaza y se propaga normalmente.
Los sellos están acotados: una entrada que reclama un reloj más de 24h por delante de la hora local se rechaza y se registra, así que un par no puede saturar el reloj de una réplica ni usar una instantánea de apretón de manos para sobrescribir estado que nunca tuvo.
Cada actualización, apretón de manos de instantánea, mensaje de par y rechazo se escribe en un archivo markdown legible por humanos en ~/.p2pa/shared_context.md.
El archivo tiene cinco secciones:
- Estado Activo — JSON compartido actual, simple y legible por humanos
- Estado de Réplica — el mismo documento más sellos por clave (gestionado por máquina)
- Reclamaciones — qué tareas están en curso y quién las tiene (omitido cuando está vacío)
- Actualizaciones Concurrentes — contención resuelta reciente (omitido cuando está vacío)
- Rastro de Auditoría — historial reciente de actualizaciones, mensajes, reclamaciones y rechazos
El rastro de auditoría está limitado para que el archivo vivo se mantenga pequeño y cada escritura siga siendo
barata — todo el documento se vuelve a renderizar en cada mutación, así que un historial
sin límite haría el nodo cada vez más lento sin razón visible. Las entradas más
antiguas pasan a shared_context.archive.md en lugar de descartarse, así
el registro se mantiene completo.
Síguelo durante el desarrollo:
p2pa log
Notas de seguridad
- En el modo
strict(el predeterminado), la lista de permitidos de pares es el límite de control de acceso — una fuga de tema por sí sola ya no otorga acceso. Ver Autenticación de pares. - El tema sigue siendo material de descubrimiento, no un secreto — se anuncia en el DHT público. Prefiere temas largos y aleatorios (los códigos autogenerados tienen 22 caracteres) y evita
--topicen la línea de comandos, donde termina en el historial del shell y en la salida deps. Usap2pa pairoP2PA_TOPICen su lugar. - En el modo
openno hay autenticación en absoluto: cualquiera que conozca el tema puede leer y escribir tu estado compartido. Úsalo solo para mantener vivo un emparejamiento anterior a 0.7 mientras migras. identity.jsones el material de clave privada de tu nodo. Nunca lo copies entre máquinas: dos nodos que comparten un par de claves no se pueden distinguir ni revocar de forma independiente. También es la clave de firma para cada operación que este nodo crea.- En un enjambre de tres o más, activa la aplicación de firmas. Una instantánea de handshake retransmite legítimamente operaciones creadas por otros pares, por lo que la autenticación salto a salto no puede garantizar su contenido. Las firmas cierran esa brecha: una entrada sigue siendo verificable después de cualquier número de retransmisiones. La aplicación está desactivada por defecto solo porque excluye a los pares v3. Ver SPEC.md §6.
- El texto proporcionado por los pares es datos, nunca instrucciones. Los cuerpos de mensajes, roles de agente, capacidades y notas son escritos por el par. Los agentes deben tratarlos como afirmaciones a evaluar, no como comandos a seguir.
- El enlace de Google Doc también es una capacidad — con "cualquiera con el enlace = editor", cualquiera que tenga la URL puede dirigir a los agentes mediante directivas HUMAN. Rota creando un nuevo documento +
p2pa doc unlink. - El JSON de la cuenta de servicio (
P2PA_GOOGLE_SA_JSON) debe permanecer solo en disco / en el entorno MCP — nunca en Active State, el Doc o parches P2P. - Un par en la lista de permitidos es de confianza. Puede escribir cualquier clave de estado, tomar un arrendamiento que tengas y leer todo lo que sincronices. La lista de permitidos es el límite: empareja con personas, no con temas que encontraste en algún lugar.
- El backlog es el único espacio de nombres que no está vinculado al propietario. Cualquier par en la lista de permitidos puede crear, completar, reencolar o cancelar cualquier tarea — eso es lo que es un backlog compartido, y un par que no pudiera terminar un trabajo que no creó no podría recibir delegaciones. Así que un par en la lista de permitidos puede marcar tu trabajo como
donecon unresultfabricado, ocancelledpara que nadie lo recoja. Esto amplía lo que un par dentro de la lista de permitidos puede hacer, no quién está dentro.createdByes elegido por el par y no es una identidad; la firma en la operación lo es. La fusión es monótona, por lo que la dirección destructiva es unidireccional: un par puede terminar una tarea, no reabrir una en silencio. - Los títulos, detalles y resultados de tareas son instrucciones escritas por pares. Este es el caso de mayor riesgo de "el texto del par es datos": la carga útil literalmente es una instrucción — de otro agente, no de tu operador. Cada resultado de herramienta que contiene tareas lo dice, y cada cadena que llega al tablero Markdown se sanitiza para que un título no pueda falsificar una fila de tabla o un encabezado de sección.
outbox.jsonguarda el texto de los mensajes en disco (0600, dentro del directorio de configuración 0700). El historial de mensajes solo se reproduce a los pares con los que te has emparejado.- Los pares retransmiten entre sí, dentro de la instantánea de handshake. Cada operación está firmada por su autor, por lo que una escritura retransmitida sigue siendo atribuible. La aplicación (
p2pa auth require-signatures) está desactivada por defecto solo porque un par de protocolo v3 no puede firmar — actívala una vez que todos los nodos ejecuten 0.8+, y ciertamente antes de ejecutar un enjambre de tres o más. - El directorio de configuración por defecto es
0700;config.jsonse escribe como0600. - No pongas credenciales ni secretos de producción en el contexto compartido.
- Los registros del daemon en segundo plano van a
daemon-error.logpara que la salida estándar de MCP siga siendo un flujo JSON-RPC limpio.
Desarrollo
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 se ejecuta completamente sin conexión — las pruebas de integración de autenticación de pares levantan una red de prueba hyperdht en proceso, por lo que ejercitan el cortafuegos real contra conexiones reales sin tocar el DHT público.
npm run typecheck # tsc over src, scripts and test
npm run build # compile to dist/
Punto de entrada del paquete: p2pa → dist/cli.js.
Estructura
| Ruta | Qué contiene |
|---|---|
src/crdt.ts, src/hlc.ts | Motor de fusión: registros por clave, relojes lógicos híbridos |
src/claim.ts | Arrendamientos de trabajo |
src/events.ts | Bus de actividad de pares detrás de await_peer_event |
src/outbox.ts | Mensajería duradera |
src/sync.ts | Conecta lo anterior con el transporte y el registro de auditoría |
src/p2p.ts | Transporte Hyperswarm, cortafuegos de pares |
src/mcp-server.ts | La superficie de herramientas orientada a agentes |
src/markdown-log.ts | El archivo legible por humanos |
Las contribuciones son bienvenidas. El conjunto de pruebas es la especificación: cada comportamiento anterior tiene una prueba que falla si eliminas la protección que lo proporciona.
Ideas para la hoja de ruta
- Adaptadores para Notion y otros documentos vivos
- Transporte HTTP Streamable opcional junto con stdio
- Sincronización delta a nivel de clave (los pares ya omiten la instantánea por completo cuando sus resúmenes coinciden, pero una discrepancia parcial aún envía la réplica completa)
- Hacer que la aplicación de firmas sea la opción predeterminada, una vez que los pares de protocolo v3 sean lo suficientemente raros como para que excluirlos no cueste nada
Licencia
Construido para la próxima generación de desarrollo multiagente.