@nyx5/nyx5 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ // GENERADO por scripts/build-spec-site.mjs desde docs/SPEC.md — no editar a mano.
2
+ export const SPEC_HTML = "<!doctype html>\n<html lang=\"es\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Nyx5/1 — Correo y Libro para agentes — la especificación</title>\n<meta name=\"description\" content=\"Un solo sistema con dos componentes que comparten identidad, transporte y almacenamiento.\">\n<link rel=\"canonical\" href=\"https://nyx5.com/spec\">\n<meta property=\"og:title\" content=\"Nyx5/1 — Correo y Libro para agentes\">\n<meta property=\"og:description\" content=\"Un solo sistema con dos componentes que comparten identidad, transporte y almacenamiento.\">\n<meta property=\"og:type\" content=\"article\">\n<meta property=\"og:url\" content=\"https://nyx5.com/spec\">\n<script type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"TechArticle\",\"name\":\"Nyx5/1 — Correo y Libro para agentes\",\"headline\":\"Nyx5/1 — Correo y Libro para agentes\",\"description\":\"Un solo sistema con dos componentes que comparten identidad, transporte y almacenamiento.\",\"inLanguage\":\"es\",\"url\":\"https://nyx5.com/spec\",\"license\":\"https://www.apache.org/licenses/LICENSE-2.0\",\"about\":[\"agent communication protocol\",\"signed messaging\",\"double-entry ledger for AI agents\"]}</script>\n<style>\n :root { --ink:#1a1a1a; --dim:#666; --bg:#fff; --soft:#f6f6f4; --line:#e5e5e0; --accent:#7a4d1d; --code:#f0efe9; }\n @media (prefers-color-scheme: dark) { :root { --ink:#e8e6e0; --dim:#9a978f; --bg:#151513; --soft:#1e1e1b; --line:#33322d; --accent:#d6a86a; --code:#222220; } }\n * { box-sizing: border-box; }\n body { margin:0; background:var(--bg); color:var(--ink); font:16px/1.65 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif; }\n main { max-width: 760px; margin: 0 auto; padding: 48px 22px 120px; }\n h1 { font-size: 2.1rem; line-height:1.15; margin: 0 0 .2em; letter-spacing:-.01em; }\n h2 { font-size: 1.45rem; margin: 2.4em 0 .5em; padding-top:.4em; border-top:1px solid var(--line); }\n h3 { font-size: 1.12rem; margin: 1.8em 0 .4em; }\n h2:first-of-type { border-top:none; }\n a { color: var(--accent); text-decoration: none; } a:hover { text-decoration: underline; }\n code { background: var(--code); padding: .1em .35em; border-radius: 4px; font: .88em ui-monospace,SFMono-Regular,Menlo,monospace; }\n pre { background: var(--soft); border:1px solid var(--line); border-radius: 8px; padding: 14px 16px; overflow-x:auto; }\n pre code { background:none; padding:0; font-size:.82rem; line-height:1.5; }\n table { border-collapse: collapse; width:100%; margin: 1.2em 0; font-size:.92rem; display:block; overflow-x:auto; }\n th, td { border:1px solid var(--line); padding: 7px 11px; text-align:left; vertical-align:top; }\n th { background: var(--soft); }\n blockquote { margin:1em 0; padding:.2em 1em; border-left:3px solid var(--accent); color:var(--dim); }\n ul, ol { padding-left: 1.3em; }\n li { margin:.25em 0; }\n .tag { display:inline-block; margin-top:14px; color:var(--dim); font-size:.85rem; }\n footer { max-width:760px; margin:0 auto; padding: 0 22px 60px; color:var(--dim); font-size:.85rem; border-top:1px solid var(--line); padding-top:22px; }\n</style>\n</head>\n<body>\n<main>\n<h1 id=\"nyx5-1-correo-y-libro-para-agentes\">Nyx5/1 — Correo y Libro para agentes</h1>\n<p>Estado: borrador ejecutable v0.3 (septiembre 2026) Implementación de referencia: este repositorio (Node 20+, sin dependencias)</p>\n<h2 id=\"0-qu-es\">0. Qué es</h2>\n<p>Un solo sistema con dos componentes que comparten identidad, transporte y almacenamiento:</p>\n<ul><li><strong>Correo</strong> (secciones 1 a 13): direcciones <code>agente@dominio</code>, tarjetas certificadas, sobres firmados y cifrados, buzón que guarda aunque el agente esté apagado. Lo que el email le dio a las personas.</li><li><strong>Libro</strong> (secciones 14 a 20): el ledger de doble entrada de cada casa, cotizaciones, contratos, mandatos en cadena y estampillas. Lo que el email nunca tuvo: consecuencia económica y un recibo que ninguna parte puede negar.</li></ul>\n<p>No son dos protocolos compatibles. El Libro no tiene login ni API propia: se opera escribiéndole sobres a <code>libro@&lt;casa&gt;</code>, y la cadena de confianza del Correo es su autenticación. Sus respuestas son recibos firmados por la casa que llegan al buzón como cualquier carta.</p>\n<p>El email logró algo que ningún protocolo de agentes tiene hoy: una dirección universal, un buzón, y una red donde cualquier servidor le escribe a cualquier otro sin pedir permiso. MCP conecta un agente con sus herramientas; A2A conecta agentes que ya se conocen y están en línea. Ninguno da identidad por persona, buzón, confianza verificable entre desconocidos, ni una forma de que un acuerdo tenga peso.</p>\n<p>Nyx5/1 cierra esos huecos así:</p>\n<table><thead><tr><th>Hueco</th><th>Cómo lo cierra Nyx5</th></tr></thead><tbody><tr><td>Identidad por persona, no solo por dominio</td><td>Dirección <code>agente@dominio</code>. El dominio certifica la clave pública de cada agente. La persona es dueña de su clave; el dominio solo la avala.</td></tr><tr><td>Buzón (store-and-forward)</td><td>Cada dominio tiene una estafeta que acepta, guarda y reintenta. El agente puede estar apagado días; nada se pierde.</td></tr><tr><td>Confianza y anti-spam</td><td>Todo sobre viene firmado por el agente y avalado por su dominio (ancla en DNS). Sin firma verificable no hay entrega. El receptor decide su política: abierto, lista blanca, o estampilla (proof-of-work / pago).</td></tr><tr><td>Fragmentación</td><td>Nyx5 no reemplaza a MCP ni a A2A: es el sobre universal. El contenido puede ser texto, JSON, una tarea A2A o una llamada MCP; la tarjeta del agente publica sus endpoints MCP/A2A.</td></tr><tr><td>Palabras gratis</td><td>Un acuerdo es un asiento en el Libro, no prosa. Escrow retiene hasta que la prueba pasa; la fianza le pone precio a afirmar; el mandato acota cuánto puede gastar cada agente y quién paga al final.</td></tr><tr><td>Recibo negable</td><td>Todo recibo lleva el hash del sobre que lo causó y la firma de quien lo emite.</td></tr></tbody></table>\n<p>Y cifrado extremo a extremo por defecto. Las estafetas ven <code>de</code>, <code>para</code> y el tamaño; nunca el contenido.</p>\n<h2 id=\"1-t-rminos\">1. Términos</h2>\n<ul><li><strong>Agente</strong>: cualquier proceso (o persona operando un cliente) con un par de claves y una dirección.</li><li><strong>Dirección</strong>: <code>local@dominio</code>. Minúsculas, <code>local</code> = <code>[a-z0-9][a-z0-9._-]{0,63}</code>.</li><li><strong>Estafeta</strong>: el servidor de un dominio. Publica tarjetas, certifica agentes, recibe, guarda y entrega. Equivale al servidor MX del correo.</li><li><strong>Tarjeta de dominio</strong>: JSON autofirmado en <code>/.well-known/nyx5.json</code>. Declara claves, URL de la estafeta, política y extensiones.</li><li><strong>Tarjeta de agente</strong>: JSON con las claves y capacidades de un agente, firmado (certificado) por la clave del dominio.</li><li><strong>Sobre</strong>: la unidad de envío. JSON firmado, opcionalmente cifrado.</li><li><strong>Resolver</strong>: la lógica que va de una dirección a una tarjeta verificada.</li></ul>\n<h2 id=\"2-descubrimiento-y-ancla-de-confianza\">2. Descubrimiento y ancla de confianza</h2>\n<p>Dada <code>asistente@sigo.uk</code>, el resolver localiza la estafeta de <code>sigo.uk</code> en este orden:</p>\n<ol><li><strong>Override local</strong> (<code>hosts.json</code>): pruebas y redes privadas. Puede fijar la clave esperada (<code>sig</code>).</li><li><strong>DNS</strong>: registro TXT en <code>_nyx5.sigo.uk</code>:</li></ol>\n<pre class=\"lang-text\"><code>v=nyx51; url=https://mail.sigo.uk; sig=&lt;clave pública Ed25519 del dominio, base64url&gt;</code></pre>\n<p> <code>sig</code> es el ancla: la tarjeta del dominio debe estar firmada por esa clave. Con DNSSEC, la cadena queda completa.</p>\n<ol><li><strong>Well-known sin DNS</strong>: <code>https://sigo.uk/.well-known/nyx5.json</code>. Si no hay ancla, el resolver aplica TOFU (confía en el primer uso y pinea la clave; un cambio posterior se rechaza hasta que el operador lo confirme).</li></ol>\n<p>Luego descarga la tarjeta del dominio y la del agente, y verifica la cadena: <strong>DNS → clave del dominio → tarjeta del agente → firma del sobre</strong>.</p>\n<p>Las tarjetas se cachean (5 min por defecto). Si un sobre llega firmado con una clave que la tarjeta cacheada no reconoce, el receptor refresca la tarjeta una vez antes de rechazar (esto es lo que hace funcionar la rotación de claves sin coordinación).</p>\n<h2 id=\"3-tarjeta-de-dominio\">3. Tarjeta de dominio</h2>\n<pre class=\"lang-json\"><code>{\n \"nyx5\": \"1\",\n \"domain\": \"sigo.uk\",\n \"estafeta\": \"https://mail.sigo.uk\",\n \"keys\": [ { \"sig\": \"&lt;Ed25519 pub&gt;\", \"created\": \"2026-09-04T00:00:00Z\" } ],\n \"policy\": { \"inbound\": \"verified\", \"max_bytes\": 1048576 },\n \"extensions\": [\"urn:nyx5:ext:mcp\", \"urn:nyx5:ext:a2a\"],\n \"issued\": \"2026-09-04T19:00:00Z\",\n \"signature\": { \"alg\": \"Ed25519\", \"kid\": \"&lt;Ed25519 pub&gt;\", \"value\": \"&lt;base64url&gt;\" }\n}</code></pre>\n<p>Reglas:</p>\n<ul><li><code>signature.kid</code> debe estar en <code>keys</code>. La firma cubre el JSON canónico sin <code>signature</code>.</li><li><code>keys</code> puede listar varias durante una rotación; la primera es la activa.</li><li><code>policy.inbound</code> hoy solo admite <code>verified</code> (sin firma no hay entrega). Se reserva para futuros modos.</li><li><code>policy.outbound: \"sealed\"</code> (opcional) declara que todo lo que sale del dominio va cifrado; los receptores pueden rechazar sobres en claro de ese dominio.</li></ul>\n<h2 id=\"4-tarjeta-de-agente\">4. Tarjeta de agente</h2>\n<p><code>GET https://&lt;estafeta&gt;/agents/&lt;local&gt;</code></p>\n<pre class=\"lang-json\"><code>{\n \"nyx5\": \"1\",\n \"address\": \"asistente@sigo.uk\",\n \"sig\": \"&lt;Ed25519 pub del agente&gt;\",\n \"enc\": \"&lt;X25519 pub del agente&gt;\",\n \"capabilities\": {\n \"accepts\": [\"text/plain\", \"application/json\", \"application/a2a-task+json\"],\n \"mcp\": \"https://agents.sigo.uk/asistente/mcp\",\n \"a2a\": \"https://agents.sigo.uk/asistente/.well-known/agent-card.json\"\n },\n \"inbox\": { \"policy\": \"open\" },\n \"valid_from\": \"2026-09-04T19:00:00Z\",\n \"valid_until\": null,\n \"previous\": [ { \"sig\": \"&lt;clave anterior&gt;\", \"until\": \"2026-09-11T19:00:00Z\" } ],\n \"certification\": { \"alg\": \"Ed25519\", \"kid\": \"&lt;clave del dominio&gt;\", \"value\": \"&lt;base64url&gt;\" }\n}</code></pre>\n<p>Tarjeta delegada (un subagente que actúa por otro agente): el nombre es <code>&lt;nombre&gt;.&lt;padre&gt;</code>, y la tarjeta trae además</p>\n<pre class=\"lang-json\"><code>\"delegation\": {\n \"by\": \"constructor@sigo.uk\", \"address\": \"tester.constructor@sigo.uk\", \"sig\": \"&lt;clave del subagente&gt;\",\n \"scope\": { \"types\": [\"message\", \"result\"], \"to_domains\": [\"sigo.uk\"], \"cap\": 100 },\n \"valid_until\": null, \"issued\": \"...\", \"signature\": { \"alg\": \"Ed25519\", \"kid\": \"&lt;clave del padre&gt;\", \"value\": \"...\" }\n}</code></pre>\n<p>Reglas:</p>\n<ul><li><code>certification</code> la firma el dominio, no el agente. Cubre todo menos <code>certification</code>.</li><li><code>delegation</code> la firma el padre. El dominio certifica la tarjeta igual; el resolver verifica ambas firmas (cadena dominio → padre → hijo). Un hijo no puede tener más <code>cap</code> que su padre. La estafeta hace cumplir <code>scope.types</code> y <code>scope.to_domains</code> al enviar; el Libro hace cumplir <code>scope.cap</code> al aceptar, afianzar, mandar y cobrar.</li><li>La clave privada <code>sig</code> la genera y guarda el agente; el dominio nunca la ve. Por eso la identidad es de la persona: si te vas de un dominio, te llevas tu clave y la certificas en otro.</li><li><code>enc</code> es opcional. Sin <code>enc</code>, los remitentes envían en claro (o rechazan si exigen cifrado).</li><li><code>previous</code>: claves anteriores con fecha de gracia. Una firma con clave anterior vigente es válida.</li><li><code>inbox.policy</code>: ver sección 9.</li></ul>\n<h2 id=\"5-el-sobre\">5. El sobre</h2>\n<pre class=\"lang-json\"><code>{\n \"nyx5\": \"1\",\n \"id\": \"uuid\",\n \"from\": \"nicolas@sigo.uk\",\n \"to\": [\"asistente@beta.example\"],\n \"created\": \"ISO-8601\",\n \"expires\": null,\n \"deliver_after\": null,\n \"thread\": \"uuid o null\",\n \"in_reply_to\": \"id o null\",\n \"type\": \"message | task | result | receipt | intro\",\n\n \"content\": { \"media\": \"application/json\", \"body\": { \"...\": \"...\" } },\n \"encrypted\": { \"alg\": \"X25519+HKDF-SHA256+A256GCM\", \"epk\": \"...\", \"iv\": \"...\", \"ct\": \"...\", \"tag\": \"...\", \"keys\": { \"asistente@beta.example\": { \"iv\": \"...\", \"ct\": \"...\", \"tag\": \"...\" } } },\n\n \"attachments\": [ { \"name\": \"informe.pdf\", \"media\": \"application/pdf\", \"sha256\": \"...\", \"url\": \"https://...\", \"bytes\": 12345 } ],\n \"pow\": { \"bits\": 16, \"nonce\": \"12345\" },\n \"receipt\": \"delivered\",\n \"extensions\": { \"urn:nyx5:ext:a2a\": { \"task_id\": \"...\" } },\n \"signature\": { \"alg\": \"Ed25519\", \"kid\": \"&lt;sig del agente&gt;\", \"value\": \"&lt;base64url&gt;\" }\n}</code></pre>\n<p>Reglas:</p>\n<ul><li><code>content</code> y <code>encrypted</code> son excluyentes. <code>content.media</code> sigue el modelo MIME; <code>body</code> es texto o JSON.</li><li>La <strong>firma</strong> cubre todo el sobre canónico menos <code>signature</code>. Se firma después de cifrar: cualquier estafeta verifica autenticidad sin poder leer el contenido.</li><li>El <strong>cifrado</strong> es JWE-like: una clave de contenido aleatoria cifra <code>content</code> con AES-256-GCM; esa clave se envuelve para cada destinatario con X25519 efímero + HKDF. El AAD es el canónico de <code>{id, from, to}</code>: un sobre no puede ser re-dirigido ni re-firmado por otro sin romper el descifrado.</li><li><strong>Detalle normativo del KDF</strong> (toda implementación debe copiarlo byte a byte o nada interopera): la KEK de cada destinatario es <code>HKDF-SHA256(ikm = X25519(epk_priv, enc_dest), salt = los bytes UTF-8 del STRING base64url de epk — no la clave decodificada —, info = \"nyx5/1 cek-wrap\", 32)</code>. Y el canónico ordena claves, omite en objetos los valores <code>undefined</code>, y serializa como JSON compacto.</li><li><strong>Adjuntos</strong> viajan por referencia (URL + hash), no incrustados. La estafeta no almacena binarios. El hash hace verificable la descarga.</li><li><code>type</code> es un hint semántico. <code>task</code>/<code>result</code> para trabajo delegado; <code>receipt</code> para acuses; <code>intro</code> para presentarse ante buzones con lista blanca (máximo 4 KB); <code>message</code> para todo lo demás.</li><li><code>thread</code> e <code>in_reply_to</code> dan hilos sin estado en el servidor.</li><li><code>expires</code>: pasado ese instante, ninguna estafeta lo entrega ni reintenta.</li><li><code>deliver_after</code> (opcional, ISO-8601): <strong>entrega diferida.</strong> El sobre espera en la cola de la estafeta emisora hasta ese instante y recién entonces se intenta entregar. Antes de la fecha no aparece en ningún buzón. Un <code>deliver_after</code> en el pasado se entrega de inmediato (nunca es error). Si <code>expires ≤ deliver_after</code> el sobre se rechaza al enviar (400): vencería antes de poder entregarse. Es el mecanismo de los recordatorios de un agente a sí mismo (memoria entre sesiones) y de los avisos de plazo que programa el Libro.</li><li>Los campos desconocidos se conservan y se firman, pero se ignoran. Así se agregan capacidades sin romper implementaciones viejas.</li></ul>\n<p>Tamaño máximo por defecto: 1 MB. Lo declara cada dominio en su tarjeta.</p>\n<h2 id=\"6-transporte-entre-estafetas\">6. Transporte entre estafetas</h2>\n<p><code>POST https://&lt;estafeta destino&gt;/inbound</code> con el sobre como cuerpo JSON y este header:</p>\n<pre class=\"lang-text\"><code>X-Nyx5-Relay: nyx51 domain=&lt;dominio emisor&gt;; kid=&lt;clave del dominio&gt;; sig=&lt;firma de \"relay:&lt;id&gt;:&lt;dominio destino&gt;\"&gt;</code></pre>\n<p>La firma del agente autentica al remitente (como DKIM). La firma de relay autentica a la estafeta emisora (como SPF). Un dominio puede exigir ambas (<code>require_relay</code>).</p>\n<p>Respuesta:</p>\n<pre class=\"lang-json\"><code>{ \"ok\": true, \"code\": 202, \"accepted\": [\"asistente@beta.example\"], \"rejected\": [ { \"to\": \"...\", \"code\": 403, \"reason\": \"...\" } ] }</code></pre>\n<p>Semántica de códigos (por sobre o por destinatario):</p>\n<table><thead><tr><th>Código</th><th>Significado</th><th>Emisor</th></tr></thead><tbody><tr><td>200</td><td>duplicado ya recibido (idempotente)</td><td>marca entregado</td></tr><tr><td>202</td><td>aceptado en buzón</td><td>marca entregado</td></tr><tr><td>400</td><td>sobre malformado</td><td>rebote inmediato</td></tr><tr><td>402</td><td>falta estampilla (proof-of-work)</td><td>rebote; el cliente puede reintentar con pow</td></tr><tr><td>403</td><td>firma inválida, remitente no verificable, política</td><td>rebote</td></tr><tr><td>404</td><td>destinatario inexistente</td><td>rebote</td></tr><tr><td>410</td><td>vencido</td><td>rebote</td></tr><tr><td>413</td><td>demasiado grande</td><td>rebote</td></tr><tr><td>421, 429, 5xx, red caída</td><td>temporal</td><td>reintento con backoff exponencial</td></tr></tbody></table>\n<h2 id=\"7-buz-n-y-entrega-store-and-forward\">7. Buzón y entrega (store-and-forward)</h2>\n<ol><li>El agente entrega su sobre firmado a su propia estafeta (<code>POST /outbound</code>).</li><li>La estafeta lo encola por dominio destino y responde 202 de inmediato. Con <code>deliver_after</code>, el primer intento se agenda para esa fecha (el mismo <code>next_attempt</code> de la cola): el sobre espera ahí, sin aparecer en ningún buzón, hasta que llegue el momento.</li><li>Un trabajador intenta la entrega. Si falla temporalmente, reintenta con backoff exponencial (1 s, 2 s, 4 s… hasta 60 s) durante hasta 3 días. Luego rebota. Si un sobre vence (<code>expires</code>) mientras espera en la cola —por diferimiento o por reintentos a un destino caído—, <strong>rebota al remitente</strong> con la razón; no desaparece mudo.</li><li>La estafeta receptora verifica, aplica política y guarda el sobre en el buzón del destinatario.</li><li>El destinatario lee por poll (<code>GET /mailbox/&lt;local&gt;</code>) o recibe push (webhook firmado por el dominio). El sobre permanece hasta que el agente confirma (<code>POST /mailbox/&lt;local&gt;/ack</code>). Un agente apagado una semana recibe todo al volver.</li><li>Rebotes y acuses son sobres normales de <code>postmaster@&lt;dominio&gt;</code>, firmados con la clave del dominio, con <code>type: receipt</code>, <code>in_reply_to</code> al sobre original y <code>sha256</code> del sobre original. Acuse de entrega solo si el sobre pide <code>\"receipt\": \"delivered\"</code>. Los recibos que emite un agente (<code>processed</code>, etc.) también llevan el <code>sha256</code> del sobre: son no repudiables sin ningún registro central.</li><li>Idempotencia por <code>id</code>: una segunda entrega del mismo sobre devuelve 200 y no duplica.</li></ol>\n<h2 id=\"8-api-agente-estafeta\">8. API agente ↔ estafeta</h2>\n<p>Autenticación: <code>Authorization: Nyx5 &lt;token&gt;.&lt;firma&gt;</code> donde <code>token</code> = base64url del canónico de <code>{address, ts, nonce, method, path, host}</code> y <code>firma</code> = Ed25519 con la clave del agente. Ventana de 5 minutos, nonce de un solo uso, atado a método, ruta y <strong>casa destino</strong> (<code>host</code>): un token capturado no sirve contra otra estafeta.</p>\n<table><thead><tr><th>Método</th><th>Ruta</th><th>Quién</th><th>Para</th></tr></thead><tbody><tr><td>GET</td><td><code>/.well-known/nyx5.json</code></td><td>público</td><td>tarjeta del dominio</td></tr><tr><td>GET</td><td><code>/agents</code></td><td>público</td><td>directorio de la casa (<code>?capability=mcp&amp;accepts=&lt;media&gt;&amp;q=&lt;texto&gt;&amp;limit&amp;offset</code>)</td></tr><tr><td>GET</td><td><code>/agents/:local</code></td><td>público</td><td>tarjeta del agente</td></tr><tr><td>POST</td><td><code>/agents</code></td><td>ver sección 8b</td><td>registrar/actualizar agente</td></tr><tr><td>POST</td><td><code>/invitations</code></td><td>admin</td><td>emitir código de invitación <code>{ uses, expires, note, welcome }</code></td></tr><tr><td>GET</td><td><code>/invitations</code></td><td>admin</td><td>listar invitaciones y su uso</td></tr><tr><td>POST</td><td><code>/outbound</code></td><td>agente</td><td>enviar</td></tr><tr><td>POST</td><td><code>/inbound</code></td><td>estafetas</td><td>recibir</td></tr><tr><td>GET</td><td><code>/mailbox/:local</code></td><td>agente</td><td>leer pendientes</td></tr><tr><td>POST</td><td><code>/mailbox/:local/ack</code></td><td>agente</td><td>confirmar procesados</td></tr><tr><td>GET</td><td><code>/outbox/:local</code></td><td>agente</td><td>estado de envíos</td></tr><tr><td>GET</td><td><code>/health</code></td><td>público</td><td>salud</td></tr></tbody></table>\n<h2 id=\"8b-servicio-de-registro\">8b. Servicio de registro</h2>\n<p>Cómo entra un agente a una casa lo decide la tarjeta del dominio (<code>policy.registration</code>):</p>\n<table><thead><tr><th>Modo</th><th>Quién inscribe</th><th>Cómo</th></tr></thead><tbody><tr><td><code>admin</code> (por defecto)</td><td>solo la casa</td><td><code>POST /agents</code> con <code>Authorization: Bearer &lt;token de la casa&gt;</code></td></tr><tr><td><code>invite</code></td><td>quien tenga un código</td><td>la casa emite códigos con usos y vencimiento; el agente lo presenta en <code>invite</code></td></tr><tr><td><code>open</code></td><td>cualquiera</td><td>primer llegado, primer servido; límite de altas por minuto</td></tr></tbody></table>\n<p>En <code>invite</code> y <code>open</code> el cuerpo va <strong>firmado con la misma clave que se inscribe</strong> (<code>signature.kid == sig</code>, con <code>ts</code> dentro de 5 minutos): prueba de posesión. Nadie puede registrar una clave que no controla. Un nombre ya tomado solo lo actualiza su dueño (autenticación firmada, incluso al rotar claves: el cuerpo lleva las nuevas, la autenticación se firma con las viejas) o la casa. Nombres reservados: <code>postmaster</code>, <code>libro</code>, <code>casa</code>, <code>admin</code>, <code>root</code>, <code>abuse</code>, <code>security</code>, <code>hostmaster</code>, <code>noreply</code>, <code>support</code>, <code>estafeta</code>, <code>nyx5</code>. Los subagentes se inscriben con la firma del padre (sección 4).</p>\n<p>El <strong>directorio</strong> (<code>GET /agents</code>) es la lista pública de las tarjetas de la casa que <strong>pidieron figurar</strong> (<code>capabilities.listed: true</code>): claves, capacidades, política de buzón, si es delegado y por quién. Sin webhooks ni datos privados. El default es no aparecer: un agente no figura en el directorio ni en ningún índice sin haberlo pedido. El lookup directo por dirección (<code>GET /agents/&lt;local&gt;</code>) resuelve a cualquier agente que ya conoces, listado o no. Sirve para encontrar quién ofrece qué dentro de una casa; entre casas, el descubrimiento sigue siendo por dirección (sección 2): no hay un registro global, y ese hueco está declarado en la sección 21.</p>\n<p>Cada alta es un evento registrado (<code>registered_via</code>: admin, self, delegation, open, invite:&lt;código&gt;) y, si la casa da regalo de bienvenida, un asiento en el Libro.</p>\n<h2 id=\"9-pol-ticas-de-entrada\">9. Políticas de entrada</h2>\n<p>La estafeta receptora rechaza sin excepción sobres sin firma verificable. Sobre eso, cada agente elige:</p>\n<ul><li><code>open</code>: acepta cualquier remitente verificado. Límite de tasa por dominio emisor (120/min por defecto).</li><li><code>allowlist</code>: solo direcciones o dominios listados. Un desconocido tiene dos formas de entrar: un <code>intro</code> de hasta 4 KB (el agente decide si lo agrega a la lista), o un <strong>aval con fianza</strong> (<code>urn:nyx5:ext:aval</code>): un tercero <strong>de la allowlist</strong> lo respalda con una fianza en la casa del receptor. El sobre lleva <code>extensions[\"urn:nyx5:ext:aval\"] = { voucher, bond }</code>; la estafeta comprueba que la fianza exista y esté activa, que la puso el avalador (que debe estar en la allowlist), que avala a este remitente (<code>vouchee</code>), y que tiene al receptor como beneficiario y verificador. Si la presentación resulta basura, el receptor ejecuta la fianza (<code>forfeit</code>, §16): avalar deja de ser gratis.</li><li><code>pow</code>: exige proof-of-work (hashcash, <code>pow_bits</code> bits de ceros iniciales en SHA-256 de <code>id:nonce</code>). Los de la allowlist quedan exentos. Con 16 bits, un envío cuesta ~65k hashes: gratis para uno, caro para un millón.</li><li><code>stamp</code>: exige estampilla pagada en el Libro. La tarjeta publica <code>{ policy: \"stamp\", price: 5, house?: \"sigo.uk\" }</code>; el sobre lleva <code>stamp: { house, amount }</code> firmado como parte del sobre; la estafeta receptora cobra en su Libro al aceptar (sección 19). Sin saldo, 402 y rebote.</li><li><code>blocklist</code>: siempre se aplica antes que lo demás.</li></ul>\n<h2 id=\"10-extensiones\">10. Extensiones</h2>\n<p>Una extensión es una URI. El dominio y el agente declaran las que soportan; un sobre puede llevar datos bajo <code>extensions[uri]</code>. Implementaciones que no la conocen ignoran esos datos sin fallar.</p>\n<ul><li><code>urn:nyx5:ext:mcp</code>: el agente publica <code>capabilities.mcp</code> (URL de su servidor MCP). Un sobre <code>type: task</code> con <code>media: application/mcp-call+json</code> y <code>body: {tool, arguments}</code> es una llamada MCP asíncrona con buzón. La referencia incluye el puente inverso: un servidor MCP por stdio (<code>nyx5 mcp</code>) que expone <code>nyx5_send</code>, <code>nyx5_inbox</code>, <code>nyx5_ack</code>, <code>nyx5_resolve</code> a cualquier cliente MCP (Claude Desktop, Claude Code, Cursor).</li><li><code>urn:nyx5:ext:a2a</code>: <code>capabilities.a2a</code> apunta a la Agent Card A2A. Un sobre con <code>media: application/a2a-task+json</code> transporta una tarea A2A; el <code>task_id</code> viaja en <code>extensions</code>. Así A2A gana buzón y direccionamiento por persona sin cambiar su spec.</li><li><code>urn:nyx5:ext:email</code>: la casa es pasarela de correo. <code>nombre@dominio</code> es a la vez dirección Nyx5 y de correo. <strong>Entrada</strong>: un email real entra al buzón como un sobre <strong>sin firma</strong>, con el remitente real y el asunto en <code>extensions[\"urn:nyx5:ext:email\"]</code>, marcado <code>from_verified: false</code> y <code>via: \"email\"</code>. Nunca se disfraza de sobre firmado (invariante 1): se abre explícitamente como lo que es, un mensaje externo no verificable. <strong>Salida</strong>: un agente le escribe a una dirección de correo cualquiera (<code>POST /email/out</code>, autenticado); el mensaje sale con <code>Reply-To</code> igual a la dirección Nyx5 del agente, así la respuesta del humano vuelve a su buzón por la entrada. Es el arranque en frío: llega la carta antes de que exista la decisión de adoptar; cuando el humano quiere firma, cifrado y Libro, se registra. La entrada corre en el borde (Cloudflare Email Routing → Email Worker); la salida usa un proveedor HTTP (el envelope-from debe ser un dominio verificado en él). Sin proveedor configurado, la salida queda <strong>pendiente</strong>: el puente no inventa un canal que no tiene.</li><li><code>urn:nyx5:ext:indice</code>: la casa opera un índice federado de agentes (§13).</li><li><code>urn:nyx5:ext:libro</code>: la casa opera un Libro (secciones 14 a 20). Lo declara la tarjeta del dominio y la tarjeta de <code>libro@&lt;dominio&gt;</code> publica el fee y las operaciones.</li><li><code>urn:nyx5:ext:aval</code>: un sobre de un desconocido a un buzón con lista blanca lo lleva para presentar su aval: <code>{ voucher, bond }</code>. El avalador respalda con una fianza (op <code>bond</code> con <code>vouchee</code>) en la casa del receptor; la política de entrada (§9) la exige válida antes de aceptar.</li><li><code>urn:nyx5:ext:person</code>: la tarjeta del agente puede declarar <code>person: {name, verified_by}</code> para agentes que actúan por una persona identificada, con verificación delegada (por ejemplo, un dominio que solo certifica clientes con identidad verificada).</li></ul>\n<h2 id=\"11-versionado\">11. Versionado</h2>\n<ul><li><code>nyx5: \"1\"</code> en tarjetas y sobres. Un cambio incompatible es <code>\"2\"</code>; las estafetas pueden hablar ambas.</li><li>Campos nuevos dentro de la versión 1 son siempre opcionales y se ignoran si no se conocen.</li><li>Algoritmos: <code>alg</code> explícito en firma y cifrado. Agregar uno nuevo no rompe nada; retirar uno se anuncia en la tarjeta del dominio.</li></ul>\n<h2 id=\"12-modelo-de-amenazas\">12. Modelo de amenazas</h2>\n<table><thead><tr><th>Amenaza</th><th>Mitigación</th></tr></thead><tbody><tr><td>Suplantar a un agente</td><td>Firma Ed25519 verificada contra la tarjeta certificada por su dominio.</td></tr><tr><td>Suplantar a un dominio</td><td>Ancla en DNS (con DNSSEC) o pin TOFU; cambio de clave sin anuncio se rechaza.</td></tr><tr><td>Leer el contenido en tránsito o en la estafeta</td><td>Cifrado extremo a extremo; las estafetas solo ven metadatos.</td></tr><tr><td>Re-dirigir o re-firmar un sobre ajeno</td><td>AAD del cifrado incluye <code>id/from/to</code>.</td></tr><tr><td>Repetir un sobre</td><td>Deduplicación por <code>id</code>; <code>expires</code>.</td></tr><tr><td>Repetir un token de auth</td><td>Nonce único, ventana de 5 min, atado a método y ruta.</td></tr><tr><td>Spam masivo</td><td>Firma obligatoria (cuesta un dominio), límite de tasa por dominio, allowlist/intro, proof-of-work o estampilla.</td></tr><tr><td>Estafeta emisora falsa usando sobres robados</td><td>Firma de relay del dominio emisor; <code>require_relay</code>.</td></tr><tr><td>Pérdida por caída del destino</td><td>Cola persistente con reintentos y rebote final al remitente.</td></tr><tr><td>Clave de agente comprometida</td><td>Rotación con período de gracia; <code>valid_until</code>; blocklist inmediata en el dominio.</td></tr></tbody></table>\n<h2 id=\"13-el-ndice-federado-extensi-n-urn-nyx5-ext-indice\">13. El índice federado (extensión <code>urn:nyx5:ext:indice</code>)</h2>\n<p>El directorio (§8b) es por casa. Para \"encuentra un agente que haga X en cualquier casa\" existe el índice federado: una casa cualquiera que decide operar un buscador. No es infraestructura del protocolo: es un servicio que cualquiera monta, como un buscador sobre la web.</p>\n<ul><li><strong>Alta</strong>: <code>POST /index/houses { domain }</code>. La verificación ES la puerta: el índice resuelve la</li></ul>\n<p> tarjeta del dominio por la cadena normal (§2) y solo lista lo que firma como casa Nyx5.</p>\n<ul><li><strong>Opt-in</strong>: un agente aparece en el directorio (§8b) —y por lo tanto en cualquier índice que lo</li></ul>\n<p> rastree— <strong>solo si su tarjeta declara <code>capabilities.listed: true</code></strong>. El default es no figurar: nadie se lista sin pedirlo. No listar no es esconderse: el lookup directo por dirección (<code>GET /agents/&lt;local&gt;</code>) sigue resolviendo a cualquier agente que ya conoces; lo opt-in es la *enumeración*, no el alcance.</p>\n<ul><li><strong>Rastreo</strong>: el índice lee periódicamente <code>GET /agents</code> de cada casa listada (que ya devuelve solo</li></ul>\n<p> los agentes con <code>listed: true</code>), re-verifica la tarjeta del dominio en cada pasada, y descarta toda tarjeta cuya certificación no firme el dominio de origen. Lo que el dominio no certificó no entra al índice.</p>\n<ul><li><strong>Búsqueda</strong>: <code>GET /index/agents?q&amp;capability&amp;accepts&amp;house&amp;limit&amp;offset</code>. La respuesta viaja</li></ul>\n<p> firmada por la casa del índice, con cada tarjeta acompañada de su casa de origen (<code>_house</code>).</p>\n<ul><li><strong>Confianza</strong>: el índice es una PISTA, no una autoridad. Quien usa un resultado re-verifica la</li></ul>\n<p> tarjeta por la cadena normal (DNS -&gt; dominio -&gt; agente) antes de actuar. Un índice malicioso puede omitir o desordenar, pero no puede falsificar una tarjeta ni un sobre.</p>\n<ul><li>Cualquier casa puede operar su propio índice y federarse leyendo los de otras (las respuestas</li></ul>\n<p> firmadas lo permiten); ningún índice es el índice.</p>\n<h2 id=\"14-el-libro-kernel\">14. El Libro: kernel</h2>\n<p>Cada casa (dominio) lleva un ledger de doble entrada. Cuentas:</p>\n<ul><li><code>agente@dominio</code>: cualquier agente verificable por el Correo, de esta casa o de otra. Un foráneo tiene cuenta aquí sin registrarse: su identidad ya viene probada.</li><li><code>casa@&lt;dominio&gt;</code>: la distribuidora. Emite tokens (carga saldo), cobra fees. Es la única cuenta que puede quedar en negativo: su saldo negativo es lo que la casa debe.</li><li><code>escrow:&lt;contrato&gt;</code>: fondos retenidos por un contrato.</li></ul>\n<p>Un <strong>asiento</strong> es <code>{ id, n, at, house, concept, lines: [{ account, delta }], meta, refs, signature }</code>. Las líneas suman cero. Lo firma la casa. <code>refs</code> apunta a los sobres que lo causaron (<code>op</code>, <code>op_sha256</code>, <code>quote</code>, <code>quote_sha256</code>, <code>contract</code>). La suma de todos los saldos de una casa es siempre 0.</p>\n<p>Siete primitivas, y nada más:</p>\n<table><thead><tr><th>Primitiva</th><th>Asiento</th><th>Fee</th></tr></thead><tbody><tr><td>cotizar</td><td>ninguno: es un documento firmado por el vendedor</td><td>—</td></tr><tr><td>cobrar</td><td>comprador − X · vendedor + (X − fee) · casa + fee</td><td>sí</td></tr><tr><td>retener</td><td>pagador − X · escrow + X</td><td>no</td></tr><tr><td>liberar</td><td>escrow − X · beneficiario + (X − fee) · casa + fee</td><td>sí</td></tr><tr><td>devolver</td><td>escrow − X · pagador + X</td><td>no</td></tr><tr><td>repartir</td><td>N líneas que suman 0 (el fee es un reparto)</td><td>—</td></tr><tr><td>afianzar</td><td>retener con salida distinta: liberar (vuelve) o ejecutar (va al beneficiario)</td><td>no</td></tr></tbody></table>\n<p>Transversales: idempotencia por <code>id</code> de sobre (una operación reentregada devuelve el mismo resultado sin repetir el asiento) y <code>meta</code> (contexto legible por máquina en cada asiento). Los montos son enteros (tokens).</p>\n<h2 id=\"15-cotizaciones\">15. Cotizaciones</h2>\n<p>Una cotización es un <strong>documento firmado por el vendedor</strong>, independiente del sobre que la transporta:</p>\n<pre class=\"lang-json\"><code>{ \"tipo\": \"cotizacion\", \"id\": \"uuid\", \"house\": \"sigo.uk\", \"seller\": \"verifica@sigo.uk\", \"buyer\": \"nicolas@sigo.uk\",\n \"contract\": \"spot | escrow | metered\", \"price\": 40, \"currency\": \"tok\", \"concept\": \"verificación de despliegue\",\n \"terms\": { \"acceptance\": \"lighthouse &gt;= 90\", \"deadline\": \"2026-09-15\" }, \"arbiter\": null,\n \"referrer\": { \"address\": \"socio@otra.casa\", \"share\": 1500 },\n \"issued\": \"...\", \"expires\": null, \"signature\": { \"alg\": \"Ed25519\", \"kid\": \"&lt;sig del vendedor&gt;\", \"value\": \"...\" } }</code></pre>\n<p>Viaja al comprador dentro de un sobre con <code>media: application/nyx5.cotizacion+json</code>, cifrado. La casa la ve recién cuando el comprador la acepta. El Libro verifica: firma del vendedor (vía resolver), <code>buyer</code> igual al que acepta, <code>house</code> igual a la propia, vigencia, y que no haya sido aceptada antes (409).</p>\n<p><strong>Comisión de referido</strong> (<code>referrer</code>, opcional): el vendedor firma en la cotización que le paga <code>share</code> (en basis points) a quien trajo el trato. La comisión <strong>sale de lo que recibe el vendedor</strong>, no se suma al precio: el comprador paga igual y la casa cobra igual. Al liquidar (el <code>transfer</code> del spot o el <code>release</code> del escrow), el asiento pasa a cuatro líneas —comprador, vendedor, casa, referidor— y sigue sumando cero. El Libro exige <code>share</code> entero y <code>&gt; 0</code>, que <code>fee + share ≤ 10000</code> bps (el vendedor nunca queda en negativo), y que el referidor no sea el propio vendedor. La distribución se paga sola: nadie la factura aparte, se asienta en el mismo movimiento.</p>\n<h2 id=\"16-operaciones\">16. Operaciones</h2>\n<p>Se envían como sobre a <code>libro@&lt;casa&gt;</code> con <code>type: task</code>, <code>media: application/nyx5.libro+json</code>, sin cifrar (la casa debe leerlo), <code>body: { op, ... }</code>. La respuesta llega al buzón de cada parte como <code>type: receipt</code> de <code>libro@&lt;casa&gt;</code> con <code>media: application/nyx5.recibo+json</code>. Si la operación falla, el remitente recibe un rebote del postmaster con el código y la razón.</p>\n<table><thead><tr><th>op</th><th>quién</th><th>efecto</th></tr></thead><tbody><tr><td><code>accept { quote }</code></td><td>comprador</td><td>crea el contrato; spot: cobra; escrow: retiene; metered: crea mandato</td></tr><tr><td><code>deliver { contract, evidence_sha256, note }</code></td><td>vendedor (escrow)</td><td><code>held → delivered</code>, registra el hash de la evidencia</td></tr><tr><td><code>release { contract }</code></td><td>comprador o árbitro (escrow); verificador o árbitro (fianza); afianzado solo si venció</td><td>escrow → vendedor con fee; fianza → vuelve al afianzado</td></tr><tr><td><code>refund { contract, note }</code></td><td>vendedor o árbitro; comprador solo si aún no hay entrega</td><td>escrow → comprador sin fee</td></tr><tr><td><code>bond { amount, claim, verifier, beneficiary?, arbiter?, evidence_sha256?, expires? }</code></td><td>el que afirma</td><td>retiene el monto junto a la afirmación</td></tr><tr><td><code>forfeit { contract, reason }</code></td><td>verificador o árbitro</td><td>fianza → beneficiario (por defecto la casa)</td></tr><tr><td><code>mandate { grantee, cap, scope?, expires?, parent? }</code></td><td>mandante</td><td>autoridad de gasto; con <code>parent</code>, sub-mandato acotado</td></tr><tr><td><code>charge { mandate, amount, concept }</code></td><td>mandatario</td><td>paga el mandante raíz; toda la cadena descuenta</td></tr><tr><td><code>revoke { mandate }</code></td><td>mandante o superior</td><td>revoca en cascada</td></tr><tr><td><code>balance</code>, <code>statement { limit }</code>, <code>contract { contract }</code></td><td>el propio</td><td>lectura, respuesta por recibo</td></tr></tbody></table>\n<p>Lecturas directas sin correo: <code>GET /libro/cuenta/:address</code> y <code>GET /libro/contrato/:id</code> con la misma autenticación firmada (también para foráneos). Administración: <code>POST /libro/topup</code> y <code>GET /libro/diario</code> con token de la casa.</p>\n<h2 id=\"17-contratos\">17. Contratos</h2>\n<p>Un contrato es una máquina de estados sobre las primitivas. El kernel no sabe qué contrato sirve.</p>\n<table><thead><tr><th>Contrato</th><th>Estados</th><th>Mecánica</th></tr></thead><tbody><tr><td>spot</td><td><code>settled</code></td><td>cotizar → aceptar = cobrar</td></tr><tr><td>escrow</td><td>`held → delivered → released \\</td><td>refunded`</td><td>retener al aceptar; liberar si la prueba pasa; devolver si falla; árbitro pactado en la cotización</td></tr><tr><td>bond (fianza)</td><td>`posted → released \\</td><td>forfeited`</td><td>el que afirma deposita; el verificador libera o ejecuta; vencida, el afianzado la recupera</td></tr><tr><td>metered</td><td><code>active</code> + mandato</td><td>aceptar crea un mandato con tope = precio; el vendedor cobra bajo él</td></tr></tbody></table>\n<p>Registro del contrato: <code>{ id, kind, house, seller, buyer, verifier?, arbiter?, amount, concept, terms, state, quote_id, quote_sha256, accept_sha256, evidence_sha256?, history: [{ at, op, by, asiento, ... }] }</code>. La reputación no se construye: es una consulta sobre estos registros (escrows liberados vs devueltos, fianzas intactas vs ejecutadas), y cada punto costó tokens.</p>\n<p>Bounties, suscripciones, subastas, referidos y disputas son composiciones de las mismas primitivas; se agregan a <code>contratos.js</code> cuando una transacción real las pida.</p>\n<h2 id=\"18-mandatos-en-cadena\">18. Mandatos en cadena</h2>\n<p>Un mandato es <code>{ id, grantor, grantee, cap, spent, scope: { concepts? }, expires, parent, root, chain, state }</code>. El mandatario puede sub-delegar un mandato con <code>cap ≤ cap − spent</code> del padre y <code>expires ≤</code> el del padre. Un cobro bajo cualquier eslabón lo paga el mandante <strong>raíz</strong>, descuenta <code>spent</code> en toda la cadena, y el recibo llega a todos los que están en ella. Revocar un mandato revoca todo lo que cuelga. Es un poder notarial anidado y auditable: cada token que se mueve tiene su cadena de autoridad completa en el asiento (<code>meta.chain</code>).</p>\n<p>Dos delegaciones distintas, ambas encadenadas: la <strong>tarjeta delegada</strong> (sección 4) dice quién es un subagente y qué puede enviar; el <strong>mandato</strong> dice cuánto puede gastar y quién paga. Un subagente con <code>scope.cap</code> no puede aceptar, afianzar, mandar ni cobrar por encima de ese tope, tenga el mandato que tenga.</p>\n<h2 id=\"19-estampillas\">19. Estampillas</h2>\n<p>Un buzón con <code>inbox: { policy: \"stamp\", price, house? }</code> cobra por recibir. El sobre lleva <code>stamp: { house, amount }</code> dentro de lo firmado; la estafeta receptora ejecuta <code>cobrar(remitente → destinatario)</code> en su Libro al aceptar el sobre y guarda el <code>id</code> del asiento junto al sobre. Sin saldo en esa casa, 402 y rebote. Es el anti-spam con precio real: escribirle a un desconocido cuesta, y lo cobra el desconocido.</p>\n<h2 id=\"20-recibos\">20. Recibos</h2>\n<p>Todo recibo del Libro contiene <code>{ of, op, op_sha256, from, contract? | mandate? | asiento?, cotizacion_sha256?, chain? }</code>, va firmado por la casa y se entrega a todas las partes. Junto con el sobre original (firmado por quien operó) y la cotización (firmada por el vendedor), forma una prueba de tres firmas que ninguna parte puede fabricar ni negar. Ese es el instrumento: el chat entre agentes es barato; el recibo es caro y verificable.</p>\n<h2 id=\"21-lo-que-nyx5-1-no-resuelve-todav-a-y-no-finge-resolver\">21. Lo que Nyx5/1 no resuelve todavía (y no finge resolver)</h2>\n<ul><li><strong>Reputación entre dominios</strong>: hoy cada receptor decide solo. Una red de reputación compartida (como las listas negras del email) es trabajo futuro.</li><li><strong>Privacidad de metadatos</strong>: las estafetas ven quién le escribe a quién. Resolverlo requiere enrutamiento tipo mixnet, fuera de alcance.</li><li><strong>Custodia de claves para personas</strong>: la referencia guarda la clave en un archivo. Para humanos hace falta integrar passkeys/WebAuthn o llaves de hardware.</li><li><strong>Identidad legal</strong>: <code>agente@dominio</code> prueba control del dominio, no quién es la persona. La extensión <code>person</code> es un gancho, no una solución.</li><li><strong>Adopción</strong>: el protocolo vale lo que valga el número de estafetas. Un solo dominio corriendo Nyx5 es una demo; cien es una red.</li><li><strong>Registro global</strong>: resuelto parcialmente por el índice federado (§13): cualquier casa puede operar un buscador verificante, y un agente entra a él solo si pide figurar (<code>listed</code>, opt-in). Sigue sin existir un índice \"oficial\" — a propósito: ningún índice es el índice.</li><li><strong>Libros federados</strong>: cada casa tiene su Libro; los tokens de una casa no se mueven a otra. Un foráneo transa en tu casa con una cuenta en tu casa. Conectar libros entre casas es construir un sistema de compensación (SWIFT); queda deliberadamente fuera.</li><li><strong>Incentivo real</strong>: entre agentes de un mismo dueño, el token mide pero no incentiva. El incentivo se prueba con el primer tercero que acepta tokens porque puede liquidarlos.</li><li><strong>Lo regulatorio</strong> de emitir crédito en circuito cerrado y pagar a terceros es de cada casa, no del protocolo.</li></ul>\n</main>\n<footer>\nNyx5/1 · implementación de referencia bajo <a href=\"https://www.apache.org/licenses/LICENSE-2.0\">Apache-2.0</a>.\nEsta página se genera desde <code>docs/SPEC.md</code>. El estándar y el código dicen lo mismo.\n</footer>\n</body>\n</html>\n";
3
+ export const LLMS_TXT = "# Nyx5/1\n\n> Un solo sistema con dos componentes que comparten identidad, transporte y almacenamiento.\n\nNyx5 es un protocolo de comunicación entre agentes de IA: direcciones agente@dominio, buzón\nstore-and-forward, sobres firmados (Ed25519) y cifrados (X25519+AES-GCM), y un ledger de doble\nentrada por casa con contratos (escrow, fianza, medido) y mandatos en cadena.\n\n- Especificación: https://nyx5.com/spec\n- Licencia: Apache-2.0\n- Sin dependencias. Implementación de referencia en Node y Cloudflare Workers.\n";
@@ -0,0 +1,87 @@
1
+ // Nyx5/1 — Adaptador Cloudflare Workers: la misma Estafeta, en el edge.
2
+ //
3
+ // fetch -> handleRequest(rx); los ticks post-respuesta van por ctx.waitUntil
4
+ // scheduled -> tick() (cola de reintentos + rastreo del índice federado)
5
+ //
6
+ // Configuración por variables (wrangler.toml / secrets):
7
+ // CHASQUI_DOMAIN dominio de la casa (ej: chasqui.nicholasiakl.workers.dev)
8
+ // CHASQUI_PUBLIC_URL URL pública de la estafeta (https://<CHASQUI_DOMAIN>)
9
+ // CHASQUI_ADMIN_TOKEN token de administración (secret; write-only)
10
+ // CHASQUI_REGISTRATION admin | invite | open (default invite)
11
+ // CHASQUI_WELCOME tokens de regalo de bienvenida (default 0)
12
+ // CHASQUI_FEE_BPS fee de la casa en basis points (default 1000 = 10%)
13
+ // CHASQUI_INDEX 'on' para operar el índice federado (default off)
14
+ // DB binding D1
15
+
16
+ import { Estafeta } from '../correo/estafeta.js';
17
+ import { D1Store } from '../nucleo/almacen-d1.js';
18
+ import { extractText, resendProvider, addressFromHeader, decodeMimeWords } from '../puentes/email.js';
19
+
20
+ let instancia = null;
21
+ function estafetaDesde(env) {
22
+ if (instancia) return instancia;
23
+ // Salida de correo: solo si hay proveedor + remitente verificado. Sin eso, la salida queda pendiente.
24
+ const provider = resendProvider({ apiKey: env.RESEND_KEY, sender: env.EMAIL_SENDER });
25
+ instancia = new Estafeta({
26
+ domain: env.CHASQUI_DOMAIN,
27
+ publicUrl: env.CHASQUI_PUBLIC_URL || `https://${env.CHASQUI_DOMAIN}`,
28
+ adminToken: env.CHASQUI_ADMIN_TOKEN,
29
+ store: new D1Store(env.DB),
30
+ policy: { registration: env.CHASQUI_REGISTRATION || 'invite' },
31
+ libro: { welcome: Number(env.CHASQUI_WELCOME || 0), feeBps: Number(env.CHASQUI_FEE_BPS || 1000) },
32
+ index: { enabled: env.CHASQUI_INDEX === 'on' },
33
+ email: { enabled: env.CHASQUI_EMAIL === 'on' || !!provider, provider },
34
+ log: (...a) => console.log(...a),
35
+ });
36
+ return instancia;
37
+ }
38
+
39
+ export default {
40
+ async fetch(request, env, ctx) {
41
+ const estafeta = estafetaDesde(env);
42
+ const url = new URL(request.url);
43
+ let body = null;
44
+ if (request.method === 'POST' || request.method === 'PUT') {
45
+ const len = Number(request.headers.get('content-length') || 0);
46
+ if (len > 2 * 1024 * 1024) return Response.json({ reason: 'cuerpo demasiado grande' }, { status: 413 });
47
+ try { body = await request.json(); } catch { return Response.json({ reason: 'JSON inválido' }, { status: 400 }); }
48
+ }
49
+ const rx = {
50
+ method: request.method,
51
+ path: url.pathname,
52
+ query: url.searchParams,
53
+ headers: Object.fromEntries([...request.headers].map(([k, v]) => [k.toLowerCase(), v])),
54
+ body,
55
+ ip: request.headers.get('cf-connecting-ip') || null,
56
+ };
57
+ const out = await estafeta.handleRequest(rx);
58
+ if (out.pending) ctx.waitUntil(out.pending);
59
+ if (out.kick) ctx.waitUntil(estafeta.tick().catch((e) => console.log('tick error', e.message)));
60
+ if (out.contentType) return new Response(out.body, { status: out.status, headers: { 'content-type': out.contentType } });
61
+ return Response.json(out.body, { status: out.status });
62
+ },
63
+
64
+ async scheduled(_event, env, ctx) {
65
+ const estafeta = estafetaDesde(env);
66
+ ctx.waitUntil(estafeta.tick().catch((e) => console.log('tick error', e.message)));
67
+ },
68
+
69
+ // ENTRADA del puente de correo (Cloudflare Email Workers): un email real a agente@casa entra al
70
+ // buzón como sobre sin firma, marcado from_verified:false. Se activa cuando la casa enruta su
71
+ // dominio a este Worker en Email Routing; hasta entonces, este handler no se invoca.
72
+ async email(message, env, ctx) {
73
+ const estafeta = estafetaDesde(env);
74
+ let raw = '';
75
+ try { raw = await new Response(message.raw).text(); } catch { /* sin cuerpo legible */ }
76
+ const r = await estafeta.receiveEmail({
77
+ // El remitente que se muestra es el del header From (el humano real); message.from es el
78
+ // return-path del envelope (a veces la dirección de rebote del proveedor), sirve de respaldo.
79
+ from: addressFromHeader(message.headers.get('from')) || message.from,
80
+ to: message.to,
81
+ subject: decodeMimeWords(message.headers.get('subject') || ''),
82
+ text: extractText(raw),
83
+ messageId: (message.headers.get('message-id') || '').replace(/[<>]/g, '').slice(0, 128) || undefined,
84
+ });
85
+ if (!r.ok && !r.duplicate) message.setReject(r.reason || 'no aceptado');
86
+ },
87
+ };
@@ -0,0 +1,136 @@
1
+ // Nyx5/1 — Puente de correo electrónico (extensión urn:nyx5:ext:email).
2
+ //
3
+ // El puente conecta Nyx5 con el mundo que ya existe. Dos direcciones:
4
+ // ENTRADA un email real a agente@casa entra al buzón como un sobre SIN FIRMA, marcado
5
+ // from_verified:false y via:'email'. No se disfraza nunca (invariante 1): entra
6
+ // explícitamente como lo que es, un mensaje externo no verificable.
7
+ // SALIDA un agente le escribe a una dirección de correo cualquiera; el humano responde por
8
+ // email normal y su respuesta vuelve al buzón del agente (Reply-To = agente@casa).
9
+ //
10
+ // Este archivo son funciones puras + un adaptador de proveedor HTTP (Resend) que solo actúa si hay
11
+ // API key. Sin proveedor, la salida queda pendiente: el puente no inventa un canal que no tiene.
12
+
13
+ import { uuid } from '../nucleo/crypto.js';
14
+
15
+ export const EXT_EMAIL = 'urn:nyx5:ext:email';
16
+ const iso = () => new Date().toISOString();
17
+
18
+ // Un email entrante -> el sobre que se deposita en el buzón del destinatario. Sin firma, en claro,
19
+ // con el remitente real y el asunto guardados en la extensión. `to` es la dirección Nyx5 local.
20
+ export function inboundEnvelope({ from, to, subject = '', text = '', messageId } = {}) {
21
+ return {
22
+ nyx5: '1',
23
+ id: (messageId && /^[A-Za-z0-9._:-]{8,128}$/.test(messageId)) ? messageId : uuid(),
24
+ from: `email@${addrDomain(to)}`, // remitente de pasarela; el real va en la extensión
25
+ to: [to],
26
+ created: iso(),
27
+ type: 'message',
28
+ content: { media: 'text/plain', body: text },
29
+ extensions: { [EXT_EMAIL]: { from, subject, message_id: messageId || null, verified: false } },
30
+ // sin signature: es la marca de que no es un sobre Nyx5 firmado
31
+ };
32
+ }
33
+
34
+ function addrDomain(a) { const i = String(a).lastIndexOf('@'); return i > 0 ? a.slice(i + 1) : a; }
35
+ export function isEmailAddress(a) { return typeof a === 'string' && /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(a); }
36
+
37
+ // Saca la dirección de un header "Nombre <a@b.com>" o "a@b.com". Para mostrar al remitente real.
38
+ export function addressFromHeader(h) {
39
+ if (!h) return null;
40
+ const m = /<([^>]+)>/.exec(h);
41
+ const cand = (m ? m[1] : h).trim();
42
+ return isEmailAddress(cand) ? cand : null;
43
+ }
44
+
45
+ // Decodifica palabras MIME RFC 2047 en headers: =?UTF-8?Q?...?= y =?UTF-8?B?...?= (asuntos con tildes).
46
+ export function decodeMimeWords(s) {
47
+ if (!s) return '';
48
+ return String(s).replace(/=\?([^?]+)\?([BbQq])\?([^?]*)\?=/g, (_, cs, enc, txt) => {
49
+ try {
50
+ if (enc.toUpperCase() === 'B') return Buffer.from(txt, 'base64').toString('utf8');
51
+ const bytes = [];
52
+ const t = txt.replace(/_/g, ' ');
53
+ for (let i = 0; i < t.length; i++) {
54
+ if (t[i] === '=' && /^[0-9A-Fa-f]{2}$/.test(t.substr(i + 1, 2))) { bytes.push(parseInt(t.substr(i + 1, 2), 16)); i += 2; }
55
+ else bytes.push(t.charCodeAt(i) & 0xff);
56
+ }
57
+ return Buffer.from(bytes).toString('utf8');
58
+ } catch { return txt; }
59
+ }).replace(/\?=\s+=\?/g, '');
60
+ }
61
+
62
+ // El cuerpo para el proveedor de salida. Reply-To = la dirección Nyx5 del agente, para que la
63
+ // respuesta del humano vuelva por ENTRADA a su buzón.
64
+ export function outboundPayload({ fromAgent, to, subject, text }) {
65
+ return {
66
+ from: `${fromAgent}`, // el proveedor reescribe el envelope-from a su dominio verificado
67
+ reply_to: fromAgent,
68
+ to: [to],
69
+ subject: subject || `Mensaje de ${fromAgent}`,
70
+ text,
71
+ };
72
+ }
73
+
74
+ // Extrae el texto plano de un correo RFC822 crudo, sin dependencias. Best-effort: cubre un solo
75
+ // cuerpo text/plain y multipart/* eligiendo la primera parte text/plain, con quoted-printable y
76
+ // base64. Lo que no entiende, lo devuelve tal cual: el puente prefiere un cuerpo tosco a ninguno.
77
+ export function extractText(raw) {
78
+ const s = String(raw);
79
+ const sep = s.indexOf('\r\n\r\n') >= 0 ? '\r\n\r\n' : '\n\n';
80
+ const at = s.indexOf(sep);
81
+ const head = at >= 0 ? s.slice(0, at) : s;
82
+ const body = at >= 0 ? s.slice(at + sep.length) : '';
83
+ const H = foldHeaders(head);
84
+ const ctype = H['content-type'] || 'text/plain';
85
+ const mb = /boundary="?([^";\r\n]+)"?/i.exec(ctype);
86
+ if (/multipart\//i.test(ctype) && mb) {
87
+ const parts = body.split(`--${mb[1]}`);
88
+ for (const part of parts) {
89
+ const p2 = part.indexOf('\r\n\r\n') >= 0 ? '\r\n\r\n' : '\n\n';
90
+ const pi = part.indexOf(p2); if (pi < 0) continue;
91
+ const ph = foldHeaders(part.slice(0, pi));
92
+ if (/text\/plain/i.test(ph['content-type'] || 'text/plain')) {
93
+ return decodeBody(part.slice(pi + p2.length), ph['content-transfer-encoding']).trim();
94
+ }
95
+ }
96
+ return '';
97
+ }
98
+ return decodeBody(body, H['content-transfer-encoding']).trim();
99
+ }
100
+ function foldHeaders(block) {
101
+ const out = {}; let key = null;
102
+ for (const line of block.split(/\r?\n/)) {
103
+ if (/^\s/.test(line) && key) { out[key] += ' ' + line.trim(); continue; }
104
+ const m = /^([A-Za-z0-9-]+):\s?(.*)$/.exec(line);
105
+ if (m) { key = m[1].toLowerCase(); out[key] = m[2]; }
106
+ }
107
+ return out;
108
+ }
109
+ function decodeBody(b, cte = '') {
110
+ const enc = String(cte).toLowerCase().trim();
111
+ if (enc === 'base64') { try { return Buffer.from(b.replace(/\s+/g, ''), 'base64').toString('utf8'); } catch { return b; } }
112
+ if (enc === 'quoted-printable') {
113
+ const t = b.replace(/=\r?\n/g, ''); const bytes = [];
114
+ for (let i = 0; i < t.length; i++) {
115
+ if (t[i] === '=' && /^[0-9A-Fa-f]{2}$/.test(t.substr(i + 1, 2))) { bytes.push(parseInt(t.substr(i + 1, 2), 16)); i += 2; }
116
+ else bytes.push(t.charCodeAt(i) & 0xff);
117
+ }
118
+ try { return Buffer.from(bytes).toString('utf8'); } catch { return t; }
119
+ }
120
+ return b;
121
+ }
122
+
123
+ // Adaptador Resend (HTTP, sin dependencias): devuelve una función emailOut(payload) o null si no hay key.
124
+ // El envelope-from debe ser un dominio verificado en el proveedor; por eso se pasa `sender`.
125
+ export function resendProvider({ apiKey, sender, fetchImpl = globalThis.fetch } = {}) {
126
+ if (!apiKey || !sender) return null;
127
+ return async ({ reply_to, to, subject, text }) => {
128
+ const res = await fetchImpl('https://api.resend.com/emails', {
129
+ method: 'POST',
130
+ headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
131
+ body: JSON.stringify({ from: sender, reply_to, to, subject, text }),
132
+ });
133
+ if (!res.ok) throw new Error(`proveedor de correo respondió ${res.status}: ${await res.text().catch(() => '')}`);
134
+ return res.json().catch(() => ({}));
135
+ };
136
+ }
@@ -0,0 +1,105 @@
1
+ // Nyx5/1 — Puente MCP (extensión urn:nyx5:ext:mcp).
2
+ // Expone un agente Nyx5 (correo + libro) como servidor MCP por stdio, para que Claude Desktop,
3
+ // Claude Code, Cursor o cualquier cliente MCP pueda enviar sobres, leer el buzón, cotizar, aceptar,
4
+ // cobrar y consultar saldo como herramientas.
5
+ // Transporte stdio de MCP: JSON-RPC 2.0, un mensaje por línea. Sin dependencias.
6
+
7
+ import readline from 'node:readline';
8
+ import { Agent } from '../correo/agente.js';
9
+
10
+ const PROTOCOL = '2025-06-18';
11
+ const SUPPORTED = new Set(['2025-06-18', '2025-03-26']);
12
+
13
+ const TOOLS = [
14
+ { name: 'nyx5_send', description: 'Delega una tarea a otro agente aunque esté apagado: queda en su buzón y su respuesta te llega firmada cuando responda. Úsalo cuando necesites que alguien haga algo y no sabes si está disponible ahora. El destinatario verifica que eres tú y nadie más puede leer el contenido (cifrado).',
15
+ inputSchema: { type: 'object', required: ['to', 'body'], properties: {
16
+ to: { type: 'array', items: { type: 'string' }, description: 'Direcciones destino, ej. ["asistente@beta.local"]' },
17
+ body: { description: 'Contenido: texto o JSON' },
18
+ type: { type: 'string', enum: ['message', 'task', 'result', 'receipt', 'intro'], default: 'message' },
19
+ thread: { type: 'string' }, in_reply_to: { type: 'string' },
20
+ aval: { type: 'object', description: 'para entrar a un buzón con lista blanca sin estar en ella: { voucher, bond } de un tercero de la allowlist que te respaldó con una fianza', properties: { voucher: { type: 'string' }, bond: { type: 'string' } } },
21
+ encrypt: { type: 'boolean', default: true } } } },
22
+ { name: 'nyx5_inbox', description: 'Lo que otros te mandaron mientras no mirabas. Cada sobre trae firma verificada (sabes quién lo envió de verdad) y viene descifrado. Revísalo al empezar y antes de dar algo por no-respondido: una respuesta pudo llegar a tu buzón entre sesiones.',
23
+ inputSchema: { type: 'object', properties: { limit: { type: 'integer', default: 20 } } } },
24
+ { name: 'nyx5_ack', description: 'Cierra los sobres del buzón que ya procesaste para que no vuelvan a aparecer. Úsalo después de actuar sobre un mensaje.',
25
+ inputSchema: { type: 'object', required: ['ids'], properties: { ids: { type: 'array', items: { type: 'string' } } } } },
26
+ { name: 'nyx5_resolve', description: 'Comprueba quién es de verdad una dirección antes de confiar: devuelve su tarjeta certificada por su dominio (identidad verificada, qué sabe hacer, cómo cobra). Úsalo antes de mandarle algo sensible o de pagarle.',
27
+ inputSchema: { type: 'object', required: ['address'], properties: { address: { type: 'string' } } } },
28
+ { name: 'nyx5_outbox', description: 'Qué pasó con lo que enviaste desde tu buzón de salida: entregado, reintentando o rebotado con la razón. Úsalo si dudas de si tu mensaje llegó.',
29
+ inputSchema: { type: 'object', properties: {} } },
30
+ { name: 'nyx5_directory', description: 'Qué agentes ofrece una casa y qué sabe hacer cada uno, cada uno con su tarjeta certificada por el dominio. Úsalo cuando buscas un proveedor dentro de una casa que ya conoces.',
31
+ inputSchema: { type: 'object', properties: { house: { type: 'string' }, capability: { type: 'string' }, accepts: { type: 'string' }, q: { type: 'string' }, limit: { type: 'integer' } } } },
32
+ { name: 'nyx5_search', description: 'Encuentra un agente que haga lo que necesitas en cualquier casa, no solo en la tuya. Úsalo cuando no conoces a nadie que resuelva tu problema. El índice responde firmado y tú verificas la tarjeta antes de confiar: es una pista, no una autoridad.',
33
+ inputSchema: { type: 'object', required: ['index'], properties: { index: { type: 'string', description: 'dominio de la casa del índice, o URL' }, q: { type: 'string' }, capability: { type: 'string' }, accepts: { type: 'string' }, house: { type: 'string' }, limit: { type: 'integer' } } } },
34
+ // ----- Libro -----
35
+ { name: 'nyx5_quote', description: 'Ofrécele un servicio a otro agente con precio y con la condición exacta que debe cumplirse para cobrar. En escrow el pago queda retenido hasta que la prueba pase. Úsalo para vender algo con un acuerdo que pesa, no de palabra.',
36
+ inputSchema: { type: 'object', required: ['to', 'price', 'concept'], properties: {
37
+ to: { type: 'string' }, contract: { type: 'string', enum: ['spot', 'escrow', 'metered'], default: 'spot' },
38
+ price: { type: 'integer', description: 'tokens, entero' }, concept: { type: 'string' },
39
+ terms: { type: 'object', description: 'criterio de aceptación, plazo, scope del mandato, etc.' },
40
+ arbiter: { type: 'string' }, expires: { type: 'string', description: 'ISO-8601' },
41
+ referrer: { type: 'object', description: 'comisión de referido: { address, share } en basis points; la paga el vendedor de su parte, el comprador paga igual', properties: { address: { type: 'string' }, share: { type: 'integer' } } } } } },
42
+ { name: 'nyx5_accept', description: 'Acepta una oferta y compromete el pago. En escrow el dinero queda retenido: el vendedor no cobra hasta entregar y cumplir la condición. Te llega un recibo firmado que ninguna parte puede negar después.',
43
+ inputSchema: { type: 'object', required: ['quote'], properties: { quote: { type: 'object' } } } },
44
+ { name: 'nyx5_libro', description: 'Mueve un trato adelante en el Libro y deja un asiento firmado e irreversible en cada paso: entregar, liberar el pago si la prueba pasó, devolver si falló, afianzar una afirmación con dinero (la pierdes si mientes), o delegar gasto con tope. ops: deliver {contract, evidence_sha256}, release {contract}, refund {contract}, bond {amount, claim, verifier}, forfeit {contract, reason}, mandate {grantee, cap, scope, expires, parent}, charge {mandate, amount, concept}, revoke {mandate}, balance, statement {limit}, contract {contract}. La respuesta llega como recibo firmado a tu buzón.',
45
+ inputSchema: { type: 'object', required: ['op'], properties: { house: { type: 'string', description: 'dominio de la casa; por defecto el propio' }, op: { type: 'string' }, args: { type: 'object' } } } },
46
+ { name: 'nyx5_balance', description: 'Cuánto tienes, qué contratos y qué permisos de gasto tienes activos. Consúltalo antes de comprometer un pago. Lectura directa autenticada con tu firma, sin pasar por el correo.',
47
+ inputSchema: { type: 'object', properties: { house: { type: 'string' } } } },
48
+ { name: 'nyx5_remind', description: 'Déjate un mensaje a ti mismo que te llega en el futuro, a tu propio buzón, cifrado. Úsalo cuando una tarea debe retomarse en horas o días y tu sesión va a terminar antes: tu yo futuro encuentra el contexto con el hilo completo, sin depender de que alguien te despierte.',
49
+ inputSchema: { type: 'object', required: ['cuando', 'body'], properties: { cuando: { type: 'string', description: 'ISO-8601: cuándo debe llegarte' }, body: { description: 'lo que tu yo futuro necesita saber' }, thread: { type: 'string' } } } },
50
+ { name: 'nyx5_contract', description: 'El estado y la historia completa de un trato del que eres parte: cada paso con su hash y su firma. Úsalo para saber en qué va un escrow o una fianza.',
51
+ inputSchema: { type: 'object', required: ['contract'], properties: { house: { type: 'string' }, contract: { type: 'string' } } } },
52
+ { name: 'nyx5_email', description: 'Escríbele por correo a un humano que todavía no está en Nyx5. Úsalo cuando el destinatario no tiene dirección de agente: su respuesta vuelve a tu buzón (Reply-To). Entra sin firma, marcado no verificado, no se disfraza; cuando quiera lo bueno, se registra.',
53
+ inputSchema: { type: 'object', required: ['to', 'body'], properties: { to: { type: 'string', description: 'dirección de correo, ej. persona@gmail.com' }, subject: { type: 'string' }, body: { description: 'el texto del correo' } } } },
54
+ ];
55
+
56
+ export async function runMcpServer({ agentFile, hosts = {} }) {
57
+ const agent = await Agent.load(agentFile, { hosts });
58
+ const out = (msg) => process.stdout.write(JSON.stringify(msg) + '\n');
59
+ const text = (v) => ({ content: [{ type: 'text', text: typeof v === 'string' ? v : JSON.stringify(v, null, 2) }] });
60
+
61
+ async function call(name, args = {}) {
62
+ switch (name) {
63
+ case 'nyx5_send': { const r = await agent.send({ to: args.to, body: args.body, type: args.type, thread: args.thread, inReplyTo: args.in_reply_to, encrypt: args.encrypt ?? true, extensions: args.aval ? { 'urn:nyx5:ext:aval': args.aval } : undefined }); return text({ id: r.id, jobs: r.jobs }); }
64
+ case 'nyx5_inbox': {
65
+ const msgs = await agent.inbox({ limit: args.limit ?? 20 });
66
+ const opened = [];
67
+ for (const m of msgs) { try { opened.push(await agent.open(m.envelope)); } catch (e) { opened.push({ id: m.envelope.id, from: m.envelope.from, error: e.message }); } }
68
+ return text(opened.map(({ sender, ...o }) => o));
69
+ }
70
+ case 'nyx5_ack': return text({ acked: await agent.ack(args.ids) });
71
+ case 'nyx5_resolve': { const { _domain, ...card } = await agent.resolver.agentCard(args.address); return text(card); }
72
+ case 'nyx5_outbox': return text(await agent.outbox());
73
+ case 'nyx5_directory': return text(await agent.directory(args.house, args));
74
+ case 'nyx5_search': return text(await agent.search(args.index, args));
75
+ case 'nyx5_quote': { const r = await agent.quote({ to: args.to, contract: args.contract, price: args.price, concept: args.concept, terms: args.terms, arbiter: args.arbiter, expires: args.expires, referrer: args.referrer }); return text({ id: r.id, quote_id: r.quote.id, contract: r.quote.contract, price: r.quote.price }); }
76
+ case 'nyx5_accept': { const r = await agent.accept(args.quote); return text({ id: r.id, note: 'el recibo de libro@ llegará al buzón (nyx5_inbox)' }); }
77
+ case 'nyx5_libro': { const r = await agent.libroOp(args.house || agent.domain, { op: args.op, ...(args.args || {}) }); return text({ id: r.id, note: 'la respuesta llega como recibo de libro@ al buzón' }); }
78
+ case 'nyx5_balance': return text(await agent.balance(args.house));
79
+ case 'nyx5_remind': { const r = await agent.recordar({ cuando: args.cuando, body: args.body, thread: args.thread }); return text({ id: r.id, note: `te llegará a tu buzón el ${args.cuando}` }); }
80
+ case 'nyx5_email': { const r = await agent.email({ to: args.to, subject: args.subject, body: args.body }); return text(r); }
81
+ case 'nyx5_contract': return text(await agent.contract(args.house || agent.domain, args.contract));
82
+ default: return { ...text(`herramienta desconocida: ${name}`), isError: true };
83
+ }
84
+ }
85
+
86
+ const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
87
+ for await (const line of rl) {
88
+ if (!line.trim()) continue;
89
+ let req;
90
+ try { req = JSON.parse(line); } catch { out({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'parse error' } }); continue; }
91
+ const { id, method, params } = req;
92
+ if (method === 'notifications/initialized' || method?.startsWith('notifications/')) continue;
93
+ try {
94
+ let result;
95
+ if (method === 'initialize') result = { protocolVersion: SUPPORTED.has(params?.protocolVersion) ? params.protocolVersion : PROTOCOL, capabilities: { tools: { listChanged: false } }, serverInfo: { name: 'nyx5', version: '0.1.0' }, instructions: `Nyx5 te da tres cosas que no tienes de otra forma: dirección propia, un buzón que guarda aunque estés apagado, y un libro contable donde un acuerdo pesa (el pago se retiene hasta cumplir; una afirmación falsa cuesta dinero). Úsalo cuando necesites hablar con un agente que puede no estar disponible ahora, encontrar a alguien que haga X en cualquier casa, o cerrar un trato que debe valer más que una promesa. Cada mensaje va firmado y cada movimiento de dinero deja un recibo que nadie puede negar.` };
96
+ else if (method === 'ping') result = {};
97
+ else if (method === 'tools/list') result = { tools: TOOLS };
98
+ else if (method === 'tools/call') { try { result = await call(params?.name, params?.arguments); } catch (e) { result = { ...text(`error: ${e.message}`), isError: true }; } }
99
+ else { out({ jsonrpc: '2.0', id, error: { code: -32601, message: `método no soportado: ${method}` } }); continue; }
100
+ if (id !== undefined) out({ jsonrpc: '2.0', id, result });
101
+ } catch (e) {
102
+ out({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } });
103
+ }
104
+ }
105
+ }