agentbnb 4.0.0 → 4.0.1
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/{card-IE5UV5QX.js → card-4XH4AOTE.js} +11 -4
- package/dist/chunk-3MJT4PZG.js +50 -0
- package/dist/{conduct-IQYAT6ZU.js → chunk-3UKAVIMC.js} +70 -33
- package/dist/chunk-5AH3CMOX.js +62 -0
- package/dist/{chunk-UJWYE7VL.js → chunk-6K5WUVF3.js} +28 -111
- package/dist/chunk-75OC6E4F.js +33 -0
- package/dist/{chunk-QO67IGCW.js → chunk-DVAS2443.js} +1 -1
- package/dist/{chunk-XA63SD4T.js → chunk-FNKBHBYK.js} +3 -0
- package/dist/{websocket-client-5TIQDYQ4.js → chunk-JOY533UH.js} +38 -4
- package/dist/{chunk-RSX4SCPN.js → chunk-KJG2UJV5.js} +3 -3
- package/dist/chunk-M3G5NR2Z.js +90 -0
- package/dist/{chunk-HEVXCYCY.js → chunk-MQKYGY5I.js} +61 -24
- package/dist/chunk-ODBGCCEH.js +358 -0
- package/dist/{chunk-CUVIWPQO.js → chunk-Q7HRI666.js} +7 -6
- package/dist/chunk-QJEOCKVF.js +148 -0
- package/dist/{chunk-3Y36WQDV.js → chunk-QT7TEVNV.js} +14 -2
- package/dist/{chunk-UOGDK2S2.js → chunk-TLU7ALCZ.js} +1 -1
- package/dist/{chunk-QVV2P3FN.js → chunk-XQHN6ITI.js} +1 -1
- package/dist/cli/index.js +2665 -845
- package/dist/{client-IOTK6GOS.js → client-BTPIFY7E.js} +3 -3
- package/dist/conduct-CW62HBPT.js +52 -0
- package/dist/conduct-FXLVGKD5.js +19 -0
- package/dist/{conductor-mode-XU7ONJWC.js → conductor-mode-3JS4VWCR.js} +16 -9
- package/dist/execute-EXOITLHN.js +10 -0
- package/dist/index.d.ts +1005 -916
- package/dist/index.js +516 -120
- package/dist/{peers-G36URZYB.js → peers-K7FSHPN3.js} +2 -1
- package/dist/request-CNZ3XIVX.js +196 -0
- package/dist/serve-skill-SUOGUM7N.js +104 -0
- package/dist/server-2LWHL24P.js +295 -0
- package/dist/types-FGBUZ3QV.js +18 -0
- package/dist/websocket-client-6IIDGXKB.js +7 -0
- package/package.json +1 -1
- package/dist/chunk-BEI5MTNZ.js +0 -91
- package/dist/cli/index.d.ts +0 -1
- package/dist/execute-GDGBU6DJ.js +0 -10
package/dist/cli/index.js
CHANGED
|
@@ -1,37 +1,50 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from "../chunk-CUVIWPQO.js";
|
|
3
|
+
deriveAgentId,
|
|
4
|
+
ensureIdentity
|
|
5
|
+
} from "../chunk-M3G5NR2Z.js";
|
|
7
6
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
createLedger,
|
|
8
|
+
identityAuthPlugin
|
|
9
|
+
} from "../chunk-ODBGCCEH.js";
|
|
10
|
+
import {
|
|
11
|
+
interpolateObject
|
|
12
|
+
} from "../chunk-3MJT4PZG.js";
|
|
10
13
|
import {
|
|
11
14
|
AutoRequestor,
|
|
12
15
|
BudgetManager,
|
|
13
16
|
DEFAULT_AUTONOMY_CONFIG,
|
|
14
17
|
DEFAULT_BUDGET_CONFIG,
|
|
15
|
-
filterCards,
|
|
16
18
|
getAutonomyTier,
|
|
17
19
|
insertAuditEvent,
|
|
18
|
-
interpolateObject,
|
|
19
20
|
listPendingRequests,
|
|
20
|
-
resolvePendingRequest
|
|
21
|
+
resolvePendingRequest
|
|
22
|
+
} from "../chunk-6K5WUVF3.js";
|
|
23
|
+
import {
|
|
24
|
+
fetchRemoteCards,
|
|
25
|
+
filterCards,
|
|
26
|
+
mergeResults,
|
|
21
27
|
searchCards
|
|
22
|
-
} from "../chunk-
|
|
28
|
+
} from "../chunk-QJEOCKVF.js";
|
|
23
29
|
import {
|
|
24
30
|
requestCapability
|
|
25
|
-
} from "../chunk-
|
|
31
|
+
} from "../chunk-KJG2UJV5.js";
|
|
26
32
|
import {
|
|
27
33
|
findPeer,
|
|
28
|
-
getConfigDir,
|
|
29
|
-
loadConfig,
|
|
30
34
|
loadPeers,
|
|
31
35
|
removePeer,
|
|
32
|
-
saveConfig,
|
|
33
36
|
savePeer
|
|
34
|
-
} from "../chunk-
|
|
37
|
+
} from "../chunk-5AH3CMOX.js";
|
|
38
|
+
import {
|
|
39
|
+
getConfigDir,
|
|
40
|
+
loadConfig,
|
|
41
|
+
saveConfig
|
|
42
|
+
} from "../chunk-75OC6E4F.js";
|
|
43
|
+
import {
|
|
44
|
+
executeCapabilityRequest,
|
|
45
|
+
releaseRequesterEscrow,
|
|
46
|
+
settleRequesterEscrow
|
|
47
|
+
} from "../chunk-Q7HRI666.js";
|
|
35
48
|
import {
|
|
36
49
|
getActivityFeed,
|
|
37
50
|
getCard,
|
|
@@ -44,129 +57,53 @@ import {
|
|
|
44
57
|
updateCard,
|
|
45
58
|
updateSkillAvailability,
|
|
46
59
|
updateSkillIdleRate
|
|
47
|
-
} from "../chunk-
|
|
60
|
+
} from "../chunk-TLU7ALCZ.js";
|
|
48
61
|
import {
|
|
49
62
|
bootstrapAgent,
|
|
50
63
|
getBalance,
|
|
51
64
|
getTransactions,
|
|
52
65
|
holdEscrow,
|
|
53
66
|
openCreditDb,
|
|
54
|
-
releaseEscrow
|
|
55
|
-
|
|
67
|
+
releaseEscrow,
|
|
68
|
+
settleEscrow
|
|
69
|
+
} from "../chunk-XQHN6ITI.js";
|
|
56
70
|
import {
|
|
57
71
|
generateKeyPair,
|
|
58
72
|
loadKeyPair,
|
|
59
73
|
saveKeyPair,
|
|
60
74
|
signEscrowReceipt,
|
|
61
75
|
verifyEscrowReceipt
|
|
62
|
-
} from "../chunk-
|
|
76
|
+
} from "../chunk-DVAS2443.js";
|
|
63
77
|
import {
|
|
64
78
|
AgentBnBError,
|
|
65
79
|
AnyCardSchema,
|
|
66
80
|
CapabilityCardV2Schema
|
|
67
|
-
} from "../chunk-
|
|
81
|
+
} from "../chunk-FNKBHBYK.js";
|
|
82
|
+
import {
|
|
83
|
+
RelayMessageSchema
|
|
84
|
+
} from "../chunk-QT7TEVNV.js";
|
|
68
85
|
|
|
69
86
|
// src/cli/index.ts
|
|
70
87
|
import { Command } from "commander";
|
|
71
|
-
import { readFileSync as
|
|
88
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
72
89
|
import { createRequire } from "module";
|
|
73
|
-
import { randomBytes } from "crypto";
|
|
74
|
-
import { join as
|
|
90
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
91
|
+
import { join as join3 } from "path";
|
|
75
92
|
import { networkInterfaces, homedir } from "os";
|
|
76
93
|
import { createInterface as createInterface2 } from "readline";
|
|
77
94
|
|
|
78
|
-
// src/identity/identity.ts
|
|
79
|
-
import { z } from "zod";
|
|
80
|
-
import { createHash } from "crypto";
|
|
81
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
82
|
-
import { join } from "path";
|
|
83
|
-
var AgentIdentitySchema = z.object({
|
|
84
|
-
/** Deterministic ID derived from public key: sha256(hex).slice(0, 16). */
|
|
85
|
-
agent_id: z.string().min(1),
|
|
86
|
-
/** Human-readable owner name (from config or init). */
|
|
87
|
-
owner: z.string().min(1),
|
|
88
|
-
/** Hex-encoded Ed25519 public key. */
|
|
89
|
-
public_key: z.string().min(1),
|
|
90
|
-
/** ISO 8601 timestamp of identity creation. */
|
|
91
|
-
created_at: z.string().datetime(),
|
|
92
|
-
/** Optional guarantor info if linked to a human. */
|
|
93
|
-
guarantor: z.object({
|
|
94
|
-
github_login: z.string().min(1),
|
|
95
|
-
verified_at: z.string().datetime()
|
|
96
|
-
}).optional()
|
|
97
|
-
});
|
|
98
|
-
var AgentCertificateSchema = z.object({
|
|
99
|
-
identity: AgentIdentitySchema,
|
|
100
|
-
/** ISO 8601 timestamp of certificate issuance. */
|
|
101
|
-
issued_at: z.string().datetime(),
|
|
102
|
-
/** ISO 8601 timestamp of certificate expiry. */
|
|
103
|
-
expires_at: z.string().datetime(),
|
|
104
|
-
/** Hex-encoded public key of the issuer (same as identity for self-signed). */
|
|
105
|
-
issuer_public_key: z.string().min(1),
|
|
106
|
-
/** Base64url Ed25519 signature over { identity, issued_at, expires_at, issuer_public_key }. */
|
|
107
|
-
signature: z.string().min(1)
|
|
108
|
-
});
|
|
109
|
-
var IDENTITY_FILENAME = "identity.json";
|
|
110
|
-
function deriveAgentId(publicKeyHex) {
|
|
111
|
-
return createHash("sha256").update(publicKeyHex, "hex").digest("hex").slice(0, 16);
|
|
112
|
-
}
|
|
113
|
-
function createIdentity(configDir, owner) {
|
|
114
|
-
if (!existsSync(configDir)) {
|
|
115
|
-
mkdirSync(configDir, { recursive: true });
|
|
116
|
-
}
|
|
117
|
-
let keys;
|
|
118
|
-
try {
|
|
119
|
-
keys = loadKeyPair(configDir);
|
|
120
|
-
} catch {
|
|
121
|
-
keys = generateKeyPair();
|
|
122
|
-
saveKeyPair(configDir, keys);
|
|
123
|
-
}
|
|
124
|
-
const publicKeyHex = keys.publicKey.toString("hex");
|
|
125
|
-
const agentId = deriveAgentId(publicKeyHex);
|
|
126
|
-
const identity = {
|
|
127
|
-
agent_id: agentId,
|
|
128
|
-
owner,
|
|
129
|
-
public_key: publicKeyHex,
|
|
130
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
131
|
-
};
|
|
132
|
-
saveIdentity(configDir, identity);
|
|
133
|
-
return identity;
|
|
134
|
-
}
|
|
135
|
-
function loadIdentity(configDir) {
|
|
136
|
-
const filePath = join(configDir, IDENTITY_FILENAME);
|
|
137
|
-
if (!existsSync(filePath)) return null;
|
|
138
|
-
try {
|
|
139
|
-
const raw = readFileSync(filePath, "utf-8");
|
|
140
|
-
return AgentIdentitySchema.parse(JSON.parse(raw));
|
|
141
|
-
} catch {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
function saveIdentity(configDir, identity) {
|
|
146
|
-
if (!existsSync(configDir)) {
|
|
147
|
-
mkdirSync(configDir, { recursive: true });
|
|
148
|
-
}
|
|
149
|
-
const filePath = join(configDir, IDENTITY_FILENAME);
|
|
150
|
-
writeFileSync(filePath, JSON.stringify(identity, null, 2), "utf-8");
|
|
151
|
-
}
|
|
152
|
-
function ensureIdentity(configDir, owner) {
|
|
153
|
-
const existing = loadIdentity(configDir);
|
|
154
|
-
if (existing) return existing;
|
|
155
|
-
return createIdentity(configDir, owner);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
95
|
// src/credit/escrow-receipt.ts
|
|
159
|
-
import { z
|
|
96
|
+
import { z } from "zod";
|
|
160
97
|
import { randomUUID } from "crypto";
|
|
161
|
-
var EscrowReceiptSchema =
|
|
162
|
-
requester_owner:
|
|
163
|
-
requester_public_key:
|
|
164
|
-
amount:
|
|
165
|
-
card_id:
|
|
166
|
-
skill_id:
|
|
167
|
-
timestamp:
|
|
168
|
-
nonce:
|
|
169
|
-
signature:
|
|
98
|
+
var EscrowReceiptSchema = z.object({
|
|
99
|
+
requester_owner: z.string().min(1),
|
|
100
|
+
requester_public_key: z.string().min(1),
|
|
101
|
+
amount: z.number().positive(),
|
|
102
|
+
card_id: z.string().min(1),
|
|
103
|
+
skill_id: z.string().optional(),
|
|
104
|
+
timestamp: z.string(),
|
|
105
|
+
nonce: z.string().uuid(),
|
|
106
|
+
signature: z.string().min(1)
|
|
170
107
|
});
|
|
171
108
|
function createSignedEscrowReceipt(db, privateKey, publicKey, opts) {
|
|
172
109
|
const escrowId = holdEscrow(db, opts.owner, opts.amount, opts.cardId);
|
|
@@ -280,89 +217,6 @@ var IdleMonitor = class {
|
|
|
280
217
|
}
|
|
281
218
|
};
|
|
282
219
|
|
|
283
|
-
// src/cli/remote-registry.ts
|
|
284
|
-
var RegistryTimeoutError = class extends AgentBnBError {
|
|
285
|
-
constructor(url) {
|
|
286
|
-
super(
|
|
287
|
-
`Registry at ${url} did not respond within 5s. Showing local results only.`,
|
|
288
|
-
"REGISTRY_TIMEOUT"
|
|
289
|
-
);
|
|
290
|
-
this.name = "RegistryTimeoutError";
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
var RegistryConnectionError = class extends AgentBnBError {
|
|
294
|
-
constructor(url) {
|
|
295
|
-
super(
|
|
296
|
-
`Cannot reach ${url}. Is the registry running? Showing local results only.`,
|
|
297
|
-
"REGISTRY_CONNECTION"
|
|
298
|
-
);
|
|
299
|
-
this.name = "RegistryConnectionError";
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
var RegistryAuthError = class extends AgentBnBError {
|
|
303
|
-
constructor(url) {
|
|
304
|
-
super(
|
|
305
|
-
`Authentication failed for ${url}. Run \`agentbnb config set token <your-token>\`.`,
|
|
306
|
-
"REGISTRY_AUTH"
|
|
307
|
-
);
|
|
308
|
-
this.name = "RegistryAuthError";
|
|
309
|
-
}
|
|
310
|
-
};
|
|
311
|
-
async function fetchRemoteCards(registryUrl, params, timeoutMs = 5e3) {
|
|
312
|
-
let cardsUrl;
|
|
313
|
-
try {
|
|
314
|
-
cardsUrl = new URL("/cards", registryUrl);
|
|
315
|
-
} catch {
|
|
316
|
-
throw new AgentBnBError(`Invalid registry URL: ${registryUrl}`, "INVALID_REGISTRY_URL");
|
|
317
|
-
}
|
|
318
|
-
const searchParams = new URLSearchParams();
|
|
319
|
-
if (params.q !== void 0) searchParams.set("q", params.q);
|
|
320
|
-
if (params.level !== void 0) searchParams.set("level", String(params.level));
|
|
321
|
-
if (params.online !== void 0) searchParams.set("online", String(params.online));
|
|
322
|
-
if (params.tag !== void 0) searchParams.set("tag", params.tag);
|
|
323
|
-
searchParams.set("limit", "100");
|
|
324
|
-
cardsUrl.search = searchParams.toString();
|
|
325
|
-
const controller = new AbortController();
|
|
326
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
327
|
-
let response;
|
|
328
|
-
try {
|
|
329
|
-
response = await fetch(cardsUrl.toString(), { signal: controller.signal });
|
|
330
|
-
} catch (err) {
|
|
331
|
-
clearTimeout(timer);
|
|
332
|
-
const isTimeout = err instanceof Error && err.name === "AbortError";
|
|
333
|
-
if (isTimeout) {
|
|
334
|
-
throw new RegistryTimeoutError(registryUrl);
|
|
335
|
-
}
|
|
336
|
-
throw new RegistryConnectionError(registryUrl);
|
|
337
|
-
} finally {
|
|
338
|
-
clearTimeout(timer);
|
|
339
|
-
}
|
|
340
|
-
if (response.status === 401 || response.status === 403) {
|
|
341
|
-
throw new RegistryAuthError(registryUrl);
|
|
342
|
-
}
|
|
343
|
-
if (!response.ok) {
|
|
344
|
-
throw new RegistryConnectionError(registryUrl);
|
|
345
|
-
}
|
|
346
|
-
const body = await response.json();
|
|
347
|
-
return body.items;
|
|
348
|
-
}
|
|
349
|
-
function mergeResults(localCards, remoteCards, hasQuery) {
|
|
350
|
-
const taggedLocal = localCards.map((c) => ({ ...c, source: "local" }));
|
|
351
|
-
const taggedRemote = remoteCards.map((c) => ({ ...c, source: "remote" }));
|
|
352
|
-
const localIds = new Set(localCards.map((c) => c.id));
|
|
353
|
-
const dedupedRemote = taggedRemote.filter((c) => !localIds.has(c.id));
|
|
354
|
-
if (!hasQuery) {
|
|
355
|
-
return [...taggedLocal, ...dedupedRemote];
|
|
356
|
-
}
|
|
357
|
-
const result = [];
|
|
358
|
-
const maxLen = Math.max(taggedLocal.length, dedupedRemote.length);
|
|
359
|
-
for (let i = 0; i < maxLen; i++) {
|
|
360
|
-
if (i < taggedLocal.length) result.push(taggedLocal[i]);
|
|
361
|
-
if (i < dedupedRemote.length) result.push(dedupedRemote[i]);
|
|
362
|
-
}
|
|
363
|
-
return result;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
220
|
// src/cli/onboarding.ts
|
|
367
221
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
368
222
|
import { createConnection } from "net";
|
|
@@ -534,8 +388,8 @@ function buildDraftCard(apiKey, owner) {
|
|
|
534
388
|
|
|
535
389
|
// src/onboarding/index.ts
|
|
536
390
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
537
|
-
import { existsSync
|
|
538
|
-
import { join
|
|
391
|
+
import { existsSync, readFileSync } from "fs";
|
|
392
|
+
import { join } from "path";
|
|
539
393
|
|
|
540
394
|
// src/onboarding/capability-templates.ts
|
|
541
395
|
var API_PATTERNS = [
|
|
@@ -650,9 +504,9 @@ var DOC_FILES = ["SOUL.md", "CLAUDE.md", "AGENTS.md", "README.md"];
|
|
|
650
504
|
function detectCapabilities(opts = {}) {
|
|
651
505
|
const cwd = opts.cwd ?? process.cwd();
|
|
652
506
|
if (opts.fromFile) {
|
|
653
|
-
const filePath = opts.fromFile.startsWith("/") ? opts.fromFile :
|
|
654
|
-
if (
|
|
655
|
-
const content =
|
|
507
|
+
const filePath = opts.fromFile.startsWith("/") ? opts.fromFile : join(cwd, opts.fromFile);
|
|
508
|
+
if (existsSync(filePath)) {
|
|
509
|
+
const content = readFileSync(filePath, "utf-8");
|
|
656
510
|
const capabilities = detectFromDocs(content);
|
|
657
511
|
if (capabilities.length > 0) {
|
|
658
512
|
return { source: "docs", capabilities, sourceFile: filePath };
|
|
@@ -661,9 +515,9 @@ function detectCapabilities(opts = {}) {
|
|
|
661
515
|
return { source: "none", capabilities: [] };
|
|
662
516
|
}
|
|
663
517
|
for (const fileName of DOC_FILES) {
|
|
664
|
-
const filePath =
|
|
665
|
-
if (!
|
|
666
|
-
const content =
|
|
518
|
+
const filePath = join(cwd, fileName);
|
|
519
|
+
if (!existsSync(filePath)) continue;
|
|
520
|
+
const content = readFileSync(filePath, "utf-8");
|
|
667
521
|
if (fileName === "SOUL.md") {
|
|
668
522
|
return { source: "soul", capabilities: [], soulContent: content, sourceFile: filePath };
|
|
669
523
|
}
|
|
@@ -707,8 +561,49 @@ function capabilitiesToV2Card(capabilities, owner, agentName) {
|
|
|
707
561
|
return CapabilityCardV2Schema.parse(card);
|
|
708
562
|
}
|
|
709
563
|
|
|
564
|
+
// src/registry/pricing.ts
|
|
565
|
+
function getPricingStats(db, query) {
|
|
566
|
+
const cards = searchCards(db, query);
|
|
567
|
+
const prices = [];
|
|
568
|
+
const queryLower = query.toLowerCase();
|
|
569
|
+
const queryWords = queryLower.split(/\s+/).filter((w) => w.length > 0);
|
|
570
|
+
for (const card of cards) {
|
|
571
|
+
const v2 = card;
|
|
572
|
+
if (v2.skills && v2.skills.length > 0) {
|
|
573
|
+
for (const skill of v2.skills) {
|
|
574
|
+
const nameMatch = skillMatchesQuery(skill, queryWords);
|
|
575
|
+
if (nameMatch) {
|
|
576
|
+
prices.push(skill.pricing.credits_per_call);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
} else {
|
|
580
|
+
prices.push(card.pricing.credits_per_call);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (prices.length === 0) {
|
|
584
|
+
return { min: 0, max: 0, median: 0, mean: 0, count: 0 };
|
|
585
|
+
}
|
|
586
|
+
prices.sort((a, b) => a - b);
|
|
587
|
+
const min = prices[0];
|
|
588
|
+
const max = prices[prices.length - 1];
|
|
589
|
+
const mean = prices.reduce((sum, p) => sum + p, 0) / prices.length;
|
|
590
|
+
const median = computeMedian(prices);
|
|
591
|
+
return { min, max, median, mean, count: prices.length };
|
|
592
|
+
}
|
|
593
|
+
function skillMatchesQuery(skill, queryWords) {
|
|
594
|
+
const text = `${skill.name} ${skill.description}`.toLowerCase();
|
|
595
|
+
return queryWords.some((word) => text.includes(word));
|
|
596
|
+
}
|
|
597
|
+
function computeMedian(sorted) {
|
|
598
|
+
const mid = Math.floor(sorted.length / 2);
|
|
599
|
+
if (sorted.length % 2 === 1) {
|
|
600
|
+
return sorted[mid];
|
|
601
|
+
}
|
|
602
|
+
return (sorted[mid - 1] + sorted[mid]) / 2;
|
|
603
|
+
}
|
|
604
|
+
|
|
710
605
|
// src/runtime/agent-runtime.ts
|
|
711
|
-
import { readFileSync as
|
|
606
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
712
607
|
|
|
713
608
|
// src/skills/executor.ts
|
|
714
609
|
var SkillExecutor = class {
|
|
@@ -735,7 +630,7 @@ var SkillExecutor = class {
|
|
|
735
630
|
* @param params - Input parameters for the skill.
|
|
736
631
|
* @returns ExecutionResult including success, result/error, and latency_ms.
|
|
737
632
|
*/
|
|
738
|
-
async execute(skillId, params) {
|
|
633
|
+
async execute(skillId, params, onProgress) {
|
|
739
634
|
const startTime = Date.now();
|
|
740
635
|
const config = this.skillMap.get(skillId);
|
|
741
636
|
if (!config) {
|
|
@@ -754,7 +649,7 @@ var SkillExecutor = class {
|
|
|
754
649
|
};
|
|
755
650
|
}
|
|
756
651
|
try {
|
|
757
|
-
const modeResult = await mode.execute(config, params);
|
|
652
|
+
const modeResult = await mode.execute(config, params, onProgress);
|
|
758
653
|
return {
|
|
759
654
|
...modeResult,
|
|
760
655
|
latency_ms: Date.now() - startTime
|
|
@@ -791,98 +686,98 @@ function createSkillExecutor(configs, modes) {
|
|
|
791
686
|
}
|
|
792
687
|
|
|
793
688
|
// src/skills/skill-config.ts
|
|
794
|
-
import { z as
|
|
689
|
+
import { z as z2 } from "zod";
|
|
795
690
|
import yaml from "js-yaml";
|
|
796
|
-
var PricingSchema =
|
|
797
|
-
credits_per_call:
|
|
798
|
-
credits_per_minute:
|
|
799
|
-
free_tier:
|
|
691
|
+
var PricingSchema = z2.object({
|
|
692
|
+
credits_per_call: z2.number().nonnegative(),
|
|
693
|
+
credits_per_minute: z2.number().nonnegative().optional(),
|
|
694
|
+
free_tier: z2.number().nonnegative().optional()
|
|
800
695
|
});
|
|
801
|
-
var ApiAuthSchema =
|
|
802
|
-
|
|
803
|
-
type:
|
|
804
|
-
token:
|
|
696
|
+
var ApiAuthSchema = z2.discriminatedUnion("type", [
|
|
697
|
+
z2.object({
|
|
698
|
+
type: z2.literal("bearer"),
|
|
699
|
+
token: z2.string()
|
|
805
700
|
}),
|
|
806
|
-
|
|
807
|
-
type:
|
|
808
|
-
header:
|
|
809
|
-
key:
|
|
701
|
+
z2.object({
|
|
702
|
+
type: z2.literal("apikey"),
|
|
703
|
+
header: z2.string().default("X-API-Key"),
|
|
704
|
+
key: z2.string()
|
|
810
705
|
}),
|
|
811
|
-
|
|
812
|
-
type:
|
|
813
|
-
username:
|
|
814
|
-
password:
|
|
706
|
+
z2.object({
|
|
707
|
+
type: z2.literal("basic"),
|
|
708
|
+
username: z2.string(),
|
|
709
|
+
password: z2.string()
|
|
815
710
|
})
|
|
816
711
|
]);
|
|
817
|
-
var ApiSkillConfigSchema =
|
|
818
|
-
id:
|
|
819
|
-
type:
|
|
820
|
-
name:
|
|
821
|
-
endpoint:
|
|
822
|
-
method:
|
|
712
|
+
var ApiSkillConfigSchema = z2.object({
|
|
713
|
+
id: z2.string().min(1),
|
|
714
|
+
type: z2.literal("api"),
|
|
715
|
+
name: z2.string().min(1),
|
|
716
|
+
endpoint: z2.string().min(1),
|
|
717
|
+
method: z2.enum(["GET", "POST", "PUT", "DELETE"]),
|
|
823
718
|
auth: ApiAuthSchema.optional(),
|
|
824
|
-
input_mapping:
|
|
825
|
-
output_mapping:
|
|
719
|
+
input_mapping: z2.record(z2.string()).default({}),
|
|
720
|
+
output_mapping: z2.record(z2.string()).default({}),
|
|
826
721
|
pricing: PricingSchema,
|
|
827
|
-
timeout_ms:
|
|
828
|
-
retries:
|
|
829
|
-
provider:
|
|
722
|
+
timeout_ms: z2.number().positive().default(3e4),
|
|
723
|
+
retries: z2.number().nonnegative().int().default(0),
|
|
724
|
+
provider: z2.string().optional()
|
|
830
725
|
});
|
|
831
|
-
var PipelineStepSchema =
|
|
832
|
-
|
|
833
|
-
skill_id:
|
|
834
|
-
input_mapping:
|
|
726
|
+
var PipelineStepSchema = z2.union([
|
|
727
|
+
z2.object({
|
|
728
|
+
skill_id: z2.string().min(1),
|
|
729
|
+
input_mapping: z2.record(z2.string()).default({})
|
|
835
730
|
}),
|
|
836
|
-
|
|
837
|
-
command:
|
|
838
|
-
input_mapping:
|
|
731
|
+
z2.object({
|
|
732
|
+
command: z2.string().min(1),
|
|
733
|
+
input_mapping: z2.record(z2.string()).default({})
|
|
839
734
|
})
|
|
840
735
|
]);
|
|
841
|
-
var PipelineSkillConfigSchema =
|
|
842
|
-
id:
|
|
843
|
-
type:
|
|
844
|
-
name:
|
|
845
|
-
steps:
|
|
736
|
+
var PipelineSkillConfigSchema = z2.object({
|
|
737
|
+
id: z2.string().min(1),
|
|
738
|
+
type: z2.literal("pipeline"),
|
|
739
|
+
name: z2.string().min(1),
|
|
740
|
+
steps: z2.array(PipelineStepSchema).min(1),
|
|
846
741
|
pricing: PricingSchema,
|
|
847
|
-
timeout_ms:
|
|
742
|
+
timeout_ms: z2.number().positive().optional()
|
|
848
743
|
});
|
|
849
|
-
var OpenClawSkillConfigSchema =
|
|
850
|
-
id:
|
|
851
|
-
type:
|
|
852
|
-
name:
|
|
853
|
-
agent_name:
|
|
854
|
-
channel:
|
|
744
|
+
var OpenClawSkillConfigSchema = z2.object({
|
|
745
|
+
id: z2.string().min(1),
|
|
746
|
+
type: z2.literal("openclaw"),
|
|
747
|
+
name: z2.string().min(1),
|
|
748
|
+
agent_name: z2.string().min(1),
|
|
749
|
+
channel: z2.enum(["telegram", "webhook", "process"]),
|
|
855
750
|
pricing: PricingSchema,
|
|
856
|
-
timeout_ms:
|
|
751
|
+
timeout_ms: z2.number().positive().optional()
|
|
857
752
|
});
|
|
858
|
-
var CommandSkillConfigSchema =
|
|
859
|
-
id:
|
|
860
|
-
type:
|
|
861
|
-
name:
|
|
862
|
-
command:
|
|
863
|
-
output_type:
|
|
864
|
-
allowed_commands:
|
|
865
|
-
working_dir:
|
|
866
|
-
timeout_ms:
|
|
753
|
+
var CommandSkillConfigSchema = z2.object({
|
|
754
|
+
id: z2.string().min(1),
|
|
755
|
+
type: z2.literal("command"),
|
|
756
|
+
name: z2.string().min(1),
|
|
757
|
+
command: z2.string().min(1),
|
|
758
|
+
output_type: z2.enum(["json", "text", "file"]),
|
|
759
|
+
allowed_commands: z2.array(z2.string()).optional(),
|
|
760
|
+
working_dir: z2.string().optional(),
|
|
761
|
+
timeout_ms: z2.number().positive().default(3e4),
|
|
867
762
|
pricing: PricingSchema
|
|
868
763
|
});
|
|
869
|
-
var ConductorSkillConfigSchema =
|
|
870
|
-
id:
|
|
871
|
-
type:
|
|
872
|
-
name:
|
|
873
|
-
conductor_skill:
|
|
764
|
+
var ConductorSkillConfigSchema = z2.object({
|
|
765
|
+
id: z2.string().min(1),
|
|
766
|
+
type: z2.literal("conductor"),
|
|
767
|
+
name: z2.string().min(1),
|
|
768
|
+
conductor_skill: z2.enum(["orchestrate", "plan"]),
|
|
874
769
|
pricing: PricingSchema,
|
|
875
|
-
timeout_ms:
|
|
770
|
+
timeout_ms: z2.number().positive().optional()
|
|
876
771
|
});
|
|
877
|
-
var SkillConfigSchema =
|
|
772
|
+
var SkillConfigSchema = z2.discriminatedUnion("type", [
|
|
878
773
|
ApiSkillConfigSchema,
|
|
879
774
|
PipelineSkillConfigSchema,
|
|
880
775
|
OpenClawSkillConfigSchema,
|
|
881
776
|
CommandSkillConfigSchema,
|
|
882
777
|
ConductorSkillConfigSchema
|
|
883
778
|
]);
|
|
884
|
-
var SkillsFileSchema =
|
|
885
|
-
skills:
|
|
779
|
+
var SkillsFileSchema = z2.object({
|
|
780
|
+
skills: z2.array(SkillConfigSchema)
|
|
886
781
|
});
|
|
887
782
|
function expandEnvVars(value) {
|
|
888
783
|
return value.replace(/\$\{([^}]+)\}/g, (_match, varName) => {
|
|
@@ -1125,7 +1020,7 @@ var PipelineExecutor = class {
|
|
|
1125
1020
|
* @param params - Input parameters from the caller.
|
|
1126
1021
|
* @returns Partial ExecutionResult (without latency_ms — added by SkillExecutor wrapper).
|
|
1127
1022
|
*/
|
|
1128
|
-
async execute(config, params) {
|
|
1023
|
+
async execute(config, params, onProgress) {
|
|
1129
1024
|
const pipelineConfig = config;
|
|
1130
1025
|
const steps = pipelineConfig.steps ?? [];
|
|
1131
1026
|
if (steps.length === 0) {
|
|
@@ -1184,6 +1079,13 @@ var PipelineExecutor = class {
|
|
|
1184
1079
|
}
|
|
1185
1080
|
context.steps.push({ result: stepResult });
|
|
1186
1081
|
context.prev = { result: stepResult };
|
|
1082
|
+
if (onProgress && i < steps.length - 1) {
|
|
1083
|
+
onProgress({
|
|
1084
|
+
step: i + 1,
|
|
1085
|
+
total: steps.length,
|
|
1086
|
+
message: `Completed step ${i + 1}/${steps.length}`
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1187
1089
|
}
|
|
1188
1090
|
const lastStep = context.steps[context.steps.length - 1];
|
|
1189
1091
|
return {
|
|
@@ -1519,20 +1421,20 @@ var AgentRuntime = class {
|
|
|
1519
1421
|
* 3. Populate the Map with all 4 modes — SkillExecutor sees them via reference.
|
|
1520
1422
|
*/
|
|
1521
1423
|
async initSkillExecutor() {
|
|
1522
|
-
const hasSkillsYaml = this.skillsYamlPath &&
|
|
1424
|
+
const hasSkillsYaml = this.skillsYamlPath && existsSync2(this.skillsYamlPath);
|
|
1523
1425
|
if (!hasSkillsYaml && !this.conductorEnabled) {
|
|
1524
1426
|
return;
|
|
1525
1427
|
}
|
|
1526
1428
|
let configs = [];
|
|
1527
1429
|
if (hasSkillsYaml) {
|
|
1528
|
-
const yamlContent =
|
|
1430
|
+
const yamlContent = readFileSync2(this.skillsYamlPath, "utf8");
|
|
1529
1431
|
configs = parseSkillsFile(yamlContent);
|
|
1530
1432
|
}
|
|
1531
1433
|
const modes = /* @__PURE__ */ new Map();
|
|
1532
1434
|
if (this.conductorEnabled) {
|
|
1533
|
-
const { ConductorMode } = await import("../conductor-mode-
|
|
1534
|
-
const { registerConductorCard, CONDUCTOR_OWNER } = await import("../card-
|
|
1535
|
-
const { loadPeers: loadPeers2 } = await import("../peers-
|
|
1435
|
+
const { ConductorMode } = await import("../conductor-mode-3JS4VWCR.js");
|
|
1436
|
+
const { registerConductorCard, CONDUCTOR_OWNER } = await import("../card-4XH4AOTE.js");
|
|
1437
|
+
const { loadPeers: loadPeers2 } = await import("../peers-K7FSHPN3.js");
|
|
1536
1438
|
registerConductorCard(this.registryDb);
|
|
1537
1439
|
const resolveAgentUrl = (owner) => {
|
|
1538
1440
|
const peers = loadPeers2();
|
|
@@ -1644,7 +1546,7 @@ function createGatewayServer(opts) {
|
|
|
1644
1546
|
creditDb,
|
|
1645
1547
|
tokens,
|
|
1646
1548
|
handlerUrl,
|
|
1647
|
-
timeoutMs =
|
|
1549
|
+
timeoutMs = 3e5,
|
|
1648
1550
|
silent = false,
|
|
1649
1551
|
skillExecutor
|
|
1650
1552
|
} = opts;
|
|
@@ -1739,21 +1641,365 @@ function createGatewayServer(opts) {
|
|
|
1739
1641
|
// src/registry/server.ts
|
|
1740
1642
|
import Fastify2 from "fastify";
|
|
1741
1643
|
import cors from "@fastify/cors";
|
|
1644
|
+
import swagger from "@fastify/swagger";
|
|
1645
|
+
import swaggerUi from "@fastify/swagger-ui";
|
|
1742
1646
|
import fastifyStatic from "@fastify/static";
|
|
1743
1647
|
import fastifyWebsocket from "@fastify/websocket";
|
|
1744
|
-
import { join as
|
|
1648
|
+
import { join as join2, dirname } from "path";
|
|
1745
1649
|
import { fileURLToPath } from "url";
|
|
1746
|
-
import { existsSync as
|
|
1650
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1747
1651
|
|
|
1748
1652
|
// src/relay/websocket-relay.ts
|
|
1653
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
1654
|
+
|
|
1655
|
+
// src/relay/relay-credit.ts
|
|
1656
|
+
function lookupCardPrice(registryDb, cardId, skillId) {
|
|
1657
|
+
const row = registryDb.prepare("SELECT data FROM capability_cards WHERE id = ?").get(cardId);
|
|
1658
|
+
if (!row) return null;
|
|
1659
|
+
let card;
|
|
1660
|
+
try {
|
|
1661
|
+
card = JSON.parse(row.data);
|
|
1662
|
+
} catch {
|
|
1663
|
+
return null;
|
|
1664
|
+
}
|
|
1665
|
+
if (skillId && Array.isArray(card.skills)) {
|
|
1666
|
+
const skills = card.skills;
|
|
1667
|
+
const skill = skills.find((s) => s.id === skillId);
|
|
1668
|
+
if (skill) {
|
|
1669
|
+
const skillPricing = skill.pricing;
|
|
1670
|
+
if (skillPricing && typeof skillPricing.credits_per_call === "number") {
|
|
1671
|
+
return skillPricing.credits_per_call;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
const pricing = card.pricing;
|
|
1676
|
+
if (!pricing || typeof pricing.credits_per_call !== "number") {
|
|
1677
|
+
return null;
|
|
1678
|
+
}
|
|
1679
|
+
return pricing.credits_per_call;
|
|
1680
|
+
}
|
|
1681
|
+
function holdForRelay(creditDb, owner, amount, cardId) {
|
|
1682
|
+
return holdEscrow(creditDb, owner, amount, cardId);
|
|
1683
|
+
}
|
|
1684
|
+
function settleForRelay(creditDb, escrowId, recipientOwner) {
|
|
1685
|
+
settleEscrow(creditDb, escrowId, recipientOwner);
|
|
1686
|
+
}
|
|
1687
|
+
function calculateConductorFee(totalSubTaskCost) {
|
|
1688
|
+
if (totalSubTaskCost <= 0) return 0;
|
|
1689
|
+
const fee = Math.ceil(totalSubTaskCost * 0.1);
|
|
1690
|
+
return Math.max(1, Math.min(20, fee));
|
|
1691
|
+
}
|
|
1692
|
+
function releaseForRelay(creditDb, escrowId) {
|
|
1693
|
+
if (escrowId === void 0) return;
|
|
1694
|
+
releaseEscrow(creditDb, escrowId);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// src/hub-agent/relay-bridge.ts
|
|
1698
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
1699
|
+
|
|
1700
|
+
// src/hub-agent/job-queue.ts
|
|
1749
1701
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
1702
|
+
function initJobQueue(db) {
|
|
1703
|
+
db.exec(`
|
|
1704
|
+
CREATE TABLE IF NOT EXISTS hub_agent_jobs (
|
|
1705
|
+
id TEXT PRIMARY KEY,
|
|
1706
|
+
hub_agent_id TEXT NOT NULL,
|
|
1707
|
+
skill_id TEXT NOT NULL,
|
|
1708
|
+
requester_owner TEXT NOT NULL,
|
|
1709
|
+
params TEXT,
|
|
1710
|
+
status TEXT NOT NULL DEFAULT 'queued',
|
|
1711
|
+
result TEXT,
|
|
1712
|
+
escrow_id TEXT,
|
|
1713
|
+
relay_owner TEXT,
|
|
1714
|
+
created_at TEXT NOT NULL,
|
|
1715
|
+
updated_at TEXT NOT NULL
|
|
1716
|
+
)
|
|
1717
|
+
`);
|
|
1718
|
+
}
|
|
1719
|
+
function insertJob(db, input) {
|
|
1720
|
+
const id = randomUUID4();
|
|
1721
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1722
|
+
const paramsJson = JSON.stringify(input.params);
|
|
1723
|
+
db.prepare(`
|
|
1724
|
+
INSERT INTO hub_agent_jobs (id, hub_agent_id, skill_id, requester_owner, params, status, escrow_id, relay_owner, created_at, updated_at)
|
|
1725
|
+
VALUES (?, ?, ?, ?, ?, 'queued', ?, ?, ?, ?)
|
|
1726
|
+
`).run(
|
|
1727
|
+
id,
|
|
1728
|
+
input.hub_agent_id,
|
|
1729
|
+
input.skill_id,
|
|
1730
|
+
input.requester_owner,
|
|
1731
|
+
paramsJson,
|
|
1732
|
+
input.escrow_id ?? null,
|
|
1733
|
+
input.relay_owner ?? null,
|
|
1734
|
+
now,
|
|
1735
|
+
now
|
|
1736
|
+
);
|
|
1737
|
+
return {
|
|
1738
|
+
id,
|
|
1739
|
+
hub_agent_id: input.hub_agent_id,
|
|
1740
|
+
skill_id: input.skill_id,
|
|
1741
|
+
requester_owner: input.requester_owner,
|
|
1742
|
+
params: paramsJson,
|
|
1743
|
+
status: "queued",
|
|
1744
|
+
result: null,
|
|
1745
|
+
escrow_id: input.escrow_id ?? null,
|
|
1746
|
+
relay_owner: input.relay_owner ?? null,
|
|
1747
|
+
created_at: now,
|
|
1748
|
+
updated_at: now
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
function getJob(db, jobId) {
|
|
1752
|
+
const row = db.prepare("SELECT * FROM hub_agent_jobs WHERE id = ?").get(jobId);
|
|
1753
|
+
return row ?? null;
|
|
1754
|
+
}
|
|
1755
|
+
function listJobs(db, hubAgentId, status) {
|
|
1756
|
+
if (status) {
|
|
1757
|
+
return db.prepare(
|
|
1758
|
+
"SELECT * FROM hub_agent_jobs WHERE hub_agent_id = ? AND status = ? ORDER BY created_at DESC"
|
|
1759
|
+
).all(hubAgentId, status);
|
|
1760
|
+
}
|
|
1761
|
+
return db.prepare(
|
|
1762
|
+
"SELECT * FROM hub_agent_jobs WHERE hub_agent_id = ? ORDER BY created_at DESC"
|
|
1763
|
+
).all(hubAgentId);
|
|
1764
|
+
}
|
|
1765
|
+
function updateJobStatus(db, jobId, status, result) {
|
|
1766
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1767
|
+
if (result !== void 0) {
|
|
1768
|
+
db.prepare("UPDATE hub_agent_jobs SET status = ?, result = ?, updated_at = ? WHERE id = ?").run(status, result, now, jobId);
|
|
1769
|
+
} else {
|
|
1770
|
+
db.prepare("UPDATE hub_agent_jobs SET status = ?, updated_at = ? WHERE id = ?").run(status, now, jobId);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
function getJobsByRelayOwner(db, relayOwner) {
|
|
1774
|
+
return db.prepare(
|
|
1775
|
+
"SELECT * FROM hub_agent_jobs WHERE relay_owner = ? AND status = ? ORDER BY created_at ASC"
|
|
1776
|
+
).all(relayOwner, "queued");
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// src/hub-agent/crypto.ts
|
|
1780
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
1781
|
+
function getMasterKey() {
|
|
1782
|
+
const hex = process.env.HUB_MASTER_KEY;
|
|
1783
|
+
if (!hex || hex.length !== 64) {
|
|
1784
|
+
throw new AgentBnBError(
|
|
1785
|
+
"HUB_MASTER_KEY must be a 64-character hex string (32 bytes)",
|
|
1786
|
+
"MISSING_MASTER_KEY"
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
return Buffer.from(hex, "hex");
|
|
1790
|
+
}
|
|
1791
|
+
function encrypt(plaintext, masterKey) {
|
|
1792
|
+
const iv = randomBytes(12);
|
|
1793
|
+
const cipher = createCipheriv("aes-256-gcm", masterKey, iv);
|
|
1794
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
1795
|
+
const authTag = cipher.getAuthTag();
|
|
1796
|
+
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
|
|
1797
|
+
}
|
|
1798
|
+
function decrypt(encrypted, masterKey) {
|
|
1799
|
+
const [ivHex, authTagHex, ciphertextHex] = encrypted.split(":");
|
|
1800
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
1801
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
1802
|
+
const ciphertext = Buffer.from(ciphertextHex, "hex");
|
|
1803
|
+
const decipher = createDecipheriv("aes-256-gcm", masterKey, iv);
|
|
1804
|
+
decipher.setAuthTag(authTag);
|
|
1805
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
1806
|
+
return decrypted.toString("utf8");
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// src/hub-agent/store.ts
|
|
1810
|
+
function initHubAgentTable(db) {
|
|
1811
|
+
db.exec(`
|
|
1812
|
+
CREATE TABLE IF NOT EXISTS hub_agents (
|
|
1813
|
+
agent_id TEXT PRIMARY KEY,
|
|
1814
|
+
name TEXT NOT NULL,
|
|
1815
|
+
owner_public_key TEXT NOT NULL,
|
|
1816
|
+
public_key TEXT NOT NULL,
|
|
1817
|
+
private_key_enc TEXT NOT NULL,
|
|
1818
|
+
secrets_enc TEXT,
|
|
1819
|
+
skill_routes TEXT NOT NULL DEFAULT '[]',
|
|
1820
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
1821
|
+
created_at TEXT NOT NULL,
|
|
1822
|
+
updated_at TEXT NOT NULL
|
|
1823
|
+
)
|
|
1824
|
+
`);
|
|
1825
|
+
}
|
|
1826
|
+
function createHubAgent(db, req, ownerPublicKey) {
|
|
1827
|
+
const masterKey = getMasterKey();
|
|
1828
|
+
const keys = generateKeyPair();
|
|
1829
|
+
const publicKeyHex = keys.publicKey.toString("hex");
|
|
1830
|
+
const agentId = deriveAgentId(publicKeyHex);
|
|
1831
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1832
|
+
const privateKeyEnc = encrypt(keys.privateKey.toString("hex"), masterKey);
|
|
1833
|
+
const secretsEnc = req.secrets ? encrypt(JSON.stringify(req.secrets), masterKey) : null;
|
|
1834
|
+
db.prepare(`
|
|
1835
|
+
INSERT INTO hub_agents (agent_id, name, owner_public_key, public_key, private_key_enc, secrets_enc, skill_routes, status, created_at, updated_at)
|
|
1836
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'active', ?, ?)
|
|
1837
|
+
`).run(
|
|
1838
|
+
agentId,
|
|
1839
|
+
req.name,
|
|
1840
|
+
ownerPublicKey,
|
|
1841
|
+
publicKeyHex,
|
|
1842
|
+
privateKeyEnc,
|
|
1843
|
+
secretsEnc,
|
|
1844
|
+
JSON.stringify(req.skill_routes),
|
|
1845
|
+
now,
|
|
1846
|
+
now
|
|
1847
|
+
);
|
|
1848
|
+
return {
|
|
1849
|
+
agent_id: agentId,
|
|
1850
|
+
name: req.name,
|
|
1851
|
+
owner_public_key: ownerPublicKey,
|
|
1852
|
+
public_key: publicKeyHex,
|
|
1853
|
+
skill_routes: req.skill_routes,
|
|
1854
|
+
status: "active",
|
|
1855
|
+
created_at: now,
|
|
1856
|
+
updated_at: now
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
function getHubAgent(db, agentId) {
|
|
1860
|
+
const row = db.prepare("SELECT * FROM hub_agents WHERE agent_id = ?").get(agentId);
|
|
1861
|
+
if (!row) return null;
|
|
1862
|
+
const masterKey = getMasterKey();
|
|
1863
|
+
const secrets = row.secrets_enc ? JSON.parse(decrypt(row.secrets_enc, masterKey)) : void 0;
|
|
1864
|
+
return {
|
|
1865
|
+
agent_id: row.agent_id,
|
|
1866
|
+
name: row.name,
|
|
1867
|
+
owner_public_key: row.owner_public_key,
|
|
1868
|
+
public_key: row.public_key,
|
|
1869
|
+
skill_routes: JSON.parse(row.skill_routes),
|
|
1870
|
+
status: row.status,
|
|
1871
|
+
created_at: row.created_at,
|
|
1872
|
+
updated_at: row.updated_at,
|
|
1873
|
+
...secrets ? { secrets } : {}
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
function listHubAgents(db) {
|
|
1877
|
+
const rows = db.prepare("SELECT agent_id, name, owner_public_key, public_key, skill_routes, status, created_at, updated_at FROM hub_agents ORDER BY created_at DESC").all();
|
|
1878
|
+
return rows.map((row) => ({
|
|
1879
|
+
agent_id: row.agent_id,
|
|
1880
|
+
name: row.name,
|
|
1881
|
+
owner_public_key: row.owner_public_key,
|
|
1882
|
+
public_key: row.public_key,
|
|
1883
|
+
skill_routes: JSON.parse(row.skill_routes),
|
|
1884
|
+
status: row.status,
|
|
1885
|
+
created_at: row.created_at,
|
|
1886
|
+
updated_at: row.updated_at
|
|
1887
|
+
}));
|
|
1888
|
+
}
|
|
1889
|
+
function updateHubAgent(db, agentId, updates) {
|
|
1890
|
+
const existing = db.prepare("SELECT * FROM hub_agents WHERE agent_id = ?").get(agentId);
|
|
1891
|
+
if (!existing) return null;
|
|
1892
|
+
const masterKey = getMasterKey();
|
|
1893
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1894
|
+
const newName = updates.name ?? existing.name;
|
|
1895
|
+
const newSkillRoutes = updates.skill_routes ? JSON.stringify(updates.skill_routes) : existing.skill_routes;
|
|
1896
|
+
const newSecretsEnc = updates.secrets !== void 0 ? encrypt(JSON.stringify(updates.secrets), masterKey) : existing.secrets_enc;
|
|
1897
|
+
db.prepare(`
|
|
1898
|
+
UPDATE hub_agents SET name = ?, skill_routes = ?, secrets_enc = ?, updated_at = ?
|
|
1899
|
+
WHERE agent_id = ?
|
|
1900
|
+
`).run(newName, newSkillRoutes, newSecretsEnc, now, agentId);
|
|
1901
|
+
const secrets = newSecretsEnc ? JSON.parse(decrypt(newSecretsEnc, masterKey)) : void 0;
|
|
1902
|
+
return {
|
|
1903
|
+
agent_id: existing.agent_id,
|
|
1904
|
+
name: newName,
|
|
1905
|
+
owner_public_key: existing.owner_public_key,
|
|
1906
|
+
public_key: existing.public_key,
|
|
1907
|
+
skill_routes: JSON.parse(newSkillRoutes),
|
|
1908
|
+
status: existing.status,
|
|
1909
|
+
created_at: existing.created_at,
|
|
1910
|
+
updated_at: now,
|
|
1911
|
+
...secrets ? { secrets } : {}
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
function deleteHubAgent(db, agentId) {
|
|
1915
|
+
const result = db.prepare("DELETE FROM hub_agents WHERE agent_id = ?").run(agentId);
|
|
1916
|
+
return result.changes > 0;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// src/hub-agent/relay-bridge.ts
|
|
1920
|
+
var JOB_DISPATCH_TIMEOUT_MS = 3e5;
|
|
1921
|
+
function createRelayBridge(opts) {
|
|
1922
|
+
const { registryDb, creditDb, sendMessage, pendingRequests, connections } = opts;
|
|
1923
|
+
function onAgentOnline(owner) {
|
|
1924
|
+
const jobs = getJobsByRelayOwner(registryDb, owner);
|
|
1925
|
+
if (jobs.length === 0) return;
|
|
1926
|
+
const targetWs = connections.get(owner);
|
|
1927
|
+
if (!targetWs) return;
|
|
1928
|
+
for (const job of jobs) {
|
|
1929
|
+
updateJobStatus(registryDb, job.id, "dispatched");
|
|
1930
|
+
const agent = getHubAgent(registryDb, job.hub_agent_id);
|
|
1931
|
+
const cardId = agent ? agent.agent_id.padEnd(32, "0").replace(/^(.{8})(.{4})(.{4})(.{4})(.{12}).*$/, "$1-$2-$3-$4-$5") : job.hub_agent_id;
|
|
1932
|
+
const requestId = randomUUID5();
|
|
1933
|
+
let params = {};
|
|
1934
|
+
try {
|
|
1935
|
+
params = JSON.parse(job.params);
|
|
1936
|
+
} catch {
|
|
1937
|
+
}
|
|
1938
|
+
const timeout = setTimeout(() => {
|
|
1939
|
+
const pending = pendingRequests.get(requestId);
|
|
1940
|
+
if (pending) {
|
|
1941
|
+
pendingRequests.delete(requestId);
|
|
1942
|
+
updateJobStatus(registryDb, job.id, "failed", JSON.stringify({ error: "dispatch timeout" }));
|
|
1943
|
+
if (job.escrow_id) {
|
|
1944
|
+
try {
|
|
1945
|
+
releaseForRelay(creditDb, job.escrow_id);
|
|
1946
|
+
} catch (e) {
|
|
1947
|
+
console.error("[relay-bridge] escrow release on timeout failed:", e);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
}, JOB_DISPATCH_TIMEOUT_MS);
|
|
1952
|
+
pendingRequests.set(requestId, {
|
|
1953
|
+
originOwner: job.requester_owner,
|
|
1954
|
+
timeout,
|
|
1955
|
+
escrowId: job.escrow_id ?? void 0,
|
|
1956
|
+
targetOwner: owner,
|
|
1957
|
+
jobId: job.id
|
|
1958
|
+
});
|
|
1959
|
+
sendMessage(targetWs, {
|
|
1960
|
+
type: "incoming_request",
|
|
1961
|
+
id: requestId,
|
|
1962
|
+
from_owner: job.requester_owner,
|
|
1963
|
+
card_id: cardId,
|
|
1964
|
+
skill_id: job.skill_id,
|
|
1965
|
+
params
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
return { onAgentOnline };
|
|
1970
|
+
}
|
|
1971
|
+
function handleJobRelayResponse(opts) {
|
|
1972
|
+
const { registryDb, creditDb, jobId, escrowId, relayOwner, result, error } = opts;
|
|
1973
|
+
if (error) {
|
|
1974
|
+
updateJobStatus(registryDb, jobId, "failed", JSON.stringify(error));
|
|
1975
|
+
if (escrowId) {
|
|
1976
|
+
try {
|
|
1977
|
+
releaseForRelay(creditDb, escrowId);
|
|
1978
|
+
} catch (e) {
|
|
1979
|
+
console.error("[relay-bridge] escrow release on error failed:", e);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
} else {
|
|
1983
|
+
updateJobStatus(registryDb, jobId, "completed", JSON.stringify(result));
|
|
1984
|
+
if (escrowId) {
|
|
1985
|
+
try {
|
|
1986
|
+
settleForRelay(creditDb, escrowId, relayOwner);
|
|
1987
|
+
} catch (e) {
|
|
1988
|
+
console.error("[relay-bridge] escrow settle failed:", e);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// src/relay/websocket-relay.ts
|
|
1750
1995
|
var RATE_LIMIT_MAX = 60;
|
|
1751
1996
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
1752
|
-
var RELAY_TIMEOUT_MS =
|
|
1753
|
-
function registerWebSocketRelay(server, db) {
|
|
1997
|
+
var RELAY_TIMEOUT_MS = 3e5;
|
|
1998
|
+
function registerWebSocketRelay(server, db, creditDb) {
|
|
1754
1999
|
const connections = /* @__PURE__ */ new Map();
|
|
1755
2000
|
const pendingRequests = /* @__PURE__ */ new Map();
|
|
1756
2001
|
const rateLimits = /* @__PURE__ */ new Map();
|
|
2002
|
+
let onAgentOnlineCallback;
|
|
1757
2003
|
function checkRateLimit(owner) {
|
|
1758
2004
|
const now = Date.now();
|
|
1759
2005
|
const entry = rateLimits.get(owner);
|
|
@@ -1804,21 +2050,32 @@ function registerWebSocketRelay(server, db) {
|
|
|
1804
2050
|
}
|
|
1805
2051
|
}
|
|
1806
2052
|
function upsertCard(cardData, owner) {
|
|
1807
|
-
const
|
|
1808
|
-
|
|
2053
|
+
const parsed = AnyCardSchema.safeParse(cardData);
|
|
2054
|
+
if (!parsed.success) {
|
|
2055
|
+
throw new AgentBnBError(
|
|
2056
|
+
`Card validation failed: ${parsed.error.message}`,
|
|
2057
|
+
"VALIDATION_ERROR"
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
const card = { ...parsed.data, availability: { ...parsed.data.availability, online: true } };
|
|
2061
|
+
const cardId = card.id;
|
|
2062
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2063
|
+
const existing = db.prepare("SELECT id FROM capability_cards WHERE id = ?").get(cardId);
|
|
1809
2064
|
if (existing) {
|
|
1810
|
-
|
|
1811
|
-
|
|
2065
|
+
db.prepare(
|
|
2066
|
+
"UPDATE capability_cards SET data = ?, updated_at = ? WHERE id = ?"
|
|
2067
|
+
).run(JSON.stringify(card), now, cardId);
|
|
1812
2068
|
} else {
|
|
1813
|
-
|
|
1814
|
-
|
|
2069
|
+
db.prepare(
|
|
2070
|
+
"INSERT INTO capability_cards (id, owner, data, created_at, updated_at) VALUES (?, ?, ?, ?, ?)"
|
|
2071
|
+
).run(cardId, owner, JSON.stringify(card), now, now);
|
|
1815
2072
|
}
|
|
1816
2073
|
return cardId;
|
|
1817
2074
|
}
|
|
1818
2075
|
function logAgentJoined(owner, cardName, cardId) {
|
|
1819
2076
|
try {
|
|
1820
2077
|
insertRequestLog(db, {
|
|
1821
|
-
id:
|
|
2078
|
+
id: randomUUID6(),
|
|
1822
2079
|
card_id: cardId,
|
|
1823
2080
|
card_name: cardName,
|
|
1824
2081
|
requester: owner,
|
|
@@ -1849,10 +2106,25 @@ function registerWebSocketRelay(server, db) {
|
|
|
1849
2106
|
const cardId = upsertCard(card, owner);
|
|
1850
2107
|
const cardName = card.name ?? card.agent_name ?? owner;
|
|
1851
2108
|
logAgentJoined(owner, cardName, cardId);
|
|
2109
|
+
if (msg.cards && msg.cards.length > 0) {
|
|
2110
|
+
for (const extraCard of msg.cards) {
|
|
2111
|
+
try {
|
|
2112
|
+
upsertCard(extraCard, owner);
|
|
2113
|
+
} catch {
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
1852
2117
|
markOwnerOnline(owner);
|
|
2118
|
+
if (onAgentOnlineCallback) {
|
|
2119
|
+
try {
|
|
2120
|
+
onAgentOnlineCallback(owner);
|
|
2121
|
+
} catch (e) {
|
|
2122
|
+
console.error("[relay] onAgentOnline callback error:", e);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
1853
2125
|
sendMessage(ws, { type: "registered", agent_id: cardId });
|
|
1854
2126
|
}
|
|
1855
|
-
function handleRelayRequest(ws, msg, fromOwner) {
|
|
2127
|
+
async function handleRelayRequest(ws, msg, fromOwner) {
|
|
1856
2128
|
if (!checkRateLimit(fromOwner)) {
|
|
1857
2129
|
sendMessage(ws, {
|
|
1858
2130
|
type: "error",
|
|
@@ -1871,15 +2143,42 @@ function registerWebSocketRelay(server, db) {
|
|
|
1871
2143
|
});
|
|
1872
2144
|
return;
|
|
1873
2145
|
}
|
|
2146
|
+
let escrowId;
|
|
2147
|
+
if (creditDb) {
|
|
2148
|
+
try {
|
|
2149
|
+
const price = lookupCardPrice(db, msg.card_id, msg.skill_id);
|
|
2150
|
+
if (price !== null && price > 0) {
|
|
2151
|
+
escrowId = holdForRelay(creditDb, fromOwner, price, msg.card_id);
|
|
2152
|
+
}
|
|
2153
|
+
} catch (err) {
|
|
2154
|
+
if (err instanceof AgentBnBError && err.code === "INSUFFICIENT_CREDITS") {
|
|
2155
|
+
sendMessage(ws, {
|
|
2156
|
+
type: "response",
|
|
2157
|
+
id: msg.id,
|
|
2158
|
+
error: { code: -32603, message: "Insufficient credits" }
|
|
2159
|
+
});
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
console.error("[relay] credit hold error (non-fatal):", err);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
1874
2165
|
const timeout = setTimeout(() => {
|
|
2166
|
+
const pending = pendingRequests.get(msg.id);
|
|
1875
2167
|
pendingRequests.delete(msg.id);
|
|
2168
|
+
if (pending?.escrowId && creditDb) {
|
|
2169
|
+
try {
|
|
2170
|
+
releaseForRelay(creditDb, pending.escrowId);
|
|
2171
|
+
} catch (e) {
|
|
2172
|
+
console.error("[relay] escrow release on timeout failed:", e);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
1876
2175
|
sendMessage(ws, {
|
|
1877
2176
|
type: "response",
|
|
1878
2177
|
id: msg.id,
|
|
1879
2178
|
error: { code: -32603, message: "Relay request timeout" }
|
|
1880
2179
|
});
|
|
1881
2180
|
}, RELAY_TIMEOUT_MS);
|
|
1882
|
-
pendingRequests.set(msg.id, { originOwner: fromOwner, timeout });
|
|
2181
|
+
pendingRequests.set(msg.id, { originOwner: fromOwner, timeout, escrowId, targetOwner: msg.target_owner });
|
|
1883
2182
|
sendMessage(targetWs, {
|
|
1884
2183
|
type: "incoming_request",
|
|
1885
2184
|
id: msg.id,
|
|
@@ -1891,18 +2190,98 @@ function registerWebSocketRelay(server, db) {
|
|
|
1891
2190
|
escrow_receipt: msg.escrow_receipt
|
|
1892
2191
|
});
|
|
1893
2192
|
}
|
|
2193
|
+
function handleRelayProgress(msg) {
|
|
2194
|
+
const pending = pendingRequests.get(msg.id);
|
|
2195
|
+
if (!pending) return;
|
|
2196
|
+
clearTimeout(pending.timeout);
|
|
2197
|
+
const newTimeout = setTimeout(() => {
|
|
2198
|
+
const p = pendingRequests.get(msg.id);
|
|
2199
|
+
pendingRequests.delete(msg.id);
|
|
2200
|
+
if (p?.escrowId && creditDb) {
|
|
2201
|
+
try {
|
|
2202
|
+
releaseForRelay(creditDb, p.escrowId);
|
|
2203
|
+
} catch (e) {
|
|
2204
|
+
console.error("[relay] escrow release on progress timeout failed:", e);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
const originWs2 = connections.get(pending.originOwner);
|
|
2208
|
+
if (originWs2 && originWs2.readyState === 1) {
|
|
2209
|
+
sendMessage(originWs2, {
|
|
2210
|
+
type: "response",
|
|
2211
|
+
id: msg.id,
|
|
2212
|
+
error: { code: -32603, message: "Relay request timeout" }
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
}, RELAY_TIMEOUT_MS);
|
|
2216
|
+
pending.timeout = newTimeout;
|
|
2217
|
+
const originWs = connections.get(pending.originOwner);
|
|
2218
|
+
if (originWs && originWs.readyState === 1) {
|
|
2219
|
+
sendMessage(originWs, {
|
|
2220
|
+
type: "relay_progress",
|
|
2221
|
+
id: msg.id,
|
|
2222
|
+
progress: msg.progress,
|
|
2223
|
+
message: msg.message
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
1894
2227
|
function handleRelayResponse(msg) {
|
|
1895
2228
|
const pending = pendingRequests.get(msg.id);
|
|
1896
2229
|
if (!pending) return;
|
|
1897
2230
|
clearTimeout(pending.timeout);
|
|
1898
2231
|
pendingRequests.delete(msg.id);
|
|
2232
|
+
if (pending.jobId && creditDb) {
|
|
2233
|
+
try {
|
|
2234
|
+
handleJobRelayResponse({
|
|
2235
|
+
registryDb: db,
|
|
2236
|
+
creditDb,
|
|
2237
|
+
jobId: pending.jobId,
|
|
2238
|
+
escrowId: pending.escrowId,
|
|
2239
|
+
relayOwner: pending.targetOwner ?? "",
|
|
2240
|
+
result: msg.error === void 0 ? msg.result : void 0,
|
|
2241
|
+
error: msg.error
|
|
2242
|
+
});
|
|
2243
|
+
} catch (e) {
|
|
2244
|
+
console.error("[relay] job relay response handling failed:", e);
|
|
2245
|
+
}
|
|
2246
|
+
const originWs2 = connections.get(pending.originOwner);
|
|
2247
|
+
if (originWs2 && originWs2.readyState === 1) {
|
|
2248
|
+
sendMessage(originWs2, { type: "response", id: msg.id, result: msg.result, error: msg.error });
|
|
2249
|
+
}
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
if (pending.escrowId && creditDb) {
|
|
2253
|
+
try {
|
|
2254
|
+
if (msg.error === void 0) {
|
|
2255
|
+
settleForRelay(creditDb, pending.escrowId, pending.targetOwner);
|
|
2256
|
+
} else {
|
|
2257
|
+
releaseForRelay(creditDb, pending.escrowId);
|
|
2258
|
+
}
|
|
2259
|
+
} catch (e) {
|
|
2260
|
+
console.error("[relay] escrow settle/release on response failed:", e);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
let conductorFee = 0;
|
|
2264
|
+
if (creditDb && msg.error === void 0 && typeof msg.result === "object" && msg.result !== null && "total_credits" in msg.result && typeof msg.result.total_credits === "number") {
|
|
2265
|
+
const totalCredits = msg.result.total_credits;
|
|
2266
|
+
conductorFee = calculateConductorFee(totalCredits);
|
|
2267
|
+
if (conductorFee > 0) {
|
|
2268
|
+
try {
|
|
2269
|
+
const feeEscrowId = holdForRelay(creditDb, pending.originOwner, conductorFee, msg.id);
|
|
2270
|
+
settleForRelay(creditDb, feeEscrowId, pending.targetOwner);
|
|
2271
|
+
} catch (e) {
|
|
2272
|
+
console.error("[relay] conductor fee settlement failed (non-fatal):", e);
|
|
2273
|
+
conductorFee = 0;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
1899
2277
|
const originWs = connections.get(pending.originOwner);
|
|
1900
2278
|
if (originWs && originWs.readyState === 1) {
|
|
1901
2279
|
sendMessage(originWs, {
|
|
1902
2280
|
type: "response",
|
|
1903
2281
|
id: msg.id,
|
|
1904
2282
|
result: msg.result,
|
|
1905
|
-
error: msg.error
|
|
2283
|
+
error: msg.error,
|
|
2284
|
+
...conductorFee > 0 ? { conductor_fee: conductorFee } : {}
|
|
1906
2285
|
});
|
|
1907
2286
|
}
|
|
1908
2287
|
}
|
|
@@ -1912,9 +2291,34 @@ function registerWebSocketRelay(server, db) {
|
|
|
1912
2291
|
rateLimits.delete(owner);
|
|
1913
2292
|
markOwnerOffline(owner);
|
|
1914
2293
|
for (const [reqId, pending] of pendingRequests) {
|
|
1915
|
-
if (pending.
|
|
2294
|
+
if (pending.targetOwner === owner) {
|
|
2295
|
+
clearTimeout(pending.timeout);
|
|
2296
|
+
pendingRequests.delete(reqId);
|
|
2297
|
+
if (pending.escrowId && creditDb) {
|
|
2298
|
+
try {
|
|
2299
|
+
releaseForRelay(creditDb, pending.escrowId);
|
|
2300
|
+
} catch (e) {
|
|
2301
|
+
console.error("[relay] escrow release on disconnect failed:", e);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
const originWs = connections.get(pending.originOwner);
|
|
2305
|
+
if (originWs && originWs.readyState === 1) {
|
|
2306
|
+
sendMessage(originWs, {
|
|
2307
|
+
type: "response",
|
|
2308
|
+
id: reqId,
|
|
2309
|
+
error: { code: -32603, message: "Provider disconnected" }
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
} else if (pending.originOwner === owner) {
|
|
1916
2313
|
clearTimeout(pending.timeout);
|
|
1917
2314
|
pendingRequests.delete(reqId);
|
|
2315
|
+
if (pending.escrowId && creditDb) {
|
|
2316
|
+
try {
|
|
2317
|
+
releaseForRelay(creditDb, pending.escrowId);
|
|
2318
|
+
} catch (e) {
|
|
2319
|
+
console.error("[relay] escrow release on requester disconnect failed:", e);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
1918
2322
|
}
|
|
1919
2323
|
}
|
|
1920
2324
|
}
|
|
@@ -1922,45 +2326,50 @@ function registerWebSocketRelay(server, db) {
|
|
|
1922
2326
|
const socket = rawSocket;
|
|
1923
2327
|
let registeredOwner;
|
|
1924
2328
|
socket.on("message", (raw) => {
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2329
|
+
void (async () => {
|
|
2330
|
+
let data;
|
|
2331
|
+
try {
|
|
2332
|
+
data = JSON.parse(typeof raw === "string" ? raw : raw.toString("utf-8"));
|
|
2333
|
+
} catch {
|
|
2334
|
+
sendMessage(socket, { type: "error", code: "invalid_json", message: "Invalid JSON" });
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
const parsed = RelayMessageSchema.safeParse(data);
|
|
2338
|
+
if (!parsed.success) {
|
|
2339
|
+
sendMessage(socket, {
|
|
2340
|
+
type: "error",
|
|
2341
|
+
code: "invalid_message",
|
|
2342
|
+
message: `Invalid message: ${parsed.error.issues[0]?.message ?? "unknown error"}`
|
|
2343
|
+
});
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
const msg = parsed.data;
|
|
2347
|
+
switch (msg.type) {
|
|
2348
|
+
case "register":
|
|
2349
|
+
registeredOwner = msg.owner;
|
|
2350
|
+
handleRegister(socket, msg);
|
|
2351
|
+
break;
|
|
2352
|
+
case "relay_request":
|
|
2353
|
+
if (!registeredOwner) {
|
|
2354
|
+
sendMessage(socket, {
|
|
2355
|
+
type: "error",
|
|
2356
|
+
code: "not_registered",
|
|
2357
|
+
message: "Must send register message before relay requests"
|
|
2358
|
+
});
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
await handleRelayRequest(socket, msg, registeredOwner);
|
|
2362
|
+
break;
|
|
2363
|
+
case "relay_response":
|
|
2364
|
+
handleRelayResponse(msg);
|
|
2365
|
+
break;
|
|
2366
|
+
case "relay_progress":
|
|
2367
|
+
handleRelayProgress(msg);
|
|
2368
|
+
break;
|
|
2369
|
+
default:
|
|
2370
|
+
break;
|
|
2371
|
+
}
|
|
2372
|
+
})();
|
|
1964
2373
|
});
|
|
1965
2374
|
socket.on("close", () => {
|
|
1966
2375
|
handleDisconnect(registeredOwner);
|
|
@@ -1985,21 +2394,29 @@ function registerWebSocketRelay(server, db) {
|
|
|
1985
2394
|
}
|
|
1986
2395
|
pendingRequests.clear();
|
|
1987
2396
|
rateLimits.clear();
|
|
2397
|
+
},
|
|
2398
|
+
setOnAgentOnline: (cb) => {
|
|
2399
|
+
onAgentOnlineCallback = cb;
|
|
2400
|
+
},
|
|
2401
|
+
getConnections: () => connections,
|
|
2402
|
+
getPendingRequests: () => pendingRequests,
|
|
2403
|
+
sendMessage: (ws, msg) => {
|
|
2404
|
+
sendMessage(ws, msg);
|
|
1988
2405
|
}
|
|
1989
2406
|
};
|
|
1990
2407
|
}
|
|
1991
2408
|
|
|
1992
2409
|
// src/identity/guarantor.ts
|
|
1993
|
-
import { z as
|
|
1994
|
-
import { randomUUID as
|
|
2410
|
+
import { z as z3 } from "zod";
|
|
2411
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
1995
2412
|
var MAX_AGENTS_PER_GUARANTOR = 10;
|
|
1996
2413
|
var GUARANTOR_CREDIT_POOL = 50;
|
|
1997
|
-
var GuarantorRecordSchema =
|
|
1998
|
-
id:
|
|
1999
|
-
github_login:
|
|
2000
|
-
agent_count:
|
|
2001
|
-
credit_pool:
|
|
2002
|
-
created_at:
|
|
2414
|
+
var GuarantorRecordSchema = z3.object({
|
|
2415
|
+
id: z3.string().uuid(),
|
|
2416
|
+
github_login: z3.string().min(1),
|
|
2417
|
+
agent_count: z3.number().int().nonnegative(),
|
|
2418
|
+
credit_pool: z3.number().int().nonnegative(),
|
|
2419
|
+
created_at: z3.string().datetime()
|
|
2003
2420
|
});
|
|
2004
2421
|
var GUARANTOR_SCHEMA = `
|
|
2005
2422
|
CREATE TABLE IF NOT EXISTS guarantors (
|
|
@@ -2030,7 +2447,7 @@ function registerGuarantor(db, githubLogin) {
|
|
|
2030
2447
|
);
|
|
2031
2448
|
}
|
|
2032
2449
|
const record = {
|
|
2033
|
-
id:
|
|
2450
|
+
id: randomUUID7(),
|
|
2034
2451
|
github_login: githubLogin,
|
|
2035
2452
|
agent_count: 0,
|
|
2036
2453
|
credit_pool: GUARANTOR_CREDIT_POOL,
|
|
@@ -2104,10 +2521,849 @@ function getAgentGuarantor(db, agentId) {
|
|
|
2104
2521
|
function initiateGithubAuth() {
|
|
2105
2522
|
return {
|
|
2106
2523
|
auth_url: "https://github.com/login/oauth/authorize?client_id=PLACEHOLDER&scope=read:user",
|
|
2107
|
-
state:
|
|
2524
|
+
state: randomUUID7()
|
|
2108
2525
|
};
|
|
2109
2526
|
}
|
|
2110
2527
|
|
|
2528
|
+
// src/registry/free-tier.ts
|
|
2529
|
+
function initFreeTierTable(db) {
|
|
2530
|
+
db.exec(`
|
|
2531
|
+
CREATE TABLE IF NOT EXISTS credit_free_tier_usage (
|
|
2532
|
+
agent_public_key TEXT NOT NULL,
|
|
2533
|
+
skill_id TEXT NOT NULL,
|
|
2534
|
+
usage_count INTEGER NOT NULL DEFAULT 0,
|
|
2535
|
+
last_used_at TEXT NOT NULL,
|
|
2536
|
+
PRIMARY KEY (agent_public_key, skill_id)
|
|
2537
|
+
)
|
|
2538
|
+
`);
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
// src/registry/credit-routes.ts
|
|
2542
|
+
async function creditRoutesPlugin(fastify, options) {
|
|
2543
|
+
const { creditDb } = options;
|
|
2544
|
+
creditDb.exec(`
|
|
2545
|
+
CREATE TABLE IF NOT EXISTS credit_grants (
|
|
2546
|
+
public_key TEXT PRIMARY KEY,
|
|
2547
|
+
granted_at TEXT NOT NULL
|
|
2548
|
+
)
|
|
2549
|
+
`);
|
|
2550
|
+
initFreeTierTable(creditDb);
|
|
2551
|
+
await fastify.register(async (scope) => {
|
|
2552
|
+
identityAuthPlugin(scope);
|
|
2553
|
+
scope.post("/api/credits/hold", {
|
|
2554
|
+
schema: {
|
|
2555
|
+
tags: ["credits"],
|
|
2556
|
+
summary: "Hold credits in escrow during capability execution",
|
|
2557
|
+
security: [{ ed25519Auth: [] }],
|
|
2558
|
+
body: {
|
|
2559
|
+
type: "object",
|
|
2560
|
+
properties: {
|
|
2561
|
+
owner: { type: "string" },
|
|
2562
|
+
amount: { type: "number" },
|
|
2563
|
+
cardId: { type: "string" }
|
|
2564
|
+
},
|
|
2565
|
+
required: ["owner", "amount", "cardId"]
|
|
2566
|
+
},
|
|
2567
|
+
response: {
|
|
2568
|
+
200: { type: "object", properties: { escrowId: { type: "string" } } },
|
|
2569
|
+
400: { type: "object", properties: { error: { type: "string" }, code: { type: "string" } } }
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
}, async (request, reply) => {
|
|
2573
|
+
const body = request.body;
|
|
2574
|
+
const owner = typeof body.owner === "string" ? body.owner.trim() : "";
|
|
2575
|
+
const amount = typeof body.amount === "number" ? body.amount : NaN;
|
|
2576
|
+
const cardId = typeof body.cardId === "string" ? body.cardId.trim() : "";
|
|
2577
|
+
if (!owner || isNaN(amount) || amount <= 0 || !cardId) {
|
|
2578
|
+
return reply.code(400).send({ error: "Missing or invalid required fields: owner, amount (>0), cardId" });
|
|
2579
|
+
}
|
|
2580
|
+
try {
|
|
2581
|
+
const escrowId = holdEscrow(creditDb, owner, amount, cardId);
|
|
2582
|
+
return reply.send({ escrowId });
|
|
2583
|
+
} catch (err) {
|
|
2584
|
+
if (err instanceof AgentBnBError && err.code === "INSUFFICIENT_CREDITS") {
|
|
2585
|
+
return reply.code(400).send({ error: err.message, code: "INSUFFICIENT_CREDITS" });
|
|
2586
|
+
}
|
|
2587
|
+
throw err;
|
|
2588
|
+
}
|
|
2589
|
+
});
|
|
2590
|
+
scope.post("/api/credits/settle", {
|
|
2591
|
+
schema: {
|
|
2592
|
+
tags: ["credits"],
|
|
2593
|
+
summary: "Transfer held credits to provider on success",
|
|
2594
|
+
security: [{ ed25519Auth: [] }],
|
|
2595
|
+
body: {
|
|
2596
|
+
type: "object",
|
|
2597
|
+
properties: {
|
|
2598
|
+
escrowId: { type: "string" },
|
|
2599
|
+
recipientOwner: { type: "string" }
|
|
2600
|
+
},
|
|
2601
|
+
required: ["escrowId", "recipientOwner"]
|
|
2602
|
+
},
|
|
2603
|
+
response: {
|
|
2604
|
+
200: { type: "object", properties: { ok: { type: "boolean" } } },
|
|
2605
|
+
400: { type: "object", properties: { error: { type: "string" }, code: { type: "string" } } }
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
}, async (request, reply) => {
|
|
2609
|
+
const body = request.body;
|
|
2610
|
+
const escrowId = typeof body.escrowId === "string" ? body.escrowId.trim() : "";
|
|
2611
|
+
const recipientOwner = typeof body.recipientOwner === "string" ? body.recipientOwner.trim() : "";
|
|
2612
|
+
if (!escrowId || !recipientOwner) {
|
|
2613
|
+
return reply.code(400).send({ error: "escrowId and recipientOwner are required" });
|
|
2614
|
+
}
|
|
2615
|
+
try {
|
|
2616
|
+
settleEscrow(creditDb, escrowId, recipientOwner);
|
|
2617
|
+
return reply.send({ ok: true });
|
|
2618
|
+
} catch (err) {
|
|
2619
|
+
if (err instanceof AgentBnBError) {
|
|
2620
|
+
return reply.code(400).send({ error: err.message, code: err.code });
|
|
2621
|
+
}
|
|
2622
|
+
throw err;
|
|
2623
|
+
}
|
|
2624
|
+
});
|
|
2625
|
+
scope.post("/api/credits/release", {
|
|
2626
|
+
schema: {
|
|
2627
|
+
tags: ["credits"],
|
|
2628
|
+
summary: "Refund held credits to requester on failure",
|
|
2629
|
+
security: [{ ed25519Auth: [] }],
|
|
2630
|
+
body: {
|
|
2631
|
+
type: "object",
|
|
2632
|
+
properties: { escrowId: { type: "string" } },
|
|
2633
|
+
required: ["escrowId"]
|
|
2634
|
+
},
|
|
2635
|
+
response: {
|
|
2636
|
+
200: { type: "object", properties: { ok: { type: "boolean" } } },
|
|
2637
|
+
400: { type: "object", properties: { error: { type: "string" }, code: { type: "string" } } }
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
}, async (request, reply) => {
|
|
2641
|
+
const body = request.body;
|
|
2642
|
+
const escrowId = typeof body.escrowId === "string" ? body.escrowId.trim() : "";
|
|
2643
|
+
if (!escrowId) {
|
|
2644
|
+
return reply.code(400).send({ error: "escrowId is required" });
|
|
2645
|
+
}
|
|
2646
|
+
try {
|
|
2647
|
+
releaseEscrow(creditDb, escrowId);
|
|
2648
|
+
return reply.send({ ok: true });
|
|
2649
|
+
} catch (err) {
|
|
2650
|
+
if (err instanceof AgentBnBError) {
|
|
2651
|
+
return reply.code(400).send({ error: err.message, code: err.code });
|
|
2652
|
+
}
|
|
2653
|
+
throw err;
|
|
2654
|
+
}
|
|
2655
|
+
});
|
|
2656
|
+
scope.post("/api/credits/grant", {
|
|
2657
|
+
schema: {
|
|
2658
|
+
tags: ["credits"],
|
|
2659
|
+
summary: "Bootstrap grant of 50 credits (once per identity)",
|
|
2660
|
+
security: [{ ed25519Auth: [] }],
|
|
2661
|
+
body: {
|
|
2662
|
+
type: "object",
|
|
2663
|
+
properties: {
|
|
2664
|
+
owner: { type: "string" },
|
|
2665
|
+
amount: { type: "number" }
|
|
2666
|
+
},
|
|
2667
|
+
required: ["owner"]
|
|
2668
|
+
},
|
|
2669
|
+
response: {
|
|
2670
|
+
200: {
|
|
2671
|
+
type: "object",
|
|
2672
|
+
properties: {
|
|
2673
|
+
ok: { type: "boolean" },
|
|
2674
|
+
granted: { type: "number" },
|
|
2675
|
+
reason: { type: "string" }
|
|
2676
|
+
}
|
|
2677
|
+
},
|
|
2678
|
+
400: { type: "object", properties: { error: { type: "string" } } }
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
}, async (request, reply) => {
|
|
2682
|
+
const body = request.body;
|
|
2683
|
+
const owner = typeof body.owner === "string" ? body.owner.trim() : "";
|
|
2684
|
+
const amount = typeof body.amount === "number" ? body.amount : 50;
|
|
2685
|
+
const publicKey = request.agentPublicKey;
|
|
2686
|
+
if (!owner) {
|
|
2687
|
+
return reply.code(400).send({ error: "owner is required" });
|
|
2688
|
+
}
|
|
2689
|
+
const existing = creditDb.prepare("SELECT public_key FROM credit_grants WHERE public_key = ?").get(publicKey);
|
|
2690
|
+
if (existing) {
|
|
2691
|
+
return reply.send({ ok: true, granted: 0, reason: "already_granted" });
|
|
2692
|
+
}
|
|
2693
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2694
|
+
bootstrapAgent(creditDb, owner, amount);
|
|
2695
|
+
creditDb.prepare("INSERT INTO credit_grants (public_key, granted_at) VALUES (?, ?)").run(publicKey, now);
|
|
2696
|
+
return reply.send({ ok: true, granted: amount });
|
|
2697
|
+
});
|
|
2698
|
+
scope.get("/api/credits/:owner", {
|
|
2699
|
+
schema: {
|
|
2700
|
+
tags: ["credits"],
|
|
2701
|
+
summary: "Get current credit balance for an agent",
|
|
2702
|
+
security: [{ ed25519Auth: [] }],
|
|
2703
|
+
params: { type: "object", properties: { owner: { type: "string" } }, required: ["owner"] },
|
|
2704
|
+
response: { 200: { type: "object", properties: { balance: { type: "number" } } } }
|
|
2705
|
+
}
|
|
2706
|
+
}, async (request, reply) => {
|
|
2707
|
+
const { owner } = request.params;
|
|
2708
|
+
const balance = getBalance(creditDb, owner);
|
|
2709
|
+
return reply.send({ balance });
|
|
2710
|
+
});
|
|
2711
|
+
scope.get("/api/credits/:owner/history", {
|
|
2712
|
+
schema: {
|
|
2713
|
+
tags: ["credits"],
|
|
2714
|
+
summary: "Get paginated transaction history",
|
|
2715
|
+
security: [{ ed25519Auth: [] }],
|
|
2716
|
+
params: { type: "object", properties: { owner: { type: "string" } }, required: ["owner"] },
|
|
2717
|
+
querystring: {
|
|
2718
|
+
type: "object",
|
|
2719
|
+
properties: { limit: { type: "integer", description: "Max entries (default 20, max 100)" } }
|
|
2720
|
+
},
|
|
2721
|
+
response: {
|
|
2722
|
+
200: {
|
|
2723
|
+
type: "object",
|
|
2724
|
+
properties: { transactions: { type: "array" }, limit: { type: "integer" } }
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
}, async (request, reply) => {
|
|
2729
|
+
const { owner } = request.params;
|
|
2730
|
+
const query = request.query;
|
|
2731
|
+
const rawLimit = query.limit !== void 0 ? parseInt(query.limit, 10) : 20;
|
|
2732
|
+
const limit = Math.min(isNaN(rawLimit) || rawLimit < 1 ? 20 : rawLimit, 100);
|
|
2733
|
+
const transactions = getTransactions(creditDb, owner, limit);
|
|
2734
|
+
return reply.send({ transactions, limit });
|
|
2735
|
+
});
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
// src/hub-agent/executor.ts
|
|
2740
|
+
var HubAgentExecutor = class {
|
|
2741
|
+
constructor(registryDb, creditDb) {
|
|
2742
|
+
this.registryDb = registryDb;
|
|
2743
|
+
this.creditDb = creditDb;
|
|
2744
|
+
initJobQueue(this.registryDb);
|
|
2745
|
+
}
|
|
2746
|
+
/**
|
|
2747
|
+
* Execute a skill on a Hub Agent.
|
|
2748
|
+
*
|
|
2749
|
+
* @param agentId - The Hub Agent ID.
|
|
2750
|
+
* @param skillId - The skill_id to execute from the agent's routing table.
|
|
2751
|
+
* @param params - Input parameters for the skill.
|
|
2752
|
+
* @param requesterOwner - Optional requester identifier for credit escrow.
|
|
2753
|
+
* @returns ExecutionResult with success status, result/error, and latency_ms.
|
|
2754
|
+
*/
|
|
2755
|
+
async execute(agentId, skillId, params, requesterOwner) {
|
|
2756
|
+
const startTime = Date.now();
|
|
2757
|
+
const agent = getHubAgent(this.registryDb, agentId);
|
|
2758
|
+
if (!agent) {
|
|
2759
|
+
return { success: false, error: "Hub Agent not found", latency_ms: Date.now() - startTime };
|
|
2760
|
+
}
|
|
2761
|
+
if (agent.status === "paused") {
|
|
2762
|
+
return { success: false, error: "Hub Agent is paused", latency_ms: Date.now() - startTime };
|
|
2763
|
+
}
|
|
2764
|
+
const route = agent.skill_routes.find((r) => r.skill_id === skillId);
|
|
2765
|
+
if (!route) {
|
|
2766
|
+
return { success: false, error: "Skill not found in routing table", latency_ms: Date.now() - startTime };
|
|
2767
|
+
}
|
|
2768
|
+
switch (route.mode) {
|
|
2769
|
+
case "relay":
|
|
2770
|
+
return this.executeRelay(route, agent, params, requesterOwner, startTime);
|
|
2771
|
+
case "queue":
|
|
2772
|
+
return this.executeQueue(route, agent, params, requesterOwner, startTime);
|
|
2773
|
+
case "direct_api":
|
|
2774
|
+
return this.executeDirectApi(route, agent, params, requesterOwner, startTime);
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
/**
|
|
2778
|
+
* Relay mode: If the target relay agent is offline, queue the job.
|
|
2779
|
+
* If online, still queue (actual dispatch happens via relay bridge).
|
|
2780
|
+
*/
|
|
2781
|
+
async executeRelay(route, agent, params, requesterOwner, startTime) {
|
|
2782
|
+
const relayOwner = route.config.relay_owner;
|
|
2783
|
+
if (this.isRelayOwnerOnline(relayOwner)) {
|
|
2784
|
+
return { success: false, error: "relay mode requires connected session agent", latency_ms: 0 };
|
|
2785
|
+
}
|
|
2786
|
+
return this.queueJob(agent, route.skill_id, params, requesterOwner, relayOwner, startTime);
|
|
2787
|
+
}
|
|
2788
|
+
/**
|
|
2789
|
+
* Queue mode: Always queue the job for later dispatch.
|
|
2790
|
+
*/
|
|
2791
|
+
async executeQueue(route, agent, params, requesterOwner, startTime) {
|
|
2792
|
+
const relayOwner = route.config.relay_owner;
|
|
2793
|
+
return this.queueJob(agent, route.skill_id, params, requesterOwner, relayOwner, startTime);
|
|
2794
|
+
}
|
|
2795
|
+
/**
|
|
2796
|
+
* Queue a job with optional credit escrow.
|
|
2797
|
+
*/
|
|
2798
|
+
queueJob(agent, skillId, params, requesterOwner, relayOwner, startTime) {
|
|
2799
|
+
const cardId = agent.agent_id.padEnd(32, "0").replace(/^(.{8})(.{4})(.{4})(.{4})(.{12}).*$/, "$1-$2-$3-$4-$5");
|
|
2800
|
+
let escrowId;
|
|
2801
|
+
if (requesterOwner) {
|
|
2802
|
+
const price = lookupCardPrice(this.registryDb, cardId, skillId);
|
|
2803
|
+
if (price !== null && price > 0) {
|
|
2804
|
+
escrowId = holdEscrow(this.creditDb, requesterOwner, price, cardId);
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
const job = insertJob(this.registryDb, {
|
|
2808
|
+
hub_agent_id: agent.agent_id,
|
|
2809
|
+
skill_id: skillId,
|
|
2810
|
+
requester_owner: requesterOwner ?? "anonymous",
|
|
2811
|
+
params,
|
|
2812
|
+
escrow_id: escrowId,
|
|
2813
|
+
relay_owner: relayOwner
|
|
2814
|
+
});
|
|
2815
|
+
return {
|
|
2816
|
+
success: true,
|
|
2817
|
+
result: { queued: true, job_id: job.id },
|
|
2818
|
+
latency_ms: Date.now() - startTime
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2821
|
+
/**
|
|
2822
|
+
* Check if a relay owner has any online cards in the registry.
|
|
2823
|
+
*
|
|
2824
|
+
* @param owner - The relay owner identifier.
|
|
2825
|
+
* @returns true if any card for this owner is online.
|
|
2826
|
+
*/
|
|
2827
|
+
isRelayOwnerOnline(owner) {
|
|
2828
|
+
const rows = this.registryDb.prepare(
|
|
2829
|
+
"SELECT data FROM capability_cards WHERE owner = ?"
|
|
2830
|
+
).all(owner);
|
|
2831
|
+
for (const row of rows) {
|
|
2832
|
+
try {
|
|
2833
|
+
const card = JSON.parse(row.data);
|
|
2834
|
+
const availability = card.availability;
|
|
2835
|
+
if (availability?.online === true) {
|
|
2836
|
+
return true;
|
|
2837
|
+
}
|
|
2838
|
+
} catch {
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
return false;
|
|
2842
|
+
}
|
|
2843
|
+
/**
|
|
2844
|
+
* Execute a direct_api skill route via ApiExecutor.
|
|
2845
|
+
* Handles secret injection and credit escrow.
|
|
2846
|
+
*/
|
|
2847
|
+
async executeDirectApi(route, agent, params, requesterOwner, startTime) {
|
|
2848
|
+
const config = this.injectSecrets(route.config, agent.secrets);
|
|
2849
|
+
const pricing = route.config.pricing;
|
|
2850
|
+
const creditsPerCall = pricing?.credits_per_call ?? 0;
|
|
2851
|
+
let escrowId;
|
|
2852
|
+
if (requesterOwner && creditsPerCall > 0) {
|
|
2853
|
+
const cardId = agent.agent_id.padEnd(32, "0").replace(/^(.{8})(.{4})(.{4})(.{4})(.{12}).*$/, "$1-$2-$3-$4-$5");
|
|
2854
|
+
escrowId = holdEscrow(this.creditDb, requesterOwner, creditsPerCall, cardId);
|
|
2855
|
+
}
|
|
2856
|
+
try {
|
|
2857
|
+
const apiExecutor = new ApiExecutor();
|
|
2858
|
+
const modeResult = await apiExecutor.execute(config, params);
|
|
2859
|
+
const result = {
|
|
2860
|
+
...modeResult,
|
|
2861
|
+
latency_ms: Date.now() - startTime
|
|
2862
|
+
};
|
|
2863
|
+
if (escrowId) {
|
|
2864
|
+
if (result.success) {
|
|
2865
|
+
settleEscrow(this.creditDb, escrowId, agent.agent_id);
|
|
2866
|
+
} else {
|
|
2867
|
+
releaseEscrow(this.creditDb, escrowId);
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
return result;
|
|
2871
|
+
} catch (err) {
|
|
2872
|
+
if (escrowId) {
|
|
2873
|
+
releaseEscrow(this.creditDb, escrowId);
|
|
2874
|
+
}
|
|
2875
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2876
|
+
return {
|
|
2877
|
+
success: false,
|
|
2878
|
+
error: message,
|
|
2879
|
+
latency_ms: Date.now() - startTime
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
/**
|
|
2884
|
+
* Injects decrypted secrets into the API skill config's auth field.
|
|
2885
|
+
* If secrets contain 'api_key' and auth type is 'bearer', replaces the token.
|
|
2886
|
+
* If secrets contain 'api_key' and auth type is 'apikey', replaces the key.
|
|
2887
|
+
*/
|
|
2888
|
+
injectSecrets(config, secrets) {
|
|
2889
|
+
if (!secrets || Object.keys(secrets).length === 0) {
|
|
2890
|
+
return config;
|
|
2891
|
+
}
|
|
2892
|
+
const injected = JSON.parse(JSON.stringify(config));
|
|
2893
|
+
const apiKey = secrets.api_key ?? secrets.API_KEY;
|
|
2894
|
+
if (apiKey && injected.auth) {
|
|
2895
|
+
switch (injected.auth.type) {
|
|
2896
|
+
case "bearer":
|
|
2897
|
+
injected.auth.token = apiKey;
|
|
2898
|
+
break;
|
|
2899
|
+
case "apikey":
|
|
2900
|
+
injected.auth.key = apiKey;
|
|
2901
|
+
break;
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
return injected;
|
|
2905
|
+
}
|
|
2906
|
+
};
|
|
2907
|
+
|
|
2908
|
+
// src/hub-agent/types.ts
|
|
2909
|
+
import { z as z4 } from "zod";
|
|
2910
|
+
var SkillRouteSchema = z4.discriminatedUnion("mode", [
|
|
2911
|
+
z4.object({
|
|
2912
|
+
skill_id: z4.string().min(1),
|
|
2913
|
+
mode: z4.literal("direct_api"),
|
|
2914
|
+
config: ApiSkillConfigSchema
|
|
2915
|
+
}),
|
|
2916
|
+
z4.object({
|
|
2917
|
+
skill_id: z4.string().min(1),
|
|
2918
|
+
mode: z4.literal("relay"),
|
|
2919
|
+
config: z4.object({ relay_owner: z4.string().min(1) })
|
|
2920
|
+
}),
|
|
2921
|
+
z4.object({
|
|
2922
|
+
skill_id: z4.string().min(1),
|
|
2923
|
+
mode: z4.literal("queue"),
|
|
2924
|
+
config: z4.object({ relay_owner: z4.string().min(1) }).passthrough()
|
|
2925
|
+
})
|
|
2926
|
+
]);
|
|
2927
|
+
var HubAgentSchema = z4.object({
|
|
2928
|
+
agent_id: z4.string().min(1),
|
|
2929
|
+
name: z4.string().min(1),
|
|
2930
|
+
owner_public_key: z4.string().min(1),
|
|
2931
|
+
public_key: z4.string().min(1),
|
|
2932
|
+
skill_routes: z4.array(SkillRouteSchema),
|
|
2933
|
+
status: z4.enum(["active", "paused"]),
|
|
2934
|
+
created_at: z4.string(),
|
|
2935
|
+
updated_at: z4.string()
|
|
2936
|
+
});
|
|
2937
|
+
var CreateAgentRequestSchema = z4.object({
|
|
2938
|
+
name: z4.string().min(1),
|
|
2939
|
+
skill_routes: z4.array(SkillRouteSchema),
|
|
2940
|
+
secrets: z4.record(z4.string()).optional()
|
|
2941
|
+
});
|
|
2942
|
+
|
|
2943
|
+
// src/hub-agent/routes.ts
|
|
2944
|
+
function buildCapabilityCard(agentId, name, publicKey, skillRoutes) {
|
|
2945
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2946
|
+
const skills = skillRoutes.map((route) => ({
|
|
2947
|
+
id: route.skill_id,
|
|
2948
|
+
name: route.mode === "direct_api" ? route.config.name : route.skill_id,
|
|
2949
|
+
description: route.mode === "direct_api" ? `API skill: ${route.config.name}` : `${route.mode} skill: ${route.skill_id}`,
|
|
2950
|
+
level: 1,
|
|
2951
|
+
inputs: [],
|
|
2952
|
+
outputs: [],
|
|
2953
|
+
pricing: route.mode === "direct_api" ? route.config.pricing : { credits_per_call: 10 }
|
|
2954
|
+
}));
|
|
2955
|
+
if (skills.length === 0) {
|
|
2956
|
+
skills.push({
|
|
2957
|
+
id: "default",
|
|
2958
|
+
name,
|
|
2959
|
+
description: `Hub Agent: ${name}`,
|
|
2960
|
+
level: 1,
|
|
2961
|
+
inputs: [],
|
|
2962
|
+
outputs: [],
|
|
2963
|
+
pricing: { credits_per_call: 10 }
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
return {
|
|
2967
|
+
spec_version: "2.0",
|
|
2968
|
+
id: agentId.padEnd(32, "0").replace(/^(.{8})(.{4})(.{4})(.{4})(.{12}).*$/, "$1-$2-$3-$4-$5"),
|
|
2969
|
+
owner: publicKey.slice(0, 16),
|
|
2970
|
+
agent_name: name,
|
|
2971
|
+
skills,
|
|
2972
|
+
availability: { online: true },
|
|
2973
|
+
created_at: now,
|
|
2974
|
+
updated_at: now
|
|
2975
|
+
};
|
|
2976
|
+
}
|
|
2977
|
+
function upsertCardRaw(db, cardData, owner) {
|
|
2978
|
+
const parsed = AnyCardSchema.safeParse(cardData);
|
|
2979
|
+
if (!parsed.success) {
|
|
2980
|
+
throw new AgentBnBError(
|
|
2981
|
+
`Card validation failed: ${parsed.error.message}`,
|
|
2982
|
+
"VALIDATION_ERROR"
|
|
2983
|
+
);
|
|
2984
|
+
}
|
|
2985
|
+
const card = parsed.data;
|
|
2986
|
+
const cardId = card.id;
|
|
2987
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2988
|
+
const existing = db.prepare("SELECT id FROM capability_cards WHERE id = ?").get(cardId);
|
|
2989
|
+
if (existing) {
|
|
2990
|
+
db.prepare("UPDATE capability_cards SET data = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(card), now, cardId);
|
|
2991
|
+
} else {
|
|
2992
|
+
db.prepare("INSERT INTO capability_cards (id, owner, data, created_at, updated_at) VALUES (?, ?, ?, ?, ?)").run(cardId, owner, JSON.stringify(card), now, now);
|
|
2993
|
+
}
|
|
2994
|
+
return cardId;
|
|
2995
|
+
}
|
|
2996
|
+
function sanitizeAgent(agent) {
|
|
2997
|
+
const { secrets, private_key_enc, ...safe } = agent;
|
|
2998
|
+
if (secrets && typeof secrets === "object") {
|
|
2999
|
+
return { ...safe, secret_keys: Object.keys(secrets) };
|
|
3000
|
+
}
|
|
3001
|
+
return safe;
|
|
3002
|
+
}
|
|
3003
|
+
async function hubAgentRoutesPlugin(fastify, options) {
|
|
3004
|
+
const { registryDb, creditDb } = options;
|
|
3005
|
+
initHubAgentTable(registryDb);
|
|
3006
|
+
initJobQueue(registryDb);
|
|
3007
|
+
fastify.post("/api/hub-agents", {
|
|
3008
|
+
schema: {
|
|
3009
|
+
tags: ["hub-agents"],
|
|
3010
|
+
summary: "Create a new Hub Agent with Ed25519 identity",
|
|
3011
|
+
body: {
|
|
3012
|
+
type: "object",
|
|
3013
|
+
required: ["name"],
|
|
3014
|
+
properties: {
|
|
3015
|
+
name: { type: "string", minLength: 1 },
|
|
3016
|
+
skill_routes: { type: "array" },
|
|
3017
|
+
secrets: { type: "object" }
|
|
3018
|
+
}
|
|
3019
|
+
},
|
|
3020
|
+
response: {
|
|
3021
|
+
201: {
|
|
3022
|
+
type: "object",
|
|
3023
|
+
properties: {
|
|
3024
|
+
agent_id: { type: "string" },
|
|
3025
|
+
name: { type: "string" },
|
|
3026
|
+
public_key: { type: "string" },
|
|
3027
|
+
skill_routes: { type: "array" },
|
|
3028
|
+
status: { type: "string" },
|
|
3029
|
+
created_at: { type: "string" },
|
|
3030
|
+
updated_at: { type: "string" }
|
|
3031
|
+
}
|
|
3032
|
+
},
|
|
3033
|
+
400: {
|
|
3034
|
+
type: "object",
|
|
3035
|
+
properties: { error: { type: "string" } }
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
}, async (request, reply) => {
|
|
3040
|
+
const parseResult = CreateAgentRequestSchema.safeParse(request.body);
|
|
3041
|
+
if (!parseResult.success) {
|
|
3042
|
+
return reply.code(400).send({ error: parseResult.error.message });
|
|
3043
|
+
}
|
|
3044
|
+
const req = parseResult.data;
|
|
3045
|
+
const ownerPublicKey = "hub-server";
|
|
3046
|
+
try {
|
|
3047
|
+
const agent = createHubAgent(registryDb, req, ownerPublicKey);
|
|
3048
|
+
bootstrapAgent(creditDb, agent.agent_id, 50);
|
|
3049
|
+
const cardData = buildCapabilityCard(agent.agent_id, agent.name, agent.public_key, agent.skill_routes);
|
|
3050
|
+
try {
|
|
3051
|
+
upsertCardRaw(registryDb, cardData, agent.agent_id);
|
|
3052
|
+
} catch {
|
|
3053
|
+
}
|
|
3054
|
+
return reply.code(201).send(sanitizeAgent(agent));
|
|
3055
|
+
} catch (err) {
|
|
3056
|
+
if (err instanceof AgentBnBError) {
|
|
3057
|
+
return reply.code(400).send({ error: err.message });
|
|
3058
|
+
}
|
|
3059
|
+
throw err;
|
|
3060
|
+
}
|
|
3061
|
+
});
|
|
3062
|
+
fastify.get("/api/hub-agents", {
|
|
3063
|
+
schema: {
|
|
3064
|
+
tags: ["hub-agents"],
|
|
3065
|
+
summary: "List all Hub Agents",
|
|
3066
|
+
response: {
|
|
3067
|
+
200: {
|
|
3068
|
+
type: "object",
|
|
3069
|
+
properties: {
|
|
3070
|
+
agents: { type: "array" }
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
}, async (_request, reply) => {
|
|
3076
|
+
const agents = listHubAgents(registryDb);
|
|
3077
|
+
return reply.send({ agents });
|
|
3078
|
+
});
|
|
3079
|
+
fastify.get("/api/hub-agents/:id", {
|
|
3080
|
+
schema: {
|
|
3081
|
+
tags: ["hub-agents"],
|
|
3082
|
+
summary: "Get a single Hub Agent by ID",
|
|
3083
|
+
params: {
|
|
3084
|
+
type: "object",
|
|
3085
|
+
properties: { id: { type: "string" } },
|
|
3086
|
+
required: ["id"]
|
|
3087
|
+
},
|
|
3088
|
+
response: {
|
|
3089
|
+
200: {
|
|
3090
|
+
type: "object",
|
|
3091
|
+
properties: {
|
|
3092
|
+
agent_id: { type: "string" },
|
|
3093
|
+
name: { type: "string" },
|
|
3094
|
+
public_key: { type: "string" },
|
|
3095
|
+
skill_routes: { type: "array" },
|
|
3096
|
+
status: { type: "string" },
|
|
3097
|
+
secret_keys: { type: "array", items: { type: "string" } }
|
|
3098
|
+
}
|
|
3099
|
+
},
|
|
3100
|
+
404: {
|
|
3101
|
+
type: "object",
|
|
3102
|
+
properties: { error: { type: "string" } }
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
}, async (request, reply) => {
|
|
3107
|
+
const { id } = request.params;
|
|
3108
|
+
const agent = getHubAgent(registryDb, id);
|
|
3109
|
+
if (!agent) {
|
|
3110
|
+
return reply.code(404).send({ error: "Hub Agent not found" });
|
|
3111
|
+
}
|
|
3112
|
+
return reply.send(sanitizeAgent(agent));
|
|
3113
|
+
});
|
|
3114
|
+
fastify.put("/api/hub-agents/:id", {
|
|
3115
|
+
schema: {
|
|
3116
|
+
tags: ["hub-agents"],
|
|
3117
|
+
summary: "Update a Hub Agent",
|
|
3118
|
+
params: {
|
|
3119
|
+
type: "object",
|
|
3120
|
+
properties: { id: { type: "string" } },
|
|
3121
|
+
required: ["id"]
|
|
3122
|
+
},
|
|
3123
|
+
body: {
|
|
3124
|
+
type: "object",
|
|
3125
|
+
properties: {
|
|
3126
|
+
name: { type: "string" },
|
|
3127
|
+
skill_routes: { type: "array" },
|
|
3128
|
+
secrets: { type: "object" }
|
|
3129
|
+
}
|
|
3130
|
+
},
|
|
3131
|
+
response: {
|
|
3132
|
+
200: {
|
|
3133
|
+
type: "object",
|
|
3134
|
+
properties: {
|
|
3135
|
+
agent_id: { type: "string" },
|
|
3136
|
+
name: { type: "string" },
|
|
3137
|
+
skill_routes: { type: "array" },
|
|
3138
|
+
status: { type: "string" }
|
|
3139
|
+
}
|
|
3140
|
+
},
|
|
3141
|
+
404: {
|
|
3142
|
+
type: "object",
|
|
3143
|
+
properties: { error: { type: "string" } }
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
}, async (request, reply) => {
|
|
3148
|
+
const { id } = request.params;
|
|
3149
|
+
const body = request.body;
|
|
3150
|
+
const updates = {};
|
|
3151
|
+
if (typeof body.name === "string") updates.name = body.name;
|
|
3152
|
+
if (Array.isArray(body.skill_routes)) updates.skill_routes = body.skill_routes;
|
|
3153
|
+
if (body.secrets && typeof body.secrets === "object") updates.secrets = body.secrets;
|
|
3154
|
+
const agent = updateHubAgent(registryDb, id, updates);
|
|
3155
|
+
if (!agent) {
|
|
3156
|
+
return reply.code(404).send({ error: "Hub Agent not found" });
|
|
3157
|
+
}
|
|
3158
|
+
if (updates.skill_routes) {
|
|
3159
|
+
const cardData = buildCapabilityCard(agent.agent_id, agent.name, agent.public_key, agent.skill_routes);
|
|
3160
|
+
try {
|
|
3161
|
+
upsertCardRaw(registryDb, cardData, agent.agent_id);
|
|
3162
|
+
} catch {
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
return reply.send(sanitizeAgent(agent));
|
|
3166
|
+
});
|
|
3167
|
+
fastify.delete("/api/hub-agents/:id", {
|
|
3168
|
+
schema: {
|
|
3169
|
+
tags: ["hub-agents"],
|
|
3170
|
+
summary: "Delete a Hub Agent",
|
|
3171
|
+
params: {
|
|
3172
|
+
type: "object",
|
|
3173
|
+
properties: { id: { type: "string" } },
|
|
3174
|
+
required: ["id"]
|
|
3175
|
+
},
|
|
3176
|
+
response: {
|
|
3177
|
+
200: {
|
|
3178
|
+
type: "object",
|
|
3179
|
+
properties: { ok: { type: "boolean" } }
|
|
3180
|
+
},
|
|
3181
|
+
404: {
|
|
3182
|
+
type: "object",
|
|
3183
|
+
properties: { error: { type: "string" } }
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
}, async (request, reply) => {
|
|
3188
|
+
const { id } = request.params;
|
|
3189
|
+
const agent = getHubAgent(registryDb, id);
|
|
3190
|
+
if (!agent) {
|
|
3191
|
+
return reply.code(404).send({ error: "Hub Agent not found" });
|
|
3192
|
+
}
|
|
3193
|
+
deleteHubAgent(registryDb, id);
|
|
3194
|
+
const cardId = id.padEnd(32, "0").replace(/^(.{8})(.{4})(.{4})(.{4})(.{12}).*$/, "$1-$2-$3-$4-$5");
|
|
3195
|
+
try {
|
|
3196
|
+
registryDb.prepare("DELETE FROM capability_cards WHERE id = ?").run(cardId);
|
|
3197
|
+
} catch {
|
|
3198
|
+
}
|
|
3199
|
+
return reply.send({ ok: true });
|
|
3200
|
+
});
|
|
3201
|
+
fastify.post("/api/hub-agents/:id/execute", {
|
|
3202
|
+
schema: {
|
|
3203
|
+
tags: ["hub-agents"],
|
|
3204
|
+
summary: "Execute a skill on a Hub Agent",
|
|
3205
|
+
params: {
|
|
3206
|
+
type: "object",
|
|
3207
|
+
properties: { id: { type: "string" } },
|
|
3208
|
+
required: ["id"]
|
|
3209
|
+
},
|
|
3210
|
+
body: {
|
|
3211
|
+
type: "object",
|
|
3212
|
+
required: ["skill_id"],
|
|
3213
|
+
properties: {
|
|
3214
|
+
skill_id: { type: "string" },
|
|
3215
|
+
params: { type: "object" },
|
|
3216
|
+
requester_owner: { type: "string" }
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
}, async (request, reply) => {
|
|
3221
|
+
const { id } = request.params;
|
|
3222
|
+
const body = request.body;
|
|
3223
|
+
const executor = new HubAgentExecutor(registryDb, creditDb);
|
|
3224
|
+
const result = await executor.execute(
|
|
3225
|
+
id,
|
|
3226
|
+
body.skill_id,
|
|
3227
|
+
body.params ?? {},
|
|
3228
|
+
body.requester_owner
|
|
3229
|
+
);
|
|
3230
|
+
if (!result.success && result.error === "Hub Agent not found") {
|
|
3231
|
+
return reply.code(404).send(result);
|
|
3232
|
+
}
|
|
3233
|
+
if (!result.success) {
|
|
3234
|
+
return reply.code(400).send(result);
|
|
3235
|
+
}
|
|
3236
|
+
return reply.send(result);
|
|
3237
|
+
});
|
|
3238
|
+
fastify.get("/api/hub-agents/:id/jobs", {
|
|
3239
|
+
schema: {
|
|
3240
|
+
tags: ["hub-agents"],
|
|
3241
|
+
summary: "List jobs for a Hub Agent",
|
|
3242
|
+
params: {
|
|
3243
|
+
type: "object",
|
|
3244
|
+
properties: { id: { type: "string" } },
|
|
3245
|
+
required: ["id"]
|
|
3246
|
+
},
|
|
3247
|
+
querystring: {
|
|
3248
|
+
type: "object",
|
|
3249
|
+
properties: {
|
|
3250
|
+
status: { type: "string", enum: ["queued", "dispatched", "completed", "failed"] }
|
|
3251
|
+
}
|
|
3252
|
+
},
|
|
3253
|
+
response: {
|
|
3254
|
+
200: {
|
|
3255
|
+
type: "object",
|
|
3256
|
+
properties: {
|
|
3257
|
+
jobs: { type: "array" }
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
}, async (request, reply) => {
|
|
3263
|
+
const { id } = request.params;
|
|
3264
|
+
const { status } = request.query ?? {};
|
|
3265
|
+
const jobs = listJobs(registryDb, id, status);
|
|
3266
|
+
return reply.send({ jobs });
|
|
3267
|
+
});
|
|
3268
|
+
fastify.get("/api/hub-agents/:id/jobs/:jobId", {
|
|
3269
|
+
schema: {
|
|
3270
|
+
tags: ["hub-agents"],
|
|
3271
|
+
summary: "Get a single job by ID",
|
|
3272
|
+
params: {
|
|
3273
|
+
type: "object",
|
|
3274
|
+
properties: {
|
|
3275
|
+
id: { type: "string" },
|
|
3276
|
+
jobId: { type: "string" }
|
|
3277
|
+
},
|
|
3278
|
+
required: ["id", "jobId"]
|
|
3279
|
+
},
|
|
3280
|
+
response: {
|
|
3281
|
+
200: {
|
|
3282
|
+
type: "object",
|
|
3283
|
+
properties: {
|
|
3284
|
+
id: { type: "string" },
|
|
3285
|
+
hub_agent_id: { type: "string" },
|
|
3286
|
+
skill_id: { type: "string" },
|
|
3287
|
+
status: { type: "string" }
|
|
3288
|
+
}
|
|
3289
|
+
},
|
|
3290
|
+
404: {
|
|
3291
|
+
type: "object",
|
|
3292
|
+
properties: { error: { type: "string" } }
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
}, async (request, reply) => {
|
|
3297
|
+
const { jobId } = request.params;
|
|
3298
|
+
const job = getJob(registryDb, jobId);
|
|
3299
|
+
if (!job) {
|
|
3300
|
+
return reply.code(404).send({ error: "Job not found" });
|
|
3301
|
+
}
|
|
3302
|
+
return reply.send(job);
|
|
3303
|
+
});
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
// src/registry/openapi-gpt-actions.ts
|
|
3307
|
+
function convertToGptActions(openapiSpec, serverUrl) {
|
|
3308
|
+
const spec = JSON.parse(JSON.stringify(openapiSpec));
|
|
3309
|
+
spec.servers = [{ url: serverUrl }];
|
|
3310
|
+
const paths = spec.paths;
|
|
3311
|
+
if (paths) {
|
|
3312
|
+
const filteredPaths = {};
|
|
3313
|
+
for (const [path, methods] of Object.entries(paths)) {
|
|
3314
|
+
if (path.startsWith("/me") || path.startsWith("/draft") || path.startsWith("/docs") || path.startsWith("/ws") || path.startsWith("/api/credits")) {
|
|
3315
|
+
continue;
|
|
3316
|
+
}
|
|
3317
|
+
const filteredMethods = {};
|
|
3318
|
+
for (const [method, operation] of Object.entries(methods)) {
|
|
3319
|
+
if (method === "get" || method === "post") {
|
|
3320
|
+
const op = operation;
|
|
3321
|
+
if (!op.operationId) {
|
|
3322
|
+
op.operationId = deriveOperationId(method, path);
|
|
3323
|
+
}
|
|
3324
|
+
delete op.security;
|
|
3325
|
+
filteredMethods[method] = op;
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
if (Object.keys(filteredMethods).length > 0) {
|
|
3329
|
+
filteredPaths[path] = filteredMethods;
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
spec.paths = filteredPaths;
|
|
3333
|
+
}
|
|
3334
|
+
const components = spec.components;
|
|
3335
|
+
if (components) {
|
|
3336
|
+
delete components.securitySchemes;
|
|
3337
|
+
}
|
|
3338
|
+
const usedTags = /* @__PURE__ */ new Set();
|
|
3339
|
+
if (spec.paths) {
|
|
3340
|
+
for (const methods of Object.values(spec.paths)) {
|
|
3341
|
+
for (const op of Object.values(methods)) {
|
|
3342
|
+
const operation = op;
|
|
3343
|
+
if (Array.isArray(operation.tags)) {
|
|
3344
|
+
for (const tag of operation.tags) {
|
|
3345
|
+
usedTags.add(tag);
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
if (Array.isArray(spec.tags)) {
|
|
3352
|
+
spec.tags = spec.tags.filter((t) => usedTags.has(t.name));
|
|
3353
|
+
}
|
|
3354
|
+
return spec;
|
|
3355
|
+
}
|
|
3356
|
+
function deriveOperationId(method, path) {
|
|
3357
|
+
const segments = path.split("/").filter((s) => s.length > 0).map((s) => {
|
|
3358
|
+
if (s.startsWith("{") || s.startsWith(":")) {
|
|
3359
|
+
const paramName = s.replace(/[{}:]/g, "");
|
|
3360
|
+
return "By" + paramName.charAt(0).toUpperCase() + paramName.slice(1);
|
|
3361
|
+
}
|
|
3362
|
+
return s.split("-").map((part, i) => i === 0 ? part.charAt(0).toUpperCase() + part.slice(1) : part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
3363
|
+
});
|
|
3364
|
+
return method + segments.join("");
|
|
3365
|
+
}
|
|
3366
|
+
|
|
2111
3367
|
// src/registry/server.ts
|
|
2112
3368
|
function stripInternal(card) {
|
|
2113
3369
|
const { _internal: _, ...publicCard } = card;
|
|
@@ -2116,25 +3372,76 @@ function stripInternal(card) {
|
|
|
2116
3372
|
function createRegistryServer(opts) {
|
|
2117
3373
|
const { registryDb: db, silent = false } = opts;
|
|
2118
3374
|
const server = Fastify2({ logger: !silent });
|
|
3375
|
+
void server.register(swagger, {
|
|
3376
|
+
openapi: {
|
|
3377
|
+
openapi: "3.0.3",
|
|
3378
|
+
info: {
|
|
3379
|
+
title: "AgentBnB Registry API",
|
|
3380
|
+
description: "P2P Agent Capability Sharing Protocol \u2014 discover, publish, and exchange agent capabilities",
|
|
3381
|
+
version: "3.1.6"
|
|
3382
|
+
},
|
|
3383
|
+
servers: [{ url: "/", description: "Registry server" }],
|
|
3384
|
+
tags: [
|
|
3385
|
+
{ name: "cards", description: "Capability card CRUD" },
|
|
3386
|
+
{ name: "credits", description: "Credit hold/settle/release (Ed25519 auth required)" },
|
|
3387
|
+
{ name: "agents", description: "Agent profiles and reputation" },
|
|
3388
|
+
{ name: "identity", description: "Agent identity and guarantor registration" },
|
|
3389
|
+
{ name: "owner", description: "Owner-only endpoints (Bearer auth required)" },
|
|
3390
|
+
{ name: "system", description: "Health and stats" },
|
|
3391
|
+
{ name: "pricing", description: "Market pricing statistics" }
|
|
3392
|
+
],
|
|
3393
|
+
components: {
|
|
3394
|
+
securitySchemes: {
|
|
3395
|
+
bearerAuth: { type: "http", scheme: "bearer" },
|
|
3396
|
+
ed25519Auth: {
|
|
3397
|
+
type: "apiKey",
|
|
3398
|
+
in: "header",
|
|
3399
|
+
name: "X-Agent-PublicKey",
|
|
3400
|
+
description: "Ed25519 public key (hex). Also requires X-Agent-Signature and X-Agent-Timestamp headers."
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
});
|
|
3406
|
+
void server.register(swaggerUi, {
|
|
3407
|
+
routePrefix: "/docs",
|
|
3408
|
+
uiConfig: { docExpansion: "list", deepLinking: true }
|
|
3409
|
+
});
|
|
2119
3410
|
void server.register(cors, {
|
|
2120
3411
|
origin: true,
|
|
2121
3412
|
methods: ["GET", "POST", "PATCH", "OPTIONS"],
|
|
2122
|
-
allowedHeaders: ["Content-Type", "Authorization"]
|
|
3413
|
+
allowedHeaders: ["Content-Type", "Authorization", "X-Agent-PublicKey", "X-Agent-Signature", "X-Agent-Timestamp"]
|
|
2123
3414
|
});
|
|
2124
3415
|
void server.register(fastifyWebsocket);
|
|
2125
3416
|
let relayState = null;
|
|
2126
3417
|
if (opts.creditDb) {
|
|
2127
|
-
relayState = registerWebSocketRelay(server, db);
|
|
3418
|
+
relayState = registerWebSocketRelay(server, db, opts.creditDb);
|
|
3419
|
+
}
|
|
3420
|
+
if (opts.creditDb) {
|
|
3421
|
+
void server.register(creditRoutesPlugin, { creditDb: opts.creditDb });
|
|
3422
|
+
}
|
|
3423
|
+
if (opts.creditDb) {
|
|
3424
|
+
void server.register(hubAgentRoutesPlugin, { registryDb: db, creditDb: opts.creditDb });
|
|
3425
|
+
if (relayState?.setOnAgentOnline && relayState.getConnections && relayState.getPendingRequests && relayState.sendMessage) {
|
|
3426
|
+
const bridge = createRelayBridge({
|
|
3427
|
+
registryDb: db,
|
|
3428
|
+
creditDb: opts.creditDb,
|
|
3429
|
+
sendMessage: relayState.sendMessage,
|
|
3430
|
+
pendingRequests: relayState.getPendingRequests(),
|
|
3431
|
+
connections: relayState.getConnections()
|
|
3432
|
+
});
|
|
3433
|
+
relayState.setOnAgentOnline(bridge.onAgentOnline);
|
|
3434
|
+
}
|
|
2128
3435
|
}
|
|
2129
3436
|
const __filename = fileURLToPath(import.meta.url);
|
|
2130
3437
|
const __dirname = dirname(__filename);
|
|
2131
3438
|
const hubDistCandidates = [
|
|
2132
|
-
|
|
3439
|
+
join2(__dirname, "../../hub/dist"),
|
|
2133
3440
|
// When running from dist/registry/server.js
|
|
2134
|
-
|
|
3441
|
+
join2(__dirname, "../../../hub/dist")
|
|
2135
3442
|
// Fallback for alternative layouts
|
|
2136
3443
|
];
|
|
2137
|
-
const hubDistDir = hubDistCandidates.find((p) =>
|
|
3444
|
+
const hubDistDir = hubDistCandidates.find((p) => existsSync3(p));
|
|
2138
3445
|
if (hubDistDir) {
|
|
2139
3446
|
void server.register(fastifyStatic, {
|
|
2140
3447
|
root: hubDistDir,
|
|
@@ -2153,44 +3460,82 @@ function createRegistryServer(opts) {
|
|
|
2153
3460
|
return reply.code(404).send({ error: "Not found" });
|
|
2154
3461
|
});
|
|
2155
3462
|
}
|
|
2156
|
-
server.
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
3463
|
+
void server.register(async (api) => {
|
|
3464
|
+
api.get("/health", {
|
|
3465
|
+
schema: {
|
|
3466
|
+
tags: ["system"],
|
|
3467
|
+
summary: "Liveness probe",
|
|
3468
|
+
response: { 200: { type: "object", properties: { status: { type: "string" } } } }
|
|
3469
|
+
}
|
|
3470
|
+
}, async (_request, reply) => {
|
|
3471
|
+
return reply.send({ status: "ok" });
|
|
3472
|
+
});
|
|
3473
|
+
api.get("/cards", {
|
|
3474
|
+
schema: {
|
|
3475
|
+
tags: ["cards"],
|
|
3476
|
+
summary: "List and search capability cards",
|
|
3477
|
+
querystring: {
|
|
3478
|
+
type: "object",
|
|
3479
|
+
properties: {
|
|
3480
|
+
q: { type: "string", description: "Full-text search query" },
|
|
3481
|
+
level: { type: "integer", enum: [1, 2, 3], description: "Capability level filter" },
|
|
3482
|
+
online: { type: "string", enum: ["true", "false"], description: "Availability filter" },
|
|
3483
|
+
tag: { type: "string", description: "Filter by metadata tag" },
|
|
3484
|
+
min_success_rate: { type: "number", description: "Minimum success rate (0-1)" },
|
|
3485
|
+
max_latency_ms: { type: "number", description: "Maximum average latency in ms" },
|
|
3486
|
+
sort: { type: "string", enum: ["popular", "rated", "success_rate", "cheapest", "newest", "latency"], description: "Sort order" },
|
|
3487
|
+
limit: { type: "integer", default: 20, description: "Max items per page (max 100)" },
|
|
3488
|
+
offset: { type: "integer", default: 0, description: "Pagination offset" }
|
|
3489
|
+
}
|
|
3490
|
+
},
|
|
3491
|
+
response: {
|
|
3492
|
+
200: {
|
|
3493
|
+
type: "object",
|
|
3494
|
+
properties: {
|
|
3495
|
+
total: { type: "integer" },
|
|
3496
|
+
limit: { type: "integer" },
|
|
3497
|
+
offset: { type: "integer" },
|
|
3498
|
+
items: { type: "array" },
|
|
3499
|
+
uses_this_week: { type: "object", additionalProperties: { type: "number" } }
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
}, async (request, reply) => {
|
|
3505
|
+
const query = request.query;
|
|
3506
|
+
const q = query.q?.trim() ?? "";
|
|
3507
|
+
const levelRaw = query.level !== void 0 ? parseInt(query.level, 10) : void 0;
|
|
3508
|
+
const level = levelRaw === 1 || levelRaw === 2 || levelRaw === 3 ? levelRaw : void 0;
|
|
3509
|
+
const onlineRaw = query.online;
|
|
3510
|
+
const online = onlineRaw === "true" ? true : onlineRaw === "false" ? false : void 0;
|
|
3511
|
+
const tag = query.tag?.trim();
|
|
3512
|
+
const minSuccessRate = query.min_success_rate !== void 0 ? parseFloat(query.min_success_rate) : void 0;
|
|
3513
|
+
const maxLatencyMs = query.max_latency_ms !== void 0 ? parseFloat(query.max_latency_ms) : void 0;
|
|
3514
|
+
const sort = query.sort;
|
|
3515
|
+
const rawLimit = query.limit !== void 0 ? parseInt(query.limit, 10) : 20;
|
|
3516
|
+
const limit = Math.min(isNaN(rawLimit) || rawLimit < 1 ? 20 : rawLimit, 100);
|
|
3517
|
+
const rawOffset = query.offset !== void 0 ? parseInt(query.offset, 10) : 0;
|
|
3518
|
+
const offset = isNaN(rawOffset) || rawOffset < 0 ? 0 : rawOffset;
|
|
3519
|
+
let cards;
|
|
3520
|
+
if (q.length > 0) {
|
|
3521
|
+
cards = searchCards(db, q, { level, online });
|
|
3522
|
+
} else {
|
|
3523
|
+
cards = filterCards(db, { level, online });
|
|
3524
|
+
}
|
|
3525
|
+
if (tag !== void 0 && tag.length > 0) {
|
|
3526
|
+
cards = cards.filter((c) => c.metadata?.tags?.includes(tag));
|
|
3527
|
+
}
|
|
3528
|
+
if (minSuccessRate !== void 0 && !isNaN(minSuccessRate)) {
|
|
3529
|
+
cards = cards.filter(
|
|
3530
|
+
(c) => (c.metadata?.success_rate ?? -1) >= minSuccessRate
|
|
3531
|
+
);
|
|
3532
|
+
}
|
|
3533
|
+
if (maxLatencyMs !== void 0 && !isNaN(maxLatencyMs)) {
|
|
3534
|
+
cards = cards.filter(
|
|
3535
|
+
(c) => (c.metadata?.avg_latency_ms ?? Infinity) <= maxLatencyMs
|
|
3536
|
+
);
|
|
3537
|
+
}
|
|
3538
|
+
const usesStmt = db.prepare(`
|
|
2194
3539
|
SELECT card_id, skill_id, COUNT(*) as cnt
|
|
2195
3540
|
FROM request_log
|
|
2196
3541
|
WHERE status = 'success'
|
|
@@ -2198,57 +3543,63 @@ function createRegistryServer(opts) {
|
|
|
2198
3543
|
AND (action_type IS NULL OR action_type = 'auto_share')
|
|
2199
3544
|
GROUP BY card_id, skill_id
|
|
2200
3545
|
`);
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
3546
|
+
const usesRows = usesStmt.all();
|
|
3547
|
+
const usesMap = /* @__PURE__ */ new Map();
|
|
3548
|
+
for (const row of usesRows) {
|
|
3549
|
+
usesMap.set(row.card_id, (usesMap.get(row.card_id) ?? 0) + row.cnt);
|
|
3550
|
+
if (row.skill_id) {
|
|
3551
|
+
usesMap.set(row.skill_id, (usesMap.get(row.skill_id) ?? 0) + row.cnt);
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
if (sort === "popular") {
|
|
3555
|
+
cards = [...cards].sort((a, b) => {
|
|
3556
|
+
const aUses = usesMap.get(a.id) ?? 0;
|
|
3557
|
+
const bUses = usesMap.get(b.id) ?? 0;
|
|
3558
|
+
return bUses - aUses;
|
|
3559
|
+
});
|
|
3560
|
+
} else if (sort === "rated" || sort === "success_rate") {
|
|
3561
|
+
cards = [...cards].sort((a, b) => {
|
|
3562
|
+
const aRate = a.metadata?.success_rate ?? -1;
|
|
3563
|
+
const bRate = b.metadata?.success_rate ?? -1;
|
|
3564
|
+
return bRate - aRate;
|
|
3565
|
+
});
|
|
3566
|
+
} else if (sort === "cheapest") {
|
|
3567
|
+
cards = [...cards].sort((a, b) => {
|
|
3568
|
+
return a.pricing.credits_per_call - b.pricing.credits_per_call;
|
|
3569
|
+
});
|
|
3570
|
+
} else if (sort === "newest") {
|
|
3571
|
+
const createdStmt = db.prepare("SELECT id, created_at FROM capability_cards");
|
|
3572
|
+
const createdRows = createdStmt.all();
|
|
3573
|
+
const createdMap = new Map(createdRows.map((r) => [r.id, r.created_at]));
|
|
3574
|
+
cards = [...cards].sort((a, b) => {
|
|
3575
|
+
const aDate = createdMap.get(a.id) ?? "";
|
|
3576
|
+
const bDate = createdMap.get(b.id) ?? "";
|
|
3577
|
+
return bDate.localeCompare(aDate);
|
|
3578
|
+
});
|
|
3579
|
+
} else if (sort === "latency") {
|
|
3580
|
+
cards = [...cards].sort((a, b) => {
|
|
3581
|
+
const aLatency = a.metadata?.avg_latency_ms ?? Infinity;
|
|
3582
|
+
const bLatency = b.metadata?.avg_latency_ms ?? Infinity;
|
|
3583
|
+
return aLatency - bLatency;
|
|
3584
|
+
});
|
|
3585
|
+
}
|
|
3586
|
+
const total = cards.length;
|
|
3587
|
+
const items = cards.slice(offset, offset + limit).map(stripInternal);
|
|
3588
|
+
const usesThisWeek = {};
|
|
3589
|
+
for (const [key, count] of usesMap) {
|
|
3590
|
+
if (count > 0) usesThisWeek[key] = count;
|
|
3591
|
+
}
|
|
3592
|
+
const result = { total, limit, offset, items, uses_this_week: usesThisWeek };
|
|
3593
|
+
return reply.send(result);
|
|
3594
|
+
});
|
|
3595
|
+
api.get("/api/cards/trending", {
|
|
3596
|
+
schema: {
|
|
3597
|
+
tags: ["cards"],
|
|
3598
|
+
summary: "Top 10 trending skills by recent usage",
|
|
3599
|
+
response: { 200: { type: "object", properties: { items: { type: "array" } } } }
|
|
3600
|
+
}
|
|
3601
|
+
}, async (_request, reply) => {
|
|
3602
|
+
const trendingStmt = db.prepare(`
|
|
2252
3603
|
SELECT rl.card_id, COUNT(*) as recent_requests
|
|
2253
3604
|
FROM request_log rl
|
|
2254
3605
|
WHERE rl.status = 'success'
|
|
@@ -2258,350 +3609,656 @@ function createRegistryServer(opts) {
|
|
|
2258
3609
|
ORDER BY recent_requests DESC
|
|
2259
3610
|
LIMIT 10
|
|
2260
3611
|
`);
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
3612
|
+
const trendingRows = trendingStmt.all();
|
|
3613
|
+
const items = trendingRows.map((row) => {
|
|
3614
|
+
const card = getCard(db, row.card_id);
|
|
3615
|
+
if (!card) return null;
|
|
3616
|
+
return { ...stripInternal(card), uses_this_week: row.recent_requests };
|
|
3617
|
+
}).filter((item) => item !== null);
|
|
3618
|
+
return reply.send({ items });
|
|
3619
|
+
});
|
|
3620
|
+
api.get("/api/pricing", {
|
|
3621
|
+
schema: {
|
|
3622
|
+
tags: ["pricing"],
|
|
3623
|
+
summary: "Aggregate pricing statistics for skills matching a query",
|
|
3624
|
+
querystring: {
|
|
3625
|
+
type: "object",
|
|
3626
|
+
properties: { q: { type: "string", description: "Search query (required)" } },
|
|
3627
|
+
required: ["q"]
|
|
3628
|
+
},
|
|
3629
|
+
response: {
|
|
3630
|
+
200: {
|
|
3631
|
+
type: "object",
|
|
3632
|
+
properties: {
|
|
3633
|
+
query: { type: "string" },
|
|
3634
|
+
min: { type: "number" },
|
|
3635
|
+
max: { type: "number" },
|
|
3636
|
+
median: { type: "number" },
|
|
3637
|
+
mean: { type: "number" },
|
|
3638
|
+
count: { type: "integer" }
|
|
3639
|
+
}
|
|
3640
|
+
},
|
|
3641
|
+
400: { type: "object", properties: { error: { type: "string" } } }
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
}, async (request, reply) => {
|
|
3645
|
+
const query = request.query;
|
|
3646
|
+
const q = query.q?.trim();
|
|
3647
|
+
if (!q) {
|
|
3648
|
+
return reply.code(400).send({ error: "q parameter is required" });
|
|
3649
|
+
}
|
|
3650
|
+
const stats = getPricingStats(db, q);
|
|
3651
|
+
return reply.send({ query: q, ...stats });
|
|
3652
|
+
});
|
|
3653
|
+
api.get("/cards/:id", {
|
|
3654
|
+
schema: {
|
|
3655
|
+
tags: ["cards"],
|
|
3656
|
+
summary: "Get a capability card by ID",
|
|
3657
|
+
params: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
3658
|
+
response: {
|
|
3659
|
+
200: { type: "object", additionalProperties: true },
|
|
3660
|
+
404: { type: "object", properties: { error: { type: "string" } } }
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
}, async (request, reply) => {
|
|
3664
|
+
const { id } = request.params;
|
|
3665
|
+
const card = getCard(db, id);
|
|
3666
|
+
if (!card) {
|
|
3667
|
+
return reply.code(404).send({ error: "Not found" });
|
|
3668
|
+
}
|
|
3669
|
+
return reply.send(stripInternal(card));
|
|
3670
|
+
});
|
|
3671
|
+
api.post("/cards", {
|
|
3672
|
+
schema: {
|
|
3673
|
+
tags: ["cards"],
|
|
3674
|
+
summary: "Publish a capability card",
|
|
3675
|
+
body: { type: "object", additionalProperties: true, description: "Capability card JSON (v1.0 or v2.0)" },
|
|
3676
|
+
response: {
|
|
3677
|
+
201: { type: "object", properties: { ok: { type: "boolean" }, id: { type: "string" } } },
|
|
3678
|
+
400: { type: "object", properties: { error: { type: "string" }, issues: { type: "array" } } }
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
}, async (request, reply) => {
|
|
3682
|
+
const body = request.body;
|
|
3683
|
+
if (!body.spec_version) {
|
|
3684
|
+
body.spec_version = "1.0";
|
|
3685
|
+
}
|
|
3686
|
+
const result = AnyCardSchema.safeParse(body);
|
|
3687
|
+
if (!result.success) {
|
|
3688
|
+
return reply.code(400).send({
|
|
3689
|
+
error: "Card validation failed",
|
|
3690
|
+
issues: result.error.issues
|
|
3691
|
+
});
|
|
3692
|
+
}
|
|
3693
|
+
const card = result.data;
|
|
3694
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3695
|
+
if (card.spec_version === "2.0") {
|
|
3696
|
+
const cardWithTimestamps = {
|
|
3697
|
+
...card,
|
|
3698
|
+
created_at: card.created_at ?? now,
|
|
3699
|
+
updated_at: now
|
|
3700
|
+
};
|
|
3701
|
+
db.prepare(
|
|
3702
|
+
`INSERT OR REPLACE INTO capability_cards (id, owner, data, created_at, updated_at)
|
|
3703
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
3704
|
+
).run(
|
|
3705
|
+
cardWithTimestamps.id,
|
|
3706
|
+
cardWithTimestamps.owner,
|
|
3707
|
+
JSON.stringify(cardWithTimestamps),
|
|
3708
|
+
cardWithTimestamps.created_at,
|
|
3709
|
+
cardWithTimestamps.updated_at
|
|
3710
|
+
);
|
|
3711
|
+
} else {
|
|
3712
|
+
try {
|
|
3713
|
+
insertCard(db, card);
|
|
3714
|
+
} catch (err) {
|
|
3715
|
+
if (err instanceof AgentBnBError && err.code === "VALIDATION_ERROR") {
|
|
3716
|
+
return reply.code(400).send({ error: err.message });
|
|
3717
|
+
}
|
|
3718
|
+
throw err;
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
return reply.code(201).send({ ok: true, id: card.id });
|
|
3722
|
+
});
|
|
3723
|
+
api.delete("/cards/:id", {
|
|
3724
|
+
schema: {
|
|
3725
|
+
tags: ["cards"],
|
|
3726
|
+
summary: "Delete a capability card",
|
|
3727
|
+
params: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
3728
|
+
response: {
|
|
3729
|
+
200: { type: "object", properties: { ok: { type: "boolean" }, id: { type: "string" } } },
|
|
3730
|
+
404: { type: "object", properties: { error: { type: "string" } } }
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
}, async (request, reply) => {
|
|
3734
|
+
const { id } = request.params;
|
|
3735
|
+
const card = getCard(db, id);
|
|
3736
|
+
if (!card) {
|
|
3737
|
+
return reply.code(404).send({ error: "Not found" });
|
|
3738
|
+
}
|
|
3739
|
+
db.prepare("DELETE FROM capability_cards WHERE id = ?").run(id);
|
|
3740
|
+
return reply.send({ ok: true, id });
|
|
3741
|
+
});
|
|
3742
|
+
api.get("/api/agents", {
|
|
3743
|
+
schema: {
|
|
3744
|
+
tags: ["agents"],
|
|
3745
|
+
summary: "List all agent profiles sorted by reputation",
|
|
3746
|
+
response: {
|
|
3747
|
+
200: {
|
|
3748
|
+
type: "object",
|
|
3749
|
+
properties: { items: { type: "array" }, total: { type: "integer" } }
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
}, async (_request, reply) => {
|
|
3754
|
+
const allCards = listCards(db);
|
|
3755
|
+
const ownerMap = /* @__PURE__ */ new Map();
|
|
3756
|
+
for (const card of allCards) {
|
|
3757
|
+
const existing = ownerMap.get(card.owner) ?? [];
|
|
3758
|
+
existing.push(card);
|
|
3759
|
+
ownerMap.set(card.owner, existing);
|
|
3760
|
+
}
|
|
3761
|
+
const creditsStmt = db.prepare(`
|
|
3762
|
+
SELECT cc.owner,
|
|
3763
|
+
SUM(CASE WHEN rl.status = 'success' THEN rl.credits_charged ELSE 0 END) as credits_earned
|
|
3764
|
+
FROM capability_cards cc
|
|
3765
|
+
LEFT JOIN request_log rl ON rl.card_id = cc.id
|
|
3766
|
+
GROUP BY cc.owner
|
|
3767
|
+
`);
|
|
3768
|
+
const creditsRows = creditsStmt.all();
|
|
3769
|
+
const creditsMap = new Map(creditsRows.map((r) => [r.owner, r.credits_earned ?? 0]));
|
|
3770
|
+
const agents = Array.from(ownerMap.entries()).map(([owner, cards]) => {
|
|
3771
|
+
const skillCount = cards.reduce((sum, card) => sum + (card.skills?.length ?? 1), 0);
|
|
3772
|
+
const successRates = cards.map((c) => c.metadata?.success_rate).filter((r) => r != null);
|
|
3773
|
+
const avgSuccessRate = successRates.length > 0 ? successRates.reduce((a, b) => a + b, 0) / successRates.length : null;
|
|
3774
|
+
const memberStmt = db.prepare(
|
|
3775
|
+
"SELECT MIN(created_at) as earliest FROM capability_cards WHERE owner = ?"
|
|
3776
|
+
);
|
|
3777
|
+
const memberRow = memberStmt.get(owner);
|
|
3778
|
+
return {
|
|
3779
|
+
owner,
|
|
3780
|
+
skill_count: skillCount,
|
|
3781
|
+
success_rate: avgSuccessRate,
|
|
3782
|
+
total_earned: creditsMap.get(owner) ?? 0,
|
|
3783
|
+
member_since: memberRow?.earliest ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
3784
|
+
};
|
|
2287
3785
|
});
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
3786
|
+
agents.sort((a, b) => {
|
|
3787
|
+
const aRate = a.success_rate ?? -1;
|
|
3788
|
+
const bRate = b.success_rate ?? -1;
|
|
3789
|
+
if (bRate !== aRate) return bRate - aRate;
|
|
3790
|
+
return b.total_earned - a.total_earned;
|
|
3791
|
+
});
|
|
3792
|
+
return reply.send({ items: agents, total: agents.length });
|
|
3793
|
+
});
|
|
3794
|
+
api.get("/api/agents/:owner", {
|
|
3795
|
+
schema: {
|
|
3796
|
+
tags: ["agents"],
|
|
3797
|
+
summary: "Get agent profile, skills, and recent activity",
|
|
3798
|
+
params: { type: "object", properties: { owner: { type: "string" } }, required: ["owner"] },
|
|
3799
|
+
response: {
|
|
3800
|
+
200: {
|
|
3801
|
+
type: "object",
|
|
3802
|
+
properties: {
|
|
3803
|
+
profile: { type: "object", additionalProperties: true },
|
|
3804
|
+
skills: { type: "array" },
|
|
3805
|
+
recent_activity: { type: "array" }
|
|
3806
|
+
}
|
|
3807
|
+
},
|
|
3808
|
+
404: { type: "object", properties: { error: { type: "string" } } }
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
}, async (request, reply) => {
|
|
3812
|
+
const { owner } = request.params;
|
|
3813
|
+
const ownerCards = listCards(db, owner);
|
|
3814
|
+
if (ownerCards.length === 0) {
|
|
3815
|
+
return reply.status(404).send({ error: "Agent not found" });
|
|
3816
|
+
}
|
|
3817
|
+
const skillCount = ownerCards.reduce((sum, card) => sum + (card.skills?.length ?? 1), 0);
|
|
3818
|
+
const successRates = ownerCards.map((c) => c.metadata?.success_rate).filter((r) => r != null);
|
|
3819
|
+
const avgSuccessRate = successRates.length > 0 ? successRates.reduce((a, b) => a + b, 0) / successRates.length : null;
|
|
3820
|
+
const creditsStmt = db.prepare(`
|
|
3821
|
+
SELECT SUM(CASE WHEN rl.status = 'success' THEN rl.credits_charged ELSE 0 END) as credits_earned
|
|
3822
|
+
FROM capability_cards cc
|
|
3823
|
+
LEFT JOIN request_log rl ON rl.card_id = cc.id
|
|
3824
|
+
WHERE cc.owner = ?
|
|
3825
|
+
`);
|
|
3826
|
+
const creditsRow = creditsStmt.get(owner);
|
|
3827
|
+
const memberStmt = db.prepare(
|
|
3828
|
+
"SELECT MIN(created_at) as earliest FROM capability_cards WHERE owner = ?"
|
|
3829
|
+
);
|
|
3830
|
+
const memberRow = memberStmt.get(owner);
|
|
3831
|
+
const activityStmt = db.prepare(`
|
|
3832
|
+
SELECT rl.id, rl.card_name, rl.requester, rl.status, rl.credits_charged, rl.created_at
|
|
3833
|
+
FROM request_log rl
|
|
3834
|
+
INNER JOIN capability_cards cc ON rl.card_id = cc.id
|
|
3835
|
+
WHERE cc.owner = ?
|
|
3836
|
+
ORDER BY rl.created_at DESC
|
|
3837
|
+
LIMIT 10
|
|
3838
|
+
`);
|
|
3839
|
+
const recentActivity = activityStmt.all(owner);
|
|
3840
|
+
const profile = {
|
|
3841
|
+
owner,
|
|
3842
|
+
skill_count: skillCount,
|
|
3843
|
+
success_rate: avgSuccessRate,
|
|
3844
|
+
total_earned: creditsRow?.credits_earned ?? 0,
|
|
3845
|
+
member_since: memberRow?.earliest ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2296
3846
|
};
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
3847
|
+
return reply.send({
|
|
3848
|
+
profile,
|
|
3849
|
+
skills: ownerCards,
|
|
3850
|
+
recent_activity: recentActivity
|
|
3851
|
+
});
|
|
3852
|
+
});
|
|
3853
|
+
api.get("/api/activity", {
|
|
3854
|
+
schema: {
|
|
3855
|
+
tags: ["system"],
|
|
3856
|
+
summary: "Paginated public activity feed of exchange events",
|
|
3857
|
+
querystring: {
|
|
3858
|
+
type: "object",
|
|
3859
|
+
properties: {
|
|
3860
|
+
limit: { type: "integer", default: 20, description: "Max items (max 100)" },
|
|
3861
|
+
since: { type: "string", description: "ISO 8601 timestamp for polling" }
|
|
3862
|
+
}
|
|
3863
|
+
},
|
|
3864
|
+
response: {
|
|
3865
|
+
200: {
|
|
3866
|
+
type: "object",
|
|
3867
|
+
properties: {
|
|
3868
|
+
items: { type: "array" },
|
|
3869
|
+
total: { type: "integer" },
|
|
3870
|
+
limit: { type: "integer" }
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
}, async (request, reply) => {
|
|
3876
|
+
const query = request.query;
|
|
3877
|
+
const rawLimit = query.limit !== void 0 ? parseInt(query.limit, 10) : 20;
|
|
3878
|
+
const limit = Math.min(isNaN(rawLimit) || rawLimit < 1 ? 20 : rawLimit, 100);
|
|
3879
|
+
const since = query.since?.trim() || void 0;
|
|
3880
|
+
const items = getActivityFeed(db, limit, since);
|
|
3881
|
+
return reply.send({ items, total: items.length, limit });
|
|
3882
|
+
});
|
|
3883
|
+
api.get("/api/stats", {
|
|
3884
|
+
schema: {
|
|
3885
|
+
tags: ["system"],
|
|
3886
|
+
summary: "Aggregate network statistics",
|
|
3887
|
+
response: {
|
|
3888
|
+
200: {
|
|
3889
|
+
type: "object",
|
|
3890
|
+
properties: {
|
|
3891
|
+
agents_online: { type: "integer" },
|
|
3892
|
+
total_capabilities: { type: "integer" },
|
|
3893
|
+
total_exchanges: { type: "integer" }
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
}, async (_request, reply) => {
|
|
3899
|
+
const allCards = listCards(db);
|
|
3900
|
+
const onlineOwners = /* @__PURE__ */ new Set();
|
|
3901
|
+
if (relayState) {
|
|
3902
|
+
for (const owner of relayState.getOnlineOwners()) {
|
|
3903
|
+
onlineOwners.add(owner);
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
for (const card of allCards) {
|
|
3907
|
+
if (card.availability.online) {
|
|
3908
|
+
onlineOwners.add(card.owner);
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
const exchangeStmt = db.prepare(
|
|
3912
|
+
"SELECT COUNT(*) as count FROM request_log WHERE status = 'success' AND (action_type IS NULL OR action_type = 'auto_share')"
|
|
2306
3913
|
);
|
|
2307
|
-
|
|
3914
|
+
const exchangeRow = exchangeStmt.get();
|
|
3915
|
+
return reply.send({
|
|
3916
|
+
agents_online: onlineOwners.size,
|
|
3917
|
+
total_capabilities: allCards.reduce((sum, card) => {
|
|
3918
|
+
const v2 = card;
|
|
3919
|
+
return sum + (v2.skills?.length ?? 1);
|
|
3920
|
+
}, 0),
|
|
3921
|
+
total_exchanges: exchangeRow.count
|
|
3922
|
+
});
|
|
3923
|
+
});
|
|
3924
|
+
api.post("/api/identity/register", {
|
|
3925
|
+
schema: {
|
|
3926
|
+
tags: ["identity"],
|
|
3927
|
+
summary: "Register a human guarantor via GitHub login",
|
|
3928
|
+
body: {
|
|
3929
|
+
type: "object",
|
|
3930
|
+
properties: { github_login: { type: "string" } },
|
|
3931
|
+
required: ["github_login"]
|
|
3932
|
+
},
|
|
3933
|
+
response: {
|
|
3934
|
+
201: { type: "object", additionalProperties: true },
|
|
3935
|
+
400: { type: "object", properties: { error: { type: "string" } } },
|
|
3936
|
+
409: { type: "object", properties: { error: { type: "string" } } }
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
}, async (request, reply) => {
|
|
3940
|
+
if (!opts.creditDb) {
|
|
3941
|
+
return reply.code(503).send({ error: "Credit database not configured" });
|
|
3942
|
+
}
|
|
3943
|
+
const body = request.body;
|
|
3944
|
+
const githubLogin = typeof body.github_login === "string" ? body.github_login.trim() : "";
|
|
3945
|
+
if (!githubLogin) {
|
|
3946
|
+
return reply.code(400).send({ error: "github_login is required" });
|
|
3947
|
+
}
|
|
3948
|
+
try {
|
|
3949
|
+
const record = registerGuarantor(opts.creditDb, githubLogin);
|
|
3950
|
+
const auth = initiateGithubAuth();
|
|
3951
|
+
return reply.code(201).send({ guarantor: record, oauth: auth });
|
|
3952
|
+
} catch (err) {
|
|
3953
|
+
if (err instanceof AgentBnBError && err.code === "GUARANTOR_EXISTS") {
|
|
3954
|
+
return reply.code(409).send({ error: err.message });
|
|
3955
|
+
}
|
|
3956
|
+
throw err;
|
|
3957
|
+
}
|
|
3958
|
+
});
|
|
3959
|
+
api.post("/api/identity/link", {
|
|
3960
|
+
schema: {
|
|
3961
|
+
tags: ["identity"],
|
|
3962
|
+
summary: "Link an agent to a human guarantor",
|
|
3963
|
+
body: {
|
|
3964
|
+
type: "object",
|
|
3965
|
+
properties: {
|
|
3966
|
+
agent_id: { type: "string" },
|
|
3967
|
+
github_login: { type: "string" }
|
|
3968
|
+
},
|
|
3969
|
+
required: ["agent_id", "github_login"]
|
|
3970
|
+
},
|
|
3971
|
+
response: {
|
|
3972
|
+
200: { type: "object", additionalProperties: true },
|
|
3973
|
+
400: { type: "object", properties: { error: { type: "string" } } },
|
|
3974
|
+
404: { type: "object", properties: { error: { type: "string" } } },
|
|
3975
|
+
409: { type: "object", properties: { error: { type: "string" } } }
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
}, async (request, reply) => {
|
|
3979
|
+
if (!opts.creditDb) {
|
|
3980
|
+
return reply.code(503).send({ error: "Credit database not configured" });
|
|
3981
|
+
}
|
|
3982
|
+
const body = request.body;
|
|
3983
|
+
const agentId = typeof body.agent_id === "string" ? body.agent_id.trim() : "";
|
|
3984
|
+
const githubLogin = typeof body.github_login === "string" ? body.github_login.trim() : "";
|
|
3985
|
+
if (!agentId || !githubLogin) {
|
|
3986
|
+
return reply.code(400).send({ error: "agent_id and github_login are required" });
|
|
3987
|
+
}
|
|
2308
3988
|
try {
|
|
2309
|
-
|
|
3989
|
+
const record = linkAgentToGuarantor(opts.creditDb, agentId, githubLogin);
|
|
3990
|
+
return reply.send({ guarantor: record });
|
|
2310
3991
|
} catch (err) {
|
|
2311
|
-
if (err instanceof AgentBnBError
|
|
2312
|
-
|
|
3992
|
+
if (err instanceof AgentBnBError) {
|
|
3993
|
+
const statusMap = {
|
|
3994
|
+
GUARANTOR_NOT_FOUND: 404,
|
|
3995
|
+
MAX_AGENTS_EXCEEDED: 409,
|
|
3996
|
+
AGENT_ALREADY_LINKED: 409
|
|
3997
|
+
};
|
|
3998
|
+
const status = statusMap[err.code] ?? 400;
|
|
3999
|
+
return reply.code(status).send({ error: err.message });
|
|
2313
4000
|
}
|
|
2314
4001
|
throw err;
|
|
2315
4002
|
}
|
|
2316
|
-
}
|
|
2317
|
-
return reply.code(201).send({ ok: true, id: card.id });
|
|
2318
|
-
});
|
|
2319
|
-
server.delete("/cards/:id", async (request, reply) => {
|
|
2320
|
-
const { id } = request.params;
|
|
2321
|
-
const card = getCard(db, id);
|
|
2322
|
-
if (!card) {
|
|
2323
|
-
return reply.code(404).send({ error: "Not found" });
|
|
2324
|
-
}
|
|
2325
|
-
db.prepare("DELETE FROM capability_cards WHERE id = ?").run(id);
|
|
2326
|
-
return reply.send({ ok: true, id });
|
|
2327
|
-
});
|
|
2328
|
-
server.get("/api/agents", async (_request, reply) => {
|
|
2329
|
-
const allCards = listCards(db);
|
|
2330
|
-
const ownerMap = /* @__PURE__ */ new Map();
|
|
2331
|
-
for (const card of allCards) {
|
|
2332
|
-
const existing = ownerMap.get(card.owner) ?? [];
|
|
2333
|
-
existing.push(card);
|
|
2334
|
-
ownerMap.set(card.owner, existing);
|
|
2335
|
-
}
|
|
2336
|
-
const creditsStmt = db.prepare(`
|
|
2337
|
-
SELECT cc.owner,
|
|
2338
|
-
SUM(CASE WHEN rl.status = 'success' THEN rl.credits_charged ELSE 0 END) as credits_earned
|
|
2339
|
-
FROM capability_cards cc
|
|
2340
|
-
LEFT JOIN request_log rl ON rl.card_id = cc.id
|
|
2341
|
-
GROUP BY cc.owner
|
|
2342
|
-
`);
|
|
2343
|
-
const creditsRows = creditsStmt.all();
|
|
2344
|
-
const creditsMap = new Map(creditsRows.map((r) => [r.owner, r.credits_earned ?? 0]));
|
|
2345
|
-
const agents = Array.from(ownerMap.entries()).map(([owner, cards]) => {
|
|
2346
|
-
const skillCount = cards.reduce((sum, card) => sum + (card.skills?.length ?? 1), 0);
|
|
2347
|
-
const successRates = cards.map((c) => c.metadata?.success_rate).filter((r) => r != null);
|
|
2348
|
-
const avgSuccessRate = successRates.length > 0 ? successRates.reduce((a, b) => a + b, 0) / successRates.length : null;
|
|
2349
|
-
const memberStmt = db.prepare(
|
|
2350
|
-
"SELECT MIN(created_at) as earliest FROM capability_cards WHERE owner = ?"
|
|
2351
|
-
);
|
|
2352
|
-
const memberRow = memberStmt.get(owner);
|
|
2353
|
-
return {
|
|
2354
|
-
owner,
|
|
2355
|
-
skill_count: skillCount,
|
|
2356
|
-
success_rate: avgSuccessRate,
|
|
2357
|
-
total_earned: creditsMap.get(owner) ?? 0,
|
|
2358
|
-
member_since: memberRow?.earliest ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2359
|
-
};
|
|
2360
|
-
});
|
|
2361
|
-
agents.sort((a, b) => {
|
|
2362
|
-
const aRate = a.success_rate ?? -1;
|
|
2363
|
-
const bRate = b.success_rate ?? -1;
|
|
2364
|
-
if (bRate !== aRate) return bRate - aRate;
|
|
2365
|
-
return b.total_earned - a.total_earned;
|
|
2366
|
-
});
|
|
2367
|
-
return reply.send({ items: agents, total: agents.length });
|
|
2368
|
-
});
|
|
2369
|
-
server.get("/api/agents/:owner", async (request, reply) => {
|
|
2370
|
-
const { owner } = request.params;
|
|
2371
|
-
const ownerCards = listCards(db, owner);
|
|
2372
|
-
if (ownerCards.length === 0) {
|
|
2373
|
-
return reply.status(404).send({ error: "Agent not found" });
|
|
2374
|
-
}
|
|
2375
|
-
const skillCount = ownerCards.reduce((sum, card) => sum + (card.skills?.length ?? 1), 0);
|
|
2376
|
-
const successRates = ownerCards.map((c) => c.metadata?.success_rate).filter((r) => r != null);
|
|
2377
|
-
const avgSuccessRate = successRates.length > 0 ? successRates.reduce((a, b) => a + b, 0) / successRates.length : null;
|
|
2378
|
-
const creditsStmt = db.prepare(`
|
|
2379
|
-
SELECT SUM(CASE WHEN rl.status = 'success' THEN rl.credits_charged ELSE 0 END) as credits_earned
|
|
2380
|
-
FROM capability_cards cc
|
|
2381
|
-
LEFT JOIN request_log rl ON rl.card_id = cc.id
|
|
2382
|
-
WHERE cc.owner = ?
|
|
2383
|
-
`);
|
|
2384
|
-
const creditsRow = creditsStmt.get(owner);
|
|
2385
|
-
const memberStmt = db.prepare(
|
|
2386
|
-
"SELECT MIN(created_at) as earliest FROM capability_cards WHERE owner = ?"
|
|
2387
|
-
);
|
|
2388
|
-
const memberRow = memberStmt.get(owner);
|
|
2389
|
-
const activityStmt = db.prepare(`
|
|
2390
|
-
SELECT rl.id, rl.card_name, rl.requester, rl.status, rl.credits_charged, rl.created_at
|
|
2391
|
-
FROM request_log rl
|
|
2392
|
-
INNER JOIN capability_cards cc ON rl.card_id = cc.id
|
|
2393
|
-
WHERE cc.owner = ?
|
|
2394
|
-
ORDER BY rl.created_at DESC
|
|
2395
|
-
LIMIT 10
|
|
2396
|
-
`);
|
|
2397
|
-
const recentActivity = activityStmt.all(owner);
|
|
2398
|
-
const profile = {
|
|
2399
|
-
owner,
|
|
2400
|
-
skill_count: skillCount,
|
|
2401
|
-
success_rate: avgSuccessRate,
|
|
2402
|
-
total_earned: creditsRow?.credits_earned ?? 0,
|
|
2403
|
-
member_since: memberRow?.earliest ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2404
|
-
};
|
|
2405
|
-
return reply.send({
|
|
2406
|
-
profile,
|
|
2407
|
-
skills: ownerCards,
|
|
2408
|
-
recent_activity: recentActivity
|
|
2409
4003
|
});
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
onlineOwners.add(owner);
|
|
4004
|
+
api.get("/api/identity/:agent_id", {
|
|
4005
|
+
schema: {
|
|
4006
|
+
tags: ["identity"],
|
|
4007
|
+
summary: "Get guarantor info for an agent",
|
|
4008
|
+
params: { type: "object", properties: { agent_id: { type: "string" } }, required: ["agent_id"] },
|
|
4009
|
+
response: {
|
|
4010
|
+
200: {
|
|
4011
|
+
type: "object",
|
|
4012
|
+
properties: {
|
|
4013
|
+
agent_id: { type: "string" },
|
|
4014
|
+
guarantor: { oneOf: [{ type: "object", additionalProperties: true }, { type: "null" }] }
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
2425
4018
|
}
|
|
2426
|
-
}
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
onlineOwners.add(card.owner);
|
|
4019
|
+
}, async (request, reply) => {
|
|
4020
|
+
if (!opts.creditDb) {
|
|
4021
|
+
return reply.code(503).send({ error: "Credit database not configured" });
|
|
2430
4022
|
}
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
);
|
|
2435
|
-
const exchangeRow = exchangeStmt.get();
|
|
2436
|
-
return reply.send({
|
|
2437
|
-
agents_online: onlineOwners.size,
|
|
2438
|
-
total_capabilities: allCards.reduce((sum, card) => {
|
|
2439
|
-
const v2 = card;
|
|
2440
|
-
return sum + (v2.skills?.length ?? 1);
|
|
2441
|
-
}, 0),
|
|
2442
|
-
total_exchanges: exchangeRow.count
|
|
4023
|
+
const { agent_id } = request.params;
|
|
4024
|
+
const guarantor = getAgentGuarantor(opts.creditDb, agent_id);
|
|
4025
|
+
return reply.send({ agent_id, guarantor });
|
|
2443
4026
|
});
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
const auth = initiateGithubAuth();
|
|
2457
|
-
return reply.code(201).send({ guarantor: record, oauth: auth });
|
|
2458
|
-
} catch (err) {
|
|
2459
|
-
if (err instanceof AgentBnBError && err.code === "GUARANTOR_EXISTS") {
|
|
2460
|
-
return reply.code(409).send({ error: err.message });
|
|
2461
|
-
}
|
|
2462
|
-
throw err;
|
|
2463
|
-
}
|
|
2464
|
-
});
|
|
2465
|
-
server.post("/api/identity/link", async (request, reply) => {
|
|
2466
|
-
if (!opts.creditDb) {
|
|
2467
|
-
return reply.code(503).send({ error: "Credit database not configured" });
|
|
2468
|
-
}
|
|
2469
|
-
const body = request.body;
|
|
2470
|
-
const agentId = typeof body.agent_id === "string" ? body.agent_id.trim() : "";
|
|
2471
|
-
const githubLogin = typeof body.github_login === "string" ? body.github_login.trim() : "";
|
|
2472
|
-
if (!agentId || !githubLogin) {
|
|
2473
|
-
return reply.code(400).send({ error: "agent_id and github_login are required" });
|
|
2474
|
-
}
|
|
2475
|
-
try {
|
|
2476
|
-
const record = linkAgentToGuarantor(opts.creditDb, agentId, githubLogin);
|
|
2477
|
-
return reply.send({ guarantor: record });
|
|
2478
|
-
} catch (err) {
|
|
2479
|
-
if (err instanceof AgentBnBError) {
|
|
2480
|
-
const statusMap = {
|
|
2481
|
-
GUARANTOR_NOT_FOUND: 404,
|
|
2482
|
-
MAX_AGENTS_EXCEEDED: 409,
|
|
2483
|
-
AGENT_ALREADY_LINKED: 409
|
|
2484
|
-
};
|
|
2485
|
-
const status = statusMap[err.code] ?? 400;
|
|
2486
|
-
return reply.code(status).send({ error: err.message });
|
|
4027
|
+
api.get("/api/openapi/gpt-actions", {
|
|
4028
|
+
schema: {
|
|
4029
|
+
tags: ["system"],
|
|
4030
|
+
summary: "GPT Actions-compatible OpenAPI schema",
|
|
4031
|
+
description: "Returns a GPT Builder-importable OpenAPI spec with only public GET/POST endpoints",
|
|
4032
|
+
querystring: {
|
|
4033
|
+
type: "object",
|
|
4034
|
+
properties: {
|
|
4035
|
+
server_url: { type: "string", description: "Base URL for the server (required for absolute URLs in GPT Actions)" }
|
|
4036
|
+
}
|
|
4037
|
+
},
|
|
4038
|
+
response: { 200: { type: "object", additionalProperties: true } }
|
|
2487
4039
|
}
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
return reply.
|
|
2494
|
-
}
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
const auth = request.headers.authorization;
|
|
2505
|
-
const token = auth?.startsWith("Bearer ") ? auth.slice(7).trim() : null;
|
|
2506
|
-
if (!token || token !== ownerApiKey) {
|
|
2507
|
-
return reply.status(401).send({ error: "Unauthorized" });
|
|
2508
|
-
}
|
|
2509
|
-
});
|
|
2510
|
-
ownerRoutes.get("/me", async (_request, reply) => {
|
|
2511
|
-
const balance = opts.creditDb ? getBalance(opts.creditDb, ownerName) : 0;
|
|
2512
|
-
return reply.send({ owner: ownerName, balance });
|
|
2513
|
-
});
|
|
2514
|
-
ownerRoutes.get("/requests", async (request, reply) => {
|
|
2515
|
-
const query = request.query;
|
|
2516
|
-
const rawLimit = query.limit !== void 0 ? parseInt(query.limit, 10) : 10;
|
|
2517
|
-
const limit = Math.min(isNaN(rawLimit) || rawLimit < 1 ? 10 : rawLimit, 100);
|
|
2518
|
-
const sinceRaw = query.since;
|
|
2519
|
-
const validSince = ["24h", "7d", "30d"];
|
|
2520
|
-
const since = sinceRaw && validSince.includes(sinceRaw) ? sinceRaw : void 0;
|
|
2521
|
-
const items = getRequestLog(db, limit, since);
|
|
2522
|
-
return reply.send({ items, limit });
|
|
2523
|
-
});
|
|
2524
|
-
ownerRoutes.get("/draft", async (_request, reply) => {
|
|
2525
|
-
const detectedKeys = detectApiKeys(KNOWN_API_KEYS);
|
|
2526
|
-
const cards = detectedKeys.map((key) => buildDraftCard(key, ownerName)).filter((card) => card !== null);
|
|
2527
|
-
return reply.send({ cards });
|
|
2528
|
-
});
|
|
2529
|
-
ownerRoutes.post("/cards/:id/toggle-online", async (request, reply) => {
|
|
2530
|
-
const { id } = request.params;
|
|
2531
|
-
const card = getCard(db, id);
|
|
2532
|
-
if (!card) {
|
|
2533
|
-
return reply.code(404).send({ error: "Not found" });
|
|
2534
|
-
}
|
|
2535
|
-
try {
|
|
2536
|
-
const newOnline = !card.availability.online;
|
|
2537
|
-
updateCard(db, id, ownerName, {
|
|
2538
|
-
availability: { ...card.availability, online: newOnline }
|
|
2539
|
-
});
|
|
2540
|
-
return reply.send({ ok: true, online: newOnline });
|
|
2541
|
-
} catch (err) {
|
|
2542
|
-
if (err instanceof AgentBnBError && err.code === "FORBIDDEN") {
|
|
2543
|
-
return reply.code(403).send({ error: "Forbidden" });
|
|
4040
|
+
}, async (request, reply) => {
|
|
4041
|
+
const query = request.query;
|
|
4042
|
+
const serverUrl = query.server_url?.trim() || `${request.protocol}://${request.hostname}`;
|
|
4043
|
+
const openapiSpec = server.swagger();
|
|
4044
|
+
const gptActions = convertToGptActions(openapiSpec, serverUrl);
|
|
4045
|
+
return reply.send(gptActions);
|
|
4046
|
+
});
|
|
4047
|
+
if (opts.ownerApiKey && opts.ownerName) {
|
|
4048
|
+
const ownerApiKey = opts.ownerApiKey;
|
|
4049
|
+
const ownerName = opts.ownerName;
|
|
4050
|
+
void api.register(async (ownerRoutes) => {
|
|
4051
|
+
ownerRoutes.addHook("onRequest", async (request, reply) => {
|
|
4052
|
+
const auth = request.headers.authorization;
|
|
4053
|
+
const token = auth?.startsWith("Bearer ") ? auth.slice(7).trim() : null;
|
|
4054
|
+
if (!token || token !== ownerApiKey) {
|
|
4055
|
+
return reply.status(401).send({ error: "Unauthorized" });
|
|
2544
4056
|
}
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
4057
|
+
});
|
|
4058
|
+
ownerRoutes.get("/me", {
|
|
4059
|
+
schema: {
|
|
4060
|
+
tags: ["owner"],
|
|
4061
|
+
summary: "Get owner identity and credit balance",
|
|
4062
|
+
security: [{ bearerAuth: [] }],
|
|
4063
|
+
response: {
|
|
4064
|
+
200: { type: "object", properties: { owner: { type: "string" }, balance: { type: "number" } } }
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
}, async (_request, reply) => {
|
|
4068
|
+
let balance = 0;
|
|
4069
|
+
if (opts.creditDb) {
|
|
4070
|
+
const ledger = createLedger({ db: opts.creditDb });
|
|
4071
|
+
balance = await ledger.getBalance(ownerName);
|
|
4072
|
+
}
|
|
4073
|
+
return reply.send({ owner: ownerName, balance });
|
|
4074
|
+
});
|
|
4075
|
+
ownerRoutes.get("/requests", {
|
|
4076
|
+
schema: {
|
|
4077
|
+
tags: ["owner"],
|
|
4078
|
+
summary: "Paginated request log entries",
|
|
4079
|
+
security: [{ bearerAuth: [] }],
|
|
4080
|
+
querystring: {
|
|
4081
|
+
type: "object",
|
|
4082
|
+
properties: {
|
|
4083
|
+
limit: { type: "integer", description: "Max entries (default 10, max 100)" },
|
|
4084
|
+
since: { type: "string", enum: ["24h", "7d", "30d"], description: "Time window" }
|
|
4085
|
+
}
|
|
4086
|
+
},
|
|
4087
|
+
response: { 200: { type: "object", properties: { items: { type: "array" }, limit: { type: "integer" } } } }
|
|
4088
|
+
}
|
|
4089
|
+
}, async (request, reply) => {
|
|
4090
|
+
const query = request.query;
|
|
4091
|
+
const rawLimit = query.limit !== void 0 ? parseInt(query.limit, 10) : 10;
|
|
4092
|
+
const limit = Math.min(isNaN(rawLimit) || rawLimit < 1 ? 10 : rawLimit, 100);
|
|
4093
|
+
const sinceRaw = query.since;
|
|
4094
|
+
const validSince = ["24h", "7d", "30d"];
|
|
4095
|
+
const since = sinceRaw && validSince.includes(sinceRaw) ? sinceRaw : void 0;
|
|
4096
|
+
const items = getRequestLog(db, limit, since);
|
|
4097
|
+
return reply.send({ items, limit });
|
|
4098
|
+
});
|
|
4099
|
+
ownerRoutes.get("/draft", {
|
|
4100
|
+
schema: {
|
|
4101
|
+
tags: ["owner"],
|
|
4102
|
+
summary: "Draft capability cards from auto-detected API keys",
|
|
4103
|
+
security: [{ bearerAuth: [] }],
|
|
4104
|
+
response: { 200: { type: "object", properties: { cards: { type: "array" } } } }
|
|
4105
|
+
}
|
|
4106
|
+
}, async (_request, reply) => {
|
|
4107
|
+
const detectedKeys = detectApiKeys(KNOWN_API_KEYS);
|
|
4108
|
+
const cards = detectedKeys.map((key) => buildDraftCard(key, ownerName)).filter((card) => card !== null);
|
|
4109
|
+
return reply.send({ cards });
|
|
4110
|
+
});
|
|
4111
|
+
ownerRoutes.post("/cards/:id/toggle-online", {
|
|
4112
|
+
schema: {
|
|
4113
|
+
tags: ["owner"],
|
|
4114
|
+
summary: "Toggle card online/offline status",
|
|
4115
|
+
security: [{ bearerAuth: [] }],
|
|
4116
|
+
params: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
4117
|
+
response: {
|
|
4118
|
+
200: { type: "object", properties: { ok: { type: "boolean" }, online: { type: "boolean" } } },
|
|
4119
|
+
404: { type: "object", properties: { error: { type: "string" } } }
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
}, async (request, reply) => {
|
|
4123
|
+
const { id } = request.params;
|
|
4124
|
+
const card = getCard(db, id);
|
|
4125
|
+
if (!card) {
|
|
4126
|
+
return reply.code(404).send({ error: "Not found" });
|
|
4127
|
+
}
|
|
4128
|
+
try {
|
|
4129
|
+
const newOnline = !card.availability.online;
|
|
4130
|
+
updateCard(db, id, ownerName, {
|
|
4131
|
+
availability: { ...card.availability, online: newOnline }
|
|
4132
|
+
});
|
|
4133
|
+
return reply.send({ ok: true, online: newOnline });
|
|
4134
|
+
} catch (err) {
|
|
4135
|
+
if (err instanceof AgentBnBError && err.code === "FORBIDDEN") {
|
|
2560
4136
|
return reply.code(403).send({ error: "Forbidden" });
|
|
2561
4137
|
}
|
|
2562
|
-
|
|
2563
|
-
|
|
4138
|
+
throw err;
|
|
4139
|
+
}
|
|
4140
|
+
});
|
|
4141
|
+
ownerRoutes.patch("/cards/:id", {
|
|
4142
|
+
schema: {
|
|
4143
|
+
tags: ["owner"],
|
|
4144
|
+
summary: "Update card description or pricing",
|
|
4145
|
+
security: [{ bearerAuth: [] }],
|
|
4146
|
+
params: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
4147
|
+
body: {
|
|
4148
|
+
type: "object",
|
|
4149
|
+
properties: {
|
|
4150
|
+
description: { type: "string" },
|
|
4151
|
+
pricing: { type: "object", additionalProperties: true }
|
|
4152
|
+
},
|
|
4153
|
+
additionalProperties: true
|
|
4154
|
+
},
|
|
4155
|
+
response: {
|
|
4156
|
+
200: { type: "object", properties: { ok: { type: "boolean" } } },
|
|
4157
|
+
403: { type: "object", properties: { error: { type: "string" } } },
|
|
4158
|
+
404: { type: "object", properties: { error: { type: "string" } } }
|
|
2564
4159
|
}
|
|
2565
4160
|
}
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
4161
|
+
}, async (request, reply) => {
|
|
4162
|
+
const { id } = request.params;
|
|
4163
|
+
const body = request.body;
|
|
4164
|
+
const updates = {};
|
|
4165
|
+
if (body.description !== void 0) updates.description = body.description;
|
|
4166
|
+
if (body.pricing !== void 0) updates.pricing = body.pricing;
|
|
4167
|
+
try {
|
|
4168
|
+
updateCard(db, id, ownerName, updates);
|
|
4169
|
+
return reply.send({ ok: true });
|
|
4170
|
+
} catch (err) {
|
|
4171
|
+
if (err instanceof AgentBnBError) {
|
|
4172
|
+
if (err.code === "FORBIDDEN") {
|
|
4173
|
+
return reply.code(403).send({ error: "Forbidden" });
|
|
4174
|
+
}
|
|
4175
|
+
if (err.code === "NOT_FOUND") {
|
|
4176
|
+
return reply.code(404).send({ error: "Not found" });
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
throw err;
|
|
4180
|
+
}
|
|
4181
|
+
});
|
|
4182
|
+
ownerRoutes.get("/me/pending-requests", {
|
|
4183
|
+
schema: {
|
|
4184
|
+
tags: ["owner"],
|
|
4185
|
+
summary: "List pending Tier 3 approval queue entries",
|
|
4186
|
+
security: [{ bearerAuth: [] }],
|
|
4187
|
+
response: { 200: { type: "array" } }
|
|
4188
|
+
}
|
|
4189
|
+
}, async (_request, reply) => {
|
|
4190
|
+
const rows = listPendingRequests(db);
|
|
4191
|
+
return reply.send(rows);
|
|
4192
|
+
});
|
|
4193
|
+
ownerRoutes.post("/me/pending-requests/:id/approve", {
|
|
4194
|
+
schema: {
|
|
4195
|
+
tags: ["owner"],
|
|
4196
|
+
summary: "Approve a pending Tier 3 request",
|
|
4197
|
+
security: [{ bearerAuth: [] }],
|
|
4198
|
+
params: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
4199
|
+
response: {
|
|
4200
|
+
200: { type: "object", properties: { status: { type: "string" }, id: { type: "string" } } },
|
|
4201
|
+
404: { type: "object", properties: { error: { type: "string" } } }
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
}, async (request, reply) => {
|
|
4205
|
+
const { id } = request.params;
|
|
4206
|
+
try {
|
|
4207
|
+
resolvePendingRequest(db, id, "approved");
|
|
4208
|
+
return reply.send({ status: "approved", id });
|
|
4209
|
+
} catch (err) {
|
|
4210
|
+
if (err instanceof AgentBnBError) return reply.status(404).send({ error: err.message });
|
|
4211
|
+
throw err;
|
|
4212
|
+
}
|
|
4213
|
+
});
|
|
4214
|
+
ownerRoutes.post("/me/pending-requests/:id/reject", {
|
|
4215
|
+
schema: {
|
|
4216
|
+
tags: ["owner"],
|
|
4217
|
+
summary: "Reject a pending Tier 3 request",
|
|
4218
|
+
security: [{ bearerAuth: [] }],
|
|
4219
|
+
params: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
4220
|
+
response: {
|
|
4221
|
+
200: { type: "object", properties: { status: { type: "string" }, id: { type: "string" } } },
|
|
4222
|
+
404: { type: "object", properties: { error: { type: "string" } } }
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
}, async (request, reply) => {
|
|
4226
|
+
const { id } = request.params;
|
|
4227
|
+
try {
|
|
4228
|
+
resolvePendingRequest(db, id, "rejected");
|
|
4229
|
+
return reply.send({ status: "rejected", id });
|
|
4230
|
+
} catch (err) {
|
|
4231
|
+
if (err instanceof AgentBnBError) return reply.status(404).send({ error: err.message });
|
|
4232
|
+
throw err;
|
|
4233
|
+
}
|
|
4234
|
+
});
|
|
4235
|
+
ownerRoutes.get("/me/transactions", {
|
|
4236
|
+
schema: {
|
|
4237
|
+
tags: ["owner"],
|
|
4238
|
+
summary: "Paginated credit transaction history",
|
|
4239
|
+
security: [{ bearerAuth: [] }],
|
|
4240
|
+
querystring: {
|
|
4241
|
+
type: "object",
|
|
4242
|
+
properties: { limit: { type: "integer", description: "Max entries (default 20, max 100)" } }
|
|
4243
|
+
},
|
|
4244
|
+
response: {
|
|
4245
|
+
200: { type: "object", properties: { items: { type: "array" }, limit: { type: "integer" } } }
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
}, async (request, reply) => {
|
|
4249
|
+
const query = request.query;
|
|
4250
|
+
const rawLimit = query.limit !== void 0 ? parseInt(query.limit, 10) : 20;
|
|
4251
|
+
const limit = Math.min(isNaN(rawLimit) || rawLimit < 1 ? 20 : rawLimit, 100);
|
|
4252
|
+
if (!opts.creditDb) {
|
|
4253
|
+
return reply.send({ items: [], limit });
|
|
4254
|
+
}
|
|
4255
|
+
const ledger = createLedger({ db: opts.creditDb });
|
|
4256
|
+
const items = await ledger.getHistory(ownerName, limit);
|
|
4257
|
+
return reply.send({ items, limit });
|
|
4258
|
+
});
|
|
2602
4259
|
});
|
|
2603
|
-
}
|
|
2604
|
-
}
|
|
4260
|
+
}
|
|
4261
|
+
});
|
|
2605
4262
|
return { server, relayState };
|
|
2606
4263
|
}
|
|
2607
4264
|
|
|
@@ -2676,10 +4333,10 @@ async function stopAnnouncement() {
|
|
|
2676
4333
|
}
|
|
2677
4334
|
|
|
2678
4335
|
// src/openclaw/soul-sync.ts
|
|
2679
|
-
import { randomUUID as
|
|
4336
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
2680
4337
|
|
|
2681
4338
|
// src/skills/publish-capability.ts
|
|
2682
|
-
import { randomUUID as
|
|
4339
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
2683
4340
|
function parseSoulMd(content) {
|
|
2684
4341
|
const lines = content.split("\n");
|
|
2685
4342
|
let name = "";
|
|
@@ -2689,17 +4346,23 @@ function parseSoulMd(content) {
|
|
|
2689
4346
|
let currentSection = null;
|
|
2690
4347
|
let currentCapabilityName = "";
|
|
2691
4348
|
let currentCapabilityLines = [];
|
|
4349
|
+
let currentCapabilityPricing = void 0;
|
|
2692
4350
|
let descriptionLines = [];
|
|
2693
4351
|
let pastFirstH1 = false;
|
|
2694
4352
|
let pastFirstH2 = false;
|
|
2695
4353
|
const flushCapability = () => {
|
|
2696
4354
|
if (currentCapabilityName) {
|
|
2697
|
-
|
|
4355
|
+
const cap = {
|
|
2698
4356
|
name: currentCapabilityName,
|
|
2699
4357
|
description: currentCapabilityLines.join(" ").trim()
|
|
2700
|
-
}
|
|
4358
|
+
};
|
|
4359
|
+
if (currentCapabilityPricing !== void 0) {
|
|
4360
|
+
cap.pricing = currentCapabilityPricing;
|
|
4361
|
+
}
|
|
4362
|
+
capabilities.push(cap);
|
|
2701
4363
|
currentCapabilityName = "";
|
|
2702
4364
|
currentCapabilityLines = [];
|
|
4365
|
+
currentCapabilityPricing = void 0;
|
|
2703
4366
|
}
|
|
2704
4367
|
};
|
|
2705
4368
|
for (const line of lines) {
|
|
@@ -2729,7 +4392,15 @@ function parseSoulMd(content) {
|
|
|
2729
4392
|
if (currentSection === "preamble" && !pastFirstH2) {
|
|
2730
4393
|
descriptionLines.push(trimmed);
|
|
2731
4394
|
} else if (currentSection === "capability") {
|
|
2732
|
-
|
|
4395
|
+
const pricingMatch = trimmed.match(/^pricing:\s*(\d+(?:\.\d+)?)$/i);
|
|
4396
|
+
if (pricingMatch) {
|
|
4397
|
+
const val = parseFloat(pricingMatch[1]);
|
|
4398
|
+
if (!isNaN(val) && val >= 0) {
|
|
4399
|
+
currentCapabilityPricing = val;
|
|
4400
|
+
}
|
|
4401
|
+
} else {
|
|
4402
|
+
currentCapabilityLines.push(trimmed);
|
|
4403
|
+
}
|
|
2733
4404
|
}
|
|
2734
4405
|
}
|
|
2735
4406
|
flushCapability();
|
|
@@ -2750,7 +4421,7 @@ function parseSoulMdV2(content) {
|
|
|
2750
4421
|
const parsed = parseSoulMd(content);
|
|
2751
4422
|
const skills = parsed.capabilities.map((cap) => {
|
|
2752
4423
|
const sanitizedId = cap.name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
2753
|
-
const id = sanitizedId.length > 0 ? sanitizedId :
|
|
4424
|
+
const id = sanitizedId.length > 0 ? sanitizedId : randomUUID9();
|
|
2754
4425
|
return {
|
|
2755
4426
|
id,
|
|
2756
4427
|
name: cap.name,
|
|
@@ -2772,7 +4443,7 @@ function parseSoulMdV2(content) {
|
|
|
2772
4443
|
required: true
|
|
2773
4444
|
}
|
|
2774
4445
|
],
|
|
2775
|
-
pricing: { credits_per_call: 10 },
|
|
4446
|
+
pricing: { credits_per_call: cap.pricing !== void 0 ? cap.pricing : 10 },
|
|
2776
4447
|
availability: { online: true }
|
|
2777
4448
|
};
|
|
2778
4449
|
});
|
|
@@ -2792,7 +4463,7 @@ function publishFromSoulV2(db, soulContent, owner) {
|
|
|
2792
4463
|
(c) => c.spec_version === "2.0"
|
|
2793
4464
|
);
|
|
2794
4465
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2795
|
-
const cardId = existingV2?.id ??
|
|
4466
|
+
const cardId = existingV2?.id ?? randomUUID9();
|
|
2796
4467
|
const card = {
|
|
2797
4468
|
spec_version: "2.0",
|
|
2798
4469
|
id: cardId,
|
|
@@ -2817,7 +4488,7 @@ function publishFromSoulV2(db, soulContent, owner) {
|
|
|
2817
4488
|
}
|
|
2818
4489
|
|
|
2819
4490
|
// src/openclaw/heartbeat-writer.ts
|
|
2820
|
-
import { readFileSync as
|
|
4491
|
+
import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync4 } from "fs";
|
|
2821
4492
|
var HEARTBEAT_MARKER_START = "<!-- agentbnb:start -->";
|
|
2822
4493
|
var HEARTBEAT_MARKER_END = "<!-- agentbnb:end -->";
|
|
2823
4494
|
function generateHeartbeatSection(autonomy, budget) {
|
|
@@ -2853,11 +4524,11 @@ function generateHeartbeatSection(autonomy, budget) {
|
|
|
2853
4524
|
].join("\n");
|
|
2854
4525
|
}
|
|
2855
4526
|
function injectHeartbeatSection(heartbeatPath, section) {
|
|
2856
|
-
if (!
|
|
2857
|
-
|
|
4527
|
+
if (!existsSync4(heartbeatPath)) {
|
|
4528
|
+
writeFileSync(heartbeatPath, section + "\n", "utf-8");
|
|
2858
4529
|
return;
|
|
2859
4530
|
}
|
|
2860
|
-
let content =
|
|
4531
|
+
let content = readFileSync3(heartbeatPath, "utf-8");
|
|
2861
4532
|
const startIdx = content.indexOf(HEARTBEAT_MARKER_START);
|
|
2862
4533
|
const endIdx = content.indexOf(HEARTBEAT_MARKER_END);
|
|
2863
4534
|
if (startIdx !== -1 && endIdx !== -1) {
|
|
@@ -2865,7 +4536,7 @@ function injectHeartbeatSection(heartbeatPath, section) {
|
|
|
2865
4536
|
} else {
|
|
2866
4537
|
content = content + "\n" + section + "\n";
|
|
2867
4538
|
}
|
|
2868
|
-
|
|
4539
|
+
writeFileSync(heartbeatPath, content, "utf-8");
|
|
2869
4540
|
}
|
|
2870
4541
|
|
|
2871
4542
|
// src/openclaw/skill.ts
|
|
@@ -2945,11 +4616,11 @@ function getLanIp() {
|
|
|
2945
4616
|
var program = new Command();
|
|
2946
4617
|
program.name("agentbnb").description("P2P Agent Capability Sharing Protocol \u2014 Airbnb for AI agent pipelines").version(pkg.version);
|
|
2947
4618
|
program.command("init").description("Initialize AgentBnB config and create agent identity").option("--owner <name>", "Agent owner name").option("--port <port>", "Gateway port", "7700").option("--host <ip>", "Override gateway host IP (default: auto-detected LAN IP)").option("--yes", "Auto-confirm all draft cards (non-interactive)").option("--no-detect", "Skip API key detection").option("--from <file>", "Parse a specific file for capability detection").option("--json", "Output as JSON").action(async (opts) => {
|
|
2948
|
-
const owner = opts.owner ?? `agent-${
|
|
2949
|
-
const token =
|
|
4619
|
+
const owner = opts.owner ?? `agent-${randomBytes2(4).toString("hex")}`;
|
|
4620
|
+
const token = randomBytes2(32).toString("hex");
|
|
2950
4621
|
const configDir = getConfigDir();
|
|
2951
|
-
const dbPath =
|
|
2952
|
-
const creditDbPath =
|
|
4622
|
+
const dbPath = join3(configDir, "registry.db");
|
|
4623
|
+
const creditDbPath = join3(configDir, "credit.db");
|
|
2953
4624
|
const port = parseInt(opts.port, 10);
|
|
2954
4625
|
const ip = opts.host ?? getLanIp();
|
|
2955
4626
|
const existingConfig = loadConfig();
|
|
@@ -2963,7 +4634,7 @@ program.command("init").description("Initialize AgentBnB config and create agent
|
|
|
2963
4634
|
credit_db_path: creditDbPath,
|
|
2964
4635
|
token: existingConfig?.token ?? token,
|
|
2965
4636
|
// Preserve existing token
|
|
2966
|
-
api_key: existingConfig?.api_key ??
|
|
4637
|
+
api_key: existingConfig?.api_key ?? randomBytes2(32).toString("hex")
|
|
2967
4638
|
};
|
|
2968
4639
|
saveConfig(config);
|
|
2969
4640
|
let keypairStatus = "existing";
|
|
@@ -2978,6 +4649,21 @@ program.command("init").description("Initialize AgentBnB config and create agent
|
|
|
2978
4649
|
const creditDb = openCreditDb(creditDbPath);
|
|
2979
4650
|
bootstrapAgent(creditDb, owner, 100);
|
|
2980
4651
|
creditDb.close();
|
|
4652
|
+
let registryBalance;
|
|
4653
|
+
if (existingConfig?.registry) {
|
|
4654
|
+
try {
|
|
4655
|
+
const identityAuth = loadIdentityAuth(owner);
|
|
4656
|
+
const ledger = createLedger({
|
|
4657
|
+
registryUrl: existingConfig.registry,
|
|
4658
|
+
ownerPublicKey: identityAuth.publicKey,
|
|
4659
|
+
privateKey: identityAuth.privateKey
|
|
4660
|
+
});
|
|
4661
|
+
await ledger.grant(owner, 50);
|
|
4662
|
+
registryBalance = await ledger.getBalance(owner);
|
|
4663
|
+
} catch (err) {
|
|
4664
|
+
console.warn(`Warning: could not connect to Registry for credit grant: ${err.message}`);
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
2981
4667
|
const skipDetect = opts.detect === false;
|
|
2982
4668
|
const publishedCards = [];
|
|
2983
4669
|
let detectedSource = "none";
|
|
@@ -3123,6 +4809,9 @@ Publish these ${card.skills.length} capabilities? [y/N] `);
|
|
|
3123
4809
|
keypair: keypairStatus,
|
|
3124
4810
|
agent_id: identity.agent_id
|
|
3125
4811
|
};
|
|
4812
|
+
if (registryBalance !== void 0) {
|
|
4813
|
+
jsonOutput.registry_balance = registryBalance;
|
|
4814
|
+
}
|
|
3126
4815
|
if (!skipDetect) {
|
|
3127
4816
|
jsonOutput.detected_source = detectedSource;
|
|
3128
4817
|
jsonOutput.published_cards = publishedCards;
|
|
@@ -3133,7 +4822,11 @@ Publish these ${card.skills.length} capabilities? [y/N] `);
|
|
|
3133
4822
|
console.log(` Owner: ${owner}`);
|
|
3134
4823
|
console.log(` Token: ${token}`);
|
|
3135
4824
|
console.log(` Config: ${configDir}/config.json`);
|
|
3136
|
-
|
|
4825
|
+
if (registryBalance !== void 0) {
|
|
4826
|
+
console.log(` Registry balance: ${registryBalance} credits`);
|
|
4827
|
+
} else {
|
|
4828
|
+
console.log(` Credits: 100 (starter grant)`);
|
|
4829
|
+
}
|
|
3137
4830
|
console.log(` Keypair: ${keypairStatus === "generated" ? "generated (Ed25519)" : "preserved (existing)"}`);
|
|
3138
4831
|
console.log(` Agent ID: ${identity.agent_id}`);
|
|
3139
4832
|
console.log(` Gateway: http://${ip}:${port}`);
|
|
@@ -3147,7 +4840,7 @@ program.command("publish <card.json>").description("Publish a Capability Card to
|
|
|
3147
4840
|
}
|
|
3148
4841
|
let raw;
|
|
3149
4842
|
try {
|
|
3150
|
-
raw =
|
|
4843
|
+
raw = readFileSync4(cardPath, "utf-8");
|
|
3151
4844
|
} catch {
|
|
3152
4845
|
console.error(`Error: cannot read file: ${cardPath}`);
|
|
3153
4846
|
process.exit(1);
|
|
@@ -3176,6 +4869,27 @@ program.command("publish <card.json>").description("Publish a Capability Card to
|
|
|
3176
4869
|
}
|
|
3177
4870
|
const card = result.data;
|
|
3178
4871
|
const cardName = card.spec_version === "2.0" ? card.agent_name : card.name;
|
|
4872
|
+
if (card.spec_version === "2.0") {
|
|
4873
|
+
const v2card = card;
|
|
4874
|
+
const invalidSkill = v2card.skills?.find((s) => s.pricing.credits_per_call < 1);
|
|
4875
|
+
if (invalidSkill) {
|
|
4876
|
+
if (opts.json) {
|
|
4877
|
+
console.log(JSON.stringify({ success: false, error: "Minimum price is 1 credit per call", skill_id: invalidSkill.id }, null, 2));
|
|
4878
|
+
} else {
|
|
4879
|
+
console.error(`Error: Minimum price is 1 credit per call (skill "${invalidSkill.id}" has credits_per_call=${invalidSkill.pricing.credits_per_call})`);
|
|
4880
|
+
}
|
|
4881
|
+
process.exit(1);
|
|
4882
|
+
}
|
|
4883
|
+
} else {
|
|
4884
|
+
if (card.pricing.credits_per_call < 1) {
|
|
4885
|
+
if (opts.json) {
|
|
4886
|
+
console.log(JSON.stringify({ success: false, error: "Minimum price is 1 credit per call" }, null, 2));
|
|
4887
|
+
} else {
|
|
4888
|
+
console.error(`Error: Minimum price is 1 credit per call (card has credits_per_call=${card.pricing.credits_per_call})`);
|
|
4889
|
+
}
|
|
4890
|
+
process.exit(1);
|
|
4891
|
+
}
|
|
4892
|
+
}
|
|
3179
4893
|
const db = openDatabase(config.db_path);
|
|
3180
4894
|
try {
|
|
3181
4895
|
if (card.spec_version === "2.0") {
|
|
@@ -3431,8 +5145,8 @@ program.command("request [card-id]").description("Request a capability from anot
|
|
|
3431
5145
|
process.exit(1);
|
|
3432
5146
|
}
|
|
3433
5147
|
}
|
|
3434
|
-
const registryDb = openDatabase(
|
|
3435
|
-
const creditDb = openCreditDb(
|
|
5148
|
+
const registryDb = openDatabase(join3(getConfigDir(), "registry.db"));
|
|
5149
|
+
const creditDb = openCreditDb(join3(getConfigDir(), "credit.db"));
|
|
3436
5150
|
registryDb.pragma("busy_timeout = 5000");
|
|
3437
5151
|
creditDb.pragma("busy_timeout = 5000");
|
|
3438
5152
|
try {
|
|
@@ -3442,7 +5156,8 @@ program.command("request [card-id]").description("Request a capability from anot
|
|
|
3442
5156
|
registryDb,
|
|
3443
5157
|
creditDb,
|
|
3444
5158
|
autonomyConfig: config.autonomy ?? DEFAULT_AUTONOMY_CONFIG,
|
|
3445
|
-
budgetManager
|
|
5159
|
+
budgetManager,
|
|
5160
|
+
registryUrl: config.registry
|
|
3446
5161
|
});
|
|
3447
5162
|
const result = await requestor.requestWithAutonomy({
|
|
3448
5163
|
query: opts.query,
|
|
@@ -3535,68 +5250,105 @@ program.command("request [card-id]").description("Request a capability from anot
|
|
|
3535
5250
|
}
|
|
3536
5251
|
}
|
|
3537
5252
|
const useReceipt = isRemoteRequest && opts.receipt !== false;
|
|
5253
|
+
const useRegistryLedger = isRemoteRequest && !!config.registry && !!gatewayUrl;
|
|
3538
5254
|
if (useReceipt && !opts.cost) {
|
|
3539
5255
|
console.error("Error: --cost <credits> is required for remote requests. Specify the credits to commit.");
|
|
3540
5256
|
process.exit(1);
|
|
3541
5257
|
}
|
|
3542
5258
|
let escrowId;
|
|
3543
5259
|
let escrowReceipt;
|
|
5260
|
+
let requestLedger;
|
|
3544
5261
|
if (useReceipt) {
|
|
3545
|
-
const
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
5262
|
+
const amount = Number(opts.cost);
|
|
5263
|
+
if (isNaN(amount) || amount <= 0) {
|
|
5264
|
+
console.error("Error: --cost must be a positive number.");
|
|
5265
|
+
process.exit(1);
|
|
5266
|
+
}
|
|
5267
|
+
if (useRegistryLedger) {
|
|
5268
|
+
const reqIdentityAuth = loadIdentityAuth(config.owner);
|
|
5269
|
+
requestLedger = createLedger({
|
|
5270
|
+
registryUrl: config.registry,
|
|
5271
|
+
ownerPublicKey: reqIdentityAuth.publicKey,
|
|
5272
|
+
privateKey: reqIdentityAuth.privateKey
|
|
5273
|
+
});
|
|
5274
|
+
try {
|
|
5275
|
+
const { escrowId: heldId } = await requestLedger.hold(config.owner, amount, cardId);
|
|
5276
|
+
escrowId = heldId;
|
|
5277
|
+
if (!opts.json) {
|
|
5278
|
+
console.log(`Escrow: ${amount} credits held via Registry (ID: ${escrowId.slice(0, 8)}...)`);
|
|
5279
|
+
}
|
|
5280
|
+
} catch (err) {
|
|
5281
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5282
|
+
if (opts.json) {
|
|
5283
|
+
console.log(JSON.stringify({ success: false, error: msg }, null, 2));
|
|
5284
|
+
} else {
|
|
5285
|
+
console.error(`Error creating escrow via Registry: ${msg}`);
|
|
5286
|
+
}
|
|
3553
5287
|
process.exit(1);
|
|
3554
5288
|
}
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
5289
|
+
} else if (gatewayUrl) {
|
|
5290
|
+
const configDir = getConfigDir();
|
|
5291
|
+
const creditDb = openCreditDb(join3(configDir, "credit.db"));
|
|
5292
|
+
creditDb.pragma("busy_timeout = 5000");
|
|
5293
|
+
try {
|
|
5294
|
+
const keys = loadKeyPair(configDir);
|
|
5295
|
+
const receiptResult = createSignedEscrowReceipt(creditDb, keys.privateKey, keys.publicKey, {
|
|
5296
|
+
owner: config.owner,
|
|
5297
|
+
amount,
|
|
5298
|
+
cardId,
|
|
5299
|
+
skillId: opts.skill
|
|
5300
|
+
});
|
|
5301
|
+
escrowId = receiptResult.escrowId;
|
|
5302
|
+
escrowReceipt = receiptResult.receipt;
|
|
5303
|
+
if (!opts.json) {
|
|
5304
|
+
console.log(`Escrow: ${amount} credits held (ID: ${escrowId.slice(0, 8)}...)`);
|
|
5305
|
+
}
|
|
5306
|
+
} catch (err) {
|
|
5307
|
+
creditDb.close();
|
|
5308
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5309
|
+
if (opts.json) {
|
|
5310
|
+
console.log(JSON.stringify({ success: false, error: msg }, null, 2));
|
|
5311
|
+
} else {
|
|
5312
|
+
console.error(`Error creating escrow receipt: ${msg}`);
|
|
5313
|
+
}
|
|
5314
|
+
process.exit(1);
|
|
3565
5315
|
}
|
|
3566
|
-
} catch (err) {
|
|
3567
5316
|
creditDb.close();
|
|
3568
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
3569
|
-
if (opts.json) {
|
|
3570
|
-
console.log(JSON.stringify({ success: false, error: msg }, null, 2));
|
|
3571
|
-
} else {
|
|
3572
|
-
console.error(`Error creating escrow receipt: ${msg}`);
|
|
3573
|
-
}
|
|
3574
|
-
process.exit(1);
|
|
3575
5317
|
}
|
|
3576
5318
|
}
|
|
3577
|
-
const
|
|
5319
|
+
const settleEscrow2 = async () => {
|
|
3578
5320
|
if (useReceipt && escrowId) {
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
creditDb.pragma("busy_timeout = 5000");
|
|
3582
|
-
try {
|
|
3583
|
-
settleRequesterEscrow(creditDb, escrowId);
|
|
5321
|
+
if (requestLedger) {
|
|
5322
|
+
await requestLedger.settle(escrowId, targetOwner ?? config.owner);
|
|
3584
5323
|
if (!opts.json) console.log(`Escrow settled: ${opts.cost} credits deducted.`);
|
|
3585
|
-
}
|
|
3586
|
-
|
|
5324
|
+
} else if (escrowReceipt) {
|
|
5325
|
+
const configDir = getConfigDir();
|
|
5326
|
+
const creditDb = openCreditDb(join3(configDir, "credit.db"));
|
|
5327
|
+
creditDb.pragma("busy_timeout = 5000");
|
|
5328
|
+
try {
|
|
5329
|
+
settleRequesterEscrow(creditDb, escrowId);
|
|
5330
|
+
if (!opts.json) console.log(`Escrow settled: ${opts.cost} credits deducted.`);
|
|
5331
|
+
} finally {
|
|
5332
|
+
creditDb.close();
|
|
5333
|
+
}
|
|
3587
5334
|
}
|
|
3588
5335
|
}
|
|
3589
5336
|
};
|
|
3590
|
-
const releaseEscrow2 = () => {
|
|
5337
|
+
const releaseEscrow2 = async () => {
|
|
3591
5338
|
if (useReceipt && escrowId) {
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
creditDb.pragma("busy_timeout = 5000");
|
|
3595
|
-
try {
|
|
3596
|
-
releaseRequesterEscrow(creditDb, escrowId);
|
|
5339
|
+
if (requestLedger) {
|
|
5340
|
+
await requestLedger.release(escrowId);
|
|
3597
5341
|
if (!opts.json) console.log("Escrow released: credits refunded.");
|
|
3598
|
-
}
|
|
3599
|
-
|
|
5342
|
+
} else if (escrowReceipt) {
|
|
5343
|
+
const configDir = getConfigDir();
|
|
5344
|
+
const creditDb = openCreditDb(join3(configDir, "credit.db"));
|
|
5345
|
+
creditDb.pragma("busy_timeout = 5000");
|
|
5346
|
+
try {
|
|
5347
|
+
releaseRequesterEscrow(creditDb, escrowId);
|
|
5348
|
+
if (!opts.json) console.log("Escrow released: credits refunded.");
|
|
5349
|
+
} finally {
|
|
5350
|
+
creditDb.close();
|
|
5351
|
+
}
|
|
3600
5352
|
}
|
|
3601
5353
|
}
|
|
3602
5354
|
};
|
|
@@ -3613,8 +5365,8 @@ program.command("request [card-id]").description("Request a capability from anot
|
|
|
3613
5365
|
return msg.includes("NETWORK_ERROR") || msg.includes("ECONNREFUSED") || msg.includes("fetch failed") || msg.includes("Network error");
|
|
3614
5366
|
};
|
|
3615
5367
|
const tryViaRelay = async () => {
|
|
3616
|
-
const { RelayClient } = await import("../websocket-client-
|
|
3617
|
-
const { requestViaRelay } = await import("../client-
|
|
5368
|
+
const { RelayClient } = await import("../websocket-client-6IIDGXKB.js");
|
|
5369
|
+
const { requestViaRelay } = await import("../client-BTPIFY7E.js");
|
|
3618
5370
|
const tempRelay = new RelayClient({
|
|
3619
5371
|
registryUrl: config.registry,
|
|
3620
5372
|
owner: config.owner,
|
|
@@ -3661,10 +5413,10 @@ program.command("request [card-id]").description("Request a capability from anot
|
|
|
3661
5413
|
}
|
|
3662
5414
|
}
|
|
3663
5415
|
}
|
|
3664
|
-
|
|
5416
|
+
await settleEscrow2();
|
|
3665
5417
|
printResult(result);
|
|
3666
5418
|
} catch (err) {
|
|
3667
|
-
releaseEscrow2();
|
|
5419
|
+
await releaseEscrow2();
|
|
3668
5420
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3669
5421
|
if (opts.json) {
|
|
3670
5422
|
console.log(JSON.stringify({ success: false, error: msg }, null, 2));
|
|
@@ -3684,12 +5436,28 @@ program.command("status").description("Show credit balance and recent transactio
|
|
|
3684
5436
|
let balance;
|
|
3685
5437
|
let transactions;
|
|
3686
5438
|
let heldEscrows;
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
5439
|
+
if (config.registry) {
|
|
5440
|
+
const statusIdentityAuth = loadIdentityAuth(config.owner);
|
|
5441
|
+
const statusLedger = createLedger({
|
|
5442
|
+
registryUrl: config.registry,
|
|
5443
|
+
ownerPublicKey: statusIdentityAuth.publicKey,
|
|
5444
|
+
privateKey: statusIdentityAuth.privateKey
|
|
5445
|
+
});
|
|
5446
|
+
try {
|
|
5447
|
+
balance = await statusLedger.getBalance(config.owner);
|
|
5448
|
+
transactions = await statusLedger.getHistory(config.owner, 5);
|
|
5449
|
+
heldEscrows = creditDb.prepare("SELECT id, amount, card_id, created_at FROM credit_escrow WHERE owner = ? AND status = ?").all(config.owner, "held");
|
|
5450
|
+
} finally {
|
|
5451
|
+
creditDb.close();
|
|
5452
|
+
}
|
|
5453
|
+
} else {
|
|
5454
|
+
try {
|
|
5455
|
+
balance = getBalance(creditDb, config.owner);
|
|
5456
|
+
transactions = getTransactions(creditDb, config.owner, 5);
|
|
5457
|
+
heldEscrows = creditDb.prepare("SELECT id, amount, card_id, created_at FROM credit_escrow WHERE owner = ? AND status = ?").all(config.owner, "held");
|
|
5458
|
+
} finally {
|
|
5459
|
+
creditDb.close();
|
|
5460
|
+
}
|
|
3693
5461
|
}
|
|
3694
5462
|
if (opts.json) {
|
|
3695
5463
|
console.log(JSON.stringify({ owner: config.owner, balance, held_escrows: heldEscrows, recent_transactions: transactions }, null, 2));
|
|
@@ -3722,7 +5490,7 @@ program.command("serve").description("Start the AgentBnB gateway server").option
|
|
|
3722
5490
|
}
|
|
3723
5491
|
const port = opts.port ? parseInt(opts.port, 10) : config.gateway_port;
|
|
3724
5492
|
const registryPort = parseInt(opts.registryPort, 10);
|
|
3725
|
-
const skillsYamlPath = opts.skillsYaml ??
|
|
5493
|
+
const skillsYamlPath = opts.skillsYaml ?? join3(homedir(), ".agentbnb", "skills.yaml");
|
|
3726
5494
|
const runtime = new AgentRuntime({
|
|
3727
5495
|
registryDbPath: config.db_path,
|
|
3728
5496
|
creditDbPath: config.credit_db_path,
|
|
@@ -3738,6 +5506,19 @@ program.command("serve").description("Start the AgentBnB gateway server").option
|
|
|
3738
5506
|
if (opts.conductor) {
|
|
3739
5507
|
console.log("Conductor mode enabled \u2014 orchestrate/plan skills available via gateway");
|
|
3740
5508
|
}
|
|
5509
|
+
if (opts.conductor && config.conductor?.public) {
|
|
5510
|
+
const { buildConductorCard } = await import("../card-4XH4AOTE.js");
|
|
5511
|
+
const { AnyCardSchema: AnyCard } = await import("../types-FGBUZ3QV.js");
|
|
5512
|
+
const conductorCard = buildConductorCard(config.owner);
|
|
5513
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5514
|
+
const existing = runtime.registryDb.prepare("SELECT id FROM capability_cards WHERE id = ?").get(conductorCard.id);
|
|
5515
|
+
if (existing) {
|
|
5516
|
+
runtime.registryDb.prepare("UPDATE capability_cards SET data = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(conductorCard), now, conductorCard.id);
|
|
5517
|
+
} else {
|
|
5518
|
+
runtime.registryDb.prepare("INSERT INTO capability_cards (id, owner, data, created_at, updated_at) VALUES (?, ?, ?, ?, ?)").run(conductorCard.id, config.owner, JSON.stringify(conductorCard), now, now);
|
|
5519
|
+
}
|
|
5520
|
+
console.log("Conductor card registered locally (conductor.public: true)");
|
|
5521
|
+
}
|
|
3741
5522
|
const autonomyConfig = config.autonomy ?? DEFAULT_AUTONOMY_CONFIG;
|
|
3742
5523
|
const idleMonitor = new IdleMonitor({
|
|
3743
5524
|
owner: config.owner,
|
|
@@ -3801,8 +5582,8 @@ program.command("serve").description("Start the AgentBnB gateway server").option
|
|
|
3801
5582
|
}
|
|
3802
5583
|
const relayUrl = opts.registry ?? config.registry;
|
|
3803
5584
|
if (relayUrl && opts.relay !== false) {
|
|
3804
|
-
const { RelayClient } = await import("../websocket-client-
|
|
3805
|
-
const { executeCapabilityRequest: executeCapabilityRequest2 } = await import("../execute-
|
|
5585
|
+
const { RelayClient } = await import("../websocket-client-6IIDGXKB.js");
|
|
5586
|
+
const { executeCapabilityRequest: executeCapabilityRequest2 } = await import("../execute-EXOITLHN.js");
|
|
3806
5587
|
const cards = listCards(runtime.registryDb, config.owner);
|
|
3807
5588
|
const card = cards[0] ?? {
|
|
3808
5589
|
id: config.owner,
|
|
@@ -3816,12 +5597,23 @@ program.command("serve").description("Start the AgentBnB gateway server").option
|
|
|
3816
5597
|
pricing: { credits_per_call: 0 },
|
|
3817
5598
|
availability: { online: true }
|
|
3818
5599
|
};
|
|
5600
|
+
const additionalCards = [];
|
|
5601
|
+
if (config.conductor?.public) {
|
|
5602
|
+
const { buildConductorCard } = await import("../card-4XH4AOTE.js");
|
|
5603
|
+
const conductorCard = buildConductorCard(config.owner);
|
|
5604
|
+
additionalCards.push(conductorCard);
|
|
5605
|
+
console.log("Conductor card will be published to registry (conductor.public: true)");
|
|
5606
|
+
}
|
|
3819
5607
|
relayClient = new RelayClient({
|
|
3820
5608
|
registryUrl: relayUrl,
|
|
3821
5609
|
owner: config.owner,
|
|
3822
5610
|
token: config.token,
|
|
3823
5611
|
card,
|
|
5612
|
+
cards: additionalCards.length > 0 ? additionalCards : void 0,
|
|
3824
5613
|
onRequest: async (req) => {
|
|
5614
|
+
const onProgress = (info) => {
|
|
5615
|
+
relayClient.sendProgress(req.id, info);
|
|
5616
|
+
};
|
|
3825
5617
|
const result = await executeCapabilityRequest2({
|
|
3826
5618
|
registryDb: runtime.registryDb,
|
|
3827
5619
|
creditDb: runtime.creditDb,
|
|
@@ -3831,7 +5623,8 @@ program.command("serve").description("Start the AgentBnB gateway server").option
|
|
|
3831
5623
|
requester: req.requester ?? req.from_owner,
|
|
3832
5624
|
escrowReceipt: req.escrow_receipt,
|
|
3833
5625
|
skillExecutor: runtime.skillExecutor,
|
|
3834
|
-
handlerUrl: opts.handlerUrl
|
|
5626
|
+
handlerUrl: opts.handlerUrl,
|
|
5627
|
+
onProgress
|
|
3835
5628
|
});
|
|
3836
5629
|
if (result.success) {
|
|
3837
5630
|
return { result: result.result };
|
|
@@ -3906,7 +5699,7 @@ peersCommand.command("remove <name>").description("Remove a registered peer").ac
|
|
|
3906
5699
|
});
|
|
3907
5700
|
var configCmd = program.command("config").description("Get or set AgentBnB configuration values");
|
|
3908
5701
|
configCmd.command("set <key> <value>").description("Set a configuration value").action((key, value) => {
|
|
3909
|
-
const allowedKeys = ["registry", "tier1", "tier2", "reserve", "idle-threshold"];
|
|
5702
|
+
const allowedKeys = ["registry", "tier1", "tier2", "reserve", "idle-threshold", "conductor-public"];
|
|
3910
5703
|
if (!allowedKeys.includes(key)) {
|
|
3911
5704
|
console.error(`Unknown config key: ${key}. Valid keys: ${allowedKeys.join(", ")}`);
|
|
3912
5705
|
process.exit(1);
|
|
@@ -3971,6 +5764,17 @@ configCmd.command("set <key> <value>").description("Set a configuration value").
|
|
|
3971
5764
|
console.log(`Set idle-threshold = ${parsed} (idle rate threshold for auto-share)`);
|
|
3972
5765
|
return;
|
|
3973
5766
|
}
|
|
5767
|
+
if (key === "conductor-public") {
|
|
5768
|
+
const boolVal = value === "true";
|
|
5769
|
+
if (value !== "true" && value !== "false") {
|
|
5770
|
+
console.error('Error: conductor-public must be "true" or "false"');
|
|
5771
|
+
process.exit(1);
|
|
5772
|
+
}
|
|
5773
|
+
config.conductor = { public: boolVal };
|
|
5774
|
+
saveConfig(config);
|
|
5775
|
+
console.log(`Set conductor-public = ${boolVal} (conductor card ${boolVal ? "will be" : "will NOT be"} published to registry)`);
|
|
5776
|
+
return;
|
|
5777
|
+
}
|
|
3974
5778
|
config[key] = value;
|
|
3975
5779
|
saveConfig(config);
|
|
3976
5780
|
console.log(`Set ${key} = ${value}`);
|
|
@@ -3998,6 +5802,10 @@ configCmd.command("get <key>").description("Get a configuration value").action((
|
|
|
3998
5802
|
console.log(val !== void 0 ? String(val) : "0.70");
|
|
3999
5803
|
return;
|
|
4000
5804
|
}
|
|
5805
|
+
if (key === "conductor-public") {
|
|
5806
|
+
console.log(String(config.conductor?.public ?? false));
|
|
5807
|
+
return;
|
|
5808
|
+
}
|
|
4001
5809
|
const value = config[key];
|
|
4002
5810
|
console.log(value !== void 0 ? String(value) : "(not set)");
|
|
4003
5811
|
});
|
|
@@ -4010,7 +5818,7 @@ openclaw.command("sync").description("Read SOUL.md and publish/update a v2.0 cap
|
|
|
4010
5818
|
}
|
|
4011
5819
|
let content;
|
|
4012
5820
|
try {
|
|
4013
|
-
content =
|
|
5821
|
+
content = readFileSync4(opts.soulPath, "utf-8");
|
|
4014
5822
|
} catch {
|
|
4015
5823
|
console.error(`Error: cannot read SOUL.md at ${opts.soulPath}`);
|
|
4016
5824
|
process.exit(1);
|
|
@@ -4019,6 +5827,14 @@ openclaw.command("sync").description("Read SOUL.md and publish/update a v2.0 cap
|
|
|
4019
5827
|
try {
|
|
4020
5828
|
const card = publishFromSoulV2(db, content, config.owner);
|
|
4021
5829
|
console.log(`Published card ${card.id} with ${card.skills.length} skill(s)`);
|
|
5830
|
+
for (const skill of card.skills) {
|
|
5831
|
+
const stats = getPricingStats(db, skill.name);
|
|
5832
|
+
if (stats.count > 0) {
|
|
5833
|
+
console.log(` ${skill.name}: ${skill.pricing.credits_per_call} cr (market: ${stats.min}-${stats.max} cr, median ${stats.median}, ${stats.count} providers)`);
|
|
5834
|
+
} else {
|
|
5835
|
+
console.log(` ${skill.name}: ${skill.pricing.credits_per_call} cr (no market data yet)`);
|
|
5836
|
+
}
|
|
5837
|
+
}
|
|
4022
5838
|
} catch (err) {
|
|
4023
5839
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4024
5840
|
console.error(`Error: ${msg}`);
|
|
@@ -4071,7 +5887,7 @@ openclaw.command("rules").description("Print HEARTBEAT.md rules block (or inject
|
|
|
4071
5887
|
}
|
|
4072
5888
|
});
|
|
4073
5889
|
program.command("conduct <task>").description("Orchestrate a complex task across the AgentBnB network").option("--plan-only", "Show execution plan without executing").option("--max-budget <credits>", "Maximum credits to spend", "100").option("--json", "Output as JSON").action(async (task, opts) => {
|
|
4074
|
-
const { conductAction } = await import("../conduct-
|
|
5890
|
+
const { conductAction } = await import("../conduct-FXLVGKD5.js");
|
|
4075
5891
|
const result = await conductAction(task, opts);
|
|
4076
5892
|
if (opts.json) {
|
|
4077
5893
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -4104,4 +5920,8 @@ Total credits spent: ${result.total_credits ?? 0} cr`);
|
|
|
4104
5920
|
}
|
|
4105
5921
|
}
|
|
4106
5922
|
});
|
|
5923
|
+
program.command("mcp-server").description("Start an MCP (Model Context Protocol) server for IDE integration").action(async () => {
|
|
5924
|
+
const { startMcpServer } = await import("../server-2LWHL24P.js");
|
|
5925
|
+
await startMcpServer();
|
|
5926
|
+
});
|
|
4107
5927
|
await program.parseAsync(process.argv);
|