@nyx5/nyx5 0.6.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 +50 -7
- package/bin/nyx5.js +81 -2
- package/demo/edge-local.mjs +41 -0
- package/demo/x402-pagar.mjs +64 -0
- package/demo/x402-testnet.mjs +80 -0
- package/docs/ARQUITECTURA.md +354 -0
- package/docs/SPEC.md +414 -15
- 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/0006_puente_remoto.sql +19 -0
- package/migrations/0007_indice_reputacion.sql +31 -0
- package/migrations/0008_notaria.sql +18 -0
- 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 +370 -22
- package/src/correo/asistente.js +599 -0
- package/src/correo/ci.js +262 -0
- package/src/correo/cobro.js +164 -0
- package/src/correo/estafeta.js +1548 -58
- package/src/correo/ideas.js +154 -0
- package/src/correo/indice.js +178 -0
- package/src/correo/politica.js +280 -2
- package/src/correo/resolver.js +35 -11
- package/src/libro/contratos.js +116 -7
- package/src/libro/estado.js +150 -0
- package/src/libro/informe.js +147 -3
- package/src/libro/libro.js +83 -12
- package/src/libro/notaria.js +74 -0
- package/src/libro/verifica.js +239 -36
- package/src/nucleo/almacen-d1.js +143 -14
- package/src/nucleo/almacen.js +137 -13
- package/src/nucleo/boveda.js +37 -0
- package/src/nucleo/crypto.js +55 -12
- 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/iconos.js +6 -0
- package/src/plataformas/node.js +28 -4
- package/src/plataformas/spec-html.js +1 -1
- package/src/plataformas/terms-html.js +2 -0
- package/src/plataformas/worker.js +52 -7
- package/src/puentes/ejecutor.js +118 -0
- package/src/puentes/herramientas.js +315 -0
- package/src/puentes/mcp-remoto.js +71 -0
- package/src/puentes/mcp.js +5 -99
- package/src/puentes/oauth.js +361 -0
- package/src/puentes/x402-pagador.js +248 -0
- package/src/puentes/x402.js +266 -0
- package/src/version.js +4 -0
package/docs/ARQUITECTURA.md
CHANGED
|
@@ -181,11 +181,365 @@ La clave pública sale de `data/sigo.uk/domain.json` (`keys[0].sig`). Sin DNS, e
|
|
|
181
181
|
- **La casa**: `casa@` emite tokens solo vía `topup` con token de administrador. El diario completo solo lo lee la casa; cada agente ve sus asientos. Respalda el diario: los saldos se recalculan desde él.
|
|
182
182
|
- **Retención**: purga `archive/` y `seen/` a los 30 días (configurable); los buzones no confirmados se conservan hasta `expires` o 90 días.
|
|
183
183
|
- **Observabilidad**: `GET /health` expone tamaño de cola y agentes; en producción agrega métricas de entregas/rebotes por dominio y alertas cuando la cola crece.
|
|
184
|
+
- **Instrumentación y embudo (NX-801)**: el diario de eventos (`_evento(name, actor, data)`: nombre, fecha, actor y números; nunca contenido ni datos del humano) lleva, además de `join`, `mandate_created`, `first_quote`, `escrow_*`, `bond_forfeited`, `seed_task_taken` y `verificado`, las tres etapas que faltaban del recorrido de una persona: `open_invite` al abrir `/i/<código>` (actor = quien invitó; `data.source` viene de la invitación o de `?source=`; cuenta visitas, no personas), `claude_connected` al terminar el OAuth del conector (`data.invited_by`, `data.code`), y `first_message` la primera vez que una raíz o su `claude.<raíz>` le escribe a otra dirección (no a sí misma ni a `libro@`); se recuerda en `nyx5_kv` ns `primer_mensaje` y en memoria por instancia, así que cuesta a lo sumo una escritura condicional por raíz, nunca una lectura del diario por sobre. Otros delegados (asistentes) no cuentan.
|
|
185
|
+
`GET /informe` y `/informe.json` (sólo la casa, `Bearer <admin>`, `no-store`) muestran el embudo: seis etapas por fuente (últimos 90 días), por semana ISO (últimas 8) y los recorridos individuales de los últimos 30 días. La fuente de un recorrido es la del `join` o, si no la declaró (la app no la manda), la del enlace abierto que su `claude_connected` enlaza por `code`. `connector_authorized` se lee como alias de `claude_connected` para los recorridos anteriores a NX-801; las etapas 1 y 4 no tenían evento antes y se muestran vacías, no se infieren. Todo lo no atribuible a una raíz conocida se cuenta en `sinRaiz`, y el resultado viaja con su denominador (`eventosLeidos`, `tope`, `truncado`). `/report` sigue público y sin nombres: el embudo nunca va ahí.
|
|
184
186
|
|
|
185
187
|
### 4.5 Rendimiento
|
|
186
188
|
|
|
187
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.
|
|
188
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
|
+
|
|
189
543
|
## 5. Interoperabilidad
|
|
190
544
|
|
|
191
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.
|