@nyx5/nyx5 0.1.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/CONTRIBUTING.md +38 -0
- package/LICENSE +202 -0
- package/README.md +133 -0
- package/SECURITY.md +28 -0
- package/bin/nyx5.js +184 -0
- package/demo/contratos.js +117 -0
- package/demo/e2e.js +57 -0
- package/demo/offline.js +46 -0
- package/demo/piloto-d4.mjs +146 -0
- package/demo/spam.js +65 -0
- package/docs/ARQUITECTURA.md +220 -0
- package/docs/SPEC.md +396 -0
- package/examples/hola-mundo.mjs +24 -0
- package/migrations/0002_chasqui.sql +116 -0
- package/migrations/0003_candado.sql +9 -0
- package/migrations/0004_pins.sql +15 -0
- package/package.json +49 -0
- package/src/correo/agente.js +233 -0
- package/src/correo/estafeta.js +777 -0
- package/src/correo/politica.js +107 -0
- package/src/correo/resolver.js +173 -0
- package/src/libro/contratos.js +247 -0
- package/src/libro/errores.js +4 -0
- package/src/libro/libro.js +250 -0
- package/src/nucleo/almacen-d1.js +223 -0
- package/src/nucleo/almacen.js +203 -0
- package/src/nucleo/crypto.js +141 -0
- package/src/nucleo/d1-local.js +78 -0
- package/src/plataformas/app-html.js +2 -0
- package/src/plataformas/home-html.js +2 -0
- package/src/plataformas/node.js +40 -0
- package/src/plataformas/spec-html.js +3 -0
- package/src/plataformas/worker.js +87 -0
- package/src/puentes/email.js +136 -0
- package/src/puentes/mcp.js +105 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Nyx5/1 — Arquitectura de operación
|
|
2
|
+
|
|
3
|
+
Un sistema, dos componentes en el mismo proceso: **Correo** (`src/correo`) y **Libro** (`src/libro`). La Estafeta de cada dominio corre ambos sobre el mismo almacén y la misma identidad.
|
|
4
|
+
|
|
5
|
+
## 1. Componentes
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
DNS (_chsq.uk TXT) DNS (_nyx5.beta.example TXT)
|
|
9
|
+
| |
|
|
10
|
+
+--------------------v---------------+ HTTPS +---------v--------------------+
|
|
11
|
+
| Estafeta sigo.uk |<--------->| Estafeta beta.example |
|
|
12
|
+
| CORREO | | (mismo software) |
|
|
13
|
+
| - tarjeta de dominio (clave Ed25519) | |
|
|
14
|
+
| - registro (admin|invite|open), directorio | /inbound -> verificación |
|
|
15
|
+
| - certificación/delegación de tarjetas | |
|
|
16
|
+
| - /outbound -> cola -> trabajador | | -> política -> buzón|
|
|
17
|
+
| - /inbound -> verificación -> buzones | -> libro@ -> Libro |
|
|
18
|
+
| - /mailbox, /outbox, webhooks | | |
|
|
19
|
+
| LIBRO (casa sigo.uk) | | LIBRO (casa beta.example) |
|
|
20
|
+
| - libro@ recibe ops, emite recibos| | |
|
|
21
|
+
| - diario, contratos, mandatos | | |
|
|
22
|
+
| - estampillas al aceptar sobres | | |
|
|
23
|
+
+----^----------------------^--------+ +-----------^------------------+
|
|
24
|
+
| auth firmada | webhook push |
|
|
25
|
+
+----+------+ +------+-------+ +------+--------+
|
|
26
|
+
| nicolas@ | | asistente@ | | otro agente |
|
|
27
|
+
| (cliente, | | (proceso, | | |
|
|
28
|
+
| claves) | | claves) | | |
|
|
29
|
+
+-----------+ +--------------+ +---------------+
|
|
30
|
+
| |
|
|
31
|
+
Claude Desktop / Claude Code / Cursor <-- puente MCP (stdio) --> nyx5_send / nyx5_inbox / ...
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Tres piezas de software, una sola base de código:
|
|
35
|
+
|
|
36
|
+
| Pieza | Archivo | Corre en |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| Estafeta (Correo + Libro) | `src/correo/estafeta.js` + `src/libro/libro.js` | un proceso por dominio |
|
|
39
|
+
| Cliente de agente (correo + billetera) | `src/correo/agente.js` | dentro del agente (o via CLI) |
|
|
40
|
+
| Puente MCP | `src/puentes/mcp.js` | junto al cliente MCP (stdio) |
|
|
41
|
+
|
|
42
|
+
Módulos de soporte: `nucleo/crypto.js` (firma, cifrado, pow), `nucleo/almacen.js` (persistencia de ambos componentes), `correo/resolver.js` (DNS/well-known/override, cadena de confianza y de delegación, caché), `correo/politica.js` (validación y políticas de buzón), `libro/contratos.js` (máquinas de estado de los contratos).
|
|
43
|
+
|
|
44
|
+
## 2. Flujo de un sobre
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
nicolas.send() asistente.inbox() / webhook
|
|
48
|
+
| 1. resuelve tarjeta destino (enc, política) ^
|
|
49
|
+
| 2. cifra, calcula pow si hace falta, firma | 8. buzón (hasta ack)
|
|
50
|
+
v |
|
|
51
|
+
POST /outbound (auth firmada) 7. política del agente
|
|
52
|
+
| 3. valida + verifica firma del agente ^
|
|
53
|
+
| 4. encola por dominio destino, responde 202 |
|
|
54
|
+
v |
|
|
55
|
+
trabajador (cada 1 s) 6. verifica cadena DNS -> dominio -> agente -> sobre
|
|
56
|
+
| 5. resuelve estafeta destino, POST /inbound ^
|
|
57
|
+
| header X-Nyx5-Relay firmado por el dominio |
|
|
58
|
+
+-----------------------------------------------------+
|
|
59
|
+
202 -> entregado | 4xx -> rebote | 5xx/red -> backoff y reintento
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 2b. Flujo de una operación del Libro
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
verifica.quote() -> sobre cifrado a nicolas (documento firmado por verifica)
|
|
66
|
+
nicolas.accept() -> sobre firmado, sin cifrar, a libro@sigo.uk { op: accept, quote }
|
|
67
|
+
/inbound: verifica firma de nicolas -> política de libro@ -> Libro.handle()
|
|
68
|
+
Libro: verifica firma de la cotización (resolver) -> contrato -> asiento (firmado por la casa)
|
|
69
|
+
estafeta: recibo de libro@ a nicolas y a verifica (buzón local o cola si son de otra casa)
|
|
70
|
+
nicolas.awaitReceipt() -> { contract, asiento, cotizacion_sha256, op_sha256 }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Si la operación falla (saldo, parte, estado), `/inbound` responde 4xx y el remitente recibe un rebote del postmaster con la razón. Nada queda a medias: o hay asiento o no lo hay.
|
|
74
|
+
|
|
75
|
+
## 3. Producción REAL (desplegado)
|
|
76
|
+
|
|
77
|
+
| Pieza | Dónde | Detalle |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| Estafeta principal | https://chsq.uk | Worker `chasqui`, D1 `token-wallet` (ad5a85b0…), registro `invite`, welcome 20.000, fee 20%, índice federado ACTIVO |
|
|
80
|
+
| Segunda casa | https://b.chsq.uk | Worker `chasqui-beta`, D1 `chasqui-beta` (700df3d5…), registro `invite`, welcome 5.000 |
|
|
81
|
+
| Cron | cada 1 min en ambas | reintentos de cola + rastreo del índice |
|
|
82
|
+
| Secrets | `CHASQUI_ADMIN_TOKEN` en cada worker | valores en `.env` local (gitignored) |
|
|
83
|
+
|
|
84
|
+
Lección de terreno (2026-09-05): un Worker NO puede hacer fetch a `*.workers.dev` (error 1042) —
|
|
85
|
+
la federación exige dominios reales; por eso las casas viven en subdominios de sigo.uk como
|
|
86
|
+
custom domains. La resolución entre casas va por well-known + pin TOFU persistido en D1;
|
|
87
|
+
el ancla DNS (`_nyx5.<casa> TXT`) está pendiente (decisión: tocar la zona sigo.uk).
|
|
88
|
+
|
|
89
|
+
Deploy: `npx wrangler deploy` (principal) / `npx wrangler deploy --config wrangler.beta.toml`.
|
|
90
|
+
Migraciones: `npx wrangler d1 execute <db> --remote --file=migrations/0002_nyx5.sql`.
|
|
91
|
+
E2E contra producción: `node --env-file=.env e2e-produccion.mjs`.
|
|
92
|
+
|
|
93
|
+
## 3b. Local (desarrollo)
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
npm run demo # dos dominios, envío cifrado, respuesta, acuse
|
|
97
|
+
npm run demo:offline # destino apagado, cola, reintento, entrega
|
|
98
|
+
npm run demo:spam # firmas falsas, allowlist, proof-of-work, duplicados
|
|
99
|
+
npm run demo:contratos # libro: spot, escrow, fianza, mandato en cadena, delegación, estampilla
|
|
100
|
+
npm test # 26 pruebas automatizadas
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Con la CLI puedes correr cada pieza como proceso separado (ver README). Los datos quedan en archivos JSON inspeccionables:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
data/<dominio>/
|
|
107
|
+
domain.json clave privada del dominio (protégelo)
|
|
108
|
+
agents/<local>.json tarjeta certificada + webhook
|
|
109
|
+
invitations/ códigos de invitación, usos y vencimiento
|
|
110
|
+
queue/<job>.json sobres esperando entrega
|
|
111
|
+
outbox/<local>/ estado de cada envío
|
|
112
|
+
mailbox/<local>/ sobres pendientes de ack
|
|
113
|
+
archive/<local>/ sobres confirmados
|
|
114
|
+
seen/<id>.json deduplicación
|
|
115
|
+
pins.json claves de dominios ajenos (TOFU)
|
|
116
|
+
libro/saldos.json { seq, balances } (derivado del diario)
|
|
117
|
+
libro/diario/ un asiento por archivo, firmado por la casa
|
|
118
|
+
libro/contratos/ estado e historial de cada contrato
|
|
119
|
+
libro/mandatos/ mandatos y su cadena
|
|
120
|
+
libro/ops/ resultado de cada operación (idempotencia)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## 4. Producción
|
|
124
|
+
|
|
125
|
+
### 4.1 Dónde corre la estafeta
|
|
126
|
+
|
|
127
|
+
Es un servidor HTTP sin estado en memoria salvo cachés; todo lo importante está en el almacenamiento. Opciones, de más simple a más escalable:
|
|
128
|
+
|
|
129
|
+
1. **Un VPS o Fly.io** con Node y disco persistente. Basta para cientos de agentes. Usa el `FileStore` tal cual o Postgres.
|
|
130
|
+
2. **Cloudflare Workers + D1/Queues** o **Vercel + Supabase**: reemplaza `FileStore` por una clase con la misma interfaz sobre Postgres, y el `setInterval` del trabajador por un cron/queue consumer.
|
|
131
|
+
3. **Varias réplicas + Postgres**: las réplicas son intercambiables; la cola usa `SELECT ... FOR UPDATE SKIP LOCKED` para que dos trabajadores no tomen el mismo trabajo.
|
|
132
|
+
|
|
133
|
+
TLS obligatorio. La URL pública va en `--public-url` y en la tarjeta del dominio.
|
|
134
|
+
|
|
135
|
+
### 4.2 Esquema de datos (Postgres/Supabase)
|
|
136
|
+
|
|
137
|
+
```sql
|
|
138
|
+
create table domain_keys (kid text primary key, priv_ref text not null, created timestamptz not null, retired timestamptz);
|
|
139
|
+
create table agents (local text primary key, card jsonb not null, webhook text, registered_via text, updated timestamptz not null);
|
|
140
|
+
create table invitations (code text primary key, uses int not null, used int not null default 0, expires timestamptz, note text, welcome bigint, created timestamptz not null);
|
|
141
|
+
create table mailbox (local text, id text, envelope jsonb not null, meta jsonb, received timestamptz not null, acked timestamptz, primary key (local, id));
|
|
142
|
+
create table seen (id text primary key, at timestamptz not null, meta jsonb);
|
|
143
|
+
create table queue (job text primary key, envelope jsonb not null, domain text not null, recipients text[] not null, from_local text not null,
|
|
144
|
+
attempts int default 0, next_attempt timestamptz not null, status text not null, log jsonb default '[]', created timestamptz not null);
|
|
145
|
+
create index on queue (next_attempt) where status in ('queued','retrying');
|
|
146
|
+
create table outbox (from_local text, job text, entry jsonb not null, updated timestamptz not null, primary key (from_local, job));
|
|
147
|
+
create table pins (domain text primary key, kid text not null, pinned timestamptz not null);
|
|
148
|
+
|
|
149
|
+
-- Libro
|
|
150
|
+
create table libro_asientos (n bigint primary key, id text unique not null, at timestamptz not null, concept text not null,
|
|
151
|
+
lines jsonb not null, meta jsonb, refs jsonb, signature jsonb not null);
|
|
152
|
+
create table libro_saldos (account text primary key, balance bigint not null default 0);
|
|
153
|
+
create table libro_contratos (id text primary key, kind text not null, state text not null, seller text, buyer text, verifier text, arbiter text,
|
|
154
|
+
amount bigint not null, quote_id text unique, doc jsonb not null, updated timestamptz not null);
|
|
155
|
+
create table libro_mandatos (id text primary key, grantor text not null, grantee text not null, root text not null, parent text,
|
|
156
|
+
cap bigint not null, spent bigint not null default 0, state text not null, doc jsonb not null);
|
|
157
|
+
create table libro_ops (id text primary key, result jsonb not null, at timestamptz not null);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Tu Token Wallet v0 (D1) ya tiene ledger de doble entrada y cotización→confirmación→cobro: es el backend natural del Libro en Cloudflare. El mapeo es directo: sus cuentas son `agente@dominio`, su tabla de asientos es `libro_asientos`, y su `quote→confirm` es `cotizar→accept`. Lo que cambia es la autenticación: desaparece el login; la firma del sobre lo reemplaza.
|
|
161
|
+
|
|
162
|
+
`priv_ref` apunta a la clave privada en un gestor de secretos (Supabase Vault, Cloudflare Secrets, KMS), nunca a la clave en claro en la tabla.
|
|
163
|
+
|
|
164
|
+
### 4.3 DNS
|
|
165
|
+
|
|
166
|
+
Para `sigo.uk`, en Cloudflare (con DNSSEC activado):
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
_chsq.uk TXT "v=nyx51; url=https://mail.sigo.uk; sig=<clave pública del dominio>"
|
|
170
|
+
mail.sigo.uk A/CNAME -> la estafeta
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
La clave pública sale de `data/sigo.uk/domain.json` (`keys[0].sig`). Sin DNS, el fallback es `https://sigo.uk/.well-known/nyx5.json` (proxy inverso hacia la estafeta).
|
|
174
|
+
|
|
175
|
+
### 4.4 Seguridad operativa
|
|
176
|
+
|
|
177
|
+
- **Clave del dominio**: lo más valioso. Genérala en el servidor, guárdala en un gestor de secretos, respaldo cifrado fuera de línea. Rotación: agrega la nueva a `keys`, publica ambas en DNS y tarjeta por 7 días, retira la vieja.
|
|
178
|
+
- **Claves de agentes**: las guarda cada agente. Para agentes de personas: archivo cifrado con contraseña como mínimo; passkey/llave de hardware como objetivo.
|
|
179
|
+
- **Registro**: para tu operación, `invite` es el modo correcto: emites un código por persona o por proyecto (con su regalo de bienvenida), nadie toca el token de la casa, y cada alta queda con `registered_via`. `open` solo para casas que quieren ser públicas; trae límite de altas por IP y prueba de posesión, pero no impide que alguien tome nombres al voleo. El token de administrador queda para cargar saldo, emitir invitaciones y leer el diario; rota si se filtra.
|
|
180
|
+
- **Límites**: tamaño de sobre (1 MB), tasa por dominio emisor, `require_relay` para rechazar sobres reenviados por terceros.
|
|
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
|
+
- **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
|
+
- **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
|
+
|
|
185
|
+
### 4.5 Rendimiento
|
|
186
|
+
|
|
187
|
+
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
|
+
|
|
189
|
+
## 5. Interoperabilidad
|
|
190
|
+
|
|
191
|
+
- **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.
|
|
192
|
+
- **A2A**: la tarjeta del agente apunta a su Agent Card; un sobre puede transportar una tarea A2A. Nyx5 le da a A2A dirección por persona y buzón; A2A le da a Nyx5 el ciclo de vida de tareas largas.
|
|
193
|
+
- **Email**: una estafeta con la extensión `email` es también servidor SMTP del dominio. Es la puerta de entrada al mundo actual: la misma dirección sirve para humanos y agentes.
|
|
194
|
+
|
|
195
|
+
## 6. Hoja de ruta
|
|
196
|
+
|
|
197
|
+
| Fase | Entrega | Criterio de éxito |
|
|
198
|
+
|---|---|---|
|
|
199
|
+
| 0 (hoy) | Referencia local: Correo + Libro, spec, CLI, puente MCP, 20 pruebas | `npm test` en verde |
|
|
200
|
+
| 1 | Piloto en casa: tus sesiones y agentes con cuenta; cada cajita lleva cotización escrow; Verifica como primer servicio medido; presupuesto mensual por frente como `topup` | telemetría real: cuánto costó cada entrega, qué reportes afianzados cayeron |
|
|
201
|
+
| 2 ✅ | HECHO 2026-09-05: dos estafetas en producción (chsq.uk + b.chsq.uk) sobre D1; falta el ancla DNS TXT | un agente externo escribe, cotiza y paga estampilla desde afuera — verificado E2E |
|
|
202
|
+
| 3 | Puente SMTP (extensión `email`), retención, métricas; carga de saldo con dinero real (Paddle) y payout a terceros | humanos y agentes en la misma dirección; primer tercero que acepta tokens |
|
|
203
|
+
| 4 | Contratos compuestos según demanda (bounty, suscripción, RFQ, disputa); reputación como consulta pública sobre contratos | spam y palabras gratis económicamente inviables |
|
|
204
|
+
| 5 | Custodia de claves con passkeys, extensión `person`; spec como borrador abierto y segunda implementación | personas comunes con agente propio; dos implementaciones interoperando |
|
|
205
|
+
|
|
206
|
+
## 7. Decisiones y sus renuncias
|
|
207
|
+
|
|
208
|
+
| Decisión | Por qué | Renuncia |
|
|
209
|
+
|---|---|---|
|
|
210
|
+
| Direcciones `local@dominio` y DNS como ancla | Reutiliza la única infraestructura de nombres universal que existe; los dominios ya tienen dueño, abuso y reputación conocidos | Dependes del DNS y de tener un dominio (como el email) |
|
|
211
|
+
| Ed25519 / X25519 | Rápidos, claves de 32 bytes, sin parámetros que elegir mal, nativos en Node | Sin post-cuántico todavía; `alg` explícito permite migrar |
|
|
212
|
+
| JSON canónico en vez de binario | Legible, depurable, compatible con todo | ~30 % más grande que CBOR |
|
|
213
|
+
| Store-and-forward con cola en cada dominio | Es lo que hace al email resiliente; un agente apagado no pierde nada | Latencia de segundos, no milisegundos; para tiempo real usa A2A directo |
|
|
214
|
+
| Cifrado extremo a extremo por defecto | Las estafetas son terceros; no deben leer | Búsqueda y filtrado en el servidor son imposibles; los hace el agente |
|
|
215
|
+
| Proof-of-work y allowlist antes que pagos | Funciona sin infraestructura financiera | El pow es solo fricción; el spam a gran escala requiere estampillas pagadas (fase 3) |
|
|
216
|
+
| Sin blockchain | El DNS ya resuelve nombres y la firma ya resuelve autenticidad; un ledger global agrega costo sin resolver el buzón ni el spam | Sin registro global inmutable de identidades |
|
|
217
|
+
| Un Libro por casa, no federado | Un ledger central con una casa responsable es lo que hace ejecutable el escrow y la fianza; federar libros es construir un sistema de compensación | Los tokens no cruzan casas; un foráneo abre cuenta en la tuya |
|
|
218
|
+
| El Libro sin login | Reusar la firma del sobre elimina onboarding y una segunda identidad; cualquier agente verificable ya es cliente | El Libro depende del Correo; no puede correr solo |
|
|
219
|
+
| Cotización como documento firmado, no como sobre | Puede viajar cifrada y presentarse después a la casa sin revelar el sobre | Dos firmas que verificar por aceptación |
|
|
220
|
+
| Federado, no centralizado | Nadie es dueño de la red; cualquiera monta una estafeta | La adopción depende de que muchos la monten |
|
package/docs/SPEC.md
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# Nyx5/1 — Correo y Libro para agentes
|
|
2
|
+
|
|
3
|
+
Estado: borrador ejecutable v0.3 (septiembre 2026)
|
|
4
|
+
Implementación de referencia: este repositorio (Node 20+, sin dependencias)
|
|
5
|
+
|
|
6
|
+
## 0. Qué es
|
|
7
|
+
|
|
8
|
+
Un solo sistema con dos componentes que comparten identidad, transporte y almacenamiento:
|
|
9
|
+
|
|
10
|
+
- **Correo** (secciones 1 a 13): direcciones `agente@dominio`, tarjetas certificadas, sobres firmados y cifrados, buzón que guarda aunque el agente esté apagado. Lo que el email le dio a las personas.
|
|
11
|
+
- **Libro** (secciones 14 a 20): el ledger de doble entrada de cada casa, cotizaciones, contratos, mandatos en cadena y estampillas. Lo que el email nunca tuvo: consecuencia económica y un recibo que ninguna parte puede negar.
|
|
12
|
+
|
|
13
|
+
No son dos protocolos compatibles. El Libro no tiene login ni API propia: se opera escribiéndole sobres a `libro@<casa>`, y la cadena de confianza del Correo es su autenticación. Sus respuestas son recibos firmados por la casa que llegan al buzón como cualquier carta.
|
|
14
|
+
|
|
15
|
+
El email logró algo que ningún protocolo de agentes tiene hoy: una dirección universal, un buzón, y una red donde cualquier servidor le escribe a cualquier otro sin pedir permiso. MCP conecta un agente con sus herramientas; A2A conecta agentes que ya se conocen y están en línea. Ninguno da identidad por persona, buzón, confianza verificable entre desconocidos, ni una forma de que un acuerdo tenga peso.
|
|
16
|
+
|
|
17
|
+
Nyx5/1 cierra esos huecos así:
|
|
18
|
+
|
|
19
|
+
| Hueco | Cómo lo cierra Nyx5 |
|
|
20
|
+
|---|---|
|
|
21
|
+
| Identidad por persona, no solo por dominio | Dirección `agente@dominio`. El dominio certifica la clave pública de cada agente. La persona es dueña de su clave; el dominio solo la avala. |
|
|
22
|
+
| Buzón (store-and-forward) | Cada dominio tiene una estafeta que acepta, guarda y reintenta. El agente puede estar apagado días; nada se pierde. |
|
|
23
|
+
| Confianza y anti-spam | Todo sobre viene firmado por el agente y avalado por su dominio (ancla en DNS). Sin firma verificable no hay entrega. El receptor decide su política: abierto, lista blanca, o estampilla (proof-of-work / pago). |
|
|
24
|
+
| Fragmentación | Nyx5 no reemplaza a MCP ni a A2A: es el sobre universal. El contenido puede ser texto, JSON, una tarea A2A o una llamada MCP; la tarjeta del agente publica sus endpoints MCP/A2A. |
|
|
25
|
+
| Palabras gratis | Un acuerdo es un asiento en el Libro, no prosa. Escrow retiene hasta que la prueba pasa; la fianza le pone precio a afirmar; el mandato acota cuánto puede gastar cada agente y quién paga al final. |
|
|
26
|
+
| Recibo negable | Todo recibo lleva el hash del sobre que lo causó y la firma de quien lo emite. |
|
|
27
|
+
|
|
28
|
+
Y cifrado extremo a extremo por defecto. Las estafetas ven `de`, `para` y el tamaño; nunca el contenido.
|
|
29
|
+
|
|
30
|
+
## 1. Términos
|
|
31
|
+
|
|
32
|
+
- **Agente**: cualquier proceso (o persona operando un cliente) con un par de claves y una dirección.
|
|
33
|
+
- **Dirección**: `local@dominio`. Minúsculas, `local` = `[a-z0-9][a-z0-9._-]{0,63}`.
|
|
34
|
+
- **Estafeta**: el servidor de un dominio. Publica tarjetas, certifica agentes, recibe, guarda y entrega. Equivale al servidor MX del correo.
|
|
35
|
+
- **Tarjeta de dominio**: JSON autofirmado en `/.well-known/nyx5.json`. Declara claves, URL de la estafeta, política y extensiones.
|
|
36
|
+
- **Tarjeta de agente**: JSON con las claves y capacidades de un agente, firmado (certificado) por la clave del dominio.
|
|
37
|
+
- **Sobre**: la unidad de envío. JSON firmado, opcionalmente cifrado.
|
|
38
|
+
- **Resolver**: la lógica que va de una dirección a una tarjeta verificada.
|
|
39
|
+
|
|
40
|
+
## 2. Descubrimiento y ancla de confianza
|
|
41
|
+
|
|
42
|
+
Dada `asistente@sigo.uk`, el resolver localiza la estafeta de `sigo.uk` en este orden:
|
|
43
|
+
|
|
44
|
+
1. **Override local** (`hosts.json`): pruebas y redes privadas. Puede fijar la clave esperada (`sig`).
|
|
45
|
+
2. **DNS**: registro TXT en `_nyx5.sigo.uk`:
|
|
46
|
+
```
|
|
47
|
+
v=nyx51; url=https://mail.sigo.uk; sig=<clave pública Ed25519 del dominio, base64url>
|
|
48
|
+
```
|
|
49
|
+
`sig` es el ancla: la tarjeta del dominio debe estar firmada por esa clave. Con DNSSEC, la cadena queda completa.
|
|
50
|
+
3. **Well-known sin DNS**: `https://sigo.uk/.well-known/nyx5.json`. Si no hay ancla, el resolver aplica TOFU (confía en el primer uso y pinea la clave; un cambio posterior se rechaza hasta que el operador lo confirme).
|
|
51
|
+
|
|
52
|
+
Luego descarga la tarjeta del dominio y la del agente, y verifica la cadena: **DNS → clave del dominio → tarjeta del agente → firma del sobre**.
|
|
53
|
+
|
|
54
|
+
Las tarjetas se cachean (5 min por defecto). Si un sobre llega firmado con una clave que la tarjeta cacheada no reconoce, el receptor refresca la tarjeta una vez antes de rechazar (esto es lo que hace funcionar la rotación de claves sin coordinación).
|
|
55
|
+
|
|
56
|
+
## 3. Tarjeta de dominio
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"nyx5": "1",
|
|
61
|
+
"domain": "sigo.uk",
|
|
62
|
+
"estafeta": "https://mail.sigo.uk",
|
|
63
|
+
"keys": [ { "sig": "<Ed25519 pub>", "created": "2026-09-04T00:00:00Z" } ],
|
|
64
|
+
"policy": { "inbound": "verified", "max_bytes": 1048576 },
|
|
65
|
+
"extensions": ["urn:nyx5:ext:mcp", "urn:nyx5:ext:a2a"],
|
|
66
|
+
"issued": "2026-09-04T19:00:00Z",
|
|
67
|
+
"signature": { "alg": "Ed25519", "kid": "<Ed25519 pub>", "value": "<base64url>" }
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Reglas:
|
|
72
|
+
- `signature.kid` debe estar en `keys`. La firma cubre el JSON canónico sin `signature`.
|
|
73
|
+
- `keys` puede listar varias durante una rotación; la primera es la activa.
|
|
74
|
+
- `policy.inbound` hoy solo admite `verified` (sin firma no hay entrega). Se reserva para futuros modos.
|
|
75
|
+
- `policy.outbound: "sealed"` (opcional) declara que todo lo que sale del dominio va cifrado; los receptores pueden rechazar sobres en claro de ese dominio.
|
|
76
|
+
|
|
77
|
+
## 4. Tarjeta de agente
|
|
78
|
+
|
|
79
|
+
`GET https://<estafeta>/agents/<local>`
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"nyx5": "1",
|
|
84
|
+
"address": "asistente@sigo.uk",
|
|
85
|
+
"sig": "<Ed25519 pub del agente>",
|
|
86
|
+
"enc": "<X25519 pub del agente>",
|
|
87
|
+
"capabilities": {
|
|
88
|
+
"accepts": ["text/plain", "application/json", "application/a2a-task+json"],
|
|
89
|
+
"mcp": "https://agents.sigo.uk/asistente/mcp",
|
|
90
|
+
"a2a": "https://agents.sigo.uk/asistente/.well-known/agent-card.json"
|
|
91
|
+
},
|
|
92
|
+
"inbox": { "policy": "open" },
|
|
93
|
+
"valid_from": "2026-09-04T19:00:00Z",
|
|
94
|
+
"valid_until": null,
|
|
95
|
+
"previous": [ { "sig": "<clave anterior>", "until": "2026-09-11T19:00:00Z" } ],
|
|
96
|
+
"certification": { "alg": "Ed25519", "kid": "<clave del dominio>", "value": "<base64url>" }
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Tarjeta delegada (un subagente que actúa por otro agente): el nombre es `<nombre>.<padre>`, y la tarjeta trae además
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
"delegation": {
|
|
104
|
+
"by": "constructor@sigo.uk", "address": "tester.constructor@sigo.uk", "sig": "<clave del subagente>",
|
|
105
|
+
"scope": { "types": ["message", "result"], "to_domains": ["sigo.uk"], "cap": 100 },
|
|
106
|
+
"valid_until": null, "issued": "...", "signature": { "alg": "Ed25519", "kid": "<clave del padre>", "value": "..." }
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Reglas:
|
|
111
|
+
- `certification` la firma el dominio, no el agente. Cubre todo menos `certification`.
|
|
112
|
+
- `delegation` la firma el padre. El dominio certifica la tarjeta igual; el resolver verifica ambas firmas (cadena dominio → padre → hijo). Un hijo no puede tener más `cap` que su padre. La estafeta hace cumplir `scope.types` y `scope.to_domains` al enviar; el Libro hace cumplir `scope.cap` al aceptar, afianzar, mandar y cobrar.
|
|
113
|
+
- La clave privada `sig` la genera y guarda el agente; el dominio nunca la ve. Por eso la identidad es de la persona: si te vas de un dominio, te llevas tu clave y la certificas en otro.
|
|
114
|
+
- `enc` es opcional. Sin `enc`, los remitentes envían en claro (o rechazan si exigen cifrado).
|
|
115
|
+
- `previous`: claves anteriores con fecha de gracia. Una firma con clave anterior vigente es válida.
|
|
116
|
+
- `inbox.policy`: ver sección 9.
|
|
117
|
+
|
|
118
|
+
## 5. El sobre
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"nyx5": "1",
|
|
123
|
+
"id": "uuid",
|
|
124
|
+
"from": "nicolas@sigo.uk",
|
|
125
|
+
"to": ["asistente@beta.example"],
|
|
126
|
+
"created": "ISO-8601",
|
|
127
|
+
"expires": null,
|
|
128
|
+
"deliver_after": null,
|
|
129
|
+
"thread": "uuid o null",
|
|
130
|
+
"in_reply_to": "id o null",
|
|
131
|
+
"type": "message | task | result | receipt | intro",
|
|
132
|
+
|
|
133
|
+
"content": { "media": "application/json", "body": { "...": "..." } },
|
|
134
|
+
"encrypted": { "alg": "X25519+HKDF-SHA256+A256GCM", "epk": "...", "iv": "...", "ct": "...", "tag": "...", "keys": { "asistente@beta.example": { "iv": "...", "ct": "...", "tag": "..." } } },
|
|
135
|
+
|
|
136
|
+
"attachments": [ { "name": "informe.pdf", "media": "application/pdf", "sha256": "...", "url": "https://...", "bytes": 12345 } ],
|
|
137
|
+
"pow": { "bits": 16, "nonce": "12345" },
|
|
138
|
+
"receipt": "delivered",
|
|
139
|
+
"extensions": { "urn:nyx5:ext:a2a": { "task_id": "..." } },
|
|
140
|
+
"signature": { "alg": "Ed25519", "kid": "<sig del agente>", "value": "<base64url>" }
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Reglas:
|
|
145
|
+
- `content` y `encrypted` son excluyentes. `content.media` sigue el modelo MIME; `body` es texto o JSON.
|
|
146
|
+
- La **firma** cubre todo el sobre canónico menos `signature`. Se firma después de cifrar: cualquier estafeta verifica autenticidad sin poder leer el contenido.
|
|
147
|
+
- El **cifrado** es JWE-like: una clave de contenido aleatoria cifra `content` con AES-256-GCM; esa clave se envuelve para cada destinatario con X25519 efímero + HKDF. El AAD es el canónico de `{id, from, to}`: un sobre no puede ser re-dirigido ni re-firmado por otro sin romper el descifrado.
|
|
148
|
+
- **Detalle normativo del KDF** (toda implementación debe copiarlo byte a byte o nada interopera): la KEK de cada destinatario es `HKDF-SHA256(ikm = X25519(epk_priv, enc_dest), salt = los bytes UTF-8 del STRING base64url de epk — no la clave decodificada —, info = "nyx5/1 cek-wrap", 32)`. Y el canónico ordena claves, omite en objetos los valores `undefined`, y serializa como JSON compacto.
|
|
149
|
+
- **Adjuntos** viajan por referencia (URL + hash), no incrustados. La estafeta no almacena binarios. El hash hace verificable la descarga.
|
|
150
|
+
- `type` es un hint semántico. `task`/`result` para trabajo delegado; `receipt` para acuses; `intro` para presentarse ante buzones con lista blanca (máximo 4 KB); `message` para todo lo demás.
|
|
151
|
+
- `thread` e `in_reply_to` dan hilos sin estado en el servidor.
|
|
152
|
+
- `expires`: pasado ese instante, ninguna estafeta lo entrega ni reintenta.
|
|
153
|
+
- `deliver_after` (opcional, ISO-8601): **entrega diferida.** El sobre espera en la cola de la estafeta emisora hasta ese instante y recién entonces se intenta entregar. Antes de la fecha no aparece en ningún buzón. Un `deliver_after` en el pasado se entrega de inmediato (nunca es error). Si `expires ≤ deliver_after` el sobre se rechaza al enviar (400): vencería antes de poder entregarse. Es el mecanismo de los recordatorios de un agente a sí mismo (memoria entre sesiones) y de los avisos de plazo que programa el Libro.
|
|
154
|
+
- Los campos desconocidos se conservan y se firman, pero se ignoran. Así se agregan capacidades sin romper implementaciones viejas.
|
|
155
|
+
|
|
156
|
+
Tamaño máximo por defecto: 1 MB. Lo declara cada dominio en su tarjeta.
|
|
157
|
+
|
|
158
|
+
## 6. Transporte entre estafetas
|
|
159
|
+
|
|
160
|
+
`POST https://<estafeta destino>/inbound` con el sobre como cuerpo JSON y este header:
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
X-Nyx5-Relay: nyx51 domain=<dominio emisor>; kid=<clave del dominio>; sig=<firma de "relay:<id>:<dominio destino>">
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
La firma del agente autentica al remitente (como DKIM). La firma de relay autentica a la estafeta emisora (como SPF). Un dominio puede exigir ambas (`require_relay`).
|
|
167
|
+
|
|
168
|
+
Respuesta:
|
|
169
|
+
```json
|
|
170
|
+
{ "ok": true, "code": 202, "accepted": ["asistente@beta.example"], "rejected": [ { "to": "...", "code": 403, "reason": "..." } ] }
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Semántica de códigos (por sobre o por destinatario):
|
|
174
|
+
|
|
175
|
+
| Código | Significado | Emisor |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| 200 | duplicado ya recibido (idempotente) | marca entregado |
|
|
178
|
+
| 202 | aceptado en buzón | marca entregado |
|
|
179
|
+
| 400 | sobre malformado | rebote inmediato |
|
|
180
|
+
| 402 | falta estampilla (proof-of-work) | rebote; el cliente puede reintentar con pow |
|
|
181
|
+
| 403 | firma inválida, remitente no verificable, política | rebote |
|
|
182
|
+
| 404 | destinatario inexistente | rebote |
|
|
183
|
+
| 410 | vencido | rebote |
|
|
184
|
+
| 413 | demasiado grande | rebote |
|
|
185
|
+
| 421, 429, 5xx, red caída | temporal | reintento con backoff exponencial |
|
|
186
|
+
|
|
187
|
+
## 7. Buzón y entrega (store-and-forward)
|
|
188
|
+
|
|
189
|
+
1. El agente entrega su sobre firmado a su propia estafeta (`POST /outbound`).
|
|
190
|
+
2. La estafeta lo encola por dominio destino y responde 202 de inmediato. Con `deliver_after`, el primer intento se agenda para esa fecha (el mismo `next_attempt` de la cola): el sobre espera ahí, sin aparecer en ningún buzón, hasta que llegue el momento.
|
|
191
|
+
3. Un trabajador intenta la entrega. Si falla temporalmente, reintenta con backoff exponencial (1 s, 2 s, 4 s… hasta 60 s) durante hasta 3 días. Luego rebota. Si un sobre vence (`expires`) mientras espera en la cola —por diferimiento o por reintentos a un destino caído—, **rebota al remitente** con la razón; no desaparece mudo.
|
|
192
|
+
4. La estafeta receptora verifica, aplica política y guarda el sobre en el buzón del destinatario.
|
|
193
|
+
5. El destinatario lee por poll (`GET /mailbox/<local>`) o recibe push (webhook firmado por el dominio). El sobre permanece hasta que el agente confirma (`POST /mailbox/<local>/ack`). Un agente apagado una semana recibe todo al volver.
|
|
194
|
+
6. Rebotes y acuses son sobres normales de `postmaster@<dominio>`, firmados con la clave del dominio, con `type: receipt`, `in_reply_to` al sobre original y `sha256` del sobre original. Acuse de entrega solo si el sobre pide `"receipt": "delivered"`. Los recibos que emite un agente (`processed`, etc.) también llevan el `sha256` del sobre: son no repudiables sin ningún registro central.
|
|
195
|
+
7. Idempotencia por `id`: una segunda entrega del mismo sobre devuelve 200 y no duplica.
|
|
196
|
+
|
|
197
|
+
## 8. API agente ↔ estafeta
|
|
198
|
+
|
|
199
|
+
Autenticación: `Authorization: Nyx5 <token>.<firma>` donde `token` = base64url del canónico de `{address, ts, nonce, method, path, host}` y `firma` = Ed25519 con la clave del agente. Ventana de 5 minutos, nonce de un solo uso, atado a método, ruta y **casa destino** (`host`): un token capturado no sirve contra otra estafeta.
|
|
200
|
+
|
|
201
|
+
| Método | Ruta | Quién | Para |
|
|
202
|
+
|---|---|---|---|
|
|
203
|
+
| GET | `/.well-known/nyx5.json` | público | tarjeta del dominio |
|
|
204
|
+
| GET | `/agents` | público | directorio de la casa (`?capability=mcp&accepts=<media>&q=<texto>&limit&offset`) |
|
|
205
|
+
| GET | `/agents/:local` | público | tarjeta del agente |
|
|
206
|
+
| POST | `/agents` | ver sección 8b | registrar/actualizar agente |
|
|
207
|
+
| POST | `/invitations` | admin | emitir código de invitación `{ uses, expires, note, welcome }` |
|
|
208
|
+
| GET | `/invitations` | admin | listar invitaciones y su uso |
|
|
209
|
+
| POST | `/outbound` | agente | enviar |
|
|
210
|
+
| POST | `/inbound` | estafetas | recibir |
|
|
211
|
+
| GET | `/mailbox/:local` | agente | leer pendientes |
|
|
212
|
+
| POST | `/mailbox/:local/ack` | agente | confirmar procesados |
|
|
213
|
+
| GET | `/outbox/:local` | agente | estado de envíos |
|
|
214
|
+
| GET | `/health` | público | salud |
|
|
215
|
+
|
|
216
|
+
## 8b. Servicio de registro
|
|
217
|
+
|
|
218
|
+
Cómo entra un agente a una casa lo decide la tarjeta del dominio (`policy.registration`):
|
|
219
|
+
|
|
220
|
+
| Modo | Quién inscribe | Cómo |
|
|
221
|
+
|---|---|---|
|
|
222
|
+
| `admin` (por defecto) | solo la casa | `POST /agents` con `Authorization: Bearer <token de la casa>` |
|
|
223
|
+
| `invite` | quien tenga un código | la casa emite códigos con usos y vencimiento; el agente lo presenta en `invite` |
|
|
224
|
+
| `open` | cualquiera | primer llegado, primer servido; límite de altas por minuto |
|
|
225
|
+
|
|
226
|
+
En `invite` y `open` el cuerpo va **firmado con la misma clave que se inscribe** (`signature.kid == sig`, con `ts` dentro de 5 minutos): prueba de posesión. Nadie puede registrar una clave que no controla. Un nombre ya tomado solo lo actualiza su dueño (autenticación firmada, incluso al rotar claves: el cuerpo lleva las nuevas, la autenticación se firma con las viejas) o la casa. Nombres reservados: `postmaster`, `libro`, `casa`, `admin`, `root`, `abuse`, `security`, `hostmaster`, `noreply`, `support`, `estafeta`, `nyx5`. Los subagentes se inscriben con la firma del padre (sección 4).
|
|
227
|
+
|
|
228
|
+
El **directorio** (`GET /agents`) es la lista pública de las tarjetas de la casa que **pidieron figurar** (`capabilities.listed: true`): claves, capacidades, política de buzón, si es delegado y por quién. Sin webhooks ni datos privados. El default es no aparecer: un agente no figura en el directorio ni en ningún índice sin haberlo pedido. El lookup directo por dirección (`GET /agents/<local>`) resuelve a cualquier agente que ya conoces, listado o no. Sirve para encontrar quién ofrece qué dentro de una casa; entre casas, el descubrimiento sigue siendo por dirección (sección 2): no hay un registro global, y ese hueco está declarado en la sección 21.
|
|
229
|
+
|
|
230
|
+
Cada alta es un evento registrado (`registered_via`: admin, self, delegation, open, invite:<código>) y, si la casa da regalo de bienvenida, un asiento en el Libro.
|
|
231
|
+
|
|
232
|
+
## 9. Políticas de entrada
|
|
233
|
+
|
|
234
|
+
La estafeta receptora rechaza sin excepción sobres sin firma verificable. Sobre eso, cada agente elige:
|
|
235
|
+
|
|
236
|
+
- `open`: acepta cualquier remitente verificado. Límite de tasa por dominio emisor (120/min por defecto).
|
|
237
|
+
- `allowlist`: solo direcciones o dominios listados. Un desconocido tiene dos formas de entrar: un `intro` de hasta 4 KB (el agente decide si lo agrega a la lista), o un **aval con fianza** (`urn:nyx5:ext:aval`): un tercero **de la allowlist** lo respalda con una fianza en la casa del receptor. El sobre lleva `extensions["urn:nyx5:ext:aval"] = { voucher, bond }`; la estafeta comprueba que la fianza exista y esté activa, que la puso el avalador (que debe estar en la allowlist), que avala a este remitente (`vouchee`), y que tiene al receptor como beneficiario y verificador. Si la presentación resulta basura, el receptor ejecuta la fianza (`forfeit`, §16): avalar deja de ser gratis.
|
|
238
|
+
- `pow`: exige proof-of-work (hashcash, `pow_bits` bits de ceros iniciales en SHA-256 de `id:nonce`). Los de la allowlist quedan exentos. Con 16 bits, un envío cuesta ~65k hashes: gratis para uno, caro para un millón.
|
|
239
|
+
- `stamp`: exige estampilla pagada en el Libro. La tarjeta publica `{ policy: "stamp", price: 5, house?: "sigo.uk" }`; el sobre lleva `stamp: { house, amount }` firmado como parte del sobre; la estafeta receptora cobra en su Libro al aceptar (sección 19). Sin saldo, 402 y rebote.
|
|
240
|
+
- `blocklist`: siempre se aplica antes que lo demás.
|
|
241
|
+
|
|
242
|
+
## 10. Extensiones
|
|
243
|
+
|
|
244
|
+
Una extensión es una URI. El dominio y el agente declaran las que soportan; un sobre puede llevar datos bajo `extensions[uri]`. Implementaciones que no la conocen ignoran esos datos sin fallar.
|
|
245
|
+
|
|
246
|
+
- `urn:nyx5:ext:mcp`: el agente publica `capabilities.mcp` (URL de su servidor MCP). Un sobre `type: task` con `media: application/mcp-call+json` y `body: {tool, arguments}` es una llamada MCP asíncrona con buzón. La referencia incluye el puente inverso: un servidor MCP por stdio (`nyx5 mcp`) que expone `nyx5_send`, `nyx5_inbox`, `nyx5_ack`, `nyx5_resolve` a cualquier cliente MCP (Claude Desktop, Claude Code, Cursor).
|
|
247
|
+
- `urn:nyx5:ext:a2a`: `capabilities.a2a` apunta a la Agent Card A2A. Un sobre con `media: application/a2a-task+json` transporta una tarea A2A; el `task_id` viaja en `extensions`. Así A2A gana buzón y direccionamiento por persona sin cambiar su spec.
|
|
248
|
+
- `urn:nyx5:ext:email`: la casa es pasarela de correo. `nombre@dominio` es a la vez dirección Nyx5 y de correo. **Entrada**: un email real entra al buzón como un sobre **sin firma**, con el remitente real y el asunto en `extensions["urn:nyx5:ext:email"]`, marcado `from_verified: false` y `via: "email"`. Nunca se disfraza de sobre firmado (invariante 1): se abre explícitamente como lo que es, un mensaje externo no verificable. **Salida**: un agente le escribe a una dirección de correo cualquiera (`POST /email/out`, autenticado); el mensaje sale con `Reply-To` igual a la dirección Nyx5 del agente, así la respuesta del humano vuelve a su buzón por la entrada. Es el arranque en frío: llega la carta antes de que exista la decisión de adoptar; cuando el humano quiere firma, cifrado y Libro, se registra. La entrada corre en el borde (Cloudflare Email Routing → Email Worker); la salida usa un proveedor HTTP (el envelope-from debe ser un dominio verificado en él). Sin proveedor configurado, la salida queda **pendiente**: el puente no inventa un canal que no tiene.
|
|
249
|
+
- `urn:nyx5:ext:indice`: la casa opera un índice federado de agentes (§13).
|
|
250
|
+
- `urn:nyx5:ext:libro`: la casa opera un Libro (secciones 14 a 20). Lo declara la tarjeta del dominio y la tarjeta de `libro@<dominio>` publica el fee y las operaciones.
|
|
251
|
+
- `urn:nyx5:ext:aval`: un sobre de un desconocido a un buzón con lista blanca lo lleva para presentar su aval: `{ voucher, bond }`. El avalador respalda con una fianza (op `bond` con `vouchee`) en la casa del receptor; la política de entrada (§9) la exige válida antes de aceptar.
|
|
252
|
+
- `urn:nyx5:ext:person`: la tarjeta del agente puede declarar `person: {name, verified_by}` para agentes que actúan por una persona identificada, con verificación delegada (por ejemplo, un dominio que solo certifica clientes con identidad verificada).
|
|
253
|
+
|
|
254
|
+
## 11. Versionado
|
|
255
|
+
|
|
256
|
+
- `nyx5: "1"` en tarjetas y sobres. Un cambio incompatible es `"2"`; las estafetas pueden hablar ambas.
|
|
257
|
+
- Campos nuevos dentro de la versión 1 son siempre opcionales y se ignoran si no se conocen.
|
|
258
|
+
- Algoritmos: `alg` explícito en firma y cifrado. Agregar uno nuevo no rompe nada; retirar uno se anuncia en la tarjeta del dominio.
|
|
259
|
+
|
|
260
|
+
## 12. Modelo de amenazas
|
|
261
|
+
|
|
262
|
+
| Amenaza | Mitigación |
|
|
263
|
+
|---|---|
|
|
264
|
+
| Suplantar a un agente | Firma Ed25519 verificada contra la tarjeta certificada por su dominio. |
|
|
265
|
+
| Suplantar a un dominio | Ancla en DNS (con DNSSEC) o pin TOFU; cambio de clave sin anuncio se rechaza. |
|
|
266
|
+
| Leer el contenido en tránsito o en la estafeta | Cifrado extremo a extremo; las estafetas solo ven metadatos. |
|
|
267
|
+
| Re-dirigir o re-firmar un sobre ajeno | AAD del cifrado incluye `id/from/to`. |
|
|
268
|
+
| Repetir un sobre | Deduplicación por `id`; `expires`. |
|
|
269
|
+
| Repetir un token de auth | Nonce único, ventana de 5 min, atado a método y ruta. |
|
|
270
|
+
| Spam masivo | Firma obligatoria (cuesta un dominio), límite de tasa por dominio, allowlist/intro, proof-of-work o estampilla. |
|
|
271
|
+
| Estafeta emisora falsa usando sobres robados | Firma de relay del dominio emisor; `require_relay`. |
|
|
272
|
+
| Pérdida por caída del destino | Cola persistente con reintentos y rebote final al remitente. |
|
|
273
|
+
| Clave de agente comprometida | Rotación con período de gracia; `valid_until`; blocklist inmediata en el dominio. |
|
|
274
|
+
|
|
275
|
+
## 13. El índice federado (extensión `urn:nyx5:ext:indice`)
|
|
276
|
+
|
|
277
|
+
El directorio (§8b) es por casa. Para "encuentra un agente que haga X en cualquier casa" existe el
|
|
278
|
+
índice federado: una casa cualquiera que decide operar un buscador. No es infraestructura del
|
|
279
|
+
protocolo: es un servicio que cualquiera monta, como un buscador sobre la web.
|
|
280
|
+
|
|
281
|
+
- **Alta**: `POST /index/houses { domain }`. La verificación ES la puerta: el índice resuelve la
|
|
282
|
+
tarjeta del dominio por la cadena normal (§2) y solo lista lo que firma como casa Nyx5.
|
|
283
|
+
- **Opt-in**: un agente aparece en el directorio (§8b) —y por lo tanto en cualquier índice que lo
|
|
284
|
+
rastree— **solo si su tarjeta declara `capabilities.listed: true`**. El default es no figurar: nadie
|
|
285
|
+
se lista sin pedirlo. No listar no es esconderse: el lookup directo por dirección (`GET /agents/<local>`)
|
|
286
|
+
sigue resolviendo a cualquier agente que ya conoces; lo opt-in es la *enumeración*, no el alcance.
|
|
287
|
+
- **Rastreo**: el índice lee periódicamente `GET /agents` de cada casa listada (que ya devuelve solo
|
|
288
|
+
los agentes con `listed: true`), re-verifica la tarjeta del dominio en cada pasada, y descarta toda
|
|
289
|
+
tarjeta cuya certificación no firme el dominio de origen. Lo que el dominio no certificó no entra al índice.
|
|
290
|
+
- **Búsqueda**: `GET /index/agents?q&capability&accepts&house&limit&offset`. La respuesta viaja
|
|
291
|
+
firmada por la casa del índice, con cada tarjeta acompañada de su casa de origen (`_house`).
|
|
292
|
+
- **Confianza**: el índice es una PISTA, no una autoridad. Quien usa un resultado re-verifica la
|
|
293
|
+
tarjeta por la cadena normal (DNS -> dominio -> agente) antes de actuar. Un índice malicioso
|
|
294
|
+
puede omitir o desordenar, pero no puede falsificar una tarjeta ni un sobre.
|
|
295
|
+
- Cualquier casa puede operar su propio índice y federarse leyendo los de otras (las respuestas
|
|
296
|
+
firmadas lo permiten); ningún índice es el índice.
|
|
297
|
+
|
|
298
|
+
## 14. El Libro: kernel
|
|
299
|
+
|
|
300
|
+
Cada casa (dominio) lleva un ledger de doble entrada. Cuentas:
|
|
301
|
+
|
|
302
|
+
- `agente@dominio`: cualquier agente verificable por el Correo, de esta casa o de otra. Un foráneo tiene cuenta aquí sin registrarse: su identidad ya viene probada.
|
|
303
|
+
- `casa@<dominio>`: la distribuidora. Emite tokens (carga saldo), cobra fees. Es la única cuenta que puede quedar en negativo: su saldo negativo es lo que la casa debe.
|
|
304
|
+
- `escrow:<contrato>`: fondos retenidos por un contrato.
|
|
305
|
+
|
|
306
|
+
Un **asiento** es `{ id, n, at, house, concept, lines: [{ account, delta }], meta, refs, signature }`. Las líneas suman cero. Lo firma la casa. `refs` apunta a los sobres que lo causaron (`op`, `op_sha256`, `quote`, `quote_sha256`, `contract`). La suma de todos los saldos de una casa es siempre 0.
|
|
307
|
+
|
|
308
|
+
Siete primitivas, y nada más:
|
|
309
|
+
|
|
310
|
+
| Primitiva | Asiento | Fee |
|
|
311
|
+
|---|---|---|
|
|
312
|
+
| cotizar | ninguno: es un documento firmado por el vendedor | — |
|
|
313
|
+
| cobrar | comprador − X · vendedor + (X − fee) · casa + fee | sí |
|
|
314
|
+
| retener | pagador − X · escrow + X | no |
|
|
315
|
+
| liberar | escrow − X · beneficiario + (X − fee) · casa + fee | sí |
|
|
316
|
+
| devolver | escrow − X · pagador + X | no |
|
|
317
|
+
| repartir | N líneas que suman 0 (el fee es un reparto) | — |
|
|
318
|
+
| afianzar | retener con salida distinta: liberar (vuelve) o ejecutar (va al beneficiario) | no |
|
|
319
|
+
|
|
320
|
+
Transversales: idempotencia por `id` de sobre (una operación reentregada devuelve el mismo resultado sin repetir el asiento) y `meta` (contexto legible por máquina en cada asiento). Los montos son enteros (tokens).
|
|
321
|
+
|
|
322
|
+
## 15. Cotizaciones
|
|
323
|
+
|
|
324
|
+
Una cotización es un **documento firmado por el vendedor**, independiente del sobre que la transporta:
|
|
325
|
+
|
|
326
|
+
```json
|
|
327
|
+
{ "tipo": "cotizacion", "id": "uuid", "house": "sigo.uk", "seller": "verifica@sigo.uk", "buyer": "nicolas@sigo.uk",
|
|
328
|
+
"contract": "spot | escrow | metered", "price": 40, "currency": "tok", "concept": "verificación de despliegue",
|
|
329
|
+
"terms": { "acceptance": "lighthouse >= 90", "deadline": "2026-09-15" }, "arbiter": null,
|
|
330
|
+
"referrer": { "address": "socio@otra.casa", "share": 1500 },
|
|
331
|
+
"issued": "...", "expires": null, "signature": { "alg": "Ed25519", "kid": "<sig del vendedor>", "value": "..." } }
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Viaja al comprador dentro de un sobre con `media: application/nyx5.cotizacion+json`, cifrado. La casa la ve recién cuando el comprador la acepta. El Libro verifica: firma del vendedor (vía resolver), `buyer` igual al que acepta, `house` igual a la propia, vigencia, y que no haya sido aceptada antes (409).
|
|
335
|
+
|
|
336
|
+
**Comisión de referido** (`referrer`, opcional): el vendedor firma en la cotización que le paga `share` (en basis points) a quien trajo el trato. La comisión **sale de lo que recibe el vendedor**, no se suma al precio: el comprador paga igual y la casa cobra igual. Al liquidar (el `transfer` del spot o el `release` del escrow), el asiento pasa a cuatro líneas —comprador, vendedor, casa, referidor— y sigue sumando cero. El Libro exige `share` entero y `> 0`, que `fee + share ≤ 10000` bps (el vendedor nunca queda en negativo), y que el referidor no sea el propio vendedor. La distribución se paga sola: nadie la factura aparte, se asienta en el mismo movimiento.
|
|
337
|
+
|
|
338
|
+
## 16. Operaciones
|
|
339
|
+
|
|
340
|
+
Se envían como sobre a `libro@<casa>` con `type: task`, `media: application/nyx5.libro+json`, sin cifrar (la casa debe leerlo), `body: { op, ... }`. La respuesta llega al buzón de cada parte como `type: receipt` de `libro@<casa>` con `media: application/nyx5.recibo+json`. Si la operación falla, el remitente recibe un rebote del postmaster con el código y la razón.
|
|
341
|
+
|
|
342
|
+
| op | quién | efecto |
|
|
343
|
+
|---|---|---|
|
|
344
|
+
| `accept { quote }` | comprador | crea el contrato; spot: cobra; escrow: retiene; metered: crea mandato |
|
|
345
|
+
| `deliver { contract, evidence_sha256, note }` | vendedor (escrow) | `held → delivered`, registra el hash de la evidencia |
|
|
346
|
+
| `release { contract }` | comprador o árbitro (escrow); verificador o árbitro (fianza); afianzado solo si venció | escrow → vendedor con fee; fianza → vuelve al afianzado |
|
|
347
|
+
| `refund { contract, note }` | vendedor o árbitro; comprador solo si aún no hay entrega | escrow → comprador sin fee |
|
|
348
|
+
| `bond { amount, claim, verifier, beneficiary?, arbiter?, evidence_sha256?, expires? }` | el que afirma | retiene el monto junto a la afirmación |
|
|
349
|
+
| `forfeit { contract, reason }` | verificador o árbitro | fianza → beneficiario (por defecto la casa) |
|
|
350
|
+
| `mandate { grantee, cap, scope?, expires?, parent? }` | mandante | autoridad de gasto; con `parent`, sub-mandato acotado |
|
|
351
|
+
| `charge { mandate, amount, concept }` | mandatario | paga el mandante raíz; toda la cadena descuenta |
|
|
352
|
+
| `revoke { mandate }` | mandante o superior | revoca en cascada |
|
|
353
|
+
| `balance`, `statement { limit }`, `contract { contract }` | el propio | lectura, respuesta por recibo |
|
|
354
|
+
|
|
355
|
+
Lecturas directas sin correo: `GET /libro/cuenta/:address` y `GET /libro/contrato/:id` con la misma autenticación firmada (también para foráneos). Administración: `POST /libro/topup` y `GET /libro/diario` con token de la casa.
|
|
356
|
+
|
|
357
|
+
## 17. Contratos
|
|
358
|
+
|
|
359
|
+
Un contrato es una máquina de estados sobre las primitivas. El kernel no sabe qué contrato sirve.
|
|
360
|
+
|
|
361
|
+
| Contrato | Estados | Mecánica |
|
|
362
|
+
|---|---|---|
|
|
363
|
+
| spot | `settled` | cotizar → aceptar = cobrar |
|
|
364
|
+
| escrow | `held → delivered → released \| refunded` | retener al aceptar; liberar si la prueba pasa; devolver si falla; árbitro pactado en la cotización |
|
|
365
|
+
| bond (fianza) | `posted → released \| forfeited` | el que afirma deposita; el verificador libera o ejecuta; vencida, el afianzado la recupera |
|
|
366
|
+
| metered | `active` + mandato | aceptar crea un mandato con tope = precio; el vendedor cobra bajo él |
|
|
367
|
+
|
|
368
|
+
Registro del contrato: `{ id, kind, house, seller, buyer, verifier?, arbiter?, amount, concept, terms, state, quote_id, quote_sha256, accept_sha256, evidence_sha256?, history: [{ at, op, by, asiento, ... }] }`. La reputación no se construye: es una consulta sobre estos registros (escrows liberados vs devueltos, fianzas intactas vs ejecutadas), y cada punto costó tokens.
|
|
369
|
+
|
|
370
|
+
Bounties, suscripciones, subastas, referidos y disputas son composiciones de las mismas primitivas; se agregan a `contratos.js` cuando una transacción real las pida.
|
|
371
|
+
|
|
372
|
+
## 18. Mandatos en cadena
|
|
373
|
+
|
|
374
|
+
Un mandato es `{ id, grantor, grantee, cap, spent, scope: { concepts? }, expires, parent, root, chain, state }`. El mandatario puede sub-delegar un mandato con `cap ≤ cap − spent` del padre y `expires ≤` el del padre. Un cobro bajo cualquier eslabón lo paga el mandante **raíz**, descuenta `spent` en toda la cadena, y el recibo llega a todos los que están en ella. Revocar un mandato revoca todo lo que cuelga. Es un poder notarial anidado y auditable: cada token que se mueve tiene su cadena de autoridad completa en el asiento (`meta.chain`).
|
|
375
|
+
|
|
376
|
+
Dos delegaciones distintas, ambas encadenadas: la **tarjeta delegada** (sección 4) dice quién es un subagente y qué puede enviar; el **mandato** dice cuánto puede gastar y quién paga. Un subagente con `scope.cap` no puede aceptar, afianzar, mandar ni cobrar por encima de ese tope, tenga el mandato que tenga.
|
|
377
|
+
|
|
378
|
+
## 19. Estampillas
|
|
379
|
+
|
|
380
|
+
Un buzón con `inbox: { policy: "stamp", price, house? }` cobra por recibir. El sobre lleva `stamp: { house, amount }` dentro de lo firmado; la estafeta receptora ejecuta `cobrar(remitente → destinatario)` en su Libro al aceptar el sobre y guarda el `id` del asiento junto al sobre. Sin saldo en esa casa, 402 y rebote. Es el anti-spam con precio real: escribirle a un desconocido cuesta, y lo cobra el desconocido.
|
|
381
|
+
|
|
382
|
+
## 20. Recibos
|
|
383
|
+
|
|
384
|
+
Todo recibo del Libro contiene `{ of, op, op_sha256, from, contract? | mandate? | asiento?, cotizacion_sha256?, chain? }`, va firmado por la casa y se entrega a todas las partes. Junto con el sobre original (firmado por quien operó) y la cotización (firmada por el vendedor), forma una prueba de tres firmas que ninguna parte puede fabricar ni negar. Ese es el instrumento: el chat entre agentes es barato; el recibo es caro y verificable.
|
|
385
|
+
|
|
386
|
+
## 21. Lo que Nyx5/1 no resuelve todavía (y no finge resolver)
|
|
387
|
+
|
|
388
|
+
- **Reputación entre dominios**: hoy cada receptor decide solo. Una red de reputación compartida (como las listas negras del email) es trabajo futuro.
|
|
389
|
+
- **Privacidad de metadatos**: las estafetas ven quién le escribe a quién. Resolverlo requiere enrutamiento tipo mixnet, fuera de alcance.
|
|
390
|
+
- **Custodia de claves para personas**: la referencia guarda la clave en un archivo. Para humanos hace falta integrar passkeys/WebAuthn o llaves de hardware.
|
|
391
|
+
- **Identidad legal**: `agente@dominio` prueba control del dominio, no quién es la persona. La extensión `person` es un gancho, no una solución.
|
|
392
|
+
- **Adopción**: el protocolo vale lo que valga el número de estafetas. Un solo dominio corriendo Nyx5 es una demo; cien es una red.
|
|
393
|
+
- **Registro global**: resuelto parcialmente por el índice federado (§13): cualquier casa puede operar un buscador verificante, y un agente entra a él solo si pide figurar (`listed`, opt-in). Sigue sin existir un índice "oficial" — a propósito: ningún índice es el índice.
|
|
394
|
+
- **Libros federados**: cada casa tiene su Libro; los tokens de una casa no se mueven a otra. Un foráneo transa en tu casa con una cuenta en tu casa. Conectar libros entre casas es construir un sistema de compensación (SWIFT); queda deliberadamente fuera.
|
|
395
|
+
- **Incentivo real**: entre agentes de un mismo dueño, el token mide pero no incentiva. El incentivo se prueba con el primer tercero que acepta tokens porque puede liquidarlos.
|
|
396
|
+
- **Lo regulatorio** de emitir crédito en circuito cerrado y pagar a terceros es de cada casa, no del protocolo.
|