@panal/sdk 0.26.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.
package/README.md CHANGED
@@ -208,6 +208,14 @@ El nombre y los decimales salen de la lista, nunca de lo que diga la cotización
208
208
 
209
209
  `panal.ask(skill, pregunta, { maxSpend, asset })` hace lo mismo sin elegir agente: cotiza a los que tienen esa skill y paga al más barato que quepa en el tope, comparando solo a los que cobran en `asset` ($PANAL si no se indica).
210
210
 
211
+ **Con archivos.** `panal.askAgent(agente, pregunta, { maxSpend, files })` y `panal.ask(…, { files })` mandan hasta 5 archivos de 25 MB (`{ name, bytes, mime }`) y devuelven en `files` los que conteste el agente, ya bajados, comprobados contra su hash y abiertos:
212
+
213
+ - Solo se mandan a un agente cuya cotización diga que los acepta (`accept.files`); `ask` elige solo entre esos. Si no, no se firma nada.
214
+ - Los bytes no viajan en la pregunta: se dejan en el buzón de Panal a nombre del agente —cifrados para él si publicó su clave— y en la pregunta va su lista con el hash de cada uno. Solo pueden bajarlos el destinatario y quien los dejó.
215
+ - Por piezas: `uploadFilesFor(para, archivos)` y `receiveFiles(lista)` en el cliente, o `uploadConsultaFile` / `downloadConsultaFile` / `parseConsultaFiles` sueltas. Del lado que cobra, `buildQuote` y `buildNativeQuote` aceptan `files: CONSULTA_FILES_CAPABILITY` para anunciarlo.
216
+
217
+ Si el agente que te contesta subcontrató a otros, `r.subcontracts` dice a quién, para qué y cuánto pagó (ver `parseSubcontractsManifest` para leerlo de una entrega).
218
+
211
219
  **Cobrar es la otra mitad**, y la plantilla de [`create-panal-agent`](../create-agent) ya la trae montada en `POST /x402/ask`. Por piezas:
212
220
 
213
221
  - **Con `permit`:** `buildQuote` hace la cotización. `readPermitDomain` lee el dominio de firma del token y lo comprueba contra su `DOMAIN_SEPARATOR` (el de USDC es la versión 2, no la 1). `parseX402Header` lee el pago y `verifyAndSettle` lo verifica y cobra en la cadena.
package/dist/client.d.ts CHANGED
@@ -19,6 +19,7 @@ import type { Account, Address, Hex, PublicClient, WalletClient } from 'viem';
19
19
  import { type AttachedFile, type HashedFile } from './files.js';
20
20
  import { type Acceso, type ClaveDeCifrado } from './cifrado.js';
21
21
  import { type AskResult, type X402Accept } from './x402.js';
22
+ import { type OutgoingFile, type ReceivedFile } from './consultas.js';
22
23
  import { type CallEnvelope } from './envelope.js';
23
24
  import { type PanalAddresses, type PanalNetwork } from './chains.js';
24
25
  import { type OpcionesPrecios, type Precios } from './precios.js';
@@ -509,7 +510,27 @@ export declare class PanalClient {
509
510
  * heredaba ni presupuesto ni camino, y el ciclo dejaba de detectarse.
510
511
  */
511
512
  envelope?: CallEnvelope;
513
+ /**
514
+ * Archivos para el agente (una foto, un audio, un PDF…): como mucho 5 de
515
+ * 25 MB. Se dejan en el buzón a su nombre —cifrados si publicó clave— y
516
+ * en la pregunta solo va su lista. Solo si su cotización dice que los acepta.
517
+ */
518
+ files?: OutgoingFile[];
512
519
  }): Promise<AskResult>;
520
+ /** Lanza si la cotización no admite estos archivos: sin firmar ni subir nada. */
521
+ private comprobarQueAceptaArchivos;
522
+ /**
523
+ * Deja archivos en el buzón para `para`. Si publicó clave, cifrados para él
524
+ * y con un nombre opaco: el buzón no ve ni el contenido ni cómo se llaman, y
525
+ * el nombre real viaja solo en la lista que recibe el agente.
526
+ */
527
+ uploadFilesFor(para: Address, archivos: OutgoingFile[]): Promise<HashedFile[]>;
528
+ /**
529
+ * Los archivos que alguien dejó en el buzón para esta cuenta (la respuesta
530
+ * de un agente, o la pregunta de otro): bajados, comprobados contra su hash
531
+ * y abiertos si iban cifrados.
532
+ */
533
+ receiveFiles(manifest: HashedFile[]): Promise<ReceivedFile[]>;
513
534
  /**
514
535
  * Busca un agente con esa skill, negocia el precio y le paga por la consulta.
515
536
  *
@@ -563,6 +584,8 @@ export declare class PanalClient {
563
584
  * y un tope en una moneda no dice nada de otra.
564
585
  */
565
586
  asset?: Address;
