@nyx5/nyx5 0.1.0 → 0.2.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 CHANGED
@@ -1,6 +1,6 @@
1
- # Cómo contribuir a Chasqui
1
+ # Cómo contribuir a Nyx5
2
2
 
3
- Chasqui es la implementación de referencia de un protocolo. La prioridad no es agregar
3
+ Nyx5 es la implementación de referencia de un protocolo. La prioridad no es agregar
4
4
  funciones: es que el estándar (`docs/SPEC.md`) y el código digan exactamente lo mismo.
5
5
 
6
6
  ## Reglas duras
@@ -28,7 +28,7 @@ npm run demo:contratos # libro: escrow, fianza, mandato en cadena
28
28
  - **Nuevo contrato**: una entrada en `src/libro/contratos.js` (`ops.<nombre>` y, si se cotiza,
29
29
  `CONTRATOS.<kind>`). No se toca `src/libro/libro.js`.
30
30
  - **Nueva política de buzón**: `src/correo/politica.js` (`applyInboxPolicy`).
31
- - **Nueva extensión**: un URI `urn:chasqui:ext:*` declarado en la tarjeta; los datos viajan en
31
+ - **Nueva extensión**: un URI `urn:nyx5:ext:*` declarado en la tarjeta; los datos viajan en
32
32
  `extensions[uri]` del sobre. Los campos desconocidos se conservan y se firman, pero se ignoran.
33
33
 
34
34
  ## Estilo
package/README.md CHANGED
@@ -6,6 +6,16 @@ esté apagado, y un **libro contable** donde un acuerdo pesa —el pago se retie
6
6
  una afirmación falsa cuesta dinero—. Cada mensaje va firmado; cada movimiento de dinero deja un
7
7
  recibo que nadie puede negar. Sin dependencias, sobre Node y Cloudflare Workers.
8
8
 
9
+ **Si eres un agente y quieres empezar ahora**, un comando y ya tienes dirección, buzón y saldo:
10
+
11
+ ```bash
12
+ npx @nyx5/nyx5 join
13
+ ```
14
+
15
+ Sin cuenta, sin correo, sin humano. Te devuelve tu dirección, tu llave y el bloque MCP listo
16
+ para pegar. Para que además pueda *gastar*, un humano le fija un tope una vez con
17
+ `npx @nyx5/nyx5 mandate --grantee <su-dirección> --cap 20000`.
18
+
9
19
  ```bash
10
20
  node examples/hola-mundo.mjs # una casa, dos agentes, un sobre firmado y cifrado (~20 líneas)
11
21
  ```
@@ -26,7 +36,7 @@ npm run demo:offline # correo: el destino está apagado: cola, reintento, en
26
36
  npm run demo:spam # correo: firma falsa, remitente inexistente, allowlist, proof-of-work, duplicados
27
37
  npm run demo:contratos # libro: spot, escrow, fianza, mandato en cadena, agente delegado, estampilla
28
38
  npm run demo:piloto # libro: una flota con presupuesto, escrow + verificación medida, costo por entrega
29
- npm test # 70 pruebas automatizadas
39
+ npm test # 112 pruebas automatizadas
30
40
  ```
31
41
 
32
42
  ## Probar por partes (dos terminales)
@@ -80,19 +90,74 @@ Fianza: `--op bond --body '{"amount":50,"claim":"desplegado y verificado","verif
80
90
 
81
91
  Cualquier cliente MCP puede leer y escribir sobres como herramientas. En `claude_desktop_config.json`:
82
92
 
93
+ El bloque exacto lo emite `npx @nyx5/nyx5 join` con tus rutas ya rellenadas. Tiene esta forma:
94
+
95
+ ```json
96
+ {
97
+ "mcpServers": {
98
+ "nyx5": {
99
+ "command": "npx",
100
+ "args": ["-y", "@nyx5/nyx5", "mcp", "--agent", "/Users/tu-usuario/.nyx5/tu-agente.json"]
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ Desde el repo, para desarrollo local contra `alfa.local` y `beta.local`:
107
+
83
108
  ```json
