@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 +3 -3
- package/README.md +70 -5
- package/SECURITY.md +1 -1
- package/bin/nyx5.js +128 -8
- package/demo/contratos.js +1 -1
- package/demo/e2e.js +1 -1
- package/demo/offline.js +1 -1
- package/demo/piloto-d4.mjs +1 -1
- package/demo/spam.js +1 -1
- package/examples/hola-mundo.mjs +1 -1
- package/migrations/0005_eventos.sql +16 -0
- package/package.json +25 -9
- package/src/correo/agente.js +11 -2
- package/src/correo/estafeta.js +179 -10
- package/src/correo/unirse.js +117 -0
- package/src/libro/contratos.js +23 -0
- package/src/libro/libro.js +66 -1
- package/src/libro/tareas.js +92 -0
- package/src/libro/verifica.js +113 -0
- package/src/nucleo/almacen-d1.js +9 -0
- package/src/nucleo/almacen.js +10 -1
- package/src/plataformas/app-html.js +1 -1
- package/src/plataformas/home-html.js +1 -1
- package/src/plataformas/worker.js +55 -15
- package/src/puentes/mcp.js +22 -0
package/CONTRIBUTING.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Cómo contribuir a
|
|
1
|
+
# Cómo contribuir a Nyx5
|
|
2
2
|
|
|
3
|
-
|
|
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:
|
|
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 #
|
|
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
|
-
"
|
|
111
|
+
"nyx5": {
|
|
87
112
|
"command": "node",
|
|
88
|
-
"args": ["/ruta/
|
|
89
|
-
"env": { "
|
|
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
|
-
|
|
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
|
|
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,
|
|
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(), '
|
|
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(), '
|
|
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(), '
|
|
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));
|
package/demo/piloto-d4.mjs
CHANGED
|
@@ -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(), '
|
|
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(), '
|
|
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}`);
|
package/examples/hola-mundo.mjs
CHANGED
|
@@ -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(), '
|
|
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.
|
|
4
|
-
"description": "Nyx5/1 —
|
|
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": {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
"
|
|
30
|
-
"
|
|
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
|
-
"
|
|
49
|
+
"x402",
|
|
50
|
+
"agent-payments",
|
|
51
|
+
"verification"
|
|
36
52
|
],
|
|
37
53
|
"scripts": {
|
|
38
54
|
"demo": "node demo/e2e.js",
|
package/src/correo/agente.js
CHANGED
|
@@ -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);
|