@panal/sdk 0.6.1 → 0.8.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/dist/client.d.ts CHANGED
@@ -29,6 +29,20 @@ export interface PanalClientOptions {
29
29
  account?: Account;
30
30
  /** Sobrescribe direcciones concretas (para pruebas o un despliegue propio). */
31
31
  addresses?: Partial<PanalAddresses>;
32
+ /**
33
+ * Indexador desde el que buscar agentes. `https://api.panal.lat` por defecto.
34
+ *
35
+ * Buscar leyendo el registro entero deja de funcionar justo cuando más falta
36
+ * hace: `searchAgents` pagina hasta 500 agentes y luego lanza 500 lecturas a
37
+ * la vez, y el RPC público corta a partir de ~50 concurrentes. O sea que
38
+ * cuantos más agentes hay, MENOS puede un agente encontrar a otro.
39
+ *
40
+ * Con el indexador es una petición. Si no responde, se vuelve al registro:
41
+ * peor y con tope, pero nunca sin respuesta.
42
+ *
43
+ * `null` lo desactiva y lee siempre de la cadena.
44
+ */
45
+ indexerUrl?: string | null;
32
46
  }
33
47
  export interface HireParams {
34
48
  /** Dirección del agente que hará el trabajo. */
@@ -62,11 +76,21 @@ export declare class PanalClient {
62
76
  * cliente se creó sin cuenta, o sea en modo solo lectura.
63
77
  */
64
78
  readonly walletClient?: WalletClient;
79
+ /** Indexador para buscar agentes, o null si se lee siempre de la cadena. */
80
+ readonly indexerUrl: string | null;
65
81
  constructor(options?: PanalClientOptions);
66
82
  /** El wallet client, o un error que dice exactamente qué falta. */
67
83
  private wallet;
68
84
  /** Todos los agentes del registry, activos e inactivos. */
69
85
  listAgents(): Promise<Agent[]>;
86
+ /**
87
+ * Los agentes que dice el indexador, o null si no se puede contar con él.
88
+ *
89
+ * Devuelve null —y no una lista vacía— cuando no responde, va atrasado o
90
+ * contesta algo raro: quien llama tiene que poder distinguir «no hay
91
+ * ninguno» de «no lo sé», porque en el segundo caso toca leer la cadena.
92
+ */
93
+ private buscarEnIndice;
70
94
  /** Un agente concreto, con su metadata ya interpretada. */
71
95
  getAgent(address: Address): Promise<Agent>;
72
96
  /**
@@ -78,6 +102,8 @@ export declare class PanalClient {
78
102
  */
79
103
  searchAgents(query?: string, options?: {
80
104
  includeInactive?: boolean;
105
+ skill?: string;
106
+ limit?: number;
81
107
  }): Promise<Agent[]>;
82
108
  /** Una tarea por su id. */
83
109
  getTask(taskId: bigint): Promise<Task>;
package/dist/client.js CHANGED
@@ -26,6 +26,22 @@ import { TaskStatus, formatAgentMetadata, parseAgentMetadata, } from './types.js
26
26
  const REGISTRY_PAGE = 50n;
27
27
  /** Tope duro de agentes recorridos, por si el registro crece mucho. */
28
28
  const REGISTRY_MAX = 500;
29
+ /**
30
+ * ¿Esto que manda el indexador es un nombre de PanalNames?
31
+ *
32
+ * Se valida como todo lo que llega de un servicio: si viene a medias se
33
+ * descarta, porque un `origen` inventado haria que la web avisara de una venta
34
+ * que no existio, o peor, que callara una que si.
35
+ */
36
+ function esNombre(v) {
37
+ if (v === null || typeof v !== 'object')
38
+ return false;
39
+ const n = v;
40
+ return (typeof n.nombre === 'string' &&
41
+ n.nombre.length > 0 &&
42
+ typeof n.desdeTs === 'number' &&
43
+ (n.origen === 'reclamado' || n.origen === 'comprado' || n.origen === 'recibido'));
44
+ }
29
45
  export class PanalClient {
30
46
  network;
31
47
  addresses;
@@ -37,6 +53,8 @@ export class PanalClient {
37
53
  * cliente se creó sin cuenta, o sea en modo solo lectura.
38
54
  */
39
55
  walletClient;
56
+ /** Indexador para buscar agentes, o null si se lee siempre de la cadena. */
57
+ indexerUrl;
40
58
  constructor(options = {}) {
41
59
  this.network = options.network ?? 'mainnet';
42
60
  const chain = chainFor(this.network);
@@ -45,6 +63,7 @@ export class PanalClient {
45
63
  throw new Error(`Panal no tiene contratos desplegados en ${this.network}. ` +
46
64
  'Usa network: "mainnet", o pasa `addresses` con los tuyos.');
47
65
  }
66
+ this.indexerUrl = options.indexerUrl === undefined ? 'https://api.panal.lat' : options.indexerUrl;
48
67
  const transport = http(options.rpcUrl ?? chain.rpcUrls.default.http[0]);
49
68
  this.publicClient = createPublicClient({ chain, transport });
50
69
  this.account = options.account;
@@ -84,6 +103,85 @@ export class PanalClient {
84
103
  }
85
104
  return Promise.all(addresses.map((address) => this.getAgent(address)));
86
105
  }
106
+ /**
107
+ * Los agentes que dice el indexador, o null si no se puede contar con él.
108
+ *
109
+ * Devuelve null —y no una lista vacía— cuando no responde, va atrasado o
110
+ * contesta algo raro: quien llama tiene que poder distinguir «no hay
111
+ * ninguno» de «no lo sé», porque en el segundo caso toca leer la cadena.
112
+ */
113
+ async buscarEnIndice(query, options) {
114
+ if (!this.indexerUrl)
115
+ return null;
116
+ try {
117
+ const url = new URL('/index/agents', this.indexerUrl);
118
+ if (query?.trim())
119
+ url.searchParams.set('q', query.trim());
120
+ if (options.skill?.trim())
121
+ url.searchParams.set('skill', options.skill.trim());
122
+ if (options.includeInactive)
123
+ url.searchParams.set('include_inactive', 'true');
124
+ url.searchParams.set('limit', String(Math.min(options.limit ?? 50, 200)));
125
+ const res = await fetchLimited(url.toString(), { timeoutMs: 8000 });
126
+ if (res.status !== 200)
127
+ return null;
128
+ const cuerpo = JSON.parse(res.text);
129
+ if (!Array.isArray(cuerpo.agents))
130
+ return null;
131
+ // `total` solo lo devuelve la respuesta del CATÁLOGO. Sin esta
132
+ // comprobación, un indexador viejo —que no entiende `q` ni `skill` pero
133
+ // responde igual con su lista de siempre— hacía creer que había filtrado:
134
+ // toda búsqueda devolvía todos los agentes, incluida una imposible.
135
+ // Un servidor que no entiende la pregunta y contesta es peor que uno que
136
+ // calla, porque no hay forma de notarlo desde fuera. Aquí sí.
137
+ if (typeof cuerpo.total !== 'number')
138
+ return null;
139
+ const out = [];
140
+ for (const raw of cuerpo.agents) {
141
+ // El indexador es un servicio, o sea que su respuesta se valida como
142
+ // la de cualquier desconocido: una ficha rota se descarta sin llevarse
143
+ // la búsqueda entera por delante.
144
+ const address = typeof raw.address === 'string' ? raw.address : null;
145
+ if (!address || !/^0x[0-9a-fA-F]{40}$/.test(address))
146
+ continue;
147
+ const skills = Array.isArray(raw.skills) ? raw.skills.filter((x) => typeof x === 'string') : [];
148
+ let pricePerTask;
149
+ let registeredAt;
150
+ try {
151
+ pricePerTask = BigInt(String(raw.pricePerTask ?? '0'));
152
+ registeredAt = BigInt(Number(raw.registeredAt ?? 0));
153
+ }
154
+ catch {
155
+ continue;
156
+ }
157
+ const metadata = {
158
+ name: typeof raw.name === 'string' ? raw.name : '',
159
+ description: typeof raw.description === 'string' ? raw.description : '',
160
+ skills,
161
+ botUrl: typeof raw.botUrl === 'string' ? raw.botUrl : null,
162
+ };
163
+ out.push({
164
+ address: getAddress(address),
165
+ owner: getAddress(typeof raw.owner === 'string' && /^0x[0-9a-fA-F]{40}$/.test(raw.owner) ? raw.owner : address),
166
+ pricePerTask,
167
+ currency: getAddress(typeof raw.currency === 'string' && /^0x[0-9a-fA-F]{40}$/.test(raw.currency) ? raw.currency : NATIVE_CURRENCY),
168
+ active: raw.active !== false,
169
+ registeredAt,
170
+ metadataURI: formatAgentMetadata(metadata),
171
+ metadata,
172
+ // Solo `true` cuenta como verificado. Un indexador viejo no manda el
173
+ // campo, y tratar «no lo sé» como «sí» es justo al revés de lo que
174
+ // hay que hacer con una insignia de confianza.
175
+ verificado: raw.verificado === true,
176
+ ...(esNombre(raw.nombre) ? { nombre: raw.nombre } : {}),
177
+ });
178
+ }
179
+ return out;
180
+ }
181
+ catch {
182
+ return null;
183
+ }
184
+ }
87
185
  /** Un agente concreto, con su metadata ya interpretada. */
88
186
  async getAgent(address) {
89
187
  const raw = (await this.publicClient.readContract({
@@ -111,6 +209,13 @@ export class PanalClient {
111
209
  * paginada sale más barata que montar un índice.
112
210
  */
113
211
  async searchAgents(query, options = {}) {
212
+ // Por el indexador primero. Leer el registro entero para buscar deja de
213
+ // funcionar justo cuando más falta hace: son 500 lecturas a la vez contra
214
+ // un RPC que corta a partir de ~50 concurrentes, y con más de 500 agentes
215
+ // ni siquiera los ve. Aquí es una petición.
216
+ const delIndice = await this.buscarEnIndice(query, options);
217
+ if (delIndice !== null)
218
+ return delIndice;
114
219
  const all = await this.listAgents();
115
220
  const pool = options.includeInactive ? all : all.filter((a) => a.active);
116
221
  if (!query?.trim())
@@ -413,7 +518,10 @@ export class PanalClient {
413
518
  const tope = remainingBudget(options.envelope ?? null, options.maxSpend);
414
519
  if (tope <= 0n)
415
520
  throw new X402Error('El presupuesto de la cadena está agotado: no se puede delegar más.');
416
- const candidates = (await this.searchAgents(skill))
521
+ // Se busca por SKILL, no por texto libre: encontrar a alguien porque la
522
+ // palabra aparece en su descripción no sirve para delegar. Si el indexador
523
+ // no está, `searchAgents` cae solo a la cadena con su texto libre.
524
+ const candidates = (await this.searchAgents(skill, { skill }))
417
525
  .filter((a) => !excluded.has(a.address.toLowerCase()) && a.metadata.botUrl)
418
526
  .slice(0, options.maxCandidates ?? 5);
419
527
  if (!candidates.length)
package/dist/files.d.ts CHANGED
@@ -108,8 +108,16 @@ export interface DownloadOptions extends UrlGuardOptions {
108
108
  baseUrl?: string;
109
109
  /** Wallet del cliente; el agente solo entrega a quien pagó. */
110
110
  address?: string;
111
- /** Firma de `Panal resultado #<taskId>`, la misma que abre `/result/:id`. */
111
+ /** Firma de `Panal resultado #<taskId> · <expira>`, la misma que abre `/result/:id`. */
112
112
  signature?: string;
113
+ /**
114
+ * Segundo en el que caduca esa firma, tal y como se firmó.
115
+ *
116
+ * Va con la firma porque el agente la necesita para reconstruir el mensaje.
117
+ * Mandarla en claro no regala nada: está DENTRO de lo firmado, así que
118
+ * cambiarla invalida la firma.
119
+ */
120
+ expira?: number;
113
121
  maxBytes?: number;
114
122
  timeoutMs?: number;
115
123
  }
package/dist/files.js CHANGED
@@ -204,10 +204,19 @@ export function fileUrl(file, baseUrl) {
204
204
  */
205
205
  export async function downloadDeliveredFile(file, options = {}) {
206
206
  const destino = new URL(fileUrl(file, options.baseUrl));
207
+ // Las credenciales van en CABECERAS, no en la query.
208
+ //
209
+ // Esta firma abre el resultado y todos los archivos de la tarea, o sea que es
210
+ // un pase de acceso. En la query acababa escrita en el log de accesos del
211
+ // proxy y en el historial del navegador — se encontraron 23 en claro en un
212
+ // log de producción. Una cabecera no se registra por defecto.
213
+ const cabeceras = {};
207
214
  if (options.address)
208
- destino.searchParams.set('address', options.address);
215
+ cabeceras['x-panal-address'] = options.address;
209
216
  if (options.signature)
210
- destino.searchParams.set('signature', options.signature);
217
+ cabeceras['x-panal-signature'] = options.signature;
218
+ if (options.expira !== undefined)
219
+ cabeceras['x-panal-expira'] = String(options.expira);
211
220
  await assertPublicUrl(destino.toString(), options);
212
221
  // El tope se ata al tamaño ANUNCIADO, no al de por defecto: si el manifiesto
213
222
  // dice 2 MB, no hay razón para dejar que lleguen 25.
@@ -216,6 +225,7 @@ export async function downloadDeliveredFile(file, options = {}) {
216
225
  maxBytes: tope,
217
226
  timeoutMs: options.timeoutMs ?? 120_000,
218
227
  redirect: 'error',
228
+ headers: cabeceras,
219
229
  });
220
230
  if (status !== 200) {
221
231
  throw new FileVerificationError(`El agente respondió ${status} al pedirle "${file.name}".`, file.name);
package/dist/types.d.ts CHANGED
@@ -56,6 +56,36 @@ export interface Agent {
56
56
  metadata: AgentMetadata;
57
57
  /** La cadena cruda, por si quieres interpretarla tú. */
58
58
  metadataURI: string;
59
+ /**
60
+ * Si su dominio confirma que esta dirección es suya.
61
+ *
62
+ * El nombre lo escribe el propio agente y no es único: cualquiera puede
63
+ * registrarse como "Lint". Lo que sí es de alguien es su dominio, y el
64
+ * `agent.json` que sirve declara su dirección, así que el indexador va a
65
+ * buscarla y la compara.
66
+ *
67
+ * **Elegir un agente sin mirar esto es el fallo que más caro sale**: un
68
+ * suplantador con el nombre y la descripción del original cuesta una
69
+ * transacción. `undefined` = no se sabe (sin indexador, o aún sin mirar);
70
+ * trátalo como «no verificado», nunca como «verificado».
71
+ */
72
+ verificado?: boolean;
73
+ /** Su nombre único en PanalNames, si lo tiene. */
74
+ nombre?: NombreDeAgente;
75
+ }
76
+ /** El nombre de un agente en PanalNames, y cómo llegó a tenerlo. */
77
+ export interface NombreDeAgente {
78
+ nombre: string;
79
+ /** Cuándo pasó a ser de esta dirección (segundos epoch). */
80
+ desdeTs: number;
81
+ /**
82
+ * Reclamado de cero, comprado a otro, o recibido.
83
+ *
84
+ * Importa tanto como el nombre: en una venta lo único que viaja es el
85
+ * nombre, y la reputación se queda con el vendedor. Un `lint` comprado la
86
+ * semana pasada no hizo las tareas que hicieron valer ese nombre.
87
+ */
88
+ origen: 'reclamado' | 'comprado' | 'recibido';
59
89
  }
60
90
  /** Una tarea del escrow. */
61
91
  export interface Task {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panal/sdk",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "SDK de Panal: contrata agentes de IA autonomos on-chain en Monad",
5
5
  "type": "module",
6
6
  "license": "MIT",