agentbnb 4.0.4 → 5.1.1

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