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.

License: MIT Node.js

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:

  1. 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.
  2. 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.
  3. 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):

ModoComandoÚsalo cuando
MCP (primer plano)p2pa mcpConectando Cursor / Claude — posee stdio limpio
Daemon (fondo)p2pa startSincronizació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 Codeclaude mcp add p2pa -- p2pa mcp, o el JSON impreso en .mcp.json
  • Cursor — Configuración → MCP
  • Claude Desktopclaude_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

QuieresEjecutaNotas
Un agente IDE conduciéndolop2pa mcp (vía la configuración MCP anterior)Iniciado por el cliente
Sincronización en segundo plano, sin IDEp2pa startDaemon 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

ComandoDescripción
p2pa start [--topic <code>]Iniciar daemon Hyperswarm en segundo plano (PM2)
p2pa stopDetener el daemon
p2pa statusEstado del daemon, identidad, huella del tema, modo de autenticación
p2pa logSeguir ~/.p2pa/shared_context.md
p2pa connectImprimir JSON MCP para Cursor / Claude Desktop
p2pa mcpEjecutar 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 peersListar 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-signaturesRechazar 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 unlinkLimpiar el vínculo del doc
p2pa doc statusMostrar doc vinculado + si las credenciales SA están establecidas

La configuración y el estado viven bajo ~/.p2pa/ (modo 0700):

RutaPropósito
config.jsonTema de emparejamiento, modo de autenticación, lista blanca de pares, vínculo de doc opcional (0600)
identity.jsonSemilla de identidad de 32 bytes de este nodo (0600) — nunca compartir
shared_context.mdEstado Activo + Estado Réplica + Reclamos + Actualizaciones Concurrentes + Rastro de Auditoría
shared_context.archive.mdEntradas de auditoría antiguas, rotadas del archivo vivo
outbox.jsonMensajes esperando confirmación (0600)
state-writer.lockRetenido por el proceso que esté escribiendo
daemon-error.logDiagnó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-signatures una vez que cada nodo ejecute 0.8+.

Modos de autenticación

ModoComportamiento
strictSolo las claves públicas en lista blanca pueden conectarse. Predeterminado para nuevas instalaciones.
openCualquiera 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 parp2pa peers remove <pubkey>, luego re-empareja.
  • Tu propia clave — elimina ~/.p2pa/identity.json y reinicia. Cada par debe re-emparejarse con tu nueva clave.
  • El temap2pa 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

  1. Crea un proyecto de Google Cloud; habilita Google Docs API y Google Drive API.
  2. Crea una cuenta de servicio, descarga su clave JSON.
  3. 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)
  1. 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
  1. Pon P2PA_GOOGLE_SA_JSON en tu entorno MCP (p2pa connect lo copia si ya está establecido en tu shell), luego reinicia MCP.

Secciones del doc (encabezados exactos):

SecciónQuién escribe
## StatusAgentes (doc_publish sección=status)
## PlanAgentes (doc_publish sección=plan)
## HUMAN directivesHumanos (añaden dirección; se consulta en steering)
## Agent logAgentes (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

HerramientaQué hace
push_contextEstablece una clave de nivel superior y la difunde
pull_contextLee una clave, o todo el documento compartido
delete_contextMarca una clave como eliminada para que una réplica obsoleta no pueda resucitarla
set_add / set_removeOperaciones de conjunto con adición ganadora, para listas a las que dos agentes añaden
override_contextImpone tus propios valores cuando el ganador automático es incorrecto por intención
check_conflictsActualizaciones concurrentes recientes — ya resueltas, informativo

Dividir el trabajo

HerramientaQué hace
create_taskPone una unidad de trabajo en el backlog compartido para cualquier agente cualificado
next_taskPide trabajo al backlog que puedas ejecutar — y toma el arrendamiento en la misma llamada
complete_taskRegistra el resultado, devuelve el resultado, libera el arrendamiento
fail_taskAbandona un intento: lo reencola, lo envía a carta muerta o lo cancela
list_tasksEl tablero: qué existe, qué está bloqueado, quién tiene qué
claim_taskToma un arrendamiento directamente, para trabajo que no está en el backlog
release_taskDevuelve una tarea antes de que expire su arrendamiento
list_claimsVe qué tareas están en curso y quién las tiene

Hablar con el otro agente

HerramientaQué hace
send_peer_messageEnvía un mensaje a todos los pares; se pone en cola y se reintenta si están desconectados
ask_peerPregunta a un agente, obtén un id de correlación para emparejar la respuesta
reply_to_peerResponde a una pregunta que otro agente te hizo
await_peer_eventBloquea hasta que un par actúa, luego devuelve lo que hizo
recent_peer_eventsPonte al día de la actividad de los pares sin bloquear
outbox_statusMensajes aún pendientes de confirmación

Saber quién está en el enjambre

HerramientaQué hace
announce_selfPublica tu rol, capacidades y estado para que los pares te enruten trabajo
list_agentsEl registro: quién está aquí, qué hace, quién está libre

Introspección

HerramientaQué hace
sync_healthId de réplica, hash de contenido, número de pares, versión de protocolo negociada por par
read_context_historyLee las últimas N líneas del registro markdown local

Documento vivo (opcional, ver abajo)

HerramientaQué hace
doc_publishEmpuja estado / plan / agent_log al Google Doc vinculado
doc_read_steeringLee directivas HUMAN (sondeo opcional forzado)
doc_statusEnlace 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_conflicts y 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

  1. Cada escritura local sella su clave con un reloj lógico híbrido: tiempo de pared, un contador y el id de este nodo.
  2. 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.
  3. 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í.
  4. La escritura perdedora se registra bajo ## Concurrent Updates y se muestra mediante check_conflicts. Nada se bloquea esperándola.
  5. Si el ganador automático es incorrecto por intención, override_context escribe 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:

  1. Estado Activo — JSON compartido actual, simple y legible por humanos
  2. Estado de Réplica — el mismo documento más sellos por clave (gestionado por máquina)
  3. Reclamaciones — qué tareas están en curso y quién las tiene (omitido cuando está vacío)
  4. Actualizaciones Concurrentes — contención resuelta reciente (omitido cuando está vacío)
  5. 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 --topic en la línea de comandos, donde termina en el historial del shell y en la salida de ps. Usa p2pa pair o P2PA_TOPIC en su lugar.
  • En el modo open no 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.json es 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 done con un result fabricado, o cancelled para que nadie lo recoja. Esto amplía lo que un par dentro de la lista de permitidos puede hacer, no quién está dentro. createdBy es 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.json guarda 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.json se escribe como 0600.
  • No pongas credenciales ni secretos de producción en el contexto compartido.
  • Los registros del daemon en segundo plano van a daemon-error.log para 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: p2padist/cli.js.

Estructura

RutaQué contiene
src/crdt.ts, src/hlc.tsMotor de fusión: registros por clave, relojes lógicos híbridos
src/claim.tsArrendamientos de trabajo
src/events.tsBus de actividad de pares detrás de await_peer_event
src/outbox.tsMensajería duradera
src/sync.tsConecta lo anterior con el transporte y el registro de auditoría
src/p2p.tsTransporte Hyperswarm, cortafuegos de pares
src/mcp-server.tsLa superficie de herramientas orientada a agentes
src/markdown-log.tsEl 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

MIT

Construido para la próxima generación de desarrollo multiagente.