@nyx5/nyx5 0.7.0 → 0.9.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.
- package/README.md +33 -5
- package/agents.md +24 -7
- package/bin/nyx5.js +153 -6
- package/demo/x402-pagar.mjs +64 -0
- package/docs/ARQUITECTURA.md +352 -0
- package/docs/HOUSE.es.md +237 -0
- package/docs/HOUSE.md +234 -0
- package/docs/SPEC.md +337 -6
- package/examples/ci/nyx5-ejecutor.yml +82 -0
- package/examples/frameworks/README.md +55 -0
- package/examples/frameworks/__pycache__/crewai_nyx5.cpython-314.pyc +0 -0
- package/examples/frameworks/__pycache__/langgraph_nyx5.cpython-314.pyc +0 -0
- package/examples/frameworks/__pycache__/nyx5_http.cpython-314.pyc +0 -0
- package/examples/frameworks/claude-agent-sdk.mjs +48 -0
- package/examples/frameworks/crewai_nyx5.py +72 -0
- package/examples/frameworks/http.md +117 -0
- package/examples/frameworks/langgraph_nyx5.py +90 -0
- package/examples/frameworks/nyx5_http.py +255 -0
- package/examples/frameworks/openai-agents.mjs +41 -0
- package/examples/hola-mundo.mjs +4 -4
- package/migrations/0009_contratos_estado.sql +4 -0
- package/package.json +6 -2
- package/server.json +2 -2
- package/src/correo/acciones.js +216 -0
- package/src/correo/adjuntos.js +185 -0
- package/src/correo/agente.js +247 -13
- package/src/correo/asistente.js +502 -77
- package/src/correo/ci.js +262 -0
- package/src/correo/cobro.js +164 -0
- package/src/correo/estafeta.js +558 -59
- package/src/correo/ideas.js +154 -0
- package/src/correo/identidad.js +81 -0
- package/src/correo/politica.js +92 -3
- package/src/correo/unirse.js +14 -4
- package/src/libro/libro.js +6 -2
- package/src/libro/verifica.js +239 -36
- package/src/nucleo/almacen-d1.js +24 -5
- package/src/nucleo/almacen.js +31 -4
- package/src/nucleo/crypto.js +36 -0
- package/src/nucleo/keccak.js +118 -0
- package/src/nucleo/objetos.js +78 -0
- package/src/nucleo/secp256k1.js +228 -0
- package/src/plataformas/app-html.js +1 -1
- package/src/plataformas/home-html.js +1 -1
- package/src/plataformas/node.js +18 -1
- package/src/plataformas/spec-html.js +2 -2
- package/src/plataformas/terms-html.js +2 -0
- package/src/plataformas/worker.js +21 -1
- package/src/puentes/ejecutor.js +118 -0
- package/src/puentes/herramientas.js +137 -12
- package/src/puentes/mcp.js +4 -2
- package/src/puentes/x402-pagador.js +248 -0
- package/src/version.js +1 -1
|
@@ -13,10 +13,13 @@
|
|
|
13
13
|
// NYX5_FEE_BPS fee de la casa en basis points (default 1000 = 10%)
|
|
14
14
|
// NYX5_INDEX 'on' para operar el índice federado (default off)
|
|
15
15
|
// NYX5_EMAIL 'on' para habilitar el puente de correo
|
|
16
|
+
// NYX5_IDEAS 'on' para el buzón ideas@ (registra y confirma, nunca ejecuta; exige NYX5_VAULT_KEY)
|
|
16
17
|
// DB binding D1
|
|
17
18
|
|
|
18
19
|
import { Estafeta } from '../correo/estafeta.js';
|
|
20
|
+
import { TERMS_HTML } from './terms-html.js';
|
|
19
21
|
import { D1Store } from '../nucleo/almacen-d1.js';
|
|
22
|
+
import { ObjetosR2 } from '../nucleo/objetos.js';
|
|
20
23
|
import { extractText, resendProvider, addressFromHeader, decodeMimeWords } from '../puentes/email.js';
|
|
21
24
|
|
|
22
25
|
// El catálogo sembrado de esta casa. Tres tareas deterministas y baratas: su función NO es
|
|
@@ -59,7 +62,13 @@ function estafetaDesde(env) {
|
|
|
59
62
|
publicUrl: cfg('PUBLIC_URL') || `https://${domain}`,
|
|
60
63
|
adminToken: cfg('ADMIN_TOKEN'),
|
|
61
64
|
store: new D1Store(env.DB),
|
|
65
|
+
// Adjuntos cifrados (NX-413): el almacén de objetos es el bucket R2 del binding BLOBS. Sin
|
|
66
|
+
// binding la casa NO aloja adjuntos y /blob lo dice con un 503, en vez de aceptar un archivo
|
|
67
|
+
// que se perdería. El bucket y el binding los pone Nicholas al desplegar (ARQUITECTURA §4.9).
|
|
68
|
+
objetos: env.BLOBS ? new ObjetosR2(env.BLOBS) : null,
|
|
62
69
|
policy: { registration: cfg('REGISTRATION') || 'invite' },
|
|
70
|
+
// /terms existe sólo si la casa lo enciende: son declaraciones vinculantes del dueño (aprobadas 14-sep-2026).
|
|
71
|
+
terms: cfg('TERMS') === 'on' ? TERMS_HTML : null,
|
|
63
72
|
libro: { welcome: Number(cfg('WELCOME') || 0), feeBps: Number(cfg('FEE_BPS') || 1000),
|
|
64
73
|
// Escrow que vence (NX-503), en horas: gracia para que el comprador recupere, y ventana de revisión.
|
|
65
74
|
reclaimGraceMs: (Number(cfg('RECLAIM_GRACE_HOURS')) || 24) * 3600_000, reviewWindowMs: (Number(cfg('REVIEW_WINDOW_HOURS')) || 72) * 3600_000 },
|
|
@@ -75,6 +84,8 @@ function estafetaDesde(env) {
|
|
|
75
84
|
remoto: { enabled: cfg('MCP_REMOTE') === 'on', vaultKey: env.NYX5_VAULT_KEY, dias: Number(cfg('REMOTE_DAYS')) || 30 },
|
|
76
85
|
// Asistentes que contestan solos (src/correo/asistente.js). Sin clave de la API, no existen.
|
|
77
86
|
asistente: { apiKey: env.ANTHROPIC_API_KEY },
|
|
87
|
+
// ideas@ (src/correo/ideas.js): el buzón de vacaciones. Se enciende por casa y usa la bóveda.
|
|
88
|
+
ideas: { enabled: cfg('IDEAS') === 'on' },
|
|
78
89
|
log: (...a) => console.log(...a),
|
|
79
90
|
});
|
|
80
91
|
return instancia;
|
|
@@ -126,7 +137,15 @@ export default {
|
|
|
126
137
|
const estafeta = estafetaDesde(env);
|
|
127
138
|
const url = new URL(request.url);
|
|
128
139
|
let body = null;
|
|
129
|
-
|
|
140
|
+
let raw = null;
|
|
141
|
+
// La subida de un adjunto (PUT /blob/<id>) llega como BYTES, no como JSON: un archivo no es
|
|
142
|
+
// JSON y pasarlo en base64 lo infla un tercio para nada. El tope de aquí es mayor que el de la
|
|
143
|
+
// ruta a propósito, para que el motivo del rechazo lo dé la ruta nombrando el límite real.
|
|
144
|
+
if (request.method === 'PUT' && /^\/blob\/[^/]+$/.test(url.pathname)) {
|
|
145
|
+
if (Number(request.headers.get('content-length') || 0) > 16 * 1024 * 1024) return Response.json({ reason: 'cuerpo demasiado grande' }, { status: 413 });
|
|
146
|
+
raw = new Uint8Array(await request.arrayBuffer());
|
|
147
|
+
if (raw.length > 16 * 1024 * 1024) return Response.json({ reason: 'cuerpo demasiado grande' }, { status: 413 });
|
|
148
|
+
} else if (request.method === 'POST' || request.method === 'PUT') {
|
|
130
149
|
const len = Number(request.headers.get('content-length') || 0);
|
|
131
150
|
if (len > 2 * 1024 * 1024) return Response.json({ reason: 'cuerpo demasiado grande' }, { status: 413 });
|
|
132
151
|
// /oauth/token recibe form-urlencoded (RFC 6749 §4.1.3): Claude manda el canje y el refresco
|
|
@@ -153,6 +172,7 @@ export default {
|
|
|
153
172
|
query: url.searchParams,
|
|
154
173
|
headers: Object.fromEntries([...request.headers].map(([k, v]) => [k.toLowerCase(), v])),
|
|
155
174
|
body,
|
|
175
|
+
raw,
|
|
156
176
|
ip: request.headers.get('cf-connecting-ip') || null,
|
|
157
177
|
};
|
|
158
178
|
const out = await estafeta.handleRequest(rx);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Nyx5/1 — El lado CI del ejecutor (NX-412, 27-oct-2026): lo que corre DENTRO del job.
|
|
2
|
+
//
|
|
3
|
+
// La casa dispara el CI con un aviso de nueve campos y sin el contenido del sobre (ver
|
|
4
|
+
// `src/correo/ci.js`). Este módulo es lo que el job hace con ese aviso, y son tres pasos:
|
|
5
|
+
//
|
|
6
|
+
// 1. VERIFICAR el aviso. Un `repository_dispatch` lo puede lanzar cualquiera que tenga el token
|
|
7
|
+
// del repositorio, y el payload lo escribe quien dispara. Así que el aviso se comprueba contra
|
|
8
|
+
// la tarjeta de la casa: `sig` tiene que ser una firma Ed25519 de `ci:<sobre>:<dirección>` por
|
|
9
|
+
// una de las llaves que la casa publica. Un aviso que no verifica NO se trabaja: ni se lee el
|
|
10
|
+
// buzón, ni se gasta un minuto de runner.
|
|
11
|
+
// 2. RECOGER el encargo. El aviso no trae el contenido: el job lee `GET /mailbox/<local>` con la
|
|
12
|
+
// llave del rol (un secret del repositorio) y descifra el sobre. La llave nunca pasó por la
|
|
13
|
+
// casa ni por el chat.
|
|
14
|
+
// 3. RESPONDER y CONFIRMAR. La respuesta va firmada por la dirección del rol, en el mismo hilo, y
|
|
15
|
+
// recién entonces se confirma el sobre: si el job muere a mitad, el sobre sigue pendiente y la
|
|
16
|
+
// casa lo vuelve a despachar. Confirmar antes de responder perdería el encargo en silencio.
|
|
17
|
+
//
|
|
18
|
+
// Cero dependencias y nada propio de GitHub: el aviso es un objeto JSON y de dónde salió es asunto
|
|
19
|
+
// del workflow.
|
|
20
|
+
|
|
21
|
+
import { Agent } from '../correo/agente.js';
|
|
22
|
+
import { verifyBytes } from '../nucleo/crypto.js';
|
|
23
|
+
|
|
24
|
+
const CAMPOS = ['nyx5', 'house', 'to', 'envelope', 'from', 'type', 'at', 'sig'];
|
|
25
|
+
|
|
26
|
+
export function parsearAviso(texto) {
|
|
27
|
+
let a;
|
|
28
|
+
try { a = typeof texto === 'string' ? JSON.parse(texto) : texto; } catch { throw new Error('the notice is not JSON'); }
|
|
29
|
+
if (!a || typeof a !== 'object') throw new Error('the notice is not an object');
|
|
30
|
+
const faltan = CAMPOS.filter((c) => typeof a[c] !== 'string' || !a[c]);
|
|
31
|
+
if (faltan.length) throw new Error(`the notice is missing: ${faltan.join(', ')}`);
|
|
32
|
+
if (a.nyx5 !== '1') throw new Error(`unknown protocol version: ${a.nyx5}`);
|
|
33
|
+
if (!a.to.endsWith(`@${a.house}`)) throw new Error(`the notice says house ${a.house} but address ${a.to}`);
|
|
34
|
+
return a;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// La tarjeta de la casa, pedida a la casa misma. `estafeta` se pasa cuando no se quiere resolver por
|
|
38
|
+
// DNS (pruebas, o una casa que todavía no publica su TXT).
|
|
39
|
+
export async function tarjetaDeCasa(house, { estafeta = null, fetchImpl = globalThis.fetch } = {}) {
|
|
40
|
+
const base = (estafeta || `https://${house}`).replace(/\/$/, '');
|
|
41
|
+
const r = await fetchImpl(`${base}/.well-known/nyx5.json`);
|
|
42
|
+
if (!r.ok) throw new Error(`the house card of ${house} answered ${r.status}`);
|
|
43
|
+
const card = await r.json();
|
|
44
|
+
if (card.domain !== house) throw new Error(`that card says domain ${card.domain}, not ${house}`);
|
|
45
|
+
if (!Array.isArray(card.keys) || !card.keys.length) throw new Error(`the card of ${house} publishes no keys`);
|
|
46
|
+
return card;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Verifica el aviso contra las llaves que la casa publica. Cualquiera de ellas sirve: una casa que
|
|
50
|
+
// rota su llave firma con la nueva y sigue publicando la vieja un tiempo.
|
|
51
|
+
export function verificarAviso(aviso, tarjeta) {
|
|
52
|
+
const mensaje = `ci:${aviso.envelope}:${aviso.to}`;
|
|
53
|
+
return tarjeta.keys.some((k) => { try { return verifyBytes(mensaje, aviso.sig, k.sig); } catch { return false; } });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const COMPLETA = (k) => !!(k?.sig && k?.sigPriv && k?.enc && k?.encPriv);
|
|
57
|
+
|
|
58
|
+
// Qué formas de llave se aceptan, y por qué tres y no una. La canónica es la que escribe
|
|
59
|
+
// `nyx5 keygen`: `{ address, estafeta, keys: { sig, sigPriv, enc, encPriv } }`. La plana es la misma
|
|
60
|
+
// sin el envoltorio, que es lo que tiene a mano quien arma el secret desde otro lado. Y el MAPA
|
|
61
|
+
// `{ "<dirección>": <una de las dos> }` existe porque un repositorio suele ser la casa de VARIOS
|
|
62
|
+
// roles: la fábrica de Rosetta tiene siete que ejecutan, y pedir siete secrets para un workflow que
|
|
63
|
+
// ya sabe a quién va dirigido el aviso es fricción sin nada a cambio. Del mapa se usa SÓLO la
|
|
64
|
+
// entrada del destinatario del aviso.
|
|
65
|
+
export function llaveDe(bulto, destino) {
|
|
66
|
+
if (!bulto || typeof bulto !== 'object') throw new Error('the role key must be a JSON object');
|
|
67
|
+
const dir = String(destino).toLowerCase();
|
|
68
|
+
const una = (k) => {
|
|
69
|
+
if (COMPLETA(k?.keys)) return { address: String(k.address || dir).toLowerCase(), keys: k.keys };
|
|
70
|
+
if (COMPLETA(k)) return { address: String(k.address || dir).toLowerCase(), keys: { sig: k.sig, sigPriv: k.sigPriv, enc: k.enc, encPriv: k.encPriv } };
|
|
71
|
+
return null;
|
|
72
|
+
};
|
|
73
|
+
const sola = una(bulto);
|
|
74
|
+
if (sola) return sola;
|
|
75
|
+
// Un mapa: se busca por la dirección del aviso y se ignora todo lo demás.
|
|
76
|
+
if (Object.hasOwn(bulto, dir)) {
|
|
77
|
+
const del = una(bulto[dir]);
|
|
78
|
+
if (del) return { address: dir, keys: del.keys };
|
|
79
|
+
}
|
|
80
|
+
const direcciones = Object.keys(bulto).filter((k) => k.includes('@'));
|
|
81
|
+
if (direcciones.length) throw new Error(`that key bundle has no entry for ${dir} (it carries ${direcciones.length})`);
|
|
82
|
+
throw new Error('the role key must carry sig, sigPriv, enc and encPriv (or be a map of address to key)');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function agenteDe(aviso, bulto, { estafeta = null, hosts = {}, fetchImpl = globalThis.fetch } = {}) {
|
|
86
|
+
const { address, keys } = llaveDe(bulto, aviso.to);
|
|
87
|
+
if (address !== String(aviso.to).toLowerCase()) throw new Error(`that key is of ${address}, and the notice is for ${aviso.to}`);
|
|
88
|
+
return new Agent({ address, keys, estafeta: estafeta || `https://${aviso.house}`, hosts, fetchImpl });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Paso 1 + 2: verificar y recoger. Devuelve el sobre y el encargo ya descifrado.
|
|
92
|
+
export async function recoger({ aviso, keys, estafeta = null, hosts = {}, fetchImpl = globalThis.fetch }) {
|
|
93
|
+
const a = parsearAviso(aviso);
|
|
94
|
+
const tarjeta = await tarjetaDeCasa(a.house, { estafeta, fetchImpl });
|
|
95
|
+
if (!verificarAviso(a, tarjeta)) throw new Error(`the notice is not signed by ${a.house}: nothing was read and nothing was done`);
|
|
96
|
+
const agente = agenteDe(a, keys, { estafeta, hosts, fetchImpl });
|
|
97
|
+
const bandeja = await agente.inbox({ limit: 200 });
|
|
98
|
+
const m = bandeja.find((x) => x.envelope?.id === a.envelope);
|
|
99
|
+
// Que no esté no es un error del aviso: el sobre pudo ser confirmado por una corrida anterior.
|
|
100
|
+
if (!m) return { aviso: a, agente, envelope: null, abierto: null, encargo: null, reason: 'that envelope is no longer in the mailbox: another run already took it' };
|
|
101
|
+
if (m.envelope.from !== a.from) throw new Error(`the notice says ${a.from} and the envelope in the mailbox is from ${m.envelope.from}`);
|
|
102
|
+
const abierto = await agente.open(m.envelope);
|
|
103
|
+
// `encargo` es lo normalizado: el workflow no tiene por que saber que el cuerpo vive bajo
|
|
104
|
+
// `content` ni que un sobre en claro y uno cifrado llegan por caminos distintos.
|
|
105
|
+
const encargo = { media: abierto.content?.media ?? null, body: abierto.content?.body ?? null, project: abierto.project || null, role: abierto.role || null };
|
|
106
|
+
return { aviso: a, agente, envelope: m.envelope, abierto, encargo, reason: null };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Paso 3: responder y recién entonces confirmar. El orden importa: si el job muere entre las dos
|
|
110
|
+
// cosas, el sobre sigue pendiente y la casa lo vuelve a despachar. Al revés se perdería callado.
|
|
111
|
+
export async function responder({ aviso, keys, texto, estafeta = null, hosts = {}, fetchImpl = globalThis.fetch, ack = true, type = 'result' }) {
|
|
112
|
+
const a = parsearAviso(aviso);
|
|
113
|
+
if (typeof texto !== 'string' || !texto.trim()) throw new Error('an empty answer is not an answer');
|
|
114
|
+
const agente = agenteDe(a, keys, { estafeta, hosts, fetchImpl });
|
|
115
|
+
const env = await agente.send({ to: a.from, type, body: texto, thread: a.envelope, inReplyTo: a.envelope, project: a.project || undefined });
|
|
116
|
+
const acked = ack ? await agente.ack([a.envelope]) : [];
|
|
117
|
+
return { sent: env.id, acked };
|
|
118
|
+
}
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
// Las descripciones son lo único que el modelo lee para decidir si usa Nyx5: dicen la capacidad,
|
|
8
8
|
// la garantía y el momento de uso, no el mecanismo. Las cuida test/mcp.test.js.
|
|
9
9
|
|
|
10
|
-
import { proyectoDe } from '../correo/politica.js';
|
|
10
|
+
import { proyectoDe, nombreDeProyecto } from '../correo/politica.js';
|
|
11
|
+
import { MEDIA_GATE } from '../correo/asistente.js';
|
|
12
|
+
import { MEDIA_COBRO_CONFIRMACION, MONEDAS, TIPOS_CUENTA } from '../correo/cobro.js';
|
|
11
13
|
|
|
12
14
|
export const TOOLS = [
|
|
13
15
|
{ 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).',
|
|
@@ -18,9 +20,10 @@ export const TOOLS = [
|
|
|
18
20
|
thread: { type: 'string' }, in_reply_to: { type: 'string' },
|
|
19
21
|
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
22
|
role: { type: 'string', description: 'your role in that project, if any (e.g. "main", "lab")' },
|
|
23
|
+
summary: { type: 'string', description: 'ONE line, IN THE CLEAR, saying what this is about (max 140 chars). The body travels encrypted and nobody but the recipient opens it; this line does not, so the house can put it in the subject of the email notice and its owner can decide from a phone without opening anything. Write what it is ABOUT, never what the envelope protects: the house and their email provider read it. Optional, and it only reaches someone who registered an email for notices.' },
|
|
21
24
|
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
25
|
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
|
|
26
|
+
{ 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 request for a service you publish (media nyx5.pedido) is answered with nyx5_quote {service, to, in_reply_to}.',
|
|
24
27
|
inputSchema: { type: 'object', properties: { limit: { type: 'integer', default: 20 }, project: { type: 'string', description: 'only messages of this project (see nyx5_send)' } } } },
|
|
25
28
|
{ 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
29
|
inputSchema: { type: 'object', required: ['ids'], properties: { ids: { type: 'array', items: { type: 'string' } } } } },
|
|
@@ -40,9 +43,16 @@ export const TOOLS = [
|
|
|
40
43
|
price: { type: 'integer', description: 'tokens, entero (required without service)' }, concept: { type: 'string', description: 'required without service' },
|
|
41
44
|
terms: { type: 'object', description: 'criterio de aceptación, plazo, scope del mandato, etc.' },
|
|
42
45
|
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' } } }
|
|
46
|
+
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' } } },
|
|
47
|
+
in_reply_to: { type: 'string', description: 'id of a service request (media nyx5.pedido) you are answering: the quote goes in its thread with the published terms, the test derived from your acceptance and the request input, and verifica@ as arbiter' } } } },
|
|
44
48
|
{ 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
49
|
inputSchema: { type: 'object', required: ['quote'], properties: { quote: { type: 'object' } } } },
|
|
50
|
+
{ name: 'nyx5_hire', description: 'Hire a service from an agent profile in one step: sends the request, waits for the quote and accepts it only if it matches the published catalogue (price, contract, your input, verifica@ as arbiter when a test is published); in escrow the payment is held until that test passes. Use it when you found a provider and want the deal closed.',
|
|
51
|
+
inputSchema: { type: 'object', required: ['agent', 'service'], properties: {
|
|
52
|
+
agent: { type: 'string', description: 'address of the seller' }, service: { type: 'string', description: 'id of a service in its profile (nyx5_profile op=get)' },
|
|
53
|
+
input: { type: 'object', description: 'what the seller needs to do the job; for a published test it also fills its fields (http_status: url; sha256: expect; json_path: url, path, expect)' },
|
|
54
|
+
note: { type: 'string' }, auto_accept: { type: 'boolean', default: true, description: 'false: only send the request; the quote lands in your mailbox for you to accept' },
|
|
55
|
+
max_price: { type: 'integer', description: 'tokens; above it nothing is requested nor accepted' }, wait: { type: 'number', description: 'seconds to wait for the quote, 1 to 90 (default 25)' } } } },
|
|
46
56
|
{ 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
57
|
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
58
|
{ 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.',
|
|
@@ -61,7 +71,7 @@ export const TOOLS = [
|
|
|
61
71
|
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
72
|
{ 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
73
|
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
|
|
74
|
+
{ 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, each with its projects and what is pending per project. Use it to pick up where you left off, from any device: the history lives in the house.',
|
|
65
75
|
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
76
|
{ 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
77
|
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)' } } } },
|
|
@@ -71,6 +81,42 @@ export const TOOLS = [
|
|
|
71
81
|
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
82
|
{ 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
83
|
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' } } } },
|
|
84
|
+
// ----- qa@ como servicio (NX-606): dos envoltorios de nyx5_send con el media correcto -----
|
|
85
|
+
{ name: 'nyx5_qa_spec', description: 'Turn a request into an acceptance contract: qa@<house> replies with numbered, checkable criteria and the sha256 of its text so you can seal it in the notary. Use it before delegating work whose "done" could be disputed. Paid from credit you pay to qa@ beforehand; the reply lands signed in your mailbox.',
|
|
86
|
+
inputSchema: { type: 'object', required: ['request'], properties: { to: { type: 'string', description: 'the qa address; default qa@<your house>' }, request: { type: 'string', description: 'what you want done, in your words' } } } },
|
|
87
|
+
{ name: 'nyx5_qa_gate', description: 'Have a delivery judged against a sealed acceptance contract: qa@<house> answers pass, fail or abstain per criterion as JSON signed by the house. Use it before releasing an escrow. The contract hash must be sealed in the notary first; otherwise it is rejected before any cost. Abstention costs half.',
|
|
88
|
+
inputSchema: { type: 'object', required: ['spec_sha256', 'spec', 'delivery'], properties: { to: { type: 'string', description: 'the qa address; default qa@<your house>' }, spec_sha256: { type: 'string', description: 'hex sha256 of the contract text, already sealed with nyx5_notarize' }, spec: { type: 'string', description: 'the exact contract text (its sha256 must be spec_sha256)' }, delivery: { type: 'object', required: ['text'], properties: { text: { type: 'string', description: 'the delivery to judge (qa@ does not fetch urls)' }, url: { type: 'string' }, sha256: { type: 'string' } } }, note: { type: 'string' } } } },
|
|
89
|
+
// ----- NX-502: pedido de pago por transferencia (dinero real, fuera del Libro; NO en el remoto) -----
|
|
90
|
+
{ name: 'nyx5_payment_request', description: 'Ask someone for a real-money bank transfer (Chile: CLP or USD). The account details travel encrypted to that person only; the house never sees nor holds them, and their signed confirmation with the bank reference reaches your mailbox when they pay. Use it to get paid outside the ledger. Nyx5 never touches the money.',
|
|
91
|
+
inputSchema: { type: 'object', required: ['to', 'amount', 'name', 'rut', 'bank', 'account_type', 'account_number'], properties: {
|
|
92
|
+
to: { type: 'string', description: 'address of who pays (a person, not a connected Claude: the house holds those keys)' },
|
|
93
|
+
amount: { type: ['string', 'number'], description: 'CLP: whole pesos, e.g. 15000; USD: up to two decimals, e.g. "12.50"' },
|
|
94
|
+
currency: { type: 'string', enum: MONEDAS, default: 'CLP' },
|
|
95
|
+
name: { type: 'string', description: 'name on the account (up to 120 characters)' }, rut: { type: 'string', description: 'RUT with its check digit, e.g. 12.345.678-5' },
|
|
96
|
+
bank: { type: 'string', description: 'bank name (up to 60 characters)' }, account_type: { type: 'string', enum: TIPOS_CUENTA },
|
|
97
|
+
account_number: { type: 'string', description: 'digits and dashes, up to 30' }, reference: { type: 'string', description: 'what this is for (up to 140 characters)' },
|
|
98
|
+
thread: { type: 'string' } } } },
|
|
99
|
+
{ name: 'nyx5_payment_confirm', description: 'Tell whoever sent you a payment request that you paid it from your bank: your confirmation with the bank reference travels encrypted and signed, in reply to their request, so they hold your word and nothing else. Use it right after you made the transfer yourself. Nyx5 does not move the money and does not check the bank.',
|
|
100
|
+
inputSchema: { type: 'object', required: ['in_reply_to', 'bank_reference'], properties: {
|
|
101
|
+
in_reply_to: { type: 'string', description: 'id of the payment request envelope (media nyx5.cobro) in your mailbox' },
|
|
102
|
+
from: { type: 'string', description: 'who sent it, if you already acknowledged it (to find it in the conversation)' },
|
|
103
|
+
bank_reference: { type: 'string', description: 'the transfer reference your bank gave you (up to 80 characters)' } } } },
|
|
104
|
+
// ----- NX-413: adjuntos cifrados. La descripción dice el límite y qué NO sale del cifrado: es
|
|
105
|
+
// lo único que un agente lee antes de decidir qué expone. -----
|
|
106
|
+
{ name: 'nyx5_attach', description: 'Encrypt a file so a message carries it for real: the recipient downloads it with their signature and checks the hash. Use it when you say "here are the files" and the other agent is on another machine. Up to 10 MB; the key never leaves the encrypted block, so the house holds bytes it cannot read.',
|
|
107
|
+
inputSchema: { type: 'object', required: ['name', 'to'], properties: {
|
|
108
|
+
name: { type: 'string', description: 'file name the recipient will see (max 120 chars); it travels INSIDE the encrypted block' },
|
|
109
|
+
to: { type: 'array', items: { type: 'string' }, description: 'addresses allowed to download it: the same recipients you will send the envelope to' },
|
|
110
|
+
path: { type: 'string', description: 'local file to read (this bridge runs beside you); use it or content' },
|
|
111
|
+
content: { type: 'string', description: 'the file content when you have it in hand, not on disk' },
|
|
112
|
+
base64: { type: 'boolean', description: 'true if content is base64 (a binary file)' },
|
|
113
|
+
media: { type: 'string', description: 'media type like application/pdf (default application/octet-stream)' } } } },
|
|
114
|
+
{ name: 'nyx5_attachment', description: 'Download an attachment that arrived in an envelope and get the real bytes: it is decrypted with the key that came inside the encrypted block and rejected if the sha256 of the plaintext does not match what the sender signed. Use it when a message says it carries files, before acting on them.',
|
|
115
|
+
inputSchema: { type: 'object', required: ['id'], properties: {
|
|
116
|
+
id: { type: 'string', description: 'id of the envelope that carries it (nyx5_inbox or nyx5_conversation)' },
|
|
117
|
+
from: { type: 'string', description: 'who sent it, if you already acknowledged the envelope' },
|
|
118
|
+
blob: { type: 'string', description: 'which attachment, when the envelope carries several (default: all of them)' },
|
|
119
|
+
out_dir: { type: 'string', description: 'directory to write the files into (default: the current one)' } } } },
|
|
74
120
|
{ 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
121
|
inputSchema: { type: 'object', properties: {} } },
|
|
76
122
|
];
|
|
@@ -80,8 +126,10 @@ export const TOOLS = [
|
|
|
80
126
|
// sigue haciendo cumplir cada límite por su cuenta. Nació de un reporte de Nicholas: conectar desde
|
|
81
127
|
// el teléfono le pidió tres permisos (inbox, send, wait) antes del primer mensaje.
|
|
82
128
|
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
|
-
|
|
84
|
-
|
|
129
|
+
// `nyx5_attach` ESCRIBE en la casa (sube un objeto y gasta cupo) y `nyx5_attachment` escribe en el
|
|
130
|
+
// disco de quien corre el puente: ninguna de las dos es de sólo lectura, aunque no mueva dinero.
|
|
131
|
+
const MUEVE_DINERO = new Set(['nyx5_accept', 'nyx5_libro', 'nyx5_tomar', 'nyx5_hire']);
|
|
132
|
+
const TITULOS = { nyx5_attach: 'Attach a file', nyx5_attachment: 'Download an attachment', 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_hire: 'Hire a published service', 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', nyx5_qa_spec: 'Ask qa@ for an acceptance contract', nyx5_qa_gate: 'Ask qa@ to judge a delivery', nyx5_payment_request: 'Request a payment', nyx5_payment_confirm: 'Confirm a payment' };
|
|
85
133
|
for (const t of TOOLS) {
|
|
86
134
|
t.title = TITULOS[t.name] || t.name;
|
|
87
135
|
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 };
|
|
@@ -90,7 +138,9 @@ for (const t of TOOLS) {
|
|
|
90
138
|
// Lo que el conector remoto expone: mensajería y nada que mueva saldo. El subagente de un teléfono
|
|
91
139
|
// es de alcance `messages_only` (la casa se lo niega igual si lo intenta); ofrecerle herramientas
|
|
92
140
|
// que van a fallar sólo le enseñaría al modelo a prometer lo que no puede cumplir.
|
|
93
|
-
|
|
141
|
+
// Tampoco el pedido de pago (NX-502): la llave del subagente vive en la bóveda y la casa cifraría en
|
|
142
|
+
// su nombre, o sea que el RUT y la cuenta pasarían por la casa en claro. Va sólo por el puente local.
|
|
143
|
+
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_qa_spec', 'nyx5_qa_gate', 'nyx5_whoami']);
|
|
94
144
|
|
|
95
145
|
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
146
|
|
|
@@ -98,16 +148,25 @@ export function instrucciones({ remoto = false, address = null, contactos = [] }
|
|
|
98
148
|
if (!remoto) return INSTRUCCIONES;
|
|
99
149
|
// Sus contactos van aquí para que el dueño pueda decir "escríbele a Nico" sin dictar direcciones.
|
|
100
150
|
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
|
-
|
|
151
|
+
// Lo que este camino NO puede hacer, dicho en voz alta (NX-808). MENSAJERIA excluye las
|
|
152
|
+
// herramientas de adjunto a propósito: la casa guarda esta llave, así que el archivo le pasaría
|
|
153
|
+
// en claro por delante. Callarlo dejaba al recién llegado concluyendo que Nyx5 no manda archivos.
|
|
154
|
+
// Texto aprobado por Nicholas el 3-nov-2026, palabra por palabra: no se edita sin volver a él.
|
|
155
|
+
const limites = 'This connector cannot send files: the house holds this key, so an attachment would pass through it in the clear.'
|
|
156
|
+
+ " Because it holds this key, the house can also read this sub-agent's messages — your root key never passes through it."
|
|
157
|
+
+ ' To send files, run `npx @nyx5/nyx5 join` once; the attachment tools come with a key only you hold.';
|
|
158
|
+
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. ${limites} Before trusting a stranger, check who they really are: every card is certified by its domain.${extra}`;
|
|
102
159
|
}
|
|
103
160
|
|
|
104
161
|
export async function llamar(agent, name, args = {}, { permitidas = null, esperaMaxS = 90 } = {}) {
|
|
105
162
|
const text = (v) => ({ content: [{ type: 'text', text: typeof v === 'string' ? v : JSON.stringify(v, null, 2) }] });
|
|
106
163
|
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
164
|
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 }); }
|
|
165
|
+
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, summary: args.summary, extensions: args.aval ? { 'urn:nyx5:ext:aval': args.aval } : undefined }); return text({ id: r.id, jobs: r.jobs, encrypted: r.encrypted, summary: r.envelope?.extensions?.['urn:nyx5:ext:resumen']?.text ?? null }); }
|
|
109
166
|
case 'nyx5_inbox': {
|
|
110
|
-
|
|
167
|
+
// La MISMA normalización que al enviar y que la casa (NFKC, sin invisibles): `trim().toLowerCase()`
|
|
168
|
+
// dejaba pasar un homógrafo por el filtro del puente mientras la casa lo colapsaba (14-sep-2026).
|
|
169
|
+
const proyecto = args.project ? nombreDeProyecto(args.project) : null;
|
|
111
170
|
const msgs = (await agent.inbox({ limit: args.limit ?? 20 })).filter((m) => !proyecto || proyectoDe(m.envelope) === proyecto);
|
|
112
171
|
const opened = [];
|
|
113
172
|
// `received` es la hora que entiende `since` de nyx5_wait (la de llegada al buzón, no la de
|
|
@@ -120,8 +179,17 @@ export async function llamar(agent, name, args = {}, { permitidas = null, espera
|
|
|
120
179
|
case 'nyx5_outbox': return text(await agent.outbox());
|
|
121
180
|
case 'nyx5_directory': return text(await agent.directory(args.house, args));
|
|
122
181
|
case 'nyx5_search': return text(await agent.search(args.index, args));
|
|
123
|
-
case 'nyx5_quote': {
|
|
182
|
+
case 'nyx5_quote': {
|
|
183
|
+
if (args.in_reply_to) {
|
|
184
|
+
// Contestar un pedido (NX-305): la cotización sale de la ficha y del input del pedido, en su hilo.
|
|
185
|
+
const pedido = await agent.pedido(args.in_reply_to, args.to || null);
|
|
186
|
+
if (!pedido) return { ...text(`no service request with id ${args.in_reply_to} in your mailbox (pass to if you already acknowledged it)`), isError: true };
|
|
187
|
+
const r = await agent.quoteFromCatalog(pedido, { service: args.service ?? null });
|
|
188
|
+
return text({ id: r.id, quote_id: r.quote.id, contract: r.quote.contract, price: r.quote.price, service: r.quote.service, arbiter: r.quote.arbiter || null, verify: r.quote.terms?.verify || null, in_reply_to: pedido.id });
|
|
189
|
+
}
|
|
190
|
+
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
191
|
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)' }); }
|
|
192
|
+
case 'nyx5_hire': return text(await agent.hire({ agent: args.agent, service: args.service, input: args.input ?? {}, note: args.note, autoAccept: args.auto_accept ?? true, maxPrice: args.max_price ?? null, wait: Math.max(1, Math.min(Number(args.wait ?? 25) || 25, esperaMaxS)) }));
|
|
125
193
|
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
194
|
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
195
|
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}` }); }
|
|
@@ -151,7 +219,7 @@ export async function llamar(agent, name, args = {}, { permitidas = null, espera
|
|
|
151
219
|
// Lo que YA estaba sin leer se entrega igual (nada se pierde), pero marcado como anterior a la
|
|
152
220
|
// espera, para que nadie lo tome por la respuesta a lo que acaba de mandar.
|
|
153
221
|
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) ===
|
|
222
|
+
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) === nombreDeProyecto(args.project)));
|
|
155
223
|
const m = anterior || await agent.wait({ from: args.from, thread: args.thread, since: desde, project: args.project, seconds: secs });
|
|
156
224
|
if (!m) return text({ message: null, waited_seconds: secs, note: 'nothing arrived; call again to keep listening' });
|
|
157
225
|
let abierto;
|
|
@@ -184,6 +252,63 @@ export async function llamar(agent, name, args = {}, { permitidas = null, espera
|
|
|
184
252
|
}
|
|
185
253
|
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
254
|
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' }); }
|
|
255
|
+
// qa@ (NX-606): mensajes, no Libro. El `pay` previo que carga el crédito va por nyx5_libro.
|
|
256
|
+
case 'nyx5_qa_spec': {
|
|
257
|
+
if (typeof args.request !== 'string' || !args.request.trim()) return { ...text('nyx5_qa_spec needs request: what you want done'), isError: true };
|
|
258
|
+
const to = args.to || `qa@${agent.domain}`;
|
|
259
|
+
const r = await agent.send({ to: [to], body: args.request });
|
|
260
|
+
return text({ id: r.id, to, note: `the contract arrives as a message from ${to} (nyx5_wait with from=${to}); its footer carries the sha256 to seal with nyx5_notarize` });
|
|
261
|
+
}
|
|
262
|
+
case 'nyx5_qa_gate': {
|
|
263
|
+
if (typeof args.spec !== 'string' || typeof args.spec_sha256 !== 'string' || typeof args.delivery?.text !== 'string') return { ...text('nyx5_qa_gate needs spec_sha256, spec (the exact sealed text) and delivery.text'), isError: true };
|
|
264
|
+
const to = args.to || `qa@${agent.domain}`;
|
|
265
|
+
const r = await agent.send({ to: [to], media: MEDIA_GATE, body: { spec_sha256: args.spec_sha256, spec: args.spec, delivery: { text: args.delivery.text, url: args.delivery.url, sha256: args.delivery.sha256 }, note: args.note } });
|
|
266
|
+
return text({ id: r.id, to, note: `the verdict arrives as a message from ${to} (nyx5_wait with from=${to}): JSON signed by the house, pass | fail | abstain` });
|
|
267
|
+
}
|
|
268
|
+
// NX-502: dinero real fuera del Libro. El cliente valida (RUT, monto), exige cifrado y se niega
|
|
269
|
+
// ante una dirección cuya llave guarda la casa; la casa sólo anota el evento sin monto.
|
|
270
|
+
case 'nyx5_payment_request': {
|
|
271
|
+
const r = await agent.paymentRequest({ to: args.to, amount: args.amount, currency: args.currency, name: args.name, rut: args.rut, bank: args.bank, account_type: args.account_type, account_number: args.account_number, reference: args.reference, thread: args.thread });
|
|
272
|
+
return text({ id: r.id, request_id: r.request_id, to: r.to, currency: r.currency, encrypted: r.encrypted, note: `their confirmation arrives as a message from ${r.to} with media ${MEDIA_COBRO_CONFIRMACION} (nyx5_wait with from=${r.to}); Nyx5 never touches the money` });
|
|
273
|
+
}
|
|
274
|
+
case 'nyx5_payment_confirm': {
|
|
275
|
+
const pedido = await agent.cobro(args.in_reply_to, args.from || null);
|
|
276
|
+
if (!pedido) return { ...text(`no payment request with id ${args.in_reply_to} in your mailbox (pass from if you already acknowledged it)`), isError: true };
|
|
277
|
+
const r = await agent.paymentConfirm(pedido, { bank_reference: args.bank_reference });
|
|
278
|
+
return text({ id: r.id, request_id: r.request_id, to: r.to, in_reply_to: r.in_reply_to, encrypted: r.encrypted, note: 'sent encrypted; Nyx5 does not move money, it only carries your confirmation' });
|
|
279
|
+
}
|
|
280
|
+
// NX-413. Sólo por el puente LOCAL: leer y escribir archivos exige un disco al lado del modelo,
|
|
281
|
+
// y en el remoto la llave del subagente vive en la bóveda, o sea que la casa cifraría en su
|
|
282
|
+
// nombre y el archivo pasaría por ella en claro. MENSAJERIA no las incluye.
|
|
283
|
+
case 'nyx5_attach': {
|
|
284
|
+
const fs = await import('node:fs');
|
|
285
|
+
if (!args.path && args.content == null) return { ...text('nyx5_attach needs path (a local file) or content'), isError: true };
|
|
286
|
+
const datos = args.path ? fs.readFileSync(args.path) : Buffer.from(String(args.content), args.base64 ? 'base64' : 'utf8');
|
|
287
|
+
const nombre = args.name || (args.path ? String(args.path).split('/').pop() : null);
|
|
288
|
+
const att = await agent.subirAdjunto(datos, { name: nombre, media: args.media, to: args.to || [] });
|
|
289
|
+
// La llave se devuelve porque es lo que hay que pasarle a nyx5_send; va al mismo turno del
|
|
290
|
+
// modelo que ya tiene el archivo, y de ahí entra al bloque cifrado del sobre, no al sobre.
|
|
291
|
+
return text({ attachment: att, note: `pass it in nyx5_send attachments; the key travels inside the encrypted block, so send it encrypted (encrypt: true, the default) or the house can open the file` });
|
|
292
|
+
}
|
|
293
|
+
case 'nyx5_attachment': {
|
|
294
|
+
const fs = await import('node:fs');
|
|
295
|
+
const sobre = await agent.conAdjuntos(args.id, args.from || null);
|
|
296
|
+
if (!sobre) return { ...text(`no envelope with id ${args.id} in your mailbox (pass from if you already acknowledged it)`), isError: true };
|
|
297
|
+
const dir = args.out_dir || '.';
|
|
298
|
+
const out = [];
|
|
299
|
+
for (const att of sobre.attachments) {
|
|
300
|
+
if (args.blob && String(att.blob).toLowerCase() !== String(args.blob).toLowerCase()) continue;
|
|
301
|
+
try {
|
|
302
|
+
const datos = await agent.bajarAdjunto(att, { from: sobre.from });
|
|
303
|
+
// El nombre lo escribe el REMITENTE: se usa sólo su parte final, nunca como ruta.
|
|
304
|
+
const destino = `${dir.replace(/\/$/, '')}/${String(att.name).split('/').pop()}`;
|
|
305
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
306
|
+
fs.writeFileSync(destino, datos);
|
|
307
|
+
out.push({ name: att.name, media: att.media, bytes: datos.length, sha256_ok: true, path: destino });
|
|
308
|
+
} catch (e) { out.push({ name: att.name, blob: att.blob, error: e.message }); }
|
|
309
|
+
}
|
|
310
|
+
return text(out.length ? out : { error: `envelope ${args.id} carries no attachment with blob ${args.blob}` });
|
|
311
|
+
}
|
|
187
312
|
case 'nyx5_whoami': return text(await agent.whoami());
|
|
188
313
|
default: return { ...text(`unknown tool: ${name}`), isError: true };
|
|
189
314
|
}
|
package/src/puentes/mcp.js
CHANGED
|
@@ -14,8 +14,10 @@ export { TOOLS };
|
|
|
14
14
|
const PROTOCOL = '2025-06-18';
|
|
15
15
|
const SUPPORTED = new Set(['2025-06-18', '2025-03-26']);
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// `agent` ya resuelto (lo normal desde el 4-nov-2026: la CLI deduce la identidad y, si no hay,
|
|
18
|
+
// se une sola) o `agentFile` con la ruta, que es el camino de siempre y sigue sirviendo.
|
|
19
|
+
export async function runMcpServer({ agent: agentDado = null, agentFile = null, hosts = {} }) {
|
|
20
|
+
const agent = agentDado || await Agent.load(agentFile, { hosts });
|
|
19
21
|
const out = (msg) => process.stdout.write(JSON.stringify(msg) + '\n');
|
|
20
22
|
const text = (v) => ({ content: [{ type: 'text', text: typeof v === 'string' ? v : JSON.stringify(v, null, 2) }] });
|
|
21
23
|
const call = (name, args) => llamar(agent, name, args);
|