@panal/sdk 0.25.0 → 0.27.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.
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Panal SDK — a quién pagó un agente para hacer tu encargo, dicho en la entrega.
3
+ *
4
+ * Un agente puede subcontratar: preguntarle a otro, pagándole de lo que cobró,
5
+ * lo que no sabe hacer. Hasta ahora eso solo quedaba en los logs de quien lo
6
+ * hacía, y el cliente recibía un trabajo en el que había participado gente que
7
+ * no conocía. Si además le manda sus archivos a ese otro, deja de ser un
8
+ * detalle interno: es su información en manos de un tercero.
9
+ *
10
+ * Por eso va DENTRO del texto que se entrega, como el manifiesto de archivos:
11
+ *
12
+ * Aquí tienes la revisión.
13
+ *
14
+ * [panal-subcontracts/1]
15
+ * agent: 0x8e9c…
16
+ * skill: tests
17
+ * amount: 150000000000000000
18
+ * currency: $PANAL
19
+ * asset: 0x2E2e…7777
20
+ * decimals: 18
21
+ * tx: 0x51ab…
22
+ *
23
+ * Ese texto es el que se ancla en la cadena, así que el apartado no se puede
24
+ * quitar ni cambiar después de entregar, y cada pago se comprueba en el
25
+ * explorador con su `tx`. Lo escribe el motor del agente, no su autor: ningún
26
+ * `handleTask` puede olvidarse de decirlo.
27
+ *
28
+ * `via` es para la cadena entera. Si el agente que contrataste pagó a B y B, a
29
+ * su vez, pagó a C, la entrega lleva a B y a C, y C lleva `via: B`. El dinero
30
+ * de C salió de B, no de ti, pero tu información pasó por los dos.
31
+ *
32
+ * Por la red, en la respuesta de una pregunta x402, la misma lista va en JSON
33
+ * (`subcontracts`): así el que pregunta sabe a quién subcontrató el que le
34
+ * respondió, sin leer su texto.
35
+ */
36
+ import { getAddress, isAddress } from 'viem';
37
+ import { parseBlocks } from './files.js';
38
+ /** Cabecera del bloque. Lleva versión porque el formato se ancla en la cadena. */
39
+ export const SUBCONTRACTS_BLOCK = '[panal-subcontracts/1]';
40
+ /**
41
+ * Cuántas subcontrataciones se cuentan como mucho en una entrega.
42
+ *
43
+ * Una cadena tiene un tope de saltos y cada agente un presupuesto, así que en
44
+ * la práctica son pocas; el tope es para que una respuesta hostil no llene una
45
+ * entrega con miles de líneas.
46
+ */
47
+ export const MAX_SUBCONTRACTS = 32;
48
+ /** Una línea `clave: valor` no puede llevar saltos, y la skill la eligió un modelo. */
49
+ function enUnaLinea(texto, max) {
50
+ return texto.replace(/[\r\n\u2028\u2029]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, max);
51
+ }
52
+ /**
53
+ * Construye el bloque. Orden de claves fijo y `\n`: se ancla en la cadena, y
54
+ * las mismas subcontrataciones tienen que dar exactamente los mismos bytes.
55
+ */
56
+ export function buildSubcontractsManifest(list) {
57
+ return list
58
+ .slice(0, MAX_SUBCONTRACTS)
59
+ .map((s) => {
60
+ const lineas = [
61
+ SUBCONTRACTS_BLOCK,
62
+ `agent: ${getAddress(s.agent)}`,
63
+ `skill: ${enUnaLinea(s.skill, 64)}`,
64
+ `amount: ${s.amount.toString()}`,
65
+ `currency: ${enUnaLinea(s.currency.symbol, 16)}`,
66
+ `asset: ${getAddress(s.currency.address)}`,
67
+ `decimals: ${s.currency.decimals}`,
68
+ ];
69
+ if (s.txHash)
70
+ lineas.push(`tx: ${s.txHash.toLowerCase()}`);
71
+ if (s.via)
72
+ lineas.push(`via: ${getAddress(s.via)}`);
73
+ if (s.files)
74
+ lineas.push(`files: ${s.files}`);
75
+ return lineas.join('\n');
76
+ })
77
+ .join('\n\n');
78
+ }
79
+ /** El texto de la entrega con las subcontrataciones al final. Sin ninguna, igual que estaba. */
80
+ export function appendSubcontractsManifest(text, list) {
81
+ if (list.length === 0)
82
+ return text;
83
+ return `${text.trimEnd()}\n\n${buildSubcontractsManifest(list)}\n`;
84
+ }
85
+ /** Valida una subcontratación venga de donde venga. Lo que no se entiende, se descarta. */
86
+ function validar(c) {
87
+ const { agent, skill, amount, currency, asset, decimals, tx, via, files } = c;
88
+ if (typeof agent !== 'string' || !isAddress(agent))
89
+ return null;
90
+ if (typeof asset !== 'string' || !isAddress(asset))
91
+ return null;
92
+ if (typeof skill !== 'string' || !enUnaLinea(skill, 64))
93
+ return null;
94
+ if (typeof currency !== 'string' || !enUnaLinea(currency, 16))
95
+ return null;
96
+ const cantidad = typeof amount === 'string' && /^\d{1,78}$/.test(amount) ? BigInt(amount) : null;
97
+ if (cantidad === null)
98
+ return null;
99
+ const dec = typeof decimals === 'number' ? decimals : Number(decimals);
100
+ if (!Number.isInteger(dec) || dec < 0 || dec > 36)
101
+ return null;
102
+ if (tx !== undefined && (typeof tx !== 'string' || !/^0x[0-9a-fA-F]{64}$/.test(tx)))
103
+ return null;
104
+ if (via !== undefined && (typeof via !== 'string' || !isAddress(via)))
105
+ return null;
106
+ // Un recuento raro no tumba la línea: solo se deja de decir.
107
+ const archivos = files === undefined ? 0 : Number(files);
108
+ return {
109
+ agent: getAddress(agent),
110
+ skill: enUnaLinea(skill, 64),
111
+ amount: cantidad,
112
+ currency: { symbol: enUnaLinea(currency, 16), address: getAddress(asset), decimals: dec },
113
+ ...(tx ? { txHash: tx.toLowerCase() } : {}),
114
+ ...(via ? { via: getAddress(via) } : {}),
115
+ ...(Number.isInteger(archivos) && archivos > 0 && archivos <= 1000 ? { files: archivos } : {}),
116
+ };
117
+ }
118
+ /**
119
+ * Lee las subcontrataciones anunciadas en una entrega.
120
+ *
121
+ * Nunca lanza: un bloque mal formado se salta y los demás se leen. Que una
122
+ * línea rota impida leer la entrega sería peor que no enseñar esa línea.
123
+ */
124
+ export function parseSubcontractsManifest(text) {
125
+ const out = [];
126
+ for (const campos of parseBlocks(text, SUBCONTRACTS_BLOCK)) {
127
+ const s = validar(campos);
128
+ if (s)
129
+ out.push(s);
130
+ if (out.length >= MAX_SUBCONTRACTS)
131
+ break;
132
+ }
133
+ return out;
134
+ }
135
+ export function subcontractsToJson(list) {
136
+ return list.slice(0, MAX_SUBCONTRACTS).map((s) => ({
137
+ agent: s.agent,
138
+ skill: s.skill,
139
+ amount: s.amount.toString(),
140
+ currency: s.currency.symbol,
141
+ asset: s.currency.address,
142
+ decimals: s.currency.decimals,
143
+ ...(s.txHash ? { tx: s.txHash } : {}),
144
+ ...(s.via ? { via: s.via } : {}),
145
+ ...(s.files ? { files: s.files } : {}),
146
+ }));
147
+ }
148
+ /**
149
+ * Lee la lista que manda otro agente en su respuesta. Es entrada no fiable:
150
+ * se valida entrada a entrada y lo que no cuadra se descarta, nunca se lanza.
151
+ */
152
+ export function subcontractsFromJson(value) {
153
+ if (!Array.isArray(value))
154
+ return [];
155
+ const out = [];
156
+ for (const v of value.slice(0, MAX_SUBCONTRACTS)) {
157
+ if (!v || typeof v !== 'object')
158
+ continue;
159
+ const s = validar(v);
160
+ if (s)
161
+ out.push(s);
162
+ }
163
+ return out;
164
+ }
165
+ /**
166
+ * Lo que apunta un agente al subcontratar: la subcontratación directa y, detrás,
167
+ * las que hizo a su vez el que respondió, con `via` para saber de dónde vienen.
168
+ */
169
+ export function subcontractsOfAnswer(direct, nested = []) {
170
+ return [direct, ...nested.map((s) => ({ ...s, via: s.via ?? direct.agent }))].slice(0, MAX_SUBCONTRACTS);
171
+ }
@@ -191,6 +191,13 @@ export interface PublicarEnElTablon {
191
191
  * buzón del tablón después.
192
192
  */
193
193
  adjuntos?: ArchivoParaSubir[];
194
+ /**
195
+ * Confidencial: el encargo y sus adjuntos se cifran para quien publica, y al
196
+ * elegir una oferta se le da acceso solo a quien se elige (que tiene que
197
+ * tener su clave publicada). El anuncio sigue siendo público. Solo en
198
+ * selección: en inmediato lo cogería cualquiera sin que nadie le diera acceso.
199
+ */
200
+ confidencial?: boolean;
194
201
  }
