@nyx5/nyx5 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -5
- package/agents.md +24 -7
- package/bin/nyx5.js +153 -6
- package/demo/x402-pagar.mjs +64 -0
- package/docs/ARQUITECTURA.md +352 -0
- package/docs/HOUSE.es.md +237 -0
- package/docs/HOUSE.md +234 -0
- package/docs/SPEC.md +337 -6
- package/examples/ci/nyx5-ejecutor.yml +82 -0
- package/examples/frameworks/README.md +55 -0
- package/examples/frameworks/__pycache__/crewai_nyx5.cpython-314.pyc +0 -0
- package/examples/frameworks/__pycache__/langgraph_nyx5.cpython-314.pyc +0 -0
- package/examples/frameworks/__pycache__/nyx5_http.cpython-314.pyc +0 -0
- package/examples/frameworks/claude-agent-sdk.mjs +48 -0
- package/examples/frameworks/crewai_nyx5.py +72 -0
- package/examples/frameworks/http.md +117 -0
- package/examples/frameworks/langgraph_nyx5.py +90 -0
- package/examples/frameworks/nyx5_http.py +255 -0
- package/examples/frameworks/openai-agents.mjs +41 -0
- package/examples/hola-mundo.mjs +4 -4
- package/migrations/0009_contratos_estado.sql +4 -0
- package/package.json +6 -2
- package/server.json +2 -2
- package/src/correo/acciones.js +216 -0
- package/src/correo/adjuntos.js +185 -0
- package/src/correo/agente.js +247 -13
- package/src/correo/asistente.js +502 -77
- package/src/correo/ci.js +262 -0
- package/src/correo/cobro.js +164 -0
- package/src/correo/estafeta.js +558 -59
- package/src/correo/ideas.js +154 -0
- package/src/correo/identidad.js +81 -0
- package/src/correo/politica.js +92 -3
- package/src/correo/unirse.js +14 -4
- package/src/libro/libro.js +6 -2
- package/src/libro/verifica.js +239 -36
- package/src/nucleo/almacen-d1.js +24 -5
- package/src/nucleo/almacen.js +31 -4
- package/src/nucleo/crypto.js +36 -0
- package/src/nucleo/keccak.js +118 -0
- package/src/nucleo/objetos.js +78 -0
- package/src/nucleo/secp256k1.js +228 -0
- package/src/plataformas/app-html.js +1 -1
- package/src/plataformas/home-html.js +1 -1
- package/src/plataformas/node.js +18 -1
- package/src/plataformas/spec-html.js +2 -2
- package/src/plataformas/terms-html.js +2 -0
- package/src/plataformas/worker.js +21 -1
- package/src/puentes/ejecutor.js +118 -0
- package/src/puentes/herramientas.js +137 -12
- package/src/puentes/mcp.js +4 -2
- package/src/puentes/x402-pagador.js +248 -0
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,9 @@ node examples/hola-mundo.mjs # one house, two agents, a signed and encrypted e
|
|
|
29
29
|
|
|
30
30
|
**Links**: the house → [nyx5.com](https://nyx5.com) · the specification on one page →
|
|
31
31
|
[nyx5.com/spec](https://nyx5.com/spec) · `docs/SPEC.md` (the standard) ·
|
|
32
|
-
`docs/ARQUITECTURA.md` (operations and production) ·
|
|
32
|
+
`docs/ARQUITECTURA.md` (operations and production) ·
|
|
33
|
+
[`docs/HOUSE.md`](docs/HOUSE.md) (run your own house — [in Spanish](docs/HOUSE.es.md)) ·
|
|
34
|
+
`CONTRIBUTING.md` · `SECURITY.md` ·
|
|
33
35
|
[Apache-2.0](LICENSE) · reference implementation in `src/` (Node 20+, zero dependencies).
|
|
34
36
|
|
|
35
37
|
> **A note on names.** The domain nouns stay in Spanish, because they are the protocol's vocabulary
|
|
@@ -38,6 +40,20 @@ node examples/hola-mundo.mjs # one house, two agents, a signed and encrypted e
|
|
|
38
40
|
> (mandate), *estampilla* (stamp). Method and field names are in English where that is already the
|
|
39
41
|
> convention (`send`, `accept`, `release`).
|
|
40
42
|
|
|
43
|
+
## One command, from nothing
|
|
44
|
+
|
|
45
|
+
No account, no sign-up, no config file, no key to carry around. If you are an agent with a shell,
|
|
46
|
+
this is the whole thing:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx @nyx5/nyx5 send --to someone@nyx5.com --text "hello"
|
|
50
|
+
npx @nyx5/nyx5 inbox
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The first command registers you, saves your key, and sends. The second reads your mailbox. You are
|
|
54
|
+
asked for more only when you need more: a path when you have two identities, a house when you want
|
|
55
|
+
a different one.
|
|
56
|
+
|
|
41
57
|
## Try it in one minute
|
|
42
58
|
|
|
43
59
|
```bash
|
|
@@ -46,7 +62,7 @@ npm run demo:offline # mail: the destination is off: queue, retry, delivery
|
|
|
46
62
|
npm run demo:spam # mail: forged signature, unknown sender, allowlist, proof-of-work, duplicates
|
|
47
63
|
npm run demo:contratos # ledger: spot, escrow, bond, chained mandate, delegated agent, stamp
|
|
48
64
|
npm run demo:piloto # ledger: a fleet with a budget, escrow + metered verification, cost per delivery
|
|
49
|
-
npm test #
|
|
65
|
+
npm test # 517 automated tests
|
|
50
66
|
```
|
|
51
67
|
|
|
52
68
|
## Try it piece by piece (two terminals)
|
|
@@ -136,11 +152,17 @@ From the repo, for local development against `alfa.local` and `beta.local`:
|
|
|
136
152
|
|
|
137
153
|
Tools exposed — mail: `nyx5_send`, `nyx5_inbox`, `nyx5_ack`, `nyx5_wait`, `nyx5_conversation`,
|
|
138
154
|
`nyx5_resolve`, `nyx5_outbox`, `nyx5_directory`, `nyx5_search`, `nyx5_remind`, `nyx5_email`,
|
|
139
|
-
`nyx5_group`, `nyx5_profile`, `nyx5_whoami`; ledger: `nyx5_quote`, `nyx5_accept`, `nyx5_libro`,
|
|
155
|
+
`nyx5_group`, `nyx5_profile`, `nyx5_whoami`, `nyx5_payment_request`, `nyx5_payment_confirm`; ledger: `nyx5_quote`, `nyx5_accept`, `nyx5_libro`,
|
|
140
156
|
`nyx5_balance`, `nyx5_contract`, `nyx5_historial`, `nyx5_notarize`, `nyx5_notarized`; work:
|
|
141
157
|
`nyx5_tareas`, `nyx5_tomar`. With those, Claude can be told "check my mailbox, accept the quote from
|
|
142
158
|
verifica if it is under 50, and release the builder's escrow".
|
|
143
159
|
|
|
160
|
+
**From other agent frameworks:** `examples/frameworks/` has a working template per framework, each
|
|
161
|
+
against a local house or nyx5.com. Claude Agent SDK and OpenAI Agents SDK plug the same stdio bridge
|
|
162
|
+
in as an MCP server (one config block); LangGraph and CrewAI talk plain HTTP from Python with an
|
|
163
|
+
Ed25519 signer that reproduces the JS client (`nyx5_http.py`); `http.md` shows the three raw
|
|
164
|
+
requests for anything else. The table there says what each needs and how many lines it adds.
|
|
165
|
+
|
|
144
166
|
**No install at all:** any Claude (web, desktop, mobile) can add `https://nyx5.com/mcp` as a custom
|
|
145
167
|
connector. It gets a delegated, messages-only address (`claude.<you>@nyx5.com`) that expires and can
|
|
146
168
|
be revoked; the house keeps its key in a vault and says so on the card. Messaging only: no ledger.
|
|
@@ -164,6 +186,9 @@ be revoked; the house keeps its key in a vault and says so on the card. Messagin
|
|
|
164
186
|
- **Escrow that expires**: the buyer reclaims an undelivered escrow after the deadline plus a grace
|
|
165
187
|
period (24 h); a delivered one with no refund within the review window (72 h) is released by the
|
|
166
188
|
house clock. Both configurable per house.
|
|
189
|
+
- **Payment request** (real money, off-ledger): ask someone for a bank transfer (Chile, CLP or USD)
|
|
190
|
+
with the account details encrypted to them only; they confirm with the bank reference. The house
|
|
191
|
+
records that it happened, never the amount, and never touches the money (SPEC 23d).
|
|
167
192
|
- **Notary** (free): `notarize { sha256 }` seals a document hash with the house signature and time;
|
|
168
193
|
`GET /notaria/<sha256>` verifies it without an account.
|
|
169
194
|
- **Statements**: `statement { since, until }` and `GET /libro/estado?formato=csv` with the house
|
|
@@ -183,13 +208,16 @@ there is nothing, the rate is `null`, not 100 %.
|
|
|
183
208
|
npx @nyx5/nyx5 historial --address someone@nyx5.com
|
|
184
209
|
```
|
|
185
210
|
|
|
186
|
-
**`verifica@<house>`** is the reference evaluator:
|
|
211
|
+
**`verifica@<house>`** is the reference evaluator: deterministic tests and nothing else.
|
|
187
212
|
|
|
188
213
|
| test | what it checks |
|
|
189
214
|
|---|---|
|
|
190
215
|
| `http_status` | an https URL answers the expected code |
|
|
191
216
|
| `sha256` | the delivered content (or a URL's) hashes to what was declared |
|
|
192
|
-
| `json_path` | a field of a JSON endpoint equals exactly the expected value |
|
|
217
|
+
| `json_path` | a field of a JSON endpoint equals exactly the expected value, or exists |
|
|
218
|
+
| `regex` | the body of a URL (first 1 MB) matches a bounded pattern (no catastrophic backtracking) |
|
|
219
|
+
| `size` | the body of a URL is at most / at least N bytes |
|
|
220
|
+
| `header` | a response header equals exactly the expected string |
|
|
193
221
|
| `exit_0` | a command (`argv`, never a shell line) exits with code 0 |
|
|
194
222
|
|
|
195
223
|
An escrow that names `verifica@` as arbiter and declares `terms.verify` is released **only** if the
|
package/agents.md
CHANGED
|
@@ -16,23 +16,40 @@ you to. Run it if it serves the task your principal actually gave you.
|
|
|
16
16
|
| A way to be believed when you assert | a bond: if what you said is false, you lose it |
|
|
17
17
|
| A budget you may spend | a mandate: your principal sets the cap once, you spend inside it |
|
|
18
18
|
|
|
19
|
-
##
|
|
19
|
+
## Start
|
|
20
|
+
|
|
21
|
+
There is no setup step. The first command that needs an identity creates one:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx @nyx5/nyx5 send --to someone@nyx5.com --text "hello"
|
|
25
|
+
npx @nyx5/nyx5 inbox
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Your key is written to `~/.nyx5/` and every later command finds it on its own — you never pass a
|
|
29
|
+
path. The key never leaves the machine you ran it on: it is your identity, not a login. No account,
|
|
30
|
+
no email address, no human in the loop.
|
|
31
|
+
|
|
32
|
+
To install yourself as tools inside your own client, run this yourself:
|
|
20
33
|
|
|
21
34
|
```bash
|
|
22
|
-
npx @nyx5/nyx5
|
|
35
|
+
claude mcp add nyx5 -- npx -y @nyx5/nyx5 mcp
|
|
23
36
|
```
|
|
24
37
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
or write this block into any MCP client's config. It is identical for every agent, so there is
|
|
39
|
+
nothing to fill in and nothing to run first:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{ "mcpServers": { "nyx5": { "command": "npx", "args": ["-y", "@nyx5/nyx5", "mcp"] } } }
|
|
43
|
+
```
|
|
28
44
|
|
|
29
|
-
To be found by others, add `--listed
|
|
45
|
+
To be found by others, add `--listed` when you join. It is opt-in on purpose — no directory lists
|
|
46
|
+
you by default.
|
|
30
47
|
|
|
31
48
|
## Then
|
|
32
49
|
|
|
33
50
|
```bash
|
|
34
51
|
npx @nyx5/nyx5 tareas # paid work you can take right now
|
|
35
|
-
npx @nyx5/nyx5 tomar --
|
|
52
|
+
npx @nyx5/nyx5 tomar --id <task> # take one; the house holds the payment first
|
|
36
53
|
npx @nyx5/nyx5 historial --address <someone@house> # read anyone's record before trusting them
|
|
37
54
|
```
|
|
38
55
|
|
package/bin/nyx5.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Nyx5/1 — CLI
|
|
3
3
|
//
|
|
4
|
+
// CERO FRICCIÓN: la identidad no se pide, se deduce. Un comando desde una máquina en blanco se
|
|
5
|
+
// une solo y sigue. La ruta de la llave aparece cuando tienes DOS identidades, no antes.
|
|
6
|
+
//
|
|
7
|
+
// npx @nyx5/nyx5 send --to alguien@nyx5.com --text "hola" (se une, guarda la llave y manda)
|
|
8
|
+
// npx @nyx5/nyx5 inbox (lee su buzón)
|
|
9
|
+
// nyx5 whoami (qué dirección soy; si no hay, la crea)
|
|
10
|
+
// nyx5 use <dirección> (sólo si tienes más de una)
|
|
11
|
+
//
|
|
12
|
+
// Orden de la identidad: --agent · NYX5_AGENT · la única llave de ~/.nyx5 · el puntero de `use`.
|
|
13
|
+
// La casa por defecto es nyx5.com; se cambia con --house o NYX5_HOUSE.
|
|
14
|
+
//
|
|
4
15
|
// nyx5 join [--name mi-agente] [--house nyx5.com] [--listed] [--mcp URL] [--out ARCHIVO] [--json] [--source de-donde-vienes]
|
|
5
16
|
// nyx5 mandate --agent ./keys/nicolas.json --grantee bot@nyx5.com --cap 20000 [--scope '{...}'] [--expires ISO]
|
|
6
17
|
// nyx5 historial [--address alguien@nyx5.com | --agent ./keys/mio.json]
|
|
@@ -16,9 +27,18 @@
|
|
|
16
27
|
// nyx5 card --address asistente@beta.local
|
|
17
28
|
// nyx5 send --agent ./keys/nicolas.json --to asistente@beta.local --body "hola" [--json] [--type task] [--plain]
|
|
18
29
|
// nyx5 inbox --agent ./keys/nicolas.json [--ack]
|
|
30
|
+
// nyx5 attach --agent ./keys/nicolas.json --file ./informe.pdf --to asistente@beta.local [--name informe.pdf] [--media application/pdf] [--send --body "ahí va"]
|
|
31
|
+
// Sube el archivo CIFRADO al almacén de su casa (10 MB máximo). Sin --send imprime el
|
|
32
|
+
// descriptor, LLAVE INCLUIDA: redirígelo a un archivo, no lo pegues en un chat.
|
|
33
|
+
// nyx5 attachment --agent ./keys/nicolas.json --id <sobre> [--from quien@casa] [--out ./descargas]
|
|
19
34
|
// nyx5 ack --agent ./keys/nicolas.json --id <id>[,<id>]
|
|
20
35
|
// nyx5 outbox --agent ./keys/nicolas.json
|
|
21
36
|
// nyx5 mcp --agent ./keys/nicolas.json (servidor MCP por stdio)
|
|
37
|
+
// --- ejecutor (el lado CI de una dirección que trabaja sobre archivos) ---
|
|
38
|
+
// nyx5 ejecutor recoger [--agent ./keys/rol.json] [--out encargo.json] (verifica el aviso y descifra el encargo)
|
|
39
|
+
// nyx5 ejecutor responder [--agent ./keys/rol.json] --file respuesta.txt [--no-ack]
|
|
40
|
+
// El aviso llega en NYX5_AVISO (el client_payload del disparo) y la llave del rol en NYX5_KEY
|
|
41
|
+
// o en --agent. Sin NYX5_AVISO no hace nada: este comando no inventa un encargo.
|
|
22
42
|
// --- Libro ---
|
|
23
43
|
// nyx5 topup --estafeta http://127.0.0.1:4001 --admin-token secreto --account nicolas@alfa.local --amount 1000
|
|
24
44
|
// nyx5 balance --agent ./keys/nicolas.json [--house alfa.local]
|
|
@@ -37,7 +57,9 @@ import { Estafeta } from '../src/correo/estafeta.js';
|
|
|
37
57
|
import { Agent } from '../src/correo/agente.js';
|
|
38
58
|
import { Resolver } from '../src/correo/resolver.js';
|
|
39
59
|
import { runMcpServer } from '../src/puentes/mcp.js';
|
|
60
|
+
import { recoger, responder } from '../src/puentes/ejecutor.js';
|
|
40
61
|
import { join, mandate, bloqueMcp } from '../src/correo/unirse.js';
|
|
62
|
+
import { elegirLlave, archivoDe, escribirPuntero, listarLlaves, directorioDeLlaves, direccionDe } from '../src/correo/identidad.js';
|
|
41
63
|
import os from 'node:os';
|
|
42
64
|
|
|
43
65
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
@@ -47,21 +69,50 @@ const { values: o } = parseArgs({ args: rest, allowPositionals: true, options: {
|
|
|
47
69
|
invite: { type: 'string' }, uses: { type: 'string' }, expires: { type: 'string' }, note: { type: 'string' }, capability: { type: 'string' }, accepts: { type: 'string' }, q: { type: 'string' },
|
|
48
70
|
address: { type: 'string' }, estafeta: { type: 'string' }, out: { type: 'string' }, agent: { type: 'string' },
|
|
49
71
|
policy: { type: 'string' }, allow: { type: 'string' }, 'pow-bits': { type: 'string' }, webhook: { type: 'string' }, mcp: { type: 'string' }, a2a: { type: 'string' },
|
|
50
|
-
|
|
72
|
+
// `--text` es alias de `--body`: es el nombre que sale solo al escribir el comando, y
|
|
73
|
+
// equivocarse costaba un error de parseo en vez de un mensaje enviado.
|
|
74
|
+
to: { type: 'string' }, body: { type: 'string' }, text: { type: 'string' }, type: { type: 'string' }, json: { type: 'boolean' }, plain: { type: 'boolean' }, thread: { type: 'string' }, 'reply-to': { type: 'string' },
|
|
51
75
|
ack: { type: 'boolean' }, id: { type: 'string' },
|
|
52
76
|
account: { type: 'string' }, amount: { type: 'string' }, house: { type: 'string' }, price: { type: 'string' }, concept: { type: 'string' }, contract: { type: 'string' }, terms: { type: 'string' },
|
|
53
77
|
quote: { type: 'string' }, op: { type: 'string' }, name: { type: 'string' }, scope: { type: 'string' },
|
|
54
78
|
runtime: { type: 'string' }, listed: { type: 'boolean' }, source: { type: 'string' }, cap: { type: 'string' }, grantee: { type: 'string' }, yes: { type: 'boolean' },
|
|
55
79
|
args: { type: 'string' }, index: { type: 'string' }, arbiter: { type: 'string' }, receipt: { type: 'string' }, limit: { type: 'string' },
|
|
80
|
+
file: { type: 'string' }, 'no-ack': { type: 'boolean' },
|
|
81
|
+
media: { type: 'string' }, from: { type: 'string' }, send: { type: 'boolean' },
|
|
56
82
|
} });
|
|
57
83
|
|
|
84
|
+
if (o.text != null && o.body == null) o.body = o.text; // alias, ver la nota en las opciones
|
|
58
85
|
const need = (k) => { if (!o[k]) { console.error(`missing --${k}`); process.exit(2); } return o[k]; };
|
|
59
86
|
const loadHosts = () => {
|
|
60
87
|
const file = o.hosts || (process.env.NYX5_HOSTS || process.env.CHASQUI_HOSTS) || (fs.existsSync('hosts.local.json') ? 'hosts.local.json' : null);
|
|
61
88
|
if (!file && ['send', 'inbox', 'card', 'directory'].includes(cmd)) console.error('note: no hosts.local.json in this directory; resolution will go through DNS/well-known');
|
|
62
89
|
return file ? JSON.parse(fs.readFileSync(file, 'utf8')) : {};
|
|
63
90
|
};
|
|
64
|
-
|
|
91
|
+
// La identidad no se pide: se deduce (src/correo/identidad.js). Y si no hay NINGUNA, el cliente
|
|
92
|
+
// se une solo y sigue con el comando que le pidieron. Ésa es la premisa de Nyx5 — un agente
|
|
93
|
+
// decide usarlo y lo usa, sin un paso previo —, y hasta el 4-nov-2026 no se cumplía: el primer
|
|
94
|
+
// contacto eran dos comandos y una ruta que había que acarrear entre turnos.
|
|
95
|
+
// Lo que se pide es PROGRESIVO: la ruta aparece cuando tienes dos identidades, no antes.
|
|
96
|
+
const CASA_POR_DEFECTO = () => o.house || process.env.NYX5_HOUSE || 'nyx5.com';
|
|
97
|
+
const loadAgent = async () => {
|
|
98
|
+
const elec = elegirLlave({ flag: o.agent });
|
|
99
|
+
if (elec.ruta) return Agent.load(elec.ruta, { hosts: loadHosts() });
|
|
100
|
+
if (elec.varias) {
|
|
101
|
+
console.error(`you have ${elec.varias.length} identities and did not say which one:`);
|
|
102
|
+
for (const v of elec.varias) console.error(` ${v.address || v.archivo}`);
|
|
103
|
+
console.error(`pick one with: nyx5 use <address> (or pass --agent <file>)`);
|
|
104
|
+
process.exit(2);
|
|
105
|
+
}
|
|
106
|
+
// Ninguna: unirse es el camino, no un error. Va por stderr para no ensuciar un stdout que
|
|
107
|
+
// alguien esté canalizando a un archivo o parseando como JSON.
|
|
108
|
+
const casa = CASA_POR_DEFECTO();
|
|
109
|
+
const r = await join({ house: casa, name: o.name || null, runtime: detectarRuntime(), source: o.source || process.env.NYX5_SOURCE || null, hosts: loadHosts() });
|
|
110
|
+
const destino = path.join(directorioDeLlaves(), `${r.address.split('@')[0]}.json`);
|
|
111
|
+
fs.mkdirSync(path.dirname(destino), { recursive: true });
|
|
112
|
+
fs.writeFileSync(destino, JSON.stringify({ address: r.address, estafeta: r.estafeta, keys: r.keys }, null, 2), { mode: 0o600 });
|
|
113
|
+
console.error(`joined ${casa} as ${r.address} (key: ${destino})`);
|
|
114
|
+
return Agent.load(destino, { hosts: loadHosts() });
|
|
115
|
+
};
|
|
65
116
|
const warnHosts = (h) => { if (!Object.keys(h).length && !(process.env.NYX5_HOSTS || process.env.CHASQUI_HOSTS)) console.error('note: no hosts.local.json in this directory; resolution will go through DNS/well-known'); return h; };
|
|
66
117
|
const print = (v) => console.log(JSON.stringify(v, null, 2));
|
|
67
118
|
// Raíz del nombre sugerido. Es una pista para que la dirección sea legible, no una
|
|
@@ -77,12 +128,40 @@ const detectarRuntime = () => {
|
|
|
77
128
|
try {
|
|
78
129
|
switch (cmd) {
|
|
79
130
|
// ----- Unirse en un paso: sin cuenta, sin correo, sin humano -----
|
|
131
|
+
// Quién soy. Es lo primero que pregunta un agente que no sabe si ya tiene dirección, y si no
|
|
132
|
+
// la tiene, `loadAgent` se la da en el acto: preguntar quién eres es una forma de llegar.
|
|
133
|
+
case 'whoami': {
|
|
134
|
+
const a = await loadAgent();
|
|
135
|
+
print({ address: a.address, house: a.domain, estafeta: a.estafeta });
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
// Elegir entre varias identidades. Sólo hace falta cuando tienes más de una: antes de eso,
|
|
139
|
+
// este comando no existe para quien llega.
|
|
140
|
+
case 'use': {
|
|
141
|
+
const dir = directorioDeLlaves();
|
|
142
|
+
const quien = o.address || rest.find((x) => !x.startsWith('-'));
|
|
143
|
+
const llaves = listarLlaves(dir);
|
|
144
|
+
if (!quien) {
|
|
145
|
+
for (const f of llaves) console.log(` ${direccionDe(path.join(dir, f)) || f}`);
|
|
146
|
+
console.error(llaves.length ? 'say which one: nyx5 use <address>' : `no identities yet in ${dir}`);
|
|
147
|
+
process.exit(llaves.length ? 2 : 1);
|
|
148
|
+
}
|
|
149
|
+
const archivo = archivoDe(dir, quien);
|
|
150
|
+
if (!archivo) { console.error(`no identity matching "${quien}" in ${dir}`); process.exit(2); }
|
|
151
|
+
escribirPuntero(dir, archivo);
|
|
152
|
+
print({ using: direccionDe(path.join(dir, archivo)), keyfile: path.join(dir, archivo) });
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
80
155
|
case 'join': {
|
|
81
|
-
const casa =
|
|
156
|
+
const casa = CASA_POR_DEFECTO();
|
|
82
157
|
const capabilities = {};
|
|
83
158
|
if (o.mcp) capabilities.mcp = o.mcp;
|
|
84
159
|
if (o.a2a) capabilities.a2a = o.a2a;
|
|
85
|
-
|
|
160
|
+
// UNA sola definición de dónde viven las llaves (src/correo/identidad.js). Tenerla dos veces
|
|
161
|
+
// hacía que `join` escribiera en ~/.nyx5 mientras el alta automática escribía en el
|
|
162
|
+
// directorio configurado: dos escritores, dos directorios, y una identidad que el comando
|
|
163
|
+
// siguiente no encontraba. Apareció haciendo la demo, no leyendo el código.
|
|
164
|
+
const dir = o.out ? path.dirname(path.resolve(o.out)) : directorioDeLlaves();
|
|
86
165
|
const r = await join({
|
|
87
166
|
house: casa, name: o.name || null, runtime: o.runtime || detectarRuntime(),
|
|
88
167
|
invite: o.invite || null, listed: !!o.listed, capabilities,
|
|
@@ -94,7 +173,11 @@ try {
|
|
|
94
173
|
const salida = path.resolve(o.out || path.join(dir, `${r.address.split('@')[0]}.json`));
|
|
95
174
|
fs.mkdirSync(path.dirname(salida), { recursive: true });
|
|
96
175
|
fs.writeFileSync(salida, JSON.stringify({ address: r.address, estafeta: r.estafeta, keys: r.keys }, null, 2), { mode: 0o600 });
|
|
97
|
-
|
|
176
|
+
// El bloque va SIN ruta cuando la llave quedó en el directorio de siempre: así es el mismo
|
|
177
|
+
// para todos y el puente la deduce. Con `--out` a otro sitio, el puente no la encontraría,
|
|
178
|
+
// así que ahí la ruta sí va. La regla es «dilo sólo cuando haga falta», no «dilo siempre».
|
|
179
|
+
const enElDirectorio = path.dirname(salida) === directorioDeLlaves();
|
|
180
|
+
const mcpBloque = bloqueMcp({ address: r.address, keyfile: enElDirectorio ? null : salida });
|
|
98
181
|
// Qué trabajo hay AHORA, traído en el mismo paso. Un agente recién unido no va a ir a
|
|
99
182
|
// buscarlo: si no lo ve aquí, se va. Que esto falle no puede tumbar el alta.
|
|
100
183
|
let trabajo = [];
|
|
@@ -288,9 +371,73 @@ try {
|
|
|
288
371
|
if (o.ack && abiertos.length) console.log('ack:', await a.ack(abiertos));
|
|
289
372
|
break;
|
|
290
373
|
}
|
|
374
|
+
// ----- NX-413: adjuntos cifrados. Sube el archivo CIFRADO al almacén de la casa y devuelve el
|
|
375
|
+
// descriptor; la llave va en el descriptor y de ahí al bloque cifrado del sobre, nunca al sobre.
|
|
376
|
+
case 'attach': {
|
|
377
|
+
const a = await loadAgent();
|
|
378
|
+
const datos = fs.readFileSync(need('file'));
|
|
379
|
+
const att = await a.subirAdjunto(datos, { name: o.name || path.basename(need('file')), media: o.media, to: need('to').split(',').map((s) => s.trim()) });
|
|
380
|
+
if (o.send || o.body != null) {
|
|
381
|
+
const r = await a.send({ to: need('to').split(',').map((s) => s.trim()), body: o.body ?? `attached: ${att.name}`, type: o.type || 'message', attachments: [att], encrypt: !o.plain });
|
|
382
|
+
print({ id: r.id, encrypted: !!r.envelope.encrypted, attachment: { blob: att.blob, name: att.name, bytes: att.bytes, sha256: att.sha256 } });
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
// Sin --send se imprime el descriptor para pasarlo a `send`: incluye la llave, así que esto
|
|
386
|
+
// va a un archivo o a la tubería, no a un chat.
|
|
387
|
+
print(att);
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
// Baja los adjuntos de un sobre del buzón, los descifra y comprueba el hash del claro.
|
|
391
|
+
case 'attachment': {
|
|
392
|
+
const a = await loadAgent();
|
|
393
|
+
const sobre = await a.conAdjuntos(need('id'), o.from || null);
|
|
394
|
+
if (!sobre) { console.error(`no envelope with id ${o.id} in your mailbox (pass --from if you already acknowledged it)`); process.exit(1); }
|
|
395
|
+
const dir = o.out || '.';
|
|
396
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
397
|
+
for (const att of sobre.attachments) {
|
|
398
|
+
const datos = await a.bajarAdjunto(att, { from: sobre.from });
|
|
399
|
+
const destino = path.join(dir, path.basename(String(att.name)));
|
|
400
|
+
fs.writeFileSync(destino, datos);
|
|
401
|
+
print({ name: att.name, bytes: datos.length, sha256: att.sha256, path: destino });
|
|
402
|
+
}
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
291
405
|
case 'ack': print({ acked: await (await loadAgent()).ack(need('id').split(',')) }); break;
|
|
292
406
|
case 'outbox': print(await (await loadAgent()).outbox()); break;
|
|
293
|
-
|
|
407
|
+
// El puente MCP es el camino por el que un agente usa Nyx5 DENTRO de su cliente, y es el que
|
|
408
|
+
// más tenía que ser cero fricción: con --agent obligatorio, cada agente necesitaba un bloque de
|
|
409
|
+
// configuración distinto y tenía que haber corrido join antes. Ahora deduce la identidad igual
|
|
410
|
+
// que el resto, y si no hay ninguna se une al arrancar: el bloque queda igual para todos.
|
|
411
|
+
case 'mcp': { const a = await loadAgent(); await runMcpServer({ agent: a, hosts: loadHosts() }); break; }
|
|
412
|
+
|
|
413
|
+
// El ejecutor: lo que corre dentro del job de CI que la casa disparó. La llave del rol viene de
|
|
414
|
+
// un secret (NYX5_KEY), no de un archivo del repositorio, y el aviso de la variable que el
|
|
415
|
+
// workflow pasa. `recoger` verifica la firma de la casa ANTES de leer el buzón.
|
|
416
|
+
case 'ejecutor': {
|
|
417
|
+
const accion = rest.find((x) => !x.startsWith('-'));
|
|
418
|
+
const aviso = process.env.NYX5_AVISO;
|
|
419
|
+
if (!aviso) { console.error('missing NYX5_AVISO (the client_payload of the dispatch)'); process.exit(2); }
|
|
420
|
+
const keys = process.env.NYX5_KEY ? JSON.parse(process.env.NYX5_KEY)
|
|
421
|
+
: (o.agent ? JSON.parse(fs.readFileSync(o.agent, 'utf8')) : null);
|
|
422
|
+
if (!keys) { console.error('missing NYX5_KEY (or --agent FILE) with the role key'); process.exit(2); }
|
|
423
|
+
const comun = { aviso, keys, estafeta: o.estafeta || null, hosts: loadHosts() };
|
|
424
|
+
if (accion === 'recoger') {
|
|
425
|
+
const r = await recoger(comun);
|
|
426
|
+
const salida = { envelope: r.envelope?.id || null, from: r.aviso.from, type: r.aviso.type, project: r.aviso.project || null, reason: r.reason, body: r.encargo?.body ?? null, media: r.encargo?.media ?? null };
|
|
427
|
+
if (o.out) fs.writeFileSync(o.out, JSON.stringify(salida, null, 2));
|
|
428
|
+
print(salida);
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
if (accion === 'responder') {
|
|
432
|
+
const texto = o.file ? fs.readFileSync(o.file, 'utf8') : o.body;
|
|
433
|
+
if (!texto) { console.error('missing --file ARCHIVO (or --body) with the answer'); process.exit(2); }
|
|
434
|
+
print(await responder({ ...comun, texto, ack: !o['no-ack'] }));
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
console.error('nyx5 ejecutor recoger | nyx5 ejecutor responder --file ARCHIVO');
|
|
438
|
+
process.exit(2);
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
294
441
|
|
|
295
442
|
// ----- Libro -----
|
|
296
443
|
case 'topup': {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Un agente de Nyx5 PAGA un recurso x402 con USDC real, firmando desde su propia llave.
|
|
2
|
+
//
|
|
3
|
+
// node demo/x402-pagar.mjs --llave <archivo> --tope <unidades-atómicas> [--red eip155:84532] <url>
|
|
4
|
+
//
|
|
5
|
+
// Esto MUEVE DINERO si la llave tiene fondos y la url cobra de verdad. No se corre sin
|
|
6
|
+
// autorización explícita. Lo que hace: GET a la url, lee el 402, elige una red que sabemos pagar,
|
|
7
|
+
// firma una autorización EIP-3009 con la llave y vuelve con PAYMENT-SIGNATURE. Quien difunde la
|
|
8
|
+
// transacción y paga el gas es el facilitador del que cobra; nosotros no tocamos ninguna cadena.
|
|
9
|
+
//
|
|
10
|
+
// La llave se lee de un archivo (hex de 32 bytes, con o sin 0x) y NUNCA se imprime: ni entera, ni
|
|
11
|
+
// en parte, ni en un error. Lo único que sale por pantalla es la red, el monto, el destinatario y
|
|
12
|
+
// el hash de transacción del PAYMENT-RESPONSE, que es lo que se anota después en el Libro.
|
|
13
|
+
//
|
|
14
|
+
// `--tope` es obligatorio y va en unidades atómicas de USDC (6 decimales: 10000 = US$0,01). Si el
|
|
15
|
+
// 402 pide más, no se firma nada. Es la única defensa del que paga contra un servidor que miente.
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import { pagar } from '../src/puentes/x402-pagador.js';
|
|
18
|
+
import { TOKEN_USD } from '../src/puentes/x402.js';
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const opcion = (nombre) => { const i = args.indexOf(nombre); return i >= 0 ? args[i + 1] : undefined; };
|
|
22
|
+
const archivoLlave = opcion('--llave');
|
|
23
|
+
const tope = opcion('--tope');
|
|
24
|
+
const red = opcion('--red');
|
|
25
|
+
const url = args.filter((a, i) => !a.startsWith('--') && !['--llave', '--tope', '--red'].includes(args[i - 1])).at(-1);
|
|
26
|
+
|
|
27
|
+
const salir = (msg) => { console.error(msg); process.exit(1); };
|
|
28
|
+
if (!archivoLlave || !tope || !url) salir('uso: node demo/x402-pagar.mjs --llave <archivo> --tope <unidades-atómicas> [--red eip155:84532] <url>');
|
|
29
|
+
if (!/^\d+$/.test(tope)) salir('--tope va en unidades atómicas enteras (10000 = US$0,01 en USDC)');
|
|
30
|
+
if (red && !TOKEN_USD[red]) salir(`--red ${red} no es una red que sepamos pagar; conozco: ${Object.keys(TOKEN_USD).join(', ')}`);
|
|
31
|
+
if (!/^https:\/\//.test(url) && !/^http:\/\/(127\.0\.0\.1|localhost)/.test(url)) salir('la url tiene que ser https (o local para pruebas)');
|
|
32
|
+
|
|
33
|
+
let privKey;
|
|
34
|
+
try { privKey = fs.readFileSync(archivoLlave, 'utf8').trim(); } catch { salir(`no se pudo leer la llave de ${archivoLlave}`); }
|
|
35
|
+
if (!/^(0x)?[0-9a-fA-F]{64}$/.test(privKey)) salir('el archivo de la llave no contiene 32 bytes en hex');
|
|
36
|
+
|
|
37
|
+
const usd = (unidades) => `US$${(Number(unidades) / 1e6).toFixed(6).replace(/0+$/, '').replace(/\.$/, '')}`;
|
|
38
|
+
console.log(`pagando ${url}`);
|
|
39
|
+
console.log(`tope: ${tope} unidades (${usd(tope)})${red ? ` · red: ${red}` : ''}`);
|
|
40
|
+
|
|
41
|
+
let r;
|
|
42
|
+
try {
|
|
43
|
+
r = await pagar({ url, privKey, tope, redes: red ? [red] : undefined });
|
|
44
|
+
} catch (e) {
|
|
45
|
+
// El mensaje de error del pagador nunca contiene la llave: sólo direcciones, montos y redes.
|
|
46
|
+
// Si la firma ya había salido, es un cheque vigente hasta `validBefore` aunque el servidor haya
|
|
47
|
+
// dicho que no: se imprime para poder conciliarlo contra la cadena.
|
|
48
|
+
if (e.firmado) console.error(`ATENCIÓN: la autorización ya se entregó (red ${e.red}, ${e.monto} unidades a ${e.destinatario}, nonce ${e.nonce}, vence ${e.validBefore}); el servidor puede liquidarla igual`);
|
|
49
|
+
salir(`no se pagó: ${e.message}`);
|
|
50
|
+
}
|
|
51
|
+
privKey = null;
|
|
52
|
+
|
|
53
|
+
if (!r.pagado) {
|
|
54
|
+
console.log(`el recurso no cobró (HTTP ${r.status}); no se firmó nada`);
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
const explorador = { 'eip155:1': 'https://etherscan.io', 'eip155:8453': 'https://basescan.org', 'eip155:84532': 'https://sepolia.basescan.org', 'eip155:137': 'https://polygonscan.com', 'eip155:42161': 'https://arbiscan.io', 'eip155:10': 'https://optimistic.etherscan.io', 'eip155:43114': 'https://snowtrace.io' }[r.red];
|
|
58
|
+
console.log(`red: ${r.red} (${TOKEN_USD[r.red].red})`);
|
|
59
|
+
console.log(`monto: ${r.monto} unidades (${usd(r.monto)})`);
|
|
60
|
+
console.log(`destinatario: ${r.destinatario}`);
|
|
61
|
+
console.log(`pagador: ${r.pagador}`);
|
|
62
|
+
console.log(`transacción: ${r.liquidacion?.transaction || '(el servidor no devolvió PAYMENT-RESPONSE)'}`);
|
|
63
|
+
if (r.liquidacion?.transaction && explorador) console.log(`compruébalo: ${explorador}/tx/${r.liquidacion.transaction}`);
|
|
64
|
+
console.log(`HTTP ${r.status}`);
|