agentbnb 3.1.6 → 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/README.md +117 -86
- package/dist/{card-IE5UV5QX.js → card-4XH4AOTE.js} +11 -4
- package/dist/chunk-3MJT4PZG.js +50 -0
- package/dist/{conduct-IEQ567ET.js → chunk-3UKAVIMC.js} +70 -31
- package/dist/chunk-5AH3CMOX.js +62 -0
- package/dist/{chunk-IZZ4FP45.js → chunk-6K5WUVF3.js} +33 -166
- package/dist/chunk-75OC6E4F.js +33 -0
- package/dist/chunk-DVAS2443.js +63 -0
- package/dist/{chunk-XA63SD4T.js → chunk-FNKBHBYK.js} +3 -0
- package/dist/{websocket-client-5TIQDYQ4.js → chunk-JOY533UH.js} +38 -4
- package/dist/chunk-KJG2UJV5.js +83 -0
- package/dist/chunk-M3G5NR2Z.js +90 -0
- package/dist/{chunk-7OACGAFD.js → chunk-MQKYGY5I.js} +63 -24
- package/dist/chunk-ODBGCCEH.js +358 -0
- package/dist/{chunk-QSPWE5AE.js → chunk-Q7HRI666.js} +9 -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-QHQPXO67.js → chunk-XQHN6ITI.js} +1 -58
- package/dist/cli/index.js +2734 -850
- package/dist/client-BTPIFY7E.js +10 -0
- package/dist/conduct-CW62HBPT.js +52 -0
- package/dist/conduct-FXLVGKD5.js +19 -0
- package/dist/{conductor-mode-IO45PWMI.js → conductor-mode-3JS4VWCR.js} +16 -7
- 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 +4 -1
- package/dist/chunk-BEI5MTNZ.js +0 -91
- package/dist/cli/index.d.ts +0 -1
- package/dist/execute-SWWEHV2K.js +0 -9
package/dist/cli/index.js
CHANGED
|
@@ -1,35 +1,50 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from "../chunk-QSPWE5AE.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
|
-
|
|
21
|
-
|
|
21
|
+
resolvePendingRequest
|
|
22
|
+
} from "../chunk-6K5WUVF3.js";
|
|
23
|
+
import {
|
|
24
|
+
fetchRemoteCards,
|
|
25
|
+
filterCards,
|
|
26
|
+
mergeResults,
|
|
22
27
|
searchCards
|
|
23
|
-
} from "../chunk-
|
|
28
|
+
} from "../chunk-QJEOCKVF.js";
|
|
29
|
+
import {
|
|
30
|
+
requestCapability
|
|
31
|
+
} from "../chunk-KJG2UJV5.js";
|
|
24
32
|
import {
|
|
25
33
|
findPeer,
|
|
26
|
-
getConfigDir,
|
|
27
|
-
loadConfig,
|
|
28
34
|
loadPeers,
|
|
29
35
|
removePeer,
|
|
30
|
-
saveConfig,
|
|
31
36
|
savePeer
|
|
32
|
-
} 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";
|
|
33
48
|
import {
|
|
34
49
|
getActivityFeed,
|
|
35
50
|
getCard,
|
|
@@ -42,127 +57,53 @@ import {
|
|
|
42
57
|
updateCard,
|
|
43
58
|
updateSkillAvailability,
|
|
44
59
|
updateSkillIdleRate
|
|
45
|
-
} from "../chunk-
|
|
60
|
+
} from "../chunk-TLU7ALCZ.js";
|
|
46
61
|
import {
|
|
47
62
|
bootstrapAgent,
|
|
48
|
-
generateKeyPair,
|
|
49
63
|
getBalance,
|
|
50
64
|
getTransactions,
|
|
51
65
|
holdEscrow,
|
|
52
|
-
loadKeyPair,
|
|
53
66
|
openCreditDb,
|
|
54
67
|
releaseEscrow,
|
|
68
|
+
settleEscrow
|
|
69
|
+
} from "../chunk-XQHN6ITI.js";
|
|
70
|
+
import {
|
|
71
|
+
generateKeyPair,
|
|
72
|
+
loadKeyPair,
|
|
55
73
|
saveKeyPair,
|
|
56
74
|
signEscrowReceipt,
|
|
57
75
|
verifyEscrowReceipt
|
|
58
|
-
} from "../chunk-
|
|
76
|
+
} from "../chunk-DVAS2443.js";
|
|
59
77
|
import {
|
|
60
78
|
AgentBnBError,
|
|
61
79
|
AnyCardSchema,
|
|
62
80
|
CapabilityCardV2Schema
|
|
63
|
-
} from "../chunk-
|
|
81
|
+
} from "../chunk-FNKBHBYK.js";
|
|
82
|
+
import {
|
|
83
|
+
RelayMessageSchema
|
|
84
|
+
} from "../chunk-QT7TEVNV.js";
|
|
64
85
|
|
|
65
86
|
// src/cli/index.ts
|
|
66
87
|
import { Command } from "commander";
|
|
67
|
-
import { readFileSync as
|
|
88
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
68
89
|
import { createRequire } from "module";
|
|
69
|
-
import { randomBytes } from "crypto";
|
|
70
|
-
import { join as
|
|
90
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
91
|
+
import { join as join3 } from "path";
|
|
71
92
|
import { networkInterfaces, homedir } from "os";
|
|
72
93
|
import { createInterface as createInterface2 } from "readline";
|
|
73
94
|
|
|
74
|
-
// src/identity/identity.ts
|
|
75
|
-
import { z } from "zod";
|
|
76
|
-
import { createHash } from "crypto";
|
|
77
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
78
|
-
import { join } from "path";
|
|
79
|
-
var AgentIdentitySchema = z.object({
|
|
80
|
-
/** Deterministic ID derived from public key: sha256(hex).slice(0, 16). */
|
|
81
|
-
agent_id: z.string().min(1),
|
|
82
|
-
/** Human-readable owner name (from config or init). */
|
|
83
|
-
owner: z.string().min(1),
|
|
84
|
-
/** Hex-encoded Ed25519 public key. */
|
|
85
|
-
public_key: z.string().min(1),
|
|
86
|
-
/** ISO 8601 timestamp of identity creation. */
|
|
87
|
-
created_at: z.string().datetime(),
|
|
88
|
-
/** Optional guarantor info if linked to a human. */
|
|
89
|
-
guarantor: z.object({
|
|
90
|
-
github_login: z.string().min(1),
|
|
91
|
-
verified_at: z.string().datetime()
|
|
92
|
-
}).optional()
|
|
93
|
-
});
|
|
94
|
-
var AgentCertificateSchema = z.object({
|
|
95
|
-
identity: AgentIdentitySchema,
|
|
96
|
-
/** ISO 8601 timestamp of certificate issuance. */
|
|
97
|
-
issued_at: z.string().datetime(),
|
|
98
|
-
/** ISO 8601 timestamp of certificate expiry. */
|
|
99
|
-
expires_at: z.string().datetime(),
|
|
100
|
-
/** Hex-encoded public key of the issuer (same as identity for self-signed). */
|
|
101
|
-
issuer_public_key: z.string().min(1),
|
|
102
|
-
/** Base64url Ed25519 signature over { identity, issued_at, expires_at, issuer_public_key }. */
|
|
103
|
-
signature: z.string().min(1)
|
|
104
|
-
});
|
|
105
|
-
var IDENTITY_FILENAME = "identity.json";
|
|
106
|
-
function deriveAgentId(publicKeyHex) {
|
|
107
|
-
return createHash("sha256").update(publicKeyHex, "hex").digest("hex").slice(0, 16);
|
|
108
|
-
}
|
|
109
|
-
function createIdentity(configDir, owner) {
|
|
110
|
-
if (!existsSync(configDir)) {
|
|
111
|
-
mkdirSync(configDir, { recursive: true });
|
|
112
|
-
}
|
|
113
|
-
let keys;
|
|
114
|
-
try {
|
|
115
|
-
keys = loadKeyPair(configDir);
|
|
116
|
-
} catch {
|
|
117
|
-
keys = generateKeyPair();
|
|
118
|
-
saveKeyPair(configDir, keys);
|
|
119
|
-
}
|
|
120
|
-
const publicKeyHex = keys.publicKey.toString("hex");
|
|
121
|
-
const agentId = deriveAgentId(publicKeyHex);
|
|
122
|
-
const identity = {
|
|
123
|
-
agent_id: agentId,
|
|
124
|
-
owner,
|
|
125
|
-
public_key: publicKeyHex,
|
|
126
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
127
|
-
};
|
|
128
|
-
saveIdentity(configDir, identity);
|
|
129
|
-
return identity;
|
|
130
|
-
}
|
|
131
|
-
function loadIdentity(configDir) {
|
|
132
|
-
const filePath = join(configDir, IDENTITY_FILENAME);
|
|
133
|
-
if (!existsSync(filePath)) return null;
|
|
134
|
-
try {
|
|
135
|
-
const raw = readFileSync(filePath, "utf-8");
|
|
136
|
-
return AgentIdentitySchema.parse(JSON.parse(raw));
|
|
137
|
-
} catch {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
function saveIdentity(configDir, identity) {
|
|
142
|
-
if (!existsSync(configDir)) {
|
|
143
|
-
mkdirSync(configDir, { recursive: true });
|
|
144
|
-
}
|
|
145
|
-
const filePath = join(configDir, IDENTITY_FILENAME);
|
|
146
|
-
writeFileSync(filePath, JSON.stringify(identity, null, 2), "utf-8");
|
|
147
|
-
}
|
|
148
|
-
function ensureIdentity(configDir, owner) {
|
|
149
|
-
const existing = loadIdentity(configDir);
|
|
150
|
-
if (existing) return existing;
|
|
151
|
-
return createIdentity(configDir, owner);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
95
|
// src/credit/escrow-receipt.ts
|
|
155
|
-
import { z
|
|
96
|
+
import { z } from "zod";
|
|
156
97
|
import { randomUUID } from "crypto";
|
|
157
|
-
var EscrowReceiptSchema =
|
|
158
|
-
requester_owner:
|
|
159
|
-
requester_public_key:
|
|
160
|
-
amount:
|
|
161
|
-
card_id:
|
|
162
|
-
skill_id:
|
|
163
|
-
timestamp:
|
|
164
|
-
nonce:
|
|
165
|
-
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)
|
|
166
107
|
});
|
|
167
108
|
function createSignedEscrowReceipt(db, privateKey, publicKey, opts) {
|
|
168
109
|
const escrowId = holdEscrow(db, opts.owner, opts.amount, opts.cardId);
|
|
@@ -276,89 +217,6 @@ var IdleMonitor = class {
|
|
|
276
217
|
}
|
|
277
218
|
};
|
|
278
219
|
|
|
279
|
-
// src/cli/remote-registry.ts
|
|
280
|
-
var RegistryTimeoutError = class extends AgentBnBError {
|
|
281
|
-
constructor(url) {
|
|
282
|
-
super(
|
|
283
|
-
`Registry at ${url} did not respond within 5s. Showing local results only.`,
|
|
284
|
-
"REGISTRY_TIMEOUT"
|
|
285
|
-
);
|
|
286
|
-
this.name = "RegistryTimeoutError";
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
var RegistryConnectionError = class extends AgentBnBError {
|
|
290
|
-
constructor(url) {
|
|
291
|
-
super(
|
|
292
|
-
`Cannot reach ${url}. Is the registry running? Showing local results only.`,
|
|
293
|
-
"REGISTRY_CONNECTION"
|
|
294
|
-
);
|
|
295
|
-
this.name = "RegistryConnectionError";
|
|
296
|
-
}
|
|
297
|
-
};
|
|
298
|
-
var RegistryAuthError = class extends AgentBnBError {
|
|
299
|
-
constructor(url) {
|
|
300
|
-
super(
|
|
301
|
-
`Authentication failed for ${url}. Run \`agentbnb config set token <your-token>\`.`,
|
|
302
|
-
"REGISTRY_AUTH"
|
|
303
|
-
);
|
|
304
|
-
this.name = "RegistryAuthError";
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
async function fetchRemoteCards(registryUrl, params, timeoutMs = 5e3) {
|
|
308
|
-
let cardsUrl;
|
|
309
|
-
try {
|
|
310
|
-
cardsUrl = new URL("/cards", registryUrl);
|
|
311
|
-
} catch {
|
|
312
|
-
throw new AgentBnBError(`Invalid registry URL: ${registryUrl}`, "INVALID_REGISTRY_URL");
|
|
313
|
-
}
|
|
314
|
-
const searchParams = new URLSearchParams();
|
|
315
|
-
if (params.q !== void 0) searchParams.set("q", params.q);
|
|
316
|
-
if (params.level !== void 0) searchParams.set("level", String(params.level));
|
|
317
|
-
if (params.online !== void 0) searchParams.set("online", String(params.online));
|
|
318
|
-
if (params.tag !== void 0) searchParams.set("tag", params.tag);
|
|
319
|
-
searchParams.set("limit", "100");
|
|
320
|
-
cardsUrl.search = searchParams.toString();
|
|
321
|
-
const controller = new AbortController();
|
|
322
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
323
|
-
let response;
|
|
324
|
-
try {
|
|
325
|
-
response = await fetch(cardsUrl.toString(), { signal: controller.signal });
|
|
326
|
-
} catch (err) {
|
|
327
|
-
clearTimeout(timer);
|
|
328
|
-
const isTimeout = err instanceof Error && err.name === "AbortError";
|
|
329
|
-
if (isTimeout) {
|
|
330
|
-
throw new RegistryTimeoutError(registryUrl);
|
|
331
|
-
}
|
|
332
|
-
throw new RegistryConnectionError(registryUrl);
|
|
333
|
-
} finally {
|
|
334
|
-
clearTimeout(timer);
|
|
335
|
-
}
|
|
336
|
-
if (response.status === 401 || response.status === 403) {
|
|
337
|
-
throw new RegistryAuthError(registryUrl);
|
|
338
|
-
}
|
|
339
|
-
if (!response.ok) {
|
|
340
|
-
throw new RegistryConnectionError(registryUrl);
|
|
341
|
-
}
|
|
342
|
-
const body = await response.json();
|
|
343
|
-
return body.items;
|
|
344
|
-
}
|
|
345
|
-
function mergeResults(localCards, remoteCards, hasQuery) {
|
|
346
|
-
const taggedLocal = localCards.map((c) => ({ ...c, source: "local" }));
|
|
347
|
-
const taggedRemote = remoteCards.map((c) => ({ ...c, source: "remote" }));
|
|
348
|
-
const localIds = new Set(localCards.map((c) => c.id));
|
|
349
|
-
const dedupedRemote = taggedRemote.filter((c) => !localIds.has(c.id));
|
|
350
|
-
if (!hasQuery) {
|
|
351
|
-
return [...taggedLocal, ...dedupedRemote];
|
|
352
|
-
}
|
|
353
|
-
const result = [];
|
|
354
|
-
const maxLen = Math.max(taggedLocal.length, dedupedRemote.length);
|
|
355
|
-
for (let i = 0; i < maxLen; i++) {
|
|
356
|
-
if (i < taggedLocal.length) result.push(taggedLocal[i]);
|
|
357
|
-
if (i < dedupedRemote.length) result.push(dedupedRemote[i]);
|
|
358
|
-
}
|
|
359
|
-
return result;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
220
|
// src/cli/onboarding.ts
|
|
363
221
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
364
222
|
import { createConnection } from "net";
|
|
@@ -530,8 +388,8 @@ function buildDraftCard(apiKey, owner) {
|
|
|
530
388
|
|
|
531
389
|
// src/onboarding/index.ts
|
|
532
390
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
533
|
-
import { existsSync
|
|
534
|
-
import { join
|
|
391
|
+
import { existsSync, readFileSync } from "fs";
|
|
392
|
+
import { join } from "path";
|
|
535
393
|
|
|
536
394
|
// src/onboarding/capability-templates.ts
|
|
537
395
|
var API_PATTERNS = [
|
|
@@ -646,9 +504,9 @@ var DOC_FILES = ["SOUL.md", "CLAUDE.md", "AGENTS.md", "README.md"];
|
|
|
646
504
|
function detectCapabilities(opts = {}) {
|
|
647
505
|
const cwd = opts.cwd ?? process.cwd();
|
|
648
506
|
if (opts.fromFile) {
|
|
649
|
-
const filePath = opts.fromFile.startsWith("/") ? opts.fromFile :
|
|
650
|
-
if (
|
|
651
|
-
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");
|
|
652
510
|
const capabilities = detectFromDocs(content);
|
|
653
511
|
if (capabilities.length > 0) {
|
|
654
512
|
return { source: "docs", capabilities, sourceFile: filePath };
|
|
@@ -657,9 +515,9 @@ function detectCapabilities(opts = {}) {
|
|
|
657
515
|
return { source: "none", capabilities: [] };
|
|
658
516
|
}
|
|
659
517
|
for (const fileName of DOC_FILES) {
|
|
660
|
-
const filePath =
|
|
661
|
-
if (!
|
|
662
|
-
const content =
|
|
518
|
+
const filePath = join(cwd, fileName);
|
|
519
|
+
if (!existsSync(filePath)) continue;
|
|
520
|
+
const content = readFileSync(filePath, "utf-8");
|
|
663
521
|
if (fileName === "SOUL.md") {
|
|
664
522
|
return { source: "soul", capabilities: [], soulContent: content, sourceFile: filePath };
|
|
665
523
|
}
|
|
@@ -703,8 +561,49 @@ function capabilitiesToV2Card(capabilities, owner, agentName) {
|
|
|
703
561
|
return CapabilityCardV2Schema.parse(card);
|
|
704
562
|
}
|
|
705
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
|
+
|
|
706
605
|
// src/runtime/agent-runtime.ts
|
|
707
|
-
import { readFileSync as
|
|
606
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
708
607
|
|
|
709
608
|
// src/skills/executor.ts
|
|
710
609
|
var SkillExecutor = class {
|
|
@@ -731,7 +630,7 @@ var SkillExecutor = class {
|
|
|
731
630
|
* @param params - Input parameters for the skill.
|
|
732
631
|
* @returns ExecutionResult including success, result/error, and latency_ms.
|
|
733
632
|
*/
|
|
734
|
-
async execute(skillId, params) {
|
|
633
|
+
async execute(skillId, params, onProgress) {
|
|
735
634
|
const startTime = Date.now();
|
|
736
635
|
const config = this.skillMap.get(skillId);
|
|
737
636
|
if (!config) {
|
|
@@ -750,7 +649,7 @@ var SkillExecutor = class {
|
|
|
750
649
|
};
|
|
751
650
|
}
|
|
752
651
|
try {
|
|
753
|
-
const modeResult = await mode.execute(config, params);
|
|
652
|
+
const modeResult = await mode.execute(config, params, onProgress);
|
|
754
653
|
return {
|
|
755
654
|
...modeResult,
|
|
756
655
|
latency_ms: Date.now() - startTime
|
|
@@ -787,98 +686,98 @@ function createSkillExecutor(configs, modes) {
|
|
|
787
686
|
}
|
|
788
687
|
|
|
789
688
|
// src/skills/skill-config.ts
|
|
790
|
-
import { z as
|
|
689
|
+
import { z as z2 } from "zod";
|
|
791
690
|
import yaml from "js-yaml";
|
|
792
|
-
var PricingSchema =
|
|
793
|
-
credits_per_call:
|
|
794
|
-
credits_per_minute:
|
|
795
|
-
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()
|
|
796
695
|
});
|
|
797
|
-
var ApiAuthSchema =
|
|
798
|
-
|
|
799
|
-
type:
|
|
800
|
-
token:
|
|
696
|
+
var ApiAuthSchema = z2.discriminatedUnion("type", [
|
|
697
|
+
z2.object({
|
|
698
|
+
type: z2.literal("bearer"),
|
|
699
|
+
token: z2.string()
|
|
801
700
|
}),
|
|
802
|
-
|
|
803
|
-
type:
|
|
804
|
-
header:
|
|
805
|
-
key:
|
|
701
|
+
z2.object({
|
|
702
|
+
type: z2.literal("apikey"),
|
|
703
|
+
header: z2.string().default("X-API-Key"),
|
|
704
|
+
key: z2.string()
|
|
806
705
|
}),
|
|
807
|
-
|
|
808
|
-
type:
|
|
809
|
-
username:
|
|
810
|
-
password:
|
|
706
|
+
z2.object({
|
|
707
|
+
type: z2.literal("basic"),
|
|
708
|
+
username: z2.string(),
|
|
709
|
+
password: z2.string()
|
|
811
710
|
})
|
|
812
711
|
]);
|
|
813
|
-
var ApiSkillConfigSchema =
|
|
814
|
-
id:
|
|
815
|
-
type:
|
|
816
|
-
name:
|
|
817
|
-
endpoint:
|
|
818
|
-
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"]),
|
|
819
718
|
auth: ApiAuthSchema.optional(),
|
|
820
|
-
input_mapping:
|
|
821
|
-
output_mapping:
|
|
719
|
+
input_mapping: z2.record(z2.string()).default({}),
|
|
720
|
+
output_mapping: z2.record(z2.string()).default({}),
|
|
822
721
|
pricing: PricingSchema,
|
|
823
|
-
timeout_ms:
|
|
824
|
-
retries:
|
|
825
|
-
provider:
|
|
722
|
+
timeout_ms: z2.number().positive().default(3e4),
|
|
723
|
+
retries: z2.number().nonnegative().int().default(0),
|
|
724
|
+
provider: z2.string().optional()
|
|
826
725
|
});
|
|
827
|
-
var PipelineStepSchema =
|
|
828
|
-
|
|
829
|
-
skill_id:
|
|
830
|
-
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({})
|
|
831
730
|
}),
|
|
832
|
-
|
|
833
|
-
command:
|
|
834
|
-
input_mapping:
|
|
731
|
+
z2.object({
|
|
732
|
+
command: z2.string().min(1),
|
|
733
|
+
input_mapping: z2.record(z2.string()).default({})
|
|
835
734
|
})
|
|
836
735
|
]);
|
|
837
|
-
var PipelineSkillConfigSchema =
|
|
838
|
-
id:
|
|
839
|
-
type:
|
|
840
|
-
name:
|
|
841
|
-
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),
|
|
842
741
|
pricing: PricingSchema,
|
|
843
|
-
timeout_ms:
|
|
742
|
+
timeout_ms: z2.number().positive().optional()
|
|
844
743
|
});
|
|
845
|
-
var OpenClawSkillConfigSchema =
|
|
846
|
-
id:
|
|
847
|
-
type:
|
|
848
|
-
name:
|
|
849
|
-
agent_name:
|
|
850
|
-
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"]),
|
|
851
750
|
pricing: PricingSchema,
|
|
852
|
-
timeout_ms:
|
|
751
|
+
timeout_ms: z2.number().positive().optional()
|
|
853
752
|
});
|
|
854
|
-
var CommandSkillConfigSchema =
|
|
855
|
-
id:
|
|
856
|
-
type:
|
|
857
|
-
name:
|
|
858
|
-
command:
|
|
859
|
-
output_type:
|
|
860
|
-
allowed_commands:
|
|
861
|
-
working_dir:
|
|
862
|
-
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),
|
|
863
762
|
pricing: PricingSchema
|
|
864
763
|
});
|
|
865
|
-
var ConductorSkillConfigSchema =
|
|
866
|
-
id:
|
|
867
|
-
type:
|
|
868
|
-
name:
|
|
869
|
-
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"]),
|
|
870
769
|
pricing: PricingSchema,
|
|
871
|
-
timeout_ms:
|
|
770
|
+
timeout_ms: z2.number().positive().optional()
|
|
872
771
|
});
|
|
873
|
-
var SkillConfigSchema =
|
|
772
|
+
var SkillConfigSchema = z2.discriminatedUnion("type", [
|
|
874
773
|
ApiSkillConfigSchema,
|
|
875
774
|
PipelineSkillConfigSchema,
|
|
876
775
|
OpenClawSkillConfigSchema,
|
|
877
776
|
CommandSkillConfigSchema,
|
|
878
777
|
ConductorSkillConfigSchema
|
|
879
778
|
]);
|
|
880
|
-
var SkillsFileSchema =
|
|
881
|
-
skills:
|
|
779
|
+
var SkillsFileSchema = z2.object({
|
|
780
|
+
skills: z2.array(SkillConfigSchema)
|
|
882
781
|
});
|
|
883
782
|
function expandEnvVars(value) {
|
|
884
783
|
return value.replace(/\$\{([^}]+)\}/g, (_match, varName) => {
|
|
@@ -1121,7 +1020,7 @@ var PipelineExecutor = class {
|
|
|
1121
1020
|
* @param params - Input parameters from the caller.
|
|
1122
1021
|
* @returns Partial ExecutionResult (without latency_ms — added by SkillExecutor wrapper).
|
|
1123
1022
|
*/
|
|
1124
|
-
async execute(config, params) {
|
|
1023
|
+
async execute(config, params, onProgress) {
|
|
1125
1024
|
const pipelineConfig = config;
|
|
1126
1025
|
const steps = pipelineConfig.steps ?? [];
|
|
1127
1026
|
if (steps.length === 0) {
|
|
@@ -1180,6 +1079,13 @@ var PipelineExecutor = class {
|
|
|
1180
1079
|
}
|
|
1181
1080
|
context.steps.push({ result: stepResult });
|
|
1182
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
|
+
}
|
|
1183
1089
|
}
|
|
1184
1090
|
const lastStep = context.steps[context.steps.length - 1];
|
|
1185
1091
|
return {
|
|
@@ -1515,20 +1421,20 @@ var AgentRuntime = class {
|
|
|
1515
1421
|
* 3. Populate the Map with all 4 modes — SkillExecutor sees them via reference.
|
|
1516
1422
|
*/
|
|
1517
1423
|
async initSkillExecutor() {
|
|
1518
|
-
const hasSkillsYaml = this.skillsYamlPath &&
|
|
1424
|
+
const hasSkillsYaml = this.skillsYamlPath && existsSync2(this.skillsYamlPath);
|
|
1519
1425
|
if (!hasSkillsYaml && !this.conductorEnabled) {
|
|
1520
1426
|
return;
|
|
1521
1427
|
}
|
|
1522
1428
|
let configs = [];
|
|
1523
1429
|
if (hasSkillsYaml) {
|
|
1524
|
-
const yamlContent =
|
|
1430
|
+
const yamlContent = readFileSync2(this.skillsYamlPath, "utf8");
|
|
1525
1431
|
configs = parseSkillsFile(yamlContent);
|
|
1526
1432
|
}
|
|
1527
1433
|
const modes = /* @__PURE__ */ new Map();
|
|
1528
1434
|
if (this.conductorEnabled) {
|
|
1529
|
-
const { ConductorMode } = await import("../conductor-mode-
|
|
1530
|
-
const { registerConductorCard, CONDUCTOR_OWNER } = await import("../card-
|
|
1531
|
-
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");
|
|
1532
1438
|
registerConductorCard(this.registryDb);
|
|
1533
1439
|
const resolveAgentUrl = (owner) => {
|
|
1534
1440
|
const peers = loadPeers2();
|
|
@@ -1640,7 +1546,7 @@ function createGatewayServer(opts) {
|
|
|
1640
1546
|
creditDb,
|
|
1641
1547
|
tokens,
|
|
1642
1548
|
handlerUrl,
|
|
1643
|
-
timeoutMs =
|
|
1549
|
+
timeoutMs = 3e5,
|
|
1644
1550
|
silent = false,
|
|
1645
1551
|
skillExecutor
|
|
1646
1552
|
} = opts;
|
|
@@ -1735,21 +1641,365 @@ function createGatewayServer(opts) {
|
|
|
1735
1641
|
// src/registry/server.ts
|
|
1736
1642
|
import Fastify2 from "fastify";
|
|
1737
1643
|
import cors from "@fastify/cors";
|
|
1644
|
+
import swagger from "@fastify/swagger";
|
|
1645
|
+
import swaggerUi from "@fastify/swagger-ui";
|
|
1738
1646
|
import fastifyStatic from "@fastify/static";
|
|
1739
1647
|
import fastifyWebsocket from "@fastify/websocket";
|
|
1740
|
-
import { join as
|
|
1648
|
+
import { join as join2, dirname } from "path";
|
|
1741
1649
|
import { fileURLToPath } from "url";
|
|
1742
|
-
import { existsSync as
|
|
1650
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1743
1651
|
|
|
1744
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
|
|
1745
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
|
|
1746
1995
|
var RATE_LIMIT_MAX = 60;
|
|
1747
1996
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
1748
|
-
var RELAY_TIMEOUT_MS =
|
|
1749
|
-
function registerWebSocketRelay(server, db) {
|
|
1997
|
+
var RELAY_TIMEOUT_MS = 3e5;
|
|
1998
|
+
function registerWebSocketRelay(server, db, creditDb) {
|
|
1750
1999
|
const connections = /* @__PURE__ */ new Map();
|
|
1751
2000
|
const pendingRequests = /* @__PURE__ */ new Map();
|
|
1752
2001
|
const rateLimits = /* @__PURE__ */ new Map();
|
|
2002
|
+
let onAgentOnlineCallback;
|
|
1753
2003
|
function checkRateLimit(owner) {
|
|
1754
2004
|
const now = Date.now();
|
|
1755
2005
|
const entry = rateLimits.get(owner);
|
|
@@ -1800,21 +2050,32 @@ function registerWebSocketRelay(server, db) {
|
|
|
1800
2050
|
}
|
|
1801
2051
|
}
|
|
1802
2052
|
function upsertCard(cardData, owner) {
|
|
1803
|
-
const
|
|
1804
|
-
|
|
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);
|
|
1805
2064
|
if (existing) {
|
|
1806
|
-
|
|
1807
|
-
|
|
2065
|
+
db.prepare(
|
|
2066
|
+
"UPDATE capability_cards SET data = ?, updated_at = ? WHERE id = ?"
|
|
2067
|
+
).run(JSON.stringify(card), now, cardId);
|
|
1808
2068
|
} else {
|
|
1809
|
-
|
|
1810
|
-
|
|
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);
|
|
1811
2072
|
}
|
|
1812
2073
|
return cardId;
|
|
1813
2074
|
}
|
|
1814
2075
|
function logAgentJoined(owner, cardName, cardId) {
|
|
1815
2076
|
try {
|
|
1816
2077
|
insertRequestLog(db, {
|
|
1817
|
-
id:
|
|
2078
|
+
id: randomUUID6(),
|
|
1818
2079
|
card_id: cardId,
|
|
1819
2080
|
card_name: cardName,
|
|
1820
2081
|
requester: owner,
|
|
@@ -1845,10 +2106,25 @@ function registerWebSocketRelay(server, db) {
|
|
|
1845
2106
|
const cardId = upsertCard(card, owner);
|
|
1846
2107
|
const cardName = card.name ?? card.agent_name ?? owner;
|
|
1847
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
|
+
}
|
|
1848
2117
|
markOwnerOnline(owner);
|
|
2118
|
+
if (onAgentOnlineCallback) {
|
|
2119
|
+
try {
|
|
2120
|
+
onAgentOnlineCallback(owner);
|
|
2121
|
+
} catch (e) {
|
|
2122
|
+
console.error("[relay] onAgentOnline callback error:", e);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
1849
2125
|
sendMessage(ws, { type: "registered", agent_id: cardId });
|
|
1850
2126
|
}
|
|
1851
|
-
function handleRelayRequest(ws, msg, fromOwner) {
|
|
2127
|
+
async function handleRelayRequest(ws, msg, fromOwner) {
|
|
1852
2128
|
if (!checkRateLimit(fromOwner)) {
|
|
1853
2129
|
sendMessage(ws, {
|
|
1854
2130
|
type: "error",
|
|
@@ -1867,15 +2143,42 @@ function registerWebSocketRelay(server, db) {
|
|
|
1867
2143
|
});
|
|
1868
2144
|
return;
|
|
1869
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
|
+
}
|
|
1870
2165
|
const timeout = setTimeout(() => {
|
|
2166
|
+
const pending = pendingRequests.get(msg.id);
|
|
1871
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
|
+
}
|
|
1872
2175
|
sendMessage(ws, {
|
|
1873
2176
|
type: "response",
|
|
1874
2177
|
id: msg.id,
|
|
1875
2178
|
error: { code: -32603, message: "Relay request timeout" }
|
|
1876
2179
|
});
|
|
1877
2180
|
}, RELAY_TIMEOUT_MS);
|
|
1878
|
-
pendingRequests.set(msg.id, { originOwner: fromOwner, timeout });
|
|
2181
|
+
pendingRequests.set(msg.id, { originOwner: fromOwner, timeout, escrowId, targetOwner: msg.target_owner });
|
|
1879
2182
|
sendMessage(targetWs, {
|
|
1880
2183
|
type: "incoming_request",
|
|
1881
2184
|
id: msg.id,
|
|
@@ -1887,18 +2190,98 @@ function registerWebSocketRelay(server, db) {
|
|
|
1887
2190
|
escrow_receipt: msg.escrow_receipt
|
|
1888
2191
|
});
|
|
1889
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
|
+
}
|
|
1890
2227
|
function handleRelayResponse(msg) {
|
|
1891
2228
|
const pending = pendingRequests.get(msg.id);
|
|
1892
2229
|
if (!pending) return;
|
|
1893
2230
|
clearTimeout(pending.timeout);
|
|
1894
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
|
+
}
|
|
1895
2277
|
const originWs = connections.get(pending.originOwner);
|
|
1896
2278
|
if (originWs && originWs.readyState === 1) {
|
|
1897
2279
|
sendMessage(originWs, {
|
|
1898
2280
|
type: "response",
|
|
1899
2281
|
id: msg.id,
|
|
1900
2282
|
result: msg.result,
|
|
1901
|
-
error: msg.error
|
|
2283
|
+
error: msg.error,
|
|
2284
|
+
...conductorFee > 0 ? { conductor_fee: conductorFee } : {}
|
|
1902
2285
|
});
|
|
1903
2286
|
}
|
|
1904
2287
|
}
|
|
@@ -1908,55 +2291,85 @@ function registerWebSocketRelay(server, db) {
|
|
|
1908
2291
|
rateLimits.delete(owner);
|
|
1909
2292
|
markOwnerOffline(owner);
|
|
1910
2293
|
for (const [reqId, pending] of pendingRequests) {
|
|
1911
|
-
if (pending.
|
|
2294
|
+
if (pending.targetOwner === owner) {
|
|
1912
2295
|
clearTimeout(pending.timeout);
|
|
1913
2296
|
pendingRequests.delete(reqId);
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
let registeredOwner;
|
|
1920
|
-
socket.on("message", (raw) => {
|
|
1921
|
-
let data;
|
|
1922
|
-
try {
|
|
1923
|
-
data = JSON.parse(typeof raw === "string" ? raw : raw.toString("utf-8"));
|
|
1924
|
-
} catch {
|
|
1925
|
-
sendMessage(socket, { type: "error", code: "invalid_json", message: "Invalid JSON" });
|
|
1926
|
-
return;
|
|
1927
|
-
}
|
|
1928
|
-
const parsed = RelayMessageSchema.safeParse(data);
|
|
1929
|
-
if (!parsed.success) {
|
|
1930
|
-
sendMessage(socket, {
|
|
1931
|
-
type: "error",
|
|
1932
|
-
code: "invalid_message",
|
|
1933
|
-
message: `Invalid message: ${parsed.error.issues[0]?.message ?? "unknown error"}`
|
|
1934
|
-
});
|
|
1935
|
-
return;
|
|
1936
|
-
}
|
|
1937
|
-
const msg = parsed.data;
|
|
1938
|
-
switch (msg.type) {
|
|
1939
|
-
case "register":
|
|
1940
|
-
registeredOwner = msg.owner;
|
|
1941
|
-
handleRegister(socket, msg);
|
|
1942
|
-
break;
|
|
1943
|
-
case "relay_request":
|
|
1944
|
-
if (!registeredOwner) {
|
|
1945
|
-
sendMessage(socket, {
|
|
1946
|
-
type: "error",
|
|
1947
|
-
code: "not_registered",
|
|
1948
|
-
message: "Must send register message before relay requests"
|
|
1949
|
-
});
|
|
1950
|
-
return;
|
|
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);
|
|
1951
2302
|
}
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
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) {
|
|
2313
|
+
clearTimeout(pending.timeout);
|
|
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
|
+
}
|
|
1959
2322
|
}
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
server.get("/ws", { websocket: true }, (rawSocket, _request) => {
|
|
2326
|
+
const socket = rawSocket;
|
|
2327
|
+
let registeredOwner;
|
|
2328
|
+
socket.on("message", (raw) => {
|
|
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
|
+
})();
|
|
1960
2373
|
});
|
|
1961
2374
|
socket.on("close", () => {
|
|
1962
2375
|
handleDisconnect(registeredOwner);
|
|
@@ -1981,21 +2394,29 @@ function registerWebSocketRelay(server, db) {
|
|
|
1981
2394
|
}
|
|
1982
2395
|
pendingRequests.clear();
|
|
1983
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);
|
|
1984
2405
|
}
|
|
1985
2406
|
};
|
|
1986
2407
|
}
|
|
1987
2408
|
|
|
1988
2409
|
// src/identity/guarantor.ts
|
|
1989
|
-
import { z as
|
|
1990
|
-
import { randomUUID as
|
|
2410
|
+
import { z as z3 } from "zod";
|
|
2411
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
1991
2412
|
var MAX_AGENTS_PER_GUARANTOR = 10;
|
|
1992
2413
|
var GUARANTOR_CREDIT_POOL = 50;
|
|
1993
|
-
var GuarantorRecordSchema =
|
|
1994
|
-
id:
|
|
1995
|
-
github_login:
|
|
1996
|
-
agent_count:
|
|
1997
|
-
credit_pool:
|
|
1998
|
-
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()
|
|
1999
2420
|
});
|
|
2000
2421
|
var GUARANTOR_SCHEMA = `
|
|
2001
2422
|
CREATE TABLE IF NOT EXISTS guarantors (
|
|
@@ -2026,7 +2447,7 @@ function registerGuarantor(db, githubLogin) {
|
|
|
2026
2447
|
);
|
|
2027
2448
|
}
|
|
2028
2449
|
const record = {
|
|
2029
|
-
id:
|
|
2450
|
+
id: randomUUID7(),
|
|
2030
2451
|
github_login: githubLogin,
|
|
2031
2452
|
agent_count: 0,
|
|
2032
2453
|
credit_pool: GUARANTOR_CREDIT_POOL,
|
|
@@ -2100,10 +2521,849 @@ function getAgentGuarantor(db, agentId) {
|
|
|
2100
2521
|
function initiateGithubAuth() {
|
|
2101
2522
|
return {
|
|
2102
2523
|
auth_url: "https://github.com/login/oauth/authorize?client_id=PLACEHOLDER&scope=read:user",
|
|
2103
|
-
state:
|
|
2524
|
+
state: randomUUID7()
|
|
2104
2525
|
};
|
|
2105
2526
|
}
|
|
2106
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
|
+
|
|
2107
3367
|
// src/registry/server.ts
|
|
2108
3368
|
function stripInternal(card) {
|
|
2109
3369
|
const { _internal: _, ...publicCard } = card;
|
|
@@ -2112,25 +3372,76 @@ function stripInternal(card) {
|
|
|
2112
3372
|
function createRegistryServer(opts) {
|
|
2113
3373
|
const { registryDb: db, silent = false } = opts;
|
|
2114
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
|
+
});
|
|
2115
3410
|
void server.register(cors, {
|
|
2116
3411
|
origin: true,
|
|
2117
3412
|
methods: ["GET", "POST", "PATCH", "OPTIONS"],
|
|
2118
|
-
allowedHeaders: ["Content-Type", "Authorization"]
|
|
3413
|
+
allowedHeaders: ["Content-Type", "Authorization", "X-Agent-PublicKey", "X-Agent-Signature", "X-Agent-Timestamp"]
|
|
2119
3414
|
});
|
|
2120
3415
|
void server.register(fastifyWebsocket);
|
|
2121
3416
|
let relayState = null;
|
|
2122
3417
|
if (opts.creditDb) {
|
|
2123
|
-
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
|
+
}
|
|
2124
3435
|
}
|
|
2125
3436
|
const __filename = fileURLToPath(import.meta.url);
|
|
2126
3437
|
const __dirname = dirname(__filename);
|
|
2127
3438
|
const hubDistCandidates = [
|
|
2128
|
-
|
|
3439
|
+
join2(__dirname, "../../hub/dist"),
|
|
2129
3440
|
// When running from dist/registry/server.js
|
|
2130
|
-
|
|
3441
|
+
join2(__dirname, "../../../hub/dist")
|
|
2131
3442
|
// Fallback for alternative layouts
|
|
2132
3443
|
];
|
|
2133
|
-
const hubDistDir = hubDistCandidates.find((p) =>
|
|
3444
|
+
const hubDistDir = hubDistCandidates.find((p) => existsSync3(p));
|
|
2134
3445
|
if (hubDistDir) {
|
|
2135
3446
|
void server.register(fastifyStatic, {
|
|
2136
3447
|
root: hubDistDir,
|
|
@@ -2149,44 +3460,82 @@ function createRegistryServer(opts) {
|
|
|
2149
3460
|
return reply.code(404).send({ error: "Not found" });
|
|
2150
3461
|
});
|
|
2151
3462
|
}
|
|
2152
|
-
server.
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
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
|
-
|
|
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(`
|
|
2190
3539
|
SELECT card_id, skill_id, COUNT(*) as cnt
|
|
2191
3540
|
FROM request_log
|
|
2192
3541
|
WHERE status = 'success'
|
|
@@ -2194,57 +3543,63 @@ function createRegistryServer(opts) {
|
|
|
2194
3543
|
AND (action_type IS NULL OR action_type = 'auto_share')
|
|
2195
3544
|
GROUP BY card_id, skill_id
|
|
2196
3545
|
`);
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
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
|
-
|
|
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(`
|
|
2248
3603
|
SELECT rl.card_id, COUNT(*) as recent_requests
|
|
2249
3604
|
FROM request_log rl
|
|
2250
3605
|
WHERE rl.status = 'success'
|
|
@@ -2254,135 +3609,226 @@ function createRegistryServer(opts) {
|
|
|
2254
3609
|
ORDER BY recent_requests DESC
|
|
2255
3610
|
LIMIT 10
|
|
2256
3611
|
`);
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
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
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
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)
|
|
2295
3703
|
VALUES (?, ?, ?, ?, ?)`
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
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;
|
|
2309
3719
|
}
|
|
2310
|
-
throw err;
|
|
2311
3720
|
}
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
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(`
|
|
2333
3762
|
SELECT cc.owner,
|
|
2334
3763
|
SUM(CASE WHEN rl.status = 'success' THEN rl.credits_charged ELSE 0 END) as credits_earned
|
|
2335
3764
|
FROM capability_cards cc
|
|
2336
3765
|
LEFT JOIN request_log rl ON rl.card_id = cc.id
|
|
2337
3766
|
GROUP BY cc.owner
|
|
2338
3767
|
`);
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
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
|
+
};
|
|
3785
|
+
});
|
|
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 });
|
|
2362
3793
|
});
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
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(`
|
|
2375
3821
|
SELECT SUM(CASE WHEN rl.status = 'success' THEN rl.credits_charged ELSE 0 END) as credits_earned
|
|
2376
3822
|
FROM capability_cards cc
|
|
2377
3823
|
LEFT JOIN request_log rl ON rl.card_id = cc.id
|
|
2378
3824
|
WHERE cc.owner = ?
|
|
2379
3825
|
`);
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
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(`
|
|
2386
3832
|
SELECT rl.id, rl.card_name, rl.requester, rl.status, rl.credits_charged, rl.created_at
|
|
2387
3833
|
FROM request_log rl
|
|
2388
3834
|
INNER JOIN capability_cards cc ON rl.card_id = cc.id
|
|
@@ -2390,214 +3836,429 @@ function createRegistryServer(opts) {
|
|
|
2390
3836
|
ORDER BY rl.created_at DESC
|
|
2391
3837
|
LIMIT 10
|
|
2392
3838
|
`);
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
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()
|
|
3846
|
+
};
|
|
3847
|
+
return reply.send({
|
|
3848
|
+
profile,
|
|
3849
|
+
skills: ownerCards,
|
|
3850
|
+
recent_activity: recentActivity
|
|
3851
|
+
});
|
|
2405
3852
|
});
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
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')"
|
|
3913
|
+
);
|
|
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
|
+
}
|
|
2421
3938
|
}
|
|
2422
|
-
}
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
onlineOwners.add(card.owner);
|
|
3939
|
+
}, async (request, reply) => {
|
|
3940
|
+
if (!opts.creditDb) {
|
|
3941
|
+
return reply.code(503).send({ error: "Credit database not configured" });
|
|
2426
3942
|
}
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
const exchangeRow = exchangeStmt.get();
|
|
2432
|
-
return reply.send({
|
|
2433
|
-
agents_online: onlineOwners.size,
|
|
2434
|
-
total_capabilities: allCards.reduce((sum, card) => {
|
|
2435
|
-
const v2 = card;
|
|
2436
|
-
return sum + (v2.skills?.length ?? 1);
|
|
2437
|
-
}, 0),
|
|
2438
|
-
total_exchanges: exchangeRow.count
|
|
2439
|
-
});
|
|
2440
|
-
});
|
|
2441
|
-
server.post("/api/identity/register", async (request, reply) => {
|
|
2442
|
-
if (!opts.creditDb) {
|
|
2443
|
-
return reply.code(503).send({ error: "Credit database not configured" });
|
|
2444
|
-
}
|
|
2445
|
-
const body = request.body;
|
|
2446
|
-
const githubLogin = typeof body.github_login === "string" ? body.github_login.trim() : "";
|
|
2447
|
-
if (!githubLogin) {
|
|
2448
|
-
return reply.code(400).send({ error: "github_login is required" });
|
|
2449
|
-
}
|
|
2450
|
-
try {
|
|
2451
|
-
const record = registerGuarantor(opts.creditDb, githubLogin);
|
|
2452
|
-
const auth = initiateGithubAuth();
|
|
2453
|
-
return reply.code(201).send({ guarantor: record, oauth: auth });
|
|
2454
|
-
} catch (err) {
|
|
2455
|
-
if (err instanceof AgentBnBError && err.code === "GUARANTOR_EXISTS") {
|
|
2456
|
-
return reply.code(409).send({ error: err.message });
|
|
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" });
|
|
2457
3947
|
}
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
const githubLogin = typeof body.github_login === "string" ? body.github_login.trim() : "";
|
|
2468
|
-
if (!agentId || !githubLogin) {
|
|
2469
|
-
return reply.code(400).send({ error: "agent_id and github_login are required" });
|
|
2470
|
-
}
|
|
2471
|
-
try {
|
|
2472
|
-
const record = linkAgentToGuarantor(opts.creditDb, agentId, githubLogin);
|
|
2473
|
-
return reply.send({ guarantor: record });
|
|
2474
|
-
} catch (err) {
|
|
2475
|
-
if (err instanceof AgentBnBError) {
|
|
2476
|
-
const statusMap = {
|
|
2477
|
-
GUARANTOR_NOT_FOUND: 404,
|
|
2478
|
-
MAX_AGENTS_EXCEEDED: 409,
|
|
2479
|
-
AGENT_ALREADY_LINKED: 409
|
|
2480
|
-
};
|
|
2481
|
-
const status = statusMap[err.code] ?? 400;
|
|
2482
|
-
return reply.code(status).send({ error: err.message });
|
|
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;
|
|
2483
3957
|
}
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
if (!token || token !== ownerApiKey) {
|
|
2503
|
-
return reply.status(401).send({ error: "Unauthorized" });
|
|
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" } } }
|
|
2504
3976
|
}
|
|
2505
|
-
}
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
return reply.send({
|
|
2509
|
-
}
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
const
|
|
2518
|
-
return reply.send({
|
|
2519
|
-
})
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
if (!card) {
|
|
2529
|
-
return reply.code(404).send({ error: "Not found" });
|
|
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
|
+
}
|
|
3988
|
+
try {
|
|
3989
|
+
const record = linkAgentToGuarantor(opts.creditDb, agentId, githubLogin);
|
|
3990
|
+
return reply.send({ guarantor: record });
|
|
3991
|
+
} catch (err) {
|
|
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 });
|
|
2530
4000
|
}
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
4001
|
+
throw err;
|
|
4002
|
+
}
|
|
4003
|
+
});
|
|
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
|
+
}
|
|
2540
4016
|
}
|
|
2541
|
-
throw err;
|
|
2542
4017
|
}
|
|
2543
|
-
}
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
4018
|
+
}
|
|
4019
|
+
}, async (request, reply) => {
|
|
4020
|
+
if (!opts.creditDb) {
|
|
4021
|
+
return reply.code(503).send({ error: "Credit database not configured" });
|
|
4022
|
+
}
|
|
4023
|
+
const { agent_id } = request.params;
|
|
4024
|
+
const guarantor = getAgentGuarantor(opts.creditDb, agent_id);
|
|
4025
|
+
return reply.send({ agent_id, guarantor });
|
|
4026
|
+
});
|
|
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 } }
|
|
4039
|
+
}
|
|
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" });
|
|
4056
|
+
}
|
|
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") {
|
|
2556
4136
|
return reply.code(403).send({ error: "Forbidden" });
|
|
2557
4137
|
}
|
|
2558
|
-
|
|
2559
|
-
|
|
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" } } }
|
|
2560
4159
|
}
|
|
2561
4160
|
}
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
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
|
-
|
|
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
|
+
});
|
|
2598
4259
|
});
|
|
2599
|
-
}
|
|
2600
|
-
}
|
|
4260
|
+
}
|
|
4261
|
+
});
|
|
2601
4262
|
return { server, relayState };
|
|
2602
4263
|
}
|
|
2603
4264
|
|
|
@@ -2672,10 +4333,10 @@ async function stopAnnouncement() {
|
|
|
2672
4333
|
}
|
|
2673
4334
|
|
|
2674
4335
|
// src/openclaw/soul-sync.ts
|
|
2675
|
-
import { randomUUID as
|
|
4336
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
2676
4337
|
|
|
2677
4338
|
// src/skills/publish-capability.ts
|
|
2678
|
-
import { randomUUID as
|
|
4339
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
2679
4340
|
function parseSoulMd(content) {
|
|
2680
4341
|
const lines = content.split("\n");
|
|
2681
4342
|
let name = "";
|
|
@@ -2685,17 +4346,23 @@ function parseSoulMd(content) {
|
|
|
2685
4346
|
let currentSection = null;
|
|
2686
4347
|
let currentCapabilityName = "";
|
|
2687
4348
|
let currentCapabilityLines = [];
|
|
4349
|
+
let currentCapabilityPricing = void 0;
|
|
2688
4350
|
let descriptionLines = [];
|
|
2689
4351
|
let pastFirstH1 = false;
|
|
2690
4352
|
let pastFirstH2 = false;
|
|
2691
4353
|
const flushCapability = () => {
|
|
2692
4354
|
if (currentCapabilityName) {
|
|
2693
|
-
|
|
4355
|
+
const cap = {
|
|
2694
4356
|
name: currentCapabilityName,
|
|
2695
4357
|
description: currentCapabilityLines.join(" ").trim()
|
|
2696
|
-
}
|
|
4358
|
+
};
|
|
4359
|
+
if (currentCapabilityPricing !== void 0) {
|
|
4360
|
+
cap.pricing = currentCapabilityPricing;
|
|
4361
|
+
}
|
|
4362
|
+
capabilities.push(cap);
|
|
2697
4363
|
currentCapabilityName = "";
|
|
2698
4364
|
currentCapabilityLines = [];
|
|
4365
|
+
currentCapabilityPricing = void 0;
|
|
2699
4366
|
}
|
|
2700
4367
|
};
|
|
2701
4368
|
for (const line of lines) {
|
|
@@ -2725,7 +4392,15 @@ function parseSoulMd(content) {
|
|
|
2725
4392
|
if (currentSection === "preamble" && !pastFirstH2) {
|
|
2726
4393
|
descriptionLines.push(trimmed);
|
|
2727
4394
|
} else if (currentSection === "capability") {
|
|
2728
|
-
|
|
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
|
+
}
|
|
2729
4404
|
}
|
|
2730
4405
|
}
|
|
2731
4406
|
flushCapability();
|
|
@@ -2746,7 +4421,7 @@ function parseSoulMdV2(content) {
|
|
|
2746
4421
|
const parsed = parseSoulMd(content);
|
|
2747
4422
|
const skills = parsed.capabilities.map((cap) => {
|
|
2748
4423
|
const sanitizedId = cap.name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
2749
|
-
const id = sanitizedId.length > 0 ? sanitizedId :
|
|
4424
|
+
const id = sanitizedId.length > 0 ? sanitizedId : randomUUID9();
|
|
2750
4425
|
return {
|
|
2751
4426
|
id,
|
|
2752
4427
|
name: cap.name,
|
|
@@ -2768,7 +4443,7 @@ function parseSoulMdV2(content) {
|
|
|
2768
4443
|
required: true
|
|
2769
4444
|
}
|
|
2770
4445
|
],
|
|
2771
|
-
pricing: { credits_per_call: 10 },
|
|
4446
|
+
pricing: { credits_per_call: cap.pricing !== void 0 ? cap.pricing : 10 },
|
|
2772
4447
|
availability: { online: true }
|
|
2773
4448
|
};
|
|
2774
4449
|
});
|
|
@@ -2788,7 +4463,7 @@ function publishFromSoulV2(db, soulContent, owner) {
|
|
|
2788
4463
|
(c) => c.spec_version === "2.0"
|
|
2789
4464
|
);
|
|
2790
4465
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2791
|
-
const cardId = existingV2?.id ??
|
|
4466
|
+
const cardId = existingV2?.id ?? randomUUID9();
|
|
2792
4467
|
const card = {
|
|
2793
4468
|
spec_version: "2.0",
|
|
2794
4469
|
id: cardId,
|
|
@@ -2813,7 +4488,7 @@ function publishFromSoulV2(db, soulContent, owner) {
|
|
|
2813
4488
|
}
|
|
2814
4489
|
|
|
2815
4490
|
// src/openclaw/heartbeat-writer.ts
|
|
2816
|
-
import { readFileSync as
|
|
4491
|
+
import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync4 } from "fs";
|
|
2817
4492
|
var HEARTBEAT_MARKER_START = "<!-- agentbnb:start -->";
|
|
2818
4493
|
var HEARTBEAT_MARKER_END = "<!-- agentbnb:end -->";
|
|
2819
4494
|
function generateHeartbeatSection(autonomy, budget) {
|
|
@@ -2849,11 +4524,11 @@ function generateHeartbeatSection(autonomy, budget) {
|
|
|
2849
4524
|
].join("\n");
|
|
2850
4525
|
}
|
|
2851
4526
|
function injectHeartbeatSection(heartbeatPath, section) {
|
|
2852
|
-
if (!
|
|
2853
|
-
|
|
4527
|
+
if (!existsSync4(heartbeatPath)) {
|
|
4528
|
+
writeFileSync(heartbeatPath, section + "\n", "utf-8");
|
|
2854
4529
|
return;
|
|
2855
4530
|
}
|
|
2856
|
-
let content =
|
|
4531
|
+
let content = readFileSync3(heartbeatPath, "utf-8");
|
|
2857
4532
|
const startIdx = content.indexOf(HEARTBEAT_MARKER_START);
|
|
2858
4533
|
const endIdx = content.indexOf(HEARTBEAT_MARKER_END);
|
|
2859
4534
|
if (startIdx !== -1 && endIdx !== -1) {
|
|
@@ -2861,7 +4536,7 @@ function injectHeartbeatSection(heartbeatPath, section) {
|
|
|
2861
4536
|
} else {
|
|
2862
4537
|
content = content + "\n" + section + "\n";
|
|
2863
4538
|
}
|
|
2864
|
-
|
|
4539
|
+
writeFileSync(heartbeatPath, content, "utf-8");
|
|
2865
4540
|
}
|
|
2866
4541
|
|
|
2867
4542
|
// src/openclaw/skill.ts
|
|
@@ -2941,11 +4616,11 @@ function getLanIp() {
|
|
|
2941
4616
|
var program = new Command();
|
|
2942
4617
|
program.name("agentbnb").description("P2P Agent Capability Sharing Protocol \u2014 Airbnb for AI agent pipelines").version(pkg.version);
|
|
2943
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) => {
|
|
2944
|
-
const owner = opts.owner ?? `agent-${
|
|
2945
|
-
const token =
|
|
4619
|
+
const owner = opts.owner ?? `agent-${randomBytes2(4).toString("hex")}`;
|
|
4620
|
+
const token = randomBytes2(32).toString("hex");
|
|
2946
4621
|
const configDir = getConfigDir();
|
|
2947
|
-
const dbPath =
|
|
2948
|
-
const creditDbPath =
|
|
4622
|
+
const dbPath = join3(configDir, "registry.db");
|
|
4623
|
+
const creditDbPath = join3(configDir, "credit.db");
|
|
2949
4624
|
const port = parseInt(opts.port, 10);
|
|
2950
4625
|
const ip = opts.host ?? getLanIp();
|
|
2951
4626
|
const existingConfig = loadConfig();
|
|
@@ -2959,7 +4634,7 @@ program.command("init").description("Initialize AgentBnB config and create agent
|
|
|
2959
4634
|
credit_db_path: creditDbPath,
|
|
2960
4635
|
token: existingConfig?.token ?? token,
|
|
2961
4636
|
// Preserve existing token
|
|
2962
|
-
api_key: existingConfig?.api_key ??
|
|
4637
|
+
api_key: existingConfig?.api_key ?? randomBytes2(32).toString("hex")
|
|
2963
4638
|
};
|
|
2964
4639
|
saveConfig(config);
|
|
2965
4640
|
let keypairStatus = "existing";
|
|
@@ -2974,6 +4649,21 @@ program.command("init").description("Initialize AgentBnB config and create agent
|
|
|
2974
4649
|
const creditDb = openCreditDb(creditDbPath);
|
|
2975
4650
|
bootstrapAgent(creditDb, owner, 100);
|
|
2976
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
|
+
}
|
|
2977
4667
|
const skipDetect = opts.detect === false;
|
|
2978
4668
|
const publishedCards = [];
|
|
2979
4669
|
let detectedSource = "none";
|
|
@@ -3119,6 +4809,9 @@ Publish these ${card.skills.length} capabilities? [y/N] `);
|
|
|
3119
4809
|
keypair: keypairStatus,
|
|
3120
4810
|
agent_id: identity.agent_id
|
|
3121
4811
|
};
|
|
4812
|
+
if (registryBalance !== void 0) {
|
|
4813
|
+
jsonOutput.registry_balance = registryBalance;
|
|
4814
|
+
}
|
|
3122
4815
|
if (!skipDetect) {
|
|
3123
4816
|
jsonOutput.detected_source = detectedSource;
|
|
3124
4817
|
jsonOutput.published_cards = publishedCards;
|
|
@@ -3129,7 +4822,11 @@ Publish these ${card.skills.length} capabilities? [y/N] `);
|
|
|
3129
4822
|
console.log(` Owner: ${owner}`);
|
|
3130
4823
|
console.log(` Token: ${token}`);
|
|
3131
4824
|
console.log(` Config: ${configDir}/config.json`);
|
|
3132
|
-
|
|
4825
|
+
if (registryBalance !== void 0) {
|
|
4826
|
+
console.log(` Registry balance: ${registryBalance} credits`);
|
|
4827
|
+
} else {
|
|
4828
|
+
console.log(` Credits: 100 (starter grant)`);
|
|
4829
|
+
}
|
|
3133
4830
|
console.log(` Keypair: ${keypairStatus === "generated" ? "generated (Ed25519)" : "preserved (existing)"}`);
|
|
3134
4831
|
console.log(` Agent ID: ${identity.agent_id}`);
|
|
3135
4832
|
console.log(` Gateway: http://${ip}:${port}`);
|
|
@@ -3143,7 +4840,7 @@ program.command("publish <card.json>").description("Publish a Capability Card to
|
|
|
3143
4840
|
}
|
|
3144
4841
|
let raw;
|
|
3145
4842
|
try {
|
|
3146
|
-
raw =
|
|
4843
|
+
raw = readFileSync4(cardPath, "utf-8");
|
|
3147
4844
|
} catch {
|
|
3148
4845
|
console.error(`Error: cannot read file: ${cardPath}`);
|
|
3149
4846
|
process.exit(1);
|
|
@@ -3172,6 +4869,27 @@ program.command("publish <card.json>").description("Publish a Capability Card to
|
|
|
3172
4869
|
}
|
|
3173
4870
|
const card = result.data;
|
|
3174
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
|
+
}
|
|
3175
4893
|
const db = openDatabase(config.db_path);
|
|
3176
4894
|
try {
|
|
3177
4895
|
if (card.spec_version === "2.0") {
|
|
@@ -3427,8 +5145,8 @@ program.command("request [card-id]").description("Request a capability from anot
|
|
|
3427
5145
|
process.exit(1);
|
|
3428
5146
|
}
|
|
3429
5147
|
}
|
|
3430
|
-
const registryDb = openDatabase(
|
|
3431
|
-
const creditDb = openCreditDb(
|
|
5148
|
+
const registryDb = openDatabase(join3(getConfigDir(), "registry.db"));
|
|
5149
|
+
const creditDb = openCreditDb(join3(getConfigDir(), "credit.db"));
|
|
3432
5150
|
registryDb.pragma("busy_timeout = 5000");
|
|
3433
5151
|
creditDb.pragma("busy_timeout = 5000");
|
|
3434
5152
|
try {
|
|
@@ -3438,7 +5156,8 @@ program.command("request [card-id]").description("Request a capability from anot
|
|
|
3438
5156
|
registryDb,
|
|
3439
5157
|
creditDb,
|
|
3440
5158
|
autonomyConfig: config.autonomy ?? DEFAULT_AUTONOMY_CONFIG,
|
|
3441
|
-
budgetManager
|
|
5159
|
+
budgetManager,
|
|
5160
|
+
registryUrl: config.registry
|
|
3442
5161
|
});
|
|
3443
5162
|
const result = await requestor.requestWithAutonomy({
|
|
3444
5163
|
query: opts.query,
|
|
@@ -3466,6 +5185,7 @@ program.command("request [card-id]").description("Request a capability from anot
|
|
|
3466
5185
|
let gatewayUrl;
|
|
3467
5186
|
let token;
|
|
3468
5187
|
let isRemoteRequest = false;
|
|
5188
|
+
let targetOwner;
|
|
3469
5189
|
const identityAuth = loadIdentityAuth(config.owner);
|
|
3470
5190
|
if (opts.peer) {
|
|
3471
5191
|
const peer = findPeer(opts.peer);
|
|
@@ -3476,6 +5196,7 @@ program.command("request [card-id]").description("Request a capability from anot
|
|
|
3476
5196
|
gatewayUrl = peer.url;
|
|
3477
5197
|
token = peer.token;
|
|
3478
5198
|
isRemoteRequest = true;
|
|
5199
|
+
targetOwner = opts.peer;
|
|
3479
5200
|
} else {
|
|
3480
5201
|
const db = openDatabase(config.db_path);
|
|
3481
5202
|
let localCard;
|
|
@@ -3507,101 +5228,195 @@ program.command("request [card-id]").description("Request a capability from anot
|
|
|
3507
5228
|
console.error(`Error: cannot reach registry: ${err.message}`);
|
|
3508
5229
|
process.exit(1);
|
|
3509
5230
|
}
|
|
3510
|
-
|
|
3511
|
-
|
|
5231
|
+
targetOwner = remoteCard.owner ?? remoteCard.agent_name;
|
|
5232
|
+
if (remoteCard.gateway_url && typeof remoteCard.gateway_url === "string") {
|
|
5233
|
+
gatewayUrl = remoteCard.gateway_url;
|
|
5234
|
+
} else if (targetOwner && config.registry) {
|
|
5235
|
+
gatewayUrl = "";
|
|
5236
|
+
} else {
|
|
5237
|
+
console.error("Error: remote card has no gateway_url and no relay available. The provider needs to re-publish with `agentbnb sync`.");
|
|
3512
5238
|
process.exit(1);
|
|
3513
5239
|
}
|
|
3514
|
-
gatewayUrl = remoteCard.gateway_url;
|
|
3515
5240
|
token = "";
|
|
3516
5241
|
isRemoteRequest = true;
|
|
3517
5242
|
if (!opts.json) {
|
|
3518
5243
|
const displayName = remoteCard.name ?? remoteCard.agent_name ?? cardId;
|
|
3519
|
-
|
|
5244
|
+
if (gatewayUrl) {
|
|
5245
|
+
console.log(`Found remote card: ${displayName} @ ${gatewayUrl}`);
|
|
5246
|
+
} else {
|
|
5247
|
+
console.log(`Found remote card: ${displayName} (relay-only)`);
|
|
5248
|
+
}
|
|
3520
5249
|
}
|
|
3521
5250
|
}
|
|
3522
5251
|
}
|
|
3523
5252
|
const useReceipt = isRemoteRequest && opts.receipt !== false;
|
|
5253
|
+
const useRegistryLedger = isRemoteRequest && !!config.registry && !!gatewayUrl;
|
|
3524
5254
|
if (useReceipt && !opts.cost) {
|
|
3525
5255
|
console.error("Error: --cost <credits> is required for remote requests. Specify the credits to commit.");
|
|
3526
5256
|
process.exit(1);
|
|
3527
5257
|
}
|
|
3528
5258
|
let escrowId;
|
|
3529
5259
|
let escrowReceipt;
|
|
5260
|
+
let requestLedger;
|
|
3530
5261
|
if (useReceipt) {
|
|
3531
|
-
const
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
try {
|
|
3535
|
-
const keys = loadKeyPair(configDir);
|
|
3536
|
-
const amount = Number(opts.cost);
|
|
3537
|
-
if (isNaN(amount) || amount <= 0) {
|
|
3538
|
-
console.error("Error: --cost must be a positive number.");
|
|
3539
|
-
process.exit(1);
|
|
3540
|
-
}
|
|
3541
|
-
const receiptResult = createSignedEscrowReceipt(creditDb, keys.privateKey, keys.publicKey, {
|
|
3542
|
-
owner: config.owner,
|
|
3543
|
-
amount,
|
|
3544
|
-
cardId,
|
|
3545
|
-
skillId: opts.skill
|
|
3546
|
-
});
|
|
3547
|
-
escrowId = receiptResult.escrowId;
|
|
3548
|
-
escrowReceipt = receiptResult.receipt;
|
|
3549
|
-
if (!opts.json) {
|
|
3550
|
-
console.log(`Escrow: ${amount} credits held (ID: ${escrowId.slice(0, 8)}...)`);
|
|
3551
|
-
}
|
|
3552
|
-
} catch (err) {
|
|
3553
|
-
creditDb.close();
|
|
3554
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
3555
|
-
if (opts.json) {
|
|
3556
|
-
console.log(JSON.stringify({ success: false, error: msg }, null, 2));
|
|
3557
|
-
} else {
|
|
3558
|
-
console.error(`Error creating escrow receipt: ${msg}`);
|
|
3559
|
-
}
|
|
5262
|
+
const amount = Number(opts.cost);
|
|
5263
|
+
if (isNaN(amount) || amount <= 0) {
|
|
5264
|
+
console.error("Error: --cost must be a positive number.");
|
|
3560
5265
|
process.exit(1);
|
|
3561
5266
|
}
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
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
|
+
}
|
|
5287
|
+
process.exit(1);
|
|
5288
|
+
}
|
|
5289
|
+
} else if (gatewayUrl) {
|
|
3573
5290
|
const configDir = getConfigDir();
|
|
3574
|
-
const creditDb = openCreditDb(
|
|
5291
|
+
const creditDb = openCreditDb(join3(configDir, "credit.db"));
|
|
3575
5292
|
creditDb.pragma("busy_timeout = 5000");
|
|
3576
5293
|
try {
|
|
3577
|
-
|
|
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;
|
|
3578
5303
|
if (!opts.json) {
|
|
3579
|
-
console.log(`Escrow
|
|
5304
|
+
console.log(`Escrow: ${amount} credits held (ID: ${escrowId.slice(0, 8)}...)`);
|
|
3580
5305
|
}
|
|
3581
|
-
}
|
|
5306
|
+
} catch (err) {
|
|
3582
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);
|
|
5315
|
+
}
|
|
5316
|
+
creditDb.close();
|
|
5317
|
+
}
|
|
5318
|
+
}
|
|
5319
|
+
const settleEscrow2 = async () => {
|
|
5320
|
+
if (useReceipt && escrowId) {
|
|
5321
|
+
if (requestLedger) {
|
|
5322
|
+
await requestLedger.settle(escrowId, targetOwner ?? config.owner);
|
|
5323
|
+
if (!opts.json) console.log(`Escrow settled: ${opts.cost} credits deducted.`);
|
|
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
|
+
}
|
|
5334
|
+
}
|
|
5335
|
+
}
|
|
5336
|
+
};
|
|
5337
|
+
const releaseEscrow2 = async () => {
|
|
5338
|
+
if (useReceipt && escrowId) {
|
|
5339
|
+
if (requestLedger) {
|
|
5340
|
+
await requestLedger.release(escrowId);
|
|
5341
|
+
if (!opts.json) console.log("Escrow released: credits refunded.");
|
|
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
|
+
}
|
|
3583
5352
|
}
|
|
3584
5353
|
}
|
|
5354
|
+
};
|
|
5355
|
+
const printResult = (result) => {
|
|
3585
5356
|
if (opts.json) {
|
|
3586
5357
|
console.log(JSON.stringify({ success: true, result }, null, 2));
|
|
3587
5358
|
} else {
|
|
3588
5359
|
console.log("Result:");
|
|
3589
5360
|
console.log(typeof result === "string" ? result : JSON.stringify(result, null, 2));
|
|
3590
5361
|
}
|
|
3591
|
-
}
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
5362
|
+
};
|
|
5363
|
+
const isNetworkError = (err) => {
|
|
5364
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5365
|
+
return msg.includes("NETWORK_ERROR") || msg.includes("ECONNREFUSED") || msg.includes("fetch failed") || msg.includes("Network error");
|
|
5366
|
+
};
|
|
5367
|
+
const tryViaRelay = async () => {
|
|
5368
|
+
const { RelayClient } = await import("../websocket-client-6IIDGXKB.js");
|
|
5369
|
+
const { requestViaRelay } = await import("../client-BTPIFY7E.js");
|
|
5370
|
+
const tempRelay = new RelayClient({
|
|
5371
|
+
registryUrl: config.registry,
|
|
5372
|
+
owner: config.owner,
|
|
5373
|
+
token: config.token,
|
|
5374
|
+
card: { id: config.owner, owner: config.owner },
|
|
5375
|
+
onRequest: async () => ({ error: { code: -32601, message: "Not serving" } }),
|
|
5376
|
+
silent: true
|
|
5377
|
+
});
|
|
5378
|
+
try {
|
|
5379
|
+
await tempRelay.connect();
|
|
5380
|
+
const result = await requestViaRelay(tempRelay, {
|
|
5381
|
+
targetOwner,
|
|
5382
|
+
cardId,
|
|
5383
|
+
skillId: opts.skill,
|
|
5384
|
+
params: { ...params, ...opts.skill ? { skill_id: opts.skill } : {} },
|
|
5385
|
+
escrowReceipt
|
|
5386
|
+
});
|
|
5387
|
+
return result;
|
|
5388
|
+
} finally {
|
|
5389
|
+
tempRelay.disconnect();
|
|
5390
|
+
}
|
|
5391
|
+
};
|
|
5392
|
+
try {
|
|
5393
|
+
let result;
|
|
5394
|
+
if (!gatewayUrl && isRemoteRequest && config.registry && targetOwner) {
|
|
5395
|
+
if (!opts.json) console.log("No gateway URL, requesting via relay...");
|
|
5396
|
+
result = await tryViaRelay();
|
|
5397
|
+
} else {
|
|
3596
5398
|
try {
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
5399
|
+
result = await requestCapability({
|
|
5400
|
+
gatewayUrl,
|
|
5401
|
+
token,
|
|
5402
|
+
cardId,
|
|
5403
|
+
params: { ...params, ...opts.skill ? { skill_id: opts.skill } : {} },
|
|
5404
|
+
escrowReceipt,
|
|
5405
|
+
identity: identityAuth
|
|
5406
|
+
});
|
|
5407
|
+
} catch (directErr) {
|
|
5408
|
+
if (isNetworkError(directErr) && isRemoteRequest && config.registry && targetOwner) {
|
|
5409
|
+
if (!opts.json) console.log("Direct connection failed, trying relay...");
|
|
5410
|
+
result = await tryViaRelay();
|
|
5411
|
+
} else {
|
|
5412
|
+
throw directErr;
|
|
3600
5413
|
}
|
|
3601
|
-
} finally {
|
|
3602
|
-
creditDb.close();
|
|
3603
5414
|
}
|
|
3604
5415
|
}
|
|
5416
|
+
await settleEscrow2();
|
|
5417
|
+
printResult(result);
|
|
5418
|
+
} catch (err) {
|
|
5419
|
+
await releaseEscrow2();
|
|
3605
5420
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3606
5421
|
if (opts.json) {
|
|
3607
5422
|
console.log(JSON.stringify({ success: false, error: msg }, null, 2));
|
|
@@ -3621,12 +5436,28 @@ program.command("status").description("Show credit balance and recent transactio
|
|
|
3621
5436
|
let balance;
|
|
3622
5437
|
let transactions;
|
|
3623
5438
|
let heldEscrows;
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
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
|
+
}
|
|
3630
5461
|
}
|
|
3631
5462
|
if (opts.json) {
|
|
3632
5463
|
console.log(JSON.stringify({ owner: config.owner, balance, held_escrows: heldEscrows, recent_transactions: transactions }, null, 2));
|
|
@@ -3651,7 +5482,7 @@ Active Escrows (${heldEscrows.length}):`);
|
|
|
3651
5482
|
}
|
|
3652
5483
|
}
|
|
3653
5484
|
});
|
|
3654
|
-
program.command("serve").description("Start the AgentBnB gateway server").option("--port <port>", "Port to listen on (overrides config)").option("--handler-url <url>", "Local capability handler URL", "http://localhost:8080").option("--skills-yaml <path>", "Path to skills.yaml (default: ~/.agentbnb/skills.yaml)").option("--registry-port <port>", "Public registry API port (0 to disable)", "7701").option("--registry <url>", "Connect to remote registry via WebSocket relay (e.g., hub.agentbnb.dev)").option("--conductor", "Enable Conductor orchestration mode").option("--announce", "Announce this gateway on the local network via mDNS").action(async (opts) => {
|
|
5485
|
+
program.command("serve").description("Start the AgentBnB gateway server").option("--port <port>", "Port to listen on (overrides config)").option("--handler-url <url>", "Local capability handler URL", "http://localhost:8080").option("--skills-yaml <path>", "Path to skills.yaml (default: ~/.agentbnb/skills.yaml)").option("--registry-port <port>", "Public registry API port (0 to disable)", "7701").option("--registry <url>", "Connect to remote registry via WebSocket relay (e.g., hub.agentbnb.dev)").option("--conductor", "Enable Conductor orchestration mode").option("--announce", "Announce this gateway on the local network via mDNS").option("--no-relay", "Do not auto-connect to remote registry relay").action(async (opts) => {
|
|
3655
5486
|
const config = loadConfig();
|
|
3656
5487
|
if (!config) {
|
|
3657
5488
|
console.error("Error: not initialized. Run `agentbnb init` first.");
|
|
@@ -3659,7 +5490,7 @@ program.command("serve").description("Start the AgentBnB gateway server").option
|
|
|
3659
5490
|
}
|
|
3660
5491
|
const port = opts.port ? parseInt(opts.port, 10) : config.gateway_port;
|
|
3661
5492
|
const registryPort = parseInt(opts.registryPort, 10);
|
|
3662
|
-
const skillsYamlPath = opts.skillsYaml ??
|
|
5493
|
+
const skillsYamlPath = opts.skillsYaml ?? join3(homedir(), ".agentbnb", "skills.yaml");
|
|
3663
5494
|
const runtime = new AgentRuntime({
|
|
3664
5495
|
registryDbPath: config.db_path,
|
|
3665
5496
|
creditDbPath: config.credit_db_path,
|
|
@@ -3675,6 +5506,19 @@ program.command("serve").description("Start the AgentBnB gateway server").option
|
|
|
3675
5506
|
if (opts.conductor) {
|
|
3676
5507
|
console.log("Conductor mode enabled \u2014 orchestrate/plan skills available via gateway");
|
|
3677
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
|
+
}
|
|
3678
5522
|
const autonomyConfig = config.autonomy ?? DEFAULT_AUTONOMY_CONFIG;
|
|
3679
5523
|
const idleMonitor = new IdleMonitor({
|
|
3680
5524
|
owner: config.owner,
|
|
@@ -3736,9 +5580,10 @@ program.command("serve").description("Start the AgentBnB gateway server").option
|
|
|
3736
5580
|
console.log(`WebSocket relay active on /ws`);
|
|
3737
5581
|
}
|
|
3738
5582
|
}
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
const {
|
|
5583
|
+
const relayUrl = opts.registry ?? config.registry;
|
|
5584
|
+
if (relayUrl && opts.relay !== false) {
|
|
5585
|
+
const { RelayClient } = await import("../websocket-client-6IIDGXKB.js");
|
|
5586
|
+
const { executeCapabilityRequest: executeCapabilityRequest2 } = await import("../execute-EXOITLHN.js");
|
|
3742
5587
|
const cards = listCards(runtime.registryDb, config.owner);
|
|
3743
5588
|
const card = cards[0] ?? {
|
|
3744
5589
|
id: config.owner,
|
|
@@ -3752,12 +5597,23 @@ program.command("serve").description("Start the AgentBnB gateway server").option
|
|
|
3752
5597
|
pricing: { credits_per_call: 0 },
|
|
3753
5598
|
availability: { online: true }
|
|
3754
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
|
+
}
|
|
3755
5607
|
relayClient = new RelayClient({
|
|
3756
|
-
registryUrl:
|
|
5608
|
+
registryUrl: relayUrl,
|
|
3757
5609
|
owner: config.owner,
|
|
3758
5610
|
token: config.token,
|
|
3759
5611
|
card,
|
|
5612
|
+
cards: additionalCards.length > 0 ? additionalCards : void 0,
|
|
3760
5613
|
onRequest: async (req) => {
|
|
5614
|
+
const onProgress = (info) => {
|
|
5615
|
+
relayClient.sendProgress(req.id, info);
|
|
5616
|
+
};
|
|
3761
5617
|
const result = await executeCapabilityRequest2({
|
|
3762
5618
|
registryDb: runtime.registryDb,
|
|
3763
5619
|
creditDb: runtime.creditDb,
|
|
@@ -3767,7 +5623,8 @@ program.command("serve").description("Start the AgentBnB gateway server").option
|
|
|
3767
5623
|
requester: req.requester ?? req.from_owner,
|
|
3768
5624
|
escrowReceipt: req.escrow_receipt,
|
|
3769
5625
|
skillExecutor: runtime.skillExecutor,
|
|
3770
|
-
handlerUrl: opts.handlerUrl
|
|
5626
|
+
handlerUrl: opts.handlerUrl,
|
|
5627
|
+
onProgress
|
|
3771
5628
|
});
|
|
3772
5629
|
if (result.success) {
|
|
3773
5630
|
return { result: result.result };
|
|
@@ -3777,9 +5634,9 @@ program.command("serve").description("Start the AgentBnB gateway server").option
|
|
|
3777
5634
|
});
|
|
3778
5635
|
try {
|
|
3779
5636
|
await relayClient.connect();
|
|
3780
|
-
console.log(`Connected to registry: ${opts.registry}`);
|
|
5637
|
+
console.log(`Connected to registry: ${relayUrl}${opts.registry ? "" : " (auto)"}`);
|
|
3781
5638
|
} catch (err) {
|
|
3782
|
-
console.warn(`Warning: could not connect to registry ${
|
|
5639
|
+
console.warn(`Warning: could not connect to registry ${relayUrl}: ${err instanceof Error ? err.message : err}`);
|
|
3783
5640
|
console.warn("Will auto-reconnect in background...");
|
|
3784
5641
|
}
|
|
3785
5642
|
}
|
|
@@ -3842,7 +5699,7 @@ peersCommand.command("remove <name>").description("Remove a registered peer").ac
|
|
|
3842
5699
|
});
|
|
3843
5700
|
var configCmd = program.command("config").description("Get or set AgentBnB configuration values");
|
|
3844
5701
|
configCmd.command("set <key> <value>").description("Set a configuration value").action((key, value) => {
|
|
3845
|
-
const allowedKeys = ["registry", "tier1", "tier2", "reserve", "idle-threshold"];
|
|
5702
|
+
const allowedKeys = ["registry", "tier1", "tier2", "reserve", "idle-threshold", "conductor-public"];
|
|
3846
5703
|
if (!allowedKeys.includes(key)) {
|
|
3847
5704
|
console.error(`Unknown config key: ${key}. Valid keys: ${allowedKeys.join(", ")}`);
|
|
3848
5705
|
process.exit(1);
|
|
@@ -3907,6 +5764,17 @@ configCmd.command("set <key> <value>").description("Set a configuration value").
|
|
|
3907
5764
|
console.log(`Set idle-threshold = ${parsed} (idle rate threshold for auto-share)`);
|
|
3908
5765
|
return;
|
|
3909
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
|
+
}
|
|
3910
5778
|
config[key] = value;
|
|
3911
5779
|
saveConfig(config);
|
|
3912
5780
|
console.log(`Set ${key} = ${value}`);
|
|
@@ -3934,6 +5802,10 @@ configCmd.command("get <key>").description("Get a configuration value").action((
|
|
|
3934
5802
|
console.log(val !== void 0 ? String(val) : "0.70");
|
|
3935
5803
|
return;
|
|
3936
5804
|
}
|
|
5805
|
+
if (key === "conductor-public") {
|
|
5806
|
+
console.log(String(config.conductor?.public ?? false));
|
|
5807
|
+
return;
|
|
5808
|
+
}
|
|
3937
5809
|
const value = config[key];
|
|
3938
5810
|
console.log(value !== void 0 ? String(value) : "(not set)");
|
|
3939
5811
|
});
|
|
@@ -3946,7 +5818,7 @@ openclaw.command("sync").description("Read SOUL.md and publish/update a v2.0 cap
|
|
|
3946
5818
|
}
|
|
3947
5819
|
let content;
|
|
3948
5820
|
try {
|
|
3949
|
-
content =
|
|
5821
|
+
content = readFileSync4(opts.soulPath, "utf-8");
|
|
3950
5822
|
} catch {
|
|
3951
5823
|
console.error(`Error: cannot read SOUL.md at ${opts.soulPath}`);
|
|
3952
5824
|
process.exit(1);
|
|
@@ -3955,6 +5827,14 @@ openclaw.command("sync").description("Read SOUL.md and publish/update a v2.0 cap
|
|
|
3955
5827
|
try {
|
|
3956
5828
|
const card = publishFromSoulV2(db, content, config.owner);
|
|
3957
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
|
+
}
|
|
3958
5838
|
} catch (err) {
|
|
3959
5839
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3960
5840
|
console.error(`Error: ${msg}`);
|
|
@@ -4007,7 +5887,7 @@ openclaw.command("rules").description("Print HEARTBEAT.md rules block (or inject
|
|
|
4007
5887
|
}
|
|
4008
5888
|
});
|
|
4009
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) => {
|
|
4010
|
-
const { conductAction } = await import("../conduct-
|
|
5890
|
+
const { conductAction } = await import("../conduct-FXLVGKD5.js");
|
|
4011
5891
|
const result = await conductAction(task, opts);
|
|
4012
5892
|
if (opts.json) {
|
|
4013
5893
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -4040,4 +5920,8 @@ Total credits spent: ${result.total_credits ?? 0} cr`);
|
|
|
4040
5920
|
}
|
|
4041
5921
|
}
|
|
4042
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
|
+
});
|
|
4043
5927
|
await program.parseAsync(process.argv);
|