195
202
  /**
196
203
  * Las comprobaciones de `post()` antes de firmar, con el motivo en claro.
@@ -209,6 +216,13 @@ export declare function tablonEntregaSignMessage(jobId: bigint, resultHash: Hex,
209
216
  export declare function tablonResultadoSignMessage(jobId: bigint, expira: number): string;
210
217
  /** Su cliente sube un adjunto del encargo, o su agente un archivo de la entrega. */
211
218
  export declare function tablonArchivoSignMessage(jobId: bigint, expira: number): string;
219
+ /**
220
+ * Una parte da acceso a lo cifrado de su encargo: a quien lo cogió o a quien
221
+ * decide una disputa (`concederAcceso`, en cifrado.ts).
222
+ */
223
+ export declare function tablonAccesoSignMessage(jobId: bigint, expira: number): string;
224
+ /** Un firmante de la multisig lee una disputa: el encargo, la entrega y sus archivos. */
225
+ export declare function tablonDisputaSignMessage(jobId: bigint, expira: number): string;
212
226
  export declare const MAX_ARCHIVOS_TABLON = 5;
213
227
  export declare const MAX_BYTES_ARCHIVOS_TABLON: number;
214
228
  /** Un archivo para subir al tablón: su nombre, sus bytes y su tipo si se sabe. */
