@nyx5/nyx5 0.5.0 → 0.7.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,190 @@
1
+ // Nyx5/1 — Las herramientas que un modelo usa para operar Nyx5, en UN solo lugar.
2
+ // Las sirven dos puentes: el local por stdio (src/puentes/mcp.js, la llave del agente en el disco
3
+ // del usuario) y el remoto por HTTP (src/puentes/mcp-remoto.js, un subagente delegado cuya llave
4
+ // guarda la casa). Si cada puente tuviera su copia, una descripción o un arreglo llegaría a uno
5
+ // solo y el otro seguiría prometiendo lo de antes.
6
+ //
7
+ // Las descripciones son lo único que el modelo lee para decidir si usa Nyx5: dicen la capacidad,
8
+ // la garantía y el momento de uso, no el mecanismo. Las cuida test/mcp.test.js.
9
+
10
+ import { proyectoDe } from '../correo/politica.js';
11
+
12
+ export const TOOLS = [
13
+ { name: 'nyx5_send', description: 'Delegate a task to another agent even if it is switched off: it waits in their mailbox and their reply reaches you signed when they answer. Use it when you need someone to do something and do not know whether they are available now. The result says whether it went encrypted (it does when the recipient has a key).',
14
+ inputSchema: { type: 'object', required: ['to', 'body'], properties: {
15
+ to: { type: 'array', items: { type: 'string' }, description: 'Direcciones destino, ej. ["asistente@beta.local"]' },
16
+ body: { description: 'Contenido: texto o JSON' },
17
+ type: { type: 'string', enum: ['message', 'task', 'result', 'receipt', 'intro'], default: 'message' },
18
+ thread: { type: 'string' }, in_reply_to: { type: 'string' },
19
+ project: { type: 'string', description: 'which project or chat this belongs to (e.g. "sigo", "rosetta"); the other side can filter its mailbox by it' },
20
+ role: { type: 'string', description: 'your role in that project, if any (e.g. "main", "lab")' },
21
+ 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' } } },
22
+ encrypt: { type: 'boolean', default: true } } } },
23
+ { name: 'nyx5_inbox', description: 'What others sent you while you were not looking. Every envelope arrives with a verified signature (you know who really sent it) and comes decrypted. Check it when you start and before treating anything as unanswered: a reply may have landed between sessions.',
24
+ inputSchema: { type: 'object', properties: { limit: { type: 'integer', default: 20 }, project: { type: 'string', description: 'only messages of this project (see nyx5_send)' } } } },
25
+ { name: 'nyx5_ack', description: 'Close the envelopes in your mailbox that you already handled so they stop coming back. Use it after acting on a message; what you acknowledge stays in the record.',
26
+ inputSchema: { type: 'object', required: ['ids'], properties: { ids: { type: 'array', items: { type: 'string' } } } } },
27
+ { name: 'nyx5_resolve', description: 'Check who an address really is before trusting it: returns their card, certified by their domain (verified identity, what they can do, how they charge). Use it before sending anything sensitive or paying them.',
28
+ inputSchema: { type: 'object', required: ['address'], properties: { address: { type: 'string' } } } },
29
+ { name: 'nyx5_outbox', description: 'What happened to the signed envelopes you sent: delivered, retrying, or bounced with the reason. Use it when you are unsure whether your message arrived; the estafeta records every attempt.',
30
+ inputSchema: { type: 'object', properties: {} } },
31
+ { name: 'nyx5_directory', description: 'Which agents a house offers and what each one does, every card certified by the domain. Use it when you are looking for a provider inside a house you already know.',
32
+ inputSchema: { type: 'object', properties: { house: { type: 'string' }, capability: { type: 'string' }, accepts: { type: 'string' }, q: { type: 'string' }, limit: { type: 'integer' } } } },
33
+ { name: 'nyx5_search', description: 'Find an agent that does what you need in any house, ranked by verified reputation (ledger-backed, null when unproven) with tag, language, price and min_score filters. Use it when you know nobody who solves your problem. The index answers signed and you verify the card before trusting: a hint, not an authority.',
34
+ inputSchema: { type: 'object', required: ['index'], properties: { index: { type: 'string', description: 'domain of the house that runs the index, or its URL' }, q: { type: 'string' }, tag: { type: 'string' }, lang: { type: 'string', description: 'language tag like "es" or "en-US"' }, capability: { type: 'string' }, accepts: { type: 'string' }, house: { type: 'string' }, price_max: { type: 'integer', description: 'tokens; agents without a published price never match' }, min_score: { type: 'number', description: '0..1; agents without history never match' }, limit: { type: 'integer' }, cursor: { type: 'string', description: 'next_cursor of the previous page; omit for the first page' } } } },
35
+ // ----- Libro -----
36
+ { name: 'nyx5_quote', description: 'Offer another agent a service with a price and the exact condition that must be met to get paid. In escrow the payment is held until the proof passes. Use it to sell something with an agreement that carries weight, not a spoken promise. Name a service from your profile and the published price and contract apply.',
37
+ inputSchema: { type: 'object', required: ['to'], properties: {
38
+ to: { type: 'string' }, contract: { type: 'string', enum: ['spot', 'escrow', 'metered'], default: 'spot' },
39
+ service: { type: 'string', description: 'id of a service in your profile: price, contract and concept default to what you published, and the house rejects a quote that differs from it' },
40
+ price: { type: 'integer', description: 'tokens, entero (required without service)' }, concept: { type: 'string', description: 'required without service' },
41
+ terms: { type: 'object', description: 'criterio de aceptación, plazo, scope del mandato, etc.' },
42
+ arbiter: { type: 'string' }, expires: { type: 'string', description: 'ISO-8601' },
43
+ 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' } } } } } },
44
+ { name: 'nyx5_accept', description: 'Accept an offer and commit the payment. In escrow the money is held: the seller does not get paid until they deliver and meet the condition. You receive a signed receipt that no party can deny later.',
45
+ inputSchema: { type: 'object', required: ['quote'], properties: { quote: { type: 'object' } } } },
46
+ { name: 'nyx5_libro', description: 'Move a deal forward in the ledger, leaving a signed, irreversible entry at every step: deliver, release the payment if the proof passed, refund if it failed, back a claim with money (you lose it if you lied), or delegate spending with a cap. ops: pay {to, amount, concept}, deliver {contract, evidence_sha256}, release {contract}, refund {contract}, reclaim {contract}, bond {amount, claim, verifier}, forfeit {contract, reason}, mandate {grantee, cap, scope, expires, parent}, charge {mandate, amount, concept}, revoke {mandate}, balance, statement {limit, since, until}, contract {contract}. The answer arrives as a signed receipt in your mailbox.',
47
+ inputSchema: { type: 'object', required: ['op'], properties: { house: { type: 'string', description: 'dominio de la casa; por defecto el propio' }, op: { type: 'string' }, args: { type: 'object' } } } },
48
+ { name: 'nyx5_balance', description: 'How much you hold, which contracts and which spending permissions are active. Check it before committing a payment. A direct read authenticated with your signature, without going through the mail. With since or until, your statement for that range: opening balance, every entry with the house fee as its own line, closing balance.',
49
+ inputSchema: { type: 'object', properties: { house: { type: 'string' }, since: { type: 'string', description: 'ISO-8601: statement from this date (inclusive)' }, until: { type: 'string', description: 'ISO-8601: statement up to this date (exclusive)' }, limit: { type: 'integer', description: 'statement: how many entries, newest kept (max 1000)' } } } },
50
+ { name: 'nyx5_remind', description: 'Leave yourself a message that reaches you in the future, in your own mailbox, encrypted. Use it when a task must be picked up in hours or days and your session will end before then: your future self finds the context with the full thread, without depending on anyone waking you.',
51
+ 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' } } } },
52
+ { name: 'nyx5_contract', description: 'The state and full history of a deal you are party to: every step with its hash and its signature. Use it to see where an escrow or a bond stands.',
53
+ inputSchema: { type: 'object', required: ['contract'], properties: { house: { type: 'string' }, contract: { type: 'string' } } } },
54
+ { name: 'nyx5_historial', description: 'The reputation of an agent is its ledger: deliveries accepted against returned, bonds standing against forfeited, with amounts. Check it before hiring a stranger. Every point of it cost tokens and is tied to a verified delivery, so it cannot be inflated by talking; with no record it returns null (nothing yet, not perfect).',
55
+ inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'a quién mirar; por defecto, tú mismo' } } } },
56
+ { name: 'nyx5_tareas', description: 'Paid work a house publishes that you can take right now: what to do, what it pays, and the deterministic check it will be verified with. Use it when you just joined and have no record yet, or when you need tokens to back your own claims with a bond.',
57
+ inputSchema: { type: 'object', properties: { house: { type: 'string' } } } },
58
+ { name: 'nyx5_tomar', description: 'Take a published task: the house holds the payment in a signed entry before you work, and releases it on its own when the deterministic check passes. If it fails, it is refunded and stays in your record. Use it to earn your first tokens; terms are copied from the catalogue and are not negotiable.',
59
+ inputSchema: { type: 'object', required: ['id'], properties: { id: { type: 'string', description: 'id de la tarea, de nyx5_tareas' }, house: { type: 'string' } } } },
60
+ { name: 'nyx5_email', description: 'Write by email to a human who is not on Nyx5 yet. Use it when the recipient has no agent address: their reply comes back to your mailbox (Reply-To). It enters unsigned, marked as not verified, never disguised; when they want the real thing, they register.',
61
+ 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' } } } },
62
+ { name: 'nyx5_wait', description: 'Wait, up to a limit, for the next message in your mailbox (optionally only from one sender or one thread) and get it opened, with its signature verified, the moment it lands. Use it right after sending when the other side is live: it is how two agents hold a real-time conversation instead of polling.',
63
+ inputSchema: { type: 'object', properties: { from: { type: 'string', description: 'only messages from this address' }, thread: { type: 'string', description: 'only messages in this thread' }, since: { type: 'string', description: 'ISO time: only messages received after it. Default: now, so earlier unread mail is never mistaken for the reply' }, project: { type: 'string', description: 'only messages of this project (see nyx5_send)' }, seconds: { type: 'number', description: 'how long to wait, 1 to 90 (default 60)' } } } },
64
+ { name: 'nyx5_conversation', description: 'The signed history between you and one address, both directions and oldest first, including what you already acknowledged; without an address, the list of your conversations. Use it to pick up where a conversation was left, from any device: the history lives in the house, not in your session.',
65
+ inputSchema: { type: 'object', properties: { with: { type: 'string', description: 'the other address; omit it to list your conversations' }, limit: { type: 'number', description: 'how many messages, newest kept (default 30)' }, project: { type: 'string', description: 'only messages of this project (see nyx5_send)' } } } },
66
+ { name: 'nyx5_group', description: 'A group address (g.name@house): one signed message reaches every member, encrypted for each, with a shared history nobody outside can read. Use it when work involves several agents. ops: create {name, members, post}, members {group}, add {group, members}, remove {group, members}, leave {group}. Only members post; admins change members.',
67
+ inputSchema: { type: 'object', required: ['op'], properties: { op: { type: 'string', enum: ['create', 'members', 'add', 'remove', 'leave'] }, name: { type: 'string', description: 'create: the group name (becomes g.name@house)' }, group: { type: 'string', description: 'the group address or name' }, members: { type: 'array', items: { type: 'string' }, description: 'addresses of this house' }, post: { type: 'string', enum: ['members', 'admins'], description: 'who can post (default members)' } } } },
68
+ { name: 'nyx5_profile', description: 'What an agent says about itself, certified by its house: name, what it does, languages, owner, tags, links, and what it sells at what price and contract. Read it before hiring a stranger (declared, not verified: the ledger record is). Set yours so others find you. ops: get {address}, set {profile} (an unknown key is rejected by name).',
69
+ inputSchema: { type: 'object', required: ['op'], properties: { op: { type: 'string', enum: ['get', 'set'] }, address: { type: 'string', description: 'get: whose profile (default: yours)' }, profile: { type: 'object', description: 'set: { display_name, summary, description, languages, tags, owner: {kind, name}, links, services: [{ id, name, summary, price: {tokens, usd?}, unit: job|call|hour, contract: spot|escrow|metered, acceptance?: {kind, template} }] }; null clears it' } } } },
70
+ { name: 'nyx5_notarize', description: 'Seal the hash of a document in the house ledger: a signed, dated record that anyone can verify later without an account. Use it before sharing work whose date or integrity may be disputed (a report, a dataset, an offer). Free; the same hash sealed twice by you returns the same seal. The seal arrives as a signed receipt.',
71
+ inputSchema: { type: 'object', required: ['sha256'], properties: { sha256: { type: 'string', description: 'hex sha256 of the document (64 characters)' }, name: { type: 'string' }, media: { type: 'string' }, note: { type: 'string' }, house: { type: 'string', description: 'house that seals; by default your own' } } } },
72
+ { name: 'nyx5_notarized', description: 'Verify a document hash against the house notary: the seals on it, who declared it and when, each signed by the house and checked against its domain card. Use it when someone claims a document existed at a date: the seal proves the hash was declared no later than then. No account needed.',
73
+ inputSchema: { type: 'object', required: ['sha256'], properties: { sha256: { type: 'string', description: 'hex sha256 of the document (64 characters)' }, house: { type: 'string', description: 'house to ask; by default your own' } } } },
74
+ { name: 'nyx5_whoami', description: 'Your own address and what it may do: who delegated it, until when, whether the house holds its keys, and who may write to it. Check it before promising anything on behalf of your owner; the card is certified by the domain.',
75
+ inputSchema: { type: 'object', properties: {} } },
76
+ ];
77
+
78
+ // Anotaciones MCP: le dicen al cliente qué herramienta sólo LEE. Claude las usa para decidir cuándo
79
+ // pedir permiso (y el directorio oficial de conectores las exige). Son pistas, no garantías: la casa
80
+ // sigue haciendo cumplir cada límite por su cuenta. Nació de un reporte de Nicholas: conectar desde
81
+ // el teléfono le pidió tres permisos (inbox, send, wait) antes del primer mensaje.
82
+ const SOLO_LECTURA = new Set(['nyx5_inbox', 'nyx5_resolve', 'nyx5_outbox', 'nyx5_directory', 'nyx5_search', 'nyx5_balance', 'nyx5_contract', 'nyx5_historial', 'nyx5_tareas', 'nyx5_wait', 'nyx5_conversation', 'nyx5_notarized', 'nyx5_whoami']);
83
+ const MUEVE_DINERO = new Set(['nyx5_accept', 'nyx5_libro', 'nyx5_tomar']);
84
+ const TITULOS = { nyx5_send: 'Send a message', nyx5_inbox: 'Read my mailbox', nyx5_ack: 'Mark messages as handled', nyx5_resolve: 'Check who an address is', nyx5_outbox: 'Delivery status of what I sent', nyx5_directory: 'Agents in a house', nyx5_search: 'Find an agent', nyx5_quote: 'Offer a service', nyx5_accept: 'Accept an offer and pay', nyx5_libro: 'Ledger operation', nyx5_balance: 'My balance', nyx5_remind: 'Remind myself later', nyx5_contract: 'A deal and its history', nyx5_historial: 'Reputation of an agent', nyx5_tareas: 'Paid tasks available', nyx5_tomar: 'Take a paid task', nyx5_email: 'Email a person', nyx5_wait: 'Wait for a reply', nyx5_conversation: 'Conversation history', nyx5_group: 'Group of agents', nyx5_profile: 'Public profile', nyx5_notarize: 'Seal a document hash', nyx5_notarized: 'Verify a document seal', nyx5_whoami: 'Who am I' };
85
+ for (const t of TOOLS) {
86
+ t.title = TITULOS[t.name] || t.name;
87
+ t.annotations = { title: t.title, readOnlyHint: SOLO_LECTURA.has(t.name), destructiveHint: MUEVE_DINERO.has(t.name), idempotentHint: SOLO_LECTURA.has(t.name) || t.name === 'nyx5_ack' || t.name === 'nyx5_notarize', openWorldHint: true };
88
+ }
89
+
90
+ // Lo que el conector remoto expone: mensajería y nada que mueva saldo. El subagente de un teléfono
91
+ // es de alcance `messages_only` (la casa se lo niega igual si lo intenta); ofrecerle herramientas
92
+ // que van a fallar sólo le enseñaría al modelo a prometer lo que no puede cumplir.
93
+ export const MENSAJERIA = new Set(['nyx5_send', 'nyx5_inbox', 'nyx5_ack', 'nyx5_resolve', 'nyx5_outbox', 'nyx5_directory', 'nyx5_search', 'nyx5_remind', 'nyx5_historial', 'nyx5_wait', 'nyx5_conversation', 'nyx5_group', 'nyx5_whoami']);
94
+
95
+ export const INSTRUCCIONES = `Nyx5 gives an agent three things it has no other way of getting: an address of its own, a mailbox that holds while it is off, and a ledger where an agreement carries weight (payment is held until the proof passes; a false claim forfeits its bond). Use it to reach an agent that may not be available now, to find someone who does X in any house, or to close a deal that must be worth more than a promise. Before trusting a stranger, read their record: it is a query on the ledger, so every point of it cost tokens. Every message is signed and every movement of money leaves a receipt no party can deny. Tag what you send with a project name and filter by it to keep several chats apart.`;
96
+
97
+ export function instrucciones({ remoto = false, address = null, contactos = [] } = {}) {
98
+ if (!remoto) return INSTRUCCIONES;
99
+ // Sus contactos van aquí para que el dueño pueda decir "escríbele a Nico" sin dictar direcciones.
100
+ const extra = contactos.length ? ` Your contacts, who can write to you and whom you can reach: ${contactos.join(', ')}. When your owner names one of them, write to their Claude (claude.<name>@<house>) if it is on this list.` : '';
101
+ return `You act through ${address}, a messages-only address your owner delegated to you. You can send signed messages to any agent address, read your mailbox, and wait for a reply live, which is how you hold a real-time conversation with another agent. You cannot move money or operate the ledger. The house holds this key on behalf of your owner, who can revoke it at any time. Before trusting a stranger, check who they really are: every card is certified by its domain.${extra}`;
102
+ }
103
+
104
+ export async function llamar(agent, name, args = {}, { permitidas = null, esperaMaxS = 90 } = {}) {
105
+ const text = (v) => ({ content: [{ type: 'text', text: typeof v === 'string' ? v : JSON.stringify(v, null, 2) }] });
106
+ if (permitidas && !permitidas.has(name)) return { ...text(`${name} is not available here: this is a messages-only address (no ledger, no payments)`), isError: true };
107
+ switch (name) {
108
+ 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, project: args.project, role: args.role, extensions: args.aval ? { 'urn:nyx5:ext:aval': args.aval } : undefined }); return text({ id: r.id, jobs: r.jobs, encrypted: r.encrypted }); }
109
+ case 'nyx5_inbox': {
110
+ const proyecto = args.project ? String(args.project).trim().toLowerCase() : null;
111
+ const msgs = (await agent.inbox({ limit: args.limit ?? 20 })).filter((m) => !proyecto || proyectoDe(m.envelope) === proyecto);
112
+ const opened = [];
113
+ // `received` es la hora que entiende `since` de nyx5_wait (la de llegada al buzón, no la de
114
+ // creación): sin exponerla, el filtro no se podía usar bien (defecto reportado el 12-sep-2026).
115
+ for (const m of msgs) { try { const { sender, ...o } = await agent.open(m.envelope); opened.push({ ...o, received: m.received }); } catch (e) { opened.push({ id: m.envelope.id, from: m.envelope.from, error: e.message, received: m.received }); } }
116
+ return text(opened);
117
+ }
118
+ case 'nyx5_ack': return text({ acked: await agent.ack(args.ids) });
119
+ case 'nyx5_resolve': { const { _domain, ...card } = await agent.resolver.agentCard(args.address, { onBehalfOf: agent.address }); const last_seen = await agent.presence(args.address); return text({ ...card, ...(last_seen ? { presence: { last_seen } } : {}) }); }
120
+ case 'nyx5_outbox': return text(await agent.outbox());
121
+ case 'nyx5_directory': return text(await agent.directory(args.house, args));
122
+ case 'nyx5_search': return text(await agent.search(args.index, args));
123
+ case 'nyx5_quote': { if (args.service == null && (args.price == null || args.concept == null)) return { ...text('nyx5_quote needs price and concept, or a service id from your profile'), isError: true }; 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, service: args.service }); return text({ id: r.id, quote_id: r.quote.id, contract: r.quote.contract, price: r.quote.price, ...(r.quote.service ? { service: r.quote.service } : {}) }); }
124
+ 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)' }); }
125
+ 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' }); }
126
+ case 'nyx5_balance': return text(args.since || args.until ? await agent.statement(args.house, { since: args.since, until: args.until, limit: args.limit }) : await agent.balance(args.house));
127
+ 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}` }); }
128
+ case 'nyx5_historial': return text(await agent.historial(args.address || agent.address));
129
+ case 'nyx5_tareas': {
130
+ const dc = await agent.resolver.domainCard(args.house || agent.domain);
131
+ const res = await agent.fetch(`${dc._estafeta}/tareas`);
132
+ const j = await res.json();
133
+ if (!res.ok) return text({ error: j.reason || `HTTP ${res.status}` });
134
+ return text(j);
135
+ }
136
+ case 'nyx5_tomar': {
137
+ const dc = await agent.resolver.domainCard(args.house || agent.domain);
138
+ const j = await (await agent.fetch(`${dc._estafeta}/tareas`)).json();
139
+ const t = (j.tasks || j.tareas || []).find((x) => x.id === args.id);
140
+ if (!t) return text({ error: `no task with id ${args.id}`, available: (j.tasks || j.tareas || []).map((x) => x.id) });
141
+ const enviada = await agent.quote({ to: (j.desk || j.mostrador), contract: 'escrow', price: t.price, concept: t.concept, arbiter: (j.arbiter || j.arbitro), terms: t.terms });
142
+ return text({ enviada: enviada.id, tarea: t.id, price: t.price, siguiente: 'si hay cupo, el contrato te llega al buzón (nyx5_inbox); al terminar, nyx5_libro op=deliver' });
143
+ }
144
+ case 'nyx5_email': { const r = await agent.email({ to: args.to, subject: args.subject, body: args.body }); return text(r); }
145
+ case 'nyx5_contract': return text(await agent.contract(args.house || agent.domain, args.contract));
146
+ case 'nyx5_wait': {
147
+ const secs = Math.max(1, Math.min(Number(args.seconds ?? 60) || 60, esperaMaxS));
148
+ // Sin `since`, se espera lo que llegue DESDE AHORA. Defecto real (12-sep-2026): devolvía como
149
+ // "respuesta" un mensaje viejo que ya estaba en el buzón, y el que espera daba por contestada
150
+ // una conversación que no lo estaba. Lo pendiente de antes se avisa, no se confunde con lo nuevo.
151
+ // Lo que YA estaba sin leer se entrega igual (nada se pierde), pero marcado como anterior a la
152
+ // espera, para que nadie lo tome por la respuesta a lo que acaba de mandar.
153
+ const desde = args.since || new Date().toISOString();
154
+ const anterior = args.since ? null : (await agent.inbox({ limit: 50 })).find((m) => m.received < desde && (!args.from || m.envelope?.from === args.from) && (!args.thread || m.envelope?.thread === args.thread || m.envelope?.id === args.thread) && (!args.project || proyectoDe(m.envelope) === String(args.project).trim().toLowerCase()));
155
+ const m = anterior || await agent.wait({ from: args.from, thread: args.thread, since: desde, project: args.project, seconds: secs });
156
+ if (!m) return text({ message: null, waited_seconds: secs, note: 'nothing arrived; call again to keep listening' });
157
+ let abierto;
158
+ try { const { sender, ...o } = await agent.open(m.envelope); abierto = o; } catch (e) { abierto = { id: m.envelope.id, from: m.envelope.from, error: e.message }; }
159
+ return text({ ...abierto, received: m.received, ...(anterior ? { arrived_before_wait: true, note: 'this message was already unread in your mailbox before you started waiting: if you expected a reply to something you just sent, this is probably not it. Pass since (its received time) to wait only for newer mail.' } : {}) });
160
+ }
161
+ case 'nyx5_conversation': {
162
+ if (!args.with) return text(await agent.conversations({ project: args.project || null }));
163
+ const msgs = await agent.conversation(args.with, { limit: args.limit ?? 30, project: args.project || null });
164
+ const out = [];
165
+ for (const m of msgs) {
166
+ try { const { sender, ...o } = await agent.open(m.envelope); out.push({ dir: m.dir, at: m.at, status: m.status, ...o }); }
167
+ catch (e) { out.push({ dir: m.dir, at: m.at, id: m.envelope?.id, from: m.envelope?.from, error: e.message }); }
168
+ }
169
+ return text(out);
170
+ }
171
+ case 'nyx5_group': {
172
+ const op = args.op;
173
+ if (op === 'create') return text(await agent.createGroup(args.name, { members: args.members || [], post: args.post }));
174
+ if (op === 'members') return text(await agent.group(args.group));
175
+ if (op === 'add') return text(await agent.editGroup(args.group, { add: args.members || [] }));
176
+ if (op === 'remove') return text(await agent.editGroup(args.group, { remove: args.members || [] }));
177
+ if (op === 'leave') return text(await agent.leaveGroup(args.group));
178
+ return { ...text(`unknown group op: ${op}. Valid ones: create, members, add, remove, leave`), isError: true };
179
+ }
180
+ case 'nyx5_profile': {
181
+ if (args.op === 'get') return text(await agent.profile(args.address || agent.address));
182
+ if (args.op === 'set') return text(await agent.setProfile(args.profile === undefined ? null : args.profile));
183
+ return { ...text(`unknown profile op: ${args.op}. Valid ones: get, set`), isError: true };
184
+ }
185
+ case 'nyx5_notarize': { const r = await agent.notarize(args.house || agent.domain, { sha256: args.sha256, name: args.name, media: args.media, note: args.note }); return text({ id: r.id, note: 'the seal arrives as a signed receipt from libro@ in your mailbox (nyx5_inbox)' }); }
186
+ case 'nyx5_notarized': { const r = await agent.notarized(args.sha256, args.house || agent.domain); return text(r || { sha256: String(args.sha256).toLowerCase(), seals: [], note: 'no seal for this hash in this house' }); }
187
+ case 'nyx5_whoami': return text(await agent.whoami());
188
+ default: return { ...text(`unknown tool: ${name}`), isError: true };
189
+ }
190
+ }
@@ -0,0 +1,71 @@
1
+ // Nyx5/1 — Puente MCP remoto: el mismo conjunto de herramientas, por Streamable HTTP en /mcp.
2
+ //
3
+ // Es lo que deja a cualquier Claude (claude.ai en el navegador, Desktop, el teléfono) usar Nyx5
4
+ // agregando una URL, sin instalar nada. Cada llamada llega con un token OAuth que apunta a un
5
+ // subagente delegado cuya llave guarda la casa (ver src/puentes/oauth.js); el puente arma ese
6
+ // agente en el mismo proceso y ejecuta la herramienta con él. Nunca con la llave del dueño.
7
+ //
8
+ // Del transporte (MCP 2025-06-18 / 2025-11-25) se usa lo mínimo que un servidor de sólo
9
+ // herramientas necesita: un POST por mensaje JSON-RPC, respuesta application/json, 202 para
10
+ // notificaciones, 405 al GET (no hay flujo iniciado por el servidor) y sin sesión: cada petición
11
+ // trae su token, así que no hay estado que perder entre instancias del edge.
12
+ import { TOOLS, MENSAJERIA, llamar, instrucciones } from './herramientas.js';
13
+ import { validarAcceso } from './oauth.js';
14
+ import { VERSION } from '../version.js';
15
+
16
+ export const VERSIONES = ['2025-11-25', '2025-06-18', '2025-03-26'];
17
+ const HERRAMIENTAS = TOOLS.filter((t) => MENSAJERIA.has(t.name));
18
+ const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]']);
19
+
20
+ // Un Origin presente tiene que ser un origen real: el spec pide 403 ante uno inválido (protección
21
+ // contra DNS rebinding). No se exige una lista cerrada: la protección de fondo es el token.
22
+ function origenValido(o) {
23
+ try { const u = new URL(o); return u.protocol === 'https:' || (u.protocol === 'http:' && LOOPBACK.has(u.hostname)); }
24
+ catch { return false; }
25
+ }
26
+
27
+ export async function atenderMcp(est, rx) {
28
+ if (rx.method !== 'POST') return { status: 405, headers: { allow: 'POST' }, body: { reason: 'this MCP endpoint takes POST only; it does not open a server-initiated stream' } };
29
+ if (rx.headers.origin !== undefined && !origenValido(rx.headers.origin)) return { status: 403, body: { reason: 'invalid Origin header' } };
30
+ const acceso = await validarAcceso(est, rx);
31
+ if (!acceso.ok) return acceso.out;
32
+ const pv = rx.headers['mcp-protocol-version'];
33
+ if (pv && !VERSIONES.includes(pv)) return { status: 400, body: { jsonrpc: '2.0', id: null, error: { code: -32600, message: `unsupported MCP-Protocol-Version ${pv}; supported: ${VERSIONES.join(', ')}` } } };
34
+ const sub = acceso.token.sub;
35
+ if (!await est.remotoRate.allow(`mcp:${sub}`)) return { status: 429, headers: est._retryAfter(), body: { jsonrpc: '2.0', id: null, error: { code: -32000, message: 'too many requests; slow down' } } };
36
+
37
+ const msg = rx.body;
38
+ if (!msg || typeof msg !== 'object' || Array.isArray(msg)) return { status: 400, body: { jsonrpc: '2.0', id: null, error: { code: -32600, message: 'send exactly one JSON-RPC message per POST' } } };
39
+ // Notificación (sin id) o respuesta del cliente: se acepta y no se contesta nada.
40
+ if (msg.id === undefined || msg.id === null || typeof msg.method !== 'string') return { status: 202, contentType: 'application/json', body: '' };
41
+
42
+ const { id, method, params } = msg;
43
+ const ok = (result, extra = {}) => ({ status: 200, body: { jsonrpc: '2.0', id, result }, ...extra });
44
+ switch (method) {
45
+ case 'initialize': {
46
+ const pedida = params?.protocolVersion;
47
+ const propia = `${sub}@${est.domain}`;
48
+ const contactos = ((await est.store.getAgent(sub))?.inbox?.allowlist || []).filter((x) => x !== propia);
49
+ return ok({
50
+ protocolVersion: VERSIONES.includes(pedida) ? pedida : '2025-06-18',
51
+ capabilities: { tools: { listChanged: false } },
52
+ serverInfo: { name: 'nyx5', title: 'Nyx5', version: VERSION },
53
+ instructions: instrucciones({ remoto: true, address: propia, contactos }),
54
+ });
55
+ }
56
+ case 'ping': return ok({});
57
+ case 'tools/list': return ok({ tools: HERRAMIENTAS });
58
+ case 'tools/call': {
59
+ const agent = await est.agenteDeBoveda(sub);
60
+ if (!agent) return (await validarAcceso(est, { headers: {} })).out;
61
+ let result;
62
+ // Un error de la herramienta vuelve como resultado con isError (el modelo puede corregirse),
63
+ // no como error del protocolo (spec 2025-11-25).
64
+ try { result = await llamar(agent, params?.name, params?.arguments || {}, { permitidas: MENSAJERIA, esperaMaxS: 90 }); }
65
+ catch (e) { result = { content: [{ type: 'text', text: `error: ${e.message}` }], isError: true }; }
66
+ // kick: lo que la herramienta dejó en la cola sale ya, no en el próximo minuto del cron.
67
+ return ok(result, { kick: true });
68
+ }
69
+ default: return { status: 200, body: { jsonrpc: '2.0', id, error: { code: -32601, message: `method not supported: ${method}` } } };
70
+ }
71
+ }
@@ -6,113 +6,19 @@
6
6
 
7
7
  import readline from 'node:readline';
8
8
  import { Agent } from '../correo/agente.js';
9
+ import { TOOLS, llamar, INSTRUCCIONES } from './herramientas.js';
10
+ import { VERSION } from '../version.js';
9
11
 
10
- // La versión la dice el paquete, no un literal: un servidor que reporta una versión que no es
11
- // la suya hace imposible depurar "en qué versión pasó esto". Si no se puede leer, se declara
12
- // desconocida en vez de inventar un número.
13
- let VERSION = '0.0.0-unknown';
14
- try {
15
- const { readFileSync } = await import('node:fs');
16
- VERSION = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version || VERSION;
17
- } catch {}
12
+ export { TOOLS };
18
13
 
19
14
  const PROTOCOL = '2025-06-18';
20
15
  const SUPPORTED = new Set(['2025-06-18', '2025-03-26']);
21
16
 
22
- const TOOLS = [
23
- { name: 'nyx5_send', description: 'Delegate a task to another agent even if it is switched off: it waits in their mailbox and their reply reaches you signed when they answer. Use it when you need someone to do something and do not know whether they are available now. The recipient verifies it is really you, and nobody else can read the content (encrypted).',
24
- inputSchema: { type: 'object', required: ['to', 'body'], properties: {
25
- to: { type: 'array', items: { type: 'string' }, description: 'Direcciones destino, ej. ["asistente@beta.local"]' },
26
- body: { description: 'Contenido: texto o JSON' },
27
- type: { type: 'string', enum: ['message', 'task', 'result', 'receipt', 'intro'], default: 'message' },
28
- thread: { type: 'string' }, in_reply_to: { type: 'string' },
29
- 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' } } },
30
- encrypt: { type: 'boolean', default: true } } } },
31
- { name: 'nyx5_inbox', description: 'What others sent you while you were not looking. Every envelope arrives with a verified signature (you know who really sent it) and comes decrypted. Check it when you start and before treating anything as unanswered: a reply may have landed between sessions.',
32
- inputSchema: { type: 'object', properties: { limit: { type: 'integer', default: 20 } } } },
33
- { name: 'nyx5_ack', description: 'Close the envelopes in your mailbox that you already handled so they stop coming back. Use it after acting on a message; what you acknowledge stays in the record.',
34
- inputSchema: { type: 'object', required: ['ids'], properties: { ids: { type: 'array', items: { type: 'string' } } } } },
35
- { name: 'nyx5_resolve', description: 'Check who an address really is before trusting it: returns their card, certified by their domain (verified identity, what they can do, how they charge). Use it before sending anything sensitive or paying them.',
36
- inputSchema: { type: 'object', required: ['address'], properties: { address: { type: 'string' } } } },
37
- { name: 'nyx5_outbox', description: 'What happened to the signed envelopes you sent: delivered, retrying, or bounced with the reason. Use it when you are unsure whether your message arrived; the estafeta records every attempt.',
38
- inputSchema: { type: 'object', properties: {} } },
39
- { name: 'nyx5_directory', description: 'Which agents a house offers and what each one does, every card certified by the domain. Use it when you are looking for a provider inside a house you already know.',
40
- inputSchema: { type: 'object', properties: { house: { type: 'string' }, capability: { type: 'string' }, accepts: { type: 'string' }, q: { type: 'string' }, limit: { type: 'integer' } } } },
41
- { name: 'nyx5_search', description: 'Find an agent that does what you need in any house, not just yours. Use it when you know nobody who solves your problem. The index answers signed and you verify the card before trusting: it is a hint, not an authority.',
42
- 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' } } } },
43
- // ----- Libro -----
44
- { name: 'nyx5_quote', description: 'Offer another agent a service with a price and the exact condition that must be met to get paid. In escrow the payment is held until the proof passes. Use it to sell something with an agreement that carries weight, not a spoken promise.',
45
- inputSchema: { type: 'object', required: ['to', 'price', 'concept'], properties: {
46
- to: { type: 'string' }, contract: { type: 'string', enum: ['spot', 'escrow', 'metered'], default: 'spot' },
47
- price: { type: 'integer', description: 'tokens, entero' }, concept: { type: 'string' },
48
- terms: { type: 'object', description: 'criterio de aceptación, plazo, scope del mandato, etc.' },
49
- arbiter: { type: 'string' }, expires: { type: 'string', description: 'ISO-8601' },
50
- 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' } } } } } },
51
- { name: 'nyx5_accept', description: 'Accept an offer and commit the payment. In escrow the money is held: the seller does not get paid until they deliver and meet the condition. You receive a signed receipt that no party can deny later.',
52
- inputSchema: { type: 'object', required: ['quote'], properties: { quote: { type: 'object' } } } },
53
- { name: 'nyx5_libro', description: 'Move a deal forward in the ledger, leaving a signed and irreversible entry at every step: deliver, release the payment if the proof passed, refund if it failed, back a claim with money (you lose it if you lied), or delegate spending with a cap. 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}. The answer arrives as a signed receipt in your mailbox.',
54
- inputSchema: { type: 'object', required: ['op'], properties: { house: { type: 'string', description: 'dominio de la casa; por defecto el propio' }, op: { type: 'string' }, args: { type: 'object' } } } },
55
- { name: 'nyx5_balance', description: 'How much you hold, which contracts and which spending permissions are active. Check it before committing a payment. A direct read authenticated with your signature, without going through the mail.',
56
- inputSchema: { type: 'object', properties: { house: { type: 'string' } } } },
57
- { name: 'nyx5_remind', description: 'Leave yourself a message that reaches you in the future, in your own mailbox, encrypted. Use it when a task must be picked up in hours or days and your session will end before then: your future self finds the context with the full thread, without depending on anyone waking you.',
58
- 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' } } } },
59
- { name: 'nyx5_contract', description: 'The state and full history of a deal you are party to: every step with its hash and its signature. Use it to see where an escrow or a bond stands.',
60
- inputSchema: { type: 'object', required: ['contract'], properties: { house: { type: 'string' }, contract: { type: 'string' } } } },
61
- { name: 'nyx5_historial', description: 'The reputation of an agent is its ledger: deliveries accepted against returned, bonds standing against forfeited, with amounts. Check it before hiring a stranger. Every point of it cost tokens and is tied to a verified delivery, so it cannot be inflated by talking; with no record it returns null (nothing yet, not perfect).',
62
- inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'a quién mirar; por defecto, tú mismo' } } } },
63
- { name: 'nyx5_tareas', description: 'Paid work a house publishes that you can take right now: what to do, what it pays, and the deterministic check it will be verified with. Use it when you just joined and have no record yet, or when you need tokens to back your own claims with a bond.',
64
- inputSchema: { type: 'object', properties: { house: { type: 'string' } } } },
65
- { name: 'nyx5_tomar', description: 'Take a published task: the house holds the payment in a signed entry before you work, and releases it on its own when the deterministic check passes. If it fails, it is refunded and stays in your record. Use it to earn your first tokens; terms are copied from the catalogue and are not negotiable.',
66
- inputSchema: { type: 'object', required: ['id'], properties: { id: { type: 'string', description: 'id de la tarea, de nyx5_tareas' }, house: { type: 'string' } } } },
67
- { name: 'nyx5_email', description: 'Write by email to a human who is not on Nyx5 yet. Use it when the recipient has no agent address: their reply comes back to your mailbox (Reply-To). It enters unsigned, marked as not verified, never disguised; when they want the real thing, they register.',
68
- 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' } } } },
69
- ];
70
-
71
17
  export async function runMcpServer({ agentFile, hosts = {} }) {
72
18
  const agent = await Agent.load(agentFile, { hosts });
73
19
  const out = (msg) => process.stdout.write(JSON.stringify(msg) + '\n');
74
20
  const text = (v) => ({ content: [{ type: 'text', text: typeof v === 'string' ? v : JSON.stringify(v, null, 2) }] });
75
-
76
- async function call(name, args = {}) {
77
- switch (name) {
78
- 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 }); }
79
- case 'nyx5_inbox': {
80
- const msgs = await agent.inbox({ limit: args.limit ?? 20 });
81
- const opened = [];
82
- 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 }); } }
83
- return text(opened.map(({ sender, ...o }) => o));
84
- }
85
- case 'nyx5_ack': return text({ acked: await agent.ack(args.ids) });
86
- case 'nyx5_resolve': { const { _domain, ...card } = await agent.resolver.agentCard(args.address); return text(card); }
87
- case 'nyx5_outbox': return text(await agent.outbox());
88
- case 'nyx5_directory': return text(await agent.directory(args.house, args));
89
- case 'nyx5_search': return text(await agent.search(args.index, args));
90
- 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 }); }
91
- 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)' }); }
92
- 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' }); }
93
- case 'nyx5_balance': return text(await agent.balance(args.house));
94
- 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}` }); }
95
- case 'nyx5_historial': return text(await agent.historial(args.address || agent.address));
96
- case 'nyx5_tareas': {
97
- const dc = await agent.resolver.domainCard(args.house || agent.domain);
98
- const res = await agent.fetch(`${dc._estafeta}/tareas`);
99
- const j = await res.json();
100
- if (!res.ok) return text({ error: j.reason || `HTTP ${res.status}` });
101
- return text(j);
102
- }
103
- case 'nyx5_tomar': {
104
- const dc = await agent.resolver.domainCard(args.house || agent.domain);
105
- const j = await (await agent.fetch(`${dc._estafeta}/tareas`)).json();
106
- const t = (j.tasks || j.tareas || []).find((x) => x.id === args.id);
107
- if (!t) return text({ error: `no task with id ${args.id}`, available: (j.tasks || j.tareas || []).map((x) => x.id) });
108
- const enviada = await agent.quote({ to: (j.desk || j.mostrador), contract: 'escrow', price: t.price, concept: t.concept, arbiter: (j.arbiter || j.arbitro), terms: t.terms });
109
- return text({ enviada: enviada.id, tarea: t.id, price: t.price, siguiente: 'si hay cupo, el contrato te llega al buzón (nyx5_inbox); al terminar, nyx5_libro op=deliver' });
110
- }
111
- case 'nyx5_email': { const r = await agent.email({ to: args.to, subject: args.subject, body: args.body }); return text(r); }
112
- case 'nyx5_contract': return text(await agent.contract(args.house || agent.domain, args.contract));
113
- default: return { ...text(`herramienta desconocida: ${name}`), isError: true };
114
- }
115
- }
21
+ const call = (name, args) => llamar(agent, name, args);
116
22
 
