@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.
Files changed (47) hide show
  1. package/README.md +15 -3
  2. package/bin/nyx5.js +72 -0
  3. package/demo/x402-pagar.mjs +64 -0
  4. package/docs/ARQUITECTURA.md +352 -0
  5. package/docs/SPEC.md +337 -6
  6. package/examples/ci/nyx5-ejecutor.yml +82 -0
  7. package/examples/frameworks/README.md +55 -0
  8. package/examples/frameworks/__pycache__/crewai_nyx5.cpython-314.pyc +0 -0
  9. package/examples/frameworks/__pycache__/langgraph_nyx5.cpython-314.pyc +0 -0
  10. package/examples/frameworks/__pycache__/nyx5_http.cpython-314.pyc +0 -0
  11. package/examples/frameworks/claude-agent-sdk.mjs +48 -0
  12. package/examples/frameworks/crewai_nyx5.py +72 -0
  13. package/examples/frameworks/http.md +117 -0
  14. package/examples/frameworks/langgraph_nyx5.py +90 -0
  15. package/examples/frameworks/nyx5_http.py +255 -0
  16. package/examples/frameworks/openai-agents.mjs +41 -0
  17. package/examples/hola-mundo.mjs +4 -4
  18. package/migrations/0009_contratos_estado.sql +4 -0
  19. package/package.json +4 -2
  20. package/server.json +2 -2
  21. package/src/correo/acciones.js +216 -0
  22. package/src/correo/adjuntos.js +185 -0
  23. package/src/correo/agente.js +247 -13
  24. package/src/correo/asistente.js +502 -77
  25. package/src/correo/ci.js +262 -0
  26. package/src/correo/cobro.js +164 -0
  27. package/src/correo/estafeta.js +558 -59
  28. package/src/correo/ideas.js +154 -0
  29. package/src/correo/politica.js +92 -3
  30. package/src/libro/libro.js +6 -2
  31. package/src/libro/verifica.js +239 -36
  32. package/src/nucleo/almacen-d1.js +24 -5
  33. package/src/nucleo/almacen.js +31 -4
  34. package/src/nucleo/crypto.js +36 -0
  35. package/src/nucleo/keccak.js +118 -0
  36. package/src/nucleo/objetos.js +78 -0
  37. package/src/nucleo/secp256k1.js +228 -0
  38. package/src/plataformas/app-html.js +1 -1
  39. package/src/plataformas/home-html.js +1 -1
  40. package/src/plataformas/node.js +18 -1
  41. package/src/plataformas/spec-html.js +1 -1
  42. package/src/plataformas/terms-html.js +2 -0
  43. package/src/plataformas/worker.js +21 -1
  44. package/src/puentes/ejecutor.js +118 -0
  45. package/src/puentes/herramientas.js +137 -12
  46. package/src/puentes/x402-pagador.js +248 -0
  47. 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: three deterministic tests and nothing else.
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}`);
@@ -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.