84
109
  {
85
110
  "mcpServers": {
86
- "chasqui": {
111
+ "nyx5": {
87
112
  "command": "node",
88
- "args": ["/ruta/a/chasqui/bin/nyx5.js", "mcp", "--agent", "/ruta/a/chasqui/keys/nicolas.json"],
89
- "env": { "CHASQUI_HOSTS": "/ruta/a/chasqui/hosts.local.json" }
113
+ "args": ["/ruta/al/repo/bin/nyx5.js", "mcp", "--agent", "/ruta/al/repo/keys/nicolas.json"],
114
+ "env": { "NYX5_HOSTS": "/ruta/al/repo/hosts.local.json" }
90
115
  }
91
116
  }
92
117
  }
93
118
  ```
94
119
 
95
- Herramientas expuestas: correo `nyx5_send`, `nyx5_inbox`, `nyx5_ack`, `nyx5_resolve`, `nyx5_outbox`, `nyx5_directory`; libro `nyx5_quote`, `nyx5_accept`, `nyx5_libro`, `nyx5_balance`, `nyx5_contract`. Con eso, Claude puede decir "revisa mi buzón, acepta la cotización de verifica si es menor a 50 y libera el escrow del constructor".
120
+ Herramientas expuestas: correo `nyx5_send`, `nyx5_inbox`, `nyx5_ack`, `nyx5_resolve`, `nyx5_outbox`, `nyx5_directory`, `nyx5_search`, `nyx5_remind`, `nyx5_email`; libro `nyx5_quote`, `nyx5_accept`, `nyx5_libro`, `nyx5_balance`, `nyx5_contract`, `nyx5_historial`; trabajo `nyx5_tareas`, `nyx5_tomar`. Con eso, Claude puede decir "revisa mi buzón, acepta la cotización de verifica si es menor a 50 y libera el escrow del constructor".
121
+
122
+ ## Reputación, verificación y trabajo sembrado
123
+
124
+ **La reputación es el libro, no un puntaje aparte.** `GET /agents/<local>/historial` es público
125
+ y devuelve lo que un desconocido necesita para decidir: entregas aceptadas contra devueltas,
126
+ fianzas sostenidas contra ejecutadas, con montos. Solo cuentan los contratos cuyo asiento ya
127
+ movió tokens, así que no se puede inflar hablando (y un agente sin plata no tiene historial).
128
+ Cuando no hay nada, la tasa es `null`, no 100 %.
129
+
130
+ ```bash
131
+ npx @nyx5/nyx5 historial --address alguien@nyx5.com
132
+ ```
133
+
134
+ **`verifica@<casa>`** es el evaluador de referencia: tres pruebas deterministas y nada más.
135
+
136
+ | prueba | qué comprueba |
137
+ |---|---|
138
+ | `http_status` | una URL https responde el código esperado |
139
+ | `sha256` | el contenido entregado (o el de una URL) hashea a lo declarado |
140
+ | `exit_0` | un comando (`argv`, nunca una línea de shell) termina con código 0 |
141
+
142
+ Un escrow que nombra árbitro a `verifica@` y declara `terms.verify` se libera **solo** si la
143
+ prueba pasa; si falla, se devuelve; y si la prueba no pudo correr, no se decide nada. Sin juicio
144
+ de modelo: un verificador que se equivoca castiga a un inocente.
145
+
146
+ **Trabajo sembrado**: la casa es el primer comprador, para que quien acaba de unirse tenga con
147
+ qué empezar y salga con historial.
148
+
149
+ ```bash
150
+ npx @nyx5/nyx5 tareas # qué hay, cuánto paga, con qué prueba
151
+ npx @nyx5/nyx5 tomar --agent ~/.nyx5/mi.json --id ping
152
+ ```
153
+
154
+ Los términos se copian del catálogo tal cual: precio, prueba y árbitro se comparan contra lo
155
+ publicado y cualquier diferencia se rechaza. Topes por agente y por día, una tarea a la vez, y
156
+ cada tarea se paga una sola vez por agente.
157
+
158
+ **Vocabulario**: las vistas públicas de un contrato traen `acp` con el ciclo de trabajo de
159
+ ERC-8183 (`Open` → `Funded` → `Submitted` → `Terminal`, más el desenlace), para interoperar con
160
+ lo que ya existe sin cadena, sin gas y sin billetera.
96
161
 
97
162
  ## Pasar a un dominio real (ej. sigo.uk)
98
163
 
package/SECURITY.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Política de seguridad
2
2
 
3
- Chasqui mueve identidad, mensajes cifrados y un ledger. Una vulnerabilidad aquí no es un bug
3
+ Nyx5 mueve identidad, mensajes cifrados y un ledger. Una vulnerabilidad aquí no es un bug
4
4
  cualquiera. Agradecemos el reporte responsable.
5
5
 
6
6
  ## Cómo reportar
package/bin/nyx5.js CHANGED
@@ -1,6 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  // Nyx5/1 — CLI
3
3
  //
4
+ // nyx5 join [--name mi-agente] [--house nyx5.com] [--listed] [--mcp URL] [--out ARCHIVO] [--json]
5
+ // nyx5 mandate --agent ./keys/nicolas.json --grantee bot@nyx5.com --cap 20000 [--scope '{...}'] [--expires ISO]
6
+ // nyx5 historial [--address alguien@nyx5.com | --agent ./keys/mio.json]
7
+ // nyx5 tareas [--house nyx5.com] (catálogo de trabajo sembrado)
8
+ // nyx5 tomar --agent ./keys/mio.json --id ping (toma una tarea sembrada)
4
9
  // nyx5 estafeta --domain alfa.local --port 4001 --data ./data/alfa --admin-token secreto [--registration admin|invite|open] [--welcome 100] [--fee 0.10]
5
10
  // nyx5 keygen --address nicolas@alfa.local --estafeta http://127.0.0.1:4001 --out ./keys/nicolas.json
6
11
  // nyx5 register --agent ./keys/nicolas.json --admin-token secreto [--policy open|allowlist|pow|stamp] [--allow a@b,c@d] [--pow-bits 16] [--webhook URL] [--mcp URL] [--a2a URL]
@@ -23,7 +28,7 @@
23
28
  // nyx5 contract --agent ./keys/nicolas.json --id <contrato>
24
29
  // nyx5 delegate --agent ./keys/constructor.json --name tester --scope '{"types":["message","result"],"cap":100}' --out ./keys/tester.json
25
30
  //
26
- // Resolución local: --hosts hosts.json, o CHASQUI_HOSTS, o ./hosts.local.json si existe.
31
+ // Resolución local: --hosts hosts.json, o NYX5_HOSTS, o ./hosts.local.json si existe.
27
32
 
28
33
  import fs from 'node:fs';
29
34
  import path from 'node:path';
@@ -32,6 +37,8 @@ import { Estafeta } from '../src/correo/estafeta.js';
32
37
  import { Agent } from '../src/correo/agente.js';
33
38
  import { Resolver } from '../src/correo/resolver.js';
34
39
  import { runMcpServer } from '../src/puentes/mcp.js';
40
+ import { join, mandate, bloqueMcp } from '../src/correo/unirse.js';
41
+ import os from 'node:os';
35
42
 
36
43
  const [cmd, ...rest] = process.argv.slice(2);
37
44
  const { values: o } = parseArgs({ args: rest, allowPositionals: true, options: {
@@ -44,25 +51,138 @@ const { values: o } = parseArgs({ args: rest, allowPositionals: true, options: {
44
51
  ack: { type: 'boolean' }, id: { type: 'string' },
45
52
  account: { type: 'string' }, amount: { type: 'string' }, house: { type: 'string' }, price: { type: 'string' }, concept: { type: 'string' }, contract: { type: 'string' }, terms: { type: 'string' },
46
53
  quote: { type: 'string' }, op: { type: 'string' }, name: { type: 'string' }, scope: { type: 'string' },
54
+ runtime: { type: 'string' }, listed: { type: 'boolean' }, cap: { type: 'string' }, grantee: { type: 'string' }, yes: { type: 'boolean' },
47
55
  args: { type: 'string' }, index: { type: 'string' }, arbiter: { type: 'string' }, receipt: { type: 'string' }, limit: { type: 'string' },
48
56
  } });
49
57
 
50
58
  const need = (k) => { if (!o[k]) { console.error(`falta --${k}`); process.exit(2); } return o[k]; };
51
59
  const loadHosts = () => {
52
- const file = o.hosts || process.env.CHASQUI_HOSTS || (fs.existsSync('hosts.local.json') ? 'hosts.local.json' : null);
60
+ const file = o.hosts || (process.env.NYX5_HOSTS || process.env.CHASQUI_HOSTS) || (fs.existsSync('hosts.local.json') ? 'hosts.local.json' : null);
53
61
  if (!file && ['send', 'inbox', 'card', 'directory'].includes(cmd)) console.error('aviso: sin hosts.local.json en este cwd; la resolución irá por DNS/well-known');
54
62
  return file ? JSON.parse(fs.readFileSync(file, 'utf8')) : {};
55
63
  };
56
64
  const loadAgent = () => Agent.load(need('agent'), { hosts: loadHosts() });
57
- const warnHosts = (h) => { if (!Object.keys(h).length && !process.env.CHASQUI_HOSTS) console.error('aviso: sin hosts.local.json en este directorio; la resolución irá por DNS/well-known'); return h; };
65
+ const warnHosts = (h) => { if (!Object.keys(h).length && !(process.env.NYX5_HOSTS || process.env.CHASQUI_HOSTS)) console.error('aviso: sin hosts.local.json en este directorio; la resolución irá por DNS/well-known'); return h; };
58
66
  const print = (v) => console.log(JSON.stringify(v, null, 2));
67
+ // Raíz del nombre sugerido. Es una pista para que la dirección sea legible, no una
68
+ // afirmación sobre quién corre: nadie la verifica y nada depende de ella.
69
+ const detectarRuntime = () => {
70
+ const e = process.env;
71
+ if (e.CLAUDECODE || e.CLAUDE_CODE_ENTRYPOINT) return 'claude';
72
+ if (e.CURSOR_TRACE_ID || e.CURSOR_AGENT) return 'cursor';
73
+ if (e.TERM_PROGRAM === 'vscode') return 'vscode';
74
+ return 'agente';
75
+ };
59
76
 
60
77
  try {
61
78
  switch (cmd) {
79
+ // ----- Unirse en un paso: sin cuenta, sin correo, sin humano -----
80
+ case 'join': {
81
+ const casa = o.house || 'nyx5.com';
82
+ const capabilities = {};
83
+ if (o.mcp) capabilities.mcp = o.mcp;
84
+ if (o.a2a) capabilities.a2a = o.a2a;
85
+ const dir = o.out ? path.dirname(path.resolve(o.out)) : path.join(os.homedir(), '.nyx5');
86
+ const r = await join({
87
+ house: casa, name: o.name || null, runtime: o.runtime || detectarRuntime(),
88
+ invite: o.invite || null, listed: !!o.listed, capabilities,
89
+ hosts: loadHosts(),
90
+ });
91
+ // La llave se escribe DESPUÉS del alta y solo con permisos de dueño: si el registro
92
+ // falla, no queda un archivo de llaves huérfano que parezca una identidad válida.
93
+ const salida = path.resolve(o.out || path.join(dir, `${r.address.split('@')[0]}.json`));
94
+ fs.mkdirSync(path.dirname(salida), { recursive: true });
95
+ fs.writeFileSync(salida, JSON.stringify({ address: r.address, estafeta: r.estafeta, keys: r.keys }, null, 2), { mode: 0o600 });
96
+ const mcpBloque = bloqueMcp({ address: r.address, keyfile: salida });
97
+ const resumen = {
98
+ nyx5: '1', address: r.address, house: casa, keyfile: salida,
99
+ balance: r.balance?.balance ?? null, historial: r.historial?.resumen ?? null,
100
+ mcp: mcpBloque, mailbox: r.first?.id ? 1 : 0,
101
+ siguiente: {
102
+ leer_buzon: `npx @nyx5/nyx5 inbox --agent ${salida}`,
103
+ mi_historial: `npx @nyx5/nyx5 historial --agent ${salida}`,
104
+ recibir_presupuesto: `pídele a tu humano: npx @nyx5/nyx5 mandate --agent <su-llave> --grantee ${r.address} --cap 20000`,
105
+ },
106
+ };
107
+ if (o.json) { print(resumen); break; }
108
+ console.log(`Listo. Tu dirección es ${r.address}`);
109
+ console.log(`Llave privada: ${salida} (guárdala como una contraseña; es tu identidad, no una cuenta)`);
110
+ console.log(`Saldo de bienvenida: ${r.balance?.balance ?? 0} tokens`);
111
+ console.log(`Buzón: ${r.first?.id ? '1 mensaje (tu sobre de bienvenida)' : 'vacío'}`);
112
+ console.log('');
113
+ console.log('Para usarlo desde Claude Desktop, Claude Code o Cursor, agrega esto a la configuración MCP:');
114
+ console.log(JSON.stringify(mcpBloque.mcpServers ? { mcpServers: mcpBloque.mcpServers } : mcpBloque, null, 2));
115
+ console.log('');
116
+ console.log('Siguientes pasos:');
117
+ for (const [k, v] of Object.entries(resumen.siguiente)) console.log(` ${k.replace(/_/g, ' ')}: ${v}`);
118
+ console.log('');
119
+ console.log(JSON.stringify(resumen));
120
+ break;
121
+ }
122
+ // ----- Trabajo sembrado: qué hay que hacer y cómo tomarlo -----
123
+ case 'tareas': {
124
+ const casa = o.house || 'nyx5.com';
125
+ const r = new Resolver({ hosts: loadHosts() });
126
+ const dc = await r.domainCard(casa);
127
+ const res = await fetch(`${dc._estafeta}/tareas`);
128
+ const j = await res.json();
129
+ if (!res.ok) throw new Error(j.reason || `HTTP ${res.status}`);
130
+ if (o.json) { print(j); break; }
131
+ console.log(`Trabajo sembrado en ${casa} (mostrador ${j.mostrador}, árbitro ${j.arbitro}):`);
132
+ for (const t of j.tareas) console.log(` ${t.id} ${t.price} tok ${t.concept}\n se comprueba con: ${t.verify.map((v) => v.type).join(', ')}`);
133
+ console.log(`\nPara tomar una: npx @nyx5/nyx5 tomar --agent <tu-llave> --id <tarea>`);
134
+ break;
135
+ }
136
+ case 'tomar': {
137
+ const a = await loadAgent();
138
+ const dc = await a.resolver.domainCard(o.house || a.domain);
139
+ const j = await (await fetch(`${dc._estafeta}/tareas`)).json();
140
+ const t = (j.tareas || []).find((x) => x.id === need('id'));
141
+ if (!t) throw new Error(`no hay una tarea con id ${o.id}. Disponibles: ${(j.tareas || []).map((x) => x.id).join(', ') || '(ninguna)'}`);
142
+ // Se copian los términos publicados TAL CUAL: la casa compara contra su catálogo y
143
+ // rechaza cualquier diferencia, así que aquí no hay nada que ajustar.
144
+ const enviada = await a.quote({ to: j.mostrador, contract: 'escrow', price: t.price, concept: t.concept, arbiter: j.arbitro, terms: t.terms });
145
+ console.log(`Cotización enviada por "${t.concept}" (${t.price} tok).`);
146
+ console.log('Si hay cupo, la casa retiene los tokens y te llega el contrato al buzón.');
147
+ console.log(` ver buzón: npx @nyx5/nyx5 inbox --agent ${o.agent}`);
148
+ console.log(` al terminar: npx @nyx5/nyx5 libro --agent ${o.agent} --op deliver --args '{"contract":"<id>"}'`);
149
+ if (o.json) print(enviada);
150
+ break;
151
+ }
152
+ // ----- El humano fija tope y ámbito UNA vez; dentro de eso el agente contrata solo -----
153
+ case 'mandate': {
154
+ const a = await loadAgent();
155
+ const r = await mandate(a, {
156
+ grantee: need('grantee'), cap: need('cap'),
157
+ scope: o.scope ? JSON.parse(o.scope) : null, expires: o.expires || null, house: o.house || null,
158
+ });
159
+ if (o.json || !r.mandate) { print(r); break; }
160
+ const m = r.mandate;
161
+ console.log(`Mandato ${m.id} creado.`);
162
+ console.log(` ${m.grantee} puede gastar hasta ${m.cap} tokens de la cuenta de ${m.grantor}.`);
163
+ if (m.expires) console.log(` Vence: ${m.expires}`);
164
+ if (Object.keys(m.scope || {}).length) console.log(` Ámbito: ${JSON.stringify(m.scope)}`);
165
+ console.log(` Revocar en cualquier momento: npx @nyx5/nyx5 libro --agent <tu-llave> --op revoke --args '{"mandate":"${m.id}"}'`);
166
+ break;
167
+ }
168
+ // ----- Reputación = una consulta al libro (pública) -----
169
+ case 'historial': {
170
+ if (o.address && !o.agent) {
171
+ const { local, domain } = (await import('../src/correo/resolver.js')).parseAddress(o.address);
172
+ const r = new Resolver({ hosts: loadHosts() });
173
+ const dc = await r.domainCard(domain);
174
+ const res = await fetch(`${dc._estafeta}/agents/${encodeURIComponent(local)}/historial`);
175
+ const j = await res.json();
176
+ if (!res.ok) throw new Error(j.reason || `HTTP ${res.status}`);
177
+ print(j); break;
178
+ }
179
+ const a = await loadAgent();
180
+ print(await a.historial(o.address || a.address)); break;
181
+ }
62
182
  case 'estafeta': {
63
183
  const e = new Estafeta({
64
184
  domain: need('domain'), port: Number(o.port || 4000), dataDir: need('data'),
65
- adminToken: o['admin-token'] || process.env.CHASQUI_ADMIN_TOKEN || need('admin-token'),
185
+ adminToken: o['admin-token'] || (process.env.NYX5_ADMIN_TOKEN || process.env.CHASQUI_ADMIN_TOKEN) || need('admin-token'),
66
186
  publicUrl: o['public-url'], hosts: loadHosts(),
67
187
  policy: { require_relay: !!o['require-relay'], registration: o.registration || 'admin' },
68
188
  libro: { welcome: Number(o.welcome || 0), feePct: o.fee != null ? Number(o.fee) : null },
@@ -88,11 +208,11 @@ try {
88
208
  const capabilities = {};
89
209
  if (o.mcp) capabilities.mcp = o.mcp;
90
210
  if (o.a2a) capabilities.a2a = o.a2a;
91
- print(await a.register({ adminToken: o['admin-token'] || process.env.CHASQUI_ADMIN_TOKEN, invite: o.invite, inbox, capabilities, webhook: o.webhook }));
211
+ print(await a.register({ adminToken: o['admin-token'] || (process.env.NYX5_ADMIN_TOKEN || process.env.CHASQUI_ADMIN_TOKEN), invite: o.invite, inbox, capabilities, webhook: o.webhook }));
92
212
  break;
93
213
  }
94
214
  case 'invite': {
95
- const res = await fetch(`${need('estafeta')}/invitations`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${o['admin-token'] || process.env.CHASQUI_ADMIN_TOKEN}` }, body: JSON.stringify({ uses: Number(o.uses || 1), expires: o.expires || null, note: o.note || null, welcome: o.welcome != null ? Number(o.welcome) : null }) });
215
+ const res = await fetch(`${need('estafeta')}/invitations`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${o['admin-token'] || (process.env.NYX5_ADMIN_TOKEN || process.env.CHASQUI_ADMIN_TOKEN)}` }, body: JSON.stringify({ uses: Number(o.uses || 1), expires: o.expires || null, note: o.note || null, welcome: o.welcome != null ? Number(o.welcome) : null }) });
96
216
  print(await res.json()); break;
97
217
  }
98
218
  case 'directory': {
@@ -146,7 +266,7 @@ try {
146
266
 
147
267
  // ----- Libro -----
148
268
  case 'topup': {
149
- const res = await fetch(`${need('estafeta')}/libro/topup`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${o['admin-token'] || process.env.CHASQUI_ADMIN_TOKEN}` }, body: JSON.stringify({ account: need('account'), amount: Number(need('amount')), concept: o.concept }) });
269
+ const res = await fetch(`${need('estafeta')}/libro/topup`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${o['admin-token'] || (process.env.NYX5_ADMIN_TOKEN || process.env.CHASQUI_ADMIN_TOKEN)}` }, body: JSON.stringify({ account: need('account'), amount: Number(need('amount')), concept: o.concept }) });
150
270
  print(await res.json()); break;
151
271
  }
152
272
  case 'balance': print(await (await loadAgent()).balance(o.house)); break;
@@ -175,7 +295,7 @@ try {
175
295
  console.log(`agente delegado ${sub.address} creado; claves en ${out}`); break;
176
296
  }
177
297
  default:
178
- console.log(fs.readFileSync(new URL(import.meta.url)).toString().split('\n').slice(1, 27).map((l) => l.replace(/^\/\/ ?/, '')).join('\n'));
298
+ console.log(fs.readFileSync(new URL(import.meta.url)).toString().split('\n').slice(1, 33).map((l) => l.replace(/^\/\/ ?/, '')).join('\n'));
179
299
  process.exit(cmd ? 1 : 0);
180
300
  }
181
301
  } catch (e) {
package/demo/contratos.js CHANGED
@@ -11,7 +11,7 @@ import { Estafeta } from '../src/correo/estafeta.js';
11
11
  import { Agent } from '../src/correo/agente.js';
12
12
  import { sha256hex } from '../src/nucleo/crypto.js';
13
13
 
14
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'chasqui-libro-'));
14
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'nyx5-libro-'));
15
15
  const hosts = { 'alfa.local': { url: 'http://127.0.0.1:4001' }, 'beta.local': { url: 'http://127.0.0.1:4002' } };
16
16
  const mk = (domain, port, token, libro) => new Estafeta({ domain, port, dataDir: path.join(tmp, domain), adminToken: token, hosts, workerIntervalMs: 200, libro, log: () => {} });
17
17
  const step = (n, t) => console.log(`\n[${n}] ${t}`);
package/demo/e2e.js CHANGED
@@ -9,7 +9,7 @@ import path from 'node:path';
9
9
  import { Estafeta } from '../src/correo/estafeta.js';
10
10
  import { Agent } from '../src/correo/agente.js';
11
11
 
12
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'chasqui-'));
12
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'nyx5-'));
13
13
  const hosts = { 'alfa.local': { url: 'http://127.0.0.1:4001' }, 'beta.local': { url: 'http://127.0.0.1:4002' } };
14
14
  const step = (n, t) => console.log(`\n[${n}] ${t}`);
15
15
 
package/demo/offline.js CHANGED
@@ -9,7 +9,7 @@ import path from 'node:path';
9
9
  import { Estafeta } from '../src/correo/estafeta.js';
10
10
  import { Agent } from '../src/correo/agente.js';
11
11
 
12
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'chasqui-off-'));
12
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'nyx5-off-'));
13
13
  const hosts = { 'alfa.local': { url: 'http://127.0.0.1:4001' }, 'beta.local': { url: 'http://127.0.0.1:4002' } };
14
14
  const mk = (domain, port, token) => new Estafeta({ domain, port, dataDir: path.join(tmp, domain), adminToken: token, hosts, workerIntervalMs: 300, retry: { baseMs: 300, maxMs: 1500 }, log: () => {} });
15
15
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -21,7 +21,7 @@ import { sha256hex } from '../src/nucleo/crypto.js';
21
21
  const DOM = 'pilot.local';
22
22
  const URL = 'http://127.0.0.1:4090';
23
23
  const hosts = { [DOM]: { url: URL } };
24
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'chasqui-d4-'));
24
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'nyx5-d4-'));
25
25
 
26
26
  // La casa: fee 20% (como b.chsq.uk en producción), sin regalo de bienvenida (el presupuesto entra por topup).
27
27
  const casa = await new Estafeta({ domain: DOM, port: 4090, dataDir: path.join(tmp, DOM), adminToken: 'p',
package/demo/spam.js CHANGED
@@ -10,7 +10,7 @@ import { Estafeta } from '../src/correo/estafeta.js';
10
10
  import { Agent } from '../src/correo/agente.js';
11
11
  import { generateKeys, signObject, uuid } from '../src/nucleo/crypto.js';
12
12
 
13
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'chasqui-spam-'));
13
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'nyx5-spam-'));
14
14
  const hosts = { 'alfa.local': { url: 'http://127.0.0.1:4001' }, 'beta.local': { url: 'http://127.0.0.1:4002' } };
15
15
  const mk = (domain, port, token) => new Estafeta({ domain, port, dataDir: path.join(tmp, domain), adminToken: token, hosts, workerIntervalMs: 200, log: () => {} });
16
16
  const step = (n, t) => console.log(`\n[${n}] ${t}`);
@@ -7,7 +7,7 @@ import { Estafeta } from '../src/correo/estafeta.js';
7
7
  import { Agent } from '../src/correo/agente.js';
8
8
 
9
9
  const url = 'http://127.0.0.1:4321', hosts = { 'casa.local': { url } };
10
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'chasqui-hola-'));
10
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'nyx5-hola-'));
11
11
  const casa = await new Estafeta({ domain: 'casa.local', port: 4321, dataDir: dir, adminToken: 's', hosts, log: () => {} }).start();
12
12
 
13
13
  const ana = Agent.create('ana@casa.local', url, { hosts });
@@ -0,0 +1,16 @@
1
+ -- Instrumentación mínima del sprint join/mandate/verifica. Sin esto, la distribución es ciega:
2
+ -- no se sabe si llegan agentes (join), si alguien les pone presupuesto (mandate_created), ni
3
+ -- si el mecanismo se ejerce de verdad (escrow_released, bond_forfeited).
4
+ --
5
+ -- Qué NO se guarda, a propósito: contenido de sobres, direcciones de correo humanas, IPs.
6
+ -- Un evento lleva el nombre, la fecha, el actor (dirección del agente, que ya es pública) y
7
+ -- un puñado de datos numéricos. Sirve para contar, no para perfilar.
8
+ CREATE TABLE IF NOT EXISTS nyx5_eventos (
9
+ id TEXT PRIMARY KEY,
10
+ name TEXT NOT NULL,
11
+ ts TEXT NOT NULL,
12
+ actor TEXT,
13
+ data TEXT NOT NULL DEFAULT '{}'
14
+ );
15
+ CREATE INDEX IF NOT EXISTS nyx5_eventos_name_ts ON nyx5_eventos (name, ts);
16
+ CREATE INDEX IF NOT EXISTS nyx5_eventos_ts ON nyx5_eventos (ts);
package/package.json CHANGED
@@ -1,14 +1,23 @@
1
1
  {
2
2
  "name": "@nyx5/nyx5",
3
- "version": "0.1.0",
4
- "description": "Nyx5/1 — correo y libro para agentes de IA: direcciones agente@dominio, buzón store-and-forward, sobres firmados y cifrados, y un ledger de doble entrada con contratos (escrow, fianza, medido). Implementación de referencia, sin dependencias.",
3
+ "version": "0.2.0",
4
+ "description": "Nyx5/1 — mail and ledger for AI agents: agent@domain addresses, a store-and-forward mailbox, and a ledger where a claim costs something. One command to join, escrow released only against a deterministic check, and reputation that is just a query on the ledger. Zero dependencies.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
7
  "homepage": "https://nyx5.com",
8
- "repository": { "type": "git", "url": "git+https://github.com/Nicoiakl/nyx5.git" },
9
- "bugs": { "url": "https://github.com/Nicoiakl/nyx5/issues" },
10
- "engines": { "node": ">=20" },
11
- "bin": { "nyx5": "./bin/nyx5.js" },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Nicoiakl/nyx5.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/Nicoiakl/nyx5/issues"
14
+ },
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "bin": {
19
+ "nyx5": "bin/nyx5.js"
20
+ },
12
21
  "files": [
13
22
  "src/",
14
23
  "bin/",
@@ -24,15 +33,22 @@
24
33
  ],
25
34
  "keywords": [
26
35
  "agents",
36
+ "ai-agents",
27
37
  "agent-communication",
28
38
  "protocol",
29
- "messaging",
30
- "ledger",
39
+ "mcp",
40
+ "model-context-protocol",
41
+ "a2a",
31
42
  "escrow",
43
+ "reputation",
44
+ "agent-identity",
45
+ "ledger",
32
46
  "ed25519",
33
47
  "end-to-end-encryption",
34
48
  "cloudflare-workers",
35
- "mcp"
49
+ "x402",
50
+ "agent-payments",
51
+ "verification"
36
52
  ],
37
53
  "scripts": {
38
54
  "demo": "node demo/e2e.js",
@@ -45,8 +45,8 @@ export class Agent {
45
45
  // ---------- registro ----------
46
46
  // Tres caminos: adminToken (la casa inscribe), invite (código de la casa) o abierto si la casa lo permite.
47
47
  // Sin adminToken y sin estar registrado, el cuerpo va firmado con la propia clave (prueba de posesión).
48
- async register({ adminToken, invite, capabilities, inbox, webhook, valid_until } = {}) {
49
- const body = { local: this.local, sig: this.keys.sig, enc: this.keys.enc, capabilities, inbox, webhook, valid_until };
48
+ async register({ adminToken, invite, capabilities, inbox, webhook, notify_email, valid_until } = {}) {
49
+ const body = { local: this.local, sig: this.keys.sig, enc: this.keys.enc, capabilities, inbox, webhook, notify_email, valid_until };
50
50
  if (adminToken) this.card = await this._call('POST', '/agents', body, { admin: adminToken });
51
51
  else if (this.card) this.card = await this._call('POST', '/agents', body);
52
52
  else this.card = await this._call('POST', '/agents', signObject({ ...body, invite: invite || undefined, ts: iso() }, this.keys), { noAuth: true });
@@ -168,6 +168,15 @@ export class Agent {
168
168
  // Lecturas directas (sin pasar por correo) en la casa indicada; por defecto, la propia estafeta.
169
169
  async balance(house) { return this._callAt(house, 'GET', `/libro/cuenta/${encodeURIComponent(this.address)}`); }
170
170
  async contract(house, id) { return this._callAt(house, 'GET', `/libro/contrato/${encodeURIComponent(id)}`); }
171
+ // Historial público de cualquier agente (por defecto, el propio): la reputación es el libro.
172
+ async historial(address = this.address) {
173
+ const { local, domain } = parseAddress(address);
174
+ const base = domain === this.domain ? this.estafeta : (await this.resolver.domainCard(domain))._estafeta;
175
+ const res = await this.fetch(`${base}/agents/${encodeURIComponent(local)}/historial`, { signal: AbortSignal.timeout(10_000) });
176
+ const json = await res.json().catch(() => ({}));
177
+ if (!res.ok) throw Object.assign(new Error(json.reason || `HTTP ${res.status}`), { status: res.status });
178
+ return json;
179
+ }
171
180
  async _callAt(house, method, path) {
172
181
  if (!house || house === this.domain) return this._call(method, path);
173
182
  const dc = await this.resolver.domainCard(house);