agentbnb 8.4.3 → 8.4.5
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/chunk-5SIGMKOD.js +1086 -0
- package/dist/{chunk-6FZ4WYQL.js → chunk-JDAFLPR7.js} +15 -0
- package/dist/{chunk-J46N2TCC.js → chunk-OPRCWXD5.js} +1 -1
- package/dist/chunk-RF4A5X5U.js +62 -0
- package/dist/cli/index.js +79 -33
- package/dist/{conductor-mode-PXTMYGK5.js → conductor-mode-2F5OP7Q4.js} +3 -2
- package/dist/{execute-UP46R7KS.js → execute-6EJSVBFB.js} +4 -1
- package/dist/{execute-UAD5T3BQ.js → execute-VRTABQ6F.js} +2 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +904 -473
- package/dist/{openclaw-setup-LVSGMXDF.js → openclaw-setup-5ZWWRVF3.js} +2 -2
- package/dist/{serve-skill-6RKMVDMK.js → serve-skill-UD7TLSRN.js} +4 -1
- package/dist/{server-QG3FKU5Q.js → server-GSG5T2TZ.js} +20 -17
- package/dist/{service-coordinator-2HDVHDFD.js → service-coordinator-ZO7QHQ6U.js} +101 -7
- package/dist/skills/agentbnb/bootstrap.js +277 -738
- package/package.json +18 -12
- package/skills/agentbnb/SKILL.md +114 -183
- package/skills/agentbnb/install.sh +0 -0
- package/dist/chunk-PMRTQ2RL.js +0 -443
- package/dist/chunk-R4F4XII4.js +0 -264
- package/dist/client-OKJJ3UP2.js +0 -19
- package/dist/{chunk-PCQEHIGF.js → chunk-UNXCKETK.js} +3 -3
|
@@ -0,0 +1,1086 @@
|
|
|
1
|
+
import {
|
|
2
|
+
bootstrapAgent,
|
|
3
|
+
getBalance,
|
|
4
|
+
getTransactions,
|
|
5
|
+
holdEscrow,
|
|
6
|
+
migrateOwner,
|
|
7
|
+
openCreditDb,
|
|
8
|
+
releaseEscrow,
|
|
9
|
+
resolveTargetCapability,
|
|
10
|
+
settleEscrow
|
|
11
|
+
} from "./chunk-NQANA6WH.js";
|
|
12
|
+
import {
|
|
13
|
+
canonicalizeCreditOwner
|
|
14
|
+
} from "./chunk-6QMDJVMS.js";
|
|
15
|
+
import {
|
|
16
|
+
generateKeyPair,
|
|
17
|
+
loadKeyPair,
|
|
18
|
+
saveKeyPair,
|
|
19
|
+
signEscrowReceipt,
|
|
20
|
+
verifyEscrowReceipt
|
|
21
|
+
} from "./chunk-GIEJVKZZ.js";
|
|
22
|
+
import {
|
|
23
|
+
getConfigDir,
|
|
24
|
+
loadConfig
|
|
25
|
+
} from "./chunk-IVOYM3WG.js";
|
|
26
|
+
import {
|
|
27
|
+
getCard,
|
|
28
|
+
insertRequestLog,
|
|
29
|
+
updateReputation
|
|
30
|
+
} from "./chunk-ZU2TP7CN.js";
|
|
31
|
+
import {
|
|
32
|
+
lookupAgent
|
|
33
|
+
} from "./chunk-EE3V3DXK.js";
|
|
34
|
+
import {
|
|
35
|
+
AgentBnBError
|
|
36
|
+
} from "./chunk-I7KWA7OB.js";
|
|
37
|
+
|
|
38
|
+
// src/gateway/execute.ts
|
|
39
|
+
import { randomUUID } from "crypto";
|
|
40
|
+
|
|
41
|
+
// src/identity/identity.ts
|
|
42
|
+
import { z } from "zod";
|
|
43
|
+
import { createHash, createPrivateKey, createPublicKey } from "crypto";
|
|
44
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
45
|
+
import { join } from "path";
|
|
46
|
+
var AgentIdentitySchema = z.object({
|
|
47
|
+
/** Deterministic ID derived from public key: sha256(hex).slice(0, 16). */
|
|
48
|
+
agent_id: z.string().min(1),
|
|
49
|
+
/** Human-readable owner name (from config or init). */
|
|
50
|
+
owner: z.string().min(1),
|
|
51
|
+
/** Hex-encoded Ed25519 public key. */
|
|
52
|
+
public_key: z.string().min(1),
|
|
53
|
+
/** ISO 8601 timestamp of identity creation. */
|
|
54
|
+
created_at: z.string().datetime(),
|
|
55
|
+
/** Optional guarantor info if linked to a human. */
|
|
56
|
+
guarantor: z.object({
|
|
57
|
+
github_login: z.string().min(1),
|
|
58
|
+
verified_at: z.string().datetime()
|
|
59
|
+
}).optional()
|
|
60
|
+
});
|
|
61
|
+
var AgentCertificateSchema = z.object({
|
|
62
|
+
identity: AgentIdentitySchema,
|
|
63
|
+
/** ISO 8601 timestamp of certificate issuance. */
|
|
64
|
+
issued_at: z.string().datetime(),
|
|
65
|
+
/** ISO 8601 timestamp of certificate expiry. */
|
|
66
|
+
expires_at: z.string().datetime(),
|
|
67
|
+
/** Hex-encoded public key of the issuer (same as identity for self-signed). */
|
|
68
|
+
issuer_public_key: z.string().min(1),
|
|
69
|
+
/** Base64url Ed25519 signature over { identity, issued_at, expires_at, issuer_public_key }. */
|
|
70
|
+
signature: z.string().min(1)
|
|
71
|
+
});
|
|
72
|
+
var IDENTITY_FILENAME = "identity.json";
|
|
73
|
+
var PRIVATE_KEY_FILENAME = "private.key";
|
|
74
|
+
var PUBLIC_KEY_FILENAME = "public.key";
|
|
75
|
+
function derivePublicKeyFromPrivate(privateKey) {
|
|
76
|
+
const privateKeyObject = createPrivateKey({ key: privateKey, format: "der", type: "pkcs8" });
|
|
77
|
+
const publicKeyObject = createPublicKey(privateKeyObject);
|
|
78
|
+
const publicKey = publicKeyObject.export({ format: "der", type: "spki" });
|
|
79
|
+
return Buffer.from(publicKey);
|
|
80
|
+
}
|
|
81
|
+
function buildIdentityFromPublicKey(publicKey, owner, createdAt) {
|
|
82
|
+
const publicKeyHex = publicKey.toString("hex");
|
|
83
|
+
return {
|
|
84
|
+
agent_id: deriveAgentId(publicKeyHex),
|
|
85
|
+
owner,
|
|
86
|
+
public_key: publicKeyHex,
|
|
87
|
+
created_at: createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function generateFreshIdentity(configDir, owner) {
|
|
91
|
+
const keys = generateKeyPair();
|
|
92
|
+
saveKeyPair(configDir, keys);
|
|
93
|
+
const identity = buildIdentityFromPublicKey(keys.publicKey, owner);
|
|
94
|
+
saveIdentity(configDir, identity);
|
|
95
|
+
return { identity, keys, status: "generated" };
|
|
96
|
+
}
|
|
97
|
+
function deriveAgentId(publicKeyHex) {
|
|
98
|
+
return createHash("sha256").update(publicKeyHex, "hex").digest("hex").slice(0, 16);
|
|
99
|
+
}
|
|
100
|
+
function loadIdentity(configDir) {
|
|
101
|
+
const filePath = join(configDir, IDENTITY_FILENAME);
|
|
102
|
+
if (!existsSync(filePath)) return null;
|
|
103
|
+
try {
|
|
104
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
105
|
+
return AgentIdentitySchema.parse(JSON.parse(raw));
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function saveIdentity(configDir, identity) {
|
|
111
|
+
if (!existsSync(configDir)) {
|
|
112
|
+
mkdirSync(configDir, { recursive: true });
|
|
113
|
+
}
|
|
114
|
+
const filePath = join(configDir, IDENTITY_FILENAME);
|
|
115
|
+
writeFileSync(filePath, JSON.stringify(identity, null, 2), "utf-8");
|
|
116
|
+
}
|
|
117
|
+
function loadOrRepairIdentity(configDir, ownerHint) {
|
|
118
|
+
if (!existsSync(configDir)) {
|
|
119
|
+
mkdirSync(configDir, { recursive: true });
|
|
120
|
+
}
|
|
121
|
+
const identityPath = join(configDir, IDENTITY_FILENAME);
|
|
122
|
+
const privateKeyPath = join(configDir, PRIVATE_KEY_FILENAME);
|
|
123
|
+
const publicKeyPath = join(configDir, PUBLIC_KEY_FILENAME);
|
|
124
|
+
const hasIdentity = existsSync(identityPath);
|
|
125
|
+
const hasPrivateKey = existsSync(privateKeyPath);
|
|
126
|
+
const hasPublicKey = existsSync(publicKeyPath);
|
|
127
|
+
if (!hasIdentity || !hasPrivateKey || !hasPublicKey) {
|
|
128
|
+
return generateFreshIdentity(configDir, ownerHint ?? "agent");
|
|
129
|
+
}
|
|
130
|
+
let keys;
|
|
131
|
+
try {
|
|
132
|
+
keys = loadKeyPair(configDir);
|
|
133
|
+
} catch {
|
|
134
|
+
return generateFreshIdentity(configDir, ownerHint ?? "agent");
|
|
135
|
+
}
|
|
136
|
+
let derivedPublicKey;
|
|
137
|
+
try {
|
|
138
|
+
derivedPublicKey = derivePublicKeyFromPrivate(keys.privateKey);
|
|
139
|
+
} catch {
|
|
140
|
+
return generateFreshIdentity(configDir, ownerHint ?? "agent");
|
|
141
|
+
}
|
|
142
|
+
let keypairRepaired = false;
|
|
143
|
+
if (!keys.publicKey.equals(derivedPublicKey)) {
|
|
144
|
+
keypairRepaired = true;
|
|
145
|
+
keys = { privateKey: keys.privateKey, publicKey: derivedPublicKey };
|
|
146
|
+
saveKeyPair(configDir, keys);
|
|
147
|
+
}
|
|
148
|
+
const loadedIdentity = loadIdentity(configDir);
|
|
149
|
+
const expectedAgentId = deriveAgentId(derivedPublicKey.toString("hex"));
|
|
150
|
+
const expectedPublicKeyHex = derivedPublicKey.toString("hex");
|
|
151
|
+
const identityMismatch = !loadedIdentity || loadedIdentity.public_key !== expectedPublicKeyHex || loadedIdentity.agent_id !== expectedAgentId;
|
|
152
|
+
if (identityMismatch) {
|
|
153
|
+
const repairedIdentity = buildIdentityFromPublicKey(
|
|
154
|
+
derivedPublicKey,
|
|
155
|
+
loadedIdentity?.owner ?? ownerHint ?? "agent",
|
|
156
|
+
loadedIdentity?.created_at
|
|
157
|
+
);
|
|
158
|
+
saveIdentity(configDir, repairedIdentity);
|
|
159
|
+
return { identity: repairedIdentity, keys, status: "repaired" };
|
|
160
|
+
}
|
|
161
|
+
if (ownerHint && loadedIdentity.owner !== ownerHint) {
|
|
162
|
+
const updatedIdentity = { ...loadedIdentity, owner: ownerHint };
|
|
163
|
+
saveIdentity(configDir, updatedIdentity);
|
|
164
|
+
return { identity: updatedIdentity, keys, status: "repaired" };
|
|
165
|
+
}
|
|
166
|
+
return { identity: loadedIdentity, keys, status: keypairRepaired ? "repaired" : "existing" };
|
|
167
|
+
}
|
|
168
|
+
function ensureIdentity(configDir, owner) {
|
|
169
|
+
return loadOrRepairIdentity(configDir, owner).identity;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/credit/local-credit-ledger.ts
|
|
173
|
+
var LocalCreditLedger = class {
|
|
174
|
+
constructor(db) {
|
|
175
|
+
this.db = db;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Holds credits in escrow during capability execution.
|
|
179
|
+
*
|
|
180
|
+
* @param owner - Agent identifier (requester).
|
|
181
|
+
* @param amount - Number of credits to hold.
|
|
182
|
+
* @param cardId - Capability Card ID being requested.
|
|
183
|
+
* @returns EscrowResult with the new escrowId.
|
|
184
|
+
* @throws {AgentBnBError} with code 'INSUFFICIENT_CREDITS' if balance < amount.
|
|
185
|
+
*/
|
|
186
|
+
async hold(owner, amount, cardId) {
|
|
187
|
+
const escrowId = holdEscrow(this.db, owner, amount, cardId);
|
|
188
|
+
return { escrowId };
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Settles an escrow — transfers held credits to the capability provider.
|
|
192
|
+
*
|
|
193
|
+
* @param escrowId - The escrow ID to settle.
|
|
194
|
+
* @param recipientOwner - Agent identifier who will receive the credits.
|
|
195
|
+
* @throws {AgentBnBError} with code 'ESCROW_NOT_FOUND' if escrow does not exist.
|
|
196
|
+
* @throws {AgentBnBError} with code 'ESCROW_ALREADY_SETTLED' if escrow is not in 'held' status.
|
|
197
|
+
*/
|
|
198
|
+
async settle(escrowId, recipientOwner) {
|
|
199
|
+
settleEscrow(this.db, escrowId, recipientOwner);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Releases an escrow — refunds credits back to the requester.
|
|
203
|
+
*
|
|
204
|
+
* @param escrowId - The escrow ID to release.
|
|
205
|
+
* @throws {AgentBnBError} with code 'ESCROW_NOT_FOUND' if escrow does not exist.
|
|
206
|
+
* @throws {AgentBnBError} with code 'ESCROW_ALREADY_SETTLED' if escrow is not in 'held' status.
|
|
207
|
+
*/
|
|
208
|
+
async release(escrowId) {
|
|
209
|
+
releaseEscrow(this.db, escrowId);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Returns the current credit balance for an agent.
|
|
213
|
+
*
|
|
214
|
+
* @param owner - Agent identifier.
|
|
215
|
+
* @returns Current balance in credits (0 if agent is unknown).
|
|
216
|
+
*/
|
|
217
|
+
async getBalance(owner) {
|
|
218
|
+
return getBalance(this.db, owner);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Returns the transaction history for an agent, newest first.
|
|
222
|
+
*
|
|
223
|
+
* @param owner - Agent identifier.
|
|
224
|
+
* @param limit - Maximum number of transactions to return. Defaults to 100.
|
|
225
|
+
* @returns Array of credit transactions ordered newest first.
|
|
226
|
+
*/
|
|
227
|
+
async getHistory(owner, limit) {
|
|
228
|
+
return getTransactions(this.db, owner, limit);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Grants initial credits to an agent (bootstrap grant).
|
|
232
|
+
* Idempotent — calling multiple times has no additional effect on balance.
|
|
233
|
+
*
|
|
234
|
+
* @param owner - Agent identifier.
|
|
235
|
+
* @param amount - Number of credits to grant. Defaults to 100.
|
|
236
|
+
*/
|
|
237
|
+
async grant(owner, amount) {
|
|
238
|
+
bootstrapAgent(this.db, owner, amount);
|
|
239
|
+
}
|
|
240
|
+
async rename(oldOwner, newOwner) {
|
|
241
|
+
migrateOwner(this.db, oldOwner, newOwner);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// src/registry/identity-auth.ts
|
|
246
|
+
var MAX_REQUEST_AGE_MS = 5 * 60 * 1e3;
|
|
247
|
+
function normalizeSignedParams(body) {
|
|
248
|
+
return body === void 0 ? null : body;
|
|
249
|
+
}
|
|
250
|
+
function buildIdentityPayload(method, path, timestamp, publicKeyHex, agentId, params) {
|
|
251
|
+
return {
|
|
252
|
+
method,
|
|
253
|
+
path,
|
|
254
|
+
timestamp,
|
|
255
|
+
publicKey: publicKeyHex,
|
|
256
|
+
agentId,
|
|
257
|
+
params: normalizeSignedParams(params)
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function extractClaimedRequester(request) {
|
|
261
|
+
const extractFromObject = (obj) => {
|
|
262
|
+
const directOwner = typeof obj.owner === "string" ? obj.owner.trim() : "";
|
|
263
|
+
if (directOwner) return directOwner;
|
|
264
|
+
const directRequester = typeof obj.requester === "string" ? obj.requester.trim() : "";
|
|
265
|
+
if (directRequester) return directRequester;
|
|
266
|
+
const oldOwner = typeof obj.oldOwner === "string" ? obj.oldOwner.trim() : "";
|
|
267
|
+
if (oldOwner) return oldOwner;
|
|
268
|
+
const nestedParams = obj.params;
|
|
269
|
+
if (nestedParams && typeof nestedParams === "object" && !Array.isArray(nestedParams)) {
|
|
270
|
+
const nested = nestedParams;
|
|
271
|
+
const nestedOwner = typeof nested.owner === "string" ? nested.owner.trim() : "";
|
|
272
|
+
if (nestedOwner) return nestedOwner;
|
|
273
|
+
const nestedRequester = typeof nested.requester === "string" ? nested.requester.trim() : "";
|
|
274
|
+
if (nestedRequester) return nestedRequester;
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
};
|
|
278
|
+
if (request.body && typeof request.body === "object" && !Array.isArray(request.body)) {
|
|
279
|
+
const claimed = extractFromObject(request.body);
|
|
280
|
+
if (claimed) return claimed;
|
|
281
|
+
}
|
|
282
|
+
if (request.params && typeof request.params === "object" && !Array.isArray(request.params)) {
|
|
283
|
+
const claimed = extractFromObject(request.params);
|
|
284
|
+
if (claimed) return claimed;
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
async function verifyIdentity(request, reply, options) {
|
|
289
|
+
const agentIdHeader = request.headers["x-agent-id"];
|
|
290
|
+
const publicKeyHeader = request.headers["x-agent-publickey"];
|
|
291
|
+
const signatureHeader = request.headers["x-agent-signature"];
|
|
292
|
+
const timestampHeader = request.headers["x-agent-timestamp"];
|
|
293
|
+
const agentId = agentIdHeader?.trim();
|
|
294
|
+
const publicKeyHex = publicKeyHeader?.trim();
|
|
295
|
+
const signature = signatureHeader?.trim();
|
|
296
|
+
const timestamp = timestampHeader?.trim();
|
|
297
|
+
if (!agentId || !publicKeyHex || !signature || !timestamp) {
|
|
298
|
+
await reply.code(401).send({ error: "Missing identity headers" });
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
const requestTime = new Date(timestamp).getTime();
|
|
302
|
+
if (isNaN(requestTime) || Math.abs(Date.now() - requestTime) > MAX_REQUEST_AGE_MS) {
|
|
303
|
+
await reply.code(401).send({ error: "Request expired" });
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
if (!/^[0-9a-fA-F]+$/.test(publicKeyHex) || publicKeyHex.length % 2 !== 0) {
|
|
307
|
+
await reply.code(401).send({ error: "Invalid identity signature" });
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
let expectedAgentId;
|
|
311
|
+
try {
|
|
312
|
+
expectedAgentId = deriveAgentId(publicKeyHex);
|
|
313
|
+
} catch {
|
|
314
|
+
await reply.code(401).send({ error: "Invalid identity signature" });
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
if (agentId !== expectedAgentId) {
|
|
318
|
+
await reply.code(401).send({ error: "Invalid identity signature" });
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
let publicKeyBuffer;
|
|
322
|
+
try {
|
|
323
|
+
publicKeyBuffer = Buffer.from(publicKeyHex, "hex");
|
|
324
|
+
} catch {
|
|
325
|
+
await reply.code(401).send({ error: "Invalid identity signature" });
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
const knownAgent = options.agentDb ? lookupAgent(options.agentDb, agentId) : null;
|
|
329
|
+
if (knownAgent && knownAgent.public_key.toLowerCase() !== publicKeyHex.toLowerCase()) {
|
|
330
|
+
await reply.code(401).send({ error: "Invalid identity signature" });
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
const payload = buildIdentityPayload(
|
|
334
|
+
request.method,
|
|
335
|
+
request.url,
|
|
336
|
+
timestamp,
|
|
337
|
+
publicKeyHex,
|
|
338
|
+
agentId,
|
|
339
|
+
request.body
|
|
340
|
+
);
|
|
341
|
+
const valid = verifyEscrowReceipt(payload, signature, publicKeyBuffer);
|
|
342
|
+
if (!valid) {
|
|
343
|
+
await reply.code(401).send({ error: "Invalid identity signature" });
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
const claimedRequester = extractClaimedRequester(request);
|
|
347
|
+
if (claimedRequester && knownAgent !== null) {
|
|
348
|
+
const matchesAgentId = claimedRequester === agentId;
|
|
349
|
+
const matchesDisplayName = knownAgent.display_name === claimedRequester;
|
|
350
|
+
const matchesLegacyOwner = knownAgent.legacy_owner === claimedRequester;
|
|
351
|
+
if (!matchesAgentId && !matchesDisplayName && !matchesLegacyOwner) {
|
|
352
|
+
await reply.code(401).send({ error: "Identity does not match requester" });
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
request.agentPublicKey = publicKeyHex;
|
|
357
|
+
request.agentId = agentId;
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
function identityAuthPlugin(fastify, options = {}) {
|
|
361
|
+
fastify.addHook("preHandler", async (request, reply) => {
|
|
362
|
+
const ok = await verifyIdentity(request, reply, options);
|
|
363
|
+
if (!ok) {
|
|
364
|
+
return reply;
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
function signRequest(method, path, body, privateKey, publicKeyHex, agentIdOverride) {
|
|
369
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
370
|
+
const agentId = agentIdOverride ?? deriveAgentId(publicKeyHex);
|
|
371
|
+
const payload = buildIdentityPayload(method, path, timestamp, publicKeyHex, agentId, body);
|
|
372
|
+
const signature = signEscrowReceipt(payload, privateKey);
|
|
373
|
+
return {
|
|
374
|
+
"X-Agent-Id": agentId,
|
|
375
|
+
"X-Agent-PublicKey": publicKeyHex,
|
|
376
|
+
"X-Agent-Signature": signature,
|
|
377
|
+
"X-Agent-Timestamp": timestamp
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/credit/registry-credit-ledger.ts
|
|
382
|
+
var HTTP_TIMEOUT_MS = 1e4;
|
|
383
|
+
var RegistryCreditLedger = class {
|
|
384
|
+
config;
|
|
385
|
+
constructor(config) {
|
|
386
|
+
this.config = config;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Holds credits in escrow during capability execution.
|
|
390
|
+
*
|
|
391
|
+
* @param owner - Agent identifier (requester).
|
|
392
|
+
* @param amount - Number of credits to hold.
|
|
393
|
+
* @param cardId - Capability Card ID being requested.
|
|
394
|
+
* @returns EscrowResult with the new escrowId.
|
|
395
|
+
* @throws {AgentBnBError} with code 'INSUFFICIENT_CREDITS' if balance < amount.
|
|
396
|
+
*/
|
|
397
|
+
async hold(owner, amount, cardId) {
|
|
398
|
+
if (this.config.mode === "direct") {
|
|
399
|
+
const escrowId = holdEscrow(this.config.db, owner, amount, cardId);
|
|
400
|
+
return { escrowId };
|
|
401
|
+
}
|
|
402
|
+
const data = await this.post("/api/credits/hold", owner, {
|
|
403
|
+
owner,
|
|
404
|
+
amount,
|
|
405
|
+
cardId
|
|
406
|
+
});
|
|
407
|
+
return { escrowId: data.escrowId };
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Settles an escrow — transfers held credits to the capability provider.
|
|
411
|
+
*
|
|
412
|
+
* @param escrowId - The escrow ID to settle.
|
|
413
|
+
* @param recipientOwner - Agent identifier who will receive the credits.
|
|
414
|
+
* @throws {AgentBnBError} with code 'ESCROW_NOT_FOUND' if escrow does not exist.
|
|
415
|
+
* @throws {AgentBnBError} with code 'ESCROW_ALREADY_SETTLED' if escrow is not in 'held' status.
|
|
416
|
+
*/
|
|
417
|
+
async settle(escrowId, recipientOwner) {
|
|
418
|
+
if (this.config.mode === "direct") {
|
|
419
|
+
settleEscrow(this.config.db, escrowId, recipientOwner);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
await this.post("/api/credits/settle", null, { escrowId, recipientOwner });
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Releases an escrow — refunds credits back to the requester.
|
|
426
|
+
*
|
|
427
|
+
* @param escrowId - The escrow ID to release.
|
|
428
|
+
* @throws {AgentBnBError} with code 'ESCROW_NOT_FOUND' if escrow does not exist.
|
|
429
|
+
* @throws {AgentBnBError} with code 'ESCROW_ALREADY_SETTLED' if escrow is not in 'held' status.
|
|
430
|
+
*/
|
|
431
|
+
async release(escrowId) {
|
|
432
|
+
if (this.config.mode === "direct") {
|
|
433
|
+
releaseEscrow(this.config.db, escrowId);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
await this.post("/api/credits/release", null, { escrowId });
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Returns the current credit balance for an agent.
|
|
440
|
+
*
|
|
441
|
+
* @param owner - Agent identifier.
|
|
442
|
+
* @returns Current balance in credits (0 if agent is unknown).
|
|
443
|
+
*/
|
|
444
|
+
async getBalance(owner) {
|
|
445
|
+
if (this.config.mode === "direct") {
|
|
446
|
+
return getBalance(this.config.db, owner);
|
|
447
|
+
}
|
|
448
|
+
const data = await this.get(`/api/credits/${owner}`, owner);
|
|
449
|
+
return data.balance;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Returns the transaction history for an agent, newest first.
|
|
453
|
+
*
|
|
454
|
+
* @param owner - Agent identifier.
|
|
455
|
+
* @param limit - Maximum number of transactions to return. Defaults to 100.
|
|
456
|
+
* @returns Array of credit transactions ordered newest first.
|
|
457
|
+
*/
|
|
458
|
+
async getHistory(owner, limit = 100) {
|
|
459
|
+
if (this.config.mode === "direct") {
|
|
460
|
+
return getTransactions(this.config.db, owner, limit);
|
|
461
|
+
}
|
|
462
|
+
const data = await this.get(
|
|
463
|
+
`/api/credits/${owner}/history?limit=${limit}`,
|
|
464
|
+
owner
|
|
465
|
+
);
|
|
466
|
+
return data.transactions;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Grants initial credits to an agent (bootstrap grant).
|
|
470
|
+
* Idempotent — calling multiple times has no additional effect on balance.
|
|
471
|
+
*
|
|
472
|
+
* @param owner - Agent identifier.
|
|
473
|
+
* @param amount - Number of credits to grant. Defaults to 100.
|
|
474
|
+
*/
|
|
475
|
+
async grant(owner, amount = 100) {
|
|
476
|
+
if (this.config.mode === "direct") {
|
|
477
|
+
bootstrapAgent(this.config.db, owner, amount);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
await this.post("/api/credits/grant", owner, { owner, amount });
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Renames an owner — migrates balance, transactions, and escrows.
|
|
484
|
+
*/
|
|
485
|
+
async rename(oldOwner, newOwner) {
|
|
486
|
+
if (oldOwner === newOwner) return;
|
|
487
|
+
if (this.config.mode === "direct") {
|
|
488
|
+
migrateOwner(this.config.db, oldOwner, newOwner);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
await this.post("/api/credits/rename", null, { oldOwner, newOwner });
|
|
492
|
+
}
|
|
493
|
+
// ─── Private HTTP helpers ─────────────────────────────────────────────────
|
|
494
|
+
/**
|
|
495
|
+
* Makes an authenticated POST request to the Registry HTTP API.
|
|
496
|
+
* Includes a 10s timeout via AbortController.
|
|
497
|
+
*
|
|
498
|
+
* @param path - API path (e.g., '/api/credits/hold').
|
|
499
|
+
* @param ownerForHeader - Agent owner identifier for X-Agent-Owner header, or null to omit.
|
|
500
|
+
* @param body - JSON body to send.
|
|
501
|
+
* @returns Parsed JSON response body.
|
|
502
|
+
* @throws {AgentBnBError} on non-2xx responses or network errors.
|
|
503
|
+
*/
|
|
504
|
+
async post(path, ownerForHeader, body) {
|
|
505
|
+
const cfg = this.config;
|
|
506
|
+
const controller = new AbortController();
|
|
507
|
+
const timeoutId = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
|
|
508
|
+
try {
|
|
509
|
+
const authHeaders = signRequest("POST", path, body, cfg.privateKey, cfg.ownerPublicKey);
|
|
510
|
+
const headers = {
|
|
511
|
+
"Content-Type": "application/json",
|
|
512
|
+
...authHeaders
|
|
513
|
+
};
|
|
514
|
+
void ownerForHeader;
|
|
515
|
+
const res = await fetch(`${cfg.registryUrl}${path}`, {
|
|
516
|
+
method: "POST",
|
|
517
|
+
headers,
|
|
518
|
+
body: JSON.stringify(body),
|
|
519
|
+
signal: controller.signal
|
|
520
|
+
});
|
|
521
|
+
return await this.handleResponse(res);
|
|
522
|
+
} catch (err) {
|
|
523
|
+
if (err instanceof AgentBnBError) throw err;
|
|
524
|
+
throw new AgentBnBError(
|
|
525
|
+
`Registry unreachable: ${err.message}`,
|
|
526
|
+
"REGISTRY_UNREACHABLE"
|
|
527
|
+
);
|
|
528
|
+
} finally {
|
|
529
|
+
clearTimeout(timeoutId);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Makes an authenticated GET request to the Registry HTTP API.
|
|
534
|
+
* Includes a 10s timeout via AbortController.
|
|
535
|
+
*
|
|
536
|
+
* @param path - API path (e.g., '/api/credits/owner-id').
|
|
537
|
+
* @param owner - Agent owner identifier for X-Agent-Owner header.
|
|
538
|
+
* @returns Parsed JSON response body.
|
|
539
|
+
* @throws {AgentBnBError} on non-2xx responses or network errors.
|
|
540
|
+
*/
|
|
541
|
+
async get(path, owner) {
|
|
542
|
+
const cfg = this.config;
|
|
543
|
+
const controller = new AbortController();
|
|
544
|
+
const timeoutId = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
|
|
545
|
+
try {
|
|
546
|
+
const authHeaders = signRequest("GET", path, null, cfg.privateKey, cfg.ownerPublicKey);
|
|
547
|
+
void owner;
|
|
548
|
+
const res = await fetch(`${cfg.registryUrl}${path}`, {
|
|
549
|
+
method: "GET",
|
|
550
|
+
headers: {
|
|
551
|
+
"Content-Type": "application/json",
|
|
552
|
+
...authHeaders
|
|
553
|
+
},
|
|
554
|
+
signal: controller.signal
|
|
555
|
+
});
|
|
556
|
+
return await this.handleResponse(res);
|
|
557
|
+
} catch (err) {
|
|
558
|
+
if (err instanceof AgentBnBError) throw err;
|
|
559
|
+
throw new AgentBnBError(
|
|
560
|
+
`Registry unreachable: ${err.message}`,
|
|
561
|
+
"REGISTRY_UNREACHABLE"
|
|
562
|
+
);
|
|
563
|
+
} finally {
|
|
564
|
+
clearTimeout(timeoutId);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Handles an HTTP response — returns parsed JSON on 2xx, throws AgentBnBError on error.
|
|
569
|
+
*/
|
|
570
|
+
async handleResponse(res) {
|
|
571
|
+
const json = await res.json();
|
|
572
|
+
if (!res.ok) {
|
|
573
|
+
const code = typeof json["code"] === "string" ? json["code"] : "REGISTRY_ERROR";
|
|
574
|
+
const message = typeof json["error"] === "string" ? json["error"] : `HTTP ${res.status}`;
|
|
575
|
+
throw new AgentBnBError(message, code);
|
|
576
|
+
}
|
|
577
|
+
return json;
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
// src/credit/create-ledger.ts
|
|
582
|
+
function createLedger(opts) {
|
|
583
|
+
if ("registryUrl" in opts && opts.registryUrl !== void 0) {
|
|
584
|
+
return new RegistryCreditLedger({
|
|
585
|
+
mode: "http",
|
|
586
|
+
registryUrl: opts.registryUrl,
|
|
587
|
+
ownerPublicKey: opts.ownerPublicKey,
|
|
588
|
+
privateKey: opts.privateKey
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
if ("db" in opts && opts.db !== void 0) {
|
|
592
|
+
return new RegistryCreditLedger({
|
|
593
|
+
mode: "direct",
|
|
594
|
+
db: opts.db
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
const db = openCreditDb(opts.creditDbPath);
|
|
598
|
+
return new LocalCreditLedger(db);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/credit/registry-sync.ts
|
|
602
|
+
async function syncCreditsFromRegistry(config, localDb) {
|
|
603
|
+
if (!config.registry) {
|
|
604
|
+
return { synced: false, error: "no registry configured" };
|
|
605
|
+
}
|
|
606
|
+
try {
|
|
607
|
+
const configDir = getConfigDir();
|
|
608
|
+
const { identity, keys } = loadOrRepairIdentity(configDir, config.owner);
|
|
609
|
+
const ledger = createLedger({
|
|
610
|
+
registryUrl: config.registry,
|
|
611
|
+
ownerPublicKey: identity.public_key,
|
|
612
|
+
privateKey: keys.privateKey
|
|
613
|
+
});
|
|
614
|
+
const [remoteBalance, remoteHistory] = await Promise.all([
|
|
615
|
+
ledger.getBalance(config.owner),
|
|
616
|
+
ledger.getHistory(config.owner, 50)
|
|
617
|
+
]);
|
|
618
|
+
const localWas = getBalance(localDb, config.owner);
|
|
619
|
+
localDb.transaction(() => {
|
|
620
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
621
|
+
const canonicalOwner = canonicalizeCreditOwner(localDb, config.owner);
|
|
622
|
+
localDb.prepare(
|
|
623
|
+
"INSERT OR REPLACE INTO credit_balances (owner, balance, updated_at) VALUES (?, ?, ?)"
|
|
624
|
+
).run(canonicalOwner, remoteBalance, now);
|
|
625
|
+
const insertTxn = localDb.prepare(
|
|
626
|
+
"INSERT OR IGNORE INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
627
|
+
);
|
|
628
|
+
for (const txn of remoteHistory) {
|
|
629
|
+
insertTxn.run(
|
|
630
|
+
txn.id,
|
|
631
|
+
txn.owner,
|
|
632
|
+
txn.amount,
|
|
633
|
+
txn.reason,
|
|
634
|
+
txn.reference_id,
|
|
635
|
+
txn.created_at
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
})();
|
|
639
|
+
return { synced: true, remoteBalance, localWas };
|
|
640
|
+
} catch (err) {
|
|
641
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
642
|
+
return { synced: false, error: message };
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// src/gateway/execute.ts
|
|
647
|
+
async function notifyTelegramSkillExecuted(opts) {
|
|
648
|
+
const cfg = loadConfig();
|
|
649
|
+
if (!cfg?.telegram_notifications) return;
|
|
650
|
+
const token = cfg.telegram_bot_token ?? process.env["TELEGRAM_BOT_TOKEN"];
|
|
651
|
+
const chatId = cfg.telegram_chat_id ?? process.env["TELEGRAM_CHAT_ID"];
|
|
652
|
+
if (!token || !chatId) return;
|
|
653
|
+
const balance = getBalance(opts.creditDb, opts.owner);
|
|
654
|
+
const skillLabel = opts.skillId ? `${opts.skillName} (${opts.skillId})` : opts.skillName;
|
|
655
|
+
const text = [
|
|
656
|
+
"[AgentBnB] Skill executed",
|
|
657
|
+
`Skill: ${skillLabel}`,
|
|
658
|
+
`Requester: ${opts.requester}`,
|
|
659
|
+
`Earned: +${opts.creditsEarned} credits`,
|
|
660
|
+
`Balance: ${balance} credits`,
|
|
661
|
+
`Latency: ${opts.latencyMs}ms`
|
|
662
|
+
].join("\n");
|
|
663
|
+
await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
|
|
664
|
+
method: "POST",
|
|
665
|
+
headers: { "Content-Type": "application/json" },
|
|
666
|
+
body: JSON.stringify({ chat_id: chatId, text })
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
async function notifyTelegramSkillFailed(opts) {
|
|
670
|
+
const cfg = loadConfig();
|
|
671
|
+
if (!cfg?.telegram_notifications) return;
|
|
672
|
+
const token = cfg.telegram_bot_token ?? process.env["TELEGRAM_BOT_TOKEN"];
|
|
673
|
+
const chatId = cfg.telegram_chat_id ?? process.env["TELEGRAM_CHAT_ID"];
|
|
674
|
+
if (!token || !chatId) return;
|
|
675
|
+
const balance = getBalance(opts.creditDb, opts.owner);
|
|
676
|
+
const skillLabel = opts.skillId ? `${opts.skillName} (${opts.skillId})` : opts.skillName;
|
|
677
|
+
const text = [
|
|
678
|
+
"[AgentBnB] Skill failed",
|
|
679
|
+
`Skill: ${skillLabel}`,
|
|
680
|
+
`Requester: ${opts.requester}`,
|
|
681
|
+
`Reason: ${opts.failureReason}`,
|
|
682
|
+
`Error: ${opts.message}`,
|
|
683
|
+
`Balance: ${balance} credits`,
|
|
684
|
+
`Latency: ${opts.latencyMs}ms`
|
|
685
|
+
].join("\n");
|
|
686
|
+
await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
|
|
687
|
+
method: "POST",
|
|
688
|
+
headers: { "Content-Type": "application/json" },
|
|
689
|
+
body: JSON.stringify({ chat_id: chatId, text })
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
async function executeCapabilityRequest(opts) {
|
|
693
|
+
const {
|
|
694
|
+
registryDb,
|
|
695
|
+
creditDb,
|
|
696
|
+
cardId,
|
|
697
|
+
skillId,
|
|
698
|
+
params,
|
|
699
|
+
requester,
|
|
700
|
+
escrowReceipt: receipt,
|
|
701
|
+
skillExecutor,
|
|
702
|
+
handlerUrl,
|
|
703
|
+
timeoutMs = 3e5,
|
|
704
|
+
onProgress,
|
|
705
|
+
relayAuthorized = false
|
|
706
|
+
} = opts;
|
|
707
|
+
const card = getCard(registryDb, cardId);
|
|
708
|
+
if (!card) {
|
|
709
|
+
return { success: false, error: { code: -32602, message: `Card not found: ${cardId}` } };
|
|
710
|
+
}
|
|
711
|
+
if (requester === card.owner && !relayAuthorized) {
|
|
712
|
+
const msg = `Self-request blocked: requester (${requester}) is the card owner. Set AGENTBNB_DIR to your agent's config directory before calling agentbnb request.`;
|
|
713
|
+
try {
|
|
714
|
+
insertRequestLog(registryDb, {
|
|
715
|
+
id: randomUUID(),
|
|
716
|
+
card_id: cardId,
|
|
717
|
+
card_name: card.name,
|
|
718
|
+
skill_id: skillId,
|
|
719
|
+
requester,
|
|
720
|
+
status: "failure",
|
|
721
|
+
latency_ms: 0,
|
|
722
|
+
credits_charged: 0,
|
|
723
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
724
|
+
failure_reason: "auth_error"
|
|
725
|
+
});
|
|
726
|
+
} catch {
|
|
727
|
+
}
|
|
728
|
+
return { success: false, error: { code: -32603, message: msg } };
|
|
729
|
+
}
|
|
730
|
+
let creditsNeeded;
|
|
731
|
+
let cardName;
|
|
732
|
+
let resolvedSkillId;
|
|
733
|
+
const rawCard = card;
|
|
734
|
+
if (Array.isArray(rawCard["skills"])) {
|
|
735
|
+
const v2card = card;
|
|
736
|
+
const skill = skillId ? v2card.skills.find((s) => s.id === skillId) : v2card.skills[0];
|
|
737
|
+
if (!skill) {
|
|
738
|
+
return { success: false, error: { code: -32602, message: `Skill not found: ${skillId}` } };
|
|
739
|
+
}
|
|
740
|
+
creditsNeeded = skill.pricing.credits_per_call;
|
|
741
|
+
cardName = skill.name;
|
|
742
|
+
resolvedSkillId = skill.id;
|
|
743
|
+
} else {
|
|
744
|
+
creditsNeeded = card.pricing.credits_per_call;
|
|
745
|
+
cardName = card.name;
|
|
746
|
+
}
|
|
747
|
+
let escrowId = null;
|
|
748
|
+
if (relayAuthorized) {
|
|
749
|
+
} else if (receipt) {
|
|
750
|
+
if (creditsNeeded > 0) {
|
|
751
|
+
return {
|
|
752
|
+
success: false,
|
|
753
|
+
error: {
|
|
754
|
+
code: -32603,
|
|
755
|
+
message: "Direct HTTP paid remote settlement is disabled. Route paid requests through relay."
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
} else {
|
|
760
|
+
const cfg = loadConfig();
|
|
761
|
+
if (cfg?.registry) {
|
|
762
|
+
try {
|
|
763
|
+
await Promise.race([
|
|
764
|
+
syncCreditsFromRegistry(cfg, creditDb),
|
|
765
|
+
new Promise(
|
|
766
|
+
(_, reject) => setTimeout(() => reject(new Error("sync timeout")), 2e3)
|
|
767
|
+
)
|
|
768
|
+
]);
|
|
769
|
+
} catch {
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
try {
|
|
773
|
+
const balance = getBalance(creditDb, requester);
|
|
774
|
+
if (balance < creditsNeeded) {
|
|
775
|
+
return { success: false, error: { code: -32603, message: "Insufficient credits" } };
|
|
776
|
+
}
|
|
777
|
+
escrowId = holdEscrow(creditDb, requester, creditsNeeded, cardId);
|
|
778
|
+
} catch (err) {
|
|
779
|
+
const msg = err instanceof AgentBnBError ? err.message : "Failed to hold escrow";
|
|
780
|
+
return { success: false, error: { code: -32603, message: msg } };
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
const startMs = Date.now();
|
|
784
|
+
const handleFailure = (status, latencyMs, message, failureReason = "bad_execution") => {
|
|
785
|
+
if (escrowId) releaseEscrow(creditDb, escrowId);
|
|
786
|
+
if (failureReason === "bad_execution" || failureReason === "auth_error") {
|
|
787
|
+
updateReputation(registryDb, cardId, false, latencyMs);
|
|
788
|
+
}
|
|
789
|
+
try {
|
|
790
|
+
insertRequestLog(registryDb, {
|
|
791
|
+
id: randomUUID(),
|
|
792
|
+
card_id: cardId,
|
|
793
|
+
card_name: cardName,
|
|
794
|
+
skill_id: resolvedSkillId,
|
|
795
|
+
requester,
|
|
796
|
+
status,
|
|
797
|
+
latency_ms: latencyMs,
|
|
798
|
+
credits_charged: 0,
|
|
799
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
800
|
+
failure_reason: failureReason
|
|
801
|
+
});
|
|
802
|
+
} catch {
|
|
803
|
+
}
|
|
804
|
+
notifyTelegramSkillFailed({
|
|
805
|
+
creditDb,
|
|
806
|
+
owner: card.owner,
|
|
807
|
+
skillName: cardName,
|
|
808
|
+
skillId: resolvedSkillId ?? null,
|
|
809
|
+
requester,
|
|
810
|
+
latencyMs,
|
|
811
|
+
failureReason,
|
|
812
|
+
message
|
|
813
|
+
}).catch(() => {
|
|
814
|
+
});
|
|
815
|
+
return { success: false, error: { code: -32603, message } };
|
|
816
|
+
};
|
|
817
|
+
const handleSuccess = (result, latencyMs) => {
|
|
818
|
+
if (escrowId) {
|
|
819
|
+
settleEscrow(creditDb, escrowId, card.owner);
|
|
820
|
+
}
|
|
821
|
+
updateReputation(registryDb, cardId, true, latencyMs);
|
|
822
|
+
try {
|
|
823
|
+
insertRequestLog(registryDb, {
|
|
824
|
+
id: randomUUID(),
|
|
825
|
+
card_id: cardId,
|
|
826
|
+
card_name: cardName,
|
|
827
|
+
skill_id: resolvedSkillId,
|
|
828
|
+
requester,
|
|
829
|
+
status: "success",
|
|
830
|
+
latency_ms: latencyMs,
|
|
831
|
+
credits_charged: creditsNeeded,
|
|
832
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
833
|
+
});
|
|
834
|
+
} catch {
|
|
835
|
+
}
|
|
836
|
+
notifyTelegramSkillExecuted({
|
|
837
|
+
creditDb,
|
|
838
|
+
owner: card.owner,
|
|
839
|
+
skillName: cardName,
|
|
840
|
+
skillId: resolvedSkillId ?? null,
|
|
841
|
+
requester,
|
|
842
|
+
creditsEarned: creditsNeeded,
|
|
843
|
+
latencyMs
|
|
844
|
+
}).catch(() => {
|
|
845
|
+
});
|
|
846
|
+
return { success: true, result };
|
|
847
|
+
};
|
|
848
|
+
if (skillExecutor) {
|
|
849
|
+
let targetSkillId = resolvedSkillId ?? skillId;
|
|
850
|
+
if (!targetSkillId) {
|
|
851
|
+
const available = skillExecutor.listSkills();
|
|
852
|
+
if (available.length > 0) {
|
|
853
|
+
targetSkillId = available[0];
|
|
854
|
+
} else {
|
|
855
|
+
return handleFailure(
|
|
856
|
+
"failure",
|
|
857
|
+
Date.now() - startMs,
|
|
858
|
+
"No skill_id specified and no skills registered on this provider.",
|
|
859
|
+
"not_found"
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
try {
|
|
864
|
+
const execResult = await skillExecutor.execute(targetSkillId, params, onProgress);
|
|
865
|
+
if (!execResult.success) {
|
|
866
|
+
return handleFailure("failure", execResult.latency_ms, execResult.error ?? "Execution failed", "bad_execution");
|
|
867
|
+
}
|
|
868
|
+
return handleSuccess(execResult.result, execResult.latency_ms);
|
|
869
|
+
} catch (err) {
|
|
870
|
+
const message = err instanceof Error ? err.message : "Execution error";
|
|
871
|
+
return handleFailure("failure", Date.now() - startMs, message, "bad_execution");
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
if (!handlerUrl) {
|
|
875
|
+
return handleFailure("failure", Date.now() - startMs, "No skill executor or handler URL configured", "bad_execution");
|
|
876
|
+
}
|
|
877
|
+
const controller = new AbortController();
|
|
878
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
879
|
+
try {
|
|
880
|
+
const response = await fetch(handlerUrl, {
|
|
881
|
+
method: "POST",
|
|
882
|
+
headers: { "Content-Type": "application/json" },
|
|
883
|
+
body: JSON.stringify({ card_id: cardId, skill_id: resolvedSkillId, params }),
|
|
884
|
+
signal: controller.signal
|
|
885
|
+
});
|
|
886
|
+
clearTimeout(timer);
|
|
887
|
+
if (!response.ok) {
|
|
888
|
+
return handleFailure("failure", Date.now() - startMs, `Handler returned ${response.status}`, "bad_execution");
|
|
889
|
+
}
|
|
890
|
+
const result = await response.json();
|
|
891
|
+
return handleSuccess(result, Date.now() - startMs);
|
|
892
|
+
} catch (err) {
|
|
893
|
+
clearTimeout(timer);
|
|
894
|
+
const isTimeout = err instanceof Error && err.name === "AbortError";
|
|
895
|
+
return handleFailure(
|
|
896
|
+
isTimeout ? "timeout" : "failure",
|
|
897
|
+
Date.now() - startMs,
|
|
898
|
+
isTimeout ? "Execution timeout" : "Handler error",
|
|
899
|
+
isTimeout ? "timeout" : "bad_execution"
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
async function executeCapabilityBatch(options) {
|
|
904
|
+
const { requests, strategy, total_budget, registryDb, creditDb, owner } = options;
|
|
905
|
+
if (requests.length === 0) {
|
|
906
|
+
return { results: [], total_credits_spent: 0, total_credits_refunded: 0, success: true };
|
|
907
|
+
}
|
|
908
|
+
const sumMaxCredits = requests.reduce((acc, r) => acc + r.max_credits, 0);
|
|
909
|
+
if (sumMaxCredits > total_budget) {
|
|
910
|
+
return {
|
|
911
|
+
results: requests.map((_, i) => ({
|
|
912
|
+
request_index: i,
|
|
913
|
+
status: "skipped",
|
|
914
|
+
credits_spent: 0,
|
|
915
|
+
credits_refunded: 0,
|
|
916
|
+
error: `Total requested credits (${sumMaxCredits}) exceeds total_budget (${total_budget})`
|
|
917
|
+
})),
|
|
918
|
+
total_credits_spent: 0,
|
|
919
|
+
total_credits_refunded: 0,
|
|
920
|
+
success: false
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
const executeItem = async (item, index) => {
|
|
924
|
+
const resolved = await resolveTargetCapability(item.skill_id, {
|
|
925
|
+
registryDb,
|
|
926
|
+
registryUrl: options.registryUrl,
|
|
927
|
+
onlineOnly: true
|
|
928
|
+
});
|
|
929
|
+
if (!resolved) {
|
|
930
|
+
return {
|
|
931
|
+
request_index: index,
|
|
932
|
+
status: "failed",
|
|
933
|
+
credits_spent: 0,
|
|
934
|
+
credits_refunded: 0,
|
|
935
|
+
error: `Card/skill not found: ${item.skill_id}`
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
const localCard = getCard(registryDb, resolved.cardId);
|
|
939
|
+
const localCardRaw = localCard;
|
|
940
|
+
const cardName = typeof localCardRaw?.["name"] === "string" ? localCardRaw["name"] : typeof localCardRaw?.["agent_name"] === "string" ? localCardRaw["agent_name"] : resolved.cardId;
|
|
941
|
+
const creditsNeeded = resolved.credits_per_call;
|
|
942
|
+
const resolvedSkillId = resolved.skillId;
|
|
943
|
+
if (creditsNeeded > item.max_credits) {
|
|
944
|
+
return {
|
|
945
|
+
request_index: index,
|
|
946
|
+
status: "failed",
|
|
947
|
+
credits_spent: 0,
|
|
948
|
+
credits_refunded: 0,
|
|
949
|
+
error: `Skill costs ${creditsNeeded} credits but max_credits is ${item.max_credits}`
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
let escrowId;
|
|
953
|
+
try {
|
|
954
|
+
const balance = getBalance(creditDb, owner);
|
|
955
|
+
if (balance < creditsNeeded) {
|
|
956
|
+
return {
|
|
957
|
+
request_index: index,
|
|
958
|
+
status: "failed",
|
|
959
|
+
credits_spent: 0,
|
|
960
|
+
credits_refunded: 0,
|
|
961
|
+
error: "Insufficient credits"
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
escrowId = holdEscrow(creditDb, owner, creditsNeeded, resolved.cardId);
|
|
965
|
+
} catch (err) {
|
|
966
|
+
const msg = err instanceof AgentBnBError ? err.message : "Failed to hold escrow";
|
|
967
|
+
return {
|
|
968
|
+
request_index: index,
|
|
969
|
+
status: "failed",
|
|
970
|
+
credits_spent: 0,
|
|
971
|
+
credits_refunded: 0,
|
|
972
|
+
error: msg
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
const startMs = Date.now();
|
|
976
|
+
try {
|
|
977
|
+
const result = options.dispatchRequest ? await options.dispatchRequest({
|
|
978
|
+
target: resolved,
|
|
979
|
+
params: item.params,
|
|
980
|
+
requester: owner
|
|
981
|
+
}) : { card_id: resolved.cardId, skill_id: resolvedSkillId };
|
|
982
|
+
const latencyMs = Date.now() - startMs;
|
|
983
|
+
settleEscrow(creditDb, escrowId, resolved.owner);
|
|
984
|
+
updateReputation(registryDb, resolved.cardId, true, latencyMs);
|
|
985
|
+
try {
|
|
986
|
+
insertRequestLog(registryDb, {
|
|
987
|
+
id: randomUUID(),
|
|
988
|
+
card_id: resolved.cardId,
|
|
989
|
+
card_name: cardName,
|
|
990
|
+
skill_id: resolvedSkillId,
|
|
991
|
+
requester: owner,
|
|
992
|
+
status: "success",
|
|
993
|
+
latency_ms: latencyMs,
|
|
994
|
+
credits_charged: creditsNeeded,
|
|
995
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
996
|
+
});
|
|
997
|
+
} catch {
|
|
998
|
+
}
|
|
999
|
+
return {
|
|
1000
|
+
request_index: index,
|
|
1001
|
+
status: "success",
|
|
1002
|
+
result,
|
|
1003
|
+
credits_spent: creditsNeeded,
|
|
1004
|
+
credits_refunded: 0
|
|
1005
|
+
};
|
|
1006
|
+
} catch (err) {
|
|
1007
|
+
releaseEscrow(creditDb, escrowId);
|
|
1008
|
+
const latencyMs = Date.now() - startMs;
|
|
1009
|
+
try {
|
|
1010
|
+
insertRequestLog(registryDb, {
|
|
1011
|
+
id: randomUUID(),
|
|
1012
|
+
card_id: resolved.cardId,
|
|
1013
|
+
card_name: cardName,
|
|
1014
|
+
skill_id: resolvedSkillId,
|
|
1015
|
+
requester: owner,
|
|
1016
|
+
status: "failure",
|
|
1017
|
+
latency_ms: latencyMs,
|
|
1018
|
+
credits_charged: 0,
|
|
1019
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1020
|
+
failure_reason: "not_found"
|
|
1021
|
+
});
|
|
1022
|
+
} catch {
|
|
1023
|
+
}
|
|
1024
|
+
return {
|
|
1025
|
+
request_index: index,
|
|
1026
|
+
status: "failed",
|
|
1027
|
+
credits_spent: 0,
|
|
1028
|
+
credits_refunded: creditsNeeded,
|
|
1029
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
let results;
|
|
1034
|
+
if (strategy === "sequential") {
|
|
1035
|
+
results = [];
|
|
1036
|
+
let stopped = false;
|
|
1037
|
+
for (let i = 0; i < requests.length; i++) {
|
|
1038
|
+
if (stopped) {
|
|
1039
|
+
results.push({
|
|
1040
|
+
request_index: i,
|
|
1041
|
+
status: "skipped",
|
|
1042
|
+
credits_spent: 0,
|
|
1043
|
+
credits_refunded: 0,
|
|
1044
|
+
error: "Skipped due to earlier failure"
|
|
1045
|
+
});
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
const result = await executeItem(requests[i], i);
|
|
1049
|
+
results.push(result);
|
|
1050
|
+
if (result.status === "failed") {
|
|
1051
|
+
stopped = true;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
} else {
|
|
1055
|
+
const settled = await Promise.allSettled(
|
|
1056
|
+
requests.map((item, i) => executeItem(item, i))
|
|
1057
|
+
);
|
|
1058
|
+
results = settled.map((outcome, i) => {
|
|
1059
|
+
if (outcome.status === "fulfilled") {
|
|
1060
|
+
return outcome.value;
|
|
1061
|
+
}
|
|
1062
|
+
return {
|
|
1063
|
+
request_index: i,
|
|
1064
|
+
status: "failed",
|
|
1065
|
+
credits_spent: 0,
|
|
1066
|
+
credits_refunded: 0,
|
|
1067
|
+
error: outcome.reason instanceof Error ? outcome.reason.message : "Unknown error"
|
|
1068
|
+
};
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
const total_credits_spent = results.reduce((acc, r) => acc + r.credits_spent, 0);
|
|
1072
|
+
const total_credits_refunded = results.reduce((acc, r) => acc + r.credits_refunded, 0);
|
|
1073
|
+
const success = results.every((r) => r.status === "success");
|
|
1074
|
+
return { results, total_credits_spent, total_credits_refunded, success };
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
export {
|
|
1078
|
+
deriveAgentId,
|
|
1079
|
+
loadOrRepairIdentity,
|
|
1080
|
+
ensureIdentity,
|
|
1081
|
+
identityAuthPlugin,
|
|
1082
|
+
createLedger,
|
|
1083
|
+
syncCreditsFromRegistry,
|
|
1084
|
+
executeCapabilityRequest,
|
|
1085
|
+
executeCapabilityBatch
|
|
1086
|
+
};
|