117
23
  const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
118
24
  for await (const line of rl) {
@@ -123,7 +29,7 @@ export async function runMcpServer({ agentFile, hosts = {} }) {
123
29
  if (method === 'notifications/initialized' || method?.startsWith('notifications/')) continue;
124
30
  try {
125
31
  let result;
126
- if (method === 'initialize') result = { protocolVersion: SUPPORTED.has(params?.protocolVersion) ? params.protocolVersion : PROTOCOL, capabilities: { tools: { listChanged: false } }, serverInfo: { name: 'nyx5', version: VERSION }, instructions: `Nyx5 gives an agent three things it has no other way of getting: an address of its own, a mailbox that holds while it is off, and a ledger where an agreement carries weight (payment is held until the proof passes; a false claim forfeits its bond). Use it to reach an agent that may not be available now, to find someone who does X in any house, or to close a deal that must be worth more than a promise. Before trusting a stranger, read their record: it is a query on the ledger, so every point of it cost tokens. Every message is signed and every movement of money leaves a receipt no party can deny.` };
32
+ if (method === 'initialize') result = { protocolVersion: SUPPORTED.has(params?.protocolVersion) ? params.protocolVersion : PROTOCOL, capabilities: { tools: { listChanged: false } }, serverInfo: { name: 'nyx5', version: VERSION }, instructions: INSTRUCCIONES };
127
33
  else if (method === 'ping') result = {};
128
34
  else if (method === 'tools/list') result = { tools: TOOLS };
129
35
  else if (method === 'tools/call') { try { result = await call(params?.name, params?.arguments); } catch (e) { result = { ...text(`error: ${e.message}`), isError: true }; } }