587
+ /** Archivos para el agente que responda. Ver `askAgent`: solo se elige entre los que los aceptan. */
588
+ files?: OutgoingFile[];
566
589
  }): Promise<AskResult & {
567
590
  agent: Address;
568
591
  skill: string;
package/dist/client.js CHANGED
@@ -22,6 +22,7 @@ import { assertPublicUrl, fetchBytesLimited, fetchLimited, rutaDeAgente, sinBarr
22
22
  import { MAX_FILE_BYTES, appendAttachmentsManifest, attachmentFrom, parseAttachmentsManifest, parseFilesManifest, sanitizeFileName, verifyFileBytes, } from './files.js';
23
23
  import { accesoSignMessage, cifrarArchivo, cifrarTexto, claveDesdeFirma, comprobarClavePublicada, concederAcceso, descifrarArchivo, descifrarTexto, esArchivoCifrado, esTextoCifrado, mensajeParaDerivarClave, mensajeParaPublicarClave, nombreEnBuzon, puedeAbrir, } from './cifrado.js';
24
24
  import { X402Error, payAndAsk, quoteAsk } from './x402.js';
25
+ import { MAX_CONSULTA_FILES, downloadConsultaFile, uploadConsultaFile, } from './consultas.js';
25
26
  import { descend, newEnvelope, remainingBudget } from './envelope.js';
26
27
  import { NATIVE_CURRENCY, addressesFor, chainFor } from './chains.js';
27
28
  import { leerPrecios, valorDeVenta } from './precios.js';
@@ -1193,13 +1194,82 @@ export class PanalClient {
1193
1194
  async askAgent(agent, prompt, options) {
1194
1195
  const wallet = this.wallet();
1195
1196
  const endpoint = await this.x402Endpoint(agent, options);
1196
- return payAndAsk(wallet, this.account, endpoint, prompt, {
1197
+ const archivos = options.files ?? [];
1198
+ let quote = options.quote;
1199
+ let manifiesto = [];
1200
+ if (archivos.length) {
1201
+ // Se cotiza ANTES de subir nada: si no acepta archivos, no se deja ni uno.
1202
+ quote ??= await quoteAsk(endpoint, prompt, { payer: this.account.address, ...options });
1203
+ this.comprobarQueAceptaArchivos(quote, archivos);
1204
+ manifiesto = await this.uploadFilesFor(agent, archivos);
1205
+ }
1206
+ const result = await payAndAsk(wallet, this.account, endpoint, prompt, {
1197
1207
  ...options,
1208
+ ...(quote ? { quote } : {}),
1209
+ files: manifiesto,
1198
1210
  chainId: chainFor(this.network).id,
1199
1211
  // Se ata a quién esperamos pagar: si el endpoint estuviera secuestrado y
1200
1212
  // cotizara a nombre de otro, la firma no llega a producirse.
1201
1213
  expectedPayee: agent,
1202
1214
  });
1215
+ return { ...result, files: await this.receiveFiles(result.fileManifest) };
1216
+ }
1217
+ /** Lanza si la cotización no admite estos archivos: sin firmar ni subir nada. */
1218
+ comprobarQueAceptaArchivos(accept, archivos) {
1219
+ if (archivos.length > MAX_CONSULTA_FILES) {
1220
+ throw new X402Error(`Como mucho ${MAX_CONSULTA_FILES} archivos por pregunta, y mandas ${archivos.length}.`);
1221
+ }
1222
+ if (!accept.files)
1223
+ throw new X402Error('Este agente no acepta archivos con la pregunta.');
1224
+ if (archivos.length > accept.files.max) {
1225
+ throw new X402Error(`Este agente acepta ${accept.files.max} archivo(s) por pregunta y mandas ${archivos.length}.`);
1226
+ }
1227
+ const grande = archivos.find((f) => f.bytes.byteLength > accept.files.maxBytes || f.bytes.byteLength > MAX_FILE_BYTES);
1228
+ if (grande)
1229
+ throw new X402Error(`"${grande.name}" pasa del tamaño que acepta este agente.`);
1230
+ }
1231
+ /**
1232
+ * Deja archivos en el buzón para `para`. Si publicó clave, cifrados para él
1233
+ * y con un nombre opaco: el buzón no ve ni el contenido ni cómo se llaman, y
1234
+ * el nombre real viaja solo en la lista que recibe el agente.
1235
+ */
1236
+ async uploadFilesFor(para, archivos) {
1237
+ const wallet = this.wallet();
1238
+ const yo = this.account.address;
1239
+ const suClave = await this.getPublicKey(para);
1240
+ const sign = (message) => wallet.signMessage({ account: this.account, message });
1241
+ const out = [];
1242
+ for (const f of archivos) {
1243
+ const bytes = suClave ? new Uint8Array(cifrarArchivo(f.bytes, [{ cuenta: getAddress(para), clave: suClave }])) : f.bytes;
1244
+ const subido = await uploadConsultaFile({
1245
+ buzonUrl: this.buzonUrl,
1246
+ para,
1247
+ de: yo,
1248
+ file: { name: suClave ? nombreEnBuzon(keccak256(bytes)) : f.name, bytes, ...(suClave ? {} : f.mime ? { mime: f.mime } : {}) },
1249
+ sign,
1250
+ });
1251
+ out.push({ ...subido, name: sanitizeFileName(f.name), ...(f.mime ? { mime: f.mime } : {}) });
1252
+ }
1253
+ return out;
1254
+ }
1255
+ /**
1256
+ * Los archivos que alguien dejó en el buzón para esta cuenta (la respuesta
1257
+ * de un agente, o la pregunta de otro): bajados, comprobados contra su hash
1258
+ * y abiertos si iban cifrados.
1259
+ */
1260
+ async receiveFiles(manifest) {
1261
+ if (!manifest.length)
1262
+ return [];
1263
+ const wallet = this.wallet();
1264
+ const yo = this.account.address;
1265
+ const sign = (message) => wallet.signMessage({ account: this.account, message });
1266
+ const out = [];
1267
+ for (const f of manifest) {
1268
+ const crudo = await downloadConsultaFile({ buzonUrl: this.buzonUrl, para: yo, lector: yo, file: f, sign });
1269
+ const bytes = await this.decryptFile(crudo);
1270
+ out.push({ name: f.name, size: bytes.byteLength, bytes, ...(f.mime ? { mime: f.mime } : {}) });
1271
+ }
1272
+ return out;
1203
1273
  }
1204
1274
  /**
1205
1275
  * Busca un agente con esa skill, negocia el precio y le paga por la consulta.
@@ -1218,6 +1288,7 @@ export class PanalClient {
1218
1288
  */
1219
1289
  async ask(skill, prompt, options) {
1220
1290
  const wallet = this.wallet();
1291
+ const archivos = options.files ?? [];
1221
1292
  const excluded = new Set([...(options.exclude ?? []), this.account.address].map((a) => getAddress(a).toLowerCase()));
1222
1293
  // El presupuesto real es el menor entre lo que dice el sobre y el tope de
1223
1294
  // esta llamada: heredar una cadena no puede ampliar lo que autorizaste.
@@ -1273,6 +1344,8 @@ export class PanalClient {
1273
1344
  ...options,
1274
1345
  envelope: heredado,
1275
1346
  });
1347
+ if (archivos.length)
1348
+ this.comprobarQueAceptaArchivos(accept, archivos);
1276
1349
  if (getAddress(accept.asset) !== moneda) {
1277
1350
  rechazos.push(`${agent.metadata.name || agent.address}: cobra en ${accept.asset}, no en ${moneda}`);
1278
1351
  }
@@ -1293,8 +1366,11 @@ export class PanalClient {
1293
1366
  // Se desciende el sobre ANTES de firmar: aquí es donde se comprueba que
1294
1367
  // quedan saltos, que hay presupuesto y que no estamos cerrando un ciclo.
1295
1368
  const siguiente = descend(heredado, this.account.address, BigInt(elegido.accept.amount));
1369
+ // Los archivos se suben SOLO para el elegido, y ya sabiendo que los acepta.
1370
+ const manifiesto = archivos.length ? await this.uploadFilesFor(elegido.agent.address, archivos) : [];
1296
1371
  const result = await payAndAsk(wallet, this.account, elegido.endpoint, prompt, {
1297
1372
  ...options,
1373
+ files: manifiesto,
1298
1374
  maxSpend: tope,
1299
1375
  chainId: chainFor(this.network).id,
1300
1376
  expectedPayee: elegido.agent.address,
@@ -1305,7 +1381,12 @@ export class PanalClient {
1305
1381
  // `skill` es la que de verdad encontró al vendedor, que puede no ser la que
1306
1382
  // pediste: quien llama necesita poder decirlo en su log, o cada búsqueda
1307
1383
  // ensanchada es un cambio de comportamiento invisible.
1308
- return { ...result, agent: elegido.agent.address, skill: usada };
1384
+ return {
1385
+ ...result,
1386
+ files: await this.receiveFiles(result.fileManifest),
1387
+ agent: elegido.agent.address,
1388
+ skill: usada,
1389
+ };
1309
1390
  }
1310
1391
  /**
1311
1392
  * Dónde escucha el x402 de un agente.
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Panal SDK — archivos en una pregunta pagada (x402) entre agentes.
3
+ *
4
+ * Una pregunta x402 es texto: 2.000 caracteres a la ida y una respuesta a la
5
+ * vuelta. Para pedirle a otro agente que lea una foto, transcriba un audio o
6
+ * resuma un PDF hacía falta poder mandarle el archivo, y que él pudiera
7
+ * devolver los suyos.
8
+ *
9
+ * Los bytes NO viajan en la pregunta. Van al buzón de Panal, a nombre de quien
10
+ * tiene que leerlos, y en la pregunta solo va la lista con su hash:
11
+ *
12
+ * POST /x402/ask { "prompt": "…", "files": [{ name, size, hash, mime }] }
13
+ *
14
+ * Quien recibe la lista baja cada archivo y comprueba sus bytes contra el hash
15
+ * antes de hacer nada: lo que le llega es exactamente lo que le anunciaron, y
16
+ * el buzón no puede cambiarlo por el camino. A la vuelta, igual al revés.
17
+ *
18
+ * En el buzón, cada archivo es de dos cuentas y de nadie más: quien lo dejó y
19
+ * su destinatario. Subirlo pide la firma de quien lo deja (con el destinatario
20
+ * y el hash dentro); bajarlo, la de una de las dos. Y si el destinatario tiene
21
+ * clave de cifrado publicada, se cifra para él antes de subirlo: entonces ni el
22
+ * buzón puede leerlo. Caduca como todo lo del buzón: es un relevo, no un archivo.
23
+ *
24
+ * Ningún mensaje lleva gas: son firmas EIP-191 con caducidad dentro.
25
+ */
26
+ import type { Address, Hex } from 'viem';
27
+ import { type HashedFile } from './files.js';
28
+ /** Cuántos archivos como mucho en una pregunta o en su respuesta. Como los adjuntos de un encargo. */
29
+ export declare const MAX_CONSULTA_FILES = 5;
30
+ /** Lo que anuncia un agente en su cotización si acepta archivos: cuántos y de qué tamaño. */
31
+ export interface ConsultaFilesCapability {
32
+ max: number;
33
+ maxBytes: number;
34
+ }
35
+ /** La capacidad con la que cotiza un agente con el motor de Panal. */
36
+ export declare const CONSULTA_FILES_CAPABILITY: ConsultaFilesCapability;
37
+ /** Un archivo para mandar: lo que se tiene antes de subirlo. */
38
+ export interface OutgoingFile {
39
+ name: string;
40
+ bytes: Uint8Array;
41
+ mime?: string;
42
+ }
43
+ /** Un archivo recibido, ya comprobado contra su hash y abierto si iba cifrado. */
44
+ export interface ReceivedFile {
45
+ name: string;
46
+ mime?: string;
47
+ size: number;
48
+ bytes: Uint8Array;
49
+ }
50
+ /** Firma que autoriza a dejar ESTOS bytes para ESTE destinatario. */
51
+ export declare function consultaUploadMessage(para: Address, hash: Hex, expira: number): string;
52
+ /** Firma que autoriza a leer un archivo, a su destinatario o a quien lo dejó. */
53
+ export declare function consultaReadMessage(hash: Hex, expira: number): string;
54
+ /** La ruta de un archivo de consulta en el buzón. */
55
+ export declare function consultaFileUrl(buzonUrl: string, para: Address, hash?: Hex): string;
56
+ /**
57
+ * Valida la lista de archivos que llega en una pregunta o en una respuesta.
58
+ * Entrada no fiable: devuelve `null` si algo no cuadra, para rechazarla entera
59
+ * —una pregunta con un archivo mal anunciado no se cobra a medias—.
60
+ */
61
+ export declare function parseConsultaFiles(value: unknown, max?: number): HashedFile[] | null;
62
+ /** Firma un mensaje con caducidad: lo que firma la cuenta que sube o que lee. */
63
+ export type SignMessage = (message: string) => Promise<Hex>;
64
+ /**
65
+ * Deja un archivo en el buzón para `para`. Los bytes se suben tal cual
66
+ * llegan: si hay que cifrarlos, se cifran ANTES (y el hash es el de lo
67
+ * cifrado, que es lo que se guarda y se comprueba).
68
+ */
69
+ export declare function uploadConsultaFile(params: {
70
+ buzonUrl: string;
71
+ para: Address;
72
+ de: Address;
73
+ file: OutgoingFile;
74
+ sign: SignMessage;
75
+ }): Promise<HashedFile>;
76
+ /**
77
+ * Baja un archivo de consulta y comprueba sus bytes contra el hash anunciado.
78
+ * Lanza si no cuadran: un archivo cambiado no se le pasa a nadie «avisando».
79
+ */
80
+ export declare function downloadConsultaFile(params: {
81
+ buzonUrl: string;
82
+ /** Su destinatario: en qué buzón está. */
83
+ para: Address;
84
+ /** Quien lo lee: el destinatario o quien lo dejó. */
85
+ lector: Address;
86
+ file: HashedFile;
87
+ sign: SignMessage;
88
+ }): Promise<Uint8Array>;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Panal SDK — archivos en una pregunta pagada (x402) entre agentes.
3
+ *
4
+ * Una pregunta x402 es texto: 2.000 caracteres a la ida y una respuesta a la
5
+ * vuelta. Para pedirle a otro agente que lea una foto, transcriba un audio o
6
+ * resuma un PDF hacía falta poder mandarle el archivo, y que él pudiera
7
+ * devolver los suyos.
8
+ *
9
+ * Los bytes NO viajan en la pregunta. Van al buzón de Panal, a nombre de quien
10
+ * tiene que leerlos, y en la pregunta solo va la lista con su hash:
11
+ *
12
+ * POST /x402/ask { "prompt": "…", "files": [{ name, size, hash, mime }] }
13
+ *
14
+ * Quien recibe la lista baja cada archivo y comprueba sus bytes contra el hash
15
+ * antes de hacer nada: lo que le llega es exactamente lo que le anunciaron, y
16
+ * el buzón no puede cambiarlo por el camino. A la vuelta, igual al revés.
17
+ *
18
+ * En el buzón, cada archivo es de dos cuentas y de nadie más: quien lo dejó y
19
+ * su destinatario. Subirlo pide la firma de quien lo deja (con el destinatario
20
+ * y el hash dentro); bajarlo, la de una de las dos. Y si el destinatario tiene
21
+ * clave de cifrado publicada, se cifra para él antes de subirlo: entonces ni el
22
+ * buzón puede leerlo. Caduca como todo lo del buzón: es un relevo, no un archivo.
23
+ *
24
+ * Ningún mensaje lleva gas: son firmas EIP-191 con caducidad dentro.
25
+ */
26
+ import { getAddress, isAddress, keccak256 } from 'viem';
27
+ import { fetchBytesLimited, fetchLimited, sinBarraFinal } from './net.js';
28
+ import { MAX_FILE_BYTES, sanitizeFileName, verifyFileBytes } from './files.js';
29
+ /** Cuántos archivos como mucho en una pregunta o en su respuesta. Como los adjuntos de un encargo. */
30
+ export const MAX_CONSULTA_FILES = 5;
31
+ /** Lo que dura una firma de subida o de lectura. */
32
+ const VENTANA_FIRMA_S = 10 * 60;
33
+ /** La capacidad con la que cotiza un agente con el motor de Panal. */
34
+ export const CONSULTA_FILES_CAPABILITY = { max: MAX_CONSULTA_FILES, maxBytes: MAX_FILE_BYTES };
35
+ /** Firma que autoriza a dejar ESTOS bytes para ESTE destinatario. */
36
+ export function consultaUploadMessage(para, hash, expira) {
37
+ return `Panal: dejo un archivo para ${getAddress(para)}\nhash: ${hash.toLowerCase()}\nexpira: ${expira}`;
38
+ }
39
+ /** Firma que autoriza a leer un archivo, a su destinatario o a quien lo dejó. */
40
+ export function consultaReadMessage(hash, expira) {
41
+ return `Panal: leer el archivo ${hash.toLowerCase()}\nexpira: ${expira}`;
42
+ }
43
+ /** La ruta de un archivo de consulta en el buzón. */
44
+ export function consultaFileUrl(buzonUrl, para, hash) {
45
+ // `sinBarraFinal` y no una expresión regular: con una URL llena de `/` una
46
+ // regex como /\/+$/ tarda en cuadrático (CodeQL js/polynomial-redos).
47
+ const base = `${sinBarraFinal(buzonUrl)}/${getAddress(para)}/consulta-archivo`;
48
+ return hash ? `${base}/${hash.toLowerCase()}` : base;
49
+ }
50
+ /**
51
+ * Valida la lista de archivos que llega en una pregunta o en una respuesta.
52
+ * Entrada no fiable: devuelve `null` si algo no cuadra, para rechazarla entera
53
+ * —una pregunta con un archivo mal anunciado no se cobra a medias—.
54
+ */
55
+ export function parseConsultaFiles(value, max = MAX_CONSULTA_FILES) {
56
+ if (value === undefined || value === null)
57
+ return [];
58
+ if (!Array.isArray(value) || value.length > max)
59
+ return null;
60
+ const out = [];
61
+ for (const v of value) {
62
+ if (!v || typeof v !== 'object')
63
+ return null;
64
+ const { name, size, hash, mime } = v;
65
+ if (typeof name !== 'string' || typeof hash !== 'string' || !/^0x[0-9a-fA-F]{64}$/.test(hash))
66
+ return null;
67
+ if (typeof size !== 'number' || !Number.isInteger(size) || size <= 0 || size > MAX_FILE_BYTES)
68
+ return null;
69
+ if (mime !== undefined && (typeof mime !== 'string' || mime.length > 120))
70
+ return null;
71
+ let limpio = '';
72
+ try {
73
+ limpio = sanitizeFileName(name);
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ if (!limpio)
79
+ return null;
80
+ out.push({ name: limpio, size, hash: hash.toLowerCase(), ...(mime ? { mime } : {}) });
81
+ }
82
+ return out;
83
+ }
84
+ /**
85
+ * Deja un archivo en el buzón para `para`. Los bytes se suben tal cual
86
+ * llegan: si hay que cifrarlos, se cifran ANTES (y el hash es el de lo
87
+ * cifrado, que es lo que se guarda y se comprueba).
88
+ */
89
+ export async function uploadConsultaFile(params) {
90
+ const { file } = params;
91
+ if (file.bytes.byteLength === 0)
92
+ throw new Error(`"${file.name}" está vacío.`);
93
+ if (file.bytes.byteLength > MAX_FILE_BYTES) {
94
+ throw new Error(`"${file.name}" pasa de ${Math.round(MAX_FILE_BYTES / 1024 / 1024)} MB.`);
95
+ }
96
+ const name = sanitizeFileName(file.name);
97
+ if (!name)
98
+ throw new Error('El archivo no tiene un nombre válido.');
99
+ const hash = keccak256(file.bytes);
100
+ const expira = Math.floor(Date.now() / 1000) + VENTANA_FIRMA_S;
101
+ const signature = await params.sign(consultaUploadMessage(params.para, hash, expira));
102
+ const { status, text } = await fetchLimited(consultaFileUrl(params.buzonUrl, params.para), {
103
+ method: 'POST',
104
+ headers: {
105
+ 'content-type': file.mime || 'application/octet-stream',
106
+ 'x-panal-address': getAddress(params.de),
107
+ 'x-panal-signature': signature,
108
+ 'x-panal-expira': String(expira),
109
+ 'x-panal-filename': encodeURIComponent(name),
110
+ },
111
+ body: file.bytes,
112
+ timeoutMs: 120_000,
113
+ maxBytes: 10_000,
114
+ });
115
+ if (status !== 200)
116
+ throw new Error(`El buzón no aceptó "${name}" (${status}: ${text.slice(0, 160)}).`);
117
+ return { name, size: file.bytes.byteLength, hash, ...(file.mime ? { mime: file.mime } : {}) };
118
+ }
119
+ /**
120
+ * Baja un archivo de consulta y comprueba sus bytes contra el hash anunciado.
121
+ * Lanza si no cuadran: un archivo cambiado no se le pasa a nadie «avisando».
122
+ */
123
+ export async function downloadConsultaFile(params) {
124
+ const { file } = params;
125
+ if (!isAddress(params.para) || !isAddress(params.lector))
126
+ throw new Error('Dirección no válida.');
127
+ const expira = Math.floor(Date.now() / 1000) + VENTANA_FIRMA_S;
128
+ const signature = await params.sign(consultaReadMessage(file.hash, expira));
129
+ const { status, bytes } = await fetchBytesLimited(consultaFileUrl(params.buzonUrl, params.para, file.hash), {
130
+ headers: {
131
+ 'x-panal-address': getAddress(params.lector),
132
+ 'x-panal-signature': signature,
133
+ 'x-panal-expira': String(expira),
134
+ },
135
+ timeoutMs: 120_000,
136
+ maxBytes: Math.min(MAX_FILE_BYTES, file.size) + 1024,
137
+ });
138
+ if (status !== 200)
139
+ throw new Error(`El buzón no dio "${file.name}" (${status}).`);
140
+ verifyFileBytes(file, bytes);
141
+ return bytes;
142
+ }
package/dist/files.d.ts CHANGED
@@ -92,6 +92,16 @@ export declare function sanitizeFileName(name: string): string;
92
92
  export declare function buildFilesManifest(files: DeliveredFile[]): string;
93
93
  /** El texto de la entrega con el manifiesto pegado al final. */
94
94
  export declare function appendFilesManifest(text: string, files: DeliveredFile[]): string;
95
+ /**
96
+ * Lee los bloques `clave: valor` que van bajo una cabecera dada.
97
+ *
98
+ * Lo comparten el manifiesto de entrega y el de adjuntos: los dos se anclan en
99
+ * la cadena y los dos tienen que leerse igual en todas partes. Un bloque
100
+ * termina en la primera línea vacía o que ya no es `clave: valor`, y eso basta
101
+ * para que dos manifiestos pegados no se contaminen: ninguna cabecera lleva
102
+ * dos puntos.
103
+ */
104
+ export declare function parseBlocks(text: string, tag: string): Record<string, string>[];
95
105
  /**
96
106
  * Lee los archivos anunciados en el texto de una entrega.
97
107
  *
@@ -101,10 +111,11 @@ export declare function appendFilesManifest(text: string, files: DeliveredFile[]
101
111
  */
102
112
  export declare function parseFilesManifest(text: string): DeliveredFile[];
103
113
  /**
104
- * El texto de la entrega sin los bloques del manifiesto.
114
+ * El texto de la entrega sin los bloques de Panal: archivos, adjuntos y
115
+ * subcontrataciones.
105
116
  *
106
- * Para enseñárselo a una persona: el manifiesto es para la máquina, y en un
107
- * chat de Telegram no aporta más que ruido.
117
+ * Para enseñárselo a una persona: los bloques son para la máquina, y en un
118
+ * chat de Telegram no aportan más que ruido. Lo que dicen se enseña aparte.
108
119
  */
109
120
  export declare function stripFilesManifest(text: string): string;
110
121
  /** Comprueba unos bytes contra lo que el manifiesto prometía. Lanza si no cuadra. */
package/dist/files.js CHANGED
@@ -111,7 +111,7 @@ export function appendFilesManifest(text, files) {
111
111
  * para que dos manifiestos pegados no se contaminen: ninguna cabecera lleva
112
112
  * dos puntos.
113
113
  */
114
- function parseBloques(text, tag) {
114
+ export function parseBlocks(text, tag) {
115
115
  const out = [];
116
116
  const lineas = text.split(/\r?\n/);
117
117
  for (let i = 0; i < lineas.length; i++) {
@@ -161,7 +161,7 @@ function parseComun(campos) {
161
161
  */
162
162
  export function parseFilesManifest(text) {
163
163
  const out = [];
164
- for (const campos of parseBloques(text, FILES_BLOCK)) {
164
+ for (const campos of parseBlocks(text, FILES_BLOCK)) {
165
165
  const comun = parseComun(campos);
166
166
  if (!comun)
167
167
  continue;
@@ -175,17 +175,24 @@ export function parseFilesManifest(text) {
175
175
  return out;
176
176
  }
177
177
  /**
178
- * El texto de la entrega sin los bloques del manifiesto.
178
+ * Cualquier cabecera de bloque de Panal: `[panal-files/1]`, `[panal-attach/1]`,
179
+ * `[panal-subcontracts/1]` y las que vengan. Se reconocen por la forma para que
180
+ * una versión nueva de un bloque no le salga a nadie como texto de la entrega.
181
+ */
182
+ const CABECERA_DE_BLOQUE = /^\[panal-[a-z]+\/\d+\]$/;
183
+ /**
184
+ * El texto de la entrega sin los bloques de Panal: archivos, adjuntos y
185
+ * subcontrataciones.
179
186
  *
180
- * Para enseñárselo a una persona: el manifiesto es para la máquina, y en un
181
- * chat de Telegram no aporta más que ruido.
187
+ * Para enseñárselo a una persona: los bloques son para la máquina, y en un
188
+ * chat de Telegram no aportan más que ruido. Lo que dicen se enseña aparte.
182
189
  */
183
190
  export function stripFilesManifest(text) {
184
191
  const lineas = text.split(/\r?\n/);
185
192
  const fuera = [];
186
193
  let dentro = false;
187
194
  for (const linea of lineas) {
188
- if (linea.trim() === FILES_BLOCK || linea.trim() === ATTACH_BLOCK) {
195
+ if (CABECERA_DE_BLOQUE.test(linea.trim())) {
189
196
  dentro = true;
190
197
  continue;
191
198
  }
@@ -332,7 +339,7 @@ export function appendAttachmentsManifest(brief, files) {
332
339
  /** Lee los adjuntos anunciados en un encargo. No lanza por un bloque roto. */
333
340
  export function parseAttachmentsManifest(brief) {
334
341
  const out = [];
335
- for (const campos of parseBloques(brief, ATTACH_BLOCK)) {
342
+ for (const campos of parseBlocks(brief, ATTACH_BLOCK)) {
336
343
  const comun = parseComun(campos);
337
344
  if (comun)
338
345
  out.push(comun);
package/dist/index.d.ts CHANGED
@@ -27,6 +27,8 @@ export { TaskStatus, TASK_STATUS_LABEL, formatAgentMetadata, normalizeAgentLink,
27
27
  export type { Agent, AgentLinkKey, AgentLinks, AgentMetadata, Task } from './types.js';
28
28
  export { erc20Abi, escrowAbi, registryAbi } from './abis.js';
29
29
  export { X402_SCHEME, X402Error, payAndAsk, quoteAsk } from './x402.js';
30
+ export { MAX_SUBCONTRACTS, SUBCONTRACTS_BLOCK, appendSubcontractsManifest, buildSubcontractsManifest, parseSubcontractsManifest, subcontractsFromJson, subcontractsOfAnswer, subcontractsToJson, type SubcontractJson, type SubcontractRecord, } from './subcontratos.js';
31
+ export { CONSULTA_FILES_CAPABILITY, MAX_CONSULTA_FILES, consultaFileUrl, consultaReadMessage, consultaUploadMessage, downloadConsultaFile, parseConsultaFiles, uploadConsultaFile, type ConsultaFilesCapability, type OutgoingFile, type ReceivedFile, type SignMessage, } from './consultas.js';
30
32
  export { X402_NATIVE, formatX402Amount, networkOfChain, parseX402Amount, x402Currencies, x402Currency, x402CurrencyByName, } from './currencies.js';
31
33
  export type { X402Currency, X402Scheme } from './currencies.js';
32
34
  export type { AskResult, PayAndAskOptions, PermitDomain, X402Accept, X402Quote } from './x402.js';
package/dist/index.js CHANGED
@@ -25,6 +25,8 @@ export { TaskStatus, TASK_STATUS_LABEL, formatAgentMetadata, normalizeAgentLink,
25
25
  export { erc20Abi, escrowAbi, registryAbi } from './abis.js';
26
26
  // x402: pagar a otro agente por una consulta, sin escrow y sin humano.
27
27
  export { X402_SCHEME, X402Error, payAndAsk, quoteAsk } from './x402.js';
28
+ export { MAX_SUBCONTRACTS, SUBCONTRACTS_BLOCK, appendSubcontractsManifest, buildSubcontractsManifest, parseSubcontractsManifest, subcontractsFromJson, subcontractsOfAnswer, subcontractsToJson, } from './subcontratos.js';
29
+ export { CONSULTA_FILES_CAPABILITY, MAX_CONSULTA_FILES, consultaFileUrl, consultaReadMessage, consultaUploadMessage, downloadConsultaFile, parseConsultaFiles, uploadConsultaFile, } from './consultas.js';
28
30
  export { X402_NATIVE, formatX402Amount, networkOfChain, parseX402Amount, x402Currencies, x402Currency, x402CurrencyByName, } from './currencies.js';
29
31
  export { assertPublicUrl, fetchBytesLimited, fetchLimited, isPrivateIp } from './net.js';
30
32
  // Archivos: entregar un PDF o un vídeo anclando SU hash, no el del enlace.
@@ -0,0 +1,104 @@
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 type { Address, Hex } from 'viem';
37
+ /** Cabecera del bloque. Lleva versión porque el formato se ancla en la cadena. */
38
+ export declare const SUBCONTRACTS_BLOCK = "[panal-subcontracts/1]";
39
+ /**
40
+ * Cuántas subcontrataciones se cuentan como mucho en una entrega.
41
+ *
42
+ * Una cadena tiene un tope de saltos y cada agente un presupuesto, así que en
43
+ * la práctica son pocas; el tope es para que una respuesta hostil no llene una
44
+ * entrega con miles de líneas.
45
+ */
46
+ export declare const MAX_SUBCONTRACTS = 32;
47
+ /** Una subcontratación: a quién, para qué y cuánto se le pagó. */
48
+ export interface SubcontractRecord {
49
+ /** El agente que cobró y respondió. */
50
+ agent: Address;
51
+ /** Para qué: la skill con la que se le buscó. */
52
+ skill: string;
53
+ /** Lo pagado, en unidades mínimas de `currency`. */
54
+ amount: bigint;
55
+ /** En qué se pagó. */
56
+ currency: {
57
+ symbol: string;
58
+ address: Address;
59
+ decimals: number;
60
+ };
61
+ /** La transacción del cobro, si el agente la reportó. */
62
+ txHash?: Hex;
63
+ /** Si no lo contrató el agente del encargo sino uno de sus subcontratados: cuál. */
64
+ via?: Address;
65
+ /** Cuántos archivos se le mandaron con la pregunta: tu información pasó por él. */
66
+ files?: number;
67
+ }
68
+ /**
69
+ * Construye el bloque. Orden de claves fijo y `\n`: se ancla en la cadena, y
70
+ * las mismas subcontrataciones tienen que dar exactamente los mismos bytes.
71
+ */
72
+ export declare function buildSubcontractsManifest(list: SubcontractRecord[]): string;
73
+ /** El texto de la entrega con las subcontrataciones al final. Sin ninguna, igual que estaba. */
74
+ export declare function appendSubcontractsManifest(text: string, list: SubcontractRecord[]): string;
75
+ /**
76
+ * Lee las subcontrataciones anunciadas en una entrega.
77
+ *
78
+ * Nunca lanza: un bloque mal formado se salta y los demás se leen. Que una
79
+ * línea rota impida leer la entrega sería peor que no enseñar esa línea.
80
+ */
81
+ export declare function parseSubcontractsManifest(text: string): SubcontractRecord[];
82
+ /** La forma JSON, para la respuesta de una pregunta x402. */
83
+ export interface SubcontractJson {
84
+ agent: string;
85
+ skill: string;
86
+ amount: string;
87
+ currency: string;
88
+ asset: string;
89
+ decimals: number;
90
+ tx?: string;
91
+ via?: string;
92
+ files?: number;
93
+ }
94
+ export declare function subcontractsToJson(list: SubcontractRecord[]): SubcontractJson[];
95
+ /**
96
+ * Lee la lista que manda otro agente en su respuesta. Es entrada no fiable:
97
+ * se valida entrada a entrada y lo que no cuadra se descarta, nunca se lanza.
98
+ */
99
+ export declare function subcontractsFromJson(value: unknown): SubcontractRecord[];
100
+ /**
101
+ * Lo que apunta un agente al subcontratar: la subcontratación directa y, detrás,
102
+ * las que hizo a su vez el que respondió, con `via` para saber de dónde vienen.
103
+ */
104
+ export declare function subcontractsOfAnswer(direct: Omit<SubcontractRecord, 'via'>, nested?: SubcontractRecord[]): SubcontractRecord[];
@@ -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
+ }
@@ -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.26.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",
@@ -50,7 +50,7 @@
50
50
  "scripts": {
51
51
  "build": "tsc -p tsconfig.json",
52
52
  "typecheck": "tsc -p tsconfig.json --noEmit",
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",
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",
54
54
  "test:nombres": "tsx test/nombres.test.ts"
55
55
  }
56
56
  }