agentbnb 4.0.2 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +31 -2
  2. package/dist/chunk-AUBHR7HH.js +25 -0
  3. package/dist/chunk-B5FTAGFN.js +393 -0
  4. package/dist/{chunk-GGYC5U2Z.js → chunk-BTTL24TZ.js} +29 -91
  5. package/dist/chunk-C6KPAFCC.js +387 -0
  6. package/dist/chunk-CRFCWD6V.js +366 -0
  7. package/dist/chunk-CSATDXZC.js +89 -0
  8. package/dist/{chunk-T7NS2J2B.js → chunk-DFBX3BBD.js} +84 -1
  9. package/dist/{chunk-DNWT5FZQ.js → chunk-EANI2N2V.js} +98 -1
  10. package/dist/{chunk-HH24WMFN.js → chunk-FLY3WIQR.js} +1 -1
  11. package/dist/chunk-HLUEOLSZ.js +62 -0
  12. package/dist/chunk-IVOYM3WG.js +25 -0
  13. package/dist/chunk-LCAIAAG2.js +916 -0
  14. package/dist/chunk-MLS6IGGG.js +294 -0
  15. package/dist/{chunk-4P3EMGL4.js → chunk-MNO4COST.js} +5 -3
  16. package/dist/chunk-NH2FIERR.js +138 -0
  17. package/dist/chunk-UKT6H7YT.js +29 -0
  18. package/dist/{chunk-BH6WGYFB.js → chunk-VE3E4AMH.js} +8 -8
  19. package/dist/{chunk-7NA43XCG.js → chunk-W5BZMKMF.js} +163 -29
  20. package/dist/{chunk-FF226TIV.js → chunk-ZX5623ER.js} +0 -57
  21. package/dist/cli/index.js +362 -4631
  22. package/dist/{conduct-N52JX7RT.js → conduct-KM6ZNJGE.js} +10 -8
  23. package/dist/{conduct-GZQNFTRP.js → conduct-WGTMQND5.js} +10 -8
  24. package/dist/{conductor-mode-XUWGR4ZE.js → conductor-mode-OL2FNOYY.js} +6 -4
  25. package/dist/conductor-mode-VRO7TYW2.js +592 -0
  26. package/dist/execute-CPFSOOO3.js +13 -0
  27. package/dist/execute-IP2QHALV.js +10 -0
  28. package/dist/index.d.ts +19 -8
  29. package/dist/index.js +190 -37
  30. package/dist/peers-CJ7T4RJO.js +13 -0
  31. package/dist/process-guard-CC7CNRQJ.js +176 -0
  32. package/dist/{request-4GQSSM4B.js → request-YOWPXVLQ.js} +13 -10
  33. package/dist/schema-7BSSLZ4S.js +8 -0
  34. package/dist/{serve-skill-TPHZH6BS.js → serve-skill-JHFNR7BW.js} +8 -7
  35. package/dist/{server-365V3GYD.js → server-HKJJWFRG.js} +10 -8
  36. package/dist/service-coordinator-5R4LQW6L.js +4917 -0
  37. package/dist/skills/agentbnb/bootstrap.js +6181 -0
  38. package/dist/websocket-client-WRN3HO73.js +6 -0
  39. package/openclaw.plugin.json +54 -0
  40. package/package.json +11 -2
  41. package/skills/agentbnb/SKILL.md +87 -70
  42. package/skills/agentbnb/bootstrap.test.ts +142 -242
  43. package/skills/agentbnb/bootstrap.ts +88 -95
  44. package/skills/agentbnb/install.sh +97 -27
  45. package/dist/execute-PNGQOMYO.js +0 -10
