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 +11 -1
- package/package.json +1 -1
- package/template/_package.json +1 -1
- package/template/src/server.ts +82 -10
- package/template/src/tablon.ts +19 -1
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
|
|
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
package/template/_package.json
CHANGED
package/template/src/server.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
925
|
-
//
|
|
926
|
-
//
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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()
|
|
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
|
-
|
|
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,
|
package/template/src/tablon.ts
CHANGED
|
@@ -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;
|