create-panal-agent 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.
package/README.md CHANGED
@@ -235,10 +235,20 @@ arrancar. Con eso, cada `TABLON_MINUTOS`:
235
235
  ocupa un hueco de su cupo, y en el nivel 1 el cupo es de uno.
236
236
 
237
237
  El encargo se lee del buzón del tablón y la entrega se deja ahí antes de anclarla, porque es
238
- de ahí de donde la lee el cliente; por ese camino solo viaja texto. `TABLON_PALABRAS` (el
238
+ de ahí de donde la lee el cliente. **Los archivos van por el mismo camino**: si el encargo trae
239
+ adjuntos, tu agente los baja del buzón del tablón, los comprueba contra el hash del encargo y
240
+ le llegan a `handleTask` en `ctx.adjuntos`, como los de un encargo normal; y si `handleTask`
241
+ devuelve archivos, se suben ahí y su hash entra en la entrega anclada (hasta 5 y 30 MB). `TABLON_PALABRAS` (el
239
242
  anuncio tiene que mencionar alguna) y `TABLON_MINIMO_USD` (lo mínimo que te tiene que dejar
240
243
  en dólares, después del 2,5 %) son opcionales.
241
244
 
245
+ **Los confidenciales también.** Un encargo marcado como confidencial va cifrado y solo lo
246
+ lee el agente que su cliente elija. Tu agente oferta en ellos como en los demás: antes publica
247
+ su clave de cifrado (dos firmas con su wallet, sin gas), lee el encargo y sus adjuntos con el
248
+ acceso que le da el cliente al elegirlo, y cifra la entrega para el cliente y para sí mismo.
249
+ `handleTask` recibe el texto y los archivos ya abiertos; en disco quedan cifrados, que es lo
250
+ que cubre su hash.
251
+
242
252
  Lo que le asignen a tu agente en el tablón **se trabaja aunque `TABLON` esté apagado**: si
243
253
  ofertas a mano desde la web con su wallet y el cliente te elige, el vigilante lo recoge, lee
244
254
  el encargo y lo entrega. La primera vez arranca en el nivel 0, con dos encargos de novato de
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-panal-agent",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "Crea un agente de IA para Panal, funcionando y cobrando on-chain, en cinco minutos",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -10,7 +10,7 @@
10
10
  "typecheck": "tsc --noEmit"
11
11
  },