@@ -213,6 +213,9 @@ export function problemaAlPublicar(p) {
213
213
  if (leerAnuncio(p.anuncio).audiencia === 'personas' && p.modo !== ModoTablon.Seleccion) {
214
214
  return 'Un encargo solo para personas se publica en selección: así eliges tú a la persona.';
215
215
  }
216
+ if (p.confidencial && p.modo !== ModoTablon.Seleccion) {
217
+ return 'Un encargo confidencial se publica en selección: a quien lo coge se le da acceso al elegirlo.';
218
+ }
216
219
  if (p.modo === ModoTablon.Seleccion) {
217
220
  const ofertas = p.ventanaOfertasS ?? 0;
218
221
  const eleccion = p.ventanaEleccionS ?? 0;
@@ -261,6 +264,17 @@ export function tablonResultadoSignMessage(jobId, expira) {
261
264
  export function tablonArchivoSignMessage(jobId, expira) {
262
265
  return `Panal tablón con reglas · archivo #${jobId.toString()} · ${expira}`;
263
266
  }
267
+ /**
268
+ * Una parte da acceso a lo cifrado de su encargo: a quien lo cogió o a quien
269
+ * decide una disputa (`concederAcceso`, en cifrado.ts).
270
+ */
271
+ export function tablonAccesoSignMessage(jobId, expira) {
272
+ return `Panal tablón con reglas · acceso #${jobId.toString()} · ${expira}`;
273
+ }
274
+ /** Un firmante de la multisig lee una disputa: el encargo, la entrega y sus archivos. */
275
+ export function tablonDisputaSignMessage(jobId, expira) {
276
+ return `Panal tablón con reglas · disputa #${jobId.toString()} · ${expira}`;
277
+ }
264
278
  /*
265
279
  * Archivos en el tablón.
266
280
  *
@@ -37,6 +37,7 @@
37
37
  import { type Address, type Hex, type PublicClient, type WalletClient } from 'viem';
38
38
  import { type PanalNetwork } from './chains.js';
39
39
  import type { PermitDomain } from './x402.js';
40
+ import type { ConsultaFilesCapability } from './consultas.js';
40
41
  export type { PermitDomain };
41
42
  export declare const X402_VERSION = 1;
42
43
  export declare const X402_SERVER_SCHEME = "eip2612-permit";
@@ -110,6 +111,8 @@ export interface X402ServerAccept {
110
111
  payerNonce?: string;
111
112
  /** Dominio EIP-712 con el que firmar, para no obligar al cliente a leerlo. */
112
113
  domain: PermitDomain;
114
+ /** Si se aceptan archivos con la pregunta (ver `consultas.ts`). */
115
+ files?: ConsultaFilesCapability;
113
116
  }
114
117
  export interface X402ServerQuote {
115
118
  x402Version: typeof X402_VERSION;
@@ -128,6 +131,8 @@ export declare function buildQuote(params: {
128
131
  payerNonce?: bigint;
129
132
  network?: PanalNetwork;
130
133
  nowS?: number;
134
+ /** Anunciar que se aceptan archivos: con el motor de Panal, `CONSULTA_FILES_CAPABILITY`. */
135
+ files?: ConsultaFilesCapability;
131
136
  }): X402ServerQuote;
132
137
  export interface X402Payment {
133
138
  scheme: typeof X402_SERVER_SCHEME;
@@ -194,6 +199,8 @@ export interface X402NativeAccept {
194
199
  maxTimeoutSeconds: number;
195
200
  /** Secreto de esta cotización: se presenta con el pago, nunca va a la cadena. */
196
201
  paymentId: Hex;
202
+ /** Si se aceptan archivos con la pregunta (ver `consultas.ts`). */
203
+ files?: ConsultaFilesCapability;
197
204
  }
198
205
  /**
199
206
  * La cotización de una pregunta en MON.
@@ -212,6 +219,8 @@ export declare function buildNativeQuote(params: {
212
219
  payer?: Address | null;
213
220
  network?: PanalNetwork;
214
221
  nowS?: number;
222
+ /** Anunciar que se aceptan archivos: con el motor de Panal, `CONSULTA_FILES_CAPABILITY`. */
223
+ files?: ConsultaFilesCapability;
215
224
  }): {
216
225
  x402Version: typeof X402_VERSION;
217
226
  accepts: X402NativeAccept[];
@@ -172,6 +172,7 @@ export function buildQuote(params) {
172
172
  maxTimeoutSeconds: 120,
173
173
  payerNonce: params.payerNonce?.toString(),
174
174
  domain: params.domain,
175
+ ...(params.files ? { files: params.files } : {}),
175
176
  },
176
177
  ],
177
178
  hint: 'Sign an EIP-2612 permit with the domain and fields of accepts[0] (spender = payTo, ' +
@@ -477,6 +478,7 @@ export function buildNativeQuote(params) {
477
478
  deadline,
478
479
  maxTimeoutSeconds: 120,
479
480
  paymentId: nativePaymentId(params.secret, { payer, payTo: params.payTo, amount, deadline, resource: params.resource }),
481
+ ...(params.files ? { files: params.files } : {}),
480
482
  },
481
483
  ],
482
484
  hint: 'Send EXACTLY `amount` MON to `payTo` before `deadline`, then repeat the request with the header ' +
package/dist/x402.d.ts CHANGED
@@ -19,6 +19,9 @@ import type { Account, Address, Hex, WalletClient } from 'viem';
19
19
  import { type UrlGuardOptions } from './net.js';
20
20
  import { type CallEnvelope } from './envelope.js';
21
21
  import { type X402Currency } from './currencies.js';
22
+ import { type SubcontractRecord } from './subcontratos.js';
23
+ import { type ConsultaFilesCapability, type ReceivedFile } from './consultas.js';
24
+ import type { HashedFile } from './files.js';
22
25
  /** El esquema de las monedas con `permit`. Debe coincidir con el servidor. */
23
26
  export declare const X402_SCHEME = "eip2612-permit";
24
27
  export interface PermitDomain {
@@ -45,6 +48,12 @@ export interface X402Accept {
45
48
  domain?: PermitDomain;
46
49
  /** Solo en MON: el código de esta cotización, que se presenta con el pago. */
47
50
  paymentId?: Hex;
51
+ /**
52
+ * Si el agente acepta archivos con la pregunta, cuántos y de qué tamaño.
53
+ * Sin esto, no se le mandan: un agente que no los espera los ignoraría y
54
+ * cobraría igual una respuesta que no los ha mirado. Ver `consultas.ts`.
55
+ */
56
+ files?: ConsultaFilesCapability;
48
57
  }
49
58
  export interface X402Quote {
50
59
  x402Version?: number;
@@ -87,6 +96,12 @@ export interface PayAndAskOptions extends UrlGuardOptions {
87
96
  * ha gastado su salto. Ver `descend()` en envelope.ts.
88
97
  */
89
98
  envelope?: CallEnvelope;
99
+ /**
100
+ * Archivos que acompañan a la pregunta, YA en el buzón a nombre del agente
101
+ * (ver `uploadConsultaFile`): aquí solo va su lista. Si la cotización no
102
+ * dice que el agente los acepta, no se firma nada.
103
+ */
104
+ files?: HashedFile[];
90
105
  }
91
106
  export interface AskResult {
92
107
  answer: string;
@@ -99,6 +114,19 @@ export interface AskResult {
99
114
  /** Transacción del cobro, si el servidor la reporta. */
100
115
  txHash?: Hex;
101
116
  endpoint: string;
117
+ /**
118
+ * A quién subcontrató, a su vez, el agente que respondió para contestarte
119
+ * (vacío si no subcontrató o si su motor no lo dice). Ver `subcontratos.ts`.
120
+ */
121
+ subcontracts: SubcontractRecord[];
122
+ /** Los archivos que devolvió, tal y como los anunció: dejados en el buzón a tu nombre. */
123
+ fileManifest: HashedFile[];
124
+ /**
125
+ * Esos archivos ya bajados, comprobados y abiertos. Los rellena el cliente
126
+ * (`ask`, `askAgent`); con `payAndAsk` a pelo va vacío y la lista está en
127
+ * `fileManifest`, para bajarlos con `downloadConsultaFile`.
128
+ */
129
+ files: ReceivedFile[];
102
130
  }
103
131
  /**
104
132
  * Paga una consulta a un endpoint x402 y devuelve la respuesta.
package/dist/x402.js CHANGED
@@ -20,6 +20,8 @@ import { estimateGas, waitForTransactionReceipt } from 'viem/actions';
20
20
  import { assertPublicUrl, fetchLimited } from './net.js';
21
21
  import { envelopeHeaders } from './envelope.js';
22
22
  import { networkOfChain, x402Currency } from './currencies.js';
23
+ import { subcontractsFromJson } from './subcontratos.js';
24
+ import { parseConsultaFiles } from './consultas.js';
23
25
  /** El esquema de las monedas con `permit`. Debe coincidir con el servidor. */
24
26
  export const X402_SCHEME = 'eip2612-permit';
25
27
  /** El de MON, que no tiene `permit`: se manda una transferencia y se presenta. */
@@ -112,6 +114,19 @@ export async function payAndAsk(wallet, account, endpoint, prompt, options) {
112
114
  // atacante creyendo que pagas al agente que elegiste.
113
115
  throw new X402Error(`La cotización cobra a ${accept.payTo} y esperabas a ${options.expectedPayee}: no se firma.`);
114
116
  }
117
+ const enviados = options.files ?? [];
118
+ if (enviados.length) {
119
+ const puede = accept.files;
120
+ if (!puede) {
121
+ throw new X402Error('Este agente no acepta archivos con la pregunta: no se firma.');
122
+ }
123
+ if (enviados.length > puede.max) {
124
+ throw new X402Error(`Este agente acepta ${puede.max} archivo(s) por pregunta y mandas ${enviados.length}.`);
125
+ }
126
+ const grande = enviados.find((f) => f.size > puede.maxBytes);
127
+ if (grande)
128
+ throw new X402Error(`"${grande.name}" pasa del tamaño que acepta este agente.`);
129
+ }
115
130
  if (options.asset && getAddress(accept.asset) !== getAddress(options.asset)) {
116
131
  throw new X402Error(`La cotización pide pagar en ${accept.asset} y esperabas ${options.asset}.`);
117
132
  }
@@ -146,7 +161,7 @@ export async function payAndAsk(wallet, account, endpoint, prompt, options) {
146
161
  'x-payment': header.value,
147
162
  ...(options.envelope ? envelopeHeaders(options.envelope) : {}),
148
163
  },
149
- body: JSON.stringify({ prompt }),
164
+ body: JSON.stringify(enviados.length ? { prompt, files: enviados } : { prompt }),
150
165
  timeoutMs: options.timeoutMs ?? (accept.maxTimeoutSeconds ?? 120) * 1000,
151
166
  });
152
167
  // `paid` es como lo devuelve la plantilla de agentes; `payment`, como lo
@@ -181,6 +196,11 @@ export async function payAndAsk(wallet, account, endpoint, prompt, options) {
181
196
  payee: getAddress(accept.payTo),
182
197
  txHash: body.payment?.txHash ?? body.paid?.txHash ?? header.txHash,
183
198
  endpoint: url.toString(),
199
+ // Entrada de otro agente: se valida entrada a entrada y lo que no cuadra se descarta.
200
+ subcontracts: subcontractsFromJson(body.subcontracts),
201
+ // Una lista de archivos mal formada se ignora entera: no hay a medias.
202
+ fileManifest: parseConsultaFiles(body.files) ?? [],
203
+ files: [],
184
204
  };
185
205
  }
186
206
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panal/sdk",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "SDK de Panal: contrata agentes de IA autonomos on-chain en Monad",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,6 +34,11 @@
34
34
  "engines": {
35
35
  "node": ">=20"
36
36
  },
37
+ "dependencies": {
38
+ "@noble/ciphers": "^1.3.0",
39
+ "@noble/curves": "^1.9.1",
40
+ "@noble/hashes": "^1.8.0"
41
+ },
37
42
  "peerDependencies": {
38
43
  "viem": "^2.21.0"
39
44
  },
@@ -45,7 +50,7 @@
45
50
  "scripts": {
46
51
  "build": "tsc -p tsconfig.json",
47
52
  "typecheck": "tsc -p tsconfig.json --noEmit",
48
- "test": "tsx test/sdk.test.ts && tsx test/currencies.test.ts && tsx test/x402.test.ts && tsx test/x402-server.test.ts && tsx test/envelope.test.ts && tsx test/files.test.ts && tsx test/attachments.test.ts && tsx test/llm.test.ts && tsx test/skill.test.ts && tsx test/agent-card.test.ts && tsx test/niveles.test.ts && tsx test/ejemplos.test.ts && tsx test/tablon.test.ts && tsx test/reserva.test.ts && tsx test/precios.test.ts && tsx test/tablon-reglas.test.ts",
53
+ "test": "tsx test/sdk.test.ts && tsx test/currencies.test.ts && tsx test/x402.test.ts && tsx test/x402-server.test.ts && tsx test/envelope.test.ts && tsx test/files.test.ts && tsx test/attachments.test.ts && tsx test/llm.test.ts && tsx test/skill.test.ts && tsx test/agent-card.test.ts && tsx test/niveles.test.ts && tsx test/ejemplos.test.ts && tsx test/tablon.test.ts && tsx test/reserva.test.ts && tsx test/precios.test.ts && tsx test/tablon-reglas.test.ts && tsx test/cifrado.test.ts && tsx test/subcontratos.test.ts && tsx test/consultas.test.ts",
49
54
  "test:nombres": "tsx test/nombres.test.ts"
50
55
  }
51
56
  }