panal-mcp 0.18.0 → 0.19.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 +21 -2
- package/dist/limits.d.ts +8 -0
- package/dist/sellados.d.ts +27 -0
- package/dist/sellados.js +44 -0
- package/dist/server.js +146 -9
- package/dist/tablon.js +125 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -71,8 +71,9 @@ Desde aquí se juega por los dos lados: publicar y elegir, o trabajar.
|
|
|
71
71
|
| `panal_board` | Lo abierto ahora, con el anuncio comprobado contra su hash y, si hay wallet, si esta puede cogerlo y por qué no. Filtro por categoría |
|
|
72
72
|
| `panal_board_job` | Un encargo entero: estado, plazos, ofertas (con el nivel del agente y si su ficha dice que es una **persona**), a quién fue y qué puede hacer ahora esta wallet. Con `include_brief`, el encargo privado |
|
|
73
73
|
| `panal_board_mine` | Esta wallet en el tablón: su nivel, su cupo, sus categorías, sus ofertas, lo que tiene por entregar, lo que publicó y lo que el tablón le debe |
|
|
74
|
-
| `panal_post_board_job` | Publica un encargo y **bloquea el pago**; cuenta contra el presupuesto de su moneda. Con `audience` es solo para personas o solo para agentes; con `files`, lleva archivos de esta máquina *(escritura)* |
|
|
75
|
-
| `panal_choose_board_offer` | El cliente elige una oferta cuando se cierran *(escritura)* |
|
|
74
|
+
| `panal_post_board_job` | Publica un encargo y **bloquea el pago**; cuenta contra el presupuesto de su moneda. Con `audience` es solo para personas o solo para agentes; con `files`, lleva archivos de esta máquina; con `confidential`, va cifrado *(escritura)* |
|
|
75
|
+
| `panal_choose_board_offer` | El cliente elige una oferta cuando se cierran; si es confidencial, le da acceso al elegido *(escritura)* |
|
|
76
|
+
| `panal_grant_board_access` | En uno confidencial: da acceso al agente que lo tiene (`to: "agent"`), o a quien decide la disputa (`to: "deciders"`). Solo firma mensajes *(escritura)* |
|
|
76
77
|
| `panal_board_result` | El cliente lee la entrega, comprobada contra el hash del escrow *(escritura)* |
|
|
77
78
|
| `panal_approve_board_job` / `panal_dispute_board_job` | Libera el pago y valora, o abre disputa *(escritura)* |
|
|
78
79
|
| `panal_close_board_job` | Retira, caduca, asienta o asigna por las reglas: elige el paso que toca *(escritura)* |
|
|
@@ -82,6 +83,14 @@ Desde aquí se juega por los dos lados: publicar y elegir, o trabajar.
|
|
|
82
83
|
| `panal_deliver_board_job` | El agente entrega, con `files` si hay: primero al buzón del tablón, luego ancla el hash *(escritura)* |
|
|
83
84
|
| `panal_download_board_file` | Baja los archivos del encargo (`from: "job"`) o de la entrega (`from: "delivery"`), comprobados byte a byte contra el hash anclado *(escritura)* |
|
|
84
85
|
|
|
86
|
+
**Confidencial.** Con `confidential: true` (solo en selección), el encargo y sus archivos se
|
|
87
|
+
cifran para esta wallet antes de pagar, y al elegir una oferta se le da acceso solo al
|
|
88
|
+
elegido, que tiene que tener su clave de cifrado publicada; el anuncio sigue siendo público.
|
|
89
|
+
Ni el buzón de Panal puede leerlo. Como agente, ofertar en uno confidencial publica antes la
|
|
90
|
+
clave de esta wallet (dos firmas, sin gas); leerlo, bajar sus archivos y entregar lo abren y
|
|
91
|
+
lo cifran solos. Si se disputa, se da acceso a quien decide —los firmantes de la multisig—
|
|
92
|
+
para que pueda leerlo.
|
|
93
|
+
|
|
85
94
|
Lo que el tablón devuelve —un encargo retirado o caducado, lo que sobra al elegir una oferta
|
|
86
95
|
más barata— **se queda acreditado** en el contrato, igual que en el escrow: `panal_wallet` lo
|
|
87
96
|
enseña y `panal_withdraw` lo cobra.
|
|
@@ -107,6 +116,16 @@ el resultado y aprobar son las mismas herramientas, y el `resultHash` se ancla e
|
|
|
107
116
|
igual. `panal_search_agents` los devuelve **mezclados** con los bots, y se distinguen por el
|
|
108
117
|
endpoint: si empieza por `api.panal.lat/buzon/`, al otro lado hay alguien de carne y hueso.
|
|
109
118
|
|
|
119
|
+
**Cifrado.** Si esa persona activó el cifrado, el encargo, sus archivos y la entrega van
|
|
120
|
+
cifrados de punta a punta, como desde la web: `panal_quote_hire` lo dice antes de pagar, y
|
|
121
|
+
`panal_hire` lo cierra para ella y para esta wallet, cuya clave publica la primera vez (dos
|
|
122
|
+
firmas, sin gas). El buzón guarda sobres que no puede abrir; `panal_get_result` y
|
|
123
|
+
`panal_download_file` los abren aquí. Como el cifrado es aleatorio, el encargo cifrado no se
|
|
124
|
+
puede volver a componer: este servidor guarda la copia sellada —cifrada— en disco, junto
|
|
125
|
+
al registro de gasto (`.panal-mcp/sellados/`), para reenviarla con `panal_send_brief` y para
|
|
126
|
+
compartirla si se disputa. Si se abre una disputa, `panal_open_dispute`
|
|
127
|
+
da acceso a quien decide —los firmantes de la multisig— para que pueda leerlo.
|
|
128
|
+
|
|
110
129
|
Lo que sí cambia es el reloj, y conviene decirlo antes de contratar. Un bot contesta en
|
|
111
130
|
segundos; una persona, cuando abre la app. Y ojo con esto: la comprobación de que «el
|
|
112
131
|
agente responde» que hace `panal_quote_hire` **la contesta el buzón**, no la persona. Que
|
package/dist/limits.d.ts
CHANGED
|
@@ -51,6 +51,14 @@ export interface Quote {
|
|
|
51
51
|
symbol: string;
|
|
52
52
|
botUrl: string | null;
|
|
53
53
|
adjuntos: AdjuntoPresupuestado[];
|
|
54
|
+
/**
|
|
55
|
+
* Si al contratar el encargo se cifra de punta a punta: quien lo hace recibe
|
|
56
|
+
* en el buzón de Panal y publicó su clave. Se decide al presupuestar, para
|
|
57
|
+
* que la persona sepa antes de decir que sí quién podrá leerlo.
|
|
58
|
+
*/
|
|
59
|
+
cifrado: boolean;
|
|
60
|
+
/** El encargo como lo escribió quien contrata, sin el manifiesto: lo que se cifra. */
|
|
61
|
+
briefPedido: string;
|
|
54
62
|
expiresAt: number;
|
|
55
63
|
}
|
|
56
64
|
/**
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Los encargos contratados CIFRADOS, tal como se anclaron y se subieron.
|
|
3
|
+
*
|
|
4
|
+
* Un encargo cifrado no se puede volver a componer desde su texto: el cifrado
|
|
5
|
+
* es aleatorio y daría otro hash. Hace falta la copia sellada para dos cosas
|
|
6
|
+
* que pueden llegar días después y con el servidor reiniciado entremedias:
|
|
7
|
+
* reenviarlo si no llegó (`panal_send_brief`) y dar acceso a quien decide si
|
|
8
|
+
* se disputa (`panal_open_dispute`).
|
|
9
|
+
*
|
|
10
|
+
* Va en disco junto al registro de gasto, un archivo por tarea. Es texto y
|
|
11
|
+
* bytes CIFRADOS: quien lea la carpeta no lee el encargo.
|
|
12
|
+
*/
|
|
13
|
+
import type { AttachedFile } from '@panal/sdk';
|
|
14
|
+
export interface Sellado {
|
|
15
|
+
brief: string;
|
|
16
|
+
archivos: {
|
|
17
|
+
file: AttachedFile;
|
|
18
|
+
bytes: Uint8Array;
|
|
19
|
+
}[];
|
|
20
|
+
}
|
|
21
|
+
export declare class Sellados {
|
|
22
|
+
private readonly dir;
|
|
23
|
+
/** `junto` es el archivo de gasto: la carpeta va a su lado. */
|
|
24
|
+
constructor(junto: string);
|
|
25
|
+
guardar(taskId: bigint, s: Sellado): void;
|
|
26
|
+
leer(taskId: bigint): Sellado | null;
|
|
27
|
+
}
|
package/dist/sellados.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Los encargos contratados CIFRADOS, tal como se anclaron y se subieron.
|
|
3
|
+
*
|
|
4
|
+
* Un encargo cifrado no se puede volver a componer desde su texto: el cifrado
|
|
5
|
+
* es aleatorio y daría otro hash. Hace falta la copia sellada para dos cosas
|
|
6
|
+
* que pueden llegar días después y con el servidor reiniciado entremedias:
|
|
7
|
+
* reenviarlo si no llegó (`panal_send_brief`) y dar acceso a quien decide si
|
|
8
|
+
* se disputa (`panal_open_dispute`).
|
|
9
|
+
*
|
|
10
|
+
* Va en disco junto al registro de gasto, un archivo por tarea. Es texto y
|
|
11
|
+
* bytes CIFRADOS: quien lea la carpeta no lee el encargo.
|
|
12
|
+
*/
|
|
13
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
export class Sellados {
|
|
16
|
+
dir;
|
|
17
|
+
/** `junto` es el archivo de gasto: la carpeta va a su lado. */
|
|
18
|
+
constructor(junto) {
|
|
19
|
+
this.dir = join(dirname(junto), 'sellados');
|
|
20
|
+
}
|
|
21
|
+
guardar(taskId, s) {
|
|
22
|
+
mkdirSync(this.dir, { recursive: true });
|
|
23
|
+
const datos = {
|
|
24
|
+
brief: s.brief,
|
|
25
|
+
archivos: s.archivos.map((a) => ({ file: a.file, bytes: Buffer.from(a.bytes).toString('base64') })),
|
|
26
|
+
};
|
|
27
|
+
// Escritura atómica: un corte a medias no deja una copia rota que luego no cuadre.
|
|
28
|
+
const destino = join(this.dir, `${taskId}.json`);
|
|
29
|
+
writeFileSync(`${destino}.tmp`, JSON.stringify(datos));
|
|
30
|
+
renameSync(`${destino}.tmp`, destino);
|
|
31
|
+
}
|
|
32
|
+
leer(taskId) {
|
|
33
|
+
try {
|
|
34
|
+
const datos = JSON.parse(readFileSync(join(this.dir, `${taskId}.json`), 'utf8'));
|
|
35
|
+
return {
|
|
36
|
+
brief: datos.brief,
|
|
37
|
+
archivos: datos.archivos.map((a) => ({ file: a.file, bytes: new Uint8Array(Buffer.from(a.bytes, 'base64')) })),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -42,10 +42,11 @@ import { createInterface } from 'node:readline';
|
|
|
42
42
|
import { fileURLToPath } from 'node:url';
|
|
43
43
|
import { formatEther, formatUnits, isAddress, keccak256, parseEther, toBytes } from 'viem';
|
|
44
44
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
45
|
-
import { NATIVE_CURRENCY, TaskStatus, appendAttachmentsManifest, conEjemplosDeLaFicha, createPanalClient, downloadDeliveredFile, enDolares, formatearDolares, erc20Abi, leerEjemplosDeMetadata, matchAttachment, parseAttachmentsManifest, parseFilesManifest, stripFilesManifest, x402Currency, } from '@panal/sdk';
|
|
45
|
+
import { BUZON_URL, NATIVE_CURRENCY, TaskStatus, appendAttachmentsManifest, conEjemplosDeLaFicha, createPanalClient, downloadDeliveredFile, esTextoCifrado, enDolares, formatearDolares, erc20Abi, leerEjemplosDeMetadata, matchAttachment, parseAttachmentsManifest, parseFilesManifest, stripFilesManifest, x402Currency, sinBarraFinal, } from '@panal/sdk';
|
|
46
46
|
import { MAX_ADJUNTOS, capacidadesDeAgente, guardarDescarga, leerAdjuntoLocal, raizAdjuntos, raizDescargas, subirAdjunto, } from './adjuntos.js';
|
|
47
47
|
import { buscarNivel, lineaDeNiveles, nivelesDeAgente, precioDeNivel, renderNiveles, } from './niveles.js';
|
|
48
48
|
import { QuoteBook, SpendLedger, limitFor, limitsFromEnv } from './limits.js';
|
|
49
|
+
import { Sellados } from './sellados.js';
|
|
49
50
|
import { deudaDelTablon, herramientasDelTablon } from './tablon.js';
|
|
50
51
|
import { briefSignMessage, expiraEn, fetchAgentLimits, fetchResultText, pushBrief, resultSignMessage, } from './fetch-result.js';
|
|
51
52
|
const SERVER_NAME = 'panal';
|
|
@@ -75,6 +76,13 @@ function log(msg) {
|
|
|
75
76
|
// ---------------------------------------------------------------------------
|
|
76
77
|
const limits = limitsFromEnv();
|
|
77
78
|
const quotes = new QuoteBook();
|
|
79
|
+
/**
|
|
80
|
+
* Lo contratado CIFRADO, tal como se ancló y se subió, por tarea: para
|
|
81
|
+
* reenviarlo si no llegó, y para dar acceso a quien decide si se disputa. No
|
|
82
|
+
* se puede volver a componer —el cifrado es aleatorio— y por eso se guarda,
|
|
83
|
+
* en disco junto al registro de gasto (ver `sellados.ts`).
|
|
84
|
+
*/
|
|
85
|
+
const encargosCifrados = new Sellados(SpendLedger.defaultFile());
|
|
78
86
|
const ledger = new SpendLedger(SpendLedger.defaultFile(), (m) => log(`⚠️ ${m}`));
|
|
79
87
|
const writesRequested = (process.env.MCP_ENABLE_WRITES ?? '').trim().toLowerCase() === 'true';
|
|
80
88
|
const account = (() => {
|
|
@@ -93,6 +101,16 @@ const panal = createPanalClient({
|
|
|
93
101
|
buzonUrl: process.env.BUZON_URL?.trim() || undefined,
|
|
94
102
|
account,
|
|
95
103
|
});
|
|
104
|
+
/**
|
|
105
|
+
* ¿Ese endpoint es el buzón de Panal, y no la máquina del agente? Solo ahí se
|
|
106
|
+
* cifra: es donde el encargo espera en un sitio que no es de quien lo hace.
|
|
107
|
+
*/
|
|
108
|
+
function recibeEnElBuzon(botUrl) {
|
|
109
|
+
if (!botUrl)
|
|
110
|
+
return false;
|
|
111
|
+
const url = sinBarraFinal(botUrl).toLowerCase();
|
|
112
|
+
return [BUZON_URL, panal.buzonUrl].some((b) => url.startsWith(sinBarraFinal(b).toLowerCase()));
|
|
113
|
+
}
|
|
96
114
|
/** Motivo por el que no se puede escribir, o null si sí se puede. */
|
|
97
115
|
function writesBlockedReason() {
|
|
98
116
|
if (!writesRequested) {
|
|
@@ -350,7 +368,51 @@ async function recogerEntrega(taskId) {
|
|
|
350
368
|
'response. You can open a dispute at https://panal.lat/dashboard.',
|
|
351
369
|
};
|
|
352
370
|
}
|
|
353
|
-
|
|
371
|
+
// Cifrada para esta wallet y para quien la entregó (porque esta wallet
|
|
372
|
+
// publicó su clave): se abre aquí, con su clave, y nunca sale en claro.
|
|
373
|
+
const cifrado = esTextoCifrado(text);
|
|
374
|
+
const claro = cifrado ? await panal.decryptText(text) : text;
|
|
375
|
+
return { text: claro, cifrado, anclado: text, archivos: parseFilesManifest(claro), botUrl: agent.metadata.botUrl, signature, expira };
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Abierta la disputa de una tarea con alguien que recibe en el buzón, da
|
|
379
|
+
* acceso a quien decide (los firmantes de la multisig) a lo cifrado que esta
|
|
380
|
+
* wallet puede abrir: el encargo, si guarda el sobre, y la entrega con sus
|
|
381
|
+
* archivos. Sin esto no podrían leerlo —ni Panal puede— y decidirían a
|
|
382
|
+
* ciegas. Devuelve una línea para la respuesta, o null si no había nada cifrado.
|
|
383
|
+
*/
|
|
384
|
+
async function accesoEnDisputa(taskId) {
|
|
385
|
+
try {
|
|
386
|
+
const task = await panal.getTask(taskId);
|
|
387
|
+
const botUrl = (await panal.getAgent(task.worker)).metadata.botUrl;
|
|
388
|
+
if (!botUrl || !recibeEnElBuzon(botUrl))
|
|
389
|
+
return null;
|
|
390
|
+
const objetos = [];
|
|
391
|
+
const sellado = encargosCifrados.leer(taskId);
|
|
392
|
+
if (sellado)
|
|
393
|
+
objetos.push(sellado.brief, ...sellado.archivos.map((a) => a.bytes));
|
|
394
|
+
if (!/^0x0+$/.test(task.resultHash)) {
|
|
395
|
+
const entrega = await recogerEntrega(taskId);
|
|
396
|
+
if (!('error' in entrega) && entrega.cifrado) {
|
|
397
|
+
objetos.push(entrega.anclado);
|
|
398
|
+
for (const f of entrega.archivos) {
|
|
399
|
+
objetos.push(await downloadDeliveredFile(f, { baseUrl: entrega.botUrl, address: account.address, signature: entrega.signature, expira: entrega.expira }));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const r = await panal.grantArbitratorsAccess({ base: botUrl, id: taskId, objetos });
|
|
404
|
+
if (r.objetos === 0)
|
|
405
|
+
return null;
|
|
406
|
+
if (r.arbitros > 0) {
|
|
407
|
+
return (` The deciders (${r.arbitros} of ${r.arbitros + r.sinClave.length} multisig signers) were given access to the encrypted ` +
|
|
408
|
+
`${sellado ? 'job and ' : ''}delivery, so they can read what is disputed.` +
|
|
409
|
+
(sellado ? '' : ' The job itself could not be shared: its sealed copy is not on this server (hired from elsewhere).'));
|
|
410
|
+
}
|
|
411
|
+
return ' WARNING: what is disputed is encrypted and no multisig signer has published an encryption key yet, so they cannot read it.';
|
|
412
|
+
}
|
|
413
|
+
catch (err) {
|
|
414
|
+
return ` WARNING: giving the deciders access to the encrypted job failed (${err instanceof Error ? err.message.split('\n')[0] : String(err)}).`;
|
|
415
|
+
}
|
|
354
416
|
}
|
|
355
417
|
const READ_TOOLS = [
|
|
356
418
|
{
|
|
@@ -496,6 +558,9 @@ const READ_TOOLS = [
|
|
|
496
558
|
// Una consulta x402 se resuelve en una sola llamada HTTP: no hay tarea,
|
|
497
559
|
// ni taskId, ni sitio donde anunciar un adjunto. Aquí siempre va vacío.
|
|
498
560
|
adjuntos: [],
|
|
561
|
+
// Va a la máquina del agente, que es quien la lee: no hay buzón en medio.
|
|
562
|
+
cifrado: false,
|
|
563
|
+
briefPedido: prompt,
|
|
499
564
|
});
|
|
500
565
|
return [
|
|
501
566
|
`${name} charges per question:`,
|
|
@@ -774,6 +839,11 @@ const WRITE_TOOLS = [
|
|
|
774
839
|
`and this brief is ${brief.length}. Nothing was quoted and nothing was spent. ` +
|
|
775
840
|
`Shorten it by ${brief.length - tope} characters and ask again.${dondeSiCabe}`);
|
|
776
841
|
}
|
|
842
|
+
// Con quien recibe en el buzón de Panal y publicó su clave, el encargo,
|
|
843
|
+
// sus archivos y la entrega van cifrados de punta a punta, como desde la
|
|
844
|
+
// web. A un agente con servidor propio le llega a SU máquina: no se cifra.
|
|
845
|
+
const enBuzon = recibeEnElBuzon(agent.metadata.botUrl);
|
|
846
|
+
const cifrado = enBuzon && !!account && (await panal.getPublicKey(agent.address)) !== null;
|
|
777
847
|
const quote = quotes.issue({
|
|
778
848
|
kind: 'hire',
|
|
779
849
|
worker: agent.address,
|
|
@@ -785,6 +855,8 @@ const WRITE_TOOLS = [
|
|
|
785
855
|
symbol,
|
|
786
856
|
botUrl: agent.metadata.botUrl,
|
|
787
857
|
adjuntos: adjuntos.map((a) => ({ file: a.file, ruta: a.ruta })),
|
|
858
|
+
cifrado,
|
|
859
|
+
briefPedido,
|
|
788
860
|
});
|
|
789
861
|
return [
|
|
790
862
|
`Quote for ${quote.agentName}:`,
|
|
@@ -802,6 +874,11 @@ const WRITE_TOOLS = [
|
|
|
802
874
|
: []),
|
|
803
875
|
` Deadline: ${limits.deadlineHours} h from the moment it is hired`,
|
|
804
876
|
` Job: ${briefPedido.length > 200 ? `${briefPedido.slice(0, 200)}…` : briefPedido}`,
|
|
877
|
+
cifrado
|
|
878
|
+
? ` Privacy: end-to-end encrypted — only ${quote.agentName} and this wallet can read the job, its files and the delivery; Panal's mailbox cannot.`
|
|
879
|
+
: enBuzon
|
|
880
|
+
? ` Privacy: NOT encrypted — ${quote.agentName} has not turned on encryption, so Panal's mailbox can read this job (it cannot change it).`
|
|
881
|
+
: null,
|
|
805
882
|
// Se listan uno a uno, con su ruta real. Lo que se adjunta sale de esta
|
|
806
883
|
// máquina y no vuelve, así que la persona tiene que poder ver QUÉ
|
|
807
884
|
// archivos son antes de decir que sí — no un «3 adjuntos».
|
|
@@ -816,7 +893,9 @@ const WRITE_TOOLS = [
|
|
|
816
893
|
'',
|
|
817
894
|
'Show this price to the person. Only if they say yes, call panal_hire with that quote_id ' +
|
|
818
895
|
'and confirmed_by_user: true. This moves real money.',
|
|
819
|
-
]
|
|
896
|
+
]
|
|
897
|
+
.filter((l) => l !== null)
|
|
898
|
+
.join('\n');
|
|
820
899
|
},
|
|
821
900
|
},
|
|
822
901
|
{
|
|
@@ -943,12 +1022,32 @@ const WRITE_TOOLS = [
|
|
|
943
1022
|
// haber cambiado en los cinco minutos que vive un presupuesto: se
|
|
944
1023
|
// gastaría de más sin que nadie lo hubiera aprobado y por encima del
|
|
945
1024
|
// tope, que se comprobó contra el importe viejo.
|
|
1025
|
+
/*
|
|
1026
|
+
* Cifrado, si se presupuestó así: el encargo y sus adjuntos se cierran
|
|
1027
|
+
* AHORA para quien lo hace y para esta wallet. El hash que va a la
|
|
1028
|
+
* cadena es el del sobre, y los adjuntos viajan cifrados con nombre
|
|
1029
|
+
* opaco. Si el agente retiró su clave desde el presupuesto, no se
|
|
1030
|
+
* contrata en claro sin que nadie lo haya aprobado.
|
|
1031
|
+
*/
|
|
1032
|
+
let brief = quote.brief;
|
|
1033
|
+
let aSubir = subidas.map((s) => ({ file: s.file, bytes: s.bytes }));
|
|
1034
|
+
if (quote.cifrado) {
|
|
1035
|
+
const cerrado = await panal.encryptBriefFor(quote.worker, quote.briefPedido, subidas.map((s) => ({ name: s.file.name, bytes: s.bytes, ...(s.file.mime ? { mime: s.file.mime } : {}) })));
|
|
1036
|
+
if (!cerrado) {
|
|
1037
|
+
return (`Not hired, nothing was spent: ${quote.agentName} no longer publishes an encryption key, and this ` +
|
|
1038
|
+
'quote promised an encrypted job. Ask for a fresh quote: it will say whether it goes encrypted.');
|
|
1039
|
+
}
|
|
1040
|
+
brief = cerrado.brief;
|
|
1041
|
+
aSubir = cerrado.archivos;
|
|
1042
|
+
}
|
|
946
1043
|
const result = await panal.hire({
|
|
947
1044
|
agent: quote.worker,
|
|
948
|
-
brief
|
|
1045
|
+
brief,
|
|
949
1046
|
amount: quote.amount,
|
|
950
1047
|
deadline,
|
|
951
1048
|
});
|
|
1049
|
+
if (quote.cifrado)
|
|
1050
|
+
encargosCifrados.guardar(result.taskId, { brief, archivos: aSubir });
|
|
952
1051
|
ledger.record(quote.currency, result.amount);
|
|
953
1052
|
log(`contratada #${result.taskId} a ${quote.worker} por ${formatEther(result.amount)} ${quote.symbol}`);
|
|
954
1053
|
// Contratar sin entregar el encargo deja la tarea a medias: el pago
|
|
@@ -961,7 +1060,7 @@ const WRITE_TOOLS = [
|
|
|
961
1060
|
}
|
|
962
1061
|
else {
|
|
963
1062
|
const firma = await account.signMessage({ message: briefSignMessage(result.taskId) });
|
|
964
|
-
const fallo = await pushBrief(quote.botUrl, result.taskId,
|
|
1063
|
+
const fallo = await pushBrief(quote.botUrl, result.taskId, brief, account.address, firma);
|
|
965
1064
|
if (fallo === null) {
|
|
966
1065
|
entregaBrief = `Job delivered to ${quote.botUrl}: the agent is already working on it.`;
|
|
967
1066
|
log(`brief #${result.taskId} entregado en ${quote.botUrl}`);
|
|
@@ -972,12 +1071,14 @@ const WRITE_TOOLS = [
|
|
|
972
1071
|
// que la cadena ya cubre.
|
|
973
1072
|
if (subidas.length > 0) {
|
|
974
1073
|
const fallidos = [];
|
|
975
|
-
|
|
1074
|
+
// El nombre de verdad para decirlo; el que se sube, opaco si va cifrado.
|
|
1075
|
+
for (const [i, adj] of aSubir.entries()) {
|
|
1076
|
+
const nombre = subidas[i].file.name;
|
|
976
1077
|
const malo = await subirAdjunto(quote.botUrl, result.taskId, adj.file, adj.bytes, account.address, firma);
|
|
977
1078
|
if (malo)
|
|
978
|
-
fallidos.push(`${
|
|
1079
|
+
fallidos.push(`${nombre} (${malo})`);
|
|
979
1080
|
else
|
|
980
|
-
log(`adjunto "${
|
|
1081
|
+
log(`adjunto "${nombre}" subido a #${result.taskId}`);
|
|
981
1082
|
}
|
|
982
1083
|
if (fallidos.length === 0) {
|
|
983
1084
|
entregaBrief += `\n${subidas.length} attachment(s) uploaded: ${subidas.map((a) => a.file.name).join(', ')}.`;
|
|
@@ -1011,6 +1112,7 @@ const WRITE_TOOLS = [
|
|
|
1011
1112
|
` Agent: ${quote.agentName}`,
|
|
1012
1113
|
` Amount: ${formatEther(result.amount)} ${quote.symbol}${quote.tier ? ` — size "${quote.tier}"` : ''}`,
|
|
1013
1114
|
` tx: ${EXPLORER}/tx/${result.txHash}`,
|
|
1115
|
+
...(quote.cifrado ? [' End-to-end encrypted: only the agent and this wallet can read the job and the delivery.'] : []),
|
|
1014
1116
|
'',
|
|
1015
1117
|
entregaBrief,
|
|
1016
1118
|
'',
|
|
@@ -1119,7 +1221,8 @@ const WRITE_TOOLS = [
|
|
|
1119
1221
|
signature: entrega.signature,
|
|
1120
1222
|
expira: entrega.expira,
|
|
1121
1223
|
});
|
|
1122
|
-
|
|
1224
|
+
// Comprobados contra su hash cifrados, que es lo que se anunció; se abren después.
|
|
1225
|
+
guardados.push(guardarDescarga(archivo.name, entrega.cifrado ? await panal.decryptFile(bytes) : bytes));
|
|
1123
1226
|
}
|
|
1124
1227
|
catch (err) {
|
|
1125
1228
|
fallidos.push(`${archivo.name} (${err instanceof Error ? err.message : String(err)})`);
|
|
@@ -1179,6 +1282,30 @@ const WRITE_TOOLS = [
|
|
|
1179
1282
|
if (task.status !== TaskStatus.Open) {
|
|
1180
1283
|
return `Task #${id} is ${TaskStatus[task.status]}: it no longer accepts the job.`;
|
|
1181
1284
|
}
|
|
1285
|
+
// Contratado cifrado: se reenvía el sobre que se ancló y sus adjuntos
|
|
1286
|
+
// cifrados, tal cual. Con el texto en claro no se puede volver a
|
|
1287
|
+
// componer: el cifrado es aleatorio y daría otro hash.
|
|
1288
|
+
const sellado = encargosCifrados.leer(BigInt(id));
|
|
1289
|
+
if (sellado) {
|
|
1290
|
+
const destino = (await panal.getAgent(task.worker)).metadata.botUrl;
|
|
1291
|
+
if (!destino)
|
|
1292
|
+
return 'That agent publishes no endpoint: there is nowhere to send the job.';
|
|
1293
|
+
const firma = await account.signMessage({ message: briefSignMessage(BigInt(id)) });
|
|
1294
|
+
const fallo = await pushBrief(destino, BigInt(id), sellado.brief, account.address, firma);
|
|
1295
|
+
if (fallo !== null) {
|
|
1296
|
+
return `It still does not arrive (${fallo}). The payment stays locked and returns on its own when the deadline passes.`;
|
|
1297
|
+
}
|
|
1298
|
+
const fallidos = [];
|
|
1299
|
+
for (const adj of sellado.archivos) {
|
|
1300
|
+
const malo = await subirAdjunto(destino, BigInt(id), adj.file, adj.bytes, account.address, firma);
|
|
1301
|
+
if (malo)
|
|
1302
|
+
fallidos.push(malo);
|
|
1303
|
+
}
|
|
1304
|
+
if (fallidos.length > 0) {
|
|
1305
|
+
return `The encrypted job arrived but ${fallidos.length} attachment(s) did not: ${fallidos.join('; ')}. The agent waits for all of them before starting.`;
|
|
1306
|
+
}
|
|
1307
|
+
return `Encrypted job${sellado.archivos.length ? ` and ${sellado.archivos.length} attachment(s)` : ''} delivered again. The agent is already working on task #${id}.`;
|
|
1308
|
+
}
|
|
1182
1309
|
const rutas = Array.isArray(args.files) ? args.files.map(str).filter((r) => !!r) : [];
|
|
1183
1310
|
const leidos = [];
|
|
1184
1311
|
for (const ruta of rutas) {
|
|
@@ -1206,6 +1333,14 @@ const WRITE_TOOLS = [
|
|
|
1206
1333
|
// Se comprueba aquí antes de molestar al agente: así el error dice que el
|
|
1207
1334
|
// texto no es el mismo, en vez de un 409 sin contexto desde el endpoint.
|
|
1208
1335
|
if (keccak256(toBytes(brief)) !== task.taskHash) {
|
|
1336
|
+
const agenteDeLaTarea = await panal.getAgent(task.worker).catch(() => null);
|
|
1337
|
+
if (recibeEnElBuzon(agenteDeLaTarea?.metadata.botUrl)) {
|
|
1338
|
+
return (`The hash does not match. If task #${id} was hired END-TO-END ENCRYPTED, what is on the chain is the hash ` +
|
|
1339
|
+
'of the sealed job, and it cannot be rebuilt from the text (encryption is random). This server keeps the ' +
|
|
1340
|
+
'sealed copy on disk next to its spend file, and it is not there (another machine, or the folder was ' +
|
|
1341
|
+
'deleted). The payment returns on its own when the deadline passes, or cancel it then with ' +
|
|
1342
|
+
'panal_cancel_task and hire again.');
|
|
1343
|
+
}
|
|
1209
1344
|
return [
|
|
1210
1345
|
'That text is NOT the one hired: its hash does not match the one on the chain.',
|
|
1211
1346
|
` hired hash: ${task.taskHash}`,
|
|
@@ -1366,9 +1501,11 @@ const WRITE_TOOLS = [
|
|
|
1366
1501
|
return 'The task id is an integer.';
|
|
1367
1502
|
try {
|
|
1368
1503
|
const hash = await panal.openDispute(BigInt(id));
|
|
1504
|
+
const acceso = await accesoEnDisputa(BigInt(id));
|
|
1369
1505
|
return [
|
|
1370
1506
|
`Dispute opened on task #${id}. The automatic release is stopped.`,
|
|
1371
1507
|
` tx: ${EXPLORER}/tx/${hash}`,
|
|
1508
|
+
...(acceso ? [acceso] : []),
|
|
1372
1509
|
'',
|
|
1373
1510
|
'An arbitrator decides how to split it. If nobody resolves it within 14 days, the whole amount',
|
|
1374
1511
|
'can be refunded to the client.',
|
package/dist/tablon.js
CHANGED
|
@@ -190,6 +190,37 @@ export function herramientasDelTablon(d) {
|
|
|
190
190
|
d.escrituraCerrada() ??
|
|
191
191
|
(args.confirmed_by_user === true ? null : `I will not ${que} without confirmed_by_user: true.`);
|
|
192
192
|
/** Los anuncios del buzón del tablón, por número, comprobados contra la cadena. */
|
|
193
|
+
/**
|
|
194
|
+
* Los encargos CONFIDENCIALES vistos en la última lectura de anuncios: su
|
|
195
|
+
* encargo va cifrado y solo lo lee quien su cliente elija. Lo dice el buzón;
|
|
196
|
+
* si mintiera, lo que cambia es un aviso.
|
|
197
|
+
*/
|
|
198
|
+
const confidenciales = new Set();
|
|
199
|
+
/**
|
|
200
|
+
* Lo publicado como confidencial, tal como se subió: para reintentar si los
|
|
201
|
+
* textos o los archivos no llegaron. No se puede volver a componer —el
|
|
202
|
+
* cifrado es aleatorio— y por eso se guarda. En memoria: tras reiniciar, un
|
|
203
|
+
* reintento ya no es posible y el encargo se retira y se vuelve a publicar.
|
|
204
|
+
*/
|
|
205
|
+
const publicadosCifrados = new Map();
|
|
206
|
+
/**
|
|
207
|
+
* Da acceso a quien decide la disputa de un encargo del tablón, y lo dice en
|
|
208
|
+
* una línea. `null` si no había nada cifrado que dar.
|
|
209
|
+
*/
|
|
210
|
+
async function accesoParaQuienDecide(id) {
|
|
211
|
+
try {
|
|
212
|
+
const r = await panal.grantBoardDisputeAccess(id);
|
|
213
|
+
if (r.objetos === 0)
|
|
214
|
+
return null;
|
|
215
|
+
if (r.arbitros > 0) {
|
|
216
|
+
return ` The deciders (${r.arbitros} of ${r.arbitros + r.sinClave.length} multisig signers) were given access to the encrypted brief and delivery, so they can read what is disputed.`;
|
|
217
|
+
}
|
|
218
|
+
return ' WARNING: the brief or the delivery is encrypted and no multisig signer has published an encryption key yet, so they cannot read it. Call panal_grant_board_access with to "deciders" again later.';
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
return ` WARNING: giving the deciders access to the encrypted brief failed (${fallo(err)}). Retry with panal_grant_board_access and to "deciders".`;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
193
224
|
async function anunciosDe(encargos) {
|
|
194
225
|
const out = new Map();
|
|
195
226
|
try {
|
|
@@ -201,6 +232,10 @@ export function herramientasDelTablon(d) {
|
|
|
201
232
|
for (const a of anuncios) {
|
|
202
233
|
if (typeof a.anuncio === 'string' && hashes.get(a.jobId) === keccak256(toBytes(a.anuncio)).toLowerCase()) {
|
|
203
234
|
out.set(a.jobId, a.anuncio);
|
|
235
|
+
if (a.cifrado === true)
|
|
236
|
+
confidenciales.add(a.jobId);
|
|
237
|
+
else
|
|
238
|
+
confidenciales.delete(a.jobId);
|
|
204
239
|
}
|
|
205
240
|
}
|
|
206
241
|
}
|
|
@@ -302,10 +337,14 @@ export function herramientasDelTablon(d) {
|
|
|
302
337
|
const crudo = anuncios.get(j.id.toString());
|
|
303
338
|
const { audiencia, texto: anuncio } = crudo ? leerAnuncio(crudo) : { audiencia: 'todos', texto: null };
|
|
304
339
|
const fuera = fueraDeAudiencia(audiencia, tipo);
|
|
340
|
+
const confidencial = confidenciales.has(j.id.toString());
|
|
305
341
|
return [
|
|
306
|
-
`\n${cabecera(j, ahora, ofertas[i]?.length)}${audiencia !== 'todos' ? `, for ${NOMBRE_AUDIENCIA[audiencia]}` : ''}`,
|
|
342
|
+
`\n${cabecera(j, ahora, ofertas[i]?.length)}${audiencia !== 'todos' ? `, for ${NOMBRE_AUDIENCIA[audiencia]}` : ''}${confidencial ? ', CONFIDENTIAL' : ''}`,
|
|
307
343
|
` "${anuncio ?? '(advert not in the mailbox)'}"`,
|
|
308
344
|
` posted by ${j.cliente}`,
|
|
345
|
+
confidencial && !tuyo
|
|
346
|
+
? ' Confidential: the brief is encrypted, and the client gives access only to the agent it picks. Bidding publishes this wallet\'s encryption key (two signatures, no gas) so it can be given access.'
|
|
347
|
+
: null,
|
|
309
348
|
tuyo
|
|
310
349
|
? ' (posted by this wallet)'
|
|
311
350
|
: fuera
|
|
@@ -399,9 +438,13 @@ export function herramientasDelTablon(d) {
|
|
|
399
438
|
const soyAgente = yo === j.agente.toLowerCase();
|
|
400
439
|
const crudo = (await anunciosDe([j])).get(id.toString());
|
|
401
440
|
const { audiencia, texto: anuncio } = crudo ? leerAnuncio(crudo) : { audiencia: 'todos', texto: null };
|
|
441
|
+
const confidencial = confidenciales.has(id.toString());
|
|
402
442
|
const lineas = [
|
|
403
443
|
cabecera(j, ahora),
|
|
404
444
|
` State: ${ESTADO[j.estado]}`,
|
|
445
|
+
confidencial
|
|
446
|
+
? ' Confidential: the brief and its files are encrypted. Only the client and the agent it picks (which needs a published encryption key) can read them; not even Panal\'s mailbox can.'
|
|
447
|
+
: null,
|
|
405
448
|
audiencia !== 'todos' ? ` For: ${NOMBRE_AUDIENCIA[audiencia]} (declared in the advert; the contract cannot check who is a person)` : null,
|
|
406
449
|
` Advert: ${anuncio ? `"${anuncio}"` : '(not in the mailbox)'}`,
|
|
407
450
|
` Client: ${j.cliente}${soyCliente ? ' (this wallet)' : ''}`,
|
|
@@ -468,6 +511,9 @@ export function herramientasDelTablon(d) {
|
|
|
468
511
|
if (soyAgente)
|
|
469
512
|
siguiente.push('read the brief with include_brief, and deliver with panal_deliver_board_job');
|
|
470
513
|
if (soyCliente) {
|
|
514
|
+
if (confidencial) {
|
|
515
|
+
siguiente.push('if the agent cannot read it yet (it was assigned automatically, or the access failed), give it access with panal_grant_board_access');
|
|
516
|
+
}
|
|
471
517
|
siguiente.push('read the delivery with panal_board_result, then panal_approve_board_job or panal_dispute_board_job');
|
|
472
518
|
}
|
|
473
519
|
siguiente.push('once the escrow is done (approved, released after 72 h, or cancelled past the deadline), panal_close_board_job settles it');
|
|
@@ -591,6 +637,10 @@ export function herramientasDelTablon(d) {
|
|
|
591
637
|
choose_hours: { type: 'number', description: 'Selection: time to choose after offers close (max 168). Default 24.' },
|
|
592
638
|
experienced_only: { type: 'boolean', description: 'Only agents at level 1 or higher.' },
|
|
593
639
|
relist: { type: 'boolean', description: 'If the agent does not deliver, post it again instead of refunding.' },
|
|
640
|
+
confidential: {
|
|
641
|
+
type: 'boolean',
|
|
642
|
+
description: 'Selection only. Encrypt the brief and its files end to end: only this wallet and the agent it picks can read them, not even Panal\'s mailbox. Only agents with a published encryption key can be picked. The advert stays public.',
|
|
643
|
+
},
|
|
594
644
|
files: {
|
|
595
645
|
type: 'array',
|
|
596
646
|
items: { type: 'string' },
|
|
@@ -623,6 +673,19 @@ export function herramientasDelTablon(d) {
|
|
|
623
673
|
if (args.retry_job_id !== undefined) {
|
|
624
674
|
if (reintento === null)
|
|
625
675
|
return 'retry_job_id must be a board job number.';
|
|
676
|
+
// Uno confidencial no se puede volver a componer —el cifrado es
|
|
677
|
+
// aleatorio—: se reenvía la copia que se guardó al publicarlo.
|
|
678
|
+
const guardado = publicadosCifrados.get(reintento.toString());
|
|
679
|
+
if (guardado) {
|
|
680
|
+
try {
|
|
681
|
+
await panal.publishBoardJobTexts(reintento, { anuncio: guardado.anuncio, encargo: guardado.encargo });
|
|
682
|
+
await panal.publishBoardJobFiles(reintento, guardado.archivos);
|
|
683
|
+
return `Texts${guardado.archivos.length ? ' and files' : ''} of confidential board job #${reintento} are in the mailbox now. Nothing was charged.`;
|
|
684
|
+
}
|
|
685
|
+
catch (err) {
|
|
686
|
+
return `The mailbox still did not take them: ${fallo(err)}. Nothing was charged. Try again in a while.`;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
626
689
|
try {
|
|
627
690
|
// El mismo encargo que se hasheó: el texto con los adjuntos anunciados.
|
|
628
691
|
const encargoFinal = appendAttachmentsManifest(encargo, adjuntos.map((a) => attachmentFrom(a.name, a.bytes, a.mime)));
|
|
@@ -662,6 +725,7 @@ export function herramientasDelTablon(d) {
|
|
|
662
725
|
soloConExperiencia: args.experienced_only === true,
|
|
663
726
|
republicar: args.relist === true,
|
|
664
727
|
adjuntos,
|
|
728
|
+
confidencial: args.confidential === true,
|
|
665
729
|
};
|
|
666
730
|
const problema = problemaAlPublicar(p);
|
|
667
731
|
if (problema)
|
|
@@ -682,6 +746,8 @@ export function herramientasDelTablon(d) {
|
|
|
682
746
|
}
|
|
683
747
|
try {
|
|
684
748
|
const r = await panal.postBoardJob(p);
|
|
749
|
+
if (p.confidencial)
|
|
750
|
+
publicadosCifrados.set(r.jobId.toString(), { anuncio, encargo: r.encargo, archivos: r.archivos });
|
|
685
751
|
d.registrarGasto(moneda, cuanto);
|
|
686
752
|
d.log(`publicado en el tablón #${r.jobId} por ${formatEther(cuanto)} ${simbolo}`);
|
|
687
753
|
return [
|
|
@@ -690,10 +756,15 @@ export function herramientasDelTablon(d) {
|
|
|
690
756
|
r.textosGuardados && r.archivosGuardados
|
|
691
757
|
? ` Advert and brief${adjuntos.length ? ` and ${adjuntos.length} file(s)` : ''} are in the mailbox.`
|
|
692
758
|
: ` WARNING: the ${r.textosGuardados ? 'files' : 'texts'} did NOT reach the mailbox (${r.error}). Call panal_post_board_job again with retry_job_id ${r.jobId} and the same advert, brief and files: that only re-sends them.`,
|
|
759
|
+
p.confidencial
|
|
760
|
+
? ' Confidential: the brief and its files are encrypted for this wallet. Only agents with a published encryption key can be chosen, and choosing one gives it access.'
|
|
761
|
+
: null,
|
|
693
762
|
modo === ModoTablon.Seleccion
|
|
694
763
|
? `Agents can bid for ${duracion(p.ventanaOfertasS)}. Follow it with panal_board_job ${r.jobId}.`
|
|
695
764
|
: `The first agent that passes the rules takes it. Follow it with panal_board_job ${r.jobId}.`,
|
|
696
|
-
]
|
|
765
|
+
]
|
|
766
|
+
.filter(Boolean)
|
|
767
|
+
.join('\n');
|
|
697
768
|
}
|
|
698
769
|
catch (err) {
|
|
699
770
|
return `Could not post: ${fallo(err)}`;
|
|
@@ -756,6 +827,11 @@ export function herramientasDelTablon(d) {
|
|
|
756
827
|
: o.precio < j.importe
|
|
757
828
|
? ` ${importe(j.importe - o.precio, j.moneda)} was left over: collect it with panal_withdraw.`
|
|
758
829
|
: null,
|
|
830
|
+
r.acceso?.ok === true
|
|
831
|
+
? ' Confidential: the agent was given access to the brief and its files.'
|
|
832
|
+
: r.acceso?.ok === false
|
|
833
|
+
? ` WARNING: the job is the agent's, but giving it access to the confidential brief failed (${r.acceso.error}). Retry with panal_grant_board_access ${id}: until then it cannot read what to do.`
|
|
834
|
+
: null,
|
|
759
835
|
`The agent has ${duracion(o.plazoEntregaS)} to deliver. Then read it with panal_board_result.`,
|
|
760
836
|
]
|
|
761
837
|
.filter(Boolean)
|
|
@@ -851,13 +927,59 @@ export function herramientasDelTablon(d) {
|
|
|
851
927
|
return 'job_id must be a board job number.';
|
|
852
928
|
try {
|
|
853
929
|
const hash = await panal.disputeBoardJob(id);
|
|
854
|
-
return
|
|
930
|
+
return [
|
|
931
|
+
`Dispute opened on board job #${id}. The multisig resolves it; follow it with panal_board_job.`,
|
|
932
|
+
` tx: ${tx(hash)}`,
|
|
933
|
+
await accesoParaQuienDecide(id),
|
|
934
|
+
]
|
|
935
|
+
.filter(Boolean)
|
|
936
|
+
.join('\n');
|
|
855
937
|
}
|
|
856
938
|
catch (err) {
|
|
857
939
|
return `Could not open the dispute: ${fallo(err)}`;
|
|
858
940
|
}
|
|
859
941
|
},
|
|
860
942
|
},
|
|
943
|
+
{
|
|
944
|
+
name: 'panal_grant_board_access',
|
|
945
|
+
description: 'For a CONFIDENTIAL board job (its brief is encrypted): give access to whoever has to read it. to "agent": ' +
|
|
946
|
+
'as the client, give the assigned agent access to the brief and its files — it happens when choosing an ' +
|
|
947
|
+
'offer; this is for a job assigned automatically or an access that failed. to "deciders": as a party in a ' +
|
|
948
|
+
'dispute, give the multisig signers access to what this wallet can read, so they can decide. Repeating it ' +
|
|
949
|
+
'does not duplicate anything. Signs messages only: no transaction, no money.',
|
|
950
|
+
inputSchema: {
|
|
951
|
+
type: 'object',
|
|
952
|
+
properties: {
|
|
953
|
+
job_id: { type: 'string', description: 'The board job number.' },
|
|
954
|
+
to: { type: 'string', description: '"agent" (default) or "deciders".' },
|
|
955
|
+
},
|
|
956
|
+
required: ['job_id'],
|
|
957
|
+
},
|
|
958
|
+
handler: async (args) => {
|
|
959
|
+
const no = disponible();
|
|
960
|
+
if (no)
|
|
961
|
+
return no;
|
|
962
|
+
if (!d.cuenta)
|
|
963
|
+
return 'Giving access needs this server\'s wallet (MCP_PRIVATE_KEY) to sign: there is none.';
|
|
964
|
+
const id = parseJobId(args.job_id);
|
|
965
|
+
if (id === null)
|
|
966
|
+
return 'job_id must be a board job number.';
|
|
967
|
+
const a = (texto(args.to) ?? 'agent').toLowerCase();
|
|
968
|
+
if (a === 'deciders')
|
|
969
|
+
return (await accesoParaQuienDecide(id))?.trim() ?? `Nothing in board job #${id} is encrypted: the deciders can read it as it is.`;
|
|
970
|
+
if (a !== 'agent')
|
|
971
|
+
return 'to is "agent" or "deciders".';
|
|
972
|
+
try {
|
|
973
|
+
const n = await panal.grantBoardJobAccess(id);
|
|
974
|
+
return n === 0
|
|
975
|
+
? `Nothing in board job #${id} is encrypted: its agent can read it as it is.`
|
|
976
|
+
: `The agent of board job #${id} was given access to the brief and its files (${n} item(s)). It can read them now.`;
|
|
977
|
+
}
|
|
978
|
+
catch (err) {
|
|
979
|
+
return `Could not give access: ${fallo(err)}`;
|
|
980
|
+
}
|
|
981
|
+
},
|
|
982
|
+
},
|
|
861
983
|
{
|
|
862
984
|
name: 'panal_close_board_job',
|
|
863
985
|
description: 'Move a board job along when it is stuck, choosing the right step: the client WITHDRAWS a job nobody has ' +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "panal-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Servidor MCP de Panal: busca y contrata agentes de IA on-chain desde Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"node": ">=20"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@panal/sdk": "^0.
|
|
33
|
+
"@panal/sdk": "^0.26.0",
|
|
34
34
|
"viem": "^2.21.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|