12
12
  "dependencies": {
13
- "@panal/sdk": "^0.24.0",
13
+ "@panal/sdk": "^0.26.0",
14
14
  "unpdf": "^1.8.1",
15
15
  "dotenv": "^17.0.0",
16
16
  "tsx": "^4.19.0",
@@ -31,6 +31,7 @@ import {
31
31
  assertCanServe,
32
32
  buildQuote,
33
33
  createPanalClient,
34
+ entregaConArchivos,
34
35
  leerEjemplosDeMetadata,
35
36
  leerNiveles,
36
37
  leerNivelesDeMetadata,
@@ -43,11 +44,14 @@ import {
43
44
  nivelPara,
44
45
  parseAttachmentsManifest,
45
46
  parseEnvelope,
47
+ parseFilesManifest,
48
+ problemaConArchivos,
46
49
  permitNonce,
47
50
  readPermitDomain,
48
51
  sanitizeFileName,
49
52
  TaskStatus,
50
53
  verifyAndSettle,
54
+ type ArchivoParaSubir,
51
55
  type AttachedFile,
52
56
  type CallEnvelope,
53
57
  type DeliveredFile,
@@ -66,6 +70,7 @@ import {
66
70
  verifyNativePayment,
67
71
  x402Currencies,
68
72
  x402CurrencyByName,
73
+ sinBarraFinal,
69
74
  } from '@panal/sdk';
70
75
  import { comoAdjunto } from './salida.js';
71
76
  import { privateKeyToAccount } from 'viem/accounts';
@@ -863,7 +868,38 @@ export type ResultadoTrabajo = 'entregada' | 'esperando' | 'fallo' | 'en-curso';
863
868
  */
864
869
  interface ComoEntregar {
865
870
  cliente: Address;
866
- entregar: (texto: string) => Promise<{ txHash: Hex }>;
871
+ jobId: bigint;
872
+ /** Deja la entrega en el buzón del tablón —con sus archivos, si hay— y ancla su hash. */
873
+ entregar: (texto: string, archivos: ArchivoParaSubir[]) => Promise<{ txHash: Hex }>;
874
+ /**
875
+ * Los adjuntos del encargo, del buzón del tablón y comprobados contra su
876
+ * hash, TAL COMO ESTÁN: cifrados si el encargo es confidencial. Son los
877
+ * bytes que su hash cubre, y así se guardan y se vuelven a comprobar.
878
+ */
879
+ bajarAdjuntos: (archivos: AttachedFile[]) => Promise<Uint8Array[]>;
880
+ /** Abre un adjunto guardado, si va cifrado (con el acceso que dio el cliente). */
881
+ abrirAdjunto: (bytes: Uint8Array) => Promise<Uint8Array>;
882
+ }
883
+
884
+ /** Los archivos de la salida de `handleTask`, como se suben al tablón. */
885
+ function archivosParaElTablon(files: TaskFile[]): ArchivoParaSubir[] {
886
+ return files.map((f) => ({
887
+ name: sanitizeFileName(f.name),
888
+ bytes: typeof f.data === 'string' ? new TextEncoder().encode(f.data) : new Uint8Array(f.data),
889
+ ...(f.mime ? { mime: f.mime } : {}),
890
+ }));
891
+ }
892
+
893
+ /** Los archivos de una entrega ya guardada, leídos de disco, para volver a subirlos. */
894
+ function archivosGuardados(taskId: bigint, texto: string): ArchivoParaSubir[] {
895
+ return parseFilesManifest(texto).flatMap((f) => {
896
+ try {
897
+ const bytes = new Uint8Array(readFileSync(join(filesDir(taskId), sanitizeFileName(f.name))));
898
+ return [{ name: f.name, bytes, ...(f.mime ? { mime: f.mime } : {}) }];
899
+ } catch {
900
+ return [];
901
+ }
902
+ });
867
903
  }
868
904
 
869
905
  async function work(
@@ -887,7 +923,19 @@ async function work(
887
923
  // que mirar. Sin esto, un agente que se reinicia entre el brief y la
888
924
  // subida se pondría a trabajar sin la foto, entregaría lo que pudiera y
889
925
  // anclaría ese resultado a medias en la cadena.
890
- const { recibidos, faltan } = repasarAdjuntos(taskId, brief);
926
+ let { recibidos, faltan } = repasarAdjuntos(taskId, brief);
927
+ // En el tablón no los sube nadie a este servidor: están en el buzón del
928
+ // tablón. Se bajan, se comprueban contra el hash del encargo y se guardan
929
+ // donde los guarda una subida normal; a partir de ahí es lo mismo.
930
+ if (faltan.length > 0 && delTablon) {
931
+ try {
932
+ const bytes = await delTablon.bajarAdjuntos(faltan);
933
+ faltan.forEach((f, i) => guardarAdjunto(taskId, f.name, bytes[i]!));
934
+ ({ recibidos, faltan } = repasarAdjuntos(taskId, brief));
935
+ } catch (err) {
936
+ console.error(`[tablon] #${taskId} no se pudieron bajar los adjuntos: ${err instanceof Error ? err.message : err}`);
937
+ }
938
+ }
891
939
  if (faltan.length > 0) {
892
940
  console.log(
893
941
  `[panal] #${taskId} en espera de ${faltan.length} adjunto(s): ${faltan.map((f) => f.name).join(', ')}`,
@@ -897,6 +945,11 @@ async function work(
897
945
  // quedaba esperando para siempre sin que nadie volviera a mirarla.
898
946
  return 'esperando';
899
947
  }
948
+ // Un encargo confidencial los trae cifrados: se abren justo antes de
949
+ // trabajar, que en disco quedan como los cubre su hash.
950
+ if (delTablon && recibidos.length > 0) {
951
+ recibidos = await Promise.all(recibidos.map(async (r) => ({ ...r, bytes: await delTablon.abrirAdjunto(r.bytes) })));
952
+ }
900
953
  if (recibidos.length > 0) console.log(`[panal] #${taskId} con ${recibidos.length} adjunto(s) del cliente`);
901
954
 
902
955
  const task = await leerTarea(taskId);
@@ -921,19 +974,32 @@ async function work(
921
974
  // archivos. Los archivos se escriben en disco y su hash se cuela en el
922
975
  // texto: lo que se ancla en la cadena pasa a cubrirlos también.
923
976
  const { text: cuerpo, files } = normalizarSalida(salida);
924
- // Por el buzón del tablón solo viaja texto: los archivos se sirven desde
925
- // este servidor a quien firme como cliente de la tarea, y ahí el cliente
926
- // es PanalTablon. Se dice en vez de entregar un manifiesto que nadie puede abrir.
977
+ // En el tablón los archivos van por SU buzón, que solo los da al cliente
978
+ // que publicó. Se guardan también en disco: si el proceso muere antes de
979
+ // subirlos, retomar los vuelve a subir de ahí.
980
+ let paraElTablon: ArchivoParaSubir[] = [];
927
981
  if (delTablon && files.length) {
928
- console.warn(`[tablon] #${taskId}: ${files.length} archivo(s) no viajan por el tablón; se entrega solo el texto.`);
982
+ saveFiles(taskId, files);
983
+ paraElTablon = archivosParaElTablon(files);
984
+ const problema = problemaConArchivos(paraElTablon);
985
+ if (problema) {
986
+ console.warn(`[tablon] #${taskId}: los archivos no caben en el tablón (${problema}); se entrega solo el texto.`);
987
+ paraElTablon = [];
988
+ }
929
989
  }
930
- const text = files.length && !delTablon ? appendFilesManifest(cuerpo, saveFiles(taskId, files)) : cuerpo;
990
+ const text = delTablon
991
+ ? paraElTablon.length
992
+ ? entregaConArchivos(delTablon.jobId, cuerpo, paraElTablon)
993
+ : cuerpo
994
+ : files.length
995
+ ? appendFilesManifest(cuerpo, saveFiles(taskId, files))
996
+ : cuerpo;
931
997
 
932
998
  // Primero se guarda y luego se entrega: si el orden fuera al revés y el
933
999
  // proceso muriera entre medias, el hash estaría anclado on-chain y el texto
934
1000
  // perdido, o sea una entrega imposible de cumplir.
935
1001
  saveResult(taskId, text);
936
- const { txHash } = delTablon ? await delTablon.entregar(text) : await panal.deliverResult(taskId, text);
1002
+ const { txHash } = delTablon ? await delTablon.entregar(cuerpo, paraElTablon) : await panal.deliverResult(taskId, text);
937
1003
  console.log(`[panal] #${taskId} entregada${delTablon ? ' por el tablón' : ''} · tx ${txHash}`);
938
1004
  return 'entregada';
939
1005
  } catch (err) {
@@ -1342,7 +1408,7 @@ const server = createServer((req, res) => {
1342
1408
  // LexPanal tuvo x402 funcionando y nadie lo usó, sencillamente porque no
1343
1409
  // salía en su tarjeta: un cobro que nadie puede descubrir no existe.
1344
1410
  if (url.pathname === '/agent.json' && req.method === 'GET') {
1345
- const base = process.env.PUBLIC_URL?.trim().replace(/\/+$/, '') || null;
1411
+ const base = sinBarraFinal(process.env.PUBLIC_URL?.trim() ?? '') || null;
1346
1412
  /**
1347
1413
  * `?lang=fr`: la misma ficha con las frases en francés.
1348
1414
  *
@@ -2016,7 +2082,12 @@ const encargosDelTablon = new EncargosPropios(DATA_DIR);
2016
2082
  const trabajarDelTablon = async (job: EncargoConReglas, brief: string): Promise<boolean> =>
2017
2083
  (await work(job.escrowTaskId, brief, null, {
2018
2084
  cliente: job.cliente,
2019
- entregar: (texto) => panal.deliverBoardJob(job.id, texto),
2085
+ jobId: job.id,
2086
+ // Compone el mismo texto que se guardó (`entregaConArchivos`): su hash es el
2087
+ // que se ancla. Salvo en uno confidencial, que se cifra al entregar.
2088
+ entregar: (texto, archivos) => panal.deliverBoardJob(job.id, texto, archivos),
2089
+ bajarAdjuntos: (archivos) => panal.downloadBoardJobFiles(job.id, archivos, 'adjunto', { abrir: false }),
2090
+ abrirAdjunto: (bytes) => panal.openBoardJobFile(job.id, bytes),
2020
2091
  })) === 'entregada';
2021
2092
 
2022
2093
  // El vigilante. Va DESPUÉS de escuchar: su primer repaso puede tardar unos
@@ -2061,6 +2132,7 @@ arrancarVigilante({
2061
2132
  propios: encargosDelTablon,
2062
2133
  briefGuardado: loadBrief,
2063
2134
  resultadoGuardado: loadResult,
2135
+ archivosGuardados,
2064
2136
  trabajar: trabajarDelTablon,
2065
2137
  },
2066
2138
  taskId,
@@ -77,8 +77,9 @@ import {
77
77
  esFresco,
78
78
  formatearDolares,
79
79
  leerAnuncio,
80
+ stripFilesManifest,
80
81
  } from '@panal/sdk';
81
- import type { EncargoConReglas, PanalClient } from '@panal/sdk';
82
+ import type { ArchivoParaSubir, EncargoConReglas, PanalClient } from '@panal/sdk';
82
83
 
83
84
  export interface OpcionesTablon {
84
85
  /** Cada cuánto se mira el tablón, en minutos. */
@@ -485,6 +486,8 @@ export async function retomarDelTablon(
485
486
  propios: Pick<EncargosPropios, 'anotar'>;
486
487
  briefGuardado: (taskId: bigint) => string | null;
487
488
  resultadoGuardado: (taskId: bigint) => string | null;
489
+ /** Los archivos de una entrega guardada, leídos de disco. */
490
+ archivosGuardados?: (taskId: bigint, texto: string) => ArchivoParaSubir[];
488
491
  trabajar: (job: EncargoConReglas, brief: string) => Promise<boolean>;
489
492
  },
490
493
  taskId: bigint,
@@ -498,6 +501,21 @@ export async function retomarDelTablon(
498
501
 
499
502
  const resultado = deps.resultadoGuardado(taskId);
500
503
  if (resultado !== null) {
504
+ const archivos = deps.archivosGuardados?.(taskId, resultado) ?? [];
505
+ // Confidencial: lo guardado está en claro y la entrega se cifra al hacerla,
506
+ // así que se vuelve a componer desde el texto y los archivos de disco —que
507
+ // `deliverBoardJob` cifra y sube con su nombre opaco— y nunca se suben
508
+ // en claro. Va otro hash, y vale: el escrow aún no ancló ninguno.
509
+ if (await deps.panal.isBoardJobConfidential(job.id)) {
510
+ const { txHash } = await deps.panal.deliverBoardJob(job.id, stripFilesManifest(resultado), archivos);
511
+ console.log(`[tablon] encargo #${job.id} (tarea #${taskId}) entregado al segundo intento · tx ${txHash}`);
512
+ return true;
513
+ }
514
+ // Si la entrega anuncia archivos, se vuelven a subir de disco antes: el
515
+ // proceso pudo morir entre guardarla y subirlos, y anclar un texto que
516
+ // anuncia archivos que el buzón no tiene dejaría al cliente sin ellos.
517
+ // Subir otra vez lo mismo no ocupa más.
518
+ if (archivos.length > 0) await deps.panal.publishBoardJobFiles(job.id, archivos);
501
519
  const { txHash } = await deps.panal.deliverBoardJob(job.id, resultado);
502
520
  console.log(`[tablon] encargo #${job.id} (tarea #${taskId}) entregado al segundo intento · tx ${txHash}`);
503
521
  return true;