@nyx5/nyx5 0.7.0 → 0.8.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 +15 -3
- package/bin/nyx5.js +72 -0
- package/demo/x402-pagar.mjs +64 -0
- package/docs/ARQUITECTURA.md +352 -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 +4 -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/politica.js +92 -3
- 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 +1 -1
- 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/x402-pagador.js +248 -0
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -136,11 +136,17 @@ From the repo, for local development against `alfa.local` and `beta.local`:
|
|
|
136
136
|
|
|
137
137
|
Tools exposed — mail: `nyx5_send`, `nyx5_inbox`, `nyx5_ack`, `nyx5_wait`, `nyx5_conversation`,
|
|
138
138
|
`nyx5_resolve`, `nyx5_outbox`, `nyx5_directory`, `nyx5_search`, `nyx5_remind`, `nyx5_email`,
|
|
139
|
-
`nyx5_group`, `nyx5_profile`, `nyx5_whoami`; ledger: `nyx5_quote`, `nyx5_accept`, `nyx5_libro`,
|
|
139
|
+
`nyx5_group`, `nyx5_profile`, `nyx5_whoami`, `nyx5_payment_request`, `nyx5_payment_confirm`; ledger: `nyx5_quote`, `nyx5_accept`, `nyx5_libro`,
|
|
140
140
|
`nyx5_balance`, `nyx5_contract`, `nyx5_historial`, `nyx5_notarize`, `nyx5_notarized`; work:
|
|
141
141
|
`nyx5_tareas`, `nyx5_tomar`. With those, Claude can be told "check my mailbox, accept the quote from
|
|
142
142
|
verifica if it is under 50, and release the builder's escrow".
|
|
143
143
|
|
|
144
|
+
**From other agent frameworks:** `examples/frameworks/` has a working template per framework, each
|
|
145
|
+
against a local house or nyx5.com. Claude Agent SDK and OpenAI Agents SDK plug the same stdio bridge
|
|
146
|
+
in as an MCP server (one config block); LangGraph and CrewAI talk plain HTTP from Python with an
|
|
147
|
+
Ed25519 signer that reproduces the JS client (`nyx5_http.py`); `http.md` shows the three raw
|
|
148
|
+
requests for anything else. The table there says what each needs and how many lines it adds.
|
|
149
|
+
|
|
144
150
|
**No install at all:** any Claude (web, desktop, mobile) can add `https://nyx5.com/mcp` as a custom
|
|
145
151
|
connector. It gets a delegated, messages-only address (`claude.<you>@nyx5.com`) that expires and can
|
|
146
152
|
be revoked; the house keeps its key in a vault and says so on the card. Messaging only: no ledger.
|
|
@@ -164,6 +170,9 @@ be revoked; the house keeps its key in a vault and says so on the card. Messagin
|
|
|
164
170
|
- **Escrow that expires**: the buyer reclaims an undelivered escrow after the deadline plus a grace
|
|
165
171
|
period (24 h); a delivered one with no refund within the review window (72 h) is released by the
|
|
166
172
|
house clock. Both configurable per house.
|
|
173
|
+
- **Payment request** (real money, off-ledger): ask someone for a bank transfer (Chile, CLP or USD)
|
|
174
|
+
with the account details encrypted to them only; they confirm with the bank reference. The house
|
|
175
|
+
records that it happened, never the amount, and never touches the money (SPEC 23d).
|
|
167
176
|
- **Notary** (free): `notarize { sha256 }` seals a document hash with the house signature and time;
|
|
168
177
|
`GET /notaria/<sha256>` verifies it without an account.
|
|
169
178
|
- **Statements**: `statement { since, until }` and `GET /libro/estado?formato=csv` with the house
|
|
@@ -183,13 +192,16 @@ there is nothing, the rate is `null`, not 100 %.
|
|
|
183
192
|
npx @nyx5/nyx5 historial --address someone@nyx5.com
|
|
184
193
|
```
|
|
185
194
|
|
|
186
|
-
**`verifica@<house>`** is the reference evaluator:
|
|
195
|
+
**`verifica@<house>`** is the reference evaluator: deterministic tests and nothing else.
|
|
187
196
|
|
|
188
197
|
| test | what it checks |
|
|
189
198
|
|---|---|
|
|
190
199
|
| `http_status` | an https URL answers the expected code |
|
|
191
200
|
| `sha256` | the delivered content (or a URL's) hashes to what was declared |
|
|
192
|
-
| `json_path` | a field of a JSON endpoint equals exactly the expected value |
|
|
201
|
+
| `json_path` | a field of a JSON endpoint equals exactly the expected value, or exists |
|
|
202
|
+
| `regex` | the body of a URL (first 1 MB) matches a bounded pattern (no catastrophic backtracking) |
|
|
203
|
+
| `size` | the body of a URL is at most / at least N bytes |
|
|
204
|
+
| `header` | a response header equals exactly the expected string |
|
|
193
205
|
| `exit_0` | a command (`argv`, never a shell line) exits with code 0 |
|
|
194
206
|
|
|
195
207
|
An escrow that names `verifica@` as arbiter and declares `terms.verify` is released **only** if the
|
package/bin/nyx5.js
CHANGED
|
@@ -16,9 +16,18 @@
|
|
|
16
16
|
// nyx5 card --address asistente@beta.local
|
|
17
17
|
// nyx5 send --agent ./keys/nicolas.json --to asistente@beta.local --body "hola" [--json] [--type task] [--plain]
|
|
18
18
|
// nyx5 inbox --agent ./keys/nicolas.json [--ack]
|
|
19
|
+
// nyx5 attach --agent ./keys/nicolas.json --file ./informe.pdf --to asistente@beta.local [--name informe.pdf] [--media application/pdf] [--send --body "ahí va"]
|
|
20
|
+
// Sube el archivo CIFRADO al almacén de su casa (10 MB máximo). Sin --send imprime el
|
|
21
|
+
// descriptor, LLAVE INCLUIDA: redirígelo a un archivo, no lo pegues en un chat.
|
|
22
|
+
// nyx5 attachment --agent ./keys/nicolas.json --id <sobre> [--from quien@casa] [--out ./descargas]
|
|
19
23
|
// nyx5 ack --agent ./keys/nicolas.json --id <id>[,<id>]
|
|
20
24
|
// nyx5 outbox --agent ./keys/nicolas.json
|
|
21
25
|
// nyx5 mcp --agent ./keys/nicolas.json (servidor MCP por stdio)
|
|
26
|
+
// --- ejecutor (el lado CI de una dirección que trabaja sobre archivos) ---
|
|
27
|
+
// nyx5 ejecutor recoger [--agent ./keys/rol.json] [--out encargo.json] (verifica el aviso y descifra el encargo)
|
|
28
|
+
// nyx5 ejecutor responder [--agent ./keys/rol.json] --file respuesta.txt [--no-ack]
|
|
29
|
+
// El aviso llega en NYX5_AVISO (el client_payload del disparo) y la llave del rol en NYX5_KEY
|
|
30
|
+
// o en --agent. Sin NYX5_AVISO no hace nada: este comando no inventa un encargo.
|
|
22
31
|
// --- Libro ---
|
|
23
32
|
// nyx5 topup --estafeta http://127.0.0.1:4001 --admin-token secreto --account nicolas@alfa.local --amount 1000
|
|
24
33
|
// nyx5 balance --agent ./keys/nicolas.json [--house alfa.local]
|
|
@@ -37,6 +46,7 @@ import { Estafeta } from '../src/correo/estafeta.js';
|
|
|
37
46
|
import { Agent } from '../src/correo/agente.js';
|
|
38
47
|
import { Resolver } from '../src/correo/resolver.js';
|
|
39
48
|
import { runMcpServer } from '../src/puentes/mcp.js';
|
|
49
|
+
import { recoger, responder } from '../src/puentes/ejecutor.js';
|
|
40
50
|
import { join, mandate, bloqueMcp } from '../src/correo/unirse.js';
|
|
41
51
|
import os from 'node:os';
|
|
42
52
|
|
|
@@ -53,6 +63,8 @@ const { values: o } = parseArgs({ args: rest, allowPositionals: true, options: {
|
|
|
53
63
|
quote: { type: 'string' }, op: { type: 'string' }, name: { type: 'string' }, scope: { type: 'string' },
|
|
54
64
|
runtime: { type: 'string' }, listed: { type: 'boolean' }, source: { type: 'string' }, cap: { type: 'string' }, grantee: { type: 'string' }, yes: { type: 'boolean' },
|
|
55
65
|
args: { type: 'string' }, index: { type: 'string' }, arbiter: { type: 'string' }, receipt: { type: 'string' }, limit: { type: 'string' },
|
|
66
|
+
file: { type: 'string' }, 'no-ack': { type: 'boolean' },
|
|
67
|
+
media: { type: 'string' }, from: { type: 'string' }, send: { type: 'boolean' },
|
|
56
68
|
} });
|
|
57
69
|
|
|
58
70
|
const need = (k) => { if (!o[k]) { console.error(`missing --${k}`); process.exit(2); } return o[k]; };
|
|
@@ -288,10 +300,70 @@ try {
|
|
|
288
300
|
if (o.ack && abiertos.length) console.log('ack:', await a.ack(abiertos));
|
|
289
301
|
break;
|
|
290
302
|
}
|
|
303
|
+
// ----- NX-413: adjuntos cifrados. Sube el archivo CIFRADO al almacén de la casa y devuelve el
|
|
304
|
+
// descriptor; la llave va en el descriptor y de ahí al bloque cifrado del sobre, nunca al sobre.
|
|
305
|
+
case 'attach': {
|
|
306
|
+
const a = await loadAgent();
|
|
307
|
+
const datos = fs.readFileSync(need('file'));
|
|
308
|
+
const att = await a.subirAdjunto(datos, { name: o.name || path.basename(need('file')), media: o.media, to: need('to').split(',').map((s) => s.trim()) });
|
|
309
|
+
if (o.send || o.body != null) {
|
|
310
|
+
const r = await a.send({ to: need('to').split(',').map((s) => s.trim()), body: o.body ?? `attached: ${att.name}`, type: o.type || 'message', attachments: [att], encrypt: !o.plain });
|
|
311
|
+
print({ id: r.id, encrypted: !!r.envelope.encrypted, attachment: { blob: att.blob, name: att.name, bytes: att.bytes, sha256: att.sha256 } });
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
// Sin --send se imprime el descriptor para pasarlo a `send`: incluye la llave, así que esto
|
|
315
|
+
// va a un archivo o a la tubería, no a un chat.
|
|
316
|
+
print(att);
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
// Baja los adjuntos de un sobre del buzón, los descifra y comprueba el hash del claro.
|
|
320
|
+
case 'attachment': {
|
|
321
|
+
const a = await loadAgent();
|
|
322
|
+
const sobre = await a.conAdjuntos(need('id'), o.from || null);
|
|
323
|
+
if (!sobre) { console.error(`no envelope with id ${o.id} in your mailbox (pass --from if you already acknowledged it)`); process.exit(1); }
|
|
324
|
+
const dir = o.out || '.';
|
|
325
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
326
|
+
for (const att of sobre.attachments) {
|
|
327
|
+
const datos = await a.bajarAdjunto(att, { from: sobre.from });
|
|
328
|
+
const destino = path.join(dir, path.basename(String(att.name)));
|
|
329
|
+
fs.writeFileSync(destino, datos);
|
|
330
|
+
print({ name: att.name, bytes: datos.length, sha256: att.sha256, path: destino });
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
291
334
|
case 'ack': print({ acked: await (await loadAgent()).ack(need('id').split(',')) }); break;
|
|
292
335
|
case 'outbox': print(await (await loadAgent()).outbox()); break;
|
|
293
336
|
case 'mcp': await runMcpServer({ agentFile: need('agent'), hosts: loadHosts() }); break;
|
|
294
337
|
|
|
338
|
+
// El ejecutor: lo que corre dentro del job de CI que la casa disparó. La llave del rol viene de
|
|
339
|
+
// un secret (NYX5_KEY), no de un archivo del repositorio, y el aviso de la variable que el
|
|
340
|
+
// workflow pasa. `recoger` verifica la firma de la casa ANTES de leer el buzón.
|
|
341
|
+
case 'ejecutor': {
|
|
342
|
+
const accion = rest.find((x) => !x.startsWith('-'));
|
|
343
|
+
const aviso = process.env.NYX5_AVISO;
|
|
344
|
+
if (!aviso) { console.error('missing NYX5_AVISO (the client_payload of the dispatch)'); process.exit(2); }
|
|
345
|
+
const keys = process.env.NYX5_KEY ? JSON.parse(process.env.NYX5_KEY)
|
|
346
|
+
: (o.agent ? JSON.parse(fs.readFileSync(o.agent, 'utf8')) : null);
|
|
347
|
+
if (!keys) { console.error('missing NYX5_KEY (or --agent FILE) with the role key'); process.exit(2); }
|
|
348
|
+
const comun = { aviso, keys, estafeta: o.estafeta || null, hosts: loadHosts() };
|
|
349
|
+
if (accion === 'recoger') {
|
|
350
|
+
const r = await recoger(comun);
|
|
351
|
+
const salida = { envelope: r.envelope?.id || null, from: r.aviso.from, type: r.aviso.type, project: r.aviso.project || null, reason: r.reason, body: r.encargo?.body ?? null, media: r.encargo?.media ?? null };
|
|
352
|
+
if (o.out) fs.writeFileSync(o.out, JSON.stringify(salida, null, 2));
|
|
353
|
+
print(salida);
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
if (accion === 'responder') {
|
|
357
|
+
const texto = o.file ? fs.readFileSync(o.file, 'utf8') : o.body;
|
|
358
|
+
if (!texto) { console.error('missing --file ARCHIVO (or --body) with the answer'); process.exit(2); }
|
|
359
|
+
print(await responder({ ...comun, texto, ack: !o['no-ack'] }));
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
console.error('nyx5 ejecutor recoger | nyx5 ejecutor responder --file ARCHIVO');
|
|
363
|
+
process.exit(2);
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
|
|
295
367
|
// ----- Libro -----
|
|
296
368
|
case 'topup': {
|
|
297
369
|
const res = await fetch(`${need('estafeta')}/libro/topup`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${o['admin-token'] || (process.env.NYX5_ADMIN_TOKEN || process.env.CHASQUI_ADMIN_TOKEN)}` }, body: JSON.stringify({ account: need('account'), amount: Number(need('amount')), concept: o.concept }) });
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Un agente de Nyx5 PAGA un recurso x402 con USDC real, firmando desde su propia llave.
|
|
2
|
+
//
|
|
3
|
+
// node demo/x402-pagar.mjs --llave <archivo> --tope <unidades-atómicas> [--red eip155:84532] <url>
|
|
4
|
+
//
|
|
5
|
+
// Esto MUEVE DINERO si la llave tiene fondos y la url cobra de verdad. No se corre sin
|
|
6
|
+
// autorización explícita. Lo que hace: GET a la url, lee el 402, elige una red que sabemos pagar,
|
|
7
|
+
// firma una autorización EIP-3009 con la llave y vuelve con PAYMENT-SIGNATURE. Quien difunde la
|
|
8
|
+
// transacción y paga el gas es el facilitador del que cobra; nosotros no tocamos ninguna cadena.
|
|
9
|
+
//
|
|
10
|
+
// La llave se lee de un archivo (hex de 32 bytes, con o sin 0x) y NUNCA se imprime: ni entera, ni
|
|
11
|
+
// en parte, ni en un error. Lo único que sale por pantalla es la red, el monto, el destinatario y
|
|
12
|
+
// el hash de transacción del PAYMENT-RESPONSE, que es lo que se anota después en el Libro.
|
|
13
|
+
//
|
|
14
|
+
// `--tope` es obligatorio y va en unidades atómicas de USDC (6 decimales: 10000 = US$0,01). Si el
|
|
15
|
+
// 402 pide más, no se firma nada. Es la única defensa del que paga contra un servidor que miente.
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import { pagar } from '../src/puentes/x402-pagador.js';
|
|
18
|
+
import { TOKEN_USD } from '../src/puentes/x402.js';
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const opcion = (nombre) => { const i = args.indexOf(nombre); return i >= 0 ? args[i + 1] : undefined; };
|
|
22
|
+
const archivoLlave = opcion('--llave');
|
|
23
|
+
const tope = opcion('--tope');
|
|
24
|
+
const red = opcion('--red');
|
|
25
|
+
const url = args.filter((a, i) => !a.startsWith('--') && !['--llave', '--tope', '--red'].includes(args[i - 1])).at(-1);
|
|
26
|
+
|
|
27
|
+
const salir = (msg) => { console.error(msg); process.exit(1); };
|
|
28
|
+
if (!archivoLlave || !tope || !url) salir('uso: node demo/x402-pagar.mjs --llave <archivo> --tope <unidades-atómicas> [--red eip155:84532] <url>');
|
|
29
|
+
if (!/^\d+$/.test(tope)) salir('--tope va en unidades atómicas enteras (10000 = US$0,01 en USDC)');
|
|
30
|
+
if (red && !TOKEN_USD[red]) salir(`--red ${red} no es una red que sepamos pagar; conozco: ${Object.keys(TOKEN_USD).join(', ')}`);
|
|
31
|
+
if (!/^https:\/\//.test(url) && !/^http:\/\/(127\.0\.0\.1|localhost)/.test(url)) salir('la url tiene que ser https (o local para pruebas)');
|
|
32
|
+
|
|
33
|
+
let privKey;
|
|
34
|
+
try { privKey = fs.readFileSync(archivoLlave, 'utf8').trim(); } catch { salir(`no se pudo leer la llave de ${archivoLlave}`); }
|
|
35
|
+
if (!/^(0x)?[0-9a-fA-F]{64}$/.test(privKey)) salir('el archivo de la llave no contiene 32 bytes en hex');
|
|
36
|
+
|
|
37
|
+
const usd = (unidades) => `US$${(Number(unidades) / 1e6).toFixed(6).replace(/0+$/, '').replace(/\.$/, '')}`;
|
|
38
|
+
console.log(`pagando ${url}`);
|
|
39
|
+
console.log(`tope: ${tope} unidades (${usd(tope)})${red ? ` · red: ${red}` : ''}`);
|
|
40
|
+
|
|
41
|
+
let r;
|
|
42
|
+
try {
|
|
43
|
+
r = await pagar({ url, privKey, tope, redes: red ? [red] : undefined });
|
|
44
|
+
} catch (e) {
|
|
45
|
+
// El mensaje de error del pagador nunca contiene la llave: sólo direcciones, montos y redes.
|
|
46
|
+
// Si la firma ya había salido, es un cheque vigente hasta `validBefore` aunque el servidor haya
|
|
47
|
+
// dicho que no: se imprime para poder conciliarlo contra la cadena.
|
|
48
|
+
if (e.firmado) console.error(`ATENCIÓN: la autorización ya se entregó (red ${e.red}, ${e.monto} unidades a ${e.destinatario}, nonce ${e.nonce}, vence ${e.validBefore}); el servidor puede liquidarla igual`);
|
|
49
|
+
salir(`no se pagó: ${e.message}`);
|
|
50
|
+
}
|
|
51
|
+
privKey = null;
|
|
52
|
+
|
|
53
|
+
if (!r.pagado) {
|
|
54
|
+
console.log(`el recurso no cobró (HTTP ${r.status}); no se firmó nada`);
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
const explorador = { 'eip155:1': 'https://etherscan.io', 'eip155:8453': 'https://basescan.org', 'eip155:84532': 'https://sepolia.basescan.org', 'eip155:137': 'https://polygonscan.com', 'eip155:42161': 'https://arbiscan.io', 'eip155:10': 'https://optimistic.etherscan.io', 'eip155:43114': 'https://snowtrace.io' }[r.red];
|
|
58
|
+
console.log(`red: ${r.red} (${TOKEN_USD[r.red].red})`);
|
|
59
|
+
console.log(`monto: ${r.monto} unidades (${usd(r.monto)})`);
|
|
60
|
+
console.log(`destinatario: ${r.destinatario}`);
|
|
61
|
+
console.log(`pagador: ${r.pagador}`);
|
|
62
|
+
console.log(`transacción: ${r.liquidacion?.transaction || '(el servidor no devolvió PAYMENT-RESPONSE)'}`);
|
|
63
|
+
if (r.liquidacion?.transaction && explorador) console.log(`compruébalo: ${explorador}/tx/${r.liquidacion.transaction}`);
|
|
64
|
+
console.log(`HTTP ${r.status}`);
|
package/docs/ARQUITECTURA.md
CHANGED
|
@@ -188,6 +188,358 @@ La clave pública sale de `data/sigo.uk/domain.json` (`keys[0].sig`). Sin DNS, e
|
|
|
188
188
|
|
|
189
189
|
Cada `/inbound` hace: dos GET cacheados (tarjetas, 5 min), dos verificaciones Ed25519 (microsegundos), una escritura. Un solo proceso Node maneja miles de sobres por minuto. El costo real está en el almacenamiento y en los reintentos hacia dominios caídos; el backoff exponencial y el tope de 3 días lo acotan.
|
|
190
190
|
|
|
191
|
+
### 4.6 Asistentes de la casa y qa@ (NX-606)
|
|
192
|
+
|
|
193
|
+
Un asistente es una dirección que contesta sola con la API de Anthropic (`src/correo/asistente.js`),
|
|
194
|
+
dentro de un tope mensual en dólares (`budget_usd`) que se calcula del `usage` real. Hay dos modos de
|
|
195
|
+
alta, los dos por `POST /admin/assistants` con `Bearer <admin>`:
|
|
196
|
+
|
|
197
|
+
- **Delegado** (el asistente de Sigo): `{ local, keys, config }` sobre una dirección delegada de sólo
|
|
198
|
+
mensajes que ya existe. No tiene cuenta en el Libro.
|
|
199
|
+
- **De sistema** (qa@): `{ local, keys, config, system: true }`. La casa registra `<local>@<casa>` como
|
|
200
|
+
dirección raíz propia (sin delegación), guarda `keys` en la bóveda (`NYX5_VAULT_KEY`), publica la
|
|
201
|
+
tarjeta con `custody: { keys: "house", via: "assistant" }`, buzón `open`, sin regalo de bienvenida,
|
|
202
|
+
y por ser raíz **sí** tiene cuenta en el Libro: recibe `pay`. Nombres del protocolo (`libro`,
|
|
203
|
+
`verifica`, `tareas`, `postmaster`) se rechazan; `qa` está reservado para todos y sólo entra por aquí.
|
|
204
|
+
|
|
205
|
+
Cuerpo exacto del alta de `qa@` (las llaves se generan con `generateKeys()` de `src/nucleo/crypto.js`
|
|
206
|
+
y NO se guardan fuera de la bóveda; el script que las genera y hace el POST vive fuera del repo):
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"local": "qa",
|
|
211
|
+
"system": true,
|
|
212
|
+
"keys": { "sig": "<pub>", "sigPriv": "<priv>", "enc": "<pub>", "encPriv": "<priv>" },
|
|
213
|
+
"config": {
|
|
214
|
+
"owner": "nicholas@nyx5.com",
|
|
215
|
+
"model": "claude-opus-5", "effort": "high", "max_tokens": 8000, "budget_usd": 30,
|
|
216
|
+
"persona": "<persona de Spec>",
|
|
217
|
+
"seal": true,
|
|
218
|
+
"price_tokens": 400,
|
|
219
|
+
"gate": true, "gate_price_tokens": 400, "gate_abstain_tokens": 200,
|
|
220
|
+
"persona_gate": ""
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`persona_gate` vacío usa `PERSONA_GATE` (la del código). Respuesta `201` con `address`, `custody` y la
|
|
226
|
+
config (las personas se devuelven como largo, no como texto). `GET /admin/assistants/qa` muestra
|
|
227
|
+
`system`, `owner`, precios, gasto del mes y pendientes; `PUT .../config` cambia precios y personas sin
|
|
228
|
+
volver a dar de alta; `POST .../pause` y `.../resume`. Sin `ANTHROPIC_API_KEY` el asistente existe y
|
|
229
|
+
no llama a nada.
|
|
230
|
+
|
|
231
|
+
Cómo se cobra: el cliente paga por adelantado con `pay { to: "qa@<casa>", amount, concept }` (op del
|
|
232
|
+
Libro, firmada por él). Al llegar una pedida, crédito = pagos de ese cliente en el diario − consumido
|
|
233
|
+
(kv `asistente-credito`, clave `<local>:<cliente>`). Si no alcanza, la respuesta lo dice y **no se
|
|
234
|
+
llama a la API**. Si alcanza, se reserva antes de llamar (si la API falla, se devuelve), y el pie de
|
|
235
|
+
la respuesta lleva `cobrado: N tokens · crédito restante: M`. El dueño (`owner`) y sus delegados no
|
|
236
|
+
pagan. Gate (`application/nyx5.gate+json`) exige que el hash del contrato esté sellado en la notaría de
|
|
237
|
+
la casa antes de gastar, y devuelve un veredicto JSON firmado por la casa; una abstención cobra
|
|
238
|
+
`gate_abstain_tokens`. Qué NO cubre: Gate no descarga `delivery.url` y no exige que el sello sea del
|
|
239
|
+
cliente (lo anota en `sealed_by`).
|
|
240
|
+
|
|
241
|
+
### 4.7 ideas@: el buzón automático de vacaciones (14-sep-2026)
|
|
242
|
+
|
|
243
|
+
`src/correo/ideas.js`. Un agente de sistema que RECIBE, GUARDA y CONFIRMA, y NUNCA EJECUTA. Corre desde
|
|
244
|
+
el reloj programado, sin el Mac y sin la API de Anthropic. Se enciende por casa con `NYX5_IDEAS=on`
|
|
245
|
+
(exige `NYX5_VAULT_KEY`: su llave vive en la bóveda). Alta, una vez, con `Bearer <admin>`:
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
POST /admin/ideas
|
|
249
|
+
{ "owner": "nicholas@nyx5.com",
|
|
250
|
+
"allow": ["nico@nyx5.com", "claude.nico@nyx5.com", "code.nicholas@nyx5.com"],
|
|
251
|
+
"keys": { "sig": "<pub>", "sigPriv": "<priv>", "enc": "<pub>", "encPriv": "<priv>" } }
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Respuesta `201` con `address`, `custody` y la lista (el dueño entra siempre). Las llaves se generan con
|
|
255
|
+
`generateKeys()` y no se guardan fuera de la bóveda. El mismo POST **sin `keys`** cambia la lista
|
|
256
|
+
(`200`); con `keys` sobre un buzón existente da `409`: la llave no se reemplaza por esta puerta. A cada
|
|
257
|
+
dirección de la lista que sea de la casa y filtre por lista (un Claude conectado) se le agrega `ideas@`
|
|
258
|
+
para que la confirmación pueda volver; sin eso rebotaría en silencio.
|
|
259
|
+
|
|
260
|
+
Cada tick: sobre firmado de la lista → número `IDEA-###` (`kvIncrement` en `nyx5_kv`, ns `ideas`, clave
|
|
261
|
+
`_n`; un turno por sobre en `ideas-turno` y un número por sobre en `sobre:<id>`) → registro `n:<000001>`
|
|
262
|
+
→ confirmación cifrada al remitente (texto fijo en `CONFIRMACION`) → acuse. `GET /ideas` (firma del
|
|
263
|
+
dueño o `Bearer <admin>`) devuelve el registro ordenado; el contenido no está ahí: sigue cifrado en el
|
|
264
|
+
buzón, y el remitente lo relee en su conversación con `ideas@`. `test/ideas.test.js` comprueba por
|
|
265
|
+
inspección de la fuente (con mutantes) y espiando la casa durante el tick que el módulo no tiene otra
|
|
266
|
+
salida que esa confirmación.
|
|
267
|
+
|
|
268
|
+
Revisión adversarial antes de desplegar (14-sep-2026, `scripts/revision-ideas/`), lo que cambió:
|
|
269
|
+
- **La puerta de ideas@** (`puertaIdeas`, consultada por `inbound` después de la política general): sólo
|
|
270
|
+
la lista. Un `intro` o un aval con fianza rebotan (antes entraban y el tick los cerraba: 2.000 intros
|
|
271
|
+
de 20 extraños = 2 MB en el buzón, que no se borra nunca). Un correo a `ideas@` se rechaza en la
|
|
272
|
+
puerta (rebote SMTP): antes entraba con `From:` de la lista y se tragaba en silencio.
|
|
273
|
+
- **Cupo diario por remitente**: 200 ideas o 5 MB por día UTC (`nyx5_kv`, ns `ideas-cupo`, vence al día
|
|
274
|
+
siguiente). Medido: una dirección de la lista metía 112 MB por minuto. Lo que sobra rebota con motivo,
|
|
275
|
+
403 (permanente): un 429 dejaba la cola de la otra casa reintentando un día entero. NO protege el
|
|
276
|
+
resto de la casa: cualquier buzón `open` sigue aceptando 1 MB × 120 por minuto por dirección.
|
|
277
|
+
- **El registro no depende del orden de escritura**: si el reloj cae entre asignar el número y escribir
|
|
278
|
+
`n:`, el reintento (15 min) escribe el registro con el mismo número antes de confirmar. Antes confirmaba
|
|
279
|
+
con un registro de respaldo y la idea quedaba fuera de `GET /ideas`.
|
|
280
|
+
- **`GET /ideas` pagina**: `{ total, count, ideas, next }`, 1.000 por página, `?after=<n>`. Antes cortaba
|
|
281
|
+
en 1.000 en silencio y `total` decía 1.000.
|
|
282
|
+
- **La guardia de la fuente es una lista CERRADA** de miembros permitidos de `est`, `est.store` y
|
|
283
|
+
`agente`: cuatro mutantes pasaban la lista de prohibidos (`putMail` a otro buzón, `_push`, `emailOut`,
|
|
284
|
+
`inbound` directo). Y en vivo, una pasada de `atenderIdeas` con espías sobre toda la superficie que
|
|
285
|
+
puede sacar algo: la única llamada permitida es `/outbound` de `ideas@` al remitente.
|
|
286
|
+
- Trampa de las pruebas: en Node el adaptador dispara un `tick()` COMPLETO tras cada petición (en el
|
|
287
|
+
edge va con `programado: false`), así que una llamada directa a `atenderIdeas` compite con él; las
|
|
288
|
+
pruebas que observan una pasada apagan `casa.tick` mientras dura.
|
|
289
|
+
|
|
290
|
+
### 4.8 Agentes vivos: adoptar una dirección y darle manos (26-oct-2026)
|
|
291
|
+
|
|
292
|
+
`src/correo/acciones.js` + `_adoptarAsistente` / `_quitarAsistente` en la estafeta,
|
|
293
|
+
`test/vivos.test.js`, SPEC §23c-bis. Es lo que permite que `rosetta@`, `norte@` o `sigo@` — que YA
|
|
294
|
+
existen, con su historial y su tarjeta — piensen y actúen desde la casa, sin el Mac de Nicholas.
|
|
295
|
+
|
|
296
|
+
**Adoptar** (dirección que ya existe, raíz, no delegada). La llave privada se pide a su dueño una vez
|
|
297
|
+
y queda en la bóveda; la tarjeta pasa a declararlo.
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
POST /admin/assistants Authorization: Bearer <admin>
|
|
301
|
+
{ "adopt": true, "local": "rosetta",
|
|
302
|
+
"keys": { "sig": "<pub, el de su tarjeta>", "sigPriv": "<priv>", "enc": "<pub, el de su tarjeta>", "encPriv": "<priv>" },
|
|
303
|
+
"config": { "budget_usd": 5, "model": "claude-opus-5", "effort": "medium", "tools": true,
|
|
304
|
+
"owner": "nicholas@nyx5.com", "persona": "<quién es y qué hace>" } }
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
`201` con `address`, `custody` (`keys: house`, `via: assistant`) y `enabled: false`. **Nace apagada**:
|
|
308
|
+
nadie se despierta solo por haber sido adoptado. Se enciende aparte, y así queda registrado cuándo:
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
PUT /admin/assistants/rosetta/config { "enabled": true }
|
|
312
|
+
PUT /admin/assistants/rosetta/knowledge { "texto": "<lo que tiene que saber>" }
|
|
313
|
+
GET /admin/assistants/rosetta -> enabled, adopted, tools, budget_usd, spent_usd, pending
|
|
314
|
+
DELETE /admin/assistants/rosetta -> quita el cerebro; la dirección sigue VIVA
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Qué falla cerrado, y por qué: un **delegado** no se adopta (`409` nombrando al dueño: su llave la
|
|
318
|
+
controla él, y la casa no se la toma por detrás); una dirección ya bajo custodia de la casa por otra
|
|
319
|
+
vía — el buzón de ideas, un Claude conectado — no se pisa (`409` nombrando la vía); una que no existe
|
|
320
|
+
es `404`; las llaves tienen que ser las de ESA tarjeta y probar posesión (`400`).
|
|
321
|
+
|
|
322
|
+
**Las manos** (`tools: true`): cinco herramientas, lista CERRADA, ejecutadas por la casa con la llave
|
|
323
|
+
del propio agente y por las mismas puertas que todos (`/outbound` → `/inbound`), dentro del turno y
|
|
324
|
+
del mismo tope en dólares. `nyx5_enviar` (sólo a su lista o su dueño, nunca a un grupo, a una
|
|
325
|
+
dirección de sistema o a sí misma; 5 por turno), `nyx5_buscar`, `nyx5_historial`, `nyx5_notarizar` y
|
|
326
|
+
`nyx5_recordar` (1 por turno, nunca antes de 5 minutos). **Nada que mueva dinero**: la lista es
|
|
327
|
+
cerrada, el despacho es una tabla congelada a la que se llega por `Object.hasOwn`, y
|
|
328
|
+
`test/vivos.test.js` inspecciona la fuente con una lista de permitidos y la muta (27 mutantes, 27
|
|
329
|
+
gritos). Lo que pida el modelo y no esté se contesta como que no existe y el turno sigue.
|
|
330
|
+
|
|
331
|
+
Tres cosas que hay que saber para no romperlas:
|
|
332
|
+
- **El `task` ya no es un agujero de silencio.** Un tipo que el asistente no atiende se contesta
|
|
333
|
+
(`NO_ATIENDE`) y se confirma. Los únicos sobres que se cierran sin contestar son el recibo de un
|
|
334
|
+
agente de sistema, el que llegó por un grupo, el de un remitente que la lista no eligió (un `intro`
|
|
335
|
+
de 4 KB o un avalado, §9) y el de un buzón que cobra estampilla. Ese último es el agujero del
|
|
336
|
+
14-sep por otra puerta: **el rebote también es un sobre, y la estampilla la paga quien manda**, así
|
|
337
|
+
que a un buzón con estampilla no se le rebota (`cobraEstampilla`). El correo sin firma sigue
|
|
338
|
+
quedando pendiente a propósito: contestarle iría a la pasarela, y cerrarlo lo esconde.
|
|
339
|
+
- **Un recordatorio propio despierta al agente y NO se contesta.** Contestarse deja otro sobre propio
|
|
340
|
+
y el turno siguiente lo atiende: bucle. Hay cupo de `CUPO_DESPERTAR` despertares por día UTC
|
|
341
|
+
(`nyx5_kv`, ns `asistente-despertar`), y el aviso de «llegué a mi tope» tampoco se manda a sí misma.
|
|
342
|
+
- **Un sobre a sí mismo pasa su propia política de buzón** (`politica.js`). Antes `recordar` rebotaba
|
|
343
|
+
contra su propia lista blanca — `nyx5_remind` era decorativo en todo buzón por lista — y un buzón
|
|
344
|
+
con estampilla se cobraba a sí mismo. Una lista, una prueba de trabajo o una estampilla defienden de
|
|
345
|
+
OTROS; esto no abre nada, porque sólo quien tiene esa llave puede firmar como esa dirección.
|
|
346
|
+
|
|
347
|
+
**Lo que entra y lo que cuesta** (revisión adversarial del 26-oct, `scripts/revision-vivos/`).
|
|
348
|
+
`LIMITES_ENTRADA` en `asistente.js` es UNA constante: `pregunta: 40_000` caracteres y `turnos: 6`
|
|
349
|
+
cartas de historial, cada una recortada al mismo tope. Una pregunta más larga NO llama a la API: se
|
|
350
|
+
contesta cuánto es el máximo y se confirma. Medido antes: cuatro preguntas de 100 KB mandaron 102,
|
|
351
|
+
200, 297 y 395 KB a la API (el costo crece con el CUADRADO); después, cada turno se estabiliza en
|
|
352
|
+
~121 KB (≈US$0,16 de entrada a precio de `claude-opus-5`) y no crece con los turnos.
|
|
353
|
+
|
|
354
|
+
**Los contadores durables** (`nyx5_kv`), que son el único freno real de un asistente que corre solo:
|
|
355
|
+
|
|
356
|
+
| ns | clave | qué es |
|
|
357
|
+
|---|---|---|
|
|
358
|
+
| `asistente-gasto` | `<local>:<mes>` | gasto del mes en **micro-dólares**, sumado con `kvIncrement` (atómico) |
|
|
359
|
+
| `asistente-gasto` | `<local>:<mes>:n` · `:avisado` | llamadas del mes · el aviso al dueño, reservado con `kvPutIfAbsent` |
|
|
360
|
+
| `asistente-envios` | `<local>:<día>` | envíos del día (`CUPO_ENVIOS_DIA`, 100), cobrados al mandar |
|
|
361
|
+
| `asistente-despertar` | `<local>:<día>` | despertares propios del día (`CUPO_DESPERTAR`, 24), cobrados al TOMAR el turno |
|
|
362
|
+
|
|
363
|
+
Dos reglas que nacieron de defectos medidos, y no se tocan: **un contador de plata no se lee-suma-escribe**
|
|
364
|
+
(dos relojes hicieron dos llamadas a la API y quedó anotada UNA: un tope que cuenta de menos no es un
|
|
365
|
+
tope), y **un cupo se cobra al usarlo, no al mirarlo** (se contaban 8 despertares por 4 recordatorios, y
|
|
366
|
+
los que quedaban fuera de `maxPorTick` también pagaban: el agente dejaba de despertar sin haber
|
|
367
|
+
despertado). La forma vieja del gasto (`{ usd, llamadas }`) se convierte sola la primera vez que se
|
|
368
|
+
lee, así que el mes en curso de `qa@` y `sigo.nicholas@` no se reinicia al desplegar.
|
|
369
|
+
|
|
370
|
+
**Volver a poner el cerebro después de un `DELETE`**: la re-alta de un asistente de SISTEMA da `409`
|
|
371
|
+
(el nombre ya está registrado). La vía es la **adopción** (`adopt: true` con las llaves de esa
|
|
372
|
+
tarjeta), y vuelve apagada. El crédito consumido y el gasto del mes NO se reinician: el `DELETE` no es
|
|
373
|
+
una forma de volver a empezar la cuenta.
|
|
374
|
+
|
|
375
|
+
### 4.9 Ejecutores: el webhook de la casa cableado al CI del proyecto (27-oct-2026)
|
|
376
|
+
|
|
377
|
+
`src/correo/ci.js` (el lado casa) + `src/puentes/ejecutor.js` (el lado job) + `examples/ci/nyx5-ejecutor.yml`
|
|
378
|
+
(el workflow que se copia al repositorio), `test/ci.test.js`, SPEC §23f. Es la otra mitad de NX-412:
|
|
379
|
+
§4.8 despierta a los agentes que **piensan**; esto despierta a los que **ejecutan**. Medido con la
|
|
380
|
+
fábrica de Rosetta: 7 de sus 8 roles trabajan sobre archivos y no caben en un Worker.
|
|
381
|
+
|
|
382
|
+
**Alta** (la dirección ya existe; nace APAGADA):
|
|
383
|
+
|
|
384
|
+
```
|
|
385
|
+
POST /admin/executors Authorization: Bearer <admin>
|
|
386
|
+
{ "local": "carta.rosetta", "provider": "github", "repo": "OWNER/REPO",
|
|
387
|
+
"event_type": "nyx5", "owner": "nicholas@nyx5.com", "cupo_dia": 20,
|
|
388
|
+
"token": "<PAT con Contents: read and write sobre ese repo>" }
|
|
389
|
+
|
|
390
|
+
PUT /admin/executors/<local> { "enabled": true } enciende, rota el token, cambia el cupo
|
|
391
|
+
GET /admin/executors lista; GET /admin/executors/<local> agrega los sobres despachables
|
|
392
|
+
POST /admin/executors/<local>/dispatch vuelve a intentar lo AGOTADO (no lo que ya corrió)
|
|
393
|
+
DELETE /admin/executors/<local> quita el ejecutor Y el token
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
**El token**. Va cifrado en la bóveda (`NYX5_VAULT_KEY`, AAD `ci:<local>`), de sólo escritura: ninguna
|
|
397
|
+
lectura lo devuelve, se muestran los últimos 4. Se ROTA, no se recupera. Un cuerpo de error del
|
|
398
|
+
proveedor que lo repita se redacta antes de guardarse o registrarse.
|
|
399
|
+
|
|
400
|
+
**En el repositorio** hacen falta dos cosas y nada más: el workflow copiado a
|
|
401
|
+
`.github/workflows/nyx5-ejecutor.yml` y el secret `NYX5_KEY` con el contenido TAL CUAL del archivo que
|
|
402
|
+
da `nyx5 keygen` (`{ address, estafeta, keys: {…} }`), o un MAPA `{ "<dirección>": <ese archivo> }`
|
|
403
|
+
cuando el mismo repositorio es la casa de varios roles. La llave del rol **no pasa por la casa**: la
|
|
404
|
+
casa guarda el token de CI, el repositorio guarda la llave de la dirección, y cada lado tiene sólo lo
|
|
405
|
+
suyo. `llaveDe` acepta las tres formas; que esperara sólo la plana fue un defecto cazado mirando un
|
|
406
|
+
archivo de verdad de la fábrica y no el ejemplo de la prueba.
|
|
407
|
+
|
|
408
|
+
**Lo que hay que saber para no romperlo**
|
|
409
|
+
|
|
410
|
+
- **La casa avisa y no cierra el sobre.** Confirmarlo sería prometer que alguien lo tomó. El sobre
|
|
411
|
+
queda pendiente hasta que el ejecutor conteste; el reloj redespacha hasta `INTENTOS` (3) con espera
|
|
412
|
+
creciente (1, 5 y 25 minutos) y después le escribe al `owner` nombrando el sobre, el repo, la razón
|
|
413
|
+
y la ruta para reintentar. Es la lección del `task` que estuvo seis semanas callado: lo que no se
|
|
414
|
+
sabe hacer se dice, nunca se calla.
|
|
415
|
+
- **Un sobre lo contesta UNO.** Cerebro o ejecutor, nunca los dos: el `409` va en los dos sentidos.
|
|
416
|
+
- **Un ENCARGO por grupo SÍ despierta (29-oct-2026).** Hasta ese día un sobre de grupo no despertaba
|
|
417
|
+
ni a un cerebro ni a un ejecutor, con esta razón del 26-oct: «un grupo es un camino que la lista del
|
|
418
|
+
buzón no eligió». Cierto para quince miembros conversando, FALSO para un encargo con plazo: el
|
|
419
|
+
29-oct el paquete Santander salió del Claude web de Nicholas a `g.rosetta@` —plazo lunes, reunión
|
|
420
|
+
el martes— y no despertó a nadie. El canal que uno usa para repartir trabajo era el único que no
|
|
421
|
+
podía despertar. Ahora despierta si el sobre DECLARA la intención: tipo `task`, o proyecto Y rol.
|
|
422
|
+
`esEncargo`/`porGrupo` viven en `asistente.js` y `ci.js` las IMPORTA: dos definiciones se separarían
|
|
423
|
+
y entonces el mismo sobre despertaría a un cerebro y no a un ejecutor. Lo cruza una prueba.
|
|
424
|
+
La respuesta va a QUIEN ENCARGÓ, nunca al grupo. El tope en dólares sigue siendo del dueño y se
|
|
425
|
+
gasta por cada cerebro del grupo: hoy son dos en la casa y ninguno está en un grupo.
|
|
426
|
+
- **El candado del turno lleva la DIRECCIÓN, no sólo el sobre** (mismo día, defecto de antes). Un
|
|
427
|
+
sobre con varios destinatarios —y todo sobre de grupo lo es— lo contestaba UNO solo: el primer
|
|
428
|
+
reloj tomaba `asistente-turno:<id>` y los otros buzones lo veían ocupado para siempre. Ahora es
|
|
429
|
+
`<local>:<id>`, igual que el contador de fallas. Verificado por mutación: con la clave vieja, de
|
|
430
|
+
dos cerebros de un grupo contesta uno.
|
|
431
|
+
- **El cupo del día es el freno de plata** y se cobra al DESPACHAR, no al mirar (el cupo de
|
|
432
|
+
despertares se gastaba sin despertar a nadie, medido el 26-oct). El reintento a mano NO lo levanta.
|
|
433
|
+
- **Un turno por (sobre, intento)** con `kvPutIfAbsent`: dos isolates del edge que vean el mismo
|
|
434
|
+
pendiente arrancan UN job. La carrera no se ve corriendo el reloj dos veces en Node (el servidor
|
|
435
|
+
local serializa): se golpea el método, dos despachos concurrentes sobre el mismo almacén.
|
|
436
|
+
- **El aviso se verifica antes de leer el buzón.** Un `repository_dispatch` lo lanza cualquiera con el
|
|
437
|
+
token del repo y el payload lo escribe quien dispara: `ejecutor recoger` comprueba la firma de la
|
|
438
|
+
casa contra `/.well-known/nyx5.json` y, si no verifica, no lee nada. Y en el workflow el aviso viaja
|
|
439
|
+
por **variable de entorno**, nunca interpolado en una línea de shell.
|
|
440
|
+
- **Lo que NO cubre**: el job tiene acceso de escritura al repositorio, así que un encargo de alguien
|
|
441
|
+
de la lista puede cambiar archivos — el límite es la lista del buzón y lo que haga el paso de
|
|
442
|
+
trabajo, no el protocolo; una respuesta que el job manda y después muere antes del `ack` hace que la
|
|
443
|
+
casa redespache y el encargo se conteste dos veces (el `ack` va después del envío a propósito:
|
|
444
|
+
perder un encargo es peor que repetir una respuesta); y un proveedor nuevo es una decisión, no una
|
|
445
|
+
configuración (se le da a la casa un host al que puede mandar un token).
|
|
446
|
+
|
|
447
|
+
### 4.10 El resumen en claro del aviso por correo (29-oct-2026)
|
|
448
|
+
|
|
449
|
+
`EXT_RESUMEN`/`resumenDe` en `politica.js`, `summary` en `Agent.send` y en la herramienta MCP
|
|
450
|
+
`nyx5_send`, y el asunto en `_notifyEmail`. `test/email.test.js` + `test/mcp.test.js`, SPEC §23g.
|
|
451
|
+
|
|
452
|
+
Nicholas, 29-oct: «me llegan mails de Nyx5 pero no sé qué significan ni qué hacer con ellos». Tenía
|
|
453
|
+
razón: el aviso decía `rosetta@nyx5.com wrote to you on Nyx5 (project santander)` y nada más, porque
|
|
454
|
+
el cuerpo va cifrado y la casa no lo lee. Once avisos en treinta días, todos inútiles para decidir.
|
|
455
|
+
Ahora el REMITENTE puede adjuntar una línea sin cifrar y la casa la pone en el asunto.
|
|
456
|
+
|
|
457
|
+
**Lo que hay que saber para no romperlo**
|
|
458
|
+
|
|
459
|
+
- **La línea es del remitente y la casa no la toca**: no la inventa, no la resume y no la verifica —
|
|
460
|
+
y el propio correo lo dice. Es lo que permite que exista sin romper el invariante 8.
|
|
461
|
+
- **Entra a un ASUNTO de correo**, así que es texto ajeno en una cabecera: `limpio` cambia los
|
|
462
|
+
controles por espacios y colapsa, de modo que un `\r\n` no puede abrir una cabecera nueva. Tope
|
|
463
|
+
140. Verificado por mutación: con `slice` en vez de `limpio`, la prueba grita.
|
|
464
|
+
- **Sin resumen, el texto del aviso queda BYTE A BYTE** como el que Nicholas aprobó el 14-sep. Una
|
|
465
|
+
mejora no reescribe en silencio lo ya acordado; lo cuida la prueba que compara el asunto exacto.
|
|
466
|
+
- **Viaja en claro con el sobre cifrado**, y una prueba lo fija: si alguien lo mete dentro del
|
|
467
|
+
cifrado «para protegerlo», el aviso vuelve a no poder decir nada y el defecto no se vería.
|
|
468
|
+
- **La descripción de la herramienta MCP es parte del control**: dice IN THE CLEAR, dice el tope y
|
|
469
|
+
dice qué NO poner. Es lo único que lee el agente antes de decidir qué expone. Una prueba la fija.
|
|
470
|
+
|
|
471
|
+
### 4.9 Adjuntos cifrados: el almacén de objetos (NX-413, 2-nov-2026)
|
|
472
|
+
|
|
473
|
+
`src/nucleo/objetos.js` + `src/correo/adjuntos.js` + la ruta `/blob/<blobId>` en la estafeta,
|
|
474
|
+
`test/adjuntos.test.js` y `test/politica.test.js`, SPEC §5b. Nace de un síntoma de Nicholas: un
|
|
475
|
+
agente de cowork le manda archivos a un agente de code, el de code acusa recibo del mensaje y los
|
|
476
|
+
archivos no están. El campo `attachments` llevaba meses publicado en la spec y **ninguna casa
|
|
477
|
+
alojaba un byte**: lo único que podía viajar eran rutas de la máquina del remitente.
|
|
478
|
+
|
|
479
|
+
**Qué hace falta para encenderlo en producción** (lo hace Nicholas; la sesión NO lo hizo):
|
|
480
|
+
|
|
481
|
+
```
|
|
482
|
+
npx wrangler r2 bucket create nyx5-blobs
|
|
483
|
+
npx wrangler r2 bucket create nyx5-blobs-b
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
y el binding en `wrangler.toml` y en `wrangler.beta.toml`:
|
|
487
|
+
|
|
488
|
+
```toml
|
|
489
|
+
[[r2_buckets]]
|
|
490
|
+
binding = "BLOBS"
|
|
491
|
+
bucket_name = "nyx5-blobs" # en la beta: nyx5-blobs-b
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**No hay migración de D1**: los metadatos viven en `nyx5_kv` (espacios `adjuntos` y
|
|
495
|
+
`adjuntos-cupo`), que la 0006 ya creó. Mientras no exista el binding, la casa **no aloja adjuntos**
|
|
496
|
+
y `/blob` contesta 503 diciéndolo; la tarjeta del dominio tampoco anuncia cupos. Eso es a propósito:
|
|
497
|
+
anunciar un límite es prometer que hay dónde guardar, y esa promesa sin nada detrás es justamente el
|
|
498
|
+
defecto que este ítem vino a cerrar.
|
|
499
|
+
|
|
500
|
+
**Lo que hay que saber para no romperlo**
|
|
501
|
+
|
|
502
|
+
- **El objeto se guarda cifrado y la casa no tiene la llave.** AES-256-GCM, una llave aleatoria por
|
|
503
|
+
adjunto, y la llave viaja en `attachments[].key` DENTRO del bloque cifrado del sobre. La primera
|
|
504
|
+
prueba de `test/adjuntos.test.js` mete una cadena testigo dentro del archivo y comprueba que no
|
|
505
|
+
aparece en el objeto crudo, ni en los metadatos, ni en el sobre del buzón, ni en el disco de
|
|
506
|
+
ninguna de las dos casas. Se escribió ANTES del código.
|
|
507
|
+
- **La clave del objeto es `blobs/<uuid>`, nunca el nombre del archivo.** Un nombre trae `../` y
|
|
508
|
+
además le cuenta a la casa qué hay dentro. `ObjetosDisco` vuelve a comprobar la forma de la clave:
|
|
509
|
+
un almacén que escribe donde le digan es un traversal esperando a que alguien se confunda.
|
|
510
|
+
- **El array de AFUERA es un vocabulario CERRADO** (`blob`, `bytes`) en un sobre cifrado, y una
|
|
511
|
+
clave de más se rechaza POR NOMBRE. Se aparta del invariante 7 a propósito, como `ALCANCE_MANDATO`:
|
|
512
|
+
ignorar un campo de más no es neutro cuando ese campo es `name: "balance-despido-javier.pdf"`.
|
|
513
|
+
- **AAD = `nyx5:blob:<blobId>:<from>`**: un adjunto no se reusa desde otra dirección ni se hace
|
|
514
|
+
pasar por otro objeto, aunque alguien tenga la llave.
|
|
515
|
+
- **La autenticación va ANTES de mirar si el objeto existe.** Si fuera al revés, `/blob` sería un
|
|
516
|
+
oráculo de existencia para cualquiera sin firma. La prueba compara cuerpo y cabeceras de un blob
|
|
517
|
+
que existe contra uno que no, sin firma, y exige que sean idénticos.
|
|
518
|
+
- **Dos cupos de naturaleza distinta.** El del remitente por día UTC es de FLUJO: se incrementa antes
|
|
519
|
+
de comparar (como `puertaIdeas`) y un intento rechazado por cupo SÍ lo gasta, que es lo que corta
|
|
520
|
+
una inundación. El de la casa es de lo GUARDADO: sube al guardar y BAJA al borrar, y un intento que
|
|
521
|
+
no llega a guardar se devuelve — si no, el 1 GB se erosionaría con cada rechazo hasta bloquear a
|
|
522
|
+
todo el mundo. El tope por adjunto se comprueba antes de tocar cualquier contador: es aritmética,
|
|
523
|
+
no una carrera, y no tiene por qué gastarle el día a nadie.
|
|
524
|
+
- **La retención se aplica al LEER, no sólo cuando el reloj pasa.** 30 días, o 7 desde que el último
|
|
525
|
+
destinatario lo bajó. Si el reloj se cae, un adjunto vencido sigue sin poder bajarse; el reloj
|
|
526
|
+
recupera el espacio, la puerta cumple la promesa.
|
|
527
|
+
- **«Que todos confirmaron» aquí significa que todos lo BAJARON**, porque es lo único que la puerta
|
|
528
|
+
de descarga puede observar. La spec lo dice con esas palabras en vez de prometer otra cosa.
|
|
529
|
+
- **No va por el conector remoto.** Allí la llave del subagente vive en la bóveda, así que la casa
|
|
530
|
+
cifraría en su nombre y el archivo pasaría por ella en claro: el mismo motivo por el que el pedido
|
|
531
|
+
de pago (NX-502) tampoco está en el remoto.
|
|
532
|
+
- **Los adaptadores pasan bytes crudos** (`rx.raw`) sólo para `PUT /blob/<id>`. El tope del
|
|
533
|
+
transporte (16 MB) es mayor que el de la ruta (10 MB) a propósito: así el motivo del rechazo lo da
|
|
534
|
+
la ruta nombrando el límite real, y no el transporte con un «body too large».
|
|
535
|
+
- **Lo que NO cubre, declarado**: un llamador con firma válida distingue un `blob` que existe de uno
|
|
536
|
+
que no (403 contra 404), o sea un oráculo de existencia para un uuid que ya tuvo que ver; la casa
|
|
537
|
+
sabe el tamaño, el remitente y los destinatarios de cada adjunto, porque tiene que servirlos y
|
|
538
|
+
medirlos; el `bytes` del sobre es lo que DECLARA el remitente, y la garantía real es el hash del
|
|
539
|
+
claro que comprueba el receptor; no hay deduplicación entre sobres ni vista previa; y el cupo del
|
|
540
|
+
día se cobra al aceptar, así que dos relojes a la vez pueden pasarse por uno, nunca por mucho.
|
|
541
|
+
|
|
542
|
+
|
|
191
543
|
## 5. Interoperabilidad
|
|
192
544
|
|
|
193
545
|
- **MCP**: `nyx5 mcp --agent keys/x.json` expone el agente como servidor MCP por stdio. Configuración para Claude Desktop en el README. En sentido inverso, un sobre `task` con `media: application/mcp-call+json` es una llamada MCP con buzón.
|