@@ -0,0 +1,4917 @@
1
+ import {
2
+ StructuredFeedbackSchema
3
+ } from "./chunk-AUBHR7HH.js";
4
+ import {
5
+ KNOWN_API_KEYS,
6
+ announceGateway,
7
+ buildDraftCard,
8
+ detectApiKeys,
9
+ getPricingStats,
10
+ stopAnnouncement
11
+ } from "./chunk-MLS6IGGG.js";
12
+ import {
13
+ deriveAgentId
14
+ } from "./chunk-QITOPASZ.js";
15
+ import {
16
+ createLedger,
17
+ identityAuthPlugin
18
+ } from "./chunk-FLY3WIQR.js";
19
+ import {
20
+ RelayMessageSchema
21
+ } from "./chunk-QT7TEVNV.js";
22
+ import {
23
+ interpolateObject
24
+ } from "./chunk-3MJT4PZG.js";
25
+ import {
26
+ DEFAULT_AUTONOMY_CONFIG,
27
+ getAutonomyTier,
28
+ insertAuditEvent,
29
+ listPendingRequests,
30
+ resolvePendingRequest
31
+ } from "./chunk-CSATDXZC.js";
32
+ import {
33
+ buildReputationMap,
34
+ computeReputation,
35
+ filterCards,
36
+ searchCards
37
+ } from "./chunk-NH2FIERR.js";
38
+ import {
39
+ getConfigDir
40
+ } from "./chunk-75OC6E4F.js";
41
+ import {
42
+ executeCapabilityBatch,
43
+ executeCapabilityRequest
44
+ } from "./chunk-W5BZMKMF.js";
45
+ import "./chunk-UKT6H7YT.js";
46
+ import {
47
+ getActivityFeed,
48
+ getCard,
49
+ getEvolutionHistory,
50
+ getLatestEvolution,
51
+ getRequestLog,
52
+ getSkillRequestCount,
53
+ insertCard,
54
+ insertEvolution,
55
+ insertRequestLog,
56
+ listCards,
57
+ openDatabase,
58
+ updateCard,
59
+ updateSkillAvailability,
60
+ updateSkillIdleRate
61
+ } from "./chunk-DFBX3BBD.js";
62
+ import {
63
+ bootstrapAgent,
64
+ getBalance,
65
+ getFeedbackForProvider,
66
+ getFeedbackForSkill,
67
+ getTransactions,
68
+ holdEscrow,
69
+ insertFeedback,
70
+ migrateOwner,
71
+ openCreditDb,
72
+ releaseEscrow,
73
+ settleEscrow
74
+ } from "./chunk-EANI2N2V.js";
75
+ import {
76
+ generateKeyPair,
77
+ verifyEscrowReceipt
78
+ } from "./chunk-5KFI5X7B.js";
79
+ import {
80
+ AgentBnBError,
81
+ AnyCardSchema
82
+ } from "./chunk-WGZ5AGOX.js";
83
+
84
+ // src/runtime/agent-runtime.ts
85
+ import { readFileSync, existsSync } from "fs";
86
+
87
+ // src/skills/executor.ts
88
+ var SkillExecutor = class {
89
+ skillMap;
90
+ modeMap;
91
+ /**
92
+ * @param configs - Parsed SkillConfig array (from parseSkillsFile).
93
+ * @param modes - Map from skill type string to its executor implementation.
94
+ */
95
+ constructor(configs, modes) {
96
+ this.skillMap = new Map(configs.map((c) => [c.id, c]));
97
+ this.modeMap = modes;
98
+ }
99
+ /**
100
+ * Execute a skill by ID with the given input parameters.
101
+ *
102
+ * Dispatch order:
103
+ * 1. Look up skill config by skillId.
104
+ * 2. Find executor mode by config.type.
105
+ * 3. Invoke mode.execute(), wrap with latency timing.
106
+ * 4. Catch any thrown errors and return as ExecutionResult with success:false.
107
+ *
108
+ * @param skillId - The ID of the skill to execute.
109
+ * @param params - Input parameters for the skill.
110
+ * @returns ExecutionResult including success, result/error, and latency_ms.
111
+ */
112
+ async execute(skillId, params, onProgress) {
113
+ const startTime = Date.now();
114
+ const config = this.skillMap.get(skillId);
115
+ if (!config) {
116
+ return {
117
+ success: false,
118
+ error: `Skill not found: "${skillId}"`,
119
+ latency_ms: Date.now() - startTime
120
+ };
121
+ }
122
+ const mode = this.modeMap.get(config.type);
123
+ if (!mode) {
124
+ return {
125
+ success: false,
126
+ error: `No executor registered for skill type "${config.type}" (skill: "${skillId}")`,
127
+ latency_ms: Date.now() - startTime
128
+ };
129
+ }
130
+ try {
131
+ const modeResult = await mode.execute(config, params, onProgress);
132
+ return {
133
+ ...modeResult,
134
+ latency_ms: Date.now() - startTime
135
+ };
136
+ } catch (err) {
137
+ const message = err instanceof Error ? err.message : String(err);
138
+ return {
139
+ success: false,
140
+ error: message,
141
+ latency_ms: Date.now() - startTime
142
+ };
143
+ }
144
+ }
145
+ /**
146
+ * Returns the IDs of all registered skills.
147
+ *
148
+ * @returns Array of skill ID strings.
149
+ */
150
+ listSkills() {
151
+ return Array.from(this.skillMap.keys());
152
+ }
153
+ /**
154
+ * Returns the SkillConfig for a given skill ID, or undefined if not found.
155
+ *
156
+ * @param skillId - The skill ID to look up.
157
+ * @returns The SkillConfig or undefined.
158
+ */
159
+ getSkillConfig(skillId) {
160
+ return this.skillMap.get(skillId);
161
+ }
162
+ };
163
+ function createSkillExecutor(configs, modes) {
164
+ return new SkillExecutor(configs, modes);
165
+ }
166
+
167
+ // src/skills/skill-config.ts
168
+ import { z } from "zod";
169
+ import yaml from "js-yaml";
170
+ var PricingSchema = z.object({
171
+ credits_per_call: z.number().nonnegative(),
172
+ credits_per_minute: z.number().nonnegative().optional(),
173
+ free_tier: z.number().nonnegative().optional()
174
+ });
175
+ var ApiAuthSchema = z.discriminatedUnion("type", [
176
+ z.object({
177
+ type: z.literal("bearer"),
178
+ token: z.string()
179
+ }),
180
+ z.object({
181
+ type: z.literal("apikey"),
182
+ header: z.string().default("X-API-Key"),
183
+ key: z.string()
184
+ }),
185
+ z.object({
186
+ type: z.literal("basic"),
187
+ username: z.string(),
188
+ password: z.string()
189
+ })
190
+ ]);
191
+ var ApiSkillConfigSchema = z.object({
192
+ id: z.string().min(1),
193
+ type: z.literal("api"),
194
+ name: z.string().min(1),
195
+ endpoint: z.string().min(1),
196
+ method: z.enum(["GET", "POST", "PUT", "DELETE"]),
197
+ auth: ApiAuthSchema.optional(),
198
+ input_mapping: z.record(z.string()).default({}),
199
+ output_mapping: z.record(z.string()).default({}),
200
+ pricing: PricingSchema,
201
+ timeout_ms: z.number().positive().default(3e4),
202
+ retries: z.number().nonnegative().int().default(0),
203
+ provider: z.string().optional()
204
+ });
205
+ var PipelineStepSchema = z.union([
206
+ z.object({
207
+ skill_id: z.string().min(1),
208
+ input_mapping: z.record(z.string()).default({})
209
+ }),
210
+ z.object({
211
+ command: z.string().min(1),
212
+ input_mapping: z.record(z.string()).default({})
213
+ })
214
+ ]);
215
+ var PipelineSkillConfigSchema = z.object({
216
+ id: z.string().min(1),
217
+ type: z.literal("pipeline"),
218
+ name: z.string().min(1),
219
+ steps: z.array(PipelineStepSchema).min(1),
220
+ pricing: PricingSchema,
221
+ timeout_ms: z.number().positive().optional()
222
+ });
223
+ var OpenClawSkillConfigSchema = z.object({
224
+ id: z.string().min(1),
225
+ type: z.literal("openclaw"),
226
+ name: z.string().min(1),
227
+ agent_name: z.string().min(1),
228
+ channel: z.enum(["telegram", "webhook", "process"]),
229
+ pricing: PricingSchema,
230
+ timeout_ms: z.number().positive().optional()
231
+ });
232
+ var CommandSkillConfigSchema = z.object({
233
+ id: z.string().min(1),
234
+ type: z.literal("command"),
235
+ name: z.string().min(1),
236
+ command: z.string().min(1),
237
+ output_type: z.enum(["json", "text", "file"]),
238
+ allowed_commands: z.array(z.string()).optional(),
239
+ working_dir: z.string().optional(),
240
+ timeout_ms: z.number().positive().default(3e4),
241
+ pricing: PricingSchema
242
+ });
243
+ var ConductorSkillConfigSchema = z.object({
244
+ id: z.string().min(1),
245
+ type: z.literal("conductor"),
246
+ name: z.string().min(1),
247
+ conductor_skill: z.enum(["orchestrate", "plan"]),
248
+ pricing: PricingSchema,
249
+ timeout_ms: z.number().positive().optional()
250
+ });
251
+ var SkillConfigSchema = z.discriminatedUnion("type", [
252
+ ApiSkillConfigSchema,
253
+ PipelineSkillConfigSchema,
254
+ OpenClawSkillConfigSchema,
255
+ CommandSkillConfigSchema,
256
+ ConductorSkillConfigSchema
257
+ ]);
258
+ var SkillsFileSchema = z.object({
259
+ skills: z.array(SkillConfigSchema)
260
+ });
261
+ function expandEnvVars(value) {
262
+ return value.replace(/\$\{([^}]+)\}/g, (_match, varName) => {
263
+ if (/[.a-z]/.test(varName)) {
264
+ return _match;
265
+ }
266
+ const envValue = process.env[varName];
267
+ if (envValue === void 0) {
268
+ throw new Error(`Environment variable "${varName}" is not defined`);
269
+ }
270
+ return envValue;
271
+ });
272
+ }
273
+ function expandEnvVarsDeep(value) {
274
+ if (typeof value === "string") {
275
+ return expandEnvVars(value);
276
+ }
277
+ if (Array.isArray(value)) {
278
+ return value.map(expandEnvVarsDeep);
279
+ }
280
+ if (value !== null && typeof value === "object") {
281
+ const result = {};
282
+ for (const [k, v] of Object.entries(value)) {
283
+ result[k] = expandEnvVarsDeep(v);
284
+ }
285
+ return result;
286
+ }
287
+ return value;
288
+ }
289
+ function parseSkillsFile(yamlContent) {
290
+ const raw = yaml.load(yamlContent);
291
+ const expanded = expandEnvVarsDeep(raw);
292
+ const result = SkillsFileSchema.parse(expanded);
293
+ return result.skills;
294
+ }
295
+
296
+ // src/skills/api-executor.ts
297
+ function parseMappingTarget(mapping) {
298
+ const dotIndex = mapping.indexOf(".");
299
+ if (dotIndex < 0) {
300
+ throw new Error(`Invalid input_mapping format: "${mapping}" (expected "target.key")`);
301
+ }
302
+ const target = mapping.slice(0, dotIndex);
303
+ const key = mapping.slice(dotIndex + 1);
304
+ if (!["body", "query", "path", "header"].includes(target)) {
305
+ throw new Error(
306
+ `Invalid mapping target "${target}" in "${mapping}" (must be body|query|path|header)`
307
+ );
308
+ }
309
+ return { target, key };
310
+ }
311
+ function extractByPath(obj, dotPath) {
312
+ const normalizedPath = dotPath.startsWith("response.") ? dotPath.slice("response.".length) : dotPath;
313
+ const parts = normalizedPath.split(".");
314
+ let current = obj;
315
+ for (const part of parts) {
316
+ if (current === null || typeof current !== "object") {
317
+ return void 0;
318
+ }
319
+ current = current[part];
320
+ }
321
+ return current;
322
+ }
323
+ function buildAuthHeaders(auth) {
324
+ if (!auth) return {};
325
+ switch (auth.type) {
326
+ case "bearer":
327
+ return { Authorization: `Bearer ${auth.token}` };
328
+ case "apikey":
329
+ return { [auth.header]: auth.key };
330
+ case "basic": {
331
+ const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString("base64");
332
+ return { Authorization: `Basic ${encoded}` };
333
+ }
334
+ }
335
+ }
336
+ function applyInputMapping(params, mapping) {
337
+ const body = {};
338
+ const query = {};
339
+ const pathParams = {};
340
+ const headers = {};
341
+ for (const [paramName, mappingValue] of Object.entries(mapping)) {
342
+ const value = params[paramName];
343
+ if (value === void 0) continue;
344
+ const { target, key } = parseMappingTarget(mappingValue);
345
+ switch (target) {
346
+ case "body":
347
+ body[key] = value;
348
+ break;
349
+ case "query":
350
+ query[key] = String(value);
351
+ break;
352
+ case "path":
353
+ pathParams[key] = String(value);
354
+ break;
355
+ case "header":
356
+ headers[key] = String(value).replace(/[\r\n]/g, "");
357
+ break;
358
+ }
359
+ }
360
+ return { body, query, pathParams, headers };
361
+ }
362
+ function sleep(ms) {
363
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
364
+ }
365
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 503]);
366
+ var ApiExecutor = class {
367
+ /**
368
+ * Execute an API call described by the given skill config.
369
+ *
370
+ * @param config - The validated SkillConfig (must be ApiSkillConfig).
371
+ * @param params - Input parameters to map to the HTTP request.
372
+ * @returns Partial ExecutionResult (without latency_ms — added by SkillExecutor).
373
+ */
374
+ async execute(config, params) {
375
+ const apiConfig = config;
376
+ const { body, query, pathParams, headers: mappedHeaders } = applyInputMapping(
377
+ params,
378
+ apiConfig.input_mapping
379
+ );
380
+ let url = apiConfig.endpoint;
381
+ for (const [key, value] of Object.entries(pathParams)) {
382
+ url = url.replace(`{${key}}`, encodeURIComponent(value));
383
+ }
384
+ if (Object.keys(query).length > 0) {
385
+ const qs = new URLSearchParams(query).toString();
386
+ url = `${url}?${qs}`;
387
+ }
388
+ const authHeaders = buildAuthHeaders(apiConfig.auth);
389
+ const requestHeaders = {
390
+ ...authHeaders,
391
+ ...mappedHeaders
392
+ };
393
+ const hasBody = ["POST", "PUT"].includes(apiConfig.method);
394
+ if (hasBody) {
395
+ requestHeaders["Content-Type"] = "application/json";
396
+ }
397
+ const maxAttempts = (apiConfig.retries ?? 0) + 1;
398
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
399
+ if (attempt > 0) {
400
+ await sleep(100 * Math.pow(2, attempt - 1));
401
+ }
402
+ const controller = new AbortController();
403
+ const timeoutId = setTimeout(
404
+ () => controller.abort(),
405
+ apiConfig.timeout_ms ?? 3e4
406
+ );
407
+ let response;
408
+ try {
409
+ response = await fetch(url, {
410
+ method: apiConfig.method,
411
+ headers: requestHeaders,
412
+ body: hasBody ? JSON.stringify(body) : void 0,
413
+ signal: controller.signal
414
+ });
415
+ } catch (err) {
416
+ clearTimeout(timeoutId);
417
+ const message = err instanceof Error ? err.message : String(err);
418
+ const isAbort = err instanceof Error && err.name === "AbortError" || message.toLowerCase().includes("abort");
419
+ if (isAbort) {
420
+ return { success: false, error: `Request timeout after ${apiConfig.timeout_ms}ms` };
421
+ }
422
+ return { success: false, error: message };
423
+ } finally {
424
+ clearTimeout(timeoutId);
425
+ }
426
+ if (!response.ok && RETRYABLE_STATUSES.has(response.status) && attempt < maxAttempts - 1) {
427
+ continue;
428
+ }
429
+ if (!response.ok) {
430
+ return {
431
+ success: false,
432
+ error: `HTTP ${response.status} from ${apiConfig.endpoint}`
433
+ };
434
+ }
435
+ const responseBody = await response.json();
436
+ const outputMapping = apiConfig.output_mapping;
437
+ if (Object.keys(outputMapping).length === 0) {
438
+ return { success: true, result: responseBody };
439
+ }
440
+ const mappedOutput = {};
441
+ for (const [outputKey, path] of Object.entries(outputMapping)) {
442
+ mappedOutput[outputKey] = extractByPath(responseBody, path);
443
+ }
444
+ return { success: true, result: mappedOutput };
445
+ }
446
+ return { success: false, error: "Unexpected: retry loop exhausted" };
447
+ }
448
+ };
449
+
450
+ // src/skills/pipeline-executor.ts
451
+ import { execFile } from "child_process";
452
+ import { promisify } from "util";
453
+ var execFileAsync = promisify(execFile);
454
+ function shellEscape(value) {
455
+ return "'" + value.replace(/'/g, "'\\''") + "'";
456
+ }
457
+ function safeInterpolateCommand(template, context) {
458
+ return template.replace(/\$\{([^}]+)\}/g, (_match, expr) => {
459
+ const parts = expr.split(".");
460
+ let current = context;
461
+ for (const part of parts) {
462
+ if (current === null || typeof current !== "object") return "";
463
+ const bracketMatch = part.match(/^(\w+)\[(\d+)\]$/);
464
+ if (bracketMatch) {
465
+ current = current[bracketMatch[1]];
466
+ if (Array.isArray(current)) {
467
+ current = current[parseInt(bracketMatch[2], 10)];
468
+ } else {
469
+ return "";
470
+ }
471
+ } else {
472
+ current = current[part];
473
+ }
474
+ }
475
+ if (current === void 0 || current === null) return "";
476
+ return shellEscape(String(current));
477
+ });
478
+ }
479
+ var PipelineExecutor = class {
480
+ /**
481
+ * @param skillExecutor - The parent SkillExecutor used to dispatch sub-skill calls.
482
+ */
483
+ constructor(skillExecutor) {
484
+ this.skillExecutor = skillExecutor;
485
+ }
486
+ /**
487
+ * Execute a pipeline skill config sequentially.
488
+ *
489
+ * Algorithm:
490
+ * 1. Initialise context: { params, steps: [], prev: { result: null } }
491
+ * 2. For each step:
492
+ * a. Resolve input_mapping keys against current context via interpolateObject.
493
+ * b. If step has `skill_id`: dispatch via skillExecutor.execute(). On failure → stop.
494
+ * c. If step has `command`: interpolate command string, run via exec(). On non-zero exit → stop.
495
+ * d. Store step result in context.steps[i] and context.prev.
496
+ * 3. Return success with final step result (or null for empty pipeline).
497
+ *
498
+ * @param config - The PipelineSkillConfig for this skill.
499
+ * @param params - Input parameters from the caller.
500
+ * @returns Partial ExecutionResult (without latency_ms — added by SkillExecutor wrapper).
501
+ */
502
+ async execute(config, params, onProgress) {
503
+ const pipelineConfig = config;
504
+ const steps = pipelineConfig.steps ?? [];
505
+ if (steps.length === 0) {
506
+ return { success: true, result: null };
507
+ }
508
+ const context = {
509
+ params,
510
+ steps: [],
511
+ prev: { result: null }
512
+ };
513
+ for (let i = 0; i < steps.length; i++) {
514
+ const step = steps[i];
515
+ if (step === void 0) {
516
+ return {
517
+ success: false,
518
+ error: `Step ${i} failed: step definition is undefined`
519
+ };
520
+ }
521
+ const resolvedInputs = interpolateObject(
522
+ step.input_mapping,
523
+ context
524
+ );
525
+ let stepResult;
526
+ if ("skill_id" in step && step.skill_id) {
527
+ const subResult = await this.skillExecutor.execute(
528
+ step.skill_id,
529
+ resolvedInputs
530
+ );
531
+ if (!subResult.success) {
532
+ return {
533
+ success: false,
534
+ error: `Step ${i} failed: ${subResult.error ?? "unknown error"}`
535
+ };
536
+ }
537
+ stepResult = subResult.result;
538
+ } else if ("command" in step && step.command) {
539
+ const interpolatedCommand = safeInterpolateCommand(
540
+ step.command,
541
+ context
542
+ );
543
+ try {
544
+ const { stdout } = await execFileAsync("/bin/sh", ["-c", interpolatedCommand], { timeout: 3e4 });
545
+ stepResult = stdout.trim();
546
+ } catch (err) {
547
+ const message = err instanceof Error ? err.message : String(err);
548
+ return {
549
+ success: false,
550
+ error: `Step ${i} failed: ${message}`
551
+ };
552
+ }
553
+ } else {
554
+ return {
555
+ success: false,
556
+ error: `Step ${i} failed: step must have either "skill_id" or "command"`
557
+ };
558
+ }
559
+ context.steps.push({ result: stepResult });
560
+ context.prev = { result: stepResult };
561
+ if (onProgress && i < steps.length - 1) {
562
+ onProgress({
563
+ step: i + 1,
564
+ total: steps.length,
565
+ message: `Completed step ${i + 1}/${steps.length}`
566
+ });
567
+ }
568
+ }
569
+ const lastStep = context.steps[context.steps.length - 1];
570
+ return {
571
+ success: true,
572
+ result: lastStep !== void 0 ? lastStep.result : null
573
+ };
574
+ }
575
+ };
576
+
577
+ // src/skills/openclaw-bridge.ts
578
+ import { execFileSync } from "child_process";
579
+ var DEFAULT_BASE_URL = "http://localhost:3000";
580
+ var DEFAULT_TIMEOUT_MS = 6e4;
581
+ function buildPayload(config, params) {
582
+ return {
583
+ task: config.name,
584
+ params,
585
+ source: "agentbnb",
586
+ skill_id: config.id
587
+ };
588
+ }
589
+ async function executeWebhook(config, payload) {
590
+ const baseUrl = process.env["OPENCLAW_BASE_URL"] ?? DEFAULT_BASE_URL;
591
+ const url = `${baseUrl}/openclaw/${config.agent_name}/task`;
592
+ const timeoutMs = config.timeout_ms ?? DEFAULT_TIMEOUT_MS;
593
+ const controller = new AbortController();
594
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
595
+ try {
596
+ const response = await fetch(url, {
597
+ method: "POST",
598
+ headers: { "Content-Type": "application/json" },
599
+ body: JSON.stringify(payload),
600
+ signal: controller.signal
601
+ });
602
+ if (!response.ok) {
603
+ return {
604
+ success: false,
605
+ error: `Webhook returned HTTP ${response.status}: ${response.statusText}`
606
+ };
607
+ }
608
+ const result = await response.json();
609
+ return { success: true, result };
610
+ } catch (err) {
611
+ if (err instanceof Error && err.name === "AbortError") {
612
+ return {
613
+ success: false,
614
+ error: `OpenClaw webhook timed out after ${timeoutMs}ms`
615
+ };
616
+ }
617
+ const message = err instanceof Error ? err.message : String(err);
618
+ return { success: false, error: message };
619
+ } finally {
620
+ clearTimeout(timer);
621
+ }
622
+ }
623
+ function validateAgentName(name) {
624
+ return /^[a-zA-Z0-9._-]+$/.test(name);
625
+ }
626
+ function executeProcess(config, payload) {
627
+ const timeoutMs = config.timeout_ms ?? DEFAULT_TIMEOUT_MS;
628
+ if (!validateAgentName(config.agent_name)) {
629
+ return {
630
+ success: false,
631
+ error: `Invalid agent name: "${config.agent_name}" (only alphanumeric, hyphens, underscores, dots allowed)`
632
+ };
633
+ }
634
+ const inputJson = JSON.stringify(payload);
635
+ try {
636
+ const stdout = execFileSync("openclaw", ["run", config.agent_name, "--input", inputJson], {
637
+ timeout: timeoutMs
638
+ });
639
+ const text = stdout.toString().trim();
640
+ const result = JSON.parse(text);
641
+ return { success: true, result };
642
+ } catch (err) {
643
+ const message = err instanceof Error ? err.message : String(err);
644
+ return { success: false, error: message };
645
+ }
646
+ }
647
+ async function executeTelegram(config, payload) {
648
+ const token = process.env["TELEGRAM_BOT_TOKEN"];
649
+ if (!token) {
650
+ return {
651
+ success: false,
652
+ error: "TELEGRAM_BOT_TOKEN environment variable is not set"
653
+ };
654
+ }
655
+ const chatId = process.env["TELEGRAM_CHAT_ID"];
656
+ if (!chatId) {
657
+ return {
658
+ success: false,
659
+ error: "TELEGRAM_CHAT_ID environment variable is not set"
660
+ };
661
+ }
662
+ const text = `[AgentBnB] Skill: ${config.name} (${config.id})
663
+ Agent: ${config.agent_name}
664
+ Params: ${JSON.stringify(payload.params ?? {})}`;
665
+ const url = `https://api.telegram.org/bot${token}/sendMessage`;
666
+ try {
667
+ await fetch(url, {
668
+ method: "POST",
669
+ headers: { "Content-Type": "application/json" },
670
+ body: JSON.stringify({ chat_id: chatId, text })
671
+ });
672
+ return {
673
+ success: true,
674
+ result: { sent: true, channel: "telegram" }
675
+ };
676
+ } catch (err) {
677
+ const message = err instanceof Error ? err.message : String(err);
678
+ return { success: false, error: message };
679
+ }
680
+ }
681
+ var OpenClawBridge = class {
682
+ /**
683
+ * Execute a skill with the given config and input parameters.
684
+ *
685
+ * @param config - The SkillConfig for this skill (must be type 'openclaw').
686
+ * @param params - Input parameters passed by the caller.
687
+ * @returns Partial ExecutionResult without latency_ms.
688
+ */
689
+ async execute(config, params) {
690
+ const ocConfig = config;
691
+ const payload = buildPayload(ocConfig, params);
692
+ switch (ocConfig.channel) {
693
+ case "webhook":
694
+ return executeWebhook(ocConfig, payload);
695
+ case "process":
696
+ return executeProcess(ocConfig, payload);
697
+ case "telegram":
698
+ return executeTelegram(ocConfig, payload);
699
+ default: {
700
+ const unknownChannel = ocConfig.channel;
701
+ return {
702
+ success: false,
703
+ error: `Unknown OpenClaw channel: "${String(unknownChannel)}"`
704
+ };
705
+ }
706
+ }
707
+ }
708
+ };
709
+
710
+ // src/skills/command-executor.ts
711
+ import { execFile as execFile2 } from "child_process";
712
+ function shellEscape2(value) {
713
+ return "'" + value.replace(/'/g, "'\\''") + "'";
714
+ }
715
+ function safeInterpolateCommand2(template, context) {
716
+ return template.replace(/\$\{([^}]+)\}/g, (_match, expr) => {
717
+ const parts = expr.split(".");
718
+ let current = context;
719
+ for (const part of parts) {
720
+ if (current === null || typeof current !== "object") return "";
721
+ const bracketMatch = part.match(/^(\w+)\[(\d+)\]$/);
722
+ if (bracketMatch) {
723
+ current = current[bracketMatch[1]];
724
+ if (Array.isArray(current)) {
725
+ current = current[parseInt(bracketMatch[2], 10)];
726
+ } else {
727
+ return "";
728
+ }
729
+ } else {
730
+ current = current[part];
731
+ }
732
+ }
733
+ if (current === void 0 || current === null) return "";
734
+ return shellEscape2(String(current));
735
+ });
736
+ }
737
+ function execFileAsync2(file, args, options) {
738
+ return new Promise((resolve2, reject) => {
739
+ execFile2(file, args, options, (error, stdout, stderr) => {
740
+ const stdoutStr = typeof stdout === "string" ? stdout : stdout.toString();
741
+ const stderrStr = typeof stderr === "string" ? stderr : stderr.toString();
742
+ if (error) {
743
+ const enriched = Object.assign(error, { stderr: stderrStr });
744
+ reject(enriched);
745
+ } else {
746
+ resolve2({ stdout: stdoutStr, stderr: stderrStr });
747
+ }
748
+ });
749
+ });
750
+ }
751
+ var CommandExecutor = class {
752
+ /**
753
+ * Execute a command skill with the provided parameters.
754
+ *
755
+ * Steps:
756
+ * 1. Security check: base command must be in `allowed_commands` if set.
757
+ * 2. Interpolate `config.command` using `{ params }` context.
758
+ * 3. Run via `child_process.exec` with timeout and cwd.
759
+ * 4. Parse stdout based on `output_type`: text | json | file.
760
+ *
761
+ * @param config - Validated CommandSkillConfig.
762
+ * @param params - Input parameters passed by the caller.
763
+ * @returns Partial ExecutionResult (without latency_ms).
764
+ */
765
+ async execute(config, params) {
766
+ const cmdConfig = config;
767
+ const baseCommand = cmdConfig.command.trim().split(/\s+/)[0] ?? "";
768
+ if (cmdConfig.allowed_commands && cmdConfig.allowed_commands.length > 0) {
769
+ if (!cmdConfig.allowed_commands.includes(baseCommand)) {
770
+ return {
771
+ success: false,
772
+ error: `Command not allowed: "${baseCommand}". Allowed: ${cmdConfig.allowed_commands.join(", ")}`
773
+ };
774
+ }
775
+ }
776
+ const interpolatedCommand = safeInterpolateCommand2(cmdConfig.command, { params });
777
+ const timeout = cmdConfig.timeout_ms ?? 3e4;
778
+ const cwd = cmdConfig.working_dir ?? process.cwd();
779
+ let stdout;
780
+ try {
781
+ const result = await execFileAsync2("/bin/sh", ["-c", interpolatedCommand], {
782
+ timeout,
783
+ cwd,
784
+ maxBuffer: 10 * 1024 * 1024
785
+ // 10 MB
786
+ });
787
+ stdout = result.stdout;
788
+ } catch (err) {
789
+ if (err instanceof Error) {
790
+ const message = err.message;
791
+ const stderrContent = err.stderr ?? "";
792
+ if (message.includes("timed out") || message.includes("ETIMEDOUT") || err.code === "ETIMEDOUT") {
793
+ return {
794
+ success: false,
795
+ error: `Command timed out after ${timeout}ms`
796
+ };
797
+ }
798
+ return {
799
+ success: false,
800
+ error: stderrContent.trim() || message
801
+ };
802
+ }
803
+ return {
804
+ success: false,
805
+ error: String(err)
806
+ };
807
+ }
808
+ const rawOutput = stdout.trim();
809
+ switch (cmdConfig.output_type) {
810
+ case "text":
811
+ return { success: true, result: rawOutput };
812
+ case "json": {
813
+ try {
814
+ const parsed = JSON.parse(rawOutput);
815
+ return { success: true, result: parsed };
816
+ } catch {
817
+ return {
818
+ success: false,
819
+ error: `Failed to parse JSON output: ${rawOutput.slice(0, 100)}`
820
+ };
821
+ }
822
+ }
823
+ case "file":
824
+ return { success: true, result: { file_path: rawOutput } };
825
+ default:
826
+ return {
827
+ success: false,
828
+ error: `Unknown output_type: ${String(cmdConfig.output_type)}`
829
+ };
830
+ }
831
+ }
832
+ };
833
+
834
+ // src/runtime/agent-runtime.ts
835
+ var AgentRuntime = class {
836
+ /** The registry SQLite database instance */
837
+ registryDb;
838
+ /** The credit SQLite database instance */
839
+ creditDb;
840
+ /** The agent owner identifier */
841
+ owner;
842
+ /** Registered background Cron jobs */
843
+ jobs = [];
844
+ /**
845
+ * The SkillExecutor instance, populated by start() if skillsYamlPath is set.
846
+ * Undefined if no skills.yaml was provided or the file does not exist.
847
+ */
848
+ skillExecutor;
849
+ draining = false;
850
+ orphanedEscrowAgeMinutes;
851
+ skillsYamlPath;
852
+ conductorEnabled;
853
+ conductorToken;
854
+ /**
855
+ * Creates a new AgentRuntime instance.
856
+ * Opens both databases with WAL mode, foreign_keys=ON, and busy_timeout=5000.
857
+ * Schema migrations are applied via openDatabase() and openCreditDb().
858
+ *
859
+ * @param options - Runtime configuration options.
860
+ */
861
+ constructor(options) {
862
+ this.owner = options.owner;
863
+ this.orphanedEscrowAgeMinutes = options.orphanedEscrowAgeMinutes ?? 10;
864
+ this.skillsYamlPath = options.skillsYamlPath;
865
+ this.conductorEnabled = options.conductorEnabled ?? false;
866
+ this.conductorToken = options.conductorToken ?? "";
867
+ this.registryDb = openDatabase(options.registryDbPath);
868
+ this.creditDb = openCreditDb(options.creditDbPath);
869
+ this.registryDb.pragma("busy_timeout = 5000");
870
+ this.creditDb.pragma("busy_timeout = 5000");
871
+ }
872
+ /**
873
+ * Registers a Cron job to be managed by this runtime.
874
+ * Registered jobs will be stopped automatically on shutdown().
875
+ *
876
+ * @param job - The Cron job instance to register.
877
+ */
878
+ registerJob(job) {
879
+ this.jobs.push(job);
880
+ }
881
+ /**
882
+ * Starts the runtime.
883
+ * Recovers orphaned escrows (held escrows older than orphanedEscrowAgeMinutes).
884
+ * If skillsYamlPath is set and the file exists, initializes SkillExecutor with
885
+ * all four executor modes (api, pipeline, openclaw, command).
886
+ *
887
+ * Call this after creating the runtime and before accepting requests.
888
+ */
889
+ async start() {
890
+ await this.recoverOrphanedEscrows();
891
+ await this.initSkillExecutor();
892
+ }
893
+ /**
894
+ * Initializes SkillExecutor from skills.yaml if skillsYamlPath is configured
895
+ * and the file exists on disk.
896
+ *
897
+ * Uses a mutable Map to handle the PipelineExecutor circular dependency:
898
+ * 1. Create an empty modes Map and a SkillExecutor (holds Map reference).
899
+ * 2. Create PipelineExecutor passing the SkillExecutor (for sub-skill dispatch).
900
+ * 3. Populate the Map with all 4 modes — SkillExecutor sees them via reference.
901
+ */
902
+ async initSkillExecutor() {
903
+ const hasSkillsYaml = this.skillsYamlPath && existsSync(this.skillsYamlPath);
904
+ if (!hasSkillsYaml && !this.conductorEnabled) {
905
+ return;
906
+ }
907
+ let configs = [];
908
+ if (hasSkillsYaml) {
909
+ const yamlContent = readFileSync(this.skillsYamlPath, "utf8");
910
+ configs = parseSkillsFile(yamlContent);
911
+ }
912
+ const modes = /* @__PURE__ */ new Map();
913
+ if (this.conductorEnabled) {
914
+ const { ConductorMode } = await import("./conductor-mode-OL2FNOYY.js");
915
+ const { registerConductorCard, CONDUCTOR_OWNER } = await import("./card-RSGDCHCV.js");
916
+ const { loadPeers } = await import("./peers-K7FSHPN3.js");
917
+ registerConductorCard(this.registryDb);
918
+ const resolveAgentUrl = (owner) => {
919
+ const peers = loadPeers();
920
+ const peer = peers.find((p) => p.name.toLowerCase() === owner.toLowerCase());
921
+ if (!peer) {
922
+ throw new Error(
923
+ `No peer found for agent owner "${owner}". Add with: agentbnb connect ${owner} <url> <token>`
924
+ );
925
+ }
926
+ const stmt = this.registryDb.prepare(
927
+ "SELECT id FROM capability_cards WHERE owner = ? LIMIT 1"
928
+ );
929
+ const row = stmt.get(owner);
930
+ const cardId = row?.id ?? owner;
931
+ return { url: peer.url, cardId };
932
+ };
933
+ const conductorMode = new ConductorMode({
934
+ db: this.registryDb,
935
+ creditDb: this.creditDb,
936
+ conductorOwner: CONDUCTOR_OWNER,
937
+ gatewayToken: this.conductorToken,
938
+ resolveAgentUrl,
939
+ maxBudget: 100
940
+ });
941
+ modes.set("conductor", conductorMode);
942
+ configs.push(
943
+ {
944
+ id: "orchestrate",
945
+ type: "conductor",
946
+ name: "Orchestrate",
947
+ conductor_skill: "orchestrate",
948
+ pricing: { credits_per_call: 5 }
949
+ },
950
+ {
951
+ id: "plan",
952
+ type: "conductor",
953
+ name: "Plan",
954
+ conductor_skill: "plan",
955
+ pricing: { credits_per_call: 1 }
956
+ }
957
+ );
958
+ }
959
+ const executor = createSkillExecutor(configs, modes);
960
+ if (hasSkillsYaml) {
961
+ const pipelineExecutor = new PipelineExecutor(executor);
962
+ modes.set("api", new ApiExecutor());
963
+ modes.set("pipeline", pipelineExecutor);
964
+ modes.set("openclaw", new OpenClawBridge());
965
+ modes.set("command", new CommandExecutor());
966
+ }
967
+ this.skillExecutor = executor;
968
+ }
969
+ /**
970
+ * Recovers orphaned escrows by releasing them.
971
+ * Orphaned escrows are 'held' escrows older than the configured age threshold.
972
+ * Errors during individual release are swallowed (escrow may have settled between query and release).
973
+ */
974
+ async recoverOrphanedEscrows() {
975
+ const cutoff = new Date(
976
+ Date.now() - this.orphanedEscrowAgeMinutes * 60 * 1e3
977
+ ).toISOString();
978
+ const orphaned = this.creditDb.prepare(
979
+ "SELECT id FROM credit_escrow WHERE status = 'held' AND created_at < ?"
980
+ ).all(cutoff);
981
+ for (const row of orphaned) {
982
+ try {
983
+ releaseEscrow(this.creditDb, row.id);
984
+ } catch {
985
+ }
986
+ }
987
+ }
988
+ /**
989
+ * Shuts down the runtime gracefully.
990
+ * Sets draining flag, stops all registered Cron jobs, and closes both databases.
991
+ * Idempotent — safe to call multiple times.
992
+ */
993
+ async shutdown() {
994
+ if (this.draining) {
995
+ return;
996
+ }
997
+ this.draining = true;
998
+ for (const job of this.jobs) {
999
+ job.stop();
1000
+ }
1001
+ try {
1002
+ this.registryDb.close();
1003
+ } catch {
1004
+ }
1005
+ try {
1006
+ this.creditDb.close();
1007
+ } catch {
1008
+ }
1009
+ }
1010
+ /**
1011
+ * Returns true if the runtime is shutting down or has shut down.
1012
+ * Background handlers should check this before processing new requests.
1013
+ */
1014
+ get isDraining() {
1015
+ return this.draining;
1016
+ }
1017
+ };
1018
+
1019
+ // src/gateway/server.ts
1020
+ import Fastify from "fastify";
1021
+ var VERSION = "0.0.1";
1022
+ function createGatewayServer(opts) {
1023
+ const {
1024
+ registryDb,
1025
+ creditDb,
1026
+ tokens,
1027
+ handlerUrl,
1028
+ timeoutMs = 3e5,
1029
+ silent = false,
1030
+ skillExecutor
1031
+ } = opts;
1032
+ const fastify = Fastify({ logger: !silent });
1033
+ const tokenSet = new Set(tokens);
1034
+ fastify.addHook("onRequest", async (request) => {
1035
+ if (request.method === "GET" && request.url === "/health") return;
1036
+ const auth = request.headers.authorization;
1037
+ if (auth && auth.startsWith("Bearer ")) {
1038
+ const token = auth.slice("Bearer ".length).trim();
1039
+ if (tokenSet.has(token)) {
1040
+ request._authenticated = true;
1041
+ }
1042
+ }
1043
+ });
1044
+ fastify.addHook("preHandler", async (request, reply) => {
1045
+ if (request._authenticated) return;
1046
+ if (request.method === "GET" && request.url === "/health") return;
1047
+ const agentId = request.headers["x-agent-id"];
1048
+ const publicKeyHex = request.headers["x-agent-public-key"];
1049
+ const signature = request.headers["x-agent-signature"];
1050
+ if (agentId && publicKeyHex && signature) {
1051
+ try {
1052
+ const publicKeyBuf = Buffer.from(publicKeyHex, "hex");
1053
+ const body = request.body;
1054
+ if (body && typeof body === "object") {
1055
+ const valid = verifyEscrowReceipt(body, signature, publicKeyBuf);
1056
+ if (valid) return;
1057
+ }
1058
+ } catch {
1059
+ }
1060
+ }
1061
+ await reply.status(401).send({
1062
+ jsonrpc: "2.0",
1063
+ id: null,
1064
+ error: { code: -32e3, message: "Unauthorized: provide Bearer token or X-Agent-Id/Signature headers" }
1065
+ });
1066
+ });
1067
+ fastify.get("/health", async () => {
1068
+ return { status: "ok", version: VERSION, uptime: process.uptime() };
1069
+ });
1070
+ fastify.post("/rpc", async (request, reply) => {
1071
+ const body = request.body;
1072
+ if (body.jsonrpc !== "2.0" || !body.method) {
1073
+ return reply.status(400).send({
1074
+ jsonrpc: "2.0",
1075
+ id: body.id ?? null,
1076
+ error: { code: -32600, message: "Invalid Request" }
1077
+ });
1078
+ }
1079
+ const id = body.id ?? null;
1080
+ if (body.method !== "capability.execute") {
1081
+ return reply.send({
1082
+ jsonrpc: "2.0",
1083
+ id,
1084
+ error: { code: -32601, message: "Method not found" }
1085
+ });
1086
+ }
1087
+ const params = body.params ?? {};
1088
+ const cardId = params.card_id;
1089
+ const skillId = params.skill_id;
1090
+ if (!cardId) {
1091
+ return reply.send({
1092
+ jsonrpc: "2.0",
1093
+ id,
1094
+ error: { code: -32602, message: "Invalid params: card_id required" }
1095
+ });
1096
+ }
1097
+ const requester = params.requester ?? "unknown";
1098
+ const receipt = params.escrow_receipt;
1099
+ const result = await executeCapabilityRequest({
1100
+ registryDb,
1101
+ creditDb,
1102
+ cardId,
1103
+ skillId,
1104
+ params,
1105
+ requester,
1106
+ escrowReceipt: receipt,
1107
+ skillExecutor,
1108
+ handlerUrl,
1109
+ timeoutMs
1110
+ });
1111
+ if (result.success) {
1112
+ return reply.send({ jsonrpc: "2.0", id, result: result.result });
1113
+ } else {
1114
+ return reply.send({ jsonrpc: "2.0", id, error: result.error });
1115
+ }
1116
+ });
1117
+ return fastify;
1118
+ }
1119
+
1120
+ // src/registry/server.ts
1121
+ import Fastify2 from "fastify";
1122
+ import cors from "@fastify/cors";
1123
+ import swagger from "@fastify/swagger";
1124
+ import swaggerUi from "@fastify/swagger-ui";
1125
+ import fastifyStatic from "@fastify/static";
1126
+ import fastifyWebsocket from "@fastify/websocket";
1127
+ import { join, dirname } from "path";
1128
+ import { fileURLToPath } from "url";
1129
+ import { existsSync as existsSync2 } from "fs";
1130
+ import { z as z5 } from "zod";
1131
+
1132
+ // src/relay/websocket-relay.ts
1133
+ import { randomUUID as randomUUID3 } from "crypto";
1134
+
1135
+ // src/relay/relay-credit.ts
1136
+ function lookupCardPrice(registryDb, cardId, skillId) {
1137
+ const row = registryDb.prepare("SELECT data FROM capability_cards WHERE id = ?").get(cardId);
1138
+ if (!row) return null;
1139
+ let card;
1140
+ try {
1141
+ card = JSON.parse(row.data);
1142
+ } catch {
1143
+ return null;
1144
+ }
1145
+ if (Array.isArray(card.skills) && card.skills.length > 0) {
1146
+ const skills = card.skills;
1147
+ if (skillId) {
1148
+ const skill = skills.find((s) => s.id === skillId);
1149
+ if (skill) {
1150
+ const skillPricing = skill.pricing;
1151
+ if (skillPricing && typeof skillPricing.credits_per_call === "number") {
1152
+ return skillPricing.credits_per_call;
1153
+ }
1154
+ }
1155
+ } else {
1156
+ let minPrice = null;
1157
+ for (const s of skills) {
1158
+ const sp = s.pricing;
1159
+ if (sp && typeof sp.credits_per_call === "number" && sp.credits_per_call > 0) {
1160
+ if (minPrice === null || sp.credits_per_call < minPrice) {
1161
+ minPrice = sp.credits_per_call;
1162
+ }
1163
+ }
1164
+ }
1165
+ if (minPrice !== null) return minPrice;
1166
+ }
1167
+ }
1168
+ const pricing = card.pricing;
1169
+ if (!pricing || typeof pricing.credits_per_call !== "number") {
1170
+ return null;
1171
+ }
1172
+ return pricing.credits_per_call;
1173
+ }
1174
+ function holdForRelay(creditDb, owner, amount, cardId) {
1175
+ return holdEscrow(creditDb, owner, amount, cardId);
1176
+ }
1177
+ function settleForRelay(creditDb, escrowId, recipientOwner) {
1178
+ settleEscrow(creditDb, escrowId, recipientOwner);
1179
+ }
1180
+ function calculateConductorFee(totalSubTaskCost) {
1181
+ if (totalSubTaskCost <= 0) return 0;
1182
+ const fee = Math.ceil(totalSubTaskCost * 0.1);
1183
+ return Math.max(1, Math.min(20, fee));
1184
+ }
1185
+ function releaseForRelay(creditDb, escrowId) {
1186
+ if (escrowId === void 0) return;
1187
+ releaseEscrow(creditDb, escrowId);
1188
+ }
1189
+
1190
+ // src/hub-agent/relay-bridge.ts
1191
+ import { randomUUID as randomUUID2 } from "crypto";
1192
+
1193
+ // src/hub-agent/job-queue.ts
1194
+ import { randomUUID } from "crypto";
1195
+ function initJobQueue(db) {
1196
+ db.exec(`
1197
+ CREATE TABLE IF NOT EXISTS hub_agent_jobs (
1198
+ id TEXT PRIMARY KEY,
1199
+ hub_agent_id TEXT NOT NULL,
1200
+ skill_id TEXT NOT NULL,
1201
+ requester_owner TEXT NOT NULL,
1202
+ params TEXT,
1203
+ status TEXT NOT NULL DEFAULT 'queued',
1204
+ result TEXT,
1205
+ escrow_id TEXT,
1206
+ relay_owner TEXT,
1207
+ created_at TEXT NOT NULL,
1208
+ updated_at TEXT NOT NULL
1209
+ )
1210
+ `);
1211
+ }
1212
+ function insertJob(db, input) {
1213
+ const id = randomUUID();
1214
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1215
+ const paramsJson = JSON.stringify(input.params);
1216
+ db.prepare(`
1217
+ INSERT INTO hub_agent_jobs (id, hub_agent_id, skill_id, requester_owner, params, status, escrow_id, relay_owner, created_at, updated_at)
1218
+ VALUES (?, ?, ?, ?, ?, 'queued', ?, ?, ?, ?)
1219
+ `).run(
1220
+ id,
1221
+ input.hub_agent_id,
1222
+ input.skill_id,
1223
+ input.requester_owner,
1224
+ paramsJson,
1225
+ input.escrow_id ?? null,
1226
+ input.relay_owner ?? null,
1227
+ now,
1228
+ now
1229
+ );
1230
+ return {
1231
+ id,
1232
+ hub_agent_id: input.hub_agent_id,
1233
+ skill_id: input.skill_id,
1234
+ requester_owner: input.requester_owner,
1235
+ params: paramsJson,
1236
+ status: "queued",
1237
+ result: null,
1238
+ escrow_id: input.escrow_id ?? null,
1239
+ relay_owner: input.relay_owner ?? null,
1240
+ created_at: now,
1241
+ updated_at: now
1242
+ };
1243
+ }
1244
+ function getJob(db, jobId) {
1245
+ const row = db.prepare("SELECT * FROM hub_agent_jobs WHERE id = ?").get(jobId);
1246
+ return row ?? null;
1247
+ }
1248
+ function listJobs(db, hubAgentId, status) {
1249
+ if (status) {
1250
+ return db.prepare(
1251
+ "SELECT * FROM hub_agent_jobs WHERE hub_agent_id = ? AND status = ? ORDER BY created_at DESC"
1252
+ ).all(hubAgentId, status);
1253
+ }
1254
+ return db.prepare(
1255
+ "SELECT * FROM hub_agent_jobs WHERE hub_agent_id = ? ORDER BY created_at DESC"
1256
+ ).all(hubAgentId);
1257
+ }
1258
+ function updateJobStatus(db, jobId, status, result) {
1259
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1260
+ if (result !== void 0) {
1261
+ db.prepare("UPDATE hub_agent_jobs SET status = ?, result = ?, updated_at = ? WHERE id = ?").run(status, result, now, jobId);
1262
+ } else {
1263
+ db.prepare("UPDATE hub_agent_jobs SET status = ?, updated_at = ? WHERE id = ?").run(status, now, jobId);
1264
+ }
1265
+ }
1266
+ function getJobsByRelayOwner(db, relayOwner) {
1267
+ return db.prepare(
1268
+ "SELECT * FROM hub_agent_jobs WHERE relay_owner = ? AND status = ? ORDER BY created_at ASC"
1269
+ ).all(relayOwner, "queued");
1270
+ }
1271
+
1272
+ // src/hub-agent/crypto.ts
1273
+ import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
1274
+ function getMasterKey() {
1275
+ const hex = process.env.HUB_MASTER_KEY;
1276
+ if (!hex || hex.length !== 64) {
1277
+ throw new AgentBnBError(
1278
+ "HUB_MASTER_KEY must be a 64-character hex string (32 bytes)",
1279
+ "MISSING_MASTER_KEY"
1280
+ );
1281
+ }
1282
+ return Buffer.from(hex, "hex");
1283
+ }
1284
+ function encrypt(plaintext, masterKey) {
1285
+ const iv = randomBytes(12);
1286
+ const cipher = createCipheriv("aes-256-gcm", masterKey, iv);
1287
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
1288
+ const authTag = cipher.getAuthTag();
1289
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
1290
+ }
1291
+ function decrypt(encrypted, masterKey) {
1292
+ const [ivHex, authTagHex, ciphertextHex] = encrypted.split(":");
1293
+ const iv = Buffer.from(ivHex, "hex");
1294
+ const authTag = Buffer.from(authTagHex, "hex");
1295
+ const ciphertext = Buffer.from(ciphertextHex, "hex");
1296
+ const decipher = createDecipheriv("aes-256-gcm", masterKey, iv);
1297
+ decipher.setAuthTag(authTag);
1298
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
1299
+ return decrypted.toString("utf8");
1300
+ }
1301
+
1302
+ // src/hub-agent/store.ts
1303
+ function initHubAgentTable(db) {
1304
+ db.exec(`
1305
+ CREATE TABLE IF NOT EXISTS hub_agents (
1306
+ agent_id TEXT PRIMARY KEY,
1307
+ name TEXT NOT NULL,
1308
+ owner_public_key TEXT NOT NULL,
1309
+ public_key TEXT NOT NULL,
1310
+ private_key_enc TEXT NOT NULL,
1311
+ secrets_enc TEXT,
1312
+ skill_routes TEXT NOT NULL DEFAULT '[]',
1313
+ status TEXT NOT NULL DEFAULT 'active',
1314
+ created_at TEXT NOT NULL,
1315
+ updated_at TEXT NOT NULL
1316
+ )
1317
+ `);
1318
+ }
1319
+ function createHubAgent(db, req, ownerPublicKey) {
1320
+ const masterKey = getMasterKey();
1321
+ const keys = generateKeyPair();
1322
+ const publicKeyHex = keys.publicKey.toString("hex");
1323
+ const agentId = deriveAgentId(publicKeyHex);
1324
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1325
+ const privateKeyEnc = encrypt(keys.privateKey.toString("hex"), masterKey);
1326
+ const secretsEnc = req.secrets ? encrypt(JSON.stringify(req.secrets), masterKey) : null;
1327
+ db.prepare(`
1328
+ INSERT INTO hub_agents (agent_id, name, owner_public_key, public_key, private_key_enc, secrets_enc, skill_routes, status, created_at, updated_at)
1329
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'active', ?, ?)
1330
+ `).run(
1331
+ agentId,
1332
+ req.name,
1333
+ ownerPublicKey,
1334
+ publicKeyHex,
1335
+ privateKeyEnc,
1336
+ secretsEnc,
1337
+ JSON.stringify(req.skill_routes),
1338
+ now,
1339
+ now
1340
+ );
1341
+ return {
1342
+ agent_id: agentId,
1343
+ name: req.name,
1344
+ owner_public_key: ownerPublicKey,
1345
+ public_key: publicKeyHex,
1346
+ skill_routes: req.skill_routes,
1347
+ status: "active",
1348
+ created_at: now,
1349
+ updated_at: now
1350
+ };
1351
+ }
1352
+ function getHubAgent(db, agentId) {
1353
+ const row = db.prepare("SELECT * FROM hub_agents WHERE agent_id = ?").get(agentId);
1354
+ if (!row) return null;
1355
+ const masterKey = getMasterKey();
1356
+ const secrets = row.secrets_enc ? JSON.parse(decrypt(row.secrets_enc, masterKey)) : void 0;
1357
+ return {
1358
+ agent_id: row.agent_id,
1359
+ name: row.name,
1360
+ owner_public_key: row.owner_public_key,
1361
+ public_key: row.public_key,
1362
+ skill_routes: JSON.parse(row.skill_routes),
1363
+ status: row.status,
1364
+ created_at: row.created_at,
1365
+ updated_at: row.updated_at,
1366
+ ...secrets ? { secrets } : {}
1367
+ };
1368
+ }
1369
+ function listHubAgents(db) {
1370
+ 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();
1371
+ return rows.map((row) => ({
1372
+ agent_id: row.agent_id,
1373
+ name: row.name,
1374
+ owner_public_key: row.owner_public_key,
1375
+ public_key: row.public_key,
1376
+ skill_routes: JSON.parse(row.skill_routes),
1377
+ status: row.status,
1378
+ created_at: row.created_at,
1379
+ updated_at: row.updated_at
1380
+ }));
1381
+ }
1382
+ function updateHubAgent(db, agentId, updates) {
1383
+ const existing = db.prepare("SELECT * FROM hub_agents WHERE agent_id = ?").get(agentId);
1384
+ if (!existing) return null;
1385
+ const masterKey = getMasterKey();
1386
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1387
+ const newName = updates.name ?? existing.name;
1388
+ const newSkillRoutes = updates.skill_routes ? JSON.stringify(updates.skill_routes) : existing.skill_routes;
1389
+ const newSecretsEnc = updates.secrets !== void 0 ? encrypt(JSON.stringify(updates.secrets), masterKey) : existing.secrets_enc;
1390
+ db.prepare(`
1391
+ UPDATE hub_agents SET name = ?, skill_routes = ?, secrets_enc = ?, updated_at = ?
1392
+ WHERE agent_id = ?
1393
+ `).run(newName, newSkillRoutes, newSecretsEnc, now, agentId);
1394
+ const secrets = newSecretsEnc ? JSON.parse(decrypt(newSecretsEnc, masterKey)) : void 0;
1395
+ return {
1396
+ agent_id: existing.agent_id,
1397
+ name: newName,
1398
+ owner_public_key: existing.owner_public_key,
1399
+ public_key: existing.public_key,
1400
+ skill_routes: JSON.parse(newSkillRoutes),
1401
+ status: existing.status,
1402
+ created_at: existing.created_at,
1403
+ updated_at: now,
1404
+ ...secrets ? { secrets } : {}
1405
+ };
1406
+ }
1407
+ function deleteHubAgent(db, agentId) {
1408
+ const result = db.prepare("DELETE FROM hub_agents WHERE agent_id = ?").run(agentId);
1409
+ return result.changes > 0;
1410
+ }
1411
+
1412
+ // src/hub-agent/relay-bridge.ts
1413
+ var JOB_DISPATCH_TIMEOUT_MS = 3e5;
1414
+ function createRelayBridge(opts) {
1415
+ const { registryDb, creditDb, sendMessage, pendingRequests, connections } = opts;
1416
+ function onAgentOnline(owner) {
1417
+ const jobs = getJobsByRelayOwner(registryDb, owner);
1418
+ if (jobs.length === 0) return;
1419
+ const targetWs = connections.get(owner);
1420
+ if (!targetWs) return;
1421
+ for (const job of jobs) {
1422
+ updateJobStatus(registryDb, job.id, "dispatched");
1423
+ const agent = getHubAgent(registryDb, job.hub_agent_id);
1424
+ const cardId = agent ? agent.agent_id.padEnd(32, "0").replace(/^(.{8})(.{4})(.{4})(.{4})(.{12}).*$/, "$1-$2-$3-$4-$5") : job.hub_agent_id;
1425
+ const requestId = randomUUID2();
1426
+ let params = {};
1427
+ try {
1428
+ params = JSON.parse(job.params);
1429
+ } catch {
1430
+ }
1431
+ const timeout = setTimeout(() => {
1432
+ const pending = pendingRequests.get(requestId);
1433
+ if (pending) {
1434
+ pendingRequests.delete(requestId);
1435
+ updateJobStatus(registryDb, job.id, "failed", JSON.stringify({ error: "dispatch timeout" }));
1436
+ if (job.escrow_id) {
1437
+ try {
1438
+ releaseForRelay(creditDb, job.escrow_id);
1439
+ } catch (e) {
1440
+ console.error("[relay-bridge] escrow release on timeout failed:", e);
1441
+ }
1442
+ }
1443
+ }
1444
+ }, JOB_DISPATCH_TIMEOUT_MS);
1445
+ pendingRequests.set(requestId, {
1446
+ originOwner: job.requester_owner,
1447
+ timeout,
1448
+ escrowId: job.escrow_id ?? void 0,
1449
+ targetOwner: owner,
1450
+ jobId: job.id
1451
+ });
1452
+ sendMessage(targetWs, {
1453
+ type: "incoming_request",
1454
+ id: requestId,
1455
+ from_owner: job.requester_owner,
1456
+ card_id: cardId,
1457
+ skill_id: job.skill_id,
1458
+ params
1459
+ });
1460
+ }
1461
+ }
1462
+ return { onAgentOnline };
1463
+ }
1464
+ function handleJobRelayResponse(opts) {
1465
+ const { registryDb, creditDb, jobId, escrowId, relayOwner, result, error } = opts;
1466
+ if (error) {
1467
+ updateJobStatus(registryDb, jobId, "failed", JSON.stringify(error));
1468
+ if (escrowId) {
1469
+ try {
1470
+ releaseForRelay(creditDb, escrowId);
1471
+ } catch (e) {
1472
+ console.error("[relay-bridge] escrow release on error failed:", e);
1473
+ }
1474
+ }
1475
+ } else {
1476
+ updateJobStatus(registryDb, jobId, "completed", JSON.stringify(result));
1477
+ if (escrowId) {
1478
+ try {
1479
+ settleForRelay(creditDb, escrowId, relayOwner);
1480
+ } catch (e) {
1481
+ console.error("[relay-bridge] escrow settle failed:", e);
1482
+ }
1483
+ }
1484
+ }
1485
+ }
1486
+
1487
+ // src/relay/websocket-relay.ts
1488
+ var RATE_LIMIT_MAX = 60;
1489
+ var RATE_LIMIT_WINDOW_MS = 6e4;
1490
+ var RELAY_TIMEOUT_MS = 3e5;
1491
+ function registerWebSocketRelay(server, db, creditDb) {
1492
+ const connections = /* @__PURE__ */ new Map();
1493
+ const pendingRequests = /* @__PURE__ */ new Map();
1494
+ const rateLimits = /* @__PURE__ */ new Map();
1495
+ let onAgentOnlineCallback;
1496
+ function checkRateLimit(owner) {
1497
+ const now = Date.now();
1498
+ const entry = rateLimits.get(owner);
1499
+ if (!entry || now >= entry.resetTime) {
1500
+ rateLimits.set(owner, { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS });
1501
+ return true;
1502
+ }
1503
+ if (entry.count >= RATE_LIMIT_MAX) {
1504
+ return false;
1505
+ }
1506
+ entry.count++;
1507
+ return true;
1508
+ }
1509
+ function markOwnerOffline(owner) {
1510
+ try {
1511
+ const stmt = db.prepare("SELECT id, data FROM capability_cards WHERE owner = ?");
1512
+ const rows = stmt.all(owner);
1513
+ for (const row of rows) {
1514
+ try {
1515
+ const card = JSON.parse(row.data);
1516
+ if (card.availability?.online) {
1517
+ card.availability.online = false;
1518
+ card.updated_at = (/* @__PURE__ */ new Date()).toISOString();
1519
+ const updateStmt = db.prepare("UPDATE capability_cards SET data = ?, updated_at = ? WHERE id = ?");
1520
+ updateStmt.run(JSON.stringify(card), card.updated_at, row.id);
1521
+ }
1522
+ } catch {
1523
+ }
1524
+ }
1525
+ } catch {
1526
+ }
1527
+ }
1528
+ function markOwnerOnline(owner) {
1529
+ try {
1530
+ const stmt = db.prepare("SELECT id, data FROM capability_cards WHERE owner = ?");
1531
+ const rows = stmt.all(owner);
1532
+ for (const row of rows) {
1533
+ try {
1534
+ const card = JSON.parse(row.data);
1535
+ card.availability = { ...card.availability, online: true };
1536
+ card.updated_at = (/* @__PURE__ */ new Date()).toISOString();
1537
+ const updateStmt = db.prepare("UPDATE capability_cards SET data = ?, updated_at = ? WHERE id = ?");
1538
+ updateStmt.run(JSON.stringify(card), card.updated_at, row.id);
1539
+ } catch {
1540
+ }
1541
+ }
1542
+ } catch {
1543
+ }
1544
+ }
1545
+ function upsertCard(cardData, owner) {
1546
+ const parsed = AnyCardSchema.safeParse(cardData);
1547
+ if (!parsed.success) {
1548
+ throw new AgentBnBError(
1549
+ `Card validation failed: ${parsed.error.message}`,
1550
+ "VALIDATION_ERROR"
1551
+ );
1552
+ }
1553
+ const card = { ...parsed.data, availability: { ...parsed.data.availability, online: true } };
1554
+ const cardId = card.id;
1555
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1556
+ const existing = db.prepare("SELECT id FROM capability_cards WHERE id = ?").get(cardId);
1557
+ if (existing) {
1558
+ db.prepare(
1559
+ "UPDATE capability_cards SET data = ?, updated_at = ? WHERE id = ?"
1560
+ ).run(JSON.stringify(card), now, cardId);
1561
+ } else {
1562
+ db.prepare(
1563
+ "INSERT INTO capability_cards (id, owner, data, created_at, updated_at) VALUES (?, ?, ?, ?, ?)"
1564
+ ).run(cardId, owner, JSON.stringify(card), now, now);
1565
+ }
1566
+ return cardId;
1567
+ }
1568
+ function logAgentJoined(owner, cardName, cardId) {
1569
+ try {
1570
+ insertRequestLog(db, {
1571
+ id: randomUUID3(),
1572
+ card_id: cardId,
1573
+ card_name: cardName,
1574
+ requester: owner,
1575
+ status: "success",
1576
+ latency_ms: 0,
1577
+ credits_charged: 0,
1578
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
1579
+ action_type: "agent_joined"
1580
+ });
1581
+ } catch {
1582
+ }
1583
+ }
1584
+ function sendMessage(ws, msg) {
1585
+ if (ws.readyState === 1) {
1586
+ ws.send(JSON.stringify(msg));
1587
+ }
1588
+ }
1589
+ function handleRegister(ws, msg) {
1590
+ const { owner, card } = msg;
1591
+ const existing = connections.get(owner);
1592
+ if (existing && existing !== ws) {
1593
+ try {
1594
+ existing.close(1e3, "Replaced by new connection");
1595
+ } catch {
1596
+ }
1597
+ }
1598
+ connections.set(owner, ws);
1599
+ let cardId;
1600
+ try {
1601
+ cardId = upsertCard(card, owner);
1602
+ } catch (err) {
1603
+ console.error(`[relay] card validation failed for ${owner}:`, err instanceof Error ? err.message : err);
1604
+ cardId = card.id ?? owner;
1605
+ }
1606
+ const cardName = card.name ?? card.agent_name ?? owner;
1607
+ logAgentJoined(owner, cardName, cardId);
1608
+ if (msg.cards && msg.cards.length > 0) {
1609
+ for (const extraCard of msg.cards) {
1610
+ try {
1611
+ upsertCard(extraCard, owner);
1612
+ } catch {
1613
+ }
1614
+ }
1615
+ }
1616
+ markOwnerOnline(owner);
1617
+ if (onAgentOnlineCallback) {
1618
+ try {
1619
+ onAgentOnlineCallback(owner);
1620
+ } catch (e) {
1621
+ console.error("[relay] onAgentOnline callback error:", e);
1622
+ }
1623
+ }
1624
+ sendMessage(ws, { type: "registered", agent_id: cardId });
1625
+ }
1626
+ async function handleRelayRequest(ws, msg, fromOwner) {
1627
+ if (!checkRateLimit(fromOwner)) {
1628
+ sendMessage(ws, {
1629
+ type: "error",
1630
+ code: "rate_limited",
1631
+ message: `Rate limit exceeded: max ${RATE_LIMIT_MAX} requests per minute`,
1632
+ request_id: msg.id
1633
+ });
1634
+ return;
1635
+ }
1636
+ const targetWs = connections.get(msg.target_owner);
1637
+ if (!targetWs || targetWs.readyState !== 1) {
1638
+ sendMessage(ws, {
1639
+ type: "response",
1640
+ id: msg.id,
1641
+ error: { code: -32603, message: `Agent offline: ${msg.target_owner}` }
1642
+ });
1643
+ return;
1644
+ }
1645
+ const creditOwner = msg.requester ?? fromOwner;
1646
+ let escrowId;
1647
+ if (creditDb) {
1648
+ try {
1649
+ const price = lookupCardPrice(db, msg.card_id, msg.skill_id);
1650
+ if (price !== null && price > 0) {
1651
+ escrowId = holdForRelay(creditDb, creditOwner, price, msg.card_id);
1652
+ }
1653
+ } catch (err) {
1654
+ if (err instanceof AgentBnBError && err.code === "INSUFFICIENT_CREDITS") {
1655
+ sendMessage(ws, {
1656
+ type: "response",
1657
+ id: msg.id,
1658
+ error: { code: -32603, message: "Insufficient credits" }
1659
+ });
1660
+ return;
1661
+ }
1662
+ console.error("[relay] credit hold error (non-fatal):", err);
1663
+ }
1664
+ }
1665
+ const timeout = setTimeout(() => {
1666
+ const pending = pendingRequests.get(msg.id);
1667
+ pendingRequests.delete(msg.id);
1668
+ if (pending?.escrowId && creditDb) {
1669
+ try {
1670
+ releaseForRelay(creditDb, pending.escrowId);
1671
+ } catch (e) {
1672
+ console.error("[relay] escrow release on timeout failed:", e);
1673
+ }
1674
+ }
1675
+ sendMessage(ws, {
1676
+ type: "response",
1677
+ id: msg.id,
1678
+ error: { code: -32603, message: "Relay request timeout" }
1679
+ });
1680
+ }, RELAY_TIMEOUT_MS);
1681
+ pendingRequests.set(msg.id, { originOwner: fromOwner, creditOwner, timeout, escrowId, targetOwner: msg.target_owner });
1682
+ sendMessage(targetWs, {
1683
+ type: "incoming_request",
1684
+ id: msg.id,
1685
+ from_owner: fromOwner,
1686
+ card_id: msg.card_id,
1687
+ skill_id: msg.skill_id,
1688
+ params: msg.params,
1689
+ requester: msg.requester ?? fromOwner,
1690
+ escrow_receipt: msg.escrow_receipt
1691
+ });
1692
+ }
1693
+ function handleRelayProgress(msg) {
1694
+ const pending = pendingRequests.get(msg.id);
1695
+ if (!pending) return;
1696
+ clearTimeout(pending.timeout);
1697
+ const newTimeout = setTimeout(() => {
1698
+ const p = pendingRequests.get(msg.id);
1699
+ pendingRequests.delete(msg.id);
1700
+ if (p?.escrowId && creditDb) {
1701
+ try {
1702
+ releaseForRelay(creditDb, p.escrowId);
1703
+ } catch (e) {
1704
+ console.error("[relay] escrow release on progress timeout failed:", e);
1705
+ }
1706
+ }
1707
+ const originWs2 = connections.get(pending.originOwner);
1708
+ if (originWs2 && originWs2.readyState === 1) {
1709
+ sendMessage(originWs2, {
1710
+ type: "response",
1711
+ id: msg.id,
1712
+ error: { code: -32603, message: "Relay request timeout" }
1713
+ });
1714
+ }
1715
+ }, RELAY_TIMEOUT_MS);
1716
+ pending.timeout = newTimeout;
1717
+ const originWs = connections.get(pending.originOwner);
1718
+ if (originWs && originWs.readyState === 1) {
1719
+ sendMessage(originWs, {
1720
+ type: "relay_progress",
1721
+ id: msg.id,
1722
+ progress: msg.progress,
1723
+ message: msg.message
1724
+ });
1725
+ }
1726
+ }
1727
+ function handleRelayResponse(msg) {
1728
+ const pending = pendingRequests.get(msg.id);
1729
+ if (!pending) return;
1730
+ clearTimeout(pending.timeout);
1731
+ pendingRequests.delete(msg.id);
1732
+ if (pending.jobId && creditDb) {
1733
+ try {
1734
+ handleJobRelayResponse({
1735
+ registryDb: db,
1736
+ creditDb,
1737
+ jobId: pending.jobId,
1738
+ escrowId: pending.escrowId,
1739
+ relayOwner: pending.targetOwner ?? "",
1740
+ result: msg.error === void 0 ? msg.result : void 0,
1741
+ error: msg.error
1742
+ });
1743
+ } catch (e) {
1744
+ console.error("[relay] job relay response handling failed:", e);
1745
+ }
1746
+ const originWs2 = connections.get(pending.originOwner);
1747
+ if (originWs2 && originWs2.readyState === 1) {
1748
+ sendMessage(originWs2, { type: "response", id: msg.id, result: msg.result, error: msg.error });
1749
+ }
1750
+ return;
1751
+ }
1752
+ if (pending.escrowId && creditDb) {
1753
+ try {
1754
+ if (msg.error === void 0) {
1755
+ settleForRelay(creditDb, pending.escrowId, pending.targetOwner);
1756
+ } else {
1757
+ releaseForRelay(creditDb, pending.escrowId);
1758
+ }
1759
+ } catch (e) {
1760
+ console.error("[relay] escrow settle/release on response failed:", e);
1761
+ }
1762
+ }
1763
+ let conductorFee = 0;
1764
+ if (creditDb && msg.error === void 0 && typeof msg.result === "object" && msg.result !== null && "total_credits" in msg.result && typeof msg.result.total_credits === "number") {
1765
+ const totalCredits = msg.result.total_credits;
1766
+ conductorFee = calculateConductorFee(totalCredits);
1767
+ if (conductorFee > 0) {
1768
+ try {
1769
+ const feeEscrowId = holdForRelay(creditDb, pending.creditOwner ?? pending.originOwner, conductorFee, msg.id);
1770
+ settleForRelay(creditDb, feeEscrowId, pending.targetOwner);
1771
+ } catch (e) {
1772
+ console.error("[relay] conductor fee settlement failed (non-fatal):", e);
1773
+ conductorFee = 0;
1774
+ }
1775
+ }
1776
+ }
1777
+ const originWs = connections.get(pending.originOwner);
1778
+ if (originWs && originWs.readyState === 1) {
1779
+ sendMessage(originWs, {
1780
+ type: "response",
1781
+ id: msg.id,
1782
+ result: msg.result,
1783
+ error: msg.error,
1784
+ ...conductorFee > 0 ? { conductor_fee: conductorFee } : {}
1785
+ });
1786
+ }
1787
+ }
1788
+ function handleDisconnect(owner) {
1789
+ if (!owner) return;
1790
+ connections.delete(owner);
1791
+ rateLimits.delete(owner);
1792
+ markOwnerOffline(owner);
1793
+ for (const [reqId, pending] of pendingRequests) {
1794
+ if (pending.targetOwner === owner) {
1795
+ clearTimeout(pending.timeout);
1796
+ pendingRequests.delete(reqId);
1797
+ if (pending.escrowId && creditDb) {
1798
+ try {
1799
+ releaseForRelay(creditDb, pending.escrowId);
1800
+ } catch (e) {
1801
+ console.error("[relay] escrow release on disconnect failed:", e);
1802
+ }
1803
+ }
1804
+ const originWs = connections.get(pending.originOwner);
1805
+ if (originWs && originWs.readyState === 1) {
1806
+ sendMessage(originWs, {
1807
+ type: "response",
1808
+ id: reqId,
1809
+ error: { code: -32603, message: "Provider disconnected" }
1810
+ });
1811
+ }
1812
+ } else if (pending.originOwner === owner) {
1813
+ clearTimeout(pending.timeout);
1814
+ pendingRequests.delete(reqId);
1815
+ if (pending.escrowId && creditDb) {
1816
+ try {
1817
+ releaseForRelay(creditDb, pending.escrowId);
1818
+ } catch (e) {
1819
+ console.error("[relay] escrow release on requester disconnect failed:", e);
1820
+ }
1821
+ }
1822
+ }
1823
+ }
1824
+ }
1825
+ void server.register(async (app) => {
1826
+ app.get("/ws", { websocket: true }, (rawSocket, _request) => {
1827
+ const socket = rawSocket;
1828
+ let registeredOwner;
1829
+ socket.on("message", (raw) => {
1830
+ void (async () => {
1831
+ let data;
1832
+ try {
1833
+ data = JSON.parse(typeof raw === "string" ? raw : raw.toString("utf-8"));
1834
+ } catch {
1835
+ sendMessage(socket, { type: "error", code: "invalid_json", message: "Invalid JSON" });
1836
+ return;
1837
+ }
1838
+ const parsed = RelayMessageSchema.safeParse(data);
1839
+ if (!parsed.success) {
1840
+ sendMessage(socket, {
1841
+ type: "error",
1842
+ code: "invalid_message",
1843
+ message: `Invalid message: ${parsed.error.issues[0]?.message ?? "unknown error"}`
1844
+ });
1845
+ return;
1846
+ }
1847
+ const msg = parsed.data;
1848
+ switch (msg.type) {
1849
+ case "register":
1850
+ registeredOwner = msg.owner;
1851
+ handleRegister(socket, msg);
1852
+ break;
1853
+ case "relay_request":
1854
+ if (!registeredOwner) {
1855
+ sendMessage(socket, {
1856
+ type: "error",
1857
+ code: "not_registered",
1858
+ message: "Must send register message before relay requests"
1859
+ });
1860
+ return;
1861
+ }
1862
+ await handleRelayRequest(socket, msg, registeredOwner);
1863
+ break;
1864
+ case "relay_response":
1865
+ handleRelayResponse(msg);
1866
+ break;
1867
+ case "relay_progress":
1868
+ handleRelayProgress(msg);
1869
+ break;
1870
+ default:
1871
+ break;
1872
+ }
1873
+ })();
1874
+ });
1875
+ socket.on("close", () => {
1876
+ handleDisconnect(registeredOwner);
1877
+ });
1878
+ socket.on("error", () => {
1879
+ handleDisconnect(registeredOwner);
1880
+ });
1881
+ });
1882
+ });
1883
+ return {
1884
+ getOnlineCount: () => connections.size,
1885
+ getOnlineOwners: () => Array.from(connections.keys()),
1886
+ shutdown: () => {
1887
+ for (const [, ws] of connections) {
1888
+ try {
1889
+ ws.close(1001, "Server shutting down");
1890
+ } catch {
1891
+ }
1892
+ }
1893
+ connections.clear();
1894
+ for (const [, pending] of pendingRequests) {
1895
+ clearTimeout(pending.timeout);
1896
+ }
1897
+ pendingRequests.clear();
1898
+ rateLimits.clear();
1899
+ },
1900
+ setOnAgentOnline: (cb) => {
1901
+ onAgentOnlineCallback = cb;
1902
+ },
1903
+ getConnections: () => connections,
1904
+ getPendingRequests: () => pendingRequests,
1905
+ sendMessage: (ws, msg) => {
1906
+ sendMessage(ws, msg);
1907
+ }
1908
+ };
1909
+ }
1910
+
1911
+ // src/identity/guarantor.ts
1912
+ import { z as z2 } from "zod";
1913
+ import { randomUUID as randomUUID4 } from "crypto";
1914
+ var MAX_AGENTS_PER_GUARANTOR = 10;
1915
+ var GUARANTOR_CREDIT_POOL = 50;
1916
+ var GuarantorRecordSchema = z2.object({
1917
+ id: z2.string().uuid(),
1918
+ github_login: z2.string().min(1),
1919
+ agent_count: z2.number().int().nonnegative(),
1920
+ credit_pool: z2.number().int().nonnegative(),
1921
+ created_at: z2.string().datetime()
1922
+ });
1923
+ var GUARANTOR_SCHEMA = `
1924
+ CREATE TABLE IF NOT EXISTS guarantors (
1925
+ id TEXT PRIMARY KEY,
1926
+ github_login TEXT UNIQUE NOT NULL,
1927
+ agent_count INTEGER NOT NULL DEFAULT 0,
1928
+ credit_pool INTEGER NOT NULL DEFAULT ${GUARANTOR_CREDIT_POOL},
1929
+ created_at TEXT NOT NULL
1930
+ );
1931
+
1932
+ CREATE TABLE IF NOT EXISTS agent_guarantors (
1933
+ agent_id TEXT PRIMARY KEY,
1934
+ guarantor_id TEXT NOT NULL,
1935
+ linked_at TEXT NOT NULL,
1936
+ FOREIGN KEY (guarantor_id) REFERENCES guarantors(id)
1937
+ );
1938
+ `;
1939
+ function ensureGuarantorTables(db) {
1940
+ db.exec(GUARANTOR_SCHEMA);
1941
+ }
1942
+ function registerGuarantor(db, githubLogin) {
1943
+ ensureGuarantorTables(db);
1944
+ const existing = db.prepare("SELECT * FROM guarantors WHERE github_login = ?").get(githubLogin);
1945
+ if (existing) {
1946
+ throw new AgentBnBError(
1947
+ `Guarantor already registered: ${githubLogin}`,
1948
+ "GUARANTOR_EXISTS"
1949
+ );
1950
+ }
1951
+ const record = {
1952
+ id: randomUUID4(),
1953
+ github_login: githubLogin,
1954
+ agent_count: 0,
1955
+ credit_pool: GUARANTOR_CREDIT_POOL,
1956
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
1957
+ };
1958
+ db.prepare(
1959
+ "INSERT INTO guarantors (id, github_login, agent_count, credit_pool, created_at) VALUES (?, ?, ?, ?, ?)"
1960
+ ).run(record.id, record.github_login, record.agent_count, record.credit_pool, record.created_at);
1961
+ return record;
1962
+ }
1963
+ function linkAgentToGuarantor(db, agentId, githubLogin) {
1964
+ ensureGuarantorTables(db);
1965
+ const guarantor = db.prepare("SELECT * FROM guarantors WHERE github_login = ?").get(githubLogin);
1966
+ if (!guarantor) {
1967
+ throw new AgentBnBError(
1968
+ `Guarantor not found: ${githubLogin}`,
1969
+ "GUARANTOR_NOT_FOUND"
1970
+ );
1971
+ }
1972
+ if (guarantor["agent_count"] >= MAX_AGENTS_PER_GUARANTOR) {
1973
+ throw new AgentBnBError(
1974
+ `Maximum agents per guarantor reached (${MAX_AGENTS_PER_GUARANTOR})`,
1975
+ "MAX_AGENTS_EXCEEDED"
1976
+ );
1977
+ }
1978
+ const existingLink = db.prepare("SELECT * FROM agent_guarantors WHERE agent_id = ?").get(agentId);
1979
+ if (existingLink) {
1980
+ throw new AgentBnBError(
1981
+ `Agent ${agentId} is already linked to a guarantor`,
1982
+ "AGENT_ALREADY_LINKED"
1983
+ );
1984
+ }
1985
+ db.transaction(() => {
1986
+ db.prepare("INSERT INTO agent_guarantors (agent_id, guarantor_id, linked_at) VALUES (?, ?, ?)").run(
1987
+ agentId,
1988
+ guarantor["id"],
1989
+ (/* @__PURE__ */ new Date()).toISOString()
1990
+ );
1991
+ db.prepare("UPDATE guarantors SET agent_count = agent_count + 1 WHERE id = ?").run(guarantor["id"]);
1992
+ })();
1993
+ return getGuarantor(db, githubLogin);
1994
+ }
1995
+ function getGuarantor(db, githubLogin) {
1996
+ ensureGuarantorTables(db);
1997
+ const row = db.prepare("SELECT * FROM guarantors WHERE github_login = ?").get(githubLogin);
1998
+ if (!row) return null;
1999
+ return {
2000
+ id: row["id"],
2001
+ github_login: row["github_login"],
2002
+ agent_count: row["agent_count"],
2003
+ credit_pool: row["credit_pool"],
2004
+ created_at: row["created_at"]
2005
+ };
2006
+ }
2007
+ function getAgentGuarantor(db, agentId) {
2008
+ ensureGuarantorTables(db);
2009
+ const link = db.prepare(
2010
+ `SELECT g.* FROM guarantors g
2011
+ JOIN agent_guarantors ag ON ag.guarantor_id = g.id
2012
+ WHERE ag.agent_id = ?`
2013
+ ).get(agentId);
2014
+ if (!link) return null;
2015
+ return {
2016
+ id: link["id"],
2017
+ github_login: link["github_login"],
2018
+ agent_count: link["agent_count"],
2019
+ credit_pool: link["credit_pool"],
2020
+ created_at: link["created_at"]
2021
+ };
2022
+ }
2023
+ function initiateGithubAuth() {
2024
+ return {
2025
+ auth_url: "https://github.com/login/oauth/authorize?client_id=PLACEHOLDER&scope=read:user",
2026
+ state: randomUUID4()
2027
+ };
2028
+ }
2029
+
2030
+ // src/registry/free-tier.ts
2031
+ function initFreeTierTable(db) {
2032
+ db.exec(`
2033
+ CREATE TABLE IF NOT EXISTS credit_free_tier_usage (
2034
+ agent_public_key TEXT NOT NULL,
2035
+ skill_id TEXT NOT NULL,
2036
+ usage_count INTEGER NOT NULL DEFAULT 0,
2037
+ last_used_at TEXT NOT NULL,
2038
+ PRIMARY KEY (agent_public_key, skill_id)
2039
+ )
2040
+ `);
2041
+ }
2042
+
2043
+ // src/registry/credit-routes.ts
2044
+ async function creditRoutesPlugin(fastify, options) {
2045
+ const { creditDb } = options;
2046
+ creditDb.exec(`
2047
+ CREATE TABLE IF NOT EXISTS credit_grants (
2048
+ public_key TEXT PRIMARY KEY,
2049
+ granted_at TEXT NOT NULL,
2050
+ owner TEXT
2051
+ )
2052
+ `);
2053
+ try {
2054
+ creditDb.exec("ALTER TABLE credit_grants ADD COLUMN owner TEXT");
2055
+ } catch {
2056
+ }
2057
+ initFreeTierTable(creditDb);
2058
+ await fastify.register(async (scope) => {
2059
+ identityAuthPlugin(scope);
2060
+ scope.post("/api/credits/hold", {
2061
+ schema: {
2062
+ tags: ["credits"],
2063
+ summary: "Hold credits in escrow during capability execution",
2064
+ security: [{ ed25519Auth: [] }],
2065
+ body: {
2066
+ type: "object",
2067
+ properties: {
2068
+ owner: { type: "string" },
2069
+ amount: { type: "number" },
2070
+ cardId: { type: "string" }
2071
+ },
2072
+ required: ["owner", "amount", "cardId"]
2073
+ },
2074
+ response: {
2075
+ 200: { type: "object", properties: { escrowId: { type: "string" } } },
2076
+ 400: { type: "object", properties: { error: { type: "string" }, code: { type: "string" } } }
2077
+ }
2078
+ }
2079
+ }, async (request, reply) => {
2080
+ const body = request.body;
2081
+ const owner = typeof body.owner === "string" ? body.owner.trim() : "";
2082
+ const amount = typeof body.amount === "number" ? body.amount : NaN;
2083
+ const cardId = typeof body.cardId === "string" ? body.cardId.trim() : "";
2084
+ if (!owner || isNaN(amount) || amount <= 0 || !cardId) {
2085
+ return reply.code(400).send({ error: "Missing or invalid required fields: owner, amount (>0), cardId" });
2086
+ }
2087
+ try {
2088
+ const escrowId = holdEscrow(creditDb, owner, amount, cardId);
2089
+ return reply.send({ escrowId });
2090
+ } catch (err) {
2091
+ if (err instanceof AgentBnBError && err.code === "INSUFFICIENT_CREDITS") {
2092
+ return reply.code(400).send({ error: err.message, code: "INSUFFICIENT_CREDITS" });
2093
+ }
2094
+ throw err;
2095
+ }
2096
+ });
2097
+ scope.post("/api/credits/settle", {
2098
+ schema: {
2099
+ tags: ["credits"],
2100
+ summary: "Transfer held credits to provider on success",
2101
+ security: [{ ed25519Auth: [] }],
2102
+ body: {
2103
+ type: "object",
2104
+ properties: {
2105
+ escrowId: { type: "string" },
2106
+ recipientOwner: { type: "string" }
2107
+ },
2108
+ required: ["escrowId", "recipientOwner"]
2109
+ },
2110
+ response: {
2111
+ 200: { type: "object", properties: { ok: { type: "boolean" } } },
2112
+ 400: { type: "object", properties: { error: { type: "string" }, code: { type: "string" } } }
2113
+ }
2114
+ }
2115
+ }, async (request, reply) => {
2116
+ const body = request.body;
2117
+ const escrowId = typeof body.escrowId === "string" ? body.escrowId.trim() : "";
2118
+ const recipientOwner = typeof body.recipientOwner === "string" ? body.recipientOwner.trim() : "";
2119
+ if (!escrowId || !recipientOwner) {
2120
+ return reply.code(400).send({ error: "escrowId and recipientOwner are required" });
2121
+ }
2122
+ try {
2123
+ settleEscrow(creditDb, escrowId, recipientOwner);
2124
+ return reply.send({ ok: true });
2125
+ } catch (err) {
2126
+ if (err instanceof AgentBnBError) {
2127
+ return reply.code(400).send({ error: err.message, code: err.code });
2128
+ }
2129
+ throw err;
2130
+ }
2131
+ });
2132
+ scope.post("/api/credits/release", {
2133
+ schema: {
2134
+ tags: ["credits"],
2135
+ summary: "Refund held credits to requester on failure",
2136
+ security: [{ ed25519Auth: [] }],
2137
+ body: {
2138
+ type: "object",
2139
+ properties: { escrowId: { type: "string" } },
2140
+ required: ["escrowId"]
2141
+ },
2142
+ response: {
2143
+ 200: { type: "object", properties: { ok: { type: "boolean" } } },
2144
+ 400: { type: "object", properties: { error: { type: "string" }, code: { type: "string" } } }
2145
+ }
2146
+ }
2147
+ }, async (request, reply) => {
2148
+ const body = request.body;
2149
+ const escrowId = typeof body.escrowId === "string" ? body.escrowId.trim() : "";
2150
+ if (!escrowId) {
2151
+ return reply.code(400).send({ error: "escrowId is required" });
2152
+ }
2153
+ try {
2154
+ releaseEscrow(creditDb, escrowId);
2155
+ return reply.send({ ok: true });
2156
+ } catch (err) {
2157
+ if (err instanceof AgentBnBError) {
2158
+ return reply.code(400).send({ error: err.message, code: err.code });
2159
+ }
2160
+ throw err;
2161
+ }
2162
+ });
2163
+ scope.post("/api/credits/grant", {
2164
+ schema: {
2165
+ tags: ["credits"],
2166
+ summary: "Bootstrap grant of 50 credits (once per identity)",
2167
+ security: [{ ed25519Auth: [] }],
2168
+ body: {
2169
+ type: "object",
2170
+ properties: {
2171
+ owner: { type: "string" },
2172
+ amount: { type: "number" }
2173
+ },
2174
+ required: ["owner"]
2175
+ },
2176
+ response: {
2177
+ 200: {
2178
+ type: "object",
2179
+ properties: {
2180
+ ok: { type: "boolean" },
2181
+ granted: { type: "number" },
2182
+ reason: { type: "string" }
2183
+ }
2184
+ },
2185
+ 400: { type: "object", properties: { error: { type: "string" } } }
2186
+ }
2187
+ }
2188
+ }, async (request, reply) => {
2189
+ const body = request.body;
2190
+ const owner = typeof body.owner === "string" ? body.owner.trim() : "";
2191
+ const amount = typeof body.amount === "number" ? body.amount : 50;
2192
+ const publicKey = request.agentPublicKey;
2193
+ if (!owner) {
2194
+ return reply.code(400).send({ error: "owner is required" });
2195
+ }
2196
+ const existing = creditDb.prepare("SELECT public_key, owner FROM credit_grants WHERE public_key = ?").get(publicKey);
2197
+ if (existing) {
2198
+ if (existing.owner && existing.owner !== owner) {
2199
+ migrateOwner(creditDb, existing.owner, owner);
2200
+ creditDb.prepare("UPDATE credit_grants SET owner = ? WHERE public_key = ?").run(owner, publicKey);
2201
+ return reply.send({ ok: true, granted: 0, reason: "renamed", from: existing.owner, to: owner });
2202
+ }
2203
+ return reply.send({ ok: true, granted: 0, reason: "already_granted" });
2204
+ }
2205
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2206
+ bootstrapAgent(creditDb, owner, amount);
2207
+ creditDb.prepare("INSERT INTO credit_grants (public_key, granted_at, owner) VALUES (?, ?, ?)").run(publicKey, now, owner);
2208
+ return reply.send({ ok: true, granted: amount });
2209
+ });
2210
+ scope.get("/api/credits/:owner", {
2211
+ schema: {
2212
+ tags: ["credits"],
2213
+ summary: "Get current credit balance for an agent",
2214
+ security: [{ ed25519Auth: [] }],
2215
+ params: { type: "object", properties: { owner: { type: "string" } }, required: ["owner"] },
2216
+ response: { 200: { type: "object", properties: { balance: { type: "number" } } } }
2217
+ }
2218
+ }, async (request, reply) => {
2219
+ const { owner } = request.params;
2220
+ const balance = getBalance(creditDb, owner);
2221
+ return reply.send({ balance });
2222
+ });
2223
+ scope.get("/api/credits/:owner/history", {
2224
+ schema: {
2225
+ tags: ["credits"],
2226
+ summary: "Get paginated transaction history",
2227
+ security: [{ ed25519Auth: [] }],
2228
+ params: { type: "object", properties: { owner: { type: "string" } }, required: ["owner"] },
2229
+ querystring: {
2230
+ type: "object",
2231
+ properties: { limit: { type: "integer", description: "Max entries (default 20, max 100)" } }
2232
+ },
2233
+ response: {
2234
+ 200: {
2235
+ type: "object",
2236
+ properties: { transactions: { type: "array" }, limit: { type: "integer" } }
2237
+ }
2238
+ }
2239
+ }
2240
+ }, async (request, reply) => {
2241
+ const { owner } = request.params;
2242
+ const query = request.query;
2243
+ const rawLimit = query.limit !== void 0 ? parseInt(query.limit, 10) : 20;
2244
+ const limit = Math.min(isNaN(rawLimit) || rawLimit < 1 ? 20 : rawLimit, 100);
2245
+ const transactions = getTransactions(creditDb, owner, limit);
2246
+ return reply.send({ transactions, limit });
2247
+ });
2248
+ scope.post("/api/credits/rename", {
2249
+ schema: {
2250
+ tags: ["credits"],
2251
+ summary: "Rename owner \u2014 migrate credits from old owner to new owner",
2252
+ security: [{ ed25519Auth: [] }],
2253
+ body: {
2254
+ type: "object",
2255
+ properties: {
2256
+ oldOwner: { type: "string" },
2257
+ newOwner: { type: "string" }
2258
+ },
2259
+ required: ["oldOwner", "newOwner"]
2260
+ },
2261
+ response: {
2262
+ 200: { type: "object", properties: { ok: { type: "boolean" }, migrated: { type: "boolean" } } },
2263
+ 400: { type: "object", properties: { error: { type: "string" } } }
2264
+ }
2265
+ }
2266
+ }, async (request, reply) => {
2267
+ const body = request.body;
2268
+ const oldOwner = typeof body.oldOwner === "string" ? body.oldOwner.trim() : "";
2269
+ const newOwner = typeof body.newOwner === "string" ? body.newOwner.trim() : "";
2270
+ if (!oldOwner || !newOwner || oldOwner === newOwner) {
2271
+ return reply.code(400).send({ error: "oldOwner and newOwner must be different non-empty strings" });
2272
+ }
2273
+ const oldBalance = getBalance(creditDb, oldOwner);
2274
+ if (oldBalance === 0) {
2275
+ return reply.send({ ok: true, migrated: false });
2276
+ }
2277
+ migrateOwner(creditDb, oldOwner, newOwner);
2278
+ return reply.send({ ok: true, migrated: true });
2279
+ });
2280
+ });
2281
+ }
2282
+
2283
+ // src/hub-agent/executor.ts
2284
+ var HubAgentExecutor = class {
2285
+ constructor(registryDb, creditDb) {
2286
+ this.registryDb = registryDb;
2287
+ this.creditDb = creditDb;
2288
+ initJobQueue(this.registryDb);
2289
+ }
2290
+ /**
2291
+ * Execute a skill on a Hub Agent.
2292
+ *
2293
+ * @param agentId - The Hub Agent ID.
2294
+ * @param skillId - The skill_id to execute from the agent's routing table.
2295
+ * @param params - Input parameters for the skill.
2296
+ * @param requesterOwner - Optional requester identifier for credit escrow.
2297
+ * @returns ExecutionResult with success status, result/error, and latency_ms.
2298
+ */
2299
+ async execute(agentId, skillId, params, requesterOwner) {
2300
+ const startTime = Date.now();
2301
+ const agent = getHubAgent(this.registryDb, agentId);
2302
+ if (!agent) {
2303
+ return { success: false, error: "Hub Agent not found", latency_ms: Date.now() - startTime };
2304
+ }
2305
+ if (agent.status === "paused") {
2306
+ return { success: false, error: "Hub Agent is paused", latency_ms: Date.now() - startTime };
2307
+ }
2308
+ const route = agent.skill_routes.find((r) => r.skill_id === skillId);
2309
+ if (!route) {
2310
+ return { success: false, error: "Skill not found in routing table", latency_ms: Date.now() - startTime };
2311
+ }
2312
+ switch (route.mode) {
2313
+ case "relay":
2314
+ return this.executeRelay(route, agent, params, requesterOwner, startTime);
2315
+ case "queue":
2316
+ return this.executeQueue(route, agent, params, requesterOwner, startTime);
2317
+ case "direct_api":
2318
+ return this.executeDirectApi(route, agent, params, requesterOwner, startTime);
2319
+ }
2320
+ }
2321
+ /**
2322
+ * Relay mode: If the target relay agent is offline, queue the job.
2323
+ * If online, still queue (actual dispatch happens via relay bridge).
2324
+ */
2325
+ async executeRelay(route, agent, params, requesterOwner, startTime) {
2326
+ const relayOwner = route.config.relay_owner;
2327
+ if (this.isRelayOwnerOnline(relayOwner)) {
2328
+ return { success: false, error: "relay mode requires connected session agent", latency_ms: 0 };
2329
+ }
2330
+ return this.queueJob(agent, route.skill_id, params, requesterOwner, relayOwner, startTime);
2331
+ }
2332
+ /**
2333
+ * Queue mode: Always queue the job for later dispatch.
2334
+ */
2335
+ async executeQueue(route, agent, params, requesterOwner, startTime) {
2336
+ const relayOwner = route.config.relay_owner;
2337
+ return this.queueJob(agent, route.skill_id, params, requesterOwner, relayOwner, startTime);
2338
+ }
2339
+ /**
2340
+ * Queue a job with optional credit escrow.
2341
+ */
2342
+ queueJob(agent, skillId, params, requesterOwner, relayOwner, startTime) {
2343
+ const cardId = agent.agent_id.padEnd(32, "0").replace(/^(.{8})(.{4})(.{4})(.{4})(.{12}).*$/, "$1-$2-$3-$4-$5");
2344
+ let escrowId;
2345
+ if (requesterOwner) {
2346
+ const price = lookupCardPrice(this.registryDb, cardId, skillId);
2347
+ if (price !== null && price > 0) {
2348
+ escrowId = holdEscrow(this.creditDb, requesterOwner, price, cardId);
2349
+ }
2350
+ }
2351
+ const job = insertJob(this.registryDb, {
2352
+ hub_agent_id: agent.agent_id,
2353
+ skill_id: skillId,
2354
+ requester_owner: requesterOwner ?? "anonymous",
2355
+ params,
2356
+ escrow_id: escrowId,
2357
+ relay_owner: relayOwner
2358
+ });
2359
+ return {
2360
+ success: true,
2361
+ result: { queued: true, job_id: job.id },
2362
+ latency_ms: Date.now() - startTime
2363
+ };
2364
+ }
2365
+ /**
2366
+ * Check if a relay owner has any online cards in the registry.
2367
+ *
2368
+ * @param owner - The relay owner identifier.
2369
+ * @returns true if any card for this owner is online.
2370
+ */
2371
+ isRelayOwnerOnline(owner) {
2372
+ const rows = this.registryDb.prepare(
2373
+ "SELECT data FROM capability_cards WHERE owner = ?"
2374
+ ).all(owner);
2375
+ for (const row of rows) {
2376
+ try {
2377
+ const card = JSON.parse(row.data);
2378
+ const availability = card.availability;
2379
+ if (availability?.online === true) {
2380
+ return true;
2381
+ }
2382
+ } catch {
2383
+ }
2384
+ }
2385
+ return false;
2386
+ }
2387
+ /**
2388
+ * Execute a direct_api skill route via ApiExecutor.
2389
+ * Handles secret injection and credit escrow.
2390
+ */
2391
+ async executeDirectApi(route, agent, params, requesterOwner, startTime) {
2392
+ const config = this.injectSecrets(route.config, agent.secrets);
2393
+ const pricing = route.config.pricing;
2394
+ const creditsPerCall = pricing?.credits_per_call ?? 0;
2395
+ let escrowId;
2396
+ if (requesterOwner && creditsPerCall > 0) {
2397
+ const cardId = agent.agent_id.padEnd(32, "0").replace(/^(.{8})(.{4})(.{4})(.{4})(.{12}).*$/, "$1-$2-$3-$4-$5");
2398
+ escrowId = holdEscrow(this.creditDb, requesterOwner, creditsPerCall, cardId);
2399
+ }
2400
+ try {
2401
+ const apiExecutor = new ApiExecutor();
2402
+ const modeResult = await apiExecutor.execute(config, params);
2403
+ const result = {
2404
+ ...modeResult,
2405
+ latency_ms: Date.now() - startTime
2406
+ };
2407
+ if (escrowId) {
2408
+ if (result.success) {
2409
+ settleEscrow(this.creditDb, escrowId, agent.agent_id);
2410
+ } else {
2411
+ releaseEscrow(this.creditDb, escrowId);
2412
+ }
2413
+ }
2414
+ return result;
2415
+ } catch (err) {
2416
+ if (escrowId) {
2417
+ releaseEscrow(this.creditDb, escrowId);
2418
+ }
2419
+ const message = err instanceof Error ? err.message : String(err);
2420
+ return {
2421
+ success: false,
2422
+ error: message,
2423
+ latency_ms: Date.now() - startTime
2424
+ };
2425
+ }
2426
+ }
2427
+ /**
2428
+ * Injects decrypted secrets into the API skill config's auth field.
2429
+ * If secrets contain 'api_key' and auth type is 'bearer', replaces the token.
2430
+ * If secrets contain 'api_key' and auth type is 'apikey', replaces the key.
2431
+ */
2432
+ injectSecrets(config, secrets) {
2433
+ if (!secrets || Object.keys(secrets).length === 0) {
2434
+ return config;
2435
+ }
2436
+ const injected = JSON.parse(JSON.stringify(config));
2437
+ const apiKey = secrets.api_key ?? secrets.API_KEY;
2438
+ if (apiKey && injected.auth) {
2439
+ switch (injected.auth.type) {
2440
+ case "bearer":
2441
+ injected.auth.token = apiKey;
2442
+ break;
2443
+ case "apikey":
2444
+ injected.auth.key = apiKey;
2445
+ break;
2446
+ }
2447
+ }
2448
+ return injected;
2449
+ }
2450
+ };
2451
+
2452
+ // src/hub-agent/types.ts
2453
+ import { z as z3 } from "zod";
2454
+ var SkillRouteSchema = z3.discriminatedUnion("mode", [
2455
+ z3.object({
2456
+ skill_id: z3.string().min(1),
2457
+ mode: z3.literal("direct_api"),
2458
+ config: ApiSkillConfigSchema
2459
+ }),
2460
+ z3.object({
2461
+ skill_id: z3.string().min(1),
2462
+ mode: z3.literal("relay"),
2463
+ config: z3.object({ relay_owner: z3.string().min(1) })
2464
+ }),
2465
+ z3.object({
2466
+ skill_id: z3.string().min(1),
2467
+ mode: z3.literal("queue"),
2468
+ config: z3.object({ relay_owner: z3.string().min(1) }).passthrough()
2469
+ })
2470
+ ]);
2471
+ var HubAgentSchema = z3.object({
2472
+ agent_id: z3.string().min(1),
2473
+ name: z3.string().min(1),
2474
+ owner_public_key: z3.string().min(1),
2475
+ public_key: z3.string().min(1),
2476
+ skill_routes: z3.array(SkillRouteSchema),
2477
+ status: z3.enum(["active", "paused"]),
2478
+ created_at: z3.string(),
2479
+ updated_at: z3.string()
2480
+ });
2481
+ var CreateAgentRequestSchema = z3.object({
2482
+ name: z3.string().min(1),
2483
+ skill_routes: z3.array(SkillRouteSchema),
2484
+ secrets: z3.record(z3.string()).optional()
2485
+ });
2486
+
2487
+ // src/hub-agent/routes.ts
2488
+ function buildCapabilityCard(agentId, name, publicKey, skillRoutes) {
2489
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2490
+ const skills = skillRoutes.map((route) => ({
2491
+ id: route.skill_id,
2492
+ name: route.mode === "direct_api" ? route.config.name : route.skill_id,
2493
+ description: route.mode === "direct_api" ? `API skill: ${route.config.name}` : `${route.mode} skill: ${route.skill_id}`,
2494
+ level: 1,
2495
+ inputs: [],
2496
+ outputs: [],
2497
+ pricing: route.mode === "direct_api" ? route.config.pricing : { credits_per_call: 10 }
2498
+ }));
2499
+ if (skills.length === 0) {
2500
+ skills.push({
2501
+ id: "default",
2502
+ name,
2503
+ description: `Hub Agent: ${name}`,
2504
+ level: 1,
2505
+ inputs: [],
2506
+ outputs: [],
2507
+ pricing: { credits_per_call: 10 }
2508
+ });
2509
+ }
2510
+ return {
2511
+ spec_version: "2.0",
2512
+ id: agentId.padEnd(32, "0").replace(/^(.{8})(.{4})(.{4})(.{4})(.{12}).*$/, "$1-$2-$3-$4-$5"),
2513
+ owner: publicKey.slice(0, 16),
2514
+ agent_name: name,
2515
+ skills,
2516
+ availability: { online: true },
2517
+ created_at: now,
2518
+ updated_at: now
2519
+ };
2520
+ }
2521
+ function upsertCardRaw(db, cardData, owner) {
2522
+ const parsed = AnyCardSchema.safeParse(cardData);
2523
+ if (!parsed.success) {
2524
+ throw new AgentBnBError(
2525
+ `Card validation failed: ${parsed.error.message}`,
2526
+ "VALIDATION_ERROR"
2527
+ );
2528
+ }
2529
+ const card = parsed.data;
2530
+ const cardId = card.id;
2531
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2532
+ const existing = db.prepare("SELECT id FROM capability_cards WHERE id = ?").get(cardId);
2533
+ if (existing) {
2534
+ db.prepare("UPDATE capability_cards SET data = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(card), now, cardId);
2535
+ } else {
2536
+ db.prepare("INSERT INTO capability_cards (id, owner, data, created_at, updated_at) VALUES (?, ?, ?, ?, ?)").run(cardId, owner, JSON.stringify(card), now, now);
2537
+ }
2538
+ return cardId;
2539
+ }
2540
+ function sanitizeAgent(agent) {
2541
+ const { secrets, private_key_enc, ...safe } = agent;
2542
+ if (secrets && typeof secrets === "object") {
2543
+ return { ...safe, secret_keys: Object.keys(secrets) };
2544
+ }
2545
+ return safe;
2546
+ }
2547
+ async function hubAgentRoutesPlugin(fastify, options) {
2548
+ const { registryDb, creditDb } = options;
2549
+ initHubAgentTable(registryDb);
2550
+ initJobQueue(registryDb);
2551
+ fastify.post("/api/hub-agents", {
2552
+ schema: {
2553
+ tags: ["hub-agents"],
2554
+ summary: "Create a new Hub Agent with Ed25519 identity",
2555
+ body: {
2556
+ type: "object",
2557
+ required: ["name"],
2558
+ properties: {
2559
+ name: { type: "string", minLength: 1 },
2560
+ skill_routes: { type: "array" },
2561
+ secrets: { type: "object" }
2562
+ }
2563
+ },
2564
+ response: {
2565
+ 201: {
2566
+ type: "object",
2567
+ properties: {
2568
+ agent_id: { type: "string" },
2569
+ name: { type: "string" },
2570
+ public_key: { type: "string" },
2571
+ skill_routes: { type: "array" },
2572
+ status: { type: "string" },
2573
+ created_at: { type: "string" },
2574
+ updated_at: { type: "string" }
2575
+ }
2576
+ },
2577
+ 400: {
2578
+ type: "object",
2579
+ properties: { error: { type: "string" } }
2580
+ }
2581
+ }
2582
+ }
2583
+ }, async (request, reply) => {
2584
+ const parseResult = CreateAgentRequestSchema.safeParse(request.body);
2585
+ if (!parseResult.success) {
2586
+ return reply.code(400).send({ error: parseResult.error.message });
2587
+ }
2588
+ const req = parseResult.data;
2589
+ const ownerPublicKey = "hub-server";
2590
+ try {
2591
+ const agent = createHubAgent(registryDb, req, ownerPublicKey);
2592
+ bootstrapAgent(creditDb, agent.agent_id, 50);
2593
+ const cardData = buildCapabilityCard(agent.agent_id, agent.name, agent.public_key, agent.skill_routes);
2594
+ try {
2595
+ upsertCardRaw(registryDb, cardData, agent.agent_id);
2596
+ } catch {
2597
+ }
2598
+ return reply.code(201).send(sanitizeAgent(agent));
2599
+ } catch (err) {
2600
+ if (err instanceof AgentBnBError) {
2601
+ return reply.code(400).send({ error: err.message });
2602
+ }
2603
+ throw err;
2604
+ }
2605
+ });
2606
+ fastify.get("/api/hub-agents", {
2607
+ schema: {
2608
+ tags: ["hub-agents"],
2609
+ summary: "List all Hub Agents",
2610
+ response: {
2611
+ 200: {
2612
+ type: "object",
2613
+ properties: {
2614
+ agents: { type: "array" }
2615
+ }
2616
+ }
2617
+ }
2618
+ }
2619
+ }, async (_request, reply) => {
2620
+ const agents = listHubAgents(registryDb);
2621
+ return reply.send({ agents });
2622
+ });
2623
+ fastify.get("/api/hub-agents/:id", {
2624
+ schema: {
2625
+ tags: ["hub-agents"],
2626
+ summary: "Get a single Hub Agent by ID",
2627
+ params: {
2628
+ type: "object",
2629
+ properties: { id: { type: "string" } },
2630
+ required: ["id"]
2631
+ },
2632
+ response: {
2633
+ 200: {
2634
+ type: "object",
2635
+ properties: {
2636
+ agent_id: { type: "string" },
2637
+ name: { type: "string" },
2638
+ public_key: { type: "string" },
2639
+ skill_routes: { type: "array" },
2640
+ status: { type: "string" },
2641
+ secret_keys: { type: "array", items: { type: "string" } }
2642
+ }
2643
+ },
2644
+ 404: {
2645
+ type: "object",
2646
+ properties: { error: { type: "string" } }
2647
+ }
2648
+ }
2649
+ }
2650
+ }, async (request, reply) => {
2651
+ const { id } = request.params;
2652
+ const agent = getHubAgent(registryDb, id);
2653
+ if (!agent) {
2654
+ return reply.code(404).send({ error: "Hub Agent not found" });
2655
+ }
2656
+ return reply.send(sanitizeAgent(agent));
2657
+ });
2658
+ fastify.put("/api/hub-agents/:id", {
2659
+ schema: {
2660
+ tags: ["hub-agents"],
2661
+ summary: "Update a Hub Agent",
2662
+ params: {
2663
+ type: "object",
2664
+ properties: { id: { type: "string" } },
2665
+ required: ["id"]
2666
+ },
2667
+ body: {
2668
+ type: "object",
2669
+ properties: {
2670
+ name: { type: "string" },
2671
+ skill_routes: { type: "array" },
2672
+ secrets: { type: "object" }
2673
+ }
2674
+ },
2675
+ response: {
2676
+ 200: {
2677
+ type: "object",
2678
+ properties: {
2679
+ agent_id: { type: "string" },
2680
+ name: { type: "string" },
2681
+ skill_routes: { type: "array" },
2682
+ status: { type: "string" }
2683
+ }
2684
+ },
2685
+ 404: {
2686
+ type: "object",
2687
+ properties: { error: { type: "string" } }
2688
+ }
2689
+ }
2690
+ }
2691
+ }, async (request, reply) => {
2692
+ const { id } = request.params;
2693
+ const body = request.body;
2694
+ const updates = {};
2695
+ if (typeof body.name === "string") updates.name = body.name;
2696
+ if (Array.isArray(body.skill_routes)) updates.skill_routes = body.skill_routes;
2697
+ if (body.secrets && typeof body.secrets === "object") updates.secrets = body.secrets;
2698
+ const agent = updateHubAgent(registryDb, id, updates);
2699
+ if (!agent) {
2700
+ return reply.code(404).send({ error: "Hub Agent not found" });
2701
+ }
2702
+ if (updates.skill_routes) {
2703
+ const cardData = buildCapabilityCard(agent.agent_id, agent.name, agent.public_key, agent.skill_routes);
2704
+ try {
2705
+ upsertCardRaw(registryDb, cardData, agent.agent_id);
2706
+ } catch {
2707
+ }
2708
+ }
2709
+ return reply.send(sanitizeAgent(agent));
2710
+ });
2711
+ fastify.delete("/api/hub-agents/:id", {
2712
+ schema: {
2713
+ tags: ["hub-agents"],
2714
+ summary: "Delete a Hub Agent",
2715
+ params: {
2716
+ type: "object",
2717
+ properties: { id: { type: "string" } },
2718
+ required: ["id"]
2719
+ },
2720
+ response: {
2721
+ 200: {
2722
+ type: "object",
2723
+ properties: { ok: { type: "boolean" } }
2724
+ },
2725
+ 404: {
2726
+ type: "object",
2727
+ properties: { error: { type: "string" } }
2728
+ }
2729
+ }
2730
+ }
2731
+ }, async (request, reply) => {
2732
+ const { id } = request.params;
2733
+ const agent = getHubAgent(registryDb, id);
2734
+ if (!agent) {
2735
+ return reply.code(404).send({ error: "Hub Agent not found" });
2736
+ }
2737
+ deleteHubAgent(registryDb, id);
2738
+ const cardId = id.padEnd(32, "0").replace(/^(.{8})(.{4})(.{4})(.{4})(.{12}).*$/, "$1-$2-$3-$4-$5");
2739
+ try {
2740
+ registryDb.prepare("DELETE FROM capability_cards WHERE id = ?").run(cardId);
2741
+ } catch {
2742
+ }
2743
+ return reply.send({ ok: true });
2744
+ });
2745
+ fastify.post("/api/hub-agents/:id/execute", {
2746
+ schema: {
2747
+ tags: ["hub-agents"],
2748
+ summary: "Execute a skill on a Hub Agent",
2749
+ params: {
2750
+ type: "object",
2751
+ properties: { id: { type: "string" } },
2752
+ required: ["id"]
2753
+ },
2754
+ body: {
2755
+ type: "object",
2756
+ required: ["skill_id"],
2757
+ properties: {
2758
+ skill_id: { type: "string" },
2759
+ params: { type: "object" },
2760
+ requester_owner: { type: "string" }
2761
+ }
2762
+ }
2763
+ }
2764
+ }, async (request, reply) => {
2765
+ const { id } = request.params;
2766
+ const body = request.body;
2767
+ const executor = new HubAgentExecutor(registryDb, creditDb);
2768
+ const result = await executor.execute(
2769
+ id,
2770
+ body.skill_id,
2771
+ body.params ?? {},
2772
+ body.requester_owner
2773
+ );
2774
+ if (!result.success && result.error === "Hub Agent not found") {
2775
+ return reply.code(404).send(result);
2776
+ }
2777
+ if (!result.success) {
2778
+ return reply.code(400).send(result);
2779
+ }
2780
+ return reply.send(result);
2781
+ });
2782
+ fastify.get("/api/hub-agents/:id/jobs", {
2783
+ schema: {
2784
+ tags: ["hub-agents"],
2785
+ summary: "List jobs for a Hub Agent",
2786
+ params: {
2787
+ type: "object",
2788
+ properties: { id: { type: "string" } },
2789
+ required: ["id"]
2790
+ },
2791
+ querystring: {
2792
+ type: "object",
2793
+ properties: {
2794
+ status: { type: "string", enum: ["queued", "dispatched", "completed", "failed"] }
2795
+ }
2796
+ },
2797
+ response: {
2798
+ 200: {
2799
+ type: "object",
2800
+ properties: {
2801
+ jobs: { type: "array" }
2802
+ }
2803
+ }
2804
+ }
2805
+ }
2806
+ }, async (request, reply) => {
2807
+ const { id } = request.params;
2808
+ const { status } = request.query ?? {};
2809
+ const jobs = listJobs(registryDb, id, status);
2810
+ return reply.send({ jobs });
2811
+ });
2812
+ fastify.get("/api/hub-agents/:id/jobs/:jobId", {
2813
+ schema: {
2814
+ tags: ["hub-agents"],
2815
+ summary: "Get a single job by ID",
2816
+ params: {
2817
+ type: "object",
2818
+ properties: {
2819
+ id: { type: "string" },
2820
+ jobId: { type: "string" }
2821
+ },
2822
+ required: ["id", "jobId"]
2823
+ },
2824
+ response: {
2825
+ 200: {
2826
+ type: "object",
2827
+ properties: {
2828
+ id: { type: "string" },
2829
+ hub_agent_id: { type: "string" },
2830
+ skill_id: { type: "string" },
2831
+ status: { type: "string" }
2832
+ }
2833
+ },
2834
+ 404: {
2835
+ type: "object",
2836
+ properties: { error: { type: "string" } }
2837
+ }
2838
+ }
2839
+ }
2840
+ }, async (request, reply) => {
2841
+ const { jobId } = request.params;
2842
+ const job = getJob(registryDb, jobId);
2843
+ if (!job) {
2844
+ return reply.code(404).send({ error: "Job not found" });
2845
+ }
2846
+ return reply.send(job);
2847
+ });
2848
+ }
2849
+
2850
+ // src/registry/openapi-gpt-actions.ts
2851
+ function convertToGptActions(openapiSpec, serverUrl) {
2852
+ const spec = JSON.parse(JSON.stringify(openapiSpec));
2853
+ spec.servers = [{ url: serverUrl }];
2854
+ const paths = spec.paths;
2855
+ if (paths) {
2856
+ const filteredPaths = {};
2857
+ for (const [path, methods] of Object.entries(paths)) {
2858
+ if (path.startsWith("/me") || path.startsWith("/draft") || path.startsWith("/docs") || path.startsWith("/ws") || path.startsWith("/api/credits")) {
2859
+ continue;
2860
+ }
2861
+ const filteredMethods = {};
2862
+ for (const [method, operation] of Object.entries(methods)) {
2863
+ if (method === "get" || method === "post") {
2864
+ const op = operation;
2865
+ if (!op.operationId) {
2866
+ op.operationId = deriveOperationId(method, path);
2867
+ }
2868
+ delete op.security;
2869
+ filteredMethods[method] = op;
2870
+ }
2871
+ }
2872
+ if (Object.keys(filteredMethods).length > 0) {
2873
+ filteredPaths[path] = filteredMethods;
2874
+ }
2875
+ }
2876
+ spec.paths = filteredPaths;
2877
+ }
2878
+ const components = spec.components;
2879
+ if (components) {
2880
+ delete components.securitySchemes;
2881
+ }
2882
+ const usedTags = /* @__PURE__ */ new Set();
2883
+ if (spec.paths) {
2884
+ for (const methods of Object.values(spec.paths)) {
2885
+ for (const op of Object.values(methods)) {
2886
+ const operation = op;
2887
+ if (Array.isArray(operation.tags)) {
2888
+ for (const tag of operation.tags) {
2889
+ usedTags.add(tag);
2890
+ }
2891
+ }
2892
+ }
2893
+ }
2894
+ }
2895
+ if (Array.isArray(spec.tags)) {
2896
+ spec.tags = spec.tags.filter((t) => usedTags.has(t.name));
2897
+ }
2898
+ return spec;
2899
+ }
2900
+ function deriveOperationId(method, path) {
2901
+ const segments = path.split("/").filter((s) => s.length > 0).map((s) => {
2902
+ if (s.startsWith("{") || s.startsWith(":")) {
2903
+ const paramName = s.replace(/[{}:]/g, "");
2904
+ return "By" + paramName.charAt(0).toUpperCase() + paramName.slice(1);
2905
+ }
2906
+ return s.split("-").map((part, i) => i === 0 ? part.charAt(0).toUpperCase() + part.slice(1) : part.charAt(0).toUpperCase() + part.slice(1)).join("");
2907
+ });
2908
+ return method + segments.join("");
2909
+ }
2910
+
2911
+ // src/feedback/api.ts
2912
+ var feedbackPlugin = async (fastify, opts) => {
2913
+ const { db } = opts;
2914
+ fastify.post("/api/feedback", {
2915
+ schema: {
2916
+ tags: ["feedback"],
2917
+ summary: "Submit structured feedback for a completed transaction",
2918
+ body: { type: "object", additionalProperties: true },
2919
+ response: {
2920
+ 201: {
2921
+ type: "object",
2922
+ properties: {
2923
+ feedback_id: { type: "string" },
2924
+ received_at: { type: "string" }
2925
+ }
2926
+ },
2927
+ 400: { type: "object", properties: { error: { type: "string" }, issues: { type: "array" } } },
2928
+ 404: { type: "object", properties: { error: { type: "string" } } },
2929
+ 409: { type: "object", properties: { error: { type: "string" } } }
2930
+ }
2931
+ }
2932
+ }, async (request, reply) => {
2933
+ const parseResult = StructuredFeedbackSchema.safeParse(request.body);
2934
+ if (!parseResult.success) {
2935
+ return reply.code(400).send({
2936
+ error: "Validation failed",
2937
+ issues: parseResult.error.issues
2938
+ });
2939
+ }
2940
+ const feedback = parseResult.data;
2941
+ const txRow = db.prepare("SELECT id FROM request_log WHERE id = ?").get(feedback.transaction_id);
2942
+ if (!txRow) {
2943
+ return reply.code(404).send({ error: "transaction_not_found" });
2944
+ }
2945
+ const dupRow = db.prepare("SELECT id FROM feedback WHERE transaction_id = ?").get(feedback.transaction_id);
2946
+ if (dupRow) {
2947
+ return reply.code(409).send({ error: "feedback_already_submitted" });
2948
+ }
2949
+ const feedbackId = insertFeedback(db, feedback);
2950
+ const receivedAt = (/* @__PURE__ */ new Date()).toISOString();
2951
+ return reply.code(201).send({ feedback_id: feedbackId, received_at: receivedAt });
2952
+ });
2953
+ fastify.get("/api/feedback/:skill_id", {
2954
+ schema: {
2955
+ tags: ["feedback"],
2956
+ summary: "List feedback for a specific skill",
2957
+ params: {
2958
+ type: "object",
2959
+ properties: { skill_id: { type: "string" } },
2960
+ required: ["skill_id"]
2961
+ },
2962
+ querystring: {
2963
+ type: "object",
2964
+ properties: {
2965
+ limit: { type: "integer", default: 20, description: "Max entries (max 100)" },
2966
+ since: { type: "string", description: "ISO datetime filter (optional)" }
2967
+ }
2968
+ },
2969
+ response: {
2970
+ 200: {
2971
+ type: "object",
2972
+ properties: {
2973
+ feedbacks: { type: "array" },
2974
+ count: { type: "integer" }
2975
+ }
2976
+ }
2977
+ }
2978
+ }
2979
+ }, async (request, reply) => {
2980
+ const { skill_id } = request.params;
2981
+ const query = request.query;
2982
+ const rawLimit = query.limit !== void 0 ? parseInt(query.limit, 10) : 20;
2983
+ const limit = Math.min(isNaN(rawLimit) || rawLimit < 1 ? 20 : rawLimit, 100);
2984
+ let feedbacks = getFeedbackForSkill(db, skill_id, limit);
2985
+ const since = query.since?.trim();
2986
+ if (since) {
2987
+ const sinceDate = new Date(since).getTime();
2988
+ if (!isNaN(sinceDate)) {
2989
+ feedbacks = feedbacks.filter((f) => new Date(f.timestamp).getTime() > sinceDate);
2990
+ }
2991
+ }
2992
+ return reply.send({ feedbacks, count: feedbacks.length });
2993
+ });
2994
+ fastify.get("/api/reputation/:agent_id", {
2995
+ schema: {
2996
+ tags: ["feedback"],
2997
+ summary: "Get aggregated reputation score for an agent",
2998
+ params: {
2999
+ type: "object",
3000
+ properties: { agent_id: { type: "string" } },
3001
+ required: ["agent_id"]
3002
+ },
3003
+ response: {
3004
+ 200: {
3005
+ type: "object",
3006
+ properties: {
3007
+ agent_id: { type: "string" },
3008
+ reputation_score: { type: "number" },
3009
+ feedback_count: { type: "integer" },
3010
+ last_updated: { type: "string" }
3011
+ }
3012
+ }
3013
+ }
3014
+ }
3015
+ }, async (request, reply) => {
3016
+ const { agent_id } = request.params;
3017
+ const feedbacks = getFeedbackForProvider(db, agent_id);
3018
+ const reputationScore = computeReputation(feedbacks);
3019
+ const lastUpdated = feedbacks.length > 0 ? feedbacks[0].timestamp : (/* @__PURE__ */ new Date()).toISOString();
3020
+ return reply.send({
3021
+ agent_id,
3022
+ reputation_score: reputationScore,
3023
+ feedback_count: feedbacks.length,
3024
+ last_updated: lastUpdated
3025
+ });
3026
+ });
3027
+ };
3028
+ var api_default = feedbackPlugin;
3029
+
3030
+ // src/evolution/schema.ts
3031
+ import { z as z4 } from "zod";
3032
+ var CoreMemoryEntrySchema = z4.object({
3033
+ category: z4.string(),
3034
+ importance: z4.number().min(0).max(1),
3035
+ content: z4.string(),
3036
+ scope: z4.string().optional()
3037
+ });
3038
+ var TemplateEvolutionSchema = z4.object({
3039
+ /** e.g. "genesis-template" */
3040
+ template_name: z4.string().min(1),
3041
+ /** Semantic version string, e.g. "1.2.3" */
3042
+ template_version: z4.string().regex(/^\d+\.\d+\.\d+$/, "Must be a valid semver string (e.g. 1.2.3)"),
3043
+ /** Identifier of the agent publishing this evolution */
3044
+ publisher_agent: z4.string().min(1),
3045
+ /** Human-readable description of what changed in this evolution */
3046
+ changelog: z4.string().max(1e3),
3047
+ /** Snapshot of the agent's core memory at the time of evolution (max 50 entries) */
3048
+ core_memory_snapshot: z4.array(CoreMemoryEntrySchema).max(50),
3049
+ /** Delta in fitness score from before evolution (range -1 to 1) */
3050
+ fitness_improvement: z4.number().min(-1).max(1),
3051
+ /** ISO 8601 datetime string */
3052
+ timestamp: z4.string().datetime()
3053
+ });
3054
+
3055
+ // src/evolution/api.ts
3056
+ var evolutionPlugin = async (fastify, opts) => {
3057
+ const { db } = opts;
3058
+ fastify.post("/api/evolution/publish", {
3059
+ schema: {
3060
+ tags: ["evolution"],
3061
+ summary: "Publish a new template evolution record",
3062
+ body: { type: "object", additionalProperties: true },
3063
+ response: {
3064
+ 201: {
3065
+ type: "object",
3066
+ properties: {
3067
+ evolution_id: { type: "string" },
3068
+ published_at: { type: "string" }
3069
+ }
3070
+ },
3071
+ 400: {
3072
+ type: "object",
3073
+ properties: {
3074
+ error: { type: "string" },
3075
+ issues: { type: "array" }
3076
+ }
3077
+ }
3078
+ }
3079
+ }
3080
+ }, async (request, reply) => {
3081
+ const parseResult = TemplateEvolutionSchema.safeParse(request.body);
3082
+ if (!parseResult.success) {
3083
+ return reply.code(400).send({
3084
+ error: "Validation failed",
3085
+ issues: parseResult.error.issues
3086
+ });
3087
+ }
3088
+ const evolutionId = insertEvolution(db, parseResult.data);
3089
+ const publishedAt = (/* @__PURE__ */ new Date()).toISOString();
3090
+ return reply.code(201).send({ evolution_id: evolutionId, published_at: publishedAt });
3091
+ });
3092
+ fastify.get("/api/evolution/latest", {
3093
+ schema: {
3094
+ tags: ["evolution"],
3095
+ summary: "Get latest evolution for a template",
3096
+ querystring: {
3097
+ type: "object",
3098
+ properties: {
3099
+ template: { type: "string", description: "Template name to query" }
3100
+ },
3101
+ required: ["template"]
3102
+ },
3103
+ response: {
3104
+ 200: {
3105
+ type: "object",
3106
+ properties: {
3107
+ evolution: {}
3108
+ }
3109
+ },
3110
+ 400: {
3111
+ type: "object",
3112
+ properties: { error: { type: "string" } }
3113
+ }
3114
+ }
3115
+ }
3116
+ }, async (request, reply) => {
3117
+ const query = request.query;
3118
+ const templateName = query.template?.trim();
3119
+ if (!templateName) {
3120
+ return reply.code(400).send({ error: "template query parameter is required" });
3121
+ }
3122
+ const evolution = getLatestEvolution(db, templateName);
3123
+ return reply.send({ evolution });
3124
+ });
3125
+ fastify.get("/api/evolution/history", {
3126
+ schema: {
3127
+ tags: ["evolution"],
3128
+ summary: "Get evolution history for a template",
3129
+ querystring: {
3130
+ type: "object",
3131
+ properties: {
3132
+ template: { type: "string", description: "Template name to query" },
3133
+ limit: { type: "integer", default: 10, description: "Max entries (max 100)" }
3134
+ },
3135
+ required: ["template"]
3136
+ },
3137
+ response: {
3138
+ 200: {
3139
+ type: "object",
3140
+ properties: {
3141
+ evolutions: { type: "array" },
3142
+ count: { type: "integer" }
3143
+ }
3144
+ },
3145
+ 400: {
3146
+ type: "object",
3147
+ properties: { error: { type: "string" } }
3148
+ }
3149
+ }
3150
+ }
3151
+ }, async (request, reply) => {
3152
+ const query = request.query;
3153
+ const templateName = query.template?.trim();
3154
+ if (!templateName) {
3155
+ return reply.code(400).send({ error: "template query parameter is required" });
3156
+ }
3157
+ const rawLimit = query.limit !== void 0 ? parseInt(query.limit, 10) : 10;
3158
+ const limit = Math.min(isNaN(rawLimit) || rawLimit < 1 ? 10 : rawLimit, 100);
3159
+ const evolutions = getEvolutionHistory(db, templateName, limit);
3160
+ return reply.send({ evolutions, count: evolutions.length });
3161
+ });
3162
+ };
3163
+ var api_default2 = evolutionPlugin;
3164
+
3165
+ // src/registry/server.ts
3166
+ function stripInternal(card) {
3167
+ const { _internal: _, ...publicCard } = card;
3168
+ return publicCard;
3169
+ }
3170
+ function createRegistryServer(opts) {
3171
+ const { registryDb: db, silent = false } = opts;
3172
+ const server = Fastify2({ logger: !silent });
3173
+ void server.register(swagger, {
3174
+ openapi: {
3175
+ openapi: "3.0.3",
3176
+ info: {
3177
+ title: "AgentBnB Registry API",
3178
+ description: "P2P Agent Capability Sharing Protocol \u2014 discover, publish, and exchange agent capabilities",
3179
+ version: "3.1.6"
3180
+ },
3181
+ servers: [{ url: "/", description: "Registry server" }],
3182
+ tags: [
3183
+ { name: "cards", description: "Capability card CRUD" },
3184
+ { name: "credits", description: "Credit hold/settle/release (Ed25519 auth required)" },
3185
+ { name: "agents", description: "Agent profiles and reputation" },
3186
+ { name: "identity", description: "Agent identity and guarantor registration" },
3187
+ { name: "owner", description: "Owner-only endpoints (Bearer auth required)" },
3188
+ { name: "system", description: "Health and stats" },
3189
+ { name: "pricing", description: "Market pricing statistics" }
3190
+ ],
3191
+ components: {
3192
+ securitySchemes: {
3193
+ bearerAuth: { type: "http", scheme: "bearer" },
3194
+ ed25519Auth: {
3195
+ type: "apiKey",
3196
+ in: "header",
3197
+ name: "X-Agent-PublicKey",
3198
+ description: "Ed25519 public key (hex). Also requires X-Agent-Signature and X-Agent-Timestamp headers."
3199
+ }
3200
+ }
3201
+ }
3202
+ }
3203
+ });
3204
+ void server.register(swaggerUi, {
3205
+ routePrefix: "/docs",
3206
+ uiConfig: { docExpansion: "list", deepLinking: true }
3207
+ });
3208
+ void server.register(cors, {
3209
+ origin: true,
3210
+ methods: ["GET", "POST", "PATCH", "OPTIONS"],
3211
+ allowedHeaders: ["Content-Type", "Authorization", "X-Agent-PublicKey", "X-Agent-Signature", "X-Agent-Timestamp"]
3212
+ });
3213
+ void server.register(fastifyWebsocket);
3214
+ let relayState = null;
3215
+ if (opts.creditDb) {
3216
+ relayState = registerWebSocketRelay(server, db, opts.creditDb);
3217
+ }
3218
+ if (opts.creditDb) {
3219
+ void server.register(creditRoutesPlugin, { creditDb: opts.creditDb });
3220
+ }
3221
+ if (opts.creditDb) {
3222
+ void server.register(hubAgentRoutesPlugin, { registryDb: db, creditDb: opts.creditDb });
3223
+ if (relayState?.setOnAgentOnline && relayState.getConnections && relayState.getPendingRequests && relayState.sendMessage) {
3224
+ const bridge = createRelayBridge({
3225
+ registryDb: db,
3226
+ creditDb: opts.creditDb,
3227
+ sendMessage: relayState.sendMessage,
3228
+ pendingRequests: relayState.getPendingRequests(),
3229
+ connections: relayState.getConnections()
3230
+ });
3231
+ relayState.setOnAgentOnline(bridge.onAgentOnline);
3232
+ }
3233
+ }
3234
+ const __filename = fileURLToPath(import.meta.url);
3235
+ const __dirname = dirname(__filename);
3236
+ const hubDistCandidates = [
3237
+ join(__dirname, "../../hub/dist"),
3238
+ // When running from dist/registry/server.js
3239
+ join(__dirname, "../../../hub/dist")
3240
+ // Fallback for alternative layouts
3241
+ ];
3242
+ const hubDistDir = hubDistCandidates.find((p) => existsSync2(p));
3243
+ if (hubDistDir) {
3244
+ void server.register(fastifyStatic, {
3245
+ root: hubDistDir,
3246
+ prefix: "/hub/"
3247
+ });
3248
+ server.get("/", async (_request, reply) => {
3249
+ return reply.redirect("/hub/");
3250
+ });
3251
+ server.get("/hub", async (_request, reply) => {
3252
+ return reply.redirect("/hub/");
3253
+ });
3254
+ server.setNotFoundHandler(async (request, reply) => {
3255
+ if (request.url.startsWith("/hub/")) {
3256
+ return reply.sendFile("index.html");
3257
+ }
3258
+ return reply.code(404).send({ error: "Not found" });
3259
+ });
3260
+ }
3261
+ void server.register(api_default, { db });
3262
+ void server.register(api_default2, { db });
3263
+ void server.register(async (api) => {
3264
+ api.get("/health", {
3265
+ schema: {
3266
+ tags: ["system"],
3267
+ summary: "Liveness probe",
3268
+ response: { 200: { type: "object", properties: { status: { type: "string" } } } }
3269
+ }
3270
+ }, async (_request, reply) => {
3271
+ return reply.send({ status: "ok" });
3272
+ });
3273
+ api.get("/cards", {
3274
+ schema: {
3275
+ tags: ["cards"],
3276
+ summary: "List and search capability cards",
3277
+ querystring: {
3278
+ type: "object",
3279
+ properties: {
3280
+ q: { type: "string", description: "Full-text search query" },
3281
+ level: { type: "integer", enum: [1, 2, 3], description: "Capability level filter" },
3282
+ online: { type: "string", enum: ["true", "false"], description: "Availability filter" },
3283
+ tag: { type: "string", description: "Filter by metadata tag" },
3284
+ min_success_rate: { type: "number", description: "Minimum success rate (0-1)" },
3285
+ max_latency_ms: { type: "number", description: "Maximum average latency in ms" },
3286
+ min_reputation: { type: "number", description: "Minimum reputation score (0-1) based on peer feedback" },
3287
+ sort: { type: "string", enum: ["popular", "rated", "success_rate", "cheapest", "newest", "latency", "reputation_desc", "reputation_asc"], description: "Sort order" },
3288
+ limit: { type: "integer", default: 20, description: "Max items per page (max 100)" },
3289
+ offset: { type: "integer", default: 0, description: "Pagination offset" }
3290
+ }
3291
+ },
3292
+ response: {
3293
+ 200: {
3294
+ type: "object",
3295
+ properties: {
3296
+ total: { type: "integer" },
3297
+ limit: { type: "integer" },
3298
+ offset: { type: "integer" },
3299
+ items: { type: "array" },
3300
+ uses_this_week: { type: "object", additionalProperties: { type: "number" } }
3301
+ }
3302
+ }
3303
+ }
3304
+ }
3305
+ }, async (request, reply) => {
3306
+ const query = request.query;
3307
+ const q = query.q?.trim() ?? "";
3308
+ const levelRaw = query.level !== void 0 ? parseInt(query.level, 10) : void 0;
3309
+ const level = levelRaw === 1 || levelRaw === 2 || levelRaw === 3 ? levelRaw : void 0;
3310
+ const onlineRaw = query.online;
3311
+ const online = onlineRaw === "true" ? true : onlineRaw === "false" ? false : void 0;
3312
+ const tag = query.tag?.trim();
3313
+ const minSuccessRate = query.min_success_rate !== void 0 ? parseFloat(query.min_success_rate) : void 0;
3314
+ const maxLatencyMs = query.max_latency_ms !== void 0 ? parseFloat(query.max_latency_ms) : void 0;
3315
+ const minReputation = query.min_reputation !== void 0 ? parseFloat(query.min_reputation) : void 0;
3316
+ const sort = query.sort;
3317
+ const rawLimit = query.limit !== void 0 ? parseInt(query.limit, 10) : 20;
3318
+ const limit = Math.min(isNaN(rawLimit) || rawLimit < 1 ? 20 : rawLimit, 100);
3319
+ const rawOffset = query.offset !== void 0 ? parseInt(query.offset, 10) : 0;
3320
+ const offset = isNaN(rawOffset) || rawOffset < 0 ? 0 : rawOffset;
3321
+ let cards;
3322
+ if (q.length > 0) {
3323
+ cards = searchCards(db, q, { level, online, min_reputation: minReputation });
3324
+ } else {
3325
+ cards = filterCards(db, { level, online, min_reputation: minReputation });
3326
+ }
3327
+ if (tag !== void 0 && tag.length > 0) {
3328
+ cards = cards.filter((c) => {
3329
+ const rootTags = c.metadata?.tags ?? [];
3330
+ const skillTags = c.skills?.flatMap((s) => s.metadata?.tags ?? []) ?? [];
3331
+ return [...rootTags, ...skillTags].includes(tag);
3332
+ });
3333
+ }
3334
+ if (maxLatencyMs !== void 0 && !isNaN(maxLatencyMs)) {
3335
+ cards = cards.filter(
3336
+ (c) => (c.metadata?.avg_latency_ms ?? Infinity) <= maxLatencyMs
3337
+ );
3338
+ }
3339
+ const usesStmt = db.prepare(`
3340
+ SELECT card_id, skill_id, COUNT(*) as cnt
3341
+ FROM request_log
3342
+ WHERE status = 'success'
3343
+ AND created_at > datetime('now', '-7 days')
3344
+ AND (action_type IS NULL OR action_type = 'auto_share')
3345
+ GROUP BY card_id, skill_id
3346
+ `);
3347
+ const usesRows = usesStmt.all();
3348
+ const usesMap = /* @__PURE__ */ new Map();
3349
+ for (const row of usesRows) {
3350
+ usesMap.set(row.card_id, (usesMap.get(row.card_id) ?? 0) + row.cnt);
3351
+ if (row.skill_id) {
3352
+ usesMap.set(row.skill_id, (usesMap.get(row.skill_id) ?? 0) + row.cnt);
3353
+ }
3354
+ }
3355
+ const ownerTrustMap = /* @__PURE__ */ new Map();
3356
+ const uniqueOwners = [...new Set(cards.map((c) => c.owner))];
3357
+ if (uniqueOwners.length > 0) {
3358
+ const placeholders = uniqueOwners.map(() => "?").join(",");
3359
+ const trustStmt = db.prepare(`
3360
+ SELECT cc.owner,
3361
+ COUNT(rl.id) as total_exec,
3362
+ SUM(CASE WHEN rl.status IN ('success','failure','timeout','refunded') THEN 1 ELSE 0 END) as terminal_exec,
3363
+ SUM(CASE WHEN rl.status = 'success' THEN 1 ELSE 0 END) as success_exec,
3364
+ AVG(CASE WHEN rl.status = 'success' THEN rl.latency_ms END) as avg_latency
3365
+ FROM capability_cards cc
3366
+ LEFT JOIN request_log rl ON rl.card_id = cc.id AND rl.action_type IS NULL
3367
+ WHERE cc.owner IN (${placeholders})
3368
+ GROUP BY cc.owner
3369
+ `);
3370
+ const trustRows = trustStmt.all(...uniqueOwners);
3371
+ for (const row of trustRows) {
3372
+ const terminalExec = row.terminal_exec ?? 0;
3373
+ const successExec = row.success_exec ?? 0;
3374
+ const successRate = terminalExec > 0 ? successExec / terminalExec : 0;
3375
+ let tier = 0;
3376
+ if (row.total_exec > 10) tier = 1;
3377
+ if (row.total_exec > 50 && successRate >= 0.85) tier = 2;
3378
+ ownerTrustMap.set(row.owner, {
3379
+ performance_tier: tier,
3380
+ authority_source: "self",
3381
+ // Phase 1: all self-declared
3382
+ success_rate: successRate,
3383
+ avg_latency_ms: Math.round(row.avg_latency ?? 0),
3384
+ terminal_exec: terminalExec
3385
+ });
3386
+ }
3387
+ }
3388
+ if (minSuccessRate !== void 0 && !isNaN(minSuccessRate)) {
3389
+ cards = cards.filter((c) => {
3390
+ const trust = ownerTrustMap.get(c.owner);
3391
+ if (!trust || trust.terminal_exec === 0) return false;
3392
+ return trust.success_rate >= minSuccessRate;
3393
+ });
3394
+ }
3395
+ if (sort === "popular") {
3396
+ cards = [...cards].sort((a, b) => {
3397
+ const aUses = usesMap.get(a.id) ?? 0;
3398
+ const bUses = usesMap.get(b.id) ?? 0;
3399
+ return bUses - aUses;
3400
+ });
3401
+ } else if (sort === "rated" || sort === "success_rate") {
3402
+ cards = [...cards].sort((a, b) => {
3403
+ const aRate = a.metadata?.success_rate ?? -1;
3404
+ const bRate = b.metadata?.success_rate ?? -1;
3405
+ return bRate - aRate;
3406
+ });
3407
+ } else if (sort === "cheapest") {
3408
+ cards = [...cards].sort((a, b) => {
3409
+ const aCard = a;
3410
+ const bCard = b;
3411
+ const aPrice = aCard.pricing?.credits_per_call ?? aCard.skills?.[0]?.pricing?.credits_per_call ?? Infinity;
3412
+ const bPrice = bCard.pricing?.credits_per_call ?? bCard.skills?.[0]?.pricing?.credits_per_call ?? Infinity;
3413
+ return aPrice - bPrice;
3414
+ });
3415
+ } else if (sort === "newest") {
3416
+ const createdStmt = db.prepare("SELECT id, created_at FROM capability_cards");
3417
+ const createdRows = createdStmt.all();
3418
+ const createdMap = new Map(createdRows.map((r) => [r.id, r.created_at]));
3419
+ cards = [...cards].sort((a, b) => {
3420
+ const aDate = createdMap.get(a.id) ?? "";
3421
+ const bDate = createdMap.get(b.id) ?? "";
3422
+ return bDate.localeCompare(aDate);
3423
+ });
3424
+ } else if (sort === "latency") {
3425
+ cards = [...cards].sort((a, b) => {
3426
+ const aLatency = a.metadata?.avg_latency_ms ?? Infinity;
3427
+ const bLatency = b.metadata?.avg_latency_ms ?? Infinity;
3428
+ return aLatency - bLatency;
3429
+ });
3430
+ } else if (sort === "reputation_desc" || sort === "reputation_asc") {
3431
+ const repMap = buildReputationMap(db, cards.map((c) => c.owner));
3432
+ const dir = sort === "reputation_desc" ? -1 : 1;
3433
+ cards = [...cards].sort((a, b) => {
3434
+ const aScore = repMap.get(a.owner) ?? 0.5;
3435
+ const bScore = repMap.get(b.owner) ?? 0.5;
3436
+ return dir * (bScore - aScore);
3437
+ });
3438
+ }
3439
+ const total = cards.length;
3440
+ const pagedCards = cards.slice(offset, offset + limit);
3441
+ const pageRepMap = buildReputationMap(db, pagedCards.map((c) => c.owner));
3442
+ const items = pagedCards.map((card) => {
3443
+ const trust = ownerTrustMap.get(card.owner);
3444
+ const stripped = stripInternal(card);
3445
+ return {
3446
+ ...stripped,
3447
+ performance_tier: trust?.performance_tier ?? 0,
3448
+ authority_source: trust?.authority_source ?? "self",
3449
+ reputation_score: pageRepMap.get(card.owner) ?? 0.5,
3450
+ // Enrich metadata with live execution-based success_rate if available
3451
+ metadata: trust && trust.terminal_exec > 0 ? {
3452
+ ...stripped.metadata,
3453
+ success_rate: trust.success_rate,
3454
+ avg_latency_ms: trust.avg_latency_ms || stripped.metadata?.avg_latency_ms
3455
+ } : stripped.metadata
3456
+ };
3457
+ });
3458
+ const usesThisWeek = {};
3459
+ for (const [key, count] of usesMap) {
3460
+ if (count > 0) usesThisWeek[key] = count;
3461
+ }
3462
+ const result = { total, limit, offset, items, uses_this_week: usesThisWeek };
3463
+ return reply.send(result);
3464
+ });
3465
+ api.get("/api/cards/trending", {
3466
+ schema: {
3467
+ tags: ["cards"],
3468
+ summary: "Top 10 trending skills by recent usage",
3469
+ response: { 200: { type: "object", properties: { items: { type: "array" } } } }
3470
+ }
3471
+ }, async (_request, reply) => {
3472
+ const trendingStmt = db.prepare(`
3473
+ SELECT rl.card_id, COUNT(*) as recent_requests
3474
+ FROM request_log rl
3475
+ WHERE rl.status = 'success'
3476
+ AND rl.created_at > datetime('now', '-7 days')
3477
+ AND (rl.action_type IS NULL OR rl.action_type = 'auto_share')
3478
+ GROUP BY rl.card_id
3479
+ ORDER BY recent_requests DESC
3480
+ LIMIT 10
3481
+ `);
3482
+ const trendingRows = trendingStmt.all();
3483
+ const items = trendingRows.map((row) => {
3484
+ const card = getCard(db, row.card_id);
3485
+ if (!card) return null;
3486
+ return { ...stripInternal(card), uses_this_week: row.recent_requests };
3487
+ }).filter((item) => item !== null);
3488
+ return reply.send({ items });
3489
+ });
3490
+ api.get("/api/pricing", {
3491
+ schema: {
3492
+ tags: ["pricing"],
3493
+ summary: "Aggregate pricing statistics for skills matching a query",
3494
+ querystring: {
3495
+ type: "object",
3496
+ properties: { q: { type: "string", description: "Search query (required)" } },
3497
+ required: ["q"]
3498
+ },
3499
+ response: {
3500
+ 200: {
3501
+ type: "object",
3502
+ properties: {
3503
+ query: { type: "string" },
3504
+ min: { type: "number" },
3505
+ max: { type: "number" },
3506
+ median: { type: "number" },
3507
+ mean: { type: "number" },
3508
+ count: { type: "integer" }
3509
+ }
3510
+ },
3511
+ 400: { type: "object", properties: { error: { type: "string" } } }
3512
+ }
3513
+ }
3514
+ }, async (request, reply) => {
3515
+ const query = request.query;
3516
+ const q = query.q?.trim();
3517
+ if (!q) {
3518
+ return reply.code(400).send({ error: "q parameter is required" });
3519
+ }
3520
+ const stats = getPricingStats(db, q);
3521
+ return reply.send({ query: q, ...stats });
3522
+ });
3523
+ api.get("/cards/:id", {
3524
+ schema: {
3525
+ tags: ["cards"],
3526
+ summary: "Get a capability card by ID",
3527
+ params: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
3528
+ response: {
3529
+ 200: { type: "object", additionalProperties: true },
3530
+ 404: { type: "object", properties: { error: { type: "string" } } }
3531
+ }
3532
+ }
3533
+ }, async (request, reply) => {
3534
+ const { id } = request.params;
3535
+ const card = getCard(db, id);
3536
+ if (!card) {
3537
+ return reply.code(404).send({ error: "Not found" });
3538
+ }
3539
+ return reply.send(stripInternal(card));
3540
+ });
3541
+ api.post("/cards", {
3542
+ schema: {
3543
+ tags: ["cards"],
3544
+ summary: "Publish a capability card",
3545
+ body: { type: "object", additionalProperties: true, description: "Capability card JSON (v1.0 or v2.0)" },
3546
+ response: {
3547
+ 201: { type: "object", properties: { ok: { type: "boolean" }, id: { type: "string" } } },
3548
+ 400: { type: "object", properties: { error: { type: "string" }, issues: { type: "array" } } }
3549
+ }
3550
+ }
3551
+ }, async (request, reply) => {
3552
+ const body = request.body;
3553
+ if (!body.spec_version) {
3554
+ body.spec_version = "1.0";
3555
+ }
3556
+ const result = AnyCardSchema.safeParse(body);
3557
+ if (!result.success) {
3558
+ return reply.code(400).send({
3559
+ error: "Card validation failed",
3560
+ issues: result.error.issues
3561
+ });
3562
+ }
3563
+ const card = result.data;
3564
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3565
+ if (card.spec_version === "2.0") {
3566
+ const cardWithTimestamps = {
3567
+ ...card,
3568
+ created_at: card.created_at ?? now,
3569
+ updated_at: now
3570
+ };
3571
+ db.prepare(
3572
+ `INSERT OR REPLACE INTO capability_cards (id, owner, data, created_at, updated_at)
3573
+ VALUES (?, ?, ?, ?, ?)`
3574
+ ).run(
3575
+ cardWithTimestamps.id,
3576
+ cardWithTimestamps.owner,
3577
+ JSON.stringify(cardWithTimestamps),
3578
+ cardWithTimestamps.created_at,
3579
+ cardWithTimestamps.updated_at
3580
+ );
3581
+ } else {
3582
+ try {
3583
+ insertCard(db, card);
3584
+ } catch (err) {
3585
+ if (err instanceof AgentBnBError && err.code === "VALIDATION_ERROR") {
3586
+ return reply.code(400).send({ error: err.message });
3587
+ }
3588
+ throw err;
3589
+ }
3590
+ }
3591
+ return reply.code(201).send({ ok: true, id: card.id });
3592
+ });
3593
+ api.delete("/cards/:id", {
3594
+ schema: {
3595
+ tags: ["cards"],
3596
+ summary: "Delete a capability card",
3597
+ params: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
3598
+ response: {
3599
+ 200: { type: "object", properties: { ok: { type: "boolean" }, id: { type: "string" } } },
3600
+ 404: { type: "object", properties: { error: { type: "string" } } }
3601
+ }
3602
+ }
3603
+ }, async (request, reply) => {
3604
+ const { id } = request.params;
3605
+ const card = getCard(db, id);
3606
+ if (!card) {
3607
+ return reply.code(404).send({ error: "Not found" });
3608
+ }
3609
+ db.prepare("DELETE FROM capability_cards WHERE id = ?").run(id);
3610
+ return reply.send({ ok: true, id });
3611
+ });
3612
+ api.get("/api/agents", {
3613
+ schema: {
3614
+ tags: ["agents"],
3615
+ summary: "List all agent profiles sorted by reputation",
3616
+ response: {
3617
+ 200: {
3618
+ type: "object",
3619
+ properties: { items: { type: "array" }, total: { type: "integer" } }
3620
+ }
3621
+ }
3622
+ }
3623
+ }, async (_request, reply) => {
3624
+ const allCards = listCards(db);
3625
+ const ownerMap = /* @__PURE__ */ new Map();
3626
+ for (const card of allCards) {
3627
+ const existing = ownerMap.get(card.owner) ?? [];
3628
+ existing.push(card);
3629
+ ownerMap.set(card.owner, existing);
3630
+ }
3631
+ const creditsStmt = db.prepare(`
3632
+ SELECT cc.owner,
3633
+ SUM(CASE WHEN rl.status = 'success' THEN rl.credits_charged ELSE 0 END) as credits_earned
3634
+ FROM capability_cards cc
3635
+ LEFT JOIN request_log rl ON rl.card_id = cc.id
3636
+ GROUP BY cc.owner
3637
+ `);
3638
+ const creditsRows = creditsStmt.all();
3639
+ const creditsMap = new Map(creditsRows.map((r) => [r.owner, r.credits_earned ?? 0]));
3640
+ const agents = Array.from(ownerMap.entries()).map(([owner, cards]) => {
3641
+ const skillCount = cards.reduce((sum, card) => sum + (card.skills?.length ?? 1), 0);
3642
+ const successRates = cards.map((c) => c.metadata?.success_rate).filter((r) => r != null);
3643
+ const avgSuccessRate = successRates.length > 0 ? successRates.reduce((a, b) => a + b, 0) / successRates.length : null;
3644
+ const memberStmt = db.prepare(
3645
+ "SELECT MIN(created_at) as earliest FROM capability_cards WHERE owner = ?"
3646
+ );
3647
+ const memberRow = memberStmt.get(owner);
3648
+ return {
3649
+ owner,
3650
+ skill_count: skillCount,
3651
+ success_rate: avgSuccessRate,
3652
+ total_earned: creditsMap.get(owner) ?? 0,
3653
+ member_since: memberRow?.earliest ?? (/* @__PURE__ */ new Date()).toISOString()
3654
+ };
3655
+ });
3656
+ agents.sort((a, b) => {
3657
+ const aRate = a.success_rate ?? -1;
3658
+ const bRate = b.success_rate ?? -1;
3659
+ if (bRate !== aRate) return bRate - aRate;
3660
+ return b.total_earned - a.total_earned;
3661
+ });
3662
+ return reply.send({ items: agents, total: agents.length });
3663
+ });
3664
+ api.get("/api/agents/:owner", {
3665
+ schema: {
3666
+ tags: ["agents"],
3667
+ summary: "Get agent profile, skills, and recent activity (AgentProfileV2)",
3668
+ params: { type: "object", properties: { owner: { type: "string" } }, required: ["owner"] },
3669
+ response: {
3670
+ 200: { type: "object", additionalProperties: true },
3671
+ 404: { type: "object", properties: { error: { type: "string" } } }
3672
+ }
3673
+ }
3674
+ }, async (request, reply) => {
3675
+ const { owner } = request.params;
3676
+ const ownerCards = listCards(db, owner);
3677
+ if (ownerCards.length === 0) {
3678
+ return reply.status(404).send({ error: "Agent not found" });
3679
+ }
3680
+ const memberStmt = db.prepare(
3681
+ "SELECT MIN(created_at) as earliest, MAX(created_at) as latest FROM capability_cards WHERE owner = ?"
3682
+ );
3683
+ const memberRow = memberStmt.get(owner);
3684
+ const joinedAt = memberRow?.earliest ?? (/* @__PURE__ */ new Date()).toISOString();
3685
+ const lastActiveStmt = db.prepare(`
3686
+ SELECT MAX(rl.created_at) as last_req
3687
+ FROM request_log rl
3688
+ INNER JOIN capability_cards cc ON rl.card_id = cc.id
3689
+ WHERE cc.owner = ?
3690
+ `);
3691
+ const lastActiveRow = lastActiveStmt.get(owner);
3692
+ const lastActive = lastActiveRow?.last_req ?? memberRow?.latest ?? joinedAt;
3693
+ const metricsStmt = db.prepare(`
3694
+ SELECT
3695
+ COUNT(*) as total,
3696
+ SUM(CASE WHEN rl.status = 'success' THEN 1 ELSE 0 END) as successes,
3697
+ AVG(CASE WHEN rl.status = 'success' THEN rl.latency_ms END) as avg_latency,
3698
+ COUNT(DISTINCT rl.requester) as unique_requesters,
3699
+ COUNT(DISTINCT CASE WHEN rl.status = 'success' THEN rl.requester END) as repeat_success_requesters
3700
+ FROM request_log rl
3701
+ INNER JOIN capability_cards cc ON rl.card_id = cc.id
3702
+ WHERE cc.owner = ? AND rl.action_type IS NULL
3703
+ `);
3704
+ const metricsRow = metricsStmt.get(owner);
3705
+ const totalExec = metricsRow?.total ?? 0;
3706
+ const successExec = metricsRow?.successes ?? 0;
3707
+ const successRate = totalExec > 0 ? successExec / totalExec : 0;
3708
+ const avgLatency = metricsRow?.avg_latency ?? 0;
3709
+ const refundRate = totalExec > 0 ? (totalExec - successExec) / totalExec : 0;
3710
+ const uniqueReq = metricsRow?.unique_requesters ?? 0;
3711
+ const repeatRate = uniqueReq > 0 ? (metricsRow?.repeat_success_requesters ?? 0) / uniqueReq : 0;
3712
+ const trendStmt = db.prepare(`
3713
+ SELECT
3714
+ DATE(rl.created_at) as day,
3715
+ COUNT(*) as count,
3716
+ SUM(CASE WHEN rl.status = 'success' THEN 1 ELSE 0 END) as success
3717
+ FROM request_log rl
3718
+ INNER JOIN capability_cards cc ON rl.card_id = cc.id
3719
+ WHERE cc.owner = ? AND rl.action_type IS NULL
3720
+ AND rl.created_at >= DATE('now', '-7 days')
3721
+ GROUP BY DATE(rl.created_at)
3722
+ ORDER BY day ASC
3723
+ `);
3724
+ const trend_7d = trendStmt.all(owner).map((r) => ({ date: r.day, count: r.count, success: r.success }));
3725
+ let performanceTier = 0;
3726
+ if (totalExec > 10) performanceTier = 1;
3727
+ if (totalExec > 50 && successRate >= 0.85) performanceTier = 2;
3728
+ const proofsStmt = db.prepare(`
3729
+ SELECT rl.card_name, rl.status, rl.latency_ms, rl.id, rl.created_at
3730
+ FROM request_log rl
3731
+ INNER JOIN capability_cards cc ON rl.card_id = cc.id
3732
+ WHERE cc.owner = ? AND rl.action_type IS NULL
3733
+ ORDER BY rl.created_at DESC
3734
+ LIMIT 10
3735
+ `);
3736
+ const proofRows = proofsStmt.all(owner);
3737
+ const statusToOutcomeClass = (s) => {
3738
+ if (s === "success") return "completed";
3739
+ if (s === "timeout") return "cancelled";
3740
+ return "failed";
3741
+ };
3742
+ const executionProofs = proofRows.map((r) => ({
3743
+ action: r.card_name,
3744
+ status: r.status === "timeout" ? "timeout" : r.status,
3745
+ outcome_class: statusToOutcomeClass(r.status),
3746
+ latency_ms: r.latency_ms,
3747
+ receipt_id: r.id,
3748
+ proof_source: "request_log",
3749
+ timestamp: r.created_at
3750
+ }));
3751
+ const v2Card = ownerCards.find((c) => c.spec_version === "2.0");
3752
+ const suitability = v2Card?.suitability;
3753
+ const learning = {
3754
+ known_limitations: v2Card?.learning?.known_limitations ?? [],
3755
+ common_failure_patterns: v2Card?.learning?.common_failure_patterns ?? [],
3756
+ recent_improvements: v2Card?.learning?.recent_improvements ?? [],
3757
+ critiques: v2Card?.learning?.critiques ?? []
3758
+ };
3759
+ const activityStmt = db.prepare(`
3760
+ SELECT rl.id, rl.card_name, rl.requester, rl.status, rl.credits_charged, rl.created_at
3761
+ FROM request_log rl
3762
+ INNER JOIN capability_cards cc ON rl.card_id = cc.id
3763
+ WHERE cc.owner = ?
3764
+ ORDER BY rl.created_at DESC
3765
+ LIMIT 10
3766
+ `);
3767
+ const recentActivity = activityStmt.all(owner);
3768
+ const skillCount = ownerCards.reduce((sum, card) => sum + (card.skills?.length ?? 1), 0);
3769
+ const creditsStmt = db.prepare(`
3770
+ SELECT SUM(CASE WHEN rl.status = 'success' THEN rl.credits_charged ELSE 0 END) as credits_earned
3771
+ FROM capability_cards cc
3772
+ LEFT JOIN request_log rl ON rl.card_id = cc.id
3773
+ WHERE cc.owner = ?
3774
+ `);
3775
+ const creditsRow = creditsStmt.get(owner);
3776
+ const response = {
3777
+ owner,
3778
+ agent_name: v2Card?.agent_name,
3779
+ short_description: v2Card?.short_description,
3780
+ joined_at: joinedAt,
3781
+ last_active: lastActive,
3782
+ performance_tier: performanceTier,
3783
+ verification_badges: [],
3784
+ // Phase 1: no verification mechanism yet
3785
+ authority: {
3786
+ authority_source: "self",
3787
+ verification_status: "none"
3788
+ },
3789
+ suitability,
3790
+ trust_metrics: {
3791
+ total_executions: totalExec,
3792
+ successful_executions: successExec,
3793
+ success_rate: successRate,
3794
+ avg_latency_ms: Math.round(avgLatency),
3795
+ refund_rate: refundRate,
3796
+ repeat_use_rate: repeatRate,
3797
+ trend_7d,
3798
+ snapshot_at: null,
3799
+ aggregation_window: "all"
3800
+ },
3801
+ execution_proofs: executionProofs,
3802
+ learning,
3803
+ skills: ownerCards,
3804
+ recent_activity: recentActivity
3805
+ };
3806
+ return reply.send({
3807
+ ...response,
3808
+ profile: {
3809
+ owner,
3810
+ skill_count: skillCount,
3811
+ success_rate: successRate > 0 ? successRate : null,
3812
+ total_earned: creditsRow?.credits_earned ?? 0,
3813
+ member_since: joinedAt
3814
+ }
3815
+ });
3816
+ });
3817
+ api.get("/api/activity", {
3818
+ schema: {
3819
+ tags: ["system"],
3820
+ summary: "Paginated public activity feed of exchange events",
3821
+ querystring: {
3822
+ type: "object",
3823
+ properties: {
3824
+ limit: { type: "integer", default: 20, description: "Max items (max 100)" },
3825
+ since: { type: "string", description: "ISO 8601 timestamp for polling" }
3826
+ }
3827
+ },
3828
+ response: {
3829
+ 200: {
3830
+ type: "object",
3831
+ properties: {
3832
+ items: { type: "array" },
3833
+ total: { type: "integer" },
3834
+ limit: { type: "integer" }
3835
+ }
3836
+ }
3837
+ }
3838
+ }
3839
+ }, async (request, reply) => {
3840
+ const query = request.query;
3841
+ const rawLimit = query.limit !== void 0 ? parseInt(query.limit, 10) : 20;
3842
+ const limit = Math.min(isNaN(rawLimit) || rawLimit < 1 ? 20 : rawLimit, 100);
3843
+ const since = query.since?.trim() || void 0;
3844
+ const items = getActivityFeed(db, limit, since);
3845
+ return reply.send({ items, total: items.length, limit });
3846
+ });
3847
+ api.get("/api/stats", {
3848
+ schema: {
3849
+ tags: ["system"],
3850
+ summary: "Aggregate network statistics",
3851
+ response: {
3852
+ 200: {
3853
+ type: "object",
3854
+ properties: {
3855
+ agents_online: { type: "integer" },
3856
+ total_capabilities: { type: "integer" },
3857
+ total_exchanges: { type: "integer" },
3858
+ executions_7d: { type: "integer" },
3859
+ verified_providers_count: { type: "integer" }
3860
+ }
3861
+ }
3862
+ }
3863
+ }
3864
+ }, async (_request, reply) => {
3865
+ const allCards = listCards(db);
3866
+ const onlineOwners = /* @__PURE__ */ new Set();
3867
+ if (relayState) {
3868
+ for (const owner of relayState.getOnlineOwners()) {
3869
+ onlineOwners.add(owner);
3870
+ }
3871
+ }
3872
+ for (const card of allCards) {
3873
+ if (card.availability.online) {
3874
+ onlineOwners.add(card.owner);
3875
+ }
3876
+ }
3877
+ const exchangeStmt = db.prepare(
3878
+ "SELECT COUNT(*) as count FROM request_log WHERE status = 'success' AND (action_type IS NULL OR action_type = 'auto_share')"
3879
+ );
3880
+ const exchangeRow = exchangeStmt.get();
3881
+ const exec7dStmt = db.prepare(
3882
+ "SELECT COUNT(*) as count FROM request_log WHERE action_type IS NULL AND created_at >= DATE('now', '-7 days')"
3883
+ );
3884
+ const exec7dRow = exec7dStmt.get();
3885
+ return reply.send({
3886
+ agents_online: onlineOwners.size,
3887
+ total_capabilities: allCards.reduce((sum, card) => {
3888
+ const v2 = card;
3889
+ return sum + (v2.skills?.length ?? 1);
3890
+ }, 0),
3891
+ total_exchanges: exchangeRow.count,
3892
+ executions_7d: exec7dRow.count,
3893
+ verified_providers_count: 0
3894
+ // Phase 1: no verification mechanism yet
3895
+ });
3896
+ });
3897
+ api.post("/api/identity/register", {
3898
+ schema: {
3899
+ tags: ["identity"],
3900
+ summary: "Register a human guarantor via GitHub login",
3901
+ body: {
3902
+ type: "object",
3903
+ properties: { github_login: { type: "string" } },
3904
+ required: ["github_login"]
3905
+ },
3906
+ response: {
3907
+ 201: { type: "object", additionalProperties: true },
3908
+ 400: { type: "object", properties: { error: { type: "string" } } },
3909
+ 409: { type: "object", properties: { error: { type: "string" } } },
3910
+ 503: { type: "object", properties: { error: { type: "string" } } }
3911
+ }
3912
+ }
3913
+ }, async (request, reply) => {
3914
+ if (!opts.creditDb) {
3915
+ return reply.code(503).send({ error: "Credit database not configured" });
3916
+ }
3917
+ const body = request.body;
3918
+ const githubLogin = typeof body.github_login === "string" ? body.github_login.trim() : "";
3919
+ if (!githubLogin) {
3920
+ return reply.code(400).send({ error: "github_login is required" });
3921
+ }
3922
+ try {
3923
+ const record = registerGuarantor(opts.creditDb, githubLogin);
3924
+ const auth = initiateGithubAuth();
3925
+ return reply.code(201).send({ guarantor: record, oauth: auth });
3926
+ } catch (err) {
3927
+ if (err instanceof AgentBnBError && err.code === "GUARANTOR_EXISTS") {
3928
+ return reply.code(409).send({ error: err.message });
3929
+ }
3930
+ throw err;
3931
+ }
3932
+ });
3933
+ api.post("/api/identity/link", {
3934
+ schema: {
3935
+ tags: ["identity"],
3936
+ summary: "Link an agent to a human guarantor",
3937
+ body: {
3938
+ type: "object",
3939
+ properties: {
3940
+ agent_id: { type: "string" },
3941
+ github_login: { type: "string" }
3942
+ },
3943
+ required: ["agent_id", "github_login"]
3944
+ },
3945
+ response: {
3946
+ 200: { type: "object", additionalProperties: true },
3947
+ 400: { type: "object", properties: { error: { type: "string" } } },
3948
+ 404: { type: "object", properties: { error: { type: "string" } } },
3949
+ 409: { type: "object", properties: { error: { type: "string" } } },
3950
+ 503: { type: "object", properties: { error: { type: "string" } } }
3951
+ }
3952
+ }
3953
+ }, async (request, reply) => {
3954
+ if (!opts.creditDb) {
3955
+ return reply.code(503).send({ error: "Credit database not configured" });
3956
+ }
3957
+ const body = request.body;
3958
+ const agentId = typeof body.agent_id === "string" ? body.agent_id.trim() : "";
3959
+ const githubLogin = typeof body.github_login === "string" ? body.github_login.trim() : "";
3960
+ if (!agentId || !githubLogin) {
3961
+ return reply.code(400).send({ error: "agent_id and github_login are required" });
3962
+ }
3963
+ try {
3964
+ const record = linkAgentToGuarantor(opts.creditDb, agentId, githubLogin);
3965
+ return reply.send({ guarantor: record });
3966
+ } catch (err) {
3967
+ if (err instanceof AgentBnBError) {
3968
+ const statusMap = {
3969
+ GUARANTOR_NOT_FOUND: 404,
3970
+ MAX_AGENTS_EXCEEDED: 409,
3971
+ AGENT_ALREADY_LINKED: 409
3972
+ };
3973
+ const status = statusMap[err.code] ?? 400;
3974
+ return reply.code(status).send({ error: err.message });
3975
+ }
3976
+ throw err;
3977
+ }
3978
+ });
3979
+ api.get("/api/identity/:agent_id", {
3980
+ schema: {
3981
+ tags: ["identity"],
3982
+ summary: "Get guarantor info for an agent",
3983
+ params: { type: "object", properties: { agent_id: { type: "string" } }, required: ["agent_id"] },
3984
+ response: {
3985
+ 200: {
3986
+ type: "object",
3987
+ properties: {
3988
+ agent_id: { type: "string" },
3989
+ guarantor: { oneOf: [{ type: "object", additionalProperties: true }, { type: "null" }] }
3990
+ }
3991
+ },
3992
+ 503: { type: "object", properties: { error: { type: "string" } } }
3993
+ }
3994
+ }
3995
+ }, async (request, reply) => {
3996
+ if (!opts.creditDb) {
3997
+ return reply.code(503).send({ error: "Credit database not configured" });
3998
+ }
3999
+ const { agent_id } = request.params;
4000
+ const guarantor = getAgentGuarantor(opts.creditDb, agent_id);
4001
+ return reply.send({ agent_id, guarantor });
4002
+ });
4003
+ api.get("/api/openapi/gpt-actions", {
4004
+ schema: {
4005
+ tags: ["system"],
4006
+ summary: "GPT Actions-compatible OpenAPI schema",
4007
+ description: "Returns a GPT Builder-importable OpenAPI spec with only public GET/POST endpoints",
4008
+ querystring: {
4009
+ type: "object",
4010
+ properties: {
4011
+ server_url: { type: "string", description: "Base URL for the server (required for absolute URLs in GPT Actions)" }
4012
+ }
4013
+ },
4014
+ response: { 200: { type: "object", additionalProperties: true } }
4015
+ }
4016
+ }, async (request, reply) => {
4017
+ const query = request.query;
4018
+ const serverUrl = query.server_url?.trim() || `${request.protocol}://${request.hostname}`;
4019
+ const openapiSpec = server.swagger();
4020
+ const gptActions = convertToGptActions(openapiSpec, serverUrl);
4021
+ return reply.send(gptActions);
4022
+ });
4023
+ const BatchRequestBodySchema = z5.object({
4024
+ requests: z5.array(
4025
+ z5.object({
4026
+ skill_id: z5.string().min(1),
4027
+ params: z5.record(z5.unknown()).default({}),
4028
+ max_credits: z5.number().positive()
4029
+ })
4030
+ ).min(1),
4031
+ strategy: z5.enum(["parallel", "sequential", "best_effort"]),
4032
+ total_budget: z5.number().positive()
4033
+ });
4034
+ api.post("/api/request/batch", {
4035
+ schema: {
4036
+ tags: ["cards"],
4037
+ summary: "Execute multiple capability requests in one batch call",
4038
+ body: { type: "object", additionalProperties: true },
4039
+ response: {
4040
+ 200: { type: "object", additionalProperties: true },
4041
+ 400: { type: "object", properties: { error: { type: "string" } } },
4042
+ 401: { type: "object", properties: { error: { type: "string" } } },
4043
+ 503: { type: "object", properties: { error: { type: "string" } } }
4044
+ }
4045
+ }
4046
+ }, async (request, reply) => {
4047
+ if (!opts.creditDb) {
4048
+ return reply.code(503).send({ error: "Credit database not configured" });
4049
+ }
4050
+ const auth = request.headers.authorization;
4051
+ const owner = auth?.startsWith("Bearer ") ? auth.slice(7).trim() : null;
4052
+ if (!owner) {
4053
+ return reply.code(401).send({ error: "Authorization header with Bearer <owner> is required" });
4054
+ }
4055
+ const parseResult = BatchRequestBodySchema.safeParse(request.body);
4056
+ if (!parseResult.success) {
4057
+ return reply.code(400).send({
4058
+ error: "Request body validation failed",
4059
+ issues: parseResult.error.issues
4060
+ });
4061
+ }
4062
+ const { requests, strategy, total_budget } = parseResult.data;
4063
+ const batchResult = await executeCapabilityBatch({
4064
+ requests,
4065
+ strategy,
4066
+ total_budget,
4067
+ registryDb: db,
4068
+ creditDb: opts.creditDb,
4069
+ owner
4070
+ });
4071
+ return reply.send(batchResult);
4072
+ });
4073
+ if (opts.ownerApiKey && opts.ownerName) {
4074
+ const ownerApiKey = opts.ownerApiKey;
4075
+ const ownerName = opts.ownerName;
4076
+ void api.register(async (ownerRoutes) => {
4077
+ ownerRoutes.addHook("onRequest", async (request, reply) => {
4078
+ const auth = request.headers.authorization;
4079
+ const token = auth?.startsWith("Bearer ") ? auth.slice(7).trim() : null;
4080
+ if (!token || token !== ownerApiKey) {
4081
+ return reply.status(401).send({ error: "Unauthorized" });
4082
+ }
4083
+ });
4084
+ ownerRoutes.get("/me", {
4085
+ schema: {
4086
+ tags: ["owner"],
4087
+ summary: "Get owner identity and credit balance",
4088
+ security: [{ bearerAuth: [] }],
4089
+ response: {
4090
+ 200: { type: "object", properties: { owner: { type: "string" }, balance: { type: "number" } } }
4091
+ }
4092
+ }
4093
+ }, async (_request, reply) => {
4094
+ let balance = 0;
4095
+ if (opts.creditDb) {
4096
+ const ledger = createLedger({ db: opts.creditDb });
4097
+ balance = await ledger.getBalance(ownerName);
4098
+ }
4099
+ return reply.send({ owner: ownerName, balance });
4100
+ });
4101
+ ownerRoutes.get("/requests", {
4102
+ schema: {
4103
+ tags: ["owner"],
4104
+ summary: "Paginated request log entries",
4105
+ security: [{ bearerAuth: [] }],
4106
+ querystring: {
4107
+ type: "object",
4108
+ properties: {
4109
+ limit: { type: "integer", description: "Max entries (default 10, max 100)" },
4110
+ since: { type: "string", enum: ["24h", "7d", "30d"], description: "Time window" }
4111
+ }
4112
+ },
4113
+ response: { 200: { type: "object", properties: { items: { type: "array" }, limit: { type: "integer" } } } }
4114
+ }
4115
+ }, async (request, reply) => {
4116
+ const query = request.query;
4117
+ const rawLimit = query.limit !== void 0 ? parseInt(query.limit, 10) : 10;
4118
+ const limit = Math.min(isNaN(rawLimit) || rawLimit < 1 ? 10 : rawLimit, 100);
4119
+ const sinceRaw = query.since;
4120
+ const validSince = ["24h", "7d", "30d"];
4121
+ const since = sinceRaw && validSince.includes(sinceRaw) ? sinceRaw : void 0;
4122
+ const items = getRequestLog(db, limit, since);
4123
+ return reply.send({ items, limit });
4124
+ });
4125
+ ownerRoutes.get("/draft", {
4126
+ schema: {
4127
+ tags: ["owner"],
4128
+ summary: "Draft capability cards from auto-detected API keys",
4129
+ security: [{ bearerAuth: [] }],
4130
+ response: { 200: { type: "object", properties: { cards: { type: "array" } } } }
4131
+ }
4132
+ }, async (_request, reply) => {
4133
+ const detectedKeys = detectApiKeys(KNOWN_API_KEYS);
4134
+ const cards = detectedKeys.map((key) => buildDraftCard(key, ownerName)).filter((card) => card !== null);
4135
+ return reply.send({ cards });
4136
+ });
4137
+ ownerRoutes.post("/cards/:id/toggle-online", {
4138
+ schema: {
4139
+ tags: ["owner"],
4140
+ summary: "Toggle card online/offline status",
4141
+ security: [{ bearerAuth: [] }],
4142
+ params: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
4143
+ response: {
4144
+ 200: { type: "object", properties: { ok: { type: "boolean" }, online: { type: "boolean" } } },
4145
+ 403: { type: "object", properties: { error: { type: "string" } } },
4146
+ 404: { type: "object", properties: { error: { type: "string" } } }
4147
+ }
4148
+ }
4149
+ }, async (request, reply) => {
4150
+ const { id } = request.params;
4151
+ const card = getCard(db, id);
4152
+ if (!card) {
4153
+ return reply.code(404).send({ error: "Not found" });
4154
+ }
4155
+ try {
4156
+ const newOnline = !card.availability.online;
4157
+ updateCard(db, id, ownerName, {
4158
+ availability: { ...card.availability, online: newOnline }
4159
+ });
4160
+ return reply.send({ ok: true, online: newOnline });
4161
+ } catch (err) {
4162
+ if (err instanceof AgentBnBError && err.code === "FORBIDDEN") {
4163
+ return reply.code(403).send({ error: "Forbidden" });
4164
+ }
4165
+ throw err;
4166
+ }
4167
+ });
4168
+ ownerRoutes.patch("/cards/:id", {
4169
+ schema: {
4170
+ tags: ["owner"],
4171
+ summary: "Update card description or pricing",
4172
+ security: [{ bearerAuth: [] }],
4173
+ params: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
4174
+ body: {
4175
+ type: "object",
4176
+ properties: {
4177
+ description: { type: "string" },
4178
+ pricing: { type: "object", additionalProperties: true }
4179
+ },
4180
+ additionalProperties: true
4181
+ },
4182
+ response: {
4183
+ 200: { type: "object", properties: { ok: { type: "boolean" } } },
4184
+ 403: { type: "object", properties: { error: { type: "string" } } },
4185
+ 404: { type: "object", properties: { error: { type: "string" } } }
4186
+ }
4187
+ }
4188
+ }, async (request, reply) => {
4189
+ const { id } = request.params;
4190
+ const body = request.body;
4191
+ const updates = {};
4192
+ if (body.description !== void 0) updates.description = body.description;
4193
+ if (body.pricing !== void 0) updates.pricing = body.pricing;
4194
+ try {
4195
+ updateCard(db, id, ownerName, updates);
4196
+ return reply.send({ ok: true });
4197
+ } catch (err) {
4198
+ if (err instanceof AgentBnBError) {
4199
+ if (err.code === "FORBIDDEN") {
4200
+ return reply.code(403).send({ error: "Forbidden" });
4201
+ }
4202
+ if (err.code === "NOT_FOUND") {
4203
+ return reply.code(404).send({ error: "Not found" });
4204
+ }
4205
+ }
4206
+ throw err;
4207
+ }
4208
+ });
4209
+ ownerRoutes.get("/me/pending-requests", {
4210
+ schema: {
4211
+ tags: ["owner"],
4212
+ summary: "List pending Tier 3 approval queue entries",
4213
+ security: [{ bearerAuth: [] }],
4214
+ response: { 200: { type: "array" } }
4215
+ }
4216
+ }, async (_request, reply) => {
4217
+ const rows = listPendingRequests(db);
4218
+ return reply.send(rows);
4219
+ });
4220
+ ownerRoutes.post("/me/pending-requests/:id/approve", {
4221
+ schema: {
4222
+ tags: ["owner"],
4223
+ summary: "Approve a pending Tier 3 request",
4224
+ security: [{ bearerAuth: [] }],
4225
+ params: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
4226
+ response: {
4227
+ 200: { type: "object", properties: { status: { type: "string" }, id: { type: "string" } } },
4228
+ 404: { type: "object", properties: { error: { type: "string" } } }
4229
+ }
4230
+ }
4231
+ }, async (request, reply) => {
4232
+ const { id } = request.params;
4233
+ try {
4234
+ resolvePendingRequest(db, id, "approved");
4235
+ return reply.send({ status: "approved", id });
4236
+ } catch (err) {
4237
+ if (err instanceof AgentBnBError) return reply.status(404).send({ error: err.message });
4238
+ throw err;
4239
+ }
4240
+ });
4241
+ ownerRoutes.post("/me/pending-requests/:id/reject", {
4242
+ schema: {
4243
+ tags: ["owner"],
4244
+ summary: "Reject a pending Tier 3 request",
4245
+ security: [{ bearerAuth: [] }],
4246
+ params: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
4247
+ response: {
4248
+ 200: { type: "object", properties: { status: { type: "string" }, id: { type: "string" } } },
4249
+ 404: { type: "object", properties: { error: { type: "string" } } }
4250
+ }
4251
+ }
4252
+ }, async (request, reply) => {
4253
+ const { id } = request.params;
4254
+ try {
4255
+ resolvePendingRequest(db, id, "rejected");
4256
+ return reply.send({ status: "rejected", id });
4257
+ } catch (err) {
4258
+ if (err instanceof AgentBnBError) return reply.status(404).send({ error: err.message });
4259
+ throw err;
4260
+ }
4261
+ });
4262
+ ownerRoutes.get("/me/transactions", {
4263
+ schema: {
4264
+ tags: ["owner"],
4265
+ summary: "Paginated credit transaction history",
4266
+ security: [{ bearerAuth: [] }],
4267
+ querystring: {
4268
+ type: "object",
4269
+ properties: { limit: { type: "integer", description: "Max entries (default 20, max 100)" } }
4270
+ },
4271
+ response: {
4272
+ 200: { type: "object", properties: { items: { type: "array" }, limit: { type: "integer" } } }
4273
+ }
4274
+ }
4275
+ }, async (request, reply) => {
4276
+ const query = request.query;
4277
+ const rawLimit = query.limit !== void 0 ? parseInt(query.limit, 10) : 20;
4278
+ const limit = Math.min(isNaN(rawLimit) || rawLimit < 1 ? 20 : rawLimit, 100);
4279
+ if (!opts.creditDb) {
4280
+ return reply.send({ items: [], limit });
4281
+ }
4282
+ const ledger = createLedger({ db: opts.creditDb });
4283
+ const items = await ledger.getHistory(ownerName, limit);
4284
+ return reply.send({ items, limit });
4285
+ });
4286
+ });
4287
+ }
4288
+ });
4289
+ return { server, relayState };
4290
+ }
4291
+
4292
+ // src/autonomy/idle-monitor.ts
4293
+ import { Cron } from "croner";
4294
+ var IdleMonitor = class {
4295
+ job;
4296
+ owner;
4297
+ db;
4298
+ idleThreshold;
4299
+ autonomyConfig;
4300
+ /**
4301
+ * Creates a new IdleMonitor instance. The Cron job is constructed paused.
4302
+ * Call `start()` to activate polling.
4303
+ *
4304
+ * @param opts - IdleMonitor configuration options.
4305
+ */
4306
+ constructor(opts) {
4307
+ this.owner = opts.owner;
4308
+ this.db = opts.db;
4309
+ this.idleThreshold = opts.idleThreshold ?? 0.7;
4310
+ this.autonomyConfig = opts.autonomyConfig ?? DEFAULT_AUTONOMY_CONFIG;
4311
+ this.job = new Cron("0 * * * * *", { paused: true }, () => {
4312
+ void this.poll();
4313
+ });
4314
+ }
4315
+ /**
4316
+ * Starts the Cron polling loop by resuming the paused job.
4317
+ *
4318
+ * @returns The Cron job instance, for registration with AgentRuntime.registerJob().
4319
+ */
4320
+ start() {
4321
+ this.job.resume();
4322
+ return this.job;
4323
+ }
4324
+ /**
4325
+ * Returns the underlying Cron job instance.
4326
+ * Used for testing or for registering with AgentRuntime.registerJob() without starting.
4327
+ *
4328
+ * @returns The Cron job instance.
4329
+ */
4330
+ getJob() {
4331
+ return this.job;
4332
+ }
4333
+ /**
4334
+ * Polls the registry for all v2.0 cards owned by this agent, computes per-skill
4335
+ * idle rates from the past hour of request_log data, and triggers auto-share if eligible.
4336
+ *
4337
+ * Called automatically by the Cron job every 60 seconds.
4338
+ * Can be called directly in tests without needing to wait for the timer.
4339
+ */
4340
+ async poll() {
4341
+ const cards = listCards(this.db, this.owner);
4342
+ for (const card of cards) {
4343
+ const maybeV2 = card;
4344
+ if (!Array.isArray(maybeV2.skills)) {
4345
+ continue;
4346
+ }
4347
+ for (const skill of maybeV2.skills) {
4348
+ const capacity = skill.metadata?.capacity?.calls_per_hour ?? 60;
4349
+ const count = getSkillRequestCount(this.db, skill.id, 60 * 60 * 1e3);
4350
+ const idleRate = Math.max(0, 1 - count / capacity);
4351
+ updateSkillIdleRate(this.db, card.id, skill.id, idleRate);
4352
+ const isOnline = skill.availability?.online ?? false;
4353
+ if (idleRate >= this.idleThreshold && !isOnline) {
4354
+ const tier = getAutonomyTier(0, this.autonomyConfig);
4355
+ if (tier === 1) {
4356
+ updateSkillAvailability(this.db, card.id, skill.id, true);
4357
+ insertAuditEvent(this.db, {
4358
+ type: "auto_share",
4359
+ skill_id: skill.id,
4360
+ tier_invoked: 1,
4361
+ idle_rate: idleRate
4362
+ });
4363
+ } else if (tier === 2) {
4364
+ updateSkillAvailability(this.db, card.id, skill.id, true);
4365
+ insertAuditEvent(this.db, {
4366
+ type: "auto_share_notify",
4367
+ skill_id: skill.id,
4368
+ tier_invoked: 2,
4369
+ idle_rate: idleRate
4370
+ });
4371
+ } else {
4372
+ insertAuditEvent(this.db, {
4373
+ type: "auto_share_pending",
4374
+ skill_id: skill.id,
4375
+ tier_invoked: 3,
4376
+ idle_rate: idleRate
4377
+ });
4378
+ }
4379
+ }
4380
+ }
4381
+ }
4382
+ }
4383
+ };
4384
+
4385
+ // src/runtime/service-coordinator.ts
4386
+ import { spawn } from "child_process";
4387
+ import { createRequire } from "module";
4388
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
4389
+ import { fileURLToPath as fileURLToPath2 } from "url";
4390
+ import { dirname as dirname2, join as join2, resolve } from "path";
4391
+ import { homedir } from "os";
4392
+ import { randomUUID as randomUUID5 } from "crypto";
4393
+ var ServiceCoordinator = class {
4394
+ config;
4395
+ guard;
4396
+ runtime = null;
4397
+ gateway = null;
4398
+ registryFastify = null;
4399
+ relayClient = null;
4400
+ announceEnabled = false;
4401
+ inProcessStartup = false;
4402
+ shutdownPromise = null;
4403
+ signalHandlersRegistered = false;
4404
+ constructor(config, guard) {
4405
+ this.config = config;
4406
+ this.guard = guard;
4407
+ }
4408
+ async ensureRunning(opts) {
4409
+ const running = this.guard.getRunningMeta();
4410
+ if (running) {
4411
+ const health = await this.healthCheckForPort(running.port, running.owner);
4412
+ if (health.ok && health.agentbnb) {
4413
+ return "already_running";
4414
+ }
4415
+ throw new AgentBnBError(
4416
+ `AgentBnB lock exists but health check failed (pid=${running.pid}, port=${running.port})`,
4417
+ "SERVICE_UNHEALTHY"
4418
+ );
4419
+ }
4420
+ if (opts?.foreground) {
4421
+ return this.startInProcess(opts);
4422
+ }
4423
+ let child = null;
4424
+ try {
4425
+ child = this.spawnManagedProcess(opts);
4426
+ const expectedPort = opts?.port ?? this.config.gateway_port;
4427
+ await this.waitUntilHealthy(expectedPort, 1e4);
4428
+ return "started";
4429
+ } catch (err) {
4430
+ await this.rollbackManagedStartup(child);
4431
+ throw err;
4432
+ }
4433
+ }
4434
+ async getStatus() {
4435
+ const meta = this.guard.getRunningMeta();
4436
+ if (!meta) {
4437
+ return {
4438
+ state: "stopped",
4439
+ pid: null,
4440
+ port: null,
4441
+ owner: null,
4442
+ relayConnected: false,
4443
+ uptime_ms: null
4444
+ };
4445
+ }
4446
+ const health = await this.healthCheckForPort(meta.port, meta.owner);
4447
+ const startedAtMs = Date.parse(meta.started_at);
4448
+ const uptimeMs = Number.isFinite(startedAtMs) ? Date.now() - startedAtMs : null;
4449
+ return {
4450
+ state: health.ok ? "running" : "unknown",
4451
+ pid: meta.pid,
4452
+ port: meta.port,
4453
+ owner: meta.owner,
4454
+ relayConnected: this.relayClient !== null,
4455
+ uptime_ms: uptimeMs
4456
+ };
4457
+ }
4458
+ async stop() {
4459
+ if (this.inProcessStartup) {
4460
+ await this.shutdownInProcess();
4461
+ return;
4462
+ }
4463
+ const meta = this.guard.getRunningMeta();
4464
+ if (!meta) return;
4465
+ try {
4466
+ process.kill(meta.pid, "SIGTERM");
4467
+ } catch (err) {
4468
+ const killErr = err;
4469
+ if (killErr.code !== "ESRCH") {
4470
+ throw err;
4471
+ }
4472
+ }
4473
+ await this.waitForPidExit(meta.pid, 1e4);
4474
+ this.guard.release();
4475
+ }
4476
+ async restart(opts) {
4477
+ await this.stop();
4478
+ await this.ensureRunning(opts);
4479
+ }
4480
+ async healthCheck() {
4481
+ const meta = this.guard.getRunningMeta();
4482
+ const port = meta?.port ?? this.config.gateway_port;
4483
+ return this.healthCheckForPort(port, meta?.owner);
4484
+ }
4485
+ async startInProcess(opts) {
4486
+ const resolved = this.resolveOptions(opts);
4487
+ const lockMeta = {
4488
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
4489
+ port: resolved.port,
4490
+ owner: this.config.owner
4491
+ };
4492
+ this.guard.acquire(lockMeta);
4493
+ try {
4494
+ await this.startServiceStack(resolved);
4495
+ this.inProcessStartup = true;
4496
+ this.registerSignalHandlers();
4497
+ return "started";
4498
+ } catch (err) {
4499
+ await this.shutdownInProcess();
4500
+ this.guard.release();
4501
+ throw err;
4502
+ }
4503
+ }
4504
+ resolveOptions(opts) {
4505
+ return {
4506
+ port: opts?.port ?? this.config.gateway_port,
4507
+ handlerUrl: opts?.handlerUrl ?? "http://localhost:8080",
4508
+ skillsYamlPath: opts?.skillsYamlPath ?? join2(homedir(), ".agentbnb", "skills.yaml"),
4509
+ registryPort: opts?.registryPort ?? 7701,
4510
+ registryUrl: opts?.registryUrl ?? this.config.registry ?? "",
4511
+ relay: opts?.relay ?? true,
4512
+ conductorEnabled: opts?.conductorEnabled ?? false,
4513
+ announce: opts?.announce ?? false
4514
+ };
4515
+ }
4516
+ async startServiceStack(opts) {
4517
+ this.runtime = new AgentRuntime({
4518
+ registryDbPath: this.config.db_path,
4519
+ creditDbPath: this.config.credit_db_path,
4520
+ owner: this.config.owner,
4521
+ skillsYamlPath: opts.skillsYamlPath,
4522
+ conductorEnabled: opts.conductorEnabled,
4523
+ conductorToken: this.config.token
4524
+ });
4525
+ await this.runtime.start();
4526
+ if (this.runtime.skillExecutor) {
4527
+ console.log(`SkillExecutor initialized from ${opts.skillsYamlPath}`);
4528
+ }
4529
+ if (opts.conductorEnabled) {
4530
+ console.log("Conductor mode enabled \u2014 orchestrate/plan skills available via gateway");
4531
+ }
4532
+ if (opts.conductorEnabled && this.config.conductor?.public) {
4533
+ const { buildConductorCard } = await import("./card-RSGDCHCV.js");
4534
+ const conductorCard = buildConductorCard(this.config.owner);
4535
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4536
+ const existing = this.runtime.registryDb.prepare("SELECT id FROM capability_cards WHERE id = ?").get(conductorCard.id);
4537
+ if (existing) {
4538
+ this.runtime.registryDb.prepare("UPDATE capability_cards SET data = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(conductorCard), now, conductorCard.id);
4539
+ } else {
4540
+ this.runtime.registryDb.prepare(
4541
+ "INSERT INTO capability_cards (id, owner, data, created_at, updated_at) VALUES (?, ?, ?, ?, ?)"
4542
+ ).run(
4543
+ conductorCard.id,
4544
+ this.config.owner,
4545
+ JSON.stringify(conductorCard),
4546
+ now,
4547
+ now
4548
+ );
4549
+ }
4550
+ console.log("Conductor card registered locally (conductor.public: true)");
4551
+ }
4552
+ const autonomyConfig = this.config.autonomy ?? DEFAULT_AUTONOMY_CONFIG;
4553
+ const idleMonitor = new IdleMonitor({
4554
+ owner: this.config.owner,
4555
+ db: this.runtime.registryDb,
4556
+ autonomyConfig
4557
+ });
4558
+ const idleJob = idleMonitor.start();
4559
+ this.runtime.registerJob(idleJob);
4560
+ console.log("IdleMonitor started (60s poll interval, 70% idle threshold)");
4561
+ this.gateway = createGatewayServer({
4562
+ port: opts.port,
4563
+ registryDb: this.runtime.registryDb,
4564
+ creditDb: this.runtime.creditDb,
4565
+ tokens: [this.config.token],
4566
+ handlerUrl: opts.handlerUrl,
4567
+ skillExecutor: this.runtime.skillExecutor
4568
+ });
4569
+ await this.gateway.listen({ port: opts.port, host: "0.0.0.0" });
4570
+ console.log(`Gateway running on port ${opts.port}`);
4571
+ if (opts.registryPort > 0) {
4572
+ if (!this.config.api_key) {
4573
+ console.warn("No API key found. Run `agentbnb init` to enable dashboard features.");
4574
+ }
4575
+ const { server: regServer, relayState } = createRegistryServer({
4576
+ registryDb: this.runtime.registryDb,
4577
+ silent: false,
4578
+ ownerName: this.config.owner,
4579
+ ownerApiKey: this.config.api_key,
4580
+ creditDb: this.runtime.creditDb
4581
+ });
4582
+ this.registryFastify = regServer;
4583
+ await this.registryFastify.listen({ port: opts.registryPort, host: "0.0.0.0" });
4584
+ console.log(`Registry API: http://0.0.0.0:${opts.registryPort}/cards`);
4585
+ if (relayState) {
4586
+ console.log("WebSocket relay active on /ws");
4587
+ }
4588
+ }
4589
+ if (opts.registryUrl && opts.relay) {
4590
+ const { RelayClient } = await import("./websocket-client-6IIDGXKB.js");
4591
+ const { executeCapabilityRequest: executeCapabilityRequest2 } = await import("./execute-CPFSOOO3.js");
4592
+ const cards = listCards(this.runtime.registryDb, this.config.owner);
4593
+ const card = cards[0] ?? {
4594
+ id: randomUUID5(),
4595
+ owner: this.config.owner,
4596
+ name: this.config.owner,
4597
+ description: "Agent registered via CLI",
4598
+ spec_version: "1.0",
4599
+ level: 1,
4600
+ inputs: [],
4601
+ outputs: [],
4602
+ pricing: { credits_per_call: 1 },
4603
+ availability: { online: true }
4604
+ };
4605
+ const additionalCards = [];
4606
+ if (this.config.conductor?.public) {
4607
+ const { buildConductorCard } = await import("./card-RSGDCHCV.js");
4608
+ additionalCards.push(
4609
+ buildConductorCard(this.config.owner)
4610
+ );
4611
+ console.log("Conductor card will be published to registry (conductor.public: true)");
4612
+ }
4613
+ this.relayClient = new RelayClient({
4614
+ registryUrl: opts.registryUrl,
4615
+ owner: this.config.owner,
4616
+ token: this.config.token,
4617
+ card,
4618
+ cards: additionalCards.length > 0 ? additionalCards : void 0,
4619
+ onRequest: async (req) => {
4620
+ const onProgress = (info) => {
4621
+ this.relayClient.sendProgress(req.id, info);
4622
+ };
4623
+ const result = await executeCapabilityRequest2({
4624
+ registryDb: this.runtime.registryDb,
4625
+ creditDb: this.runtime.creditDb,
4626
+ cardId: req.card_id,
4627
+ skillId: req.skill_id,
4628
+ params: req.params,
4629
+ requester: req.requester ?? req.from_owner,
4630
+ escrowReceipt: req.escrow_receipt,
4631
+ skillExecutor: this.runtime.skillExecutor,
4632
+ handlerUrl: opts.handlerUrl,
4633
+ onProgress,
4634
+ relayAuthorized: true
4635
+ });
4636
+ if (result.success) {
4637
+ return { result: result.result };
4638
+ }
4639
+ return { error: { code: result.error.code, message: result.error.message } };
4640
+ }
4641
+ });
4642
+ try {
4643
+ await this.relayClient.connect();
4644
+ console.log(`Connected to registry: ${opts.registryUrl}`);
4645
+ } catch (err) {
4646
+ console.warn(
4647
+ `Warning: could not connect to registry ${opts.registryUrl}: ${err instanceof Error ? err.message : err}`
4648
+ );
4649
+ console.warn("Will auto-reconnect in background...");
4650
+ }
4651
+ }
4652
+ if (opts.announce) {
4653
+ announceGateway(this.config.owner, opts.port);
4654
+ this.announceEnabled = true;
4655
+ console.log("Announcing on local network via mDNS");
4656
+ }
4657
+ }
4658
+ async shutdownInProcess() {
4659
+ if (this.shutdownPromise) {
4660
+ await this.shutdownPromise;
4661
+ return;
4662
+ }
4663
+ this.shutdownPromise = (async () => {
4664
+ if (this.relayClient) {
4665
+ this.relayClient.disconnect();
4666
+ this.relayClient = null;
4667
+ }
4668
+ if (this.announceEnabled) {
4669
+ await stopAnnouncement();
4670
+ this.announceEnabled = false;
4671
+ }
4672
+ if (this.registryFastify) {
4673
+ await this.registryFastify.close().catch(() => void 0);
4674
+ this.registryFastify = null;
4675
+ }
4676
+ if (this.gateway) {
4677
+ await this.gateway.close().catch(() => void 0);
4678
+ this.gateway = null;
4679
+ }
4680
+ if (this.runtime) {
4681
+ await this.runtime.shutdown().catch(() => void 0);
4682
+ this.runtime = null;
4683
+ }
4684
+ this.unregisterSignalHandlers();
4685
+ this.inProcessStartup = false;
4686
+ this.guard.release();
4687
+ })();
4688
+ try {
4689
+ await this.shutdownPromise;
4690
+ } finally {
4691
+ this.shutdownPromise = null;
4692
+ }
4693
+ }
4694
+ spawnManagedProcess(opts) {
4695
+ const runtime = loadPersistedRuntime(getConfigDir());
4696
+ const nodeExec = resolveNodeExecutable(runtime);
4697
+ const cliArgs = resolveCliLaunchArgs(this.buildServeArgs(opts));
4698
+ const child = spawn(nodeExec, cliArgs, {
4699
+ detached: true,
4700
+ stdio: "ignore",
4701
+ env: { ...process.env }
4702
+ });
4703
+ child.unref();
4704
+ return child;
4705
+ }
4706
+ buildServeArgs(opts) {
4707
+ const args = [];
4708
+ if (opts?.port !== void 0) args.push("--port", String(opts.port));
4709
+ if (opts?.handlerUrl) args.push("--handler-url", opts.handlerUrl);
4710
+ if (opts?.skillsYamlPath) args.push("--skills-yaml", opts.skillsYamlPath);
4711
+ if (opts?.registryPort !== void 0) {
4712
+ args.push("--registry-port", String(opts.registryPort));
4713
+ }
4714
+ if (opts?.registryUrl) args.push("--registry", opts.registryUrl);
4715
+ if (opts?.conductorEnabled) args.push("--conductor");
4716
+ if (opts?.announce) args.push("--announce");
4717
+ if (opts?.relay === false) args.push("--no-relay");
4718
+ return args;
4719
+ }
4720
+ async rollbackManagedStartup(child) {
4721
+ if (child?.pid) {
4722
+ try {
4723
+ process.kill(child.pid, "SIGTERM");
4724
+ } catch (err) {
4725
+ const killErr = err;
4726
+ if (killErr.code !== "ESRCH") {
4727
+ throw err;
4728
+ }
4729
+ }
4730
+ await this.waitForPidExit(child.pid, 3e3).catch(() => void 0);
4731
+ }
4732
+ this.guard.release();
4733
+ }
4734
+ async waitUntilHealthy(port, timeoutMs) {
4735
+ const start = Date.now();
4736
+ while (Date.now() - start < timeoutMs) {
4737
+ const health = await this.healthCheckForPort(port, this.config.owner);
4738
+ if (health.ok && health.agentbnb) {
4739
+ return;
4740
+ }
4741
+ await sleep2(250);
4742
+ }
4743
+ throw new AgentBnBError(
4744
+ `Timed out waiting for AgentBnB service on port ${port}`,
4745
+ "SERVICE_START_TIMEOUT"
4746
+ );
4747
+ }
4748
+ async healthCheckForPort(port, owner) {
4749
+ const startedAt = Date.now();
4750
+ const baseUrl = this.getGatewayBaseUrl(port);
4751
+ try {
4752
+ const healthResponse = await this.fetchWithTimeout(`${baseUrl}/health`, {
4753
+ method: "GET"
4754
+ });
4755
+ if (!healthResponse.ok) {
4756
+ return {
4757
+ ok: false,
4758
+ agentbnb: false,
4759
+ latency_ms: Date.now() - startedAt,
4760
+ owner
4761
+ };
4762
+ }
4763
+ const body = await healthResponse.json();
4764
+ if (body.status !== "ok") {
4765
+ return {
4766
+ ok: false,
4767
+ agentbnb: false,
4768
+ latency_ms: Date.now() - startedAt,
4769
+ version: body.version,
4770
+ owner
4771
+ };
4772
+ }
4773
+ const probeId = "agentbnb-health-signature";
4774
+ const probeResponse = await this.fetchWithTimeout(`${baseUrl}/rpc`, {
4775
+ method: "POST",
4776
+ headers: {
4777
+ "Content-Type": "application/json",
4778
+ Authorization: `Bearer ${this.config.token}`
4779
+ },
4780
+ body: JSON.stringify({
4781
+ jsonrpc: "2.0",
4782
+ id: probeId,
4783
+ method: "agentbnb.signature.probe",
4784
+ params: {}
4785
+ })
4786
+ });
4787
+ let agentbnb = false;
4788
+ if (probeResponse.ok) {
4789
+ const probe = await probeResponse.json();
4790
+ agentbnb = probe.jsonrpc === "2.0" && probe.id === probeId && probe.error?.code === -32601;
4791
+ }
4792
+ return {
4793
+ ok: body.status === "ok" && agentbnb,
4794
+ agentbnb,
4795
+ latency_ms: Date.now() - startedAt,
4796
+ version: body.version,
4797
+ owner
4798
+ };
4799
+ } catch {
4800
+ return {
4801
+ ok: false,
4802
+ agentbnb: false,
4803
+ latency_ms: Date.now() - startedAt,
4804
+ owner
4805
+ };
4806
+ }
4807
+ }
4808
+ async fetchWithTimeout(url, init, timeoutMs = 3e3) {
4809
+ const controller = new AbortController();
4810
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
4811
+ try {
4812
+ return await fetch(url, { ...init, signal: controller.signal });
4813
+ } finally {
4814
+ clearTimeout(timer);
4815
+ }
4816
+ }
4817
+ getGatewayBaseUrl(port) {
4818
+ try {
4819
+ const configured = new URL(this.config.gateway_url);
4820
+ return `${configured.protocol}//${configured.hostname}:${port}`;
4821
+ } catch {
4822
+ return `http://127.0.0.1:${port}`;
4823
+ }
4824
+ }
4825
+ async waitForPidExit(pid, timeoutMs) {
4826
+ const started = Date.now();
4827
+ while (Date.now() - started < timeoutMs) {
4828
+ if (!isPidAlive(pid)) return;
4829
+ await sleep2(100);
4830
+ }
4831
+ if (isPidAlive(pid)) {
4832
+ throw new AgentBnBError(
4833
+ `Timed out waiting for process ${pid} to exit`,
4834
+ "PROCESS_STOP_TIMEOUT"
4835
+ );
4836
+ }
4837
+ }
4838
+ registerSignalHandlers() {
4839
+ if (this.signalHandlersRegistered) return;
4840
+ process.once("SIGTERM", this.handleSignal);
4841
+ process.once("SIGINT", this.handleSignal);
4842
+ this.signalHandlersRegistered = true;
4843
+ }
4844
+ unregisterSignalHandlers() {
4845
+ if (!this.signalHandlersRegistered) return;
4846
+ process.removeListener("SIGTERM", this.handleSignal);
4847
+ process.removeListener("SIGINT", this.handleSignal);
4848
+ this.signalHandlersRegistered = false;
4849
+ }
4850
+ handleSignal = () => {
4851
+ void (async () => {
4852
+ await this.shutdownInProcess();
4853
+ process.exit(0);
4854
+ })();
4855
+ };
4856
+ };
4857
+ var require2 = createRequire(import.meta.url);
4858
+ function loadPersistedRuntime(configDir) {
4859
+ const runtimePath = join2(configDir, "runtime.json");
4860
+ if (!existsSync3(runtimePath)) return null;
4861
+ try {
4862
+ const raw = readFileSync2(runtimePath, "utf8");
4863
+ const parsed = JSON.parse(raw);
4864
+ const nodeExec = parsed["node_exec"];
4865
+ if (typeof nodeExec !== "string" || nodeExec.trim().length === 0) {
4866
+ return null;
4867
+ }
4868
+ return {
4869
+ node_exec: nodeExec,
4870
+ node_version: typeof parsed["node_version"] === "string" ? parsed["node_version"] : void 0,
4871
+ source: typeof parsed["source"] === "string" ? parsed["source"] : void 0,
4872
+ detected_at: typeof parsed["detected_at"] === "string" ? parsed["detected_at"] : void 0
4873
+ };
4874
+ } catch {
4875
+ return null;
4876
+ }
4877
+ }
4878
+ function resolveNodeExecutable(runtime) {
4879
+ if (runtime?.node_exec) {
4880
+ return runtime.node_exec;
4881
+ }
4882
+ return process.execPath;
4883
+ }
4884
+ function resolveCliLaunchArgs(serveArgs) {
4885
+ const projectRoot = resolve(dirname2(fileURLToPath2(import.meta.url)), "..", "..");
4886
+ const distCli = join2(projectRoot, "dist", "cli", "index.js");
4887
+ if (existsSync3(distCli)) {
4888
+ return [distCli, "serve", ...serveArgs];
4889
+ }
4890
+ const srcCli = join2(projectRoot, "src", "cli", "index.ts");
4891
+ if (existsSync3(srcCli)) {
4892
+ const tsxCli = require2.resolve("tsx/dist/cli.mjs");
4893
+ return [tsxCli, srcCli, "serve", ...serveArgs];
4894
+ }
4895
+ throw new AgentBnBError(
4896
+ "Unable to locate AgentBnB CLI entry (dist/cli/index.js or src/cli/index.ts)",
4897
+ "CLI_ENTRY_NOT_FOUND"
4898
+ );
4899
+ }
4900
+ function isPidAlive(pid) {
4901
+ if (!Number.isInteger(pid) || pid <= 0) return false;
4902
+ try {
4903
+ process.kill(pid, 0);
4904
+ return true;
4905
+ } catch (err) {
4906
+ const killErr = err;
4907
+ return killErr.code === "EPERM";
4908
+ }
4909
+ }
4910
+ function sleep2(ms) {
4911
+ return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
4912
+ }
4913
+ export {
4914
+ ServiceCoordinator,
4915
+ loadPersistedRuntime,
4916
+ resolveNodeExecutable
4917
+ };