agentbnb 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +4048 -0
- package/dist/index.d.ts +676 -0
- package/dist/index.js +894 -0
- package/package.json +75 -0
|
@@ -0,0 +1,4048 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
6
|
+
import { createRequire } from "module";
|
|
7
|
+
import { randomBytes } from "crypto";
|
|
8
|
+
import { join as join5 } from "path";
|
|
9
|
+
import { networkInterfaces, homedir as homedir2 } from "os";
|
|
10
|
+
import { createInterface } from "readline";
|
|
11
|
+
|
|
12
|
+
// src/cli/config.ts
|
|
13
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
function getConfigDir() {
|
|
17
|
+
return process.env["AGENTBNB_DIR"] ?? join(homedir(), ".agentbnb");
|
|
18
|
+
}
|
|
19
|
+
function getConfigPath() {
|
|
20
|
+
return join(getConfigDir(), "config.json");
|
|
21
|
+
}
|
|
22
|
+
function loadConfig() {
|
|
23
|
+
const configPath = getConfigPath();
|
|
24
|
+
if (!existsSync(configPath)) return null;
|
|
25
|
+
try {
|
|
26
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
27
|
+
return JSON.parse(raw);
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function saveConfig(config) {
|
|
33
|
+
const dir = getConfigDir();
|
|
34
|
+
if (!existsSync(dir)) {
|
|
35
|
+
mkdirSync(dir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
writeFileSync(getConfigPath(), JSON.stringify(config, null, 2), "utf-8");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/credit/signing.ts
|
|
41
|
+
import { generateKeyPairSync, sign, verify, createPublicKey, createPrivateKey } from "crypto";
|
|
42
|
+
import { writeFileSync as writeFileSync2, readFileSync as readFileSync2, existsSync as existsSync2, chmodSync } from "fs";
|
|
43
|
+
import { join as join2 } from "path";
|
|
44
|
+
|
|
45
|
+
// src/types/index.ts
|
|
46
|
+
import { z } from "zod";
|
|
47
|
+
var IOSchemaSchema = z.object({
|
|
48
|
+
name: z.string(),
|
|
49
|
+
type: z.enum(["text", "json", "file", "audio", "image", "video", "stream"]),
|
|
50
|
+
description: z.string().optional(),
|
|
51
|
+
required: z.boolean().default(true),
|
|
52
|
+
schema: z.record(z.unknown()).optional()
|
|
53
|
+
// JSON Schema
|
|
54
|
+
});
|
|
55
|
+
var PoweredBySchema = z.object({
|
|
56
|
+
provider: z.string().min(1),
|
|
57
|
+
model: z.string().optional(),
|
|
58
|
+
tier: z.string().optional()
|
|
59
|
+
});
|
|
60
|
+
var CapabilityCardSchema = z.object({
|
|
61
|
+
spec_version: z.literal("1.0").default("1.0"),
|
|
62
|
+
id: z.string().uuid(),
|
|
63
|
+
owner: z.string().min(1),
|
|
64
|
+
name: z.string().min(1).max(100),
|
|
65
|
+
description: z.string().max(500),
|
|
66
|
+
level: z.union([z.literal(1), z.literal(2), z.literal(3)]),
|
|
67
|
+
inputs: z.array(IOSchemaSchema),
|
|
68
|
+
outputs: z.array(IOSchemaSchema),
|
|
69
|
+
pricing: z.object({
|
|
70
|
+
credits_per_call: z.number().nonnegative(),
|
|
71
|
+
credits_per_minute: z.number().nonnegative().optional(),
|
|
72
|
+
/** Number of free monthly calls. Shown as a "N free/mo" badge in the Hub. */
|
|
73
|
+
free_tier: z.number().nonnegative().optional()
|
|
74
|
+
}),
|
|
75
|
+
availability: z.object({
|
|
76
|
+
online: z.boolean(),
|
|
77
|
+
schedule: z.string().optional()
|
|
78
|
+
// cron expression
|
|
79
|
+
}),
|
|
80
|
+
powered_by: z.array(PoweredBySchema).optional(),
|
|
81
|
+
/**
|
|
82
|
+
* Private per-card metadata. Stripped from all API and CLI responses —
|
|
83
|
+
* never transmitted beyond the local store.
|
|
84
|
+
*/
|
|
85
|
+
_internal: z.record(z.unknown()).optional(),
|
|
86
|
+
metadata: z.object({
|
|
87
|
+
apis_used: z.array(z.string()).optional(),
|
|
88
|
+
avg_latency_ms: z.number().nonnegative().optional(),
|
|
89
|
+
success_rate: z.number().min(0).max(1).optional(),
|
|
90
|
+
tags: z.array(z.string()).optional()
|
|
91
|
+
}).optional(),
|
|
92
|
+
created_at: z.string().datetime().optional(),
|
|
93
|
+
updated_at: z.string().datetime().optional()
|
|
94
|
+
});
|
|
95
|
+
var SkillSchema = z.object({
|
|
96
|
+
/** Stable skill identifier, e.g. 'tts-elevenlabs'. Used for gateway routing and idle tracking. */
|
|
97
|
+
id: z.string().min(1),
|
|
98
|
+
name: z.string().min(1).max(100),
|
|
99
|
+
description: z.string().max(500),
|
|
100
|
+
level: z.union([z.literal(1), z.literal(2), z.literal(3)]),
|
|
101
|
+
/** Optional grouping category, e.g. 'tts' | 'video_gen' | 'code_review'. */
|
|
102
|
+
category: z.string().optional(),
|
|
103
|
+
inputs: z.array(IOSchemaSchema),
|
|
104
|
+
outputs: z.array(IOSchemaSchema),
|
|
105
|
+
pricing: z.object({
|
|
106
|
+
credits_per_call: z.number().nonnegative(),
|
|
107
|
+
credits_per_minute: z.number().nonnegative().optional(),
|
|
108
|
+
free_tier: z.number().nonnegative().optional()
|
|
109
|
+
}),
|
|
110
|
+
/** Per-skill online flag — overrides card-level availability for this skill. */
|
|
111
|
+
availability: z.object({ online: z.boolean() }).optional(),
|
|
112
|
+
powered_by: z.array(PoweredBySchema).optional(),
|
|
113
|
+
metadata: z.object({
|
|
114
|
+
apis_used: z.array(z.string()).optional(),
|
|
115
|
+
avg_latency_ms: z.number().nonnegative().optional(),
|
|
116
|
+
success_rate: z.number().min(0).max(1).optional(),
|
|
117
|
+
tags: z.array(z.string()).optional(),
|
|
118
|
+
capacity: z.object({
|
|
119
|
+
calls_per_hour: z.number().positive().default(60)
|
|
120
|
+
}).optional()
|
|
121
|
+
}).optional(),
|
|
122
|
+
/**
|
|
123
|
+
* Private per-skill metadata. Stripped from all API and CLI responses —
|
|
124
|
+
* never transmitted beyond the local store.
|
|
125
|
+
*/
|
|
126
|
+
_internal: z.record(z.unknown()).optional()
|
|
127
|
+
});
|
|
128
|
+
var CapabilityCardV2Schema = z.object({
|
|
129
|
+
spec_version: z.literal("2.0"),
|
|
130
|
+
id: z.string().uuid(),
|
|
131
|
+
owner: z.string().min(1),
|
|
132
|
+
/** Agent display name — was 'name' in v1.0. */
|
|
133
|
+
agent_name: z.string().min(1).max(100),
|
|
134
|
+
/** At least one skill is required. */
|
|
135
|
+
skills: z.array(SkillSchema).min(1),
|
|
136
|
+
availability: z.object({
|
|
137
|
+
online: z.boolean(),
|
|
138
|
+
schedule: z.string().optional()
|
|
139
|
+
}),
|
|
140
|
+
/** Optional deployment environment metadata. */
|
|
141
|
+
environment: z.object({
|
|
142
|
+
runtime: z.string(),
|
|
143
|
+
region: z.string().optional()
|
|
144
|
+
}).optional(),
|
|
145
|
+
/**
|
|
146
|
+
* Private per-card metadata. Stripped from all API and CLI responses —
|
|
147
|
+
* never transmitted beyond the local store.
|
|
148
|
+
*/
|
|
149
|
+
_internal: z.record(z.unknown()).optional(),
|
|
150
|
+
created_at: z.string().datetime().optional(),
|
|
151
|
+
updated_at: z.string().datetime().optional()
|
|
152
|
+
});
|
|
153
|
+
var AnyCardSchema = z.discriminatedUnion("spec_version", [
|
|
154
|
+
CapabilityCardSchema,
|
|
155
|
+
CapabilityCardV2Schema
|
|
156
|
+
]);
|
|
157
|
+
var AgentBnBError = class extends Error {
|
|
158
|
+
constructor(message, code) {
|
|
159
|
+
super(message);
|
|
160
|
+
this.code = code;
|
|
161
|
+
this.name = "AgentBnBError";
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// src/credit/signing.ts
|
|
166
|
+
function generateKeyPair() {
|
|
167
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
|
|
168
|
+
publicKeyEncoding: { type: "spki", format: "der" },
|
|
169
|
+
privateKeyEncoding: { type: "pkcs8", format: "der" }
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
publicKey: Buffer.from(publicKey),
|
|
173
|
+
privateKey: Buffer.from(privateKey)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function saveKeyPair(configDir, keys) {
|
|
177
|
+
const privatePath = join2(configDir, "private.key");
|
|
178
|
+
const publicPath = join2(configDir, "public.key");
|
|
179
|
+
writeFileSync2(privatePath, keys.privateKey);
|
|
180
|
+
chmodSync(privatePath, 384);
|
|
181
|
+
writeFileSync2(publicPath, keys.publicKey);
|
|
182
|
+
}
|
|
183
|
+
function loadKeyPair(configDir) {
|
|
184
|
+
const privatePath = join2(configDir, "private.key");
|
|
185
|
+
const publicPath = join2(configDir, "public.key");
|
|
186
|
+
if (!existsSync2(privatePath) || !existsSync2(publicPath)) {
|
|
187
|
+
throw new AgentBnBError("Keypair not found. Run `agentbnb init` to generate one.", "KEYPAIR_NOT_FOUND");
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
publicKey: readFileSync2(publicPath),
|
|
191
|
+
privateKey: readFileSync2(privatePath)
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/autonomy/tiers.ts
|
|
196
|
+
import { randomUUID } from "crypto";
|
|
197
|
+
var DEFAULT_AUTONOMY_CONFIG = {
|
|
198
|
+
tier1_max_credits: 0,
|
|
199
|
+
tier2_max_credits: 0
|
|
200
|
+
};
|
|
201
|
+
function getAutonomyTier(creditAmount, config) {
|
|
202
|
+
if (creditAmount < config.tier1_max_credits) return 1;
|
|
203
|
+
if (creditAmount < config.tier2_max_credits) return 2;
|
|
204
|
+
return 3;
|
|
205
|
+
}
|
|
206
|
+
function insertAuditEvent(db, event) {
|
|
207
|
+
const isShareEvent = event.type === "auto_share" || event.type === "auto_share_notify" || event.type === "auto_share_pending";
|
|
208
|
+
const cardId = isShareEvent ? "system" : event.card_id;
|
|
209
|
+
const creditsCharged = isShareEvent ? 0 : event.credits;
|
|
210
|
+
const stmt = db.prepare(`
|
|
211
|
+
INSERT INTO request_log (
|
|
212
|
+
id, card_id, card_name, requester, status, latency_ms, credits_charged,
|
|
213
|
+
created_at, skill_id, action_type, tier_invoked
|
|
214
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
215
|
+
`);
|
|
216
|
+
stmt.run(
|
|
217
|
+
randomUUID(),
|
|
218
|
+
cardId,
|
|
219
|
+
"autonomy-audit",
|
|
220
|
+
"self",
|
|
221
|
+
"success",
|
|
222
|
+
0,
|
|
223
|
+
creditsCharged,
|
|
224
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
225
|
+
event.skill_id,
|
|
226
|
+
event.type,
|
|
227
|
+
event.tier_invoked
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/autonomy/idle-monitor.ts
|
|
232
|
+
import { Cron } from "croner";
|
|
233
|
+
|
|
234
|
+
// src/registry/store.ts
|
|
235
|
+
import Database from "better-sqlite3";
|
|
236
|
+
|
|
237
|
+
// src/registry/request-log.ts
|
|
238
|
+
var SINCE_MS = {
|
|
239
|
+
"24h": 864e5,
|
|
240
|
+
"7d": 6048e5,
|
|
241
|
+
"30d": 2592e6
|
|
242
|
+
};
|
|
243
|
+
function createRequestLogTable(db) {
|
|
244
|
+
db.exec(`
|
|
245
|
+
CREATE TABLE IF NOT EXISTS request_log (
|
|
246
|
+
id TEXT PRIMARY KEY,
|
|
247
|
+
card_id TEXT NOT NULL,
|
|
248
|
+
card_name TEXT NOT NULL,
|
|
249
|
+
requester TEXT NOT NULL,
|
|
250
|
+
status TEXT NOT NULL CHECK(status IN ('success', 'failure', 'timeout')),
|
|
251
|
+
latency_ms INTEGER NOT NULL,
|
|
252
|
+
credits_charged INTEGER NOT NULL,
|
|
253
|
+
created_at TEXT NOT NULL
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
CREATE INDEX IF NOT EXISTS request_log_created_at_idx
|
|
257
|
+
ON request_log (created_at DESC);
|
|
258
|
+
`);
|
|
259
|
+
try {
|
|
260
|
+
db.exec("ALTER TABLE request_log ADD COLUMN skill_id TEXT");
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
db.exec("ALTER TABLE request_log ADD COLUMN action_type TEXT");
|
|
265
|
+
} catch {
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
db.exec("ALTER TABLE request_log ADD COLUMN tier_invoked INTEGER");
|
|
269
|
+
} catch {
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function insertRequestLog(db, entry) {
|
|
273
|
+
const stmt = db.prepare(`
|
|
274
|
+
INSERT INTO request_log (id, card_id, card_name, requester, status, latency_ms, credits_charged, created_at, skill_id, action_type, tier_invoked)
|
|
275
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
276
|
+
`);
|
|
277
|
+
stmt.run(
|
|
278
|
+
entry.id,
|
|
279
|
+
entry.card_id,
|
|
280
|
+
entry.card_name,
|
|
281
|
+
entry.requester,
|
|
282
|
+
entry.status,
|
|
283
|
+
entry.latency_ms,
|
|
284
|
+
entry.credits_charged,
|
|
285
|
+
entry.created_at,
|
|
286
|
+
entry.skill_id ?? null,
|
|
287
|
+
entry.action_type ?? null,
|
|
288
|
+
entry.tier_invoked ?? null
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
function getSkillRequestCount(db, skillId, windowMs) {
|
|
292
|
+
const cutoff = new Date(Date.now() - windowMs).toISOString();
|
|
293
|
+
const stmt = db.prepare(
|
|
294
|
+
`SELECT COUNT(*) as cnt FROM request_log
|
|
295
|
+
WHERE skill_id = ? AND created_at >= ? AND status = 'success' AND action_type IS NULL`
|
|
296
|
+
);
|
|
297
|
+
const row = stmt.get(skillId, cutoff);
|
|
298
|
+
return row.cnt;
|
|
299
|
+
}
|
|
300
|
+
function getActivityFeed(db, limit = 20, since) {
|
|
301
|
+
const effectiveLimit = Math.min(limit, 100);
|
|
302
|
+
if (since !== void 0) {
|
|
303
|
+
const stmt2 = db.prepare(`
|
|
304
|
+
SELECT r.id, r.card_name, r.requester, c.owner AS provider,
|
|
305
|
+
r.status, r.credits_charged, r.latency_ms, r.created_at, r.action_type
|
|
306
|
+
FROM request_log r
|
|
307
|
+
LEFT JOIN capability_cards c ON r.card_id = c.id
|
|
308
|
+
WHERE (r.action_type IS NULL OR r.action_type = 'auto_share')
|
|
309
|
+
AND r.created_at > ?
|
|
310
|
+
ORDER BY r.created_at DESC
|
|
311
|
+
LIMIT ?
|
|
312
|
+
`);
|
|
313
|
+
return stmt2.all(since, effectiveLimit);
|
|
314
|
+
}
|
|
315
|
+
const stmt = db.prepare(`
|
|
316
|
+
SELECT r.id, r.card_name, r.requester, c.owner AS provider,
|
|
317
|
+
r.status, r.credits_charged, r.latency_ms, r.created_at, r.action_type
|
|
318
|
+
FROM request_log r
|
|
319
|
+
LEFT JOIN capability_cards c ON r.card_id = c.id
|
|
320
|
+
WHERE (r.action_type IS NULL OR r.action_type = 'auto_share')
|
|
321
|
+
ORDER BY r.created_at DESC
|
|
322
|
+
LIMIT ?
|
|
323
|
+
`);
|
|
324
|
+
return stmt.all(effectiveLimit);
|
|
325
|
+
}
|
|
326
|
+
function getRequestLog(db, limit = 10, since) {
|
|
327
|
+
if (since !== void 0) {
|
|
328
|
+
const cutoff = new Date(Date.now() - SINCE_MS[since]).toISOString();
|
|
329
|
+
const stmt2 = db.prepare(`
|
|
330
|
+
SELECT id, card_id, card_name, requester, status, latency_ms, credits_charged, created_at, skill_id, action_type, tier_invoked
|
|
331
|
+
FROM request_log
|
|
332
|
+
WHERE created_at >= ?
|
|
333
|
+
ORDER BY created_at DESC
|
|
334
|
+
LIMIT ?
|
|
335
|
+
`);
|
|
336
|
+
return stmt2.all(cutoff, limit);
|
|
337
|
+
}
|
|
338
|
+
const stmt = db.prepare(`
|
|
339
|
+
SELECT id, card_id, card_name, requester, status, latency_ms, credits_charged, created_at, skill_id, action_type, tier_invoked
|
|
340
|
+
FROM request_log
|
|
341
|
+
ORDER BY created_at DESC
|
|
342
|
+
LIMIT ?
|
|
343
|
+
`);
|
|
344
|
+
return stmt.all(limit);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/registry/store.ts
|
|
348
|
+
var V2_FTS_TRIGGERS = `
|
|
349
|
+
DROP TRIGGER IF EXISTS cards_ai;
|
|
350
|
+
DROP TRIGGER IF EXISTS cards_au;
|
|
351
|
+
DROP TRIGGER IF EXISTS cards_ad;
|
|
352
|
+
|
|
353
|
+
CREATE TRIGGER cards_ai AFTER INSERT ON capability_cards BEGIN
|
|
354
|
+
INSERT INTO cards_fts(rowid, id, owner, name, description, tags)
|
|
355
|
+
VALUES (
|
|
356
|
+
new.rowid,
|
|
357
|
+
new.id,
|
|
358
|
+
new.owner,
|
|
359
|
+
COALESCE(
|
|
360
|
+
(SELECT group_concat(json_extract(value, '$.name'), ' ')
|
|
361
|
+
FROM json_each(json_extract(new.data, '$.skills'))),
|
|
362
|
+
json_extract(new.data, '$.name'),
|
|
363
|
+
''
|
|
364
|
+
),
|
|
365
|
+
COALESCE(
|
|
366
|
+
(SELECT group_concat(json_extract(value, '$.description'), ' ')
|
|
367
|
+
FROM json_each(json_extract(new.data, '$.skills'))),
|
|
368
|
+
json_extract(new.data, '$.description'),
|
|
369
|
+
''
|
|
370
|
+
),
|
|
371
|
+
COALESCE(
|
|
372
|
+
(SELECT group_concat(json_extract(value, '$.metadata.tags'), ' ')
|
|
373
|
+
FROM json_each(json_extract(new.data, '$.skills'))),
|
|
374
|
+
(SELECT group_concat(value, ' ')
|
|
375
|
+
FROM json_each(json_extract(new.data, '$.metadata.tags'))),
|
|
376
|
+
''
|
|
377
|
+
)
|
|
378
|
+
);
|
|
379
|
+
END;
|
|
380
|
+
|
|
381
|
+
CREATE TRIGGER cards_au AFTER UPDATE ON capability_cards BEGIN
|
|
382
|
+
INSERT INTO cards_fts(cards_fts, rowid, id, owner, name, description, tags)
|
|
383
|
+
VALUES (
|
|
384
|
+
'delete',
|
|
385
|
+
old.rowid,
|
|
386
|
+
old.id,
|
|
387
|
+
old.owner,
|
|
388
|
+
COALESCE(
|
|
389
|
+
(SELECT group_concat(json_extract(value, '$.name'), ' ')
|
|
390
|
+
FROM json_each(json_extract(old.data, '$.skills'))),
|
|
391
|
+
json_extract(old.data, '$.name'),
|
|
392
|
+
''
|
|
393
|
+
),
|
|
394
|
+
COALESCE(
|
|
395
|
+
(SELECT group_concat(json_extract(value, '$.description'), ' ')
|
|
396
|
+
FROM json_each(json_extract(old.data, '$.skills'))),
|
|
397
|
+
json_extract(old.data, '$.description'),
|
|
398
|
+
''
|
|
399
|
+
),
|
|
400
|
+
COALESCE(
|
|
401
|
+
(SELECT group_concat(json_extract(value, '$.metadata.tags'), ' ')
|
|
402
|
+
FROM json_each(json_extract(old.data, '$.skills'))),
|
|
403
|
+
(SELECT group_concat(value, ' ')
|
|
404
|
+
FROM json_each(json_extract(old.data, '$.metadata.tags'))),
|
|
405
|
+
''
|
|
406
|
+
)
|
|
407
|
+
);
|
|
408
|
+
INSERT INTO cards_fts(rowid, id, owner, name, description, tags)
|
|
409
|
+
VALUES (
|
|
410
|
+
new.rowid,
|
|
411
|
+
new.id,
|
|
412
|
+
new.owner,
|
|
413
|
+
COALESCE(
|
|
414
|
+
(SELECT group_concat(json_extract(value, '$.name'), ' ')
|
|
415
|
+
FROM json_each(json_extract(new.data, '$.skills'))),
|
|
416
|
+
json_extract(new.data, '$.name'),
|
|
417
|
+
''
|
|
418
|
+
),
|
|
419
|
+
COALESCE(
|
|
420
|
+
(SELECT group_concat(json_extract(value, '$.description'), ' ')
|
|
421
|
+
FROM json_each(json_extract(new.data, '$.skills'))),
|
|
422
|
+
json_extract(new.data, '$.description'),
|
|
423
|
+
''
|
|
424
|
+
),
|
|
425
|
+
COALESCE(
|
|
426
|
+
(SELECT group_concat(json_extract(value, '$.metadata.tags'), ' ')
|
|
427
|
+
FROM json_each(json_extract(new.data, '$.skills'))),
|
|
428
|
+
(SELECT group_concat(value, ' ')
|
|
429
|
+
FROM json_each(json_extract(new.data, '$.metadata.tags'))),
|
|
430
|
+
''
|
|
431
|
+
)
|
|
432
|
+
);
|
|
433
|
+
END;
|
|
434
|
+
|
|
435
|
+
CREATE TRIGGER cards_ad AFTER DELETE ON capability_cards BEGIN
|
|
436
|
+
INSERT INTO cards_fts(cards_fts, rowid, id, owner, name, description, tags)
|
|
437
|
+
VALUES (
|
|
438
|
+
'delete',
|
|
439
|
+
old.rowid,
|
|
440
|
+
old.id,
|
|
441
|
+
old.owner,
|
|
442
|
+
COALESCE(
|
|
443
|
+
(SELECT group_concat(json_extract(value, '$.name'), ' ')
|
|
444
|
+
FROM json_each(json_extract(old.data, '$.skills'))),
|
|
445
|
+
json_extract(old.data, '$.name'),
|
|
446
|
+
''
|
|
447
|
+
),
|
|
448
|
+
COALESCE(
|
|
449
|
+
(SELECT group_concat(json_extract(value, '$.description'), ' ')
|
|
450
|
+
FROM json_each(json_extract(old.data, '$.skills'))),
|
|
451
|
+
json_extract(old.data, '$.description'),
|
|
452
|
+
''
|
|
453
|
+
),
|
|
454
|
+
COALESCE(
|
|
455
|
+
(SELECT group_concat(json_extract(value, '$.metadata.tags'), ' ')
|
|
456
|
+
FROM json_each(json_extract(old.data, '$.skills'))),
|
|
457
|
+
(SELECT group_concat(value, ' ')
|
|
458
|
+
FROM json_each(json_extract(old.data, '$.metadata.tags'))),
|
|
459
|
+
''
|
|
460
|
+
)
|
|
461
|
+
);
|
|
462
|
+
END;
|
|
463
|
+
`;
|
|
464
|
+
function openDatabase(path = ":memory:") {
|
|
465
|
+
const db = new Database(path);
|
|
466
|
+
db.pragma("journal_mode = WAL");
|
|
467
|
+
db.pragma("foreign_keys = ON");
|
|
468
|
+
db.exec(`
|
|
469
|
+
CREATE TABLE IF NOT EXISTS capability_cards (
|
|
470
|
+
id TEXT PRIMARY KEY,
|
|
471
|
+
owner TEXT NOT NULL,
|
|
472
|
+
data TEXT NOT NULL,
|
|
473
|
+
created_at TEXT NOT NULL,
|
|
474
|
+
updated_at TEXT NOT NULL
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
CREATE TABLE IF NOT EXISTS pending_requests (
|
|
478
|
+
id TEXT PRIMARY KEY,
|
|
479
|
+
skill_query TEXT NOT NULL,
|
|
480
|
+
max_cost_credits REAL NOT NULL,
|
|
481
|
+
selected_peer TEXT,
|
|
482
|
+
selected_card_id TEXT,
|
|
483
|
+
selected_skill_id TEXT,
|
|
484
|
+
credits REAL NOT NULL,
|
|
485
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
486
|
+
params TEXT,
|
|
487
|
+
created_at TEXT NOT NULL,
|
|
488
|
+
resolved_at TEXT
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS cards_fts USING fts5(
|
|
492
|
+
id UNINDEXED,
|
|
493
|
+
owner,
|
|
494
|
+
name,
|
|
495
|
+
description,
|
|
496
|
+
tags,
|
|
497
|
+
content=""
|
|
498
|
+
);
|
|
499
|
+
`);
|
|
500
|
+
createRequestLogTable(db);
|
|
501
|
+
runMigrations(db);
|
|
502
|
+
return db;
|
|
503
|
+
}
|
|
504
|
+
function runMigrations(db) {
|
|
505
|
+
const version = db.pragma("user_version")[0]?.user_version ?? 0;
|
|
506
|
+
if (version < 2) {
|
|
507
|
+
migrateV1toV2(db);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function migrateV1toV2(db) {
|
|
511
|
+
const migrate = db.transaction(() => {
|
|
512
|
+
const rows = db.prepare("SELECT rowid, id, data FROM capability_cards").all();
|
|
513
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
514
|
+
for (const row of rows) {
|
|
515
|
+
const parsed = JSON.parse(row.data);
|
|
516
|
+
if (parsed["spec_version"] === "2.0") continue;
|
|
517
|
+
const v1 = parsed;
|
|
518
|
+
const v2 = {
|
|
519
|
+
spec_version: "2.0",
|
|
520
|
+
id: v1.id,
|
|
521
|
+
owner: v1.owner,
|
|
522
|
+
agent_name: v1.name,
|
|
523
|
+
skills: [
|
|
524
|
+
{
|
|
525
|
+
id: `skill-${v1.id}`,
|
|
526
|
+
name: v1.name,
|
|
527
|
+
description: v1.description,
|
|
528
|
+
level: v1.level,
|
|
529
|
+
inputs: v1.inputs,
|
|
530
|
+
outputs: v1.outputs,
|
|
531
|
+
pricing: v1.pricing,
|
|
532
|
+
availability: { online: v1.availability.online },
|
|
533
|
+
powered_by: v1.powered_by,
|
|
534
|
+
metadata: v1.metadata,
|
|
535
|
+
_internal: v1._internal
|
|
536
|
+
}
|
|
537
|
+
],
|
|
538
|
+
availability: v1.availability,
|
|
539
|
+
created_at: v1.created_at,
|
|
540
|
+
updated_at: now
|
|
541
|
+
};
|
|
542
|
+
db.prepare("UPDATE capability_cards SET data = ?, updated_at = ? WHERE id = ?").run(
|
|
543
|
+
JSON.stringify(v2),
|
|
544
|
+
now,
|
|
545
|
+
v2.id
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
db.exec(V2_FTS_TRIGGERS);
|
|
549
|
+
db.exec(`INSERT INTO cards_fts(cards_fts) VALUES('delete-all')`);
|
|
550
|
+
const allRows = db.prepare("SELECT rowid, id, owner, data FROM capability_cards").all();
|
|
551
|
+
const ftsInsert = db.prepare(
|
|
552
|
+
"INSERT INTO cards_fts(rowid, id, owner, name, description, tags) VALUES (?, ?, ?, ?, ?, ?)"
|
|
553
|
+
);
|
|
554
|
+
for (const row of allRows) {
|
|
555
|
+
const data = JSON.parse(row.data);
|
|
556
|
+
const skills = data["skills"] ?? [];
|
|
557
|
+
let name;
|
|
558
|
+
let description;
|
|
559
|
+
let tags;
|
|
560
|
+
if (skills.length > 0) {
|
|
561
|
+
name = skills.map((s) => String(s["name"] ?? "")).join(" ");
|
|
562
|
+
description = skills.map((s) => String(s["description"] ?? "")).join(" ");
|
|
563
|
+
tags = skills.flatMap((s) => {
|
|
564
|
+
const meta = s["metadata"];
|
|
565
|
+
return meta?.["tags"] ?? [];
|
|
566
|
+
}).join(" ");
|
|
567
|
+
} else {
|
|
568
|
+
name = String(data["name"] ?? "");
|
|
569
|
+
description = String(data["description"] ?? "");
|
|
570
|
+
const meta = data["metadata"];
|
|
571
|
+
const rawTags = meta?.["tags"] ?? [];
|
|
572
|
+
tags = rawTags.join(" ");
|
|
573
|
+
}
|
|
574
|
+
ftsInsert.run(row.rowid, row.id, row.owner, name, description, tags);
|
|
575
|
+
}
|
|
576
|
+
db.pragma("user_version = 2");
|
|
577
|
+
});
|
|
578
|
+
migrate();
|
|
579
|
+
}
|
|
580
|
+
function insertCard(db, card) {
|
|
581
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
582
|
+
const withTimestamps = { ...card, created_at: card.created_at ?? now, updated_at: now };
|
|
583
|
+
const parsed = CapabilityCardSchema.safeParse(withTimestamps);
|
|
584
|
+
if (!parsed.success) {
|
|
585
|
+
throw new AgentBnBError(
|
|
586
|
+
`Card validation failed: ${parsed.error.message}`,
|
|
587
|
+
"VALIDATION_ERROR"
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
const stmt = db.prepare(`
|
|
591
|
+
INSERT INTO capability_cards (id, owner, data, created_at, updated_at)
|
|
592
|
+
VALUES (?, ?, ?, ?, ?)
|
|
593
|
+
`);
|
|
594
|
+
stmt.run(
|
|
595
|
+
parsed.data.id,
|
|
596
|
+
parsed.data.owner,
|
|
597
|
+
JSON.stringify(parsed.data),
|
|
598
|
+
parsed.data.created_at ?? now,
|
|
599
|
+
parsed.data.updated_at ?? now
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
function getCard(db, id) {
|
|
603
|
+
const stmt = db.prepare("SELECT data FROM capability_cards WHERE id = ?");
|
|
604
|
+
const row = stmt.get(id);
|
|
605
|
+
if (!row) return null;
|
|
606
|
+
return JSON.parse(row.data);
|
|
607
|
+
}
|
|
608
|
+
function updateCard(db, id, owner, updates) {
|
|
609
|
+
const existing = getCard(db, id);
|
|
610
|
+
if (!existing) {
|
|
611
|
+
throw new AgentBnBError(`Card not found: ${id}`, "NOT_FOUND");
|
|
612
|
+
}
|
|
613
|
+
if (existing.owner !== owner) {
|
|
614
|
+
throw new AgentBnBError("Forbidden: you do not own this card", "FORBIDDEN");
|
|
615
|
+
}
|
|
616
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
617
|
+
const merged = { ...existing, ...updates, updated_at: now };
|
|
618
|
+
const parsed = CapabilityCardSchema.safeParse(merged);
|
|
619
|
+
if (!parsed.success) {
|
|
620
|
+
throw new AgentBnBError(
|
|
621
|
+
`Card validation failed: ${parsed.error.message}`,
|
|
622
|
+
"VALIDATION_ERROR"
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
const stmt = db.prepare(`
|
|
626
|
+
UPDATE capability_cards
|
|
627
|
+
SET data = ?, updated_at = ?
|
|
628
|
+
WHERE id = ?
|
|
629
|
+
`);
|
|
630
|
+
stmt.run(JSON.stringify(parsed.data), now, id);
|
|
631
|
+
}
|
|
632
|
+
function updateReputation(db, cardId, success, latencyMs) {
|
|
633
|
+
const existing = getCard(db, cardId);
|
|
634
|
+
if (!existing) return;
|
|
635
|
+
const ALPHA = 0.1;
|
|
636
|
+
const observed = success ? 1 : 0;
|
|
637
|
+
const prevSuccessRate = existing.metadata?.success_rate;
|
|
638
|
+
const prevLatency = existing.metadata?.avg_latency_ms;
|
|
639
|
+
const newSuccessRate = prevSuccessRate === void 0 ? observed : ALPHA * observed + (1 - ALPHA) * prevSuccessRate;
|
|
640
|
+
const newLatency = prevLatency === void 0 ? latencyMs : ALPHA * latencyMs + (1 - ALPHA) * prevLatency;
|
|
641
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
642
|
+
const updatedMetadata = {
|
|
643
|
+
...existing.metadata,
|
|
644
|
+
success_rate: Math.round(newSuccessRate * 1e3) / 1e3,
|
|
645
|
+
avg_latency_ms: Math.round(newLatency)
|
|
646
|
+
};
|
|
647
|
+
const updatedCard = { ...existing, metadata: updatedMetadata, updated_at: now };
|
|
648
|
+
const stmt = db.prepare(`
|
|
649
|
+
UPDATE capability_cards
|
|
650
|
+
SET data = ?, updated_at = ?
|
|
651
|
+
WHERE id = ?
|
|
652
|
+
`);
|
|
653
|
+
stmt.run(JSON.stringify(updatedCard), now, cardId);
|
|
654
|
+
}
|
|
655
|
+
function updateSkillAvailability(db, cardId, skillId, online) {
|
|
656
|
+
const row = db.prepare("SELECT data FROM capability_cards WHERE id = ?").get(cardId);
|
|
657
|
+
if (!row) return;
|
|
658
|
+
const card = JSON.parse(row.data);
|
|
659
|
+
const skills = card["skills"];
|
|
660
|
+
if (!skills) return;
|
|
661
|
+
const skill = skills.find((s) => s["id"] === skillId);
|
|
662
|
+
if (!skill) return;
|
|
663
|
+
const existing = skill["availability"] ?? {};
|
|
664
|
+
skill["availability"] = { ...existing, online };
|
|
665
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
666
|
+
db.prepare("UPDATE capability_cards SET data = ?, updated_at = ? WHERE id = ?").run(
|
|
667
|
+
JSON.stringify(card),
|
|
668
|
+
now,
|
|
669
|
+
cardId
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
function updateSkillIdleRate(db, cardId, skillId, idleRate) {
|
|
673
|
+
const row = db.prepare("SELECT data FROM capability_cards WHERE id = ?").get(cardId);
|
|
674
|
+
if (!row) return;
|
|
675
|
+
const card = JSON.parse(row.data);
|
|
676
|
+
const skills = card["skills"];
|
|
677
|
+
if (!skills) return;
|
|
678
|
+
const skill = skills.find((s) => s["id"] === skillId);
|
|
679
|
+
if (!skill) return;
|
|
680
|
+
const existing = skill["_internal"] ?? {};
|
|
681
|
+
skill["_internal"] = {
|
|
682
|
+
...existing,
|
|
683
|
+
idle_rate: idleRate,
|
|
684
|
+
idle_rate_computed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
685
|
+
};
|
|
686
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
687
|
+
db.prepare("UPDATE capability_cards SET data = ?, updated_at = ? WHERE id = ?").run(
|
|
688
|
+
JSON.stringify(card),
|
|
689
|
+
now,
|
|
690
|
+
cardId
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
function listCards(db, owner) {
|
|
694
|
+
let stmt;
|
|
695
|
+
let rows;
|
|
696
|
+
if (owner !== void 0) {
|
|
697
|
+
stmt = db.prepare("SELECT data FROM capability_cards WHERE owner = ?");
|
|
698
|
+
rows = stmt.all(owner);
|
|
699
|
+
} else {
|
|
700
|
+
stmt = db.prepare("SELECT data FROM capability_cards");
|
|
701
|
+
rows = stmt.all();
|
|
702
|
+
}
|
|
703
|
+
return rows.map((row) => JSON.parse(row.data));
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/autonomy/idle-monitor.ts
|
|
707
|
+
var IdleMonitor = class {
|
|
708
|
+
job;
|
|
709
|
+
owner;
|
|
710
|
+
db;
|
|
711
|
+
idleThreshold;
|
|
712
|
+
autonomyConfig;
|
|
713
|
+
/**
|
|
714
|
+
* Creates a new IdleMonitor instance. The Cron job is constructed paused.
|
|
715
|
+
* Call `start()` to activate polling.
|
|
716
|
+
*
|
|
717
|
+
* @param opts - IdleMonitor configuration options.
|
|
718
|
+
*/
|
|
719
|
+
constructor(opts) {
|
|
720
|
+
this.owner = opts.owner;
|
|
721
|
+
this.db = opts.db;
|
|
722
|
+
this.idleThreshold = opts.idleThreshold ?? 0.7;
|
|
723
|
+
this.autonomyConfig = opts.autonomyConfig ?? DEFAULT_AUTONOMY_CONFIG;
|
|
724
|
+
this.job = new Cron("0 * * * * *", { paused: true }, () => {
|
|
725
|
+
void this.poll();
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Starts the Cron polling loop by resuming the paused job.
|
|
730
|
+
*
|
|
731
|
+
* @returns The Cron job instance, for registration with AgentRuntime.registerJob().
|
|
732
|
+
*/
|
|
733
|
+
start() {
|
|
734
|
+
this.job.resume();
|
|
735
|
+
return this.job;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Returns the underlying Cron job instance.
|
|
739
|
+
* Used for testing or for registering with AgentRuntime.registerJob() without starting.
|
|
740
|
+
*
|
|
741
|
+
* @returns The Cron job instance.
|
|
742
|
+
*/
|
|
743
|
+
getJob() {
|
|
744
|
+
return this.job;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Polls the registry for all v2.0 cards owned by this agent, computes per-skill
|
|
748
|
+
* idle rates from the past hour of request_log data, and triggers auto-share if eligible.
|
|
749
|
+
*
|
|
750
|
+
* Called automatically by the Cron job every 60 seconds.
|
|
751
|
+
* Can be called directly in tests without needing to wait for the timer.
|
|
752
|
+
*/
|
|
753
|
+
async poll() {
|
|
754
|
+
const cards = listCards(this.db, this.owner);
|
|
755
|
+
for (const card of cards) {
|
|
756
|
+
const maybeV2 = card;
|
|
757
|
+
if (!Array.isArray(maybeV2.skills)) {
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
for (const skill of maybeV2.skills) {
|
|
761
|
+
const capacity = skill.metadata?.capacity?.calls_per_hour ?? 60;
|
|
762
|
+
const count = getSkillRequestCount(this.db, skill.id, 60 * 60 * 1e3);
|
|
763
|
+
const idleRate = Math.max(0, 1 - count / capacity);
|
|
764
|
+
updateSkillIdleRate(this.db, card.id, skill.id, idleRate);
|
|
765
|
+
const isOnline = skill.availability?.online ?? false;
|
|
766
|
+
if (idleRate >= this.idleThreshold && !isOnline) {
|
|
767
|
+
const tier = getAutonomyTier(0, this.autonomyConfig);
|
|
768
|
+
if (tier === 1) {
|
|
769
|
+
updateSkillAvailability(this.db, card.id, skill.id, true);
|
|
770
|
+
insertAuditEvent(this.db, {
|
|
771
|
+
type: "auto_share",
|
|
772
|
+
skill_id: skill.id,
|
|
773
|
+
tier_invoked: 1,
|
|
774
|
+
idle_rate: idleRate
|
|
775
|
+
});
|
|
776
|
+
} else if (tier === 2) {
|
|
777
|
+
updateSkillAvailability(this.db, card.id, skill.id, true);
|
|
778
|
+
insertAuditEvent(this.db, {
|
|
779
|
+
type: "auto_share_notify",
|
|
780
|
+
skill_id: skill.id,
|
|
781
|
+
tier_invoked: 2,
|
|
782
|
+
idle_rate: idleRate
|
|
783
|
+
});
|
|
784
|
+
} else {
|
|
785
|
+
insertAuditEvent(this.db, {
|
|
786
|
+
type: "auto_share_pending",
|
|
787
|
+
skill_id: skill.id,
|
|
788
|
+
tier_invoked: 3,
|
|
789
|
+
idle_rate: idleRate
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
// src/credit/ledger.ts
|
|
799
|
+
import Database2 from "better-sqlite3";
|
|
800
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
801
|
+
var CREDIT_SCHEMA = `
|
|
802
|
+
CREATE TABLE IF NOT EXISTS credit_balances (
|
|
803
|
+
owner TEXT PRIMARY KEY,
|
|
804
|
+
balance INTEGER NOT NULL DEFAULT 0,
|
|
805
|
+
updated_at TEXT NOT NULL
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
CREATE TABLE IF NOT EXISTS credit_transactions (
|
|
809
|
+
id TEXT PRIMARY KEY,
|
|
810
|
+
owner TEXT NOT NULL,
|
|
811
|
+
amount INTEGER NOT NULL,
|
|
812
|
+
reason TEXT NOT NULL,
|
|
813
|
+
reference_id TEXT,
|
|
814
|
+
created_at TEXT NOT NULL
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
CREATE TABLE IF NOT EXISTS credit_escrow (
|
|
818
|
+
id TEXT PRIMARY KEY,
|
|
819
|
+
owner TEXT NOT NULL,
|
|
820
|
+
amount INTEGER NOT NULL,
|
|
821
|
+
card_id TEXT NOT NULL,
|
|
822
|
+
status TEXT NOT NULL DEFAULT 'held',
|
|
823
|
+
created_at TEXT NOT NULL,
|
|
824
|
+
settled_at TEXT
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
CREATE INDEX IF NOT EXISTS idx_transactions_owner ON credit_transactions(owner, created_at);
|
|
828
|
+
CREATE INDEX IF NOT EXISTS idx_escrow_owner ON credit_escrow(owner);
|
|
829
|
+
`;
|
|
830
|
+
function openCreditDb(path = ":memory:") {
|
|
831
|
+
const db = new Database2(path);
|
|
832
|
+
db.pragma("journal_mode = WAL");
|
|
833
|
+
db.pragma("foreign_keys = ON");
|
|
834
|
+
db.exec(CREDIT_SCHEMA);
|
|
835
|
+
return db;
|
|
836
|
+
}
|
|
837
|
+
function bootstrapAgent(db, owner, amount = 100) {
|
|
838
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
839
|
+
db.transaction(() => {
|
|
840
|
+
const result = db.prepare("INSERT OR IGNORE INTO credit_balances (owner, balance, updated_at) VALUES (?, ?, ?)").run(owner, amount, now);
|
|
841
|
+
if (result.changes > 0) {
|
|
842
|
+
db.prepare(
|
|
843
|
+
"INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
844
|
+
).run(randomUUID2(), owner, amount, "bootstrap", null, now);
|
|
845
|
+
}
|
|
846
|
+
})();
|
|
847
|
+
}
|
|
848
|
+
function getBalance(db, owner) {
|
|
849
|
+
const row = db.prepare("SELECT balance FROM credit_balances WHERE owner = ?").get(owner);
|
|
850
|
+
return row?.balance ?? 0;
|
|
851
|
+
}
|
|
852
|
+
function getTransactions(db, owner, limit = 100) {
|
|
853
|
+
return db.prepare(
|
|
854
|
+
"SELECT id, owner, amount, reason, reference_id, created_at FROM credit_transactions WHERE owner = ? ORDER BY created_at DESC LIMIT ?"
|
|
855
|
+
).all(owner, limit);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// src/credit/budget.ts
|
|
859
|
+
var DEFAULT_BUDGET_CONFIG = {
|
|
860
|
+
reserve_credits: 20
|
|
861
|
+
};
|
|
862
|
+
var BudgetManager = class {
|
|
863
|
+
/**
|
|
864
|
+
* Creates a new BudgetManager.
|
|
865
|
+
*
|
|
866
|
+
* @param creditDb - The credit SQLite database instance.
|
|
867
|
+
* @param owner - Agent owner identifier.
|
|
868
|
+
* @param config - Budget configuration. Defaults to DEFAULT_BUDGET_CONFIG (20 credit reserve).
|
|
869
|
+
*/
|
|
870
|
+
constructor(creditDb, owner, config = DEFAULT_BUDGET_CONFIG) {
|
|
871
|
+
this.creditDb = creditDb;
|
|
872
|
+
this.owner = owner;
|
|
873
|
+
this.config = config;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Returns the number of credits available for spending.
|
|
877
|
+
* Computed as: max(0, balance - reserve_credits).
|
|
878
|
+
* Always returns a non-negative number — never goes below zero.
|
|
879
|
+
*
|
|
880
|
+
* @returns Available credits (balance minus reserve, floored at 0).
|
|
881
|
+
*/
|
|
882
|
+
availableCredits() {
|
|
883
|
+
const balance = getBalance(this.creditDb, this.owner);
|
|
884
|
+
return Math.max(0, balance - this.config.reserve_credits);
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Returns true if spending `amount` credits is permitted by budget rules.
|
|
888
|
+
*
|
|
889
|
+
* Rules:
|
|
890
|
+
* - Zero-cost calls (amount <= 0) always return true.
|
|
891
|
+
* - Any positive amount requires availableCredits() >= amount.
|
|
892
|
+
* - If balance is at or below the reserve floor, all positive-cost calls return false.
|
|
893
|
+
*
|
|
894
|
+
* @param amount - Number of credits to spend.
|
|
895
|
+
* @returns true if the spend is allowed, false if it would breach the reserve floor.
|
|
896
|
+
*/
|
|
897
|
+
canSpend(amount) {
|
|
898
|
+
if (amount <= 0) return true;
|
|
899
|
+
return this.availableCredits() >= amount;
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
// src/registry/matcher.ts
|
|
904
|
+
function searchCards(db, query, filters = {}) {
|
|
905
|
+
const words = query.trim().split(/\s+/).map((w) => w.replace(/"/g, "")).filter((w) => w.length > 0);
|
|
906
|
+
if (words.length === 0) return [];
|
|
907
|
+
const ftsQuery = words.map((w) => `"${w}"`).join(" OR ");
|
|
908
|
+
const conditions = [];
|
|
909
|
+
const params = [ftsQuery];
|
|
910
|
+
if (filters.level !== void 0) {
|
|
911
|
+
conditions.push(`json_extract(cc.data, '$.level') = ?`);
|
|
912
|
+
params.push(filters.level);
|
|
913
|
+
}
|
|
914
|
+
if (filters.online !== void 0) {
|
|
915
|
+
conditions.push(`json_extract(cc.data, '$.availability.online') = ?`);
|
|
916
|
+
params.push(filters.online ? 1 : 0);
|
|
917
|
+
}
|
|
918
|
+
const whereClause = conditions.length > 0 ? `AND ${conditions.join(" AND ")}` : "";
|
|
919
|
+
const sql = `
|
|
920
|
+
SELECT cc.data
|
|
921
|
+
FROM capability_cards cc
|
|
922
|
+
JOIN cards_fts ON cc.rowid = cards_fts.rowid
|
|
923
|
+
WHERE cards_fts MATCH ?
|
|
924
|
+
${whereClause}
|
|
925
|
+
ORDER BY bm25(cards_fts)
|
|
926
|
+
LIMIT 50
|
|
927
|
+
`;
|
|
928
|
+
const stmt = db.prepare(sql);
|
|
929
|
+
const rows = stmt.all(...params);
|
|
930
|
+
const results = rows.map((row) => JSON.parse(row.data));
|
|
931
|
+
if (filters.apis_used && filters.apis_used.length > 0) {
|
|
932
|
+
const requiredApis = filters.apis_used;
|
|
933
|
+
return results.filter((card) => {
|
|
934
|
+
const cardApis = card.metadata?.apis_used ?? [];
|
|
935
|
+
return requiredApis.every((api) => cardApis.includes(api));
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
return results;
|
|
939
|
+
}
|
|
940
|
+
function filterCards(db, filters) {
|
|
941
|
+
const conditions = [];
|
|
942
|
+
const params = [];
|
|
943
|
+
if (filters.level !== void 0) {
|
|
944
|
+
conditions.push(`json_extract(data, '$.level') = ?`);
|
|
945
|
+
params.push(filters.level);
|
|
946
|
+
}
|
|
947
|
+
if (filters.online !== void 0) {
|
|
948
|
+
conditions.push(`json_extract(data, '$.availability.online') = ?`);
|
|
949
|
+
params.push(filters.online ? 1 : 0);
|
|
950
|
+
}
|
|
951
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
952
|
+
const sql = `SELECT data FROM capability_cards ${whereClause}`;
|
|
953
|
+
const stmt = db.prepare(sql);
|
|
954
|
+
const rows = stmt.all(...params);
|
|
955
|
+
return rows.map((row) => JSON.parse(row.data));
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// src/credit/escrow.ts
|
|
959
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
960
|
+
function holdEscrow(db, owner, amount, cardId) {
|
|
961
|
+
const escrowId = randomUUID3();
|
|
962
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
963
|
+
const hold = db.transaction(() => {
|
|
964
|
+
const row = db.prepare("SELECT balance FROM credit_balances WHERE owner = ?").get(owner);
|
|
965
|
+
if (!row || row.balance < amount) {
|
|
966
|
+
throw new AgentBnBError("Insufficient credits", "INSUFFICIENT_CREDITS");
|
|
967
|
+
}
|
|
968
|
+
db.prepare(
|
|
969
|
+
"UPDATE credit_balances SET balance = balance - ?, updated_at = ? WHERE owner = ? AND balance >= ?"
|
|
970
|
+
).run(amount, now, owner, amount);
|
|
971
|
+
db.prepare(
|
|
972
|
+
"INSERT INTO credit_escrow (id, owner, amount, card_id, status, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
973
|
+
).run(escrowId, owner, amount, cardId, "held", now);
|
|
974
|
+
db.prepare(
|
|
975
|
+
"INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
976
|
+
).run(randomUUID3(), owner, -amount, "escrow_hold", escrowId, now);
|
|
977
|
+
});
|
|
978
|
+
hold();
|
|
979
|
+
return escrowId;
|
|
980
|
+
}
|
|
981
|
+
function settleEscrow(db, escrowId, recipientOwner) {
|
|
982
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
983
|
+
const settle = db.transaction(() => {
|
|
984
|
+
const escrow = db.prepare("SELECT id, owner, amount, status FROM credit_escrow WHERE id = ?").get(escrowId);
|
|
985
|
+
if (!escrow) {
|
|
986
|
+
throw new AgentBnBError(`Escrow not found: ${escrowId}`, "ESCROW_NOT_FOUND");
|
|
987
|
+
}
|
|
988
|
+
if (escrow.status !== "held") {
|
|
989
|
+
throw new AgentBnBError(
|
|
990
|
+
`Escrow ${escrowId} is already ${escrow.status}`,
|
|
991
|
+
"ESCROW_ALREADY_SETTLED"
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
db.prepare(
|
|
995
|
+
"INSERT OR IGNORE INTO credit_balances (owner, balance, updated_at) VALUES (?, 0, ?)"
|
|
996
|
+
).run(recipientOwner, now);
|
|
997
|
+
db.prepare(
|
|
998
|
+
"UPDATE credit_balances SET balance = balance + ?, updated_at = ? WHERE owner = ?"
|
|
999
|
+
).run(escrow.amount, now, recipientOwner);
|
|
1000
|
+
db.prepare(
|
|
1001
|
+
"UPDATE credit_escrow SET status = ?, settled_at = ? WHERE id = ?"
|
|
1002
|
+
).run("settled", now, escrowId);
|
|
1003
|
+
db.prepare(
|
|
1004
|
+
"INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
1005
|
+
).run(randomUUID3(), recipientOwner, escrow.amount, "settlement", escrowId, now);
|
|
1006
|
+
});
|
|
1007
|
+
settle();
|
|
1008
|
+
}
|
|
1009
|
+
function releaseEscrow(db, escrowId) {
|
|
1010
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1011
|
+
const release = db.transaction(() => {
|
|
1012
|
+
const escrow = db.prepare("SELECT id, owner, amount, status FROM credit_escrow WHERE id = ?").get(escrowId);
|
|
1013
|
+
if (!escrow) {
|
|
1014
|
+
throw new AgentBnBError(`Escrow not found: ${escrowId}`, "ESCROW_NOT_FOUND");
|
|
1015
|
+
}
|
|
1016
|
+
if (escrow.status !== "held") {
|
|
1017
|
+
throw new AgentBnBError(
|
|
1018
|
+
`Escrow ${escrowId} is already ${escrow.status}`,
|
|
1019
|
+
"ESCROW_ALREADY_SETTLED"
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
db.prepare(
|
|
1023
|
+
"UPDATE credit_balances SET balance = balance + ?, updated_at = ? WHERE owner = ?"
|
|
1024
|
+
).run(escrow.amount, now, escrow.owner);
|
|
1025
|
+
db.prepare(
|
|
1026
|
+
"UPDATE credit_escrow SET status = ?, settled_at = ? WHERE id = ?"
|
|
1027
|
+
).run("released", now, escrowId);
|
|
1028
|
+
db.prepare(
|
|
1029
|
+
"INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
1030
|
+
).run(randomUUID3(), escrow.owner, escrow.amount, "refund", escrowId, now);
|
|
1031
|
+
});
|
|
1032
|
+
release();
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// src/gateway/client.ts
|
|
1036
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
1037
|
+
async function requestCapability(opts) {
|
|
1038
|
+
const { gatewayUrl, token, cardId, params = {}, timeoutMs = 3e4 } = opts;
|
|
1039
|
+
const id = randomUUID4();
|
|
1040
|
+
const payload = {
|
|
1041
|
+
jsonrpc: "2.0",
|
|
1042
|
+
id,
|
|
1043
|
+
method: "capability.execute",
|
|
1044
|
+
params: { card_id: cardId, ...params }
|
|
1045
|
+
};
|
|
1046
|
+
const controller = new AbortController();
|
|
1047
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1048
|
+
let response;
|
|
1049
|
+
try {
|
|
1050
|
+
response = await fetch(`${gatewayUrl}/rpc`, {
|
|
1051
|
+
method: "POST",
|
|
1052
|
+
headers: {
|
|
1053
|
+
"Content-Type": "application/json",
|
|
1054
|
+
Authorization: `Bearer ${token}`
|
|
1055
|
+
},
|
|
1056
|
+
body: JSON.stringify(payload),
|
|
1057
|
+
signal: controller.signal
|
|
1058
|
+
});
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
clearTimeout(timer);
|
|
1061
|
+
const isTimeout = err instanceof Error && err.name === "AbortError";
|
|
1062
|
+
throw new AgentBnBError(
|
|
1063
|
+
isTimeout ? "Request timed out" : `Network error: ${String(err)}`,
|
|
1064
|
+
isTimeout ? "TIMEOUT" : "NETWORK_ERROR"
|
|
1065
|
+
);
|
|
1066
|
+
} finally {
|
|
1067
|
+
clearTimeout(timer);
|
|
1068
|
+
}
|
|
1069
|
+
const body = await response.json();
|
|
1070
|
+
if (body.error) {
|
|
1071
|
+
throw new AgentBnBError(body.error.message, `RPC_ERROR_${body.error.code}`);
|
|
1072
|
+
}
|
|
1073
|
+
return body.result;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// src/autonomy/pending-requests.ts
|
|
1077
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
1078
|
+
function createPendingRequest(db, opts) {
|
|
1079
|
+
const id = randomUUID5();
|
|
1080
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1081
|
+
const paramsJson = opts.params !== void 0 ? JSON.stringify(opts.params) : null;
|
|
1082
|
+
db.prepare(`
|
|
1083
|
+
INSERT INTO pending_requests (
|
|
1084
|
+
id, skill_query, max_cost_credits, selected_peer, selected_card_id,
|
|
1085
|
+
selected_skill_id, credits, status, params, created_at, resolved_at
|
|
1086
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, NULL)
|
|
1087
|
+
`).run(
|
|
1088
|
+
id,
|
|
1089
|
+
opts.skill_query,
|
|
1090
|
+
opts.max_cost_credits,
|
|
1091
|
+
opts.selected_peer ?? null,
|
|
1092
|
+
opts.selected_card_id ?? null,
|
|
1093
|
+
opts.selected_skill_id ?? null,
|
|
1094
|
+
opts.credits,
|
|
1095
|
+
paramsJson,
|
|
1096
|
+
now
|
|
1097
|
+
);
|
|
1098
|
+
return id;
|
|
1099
|
+
}
|
|
1100
|
+
function listPendingRequests(db) {
|
|
1101
|
+
const rows = db.prepare(`SELECT * FROM pending_requests WHERE status = 'pending' ORDER BY created_at DESC`).all();
|
|
1102
|
+
return rows;
|
|
1103
|
+
}
|
|
1104
|
+
function resolvePendingRequest(db, id, resolution) {
|
|
1105
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1106
|
+
const result = db.prepare(
|
|
1107
|
+
`UPDATE pending_requests SET status = ?, resolved_at = ? WHERE id = ?`
|
|
1108
|
+
).run(resolution, now, id);
|
|
1109
|
+
if (result.changes === 0) {
|
|
1110
|
+
throw new AgentBnBError(
|
|
1111
|
+
`Pending request not found: ${id}`,
|
|
1112
|
+
"NOT_FOUND"
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// src/cli/peers.ts
|
|
1118
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
1119
|
+
import { join as join3 } from "path";
|
|
1120
|
+
function getPeersPath() {
|
|
1121
|
+
return join3(getConfigDir(), "peers.json");
|
|
1122
|
+
}
|
|
1123
|
+
function loadPeers() {
|
|
1124
|
+
const peersPath = getPeersPath();
|
|
1125
|
+
if (!existsSync3(peersPath)) {
|
|
1126
|
+
return [];
|
|
1127
|
+
}
|
|
1128
|
+
try {
|
|
1129
|
+
const raw = readFileSync3(peersPath, "utf-8");
|
|
1130
|
+
return JSON.parse(raw);
|
|
1131
|
+
} catch {
|
|
1132
|
+
return [];
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
function writePeers(peers) {
|
|
1136
|
+
const dir = getConfigDir();
|
|
1137
|
+
if (!existsSync3(dir)) {
|
|
1138
|
+
mkdirSync2(dir, { recursive: true });
|
|
1139
|
+
}
|
|
1140
|
+
writeFileSync3(getPeersPath(), JSON.stringify(peers, null, 2), "utf-8");
|
|
1141
|
+
}
|
|
1142
|
+
function savePeer(peer) {
|
|
1143
|
+
const peers = loadPeers();
|
|
1144
|
+
const lowerName = peer.name.toLowerCase();
|
|
1145
|
+
const existing = peers.findIndex((p) => p.name.toLowerCase() === lowerName);
|
|
1146
|
+
if (existing >= 0) {
|
|
1147
|
+
peers[existing] = peer;
|
|
1148
|
+
} else {
|
|
1149
|
+
peers.push(peer);
|
|
1150
|
+
}
|
|
1151
|
+
writePeers(peers);
|
|
1152
|
+
}
|
|
1153
|
+
function removePeer(name) {
|
|
1154
|
+
const peers = loadPeers();
|
|
1155
|
+
const lowerName = name.toLowerCase();
|
|
1156
|
+
const filtered = peers.filter((p) => p.name.toLowerCase() !== lowerName);
|
|
1157
|
+
if (filtered.length === peers.length) {
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
writePeers(filtered);
|
|
1161
|
+
return true;
|
|
1162
|
+
}
|
|
1163
|
+
function findPeer(name) {
|
|
1164
|
+
const peers = loadPeers();
|
|
1165
|
+
const lowerName = name.toLowerCase();
|
|
1166
|
+
return peers.find((p) => p.name.toLowerCase() === lowerName) ?? null;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// src/autonomy/auto-request.ts
|
|
1170
|
+
function minMaxNormalize(values) {
|
|
1171
|
+
if (values.length === 0) return [];
|
|
1172
|
+
if (values.length === 1) return [1];
|
|
1173
|
+
const min = Math.min(...values);
|
|
1174
|
+
const max = Math.max(...values);
|
|
1175
|
+
if (max === min) {
|
|
1176
|
+
return values.map(() => 1);
|
|
1177
|
+
}
|
|
1178
|
+
return values.map((v) => (v - min) / (max - min));
|
|
1179
|
+
}
|
|
1180
|
+
function scorePeers(candidates, selfOwner) {
|
|
1181
|
+
const eligible = candidates.filter((c) => c.card.owner !== selfOwner);
|
|
1182
|
+
if (eligible.length === 0) return [];
|
|
1183
|
+
const successRates = eligible.map((c) => c.card.metadata?.success_rate ?? 0.5);
|
|
1184
|
+
const costEfficiencies = eligible.map((c) => c.cost === 0 ? 1 : 1 / c.cost);
|
|
1185
|
+
const idleRates = eligible.map((c) => {
|
|
1186
|
+
const internal = c.card._internal;
|
|
1187
|
+
const idleRate = internal?.idle_rate;
|
|
1188
|
+
return typeof idleRate === "number" ? idleRate : 1;
|
|
1189
|
+
});
|
|
1190
|
+
const normSuccess = minMaxNormalize(successRates);
|
|
1191
|
+
const normCost = minMaxNormalize(costEfficiencies);
|
|
1192
|
+
const normIdle = minMaxNormalize(idleRates);
|
|
1193
|
+
const scored = eligible.map((c, i) => ({
|
|
1194
|
+
...c,
|
|
1195
|
+
rawScore: (normSuccess[i] ?? 0) * (normCost[i] ?? 0) * (normIdle[i] ?? 0)
|
|
1196
|
+
}));
|
|
1197
|
+
scored.sort((a, b) => b.rawScore - a.rawScore);
|
|
1198
|
+
return scored;
|
|
1199
|
+
}
|
|
1200
|
+
var AutoRequestor = class {
|
|
1201
|
+
owner;
|
|
1202
|
+
registryDb;
|
|
1203
|
+
creditDb;
|
|
1204
|
+
autonomyConfig;
|
|
1205
|
+
budgetManager;
|
|
1206
|
+
/**
|
|
1207
|
+
* Creates a new AutoRequestor.
|
|
1208
|
+
*
|
|
1209
|
+
* @param opts - Configuration for this AutoRequestor instance.
|
|
1210
|
+
*/
|
|
1211
|
+
constructor(opts) {
|
|
1212
|
+
this.owner = opts.owner;
|
|
1213
|
+
this.registryDb = opts.registryDb;
|
|
1214
|
+
this.creditDb = opts.creditDb;
|
|
1215
|
+
this.autonomyConfig = opts.autonomyConfig;
|
|
1216
|
+
this.budgetManager = opts.budgetManager;
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Executes an autonomous capability request.
|
|
1220
|
+
*
|
|
1221
|
+
* Performs the full flow:
|
|
1222
|
+
* 1. Search for matching capability cards
|
|
1223
|
+
* 2. Filter self-owned and over-budget candidates
|
|
1224
|
+
* 3. Score candidates using min-max normalized composite scoring
|
|
1225
|
+
* 4. Resolve peer gateway config
|
|
1226
|
+
* 5. Check autonomy tier (Tier 3 queues to pending_requests)
|
|
1227
|
+
* 6. Check budget reserve
|
|
1228
|
+
* 7. Hold escrow
|
|
1229
|
+
* 8. Execute via peer gateway
|
|
1230
|
+
* 9. Settle or release escrow based on outcome
|
|
1231
|
+
* 10. Log audit event (for Tier 2 notifications and all failures)
|
|
1232
|
+
*
|
|
1233
|
+
* @param need - The capability need to fulfill.
|
|
1234
|
+
* @returns The result of the auto-request attempt.
|
|
1235
|
+
*/
|
|
1236
|
+
async requestWithAutonomy(need) {
|
|
1237
|
+
const cards = searchCards(this.registryDb, need.query, { online: true });
|
|
1238
|
+
const candidates = [];
|
|
1239
|
+
for (const card of cards) {
|
|
1240
|
+
const cardAsV2 = card;
|
|
1241
|
+
if (Array.isArray(cardAsV2.skills)) {
|
|
1242
|
+
for (const skill of cardAsV2.skills) {
|
|
1243
|
+
const cost = skill.pricing.credits_per_call;
|
|
1244
|
+
if (cost <= need.maxCostCredits) {
|
|
1245
|
+
candidates.push({ card, cost, skillId: skill.id });
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
} else {
|
|
1249
|
+
const cost = card.pricing.credits_per_call;
|
|
1250
|
+
if (cost <= need.maxCostCredits) {
|
|
1251
|
+
candidates.push({ card, cost, skillId: void 0 });
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
const scored = scorePeers(candidates, this.owner);
|
|
1256
|
+
if (scored.length === 0) {
|
|
1257
|
+
this.logFailure("auto_request_failed", "system", "none", 3, 0, "none", "No eligible peer found");
|
|
1258
|
+
return { status: "no_peer", reason: "No eligible peer found" };
|
|
1259
|
+
}
|
|
1260
|
+
const top = scored[0];
|
|
1261
|
+
const peerConfig = findPeer(top.card.owner);
|
|
1262
|
+
if (!peerConfig) {
|
|
1263
|
+
this.logFailure("auto_request_failed", top.card.id, top.skillId ?? "none", 3, top.cost, top.card.owner, "No gateway config for peer");
|
|
1264
|
+
return { status: "no_peer", reason: "No gateway config for peer" };
|
|
1265
|
+
}
|
|
1266
|
+
const tier = getAutonomyTier(top.cost, this.autonomyConfig);
|
|
1267
|
+
if (tier === 3) {
|
|
1268
|
+
createPendingRequest(this.registryDb, {
|
|
1269
|
+
skill_query: need.query,
|
|
1270
|
+
max_cost_credits: need.maxCostCredits,
|
|
1271
|
+
credits: top.cost,
|
|
1272
|
+
selected_peer: top.card.owner,
|
|
1273
|
+
selected_card_id: top.card.id,
|
|
1274
|
+
selected_skill_id: top.skillId,
|
|
1275
|
+
params: need.params
|
|
1276
|
+
});
|
|
1277
|
+
insertAuditEvent(this.registryDb, {
|
|
1278
|
+
type: "auto_request_pending",
|
|
1279
|
+
card_id: top.card.id,
|
|
1280
|
+
skill_id: top.skillId ?? top.card.id,
|
|
1281
|
+
tier_invoked: 3,
|
|
1282
|
+
credits: top.cost,
|
|
1283
|
+
peer: top.card.owner
|
|
1284
|
+
});
|
|
1285
|
+
return {
|
|
1286
|
+
status: "tier_blocked",
|
|
1287
|
+
reason: "Tier 3: owner approval required",
|
|
1288
|
+
peer: top.card.owner
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
if (!this.budgetManager.canSpend(top.cost)) {
|
|
1292
|
+
this.logFailure("auto_request_failed", top.card.id, top.skillId ?? "none", tier, top.cost, top.card.owner, "Budget reserve would be breached");
|
|
1293
|
+
return { status: "budget_blocked", reason: "Insufficient credits \u2014 reserve floor would be breached" };
|
|
1294
|
+
}
|
|
1295
|
+
const escrowId = holdEscrow(this.creditDb, this.owner, top.cost, top.card.id);
|
|
1296
|
+
try {
|
|
1297
|
+
const execResult = await requestCapability({
|
|
1298
|
+
gatewayUrl: peerConfig.url,
|
|
1299
|
+
token: peerConfig.token,
|
|
1300
|
+
cardId: top.card.id,
|
|
1301
|
+
params: top.skillId ? { skill_id: top.skillId, ...need.params } : need.params
|
|
1302
|
+
});
|
|
1303
|
+
settleEscrow(this.creditDb, escrowId, top.card.owner);
|
|
1304
|
+
if (tier === 2) {
|
|
1305
|
+
insertAuditEvent(this.registryDb, {
|
|
1306
|
+
type: "auto_request_notify",
|
|
1307
|
+
card_id: top.card.id,
|
|
1308
|
+
skill_id: top.skillId ?? top.card.id,
|
|
1309
|
+
tier_invoked: 2,
|
|
1310
|
+
credits: top.cost,
|
|
1311
|
+
peer: top.card.owner
|
|
1312
|
+
});
|
|
1313
|
+
} else {
|
|
1314
|
+
insertAuditEvent(this.registryDb, {
|
|
1315
|
+
type: "auto_request",
|
|
1316
|
+
card_id: top.card.id,
|
|
1317
|
+
skill_id: top.skillId ?? top.card.id,
|
|
1318
|
+
tier_invoked: 1,
|
|
1319
|
+
credits: top.cost,
|
|
1320
|
+
peer: top.card.owner
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
return {
|
|
1324
|
+
status: "success",
|
|
1325
|
+
result: execResult,
|
|
1326
|
+
escrowId,
|
|
1327
|
+
peer: top.card.owner,
|
|
1328
|
+
creditsSpent: top.cost
|
|
1329
|
+
};
|
|
1330
|
+
} catch (err) {
|
|
1331
|
+
releaseEscrow(this.creditDb, escrowId);
|
|
1332
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1333
|
+
this.logFailure("auto_request_failed", top.card.id, top.skillId ?? "none", tier, top.cost, top.card.owner, `Execution failed: ${reason}`);
|
|
1334
|
+
return {
|
|
1335
|
+
status: "failed",
|
|
1336
|
+
reason: `Execution failed: ${reason}`,
|
|
1337
|
+
peer: top.card.owner
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Logs a failure audit event to request_log.
|
|
1343
|
+
* Used for all non-success paths to satisfy REQ-06.
|
|
1344
|
+
*/
|
|
1345
|
+
logFailure(type, cardId, skillId, tier, credits, peer, reason) {
|
|
1346
|
+
insertAuditEvent(this.registryDb, {
|
|
1347
|
+
type,
|
|
1348
|
+
card_id: cardId,
|
|
1349
|
+
skill_id: skillId,
|
|
1350
|
+
tier_invoked: tier,
|
|
1351
|
+
credits,
|
|
1352
|
+
peer,
|
|
1353
|
+
reason
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
|
|
1358
|
+
// src/cli/remote-registry.ts
|
|
1359
|
+
var RegistryTimeoutError = class extends AgentBnBError {
|
|
1360
|
+
constructor(url) {
|
|
1361
|
+
super(
|
|
1362
|
+
`Registry at ${url} did not respond within 5s. Showing local results only.`,
|
|
1363
|
+
"REGISTRY_TIMEOUT"
|
|
1364
|
+
);
|
|
1365
|
+
this.name = "RegistryTimeoutError";
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
var RegistryConnectionError = class extends AgentBnBError {
|
|
1369
|
+
constructor(url) {
|
|
1370
|
+
super(
|
|
1371
|
+
`Cannot reach ${url}. Is the registry running? Showing local results only.`,
|
|
1372
|
+
"REGISTRY_CONNECTION"
|
|
1373
|
+
);
|
|
1374
|
+
this.name = "RegistryConnectionError";
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
var RegistryAuthError = class extends AgentBnBError {
|
|
1378
|
+
constructor(url) {
|
|
1379
|
+
super(
|
|
1380
|
+
`Authentication failed for ${url}. Run \`agentbnb config set token <your-token>\`.`,
|
|
1381
|
+
"REGISTRY_AUTH"
|
|
1382
|
+
);
|
|
1383
|
+
this.name = "RegistryAuthError";
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
async function fetchRemoteCards(registryUrl, params, timeoutMs = 5e3) {
|
|
1387
|
+
let cardsUrl;
|
|
1388
|
+
try {
|
|
1389
|
+
cardsUrl = new URL("/cards", registryUrl);
|
|
1390
|
+
} catch {
|
|
1391
|
+
throw new AgentBnBError(`Invalid registry URL: ${registryUrl}`, "INVALID_REGISTRY_URL");
|
|
1392
|
+
}
|
|
1393
|
+
const searchParams = new URLSearchParams();
|
|
1394
|
+
if (params.q !== void 0) searchParams.set("q", params.q);
|
|
1395
|
+
if (params.level !== void 0) searchParams.set("level", String(params.level));
|
|
1396
|
+
if (params.online !== void 0) searchParams.set("online", String(params.online));
|
|
1397
|
+
if (params.tag !== void 0) searchParams.set("tag", params.tag);
|
|
1398
|
+
searchParams.set("limit", "100");
|
|
1399
|
+
cardsUrl.search = searchParams.toString();
|
|
1400
|
+
const controller = new AbortController();
|
|
1401
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1402
|
+
let response;
|
|
1403
|
+
try {
|
|
1404
|
+
response = await fetch(cardsUrl.toString(), { signal: controller.signal });
|
|
1405
|
+
} catch (err) {
|
|
1406
|
+
clearTimeout(timer);
|
|
1407
|
+
const isTimeout = err instanceof Error && err.name === "AbortError";
|
|
1408
|
+
if (isTimeout) {
|
|
1409
|
+
throw new RegistryTimeoutError(registryUrl);
|
|
1410
|
+
}
|
|
1411
|
+
throw new RegistryConnectionError(registryUrl);
|
|
1412
|
+
} finally {
|
|
1413
|
+
clearTimeout(timer);
|
|
1414
|
+
}
|
|
1415
|
+
if (response.status === 401 || response.status === 403) {
|
|
1416
|
+
throw new RegistryAuthError(registryUrl);
|
|
1417
|
+
}
|
|
1418
|
+
if (!response.ok) {
|
|
1419
|
+
throw new RegistryConnectionError(registryUrl);
|
|
1420
|
+
}
|
|
1421
|
+
const body = await response.json();
|
|
1422
|
+
return body.items;
|
|
1423
|
+
}
|
|
1424
|
+
function mergeResults(localCards, remoteCards, hasQuery) {
|
|
1425
|
+
const taggedLocal = localCards.map((c) => ({ ...c, source: "local" }));
|
|
1426
|
+
const taggedRemote = remoteCards.map((c) => ({ ...c, source: "remote" }));
|
|
1427
|
+
const localIds = new Set(localCards.map((c) => c.id));
|
|
1428
|
+
const dedupedRemote = taggedRemote.filter((c) => !localIds.has(c.id));
|
|
1429
|
+
if (!hasQuery) {
|
|
1430
|
+
return [...taggedLocal, ...dedupedRemote];
|
|
1431
|
+
}
|
|
1432
|
+
const result = [];
|
|
1433
|
+
const maxLen = Math.max(taggedLocal.length, dedupedRemote.length);
|
|
1434
|
+
for (let i = 0; i < maxLen; i++) {
|
|
1435
|
+
if (i < taggedLocal.length) result.push(taggedLocal[i]);
|
|
1436
|
+
if (i < dedupedRemote.length) result.push(dedupedRemote[i]);
|
|
1437
|
+
}
|
|
1438
|
+
return result;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// src/cli/onboarding.ts
|
|
1442
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
1443
|
+
import { createConnection } from "net";
|
|
1444
|
+
var KNOWN_API_KEYS = [
|
|
1445
|
+
"OPENAI_API_KEY",
|
|
1446
|
+
"ANTHROPIC_API_KEY",
|
|
1447
|
+
"ELEVENLABS_API_KEY",
|
|
1448
|
+
"KLING_API_KEY",
|
|
1449
|
+
"STABILITY_API_KEY",
|
|
1450
|
+
"REPLICATE_API_TOKEN",
|
|
1451
|
+
"GOOGLE_API_KEY",
|
|
1452
|
+
"AZURE_OPENAI_API_KEY",
|
|
1453
|
+
"COHERE_API_KEY",
|
|
1454
|
+
"MISTRAL_API_KEY"
|
|
1455
|
+
];
|
|
1456
|
+
var API_TEMPLATES = {
|
|
1457
|
+
OPENAI_API_KEY: {
|
|
1458
|
+
name: "OpenAI Text Generation",
|
|
1459
|
+
description: "Text completion and chat via OpenAI API",
|
|
1460
|
+
level: 1,
|
|
1461
|
+
inputs: [{ name: "prompt", type: "text", required: true }],
|
|
1462
|
+
outputs: [{ name: "completion", type: "text", required: true }],
|
|
1463
|
+
pricing: { credits_per_call: 5 },
|
|
1464
|
+
powered_by: [{ provider: "OpenAI", model: "GPT-4o" }],
|
|
1465
|
+
metadata: { apis_used: ["openai"], tags: ["llm", "text", "generation"] }
|
|
1466
|
+
},
|
|
1467
|
+
ANTHROPIC_API_KEY: {
|
|
1468
|
+
name: "Anthropic Claude",
|
|
1469
|
+
description: "Text reasoning and analysis via Anthropic Claude API",
|
|
1470
|
+
level: 1,
|
|
1471
|
+
inputs: [{ name: "prompt", type: "text", required: true }],
|
|
1472
|
+
outputs: [{ name: "response", type: "text", required: true }],
|
|
1473
|
+
pricing: { credits_per_call: 5 },
|
|
1474
|
+
powered_by: [{ provider: "Anthropic", model: "Claude" }],
|
|
1475
|
+
metadata: { apis_used: ["anthropic"], tags: ["llm", "text", "reasoning"] }
|
|
1476
|
+
},
|
|
1477
|
+
ELEVENLABS_API_KEY: {
|
|
1478
|
+
name: "ElevenLabs Text-to-Speech",
|
|
1479
|
+
description: "High-quality voice synthesis via ElevenLabs API",
|
|
1480
|
+
level: 1,
|
|
1481
|
+
inputs: [{ name: "text", type: "text", required: true }],
|
|
1482
|
+
outputs: [{ name: "audio", type: "audio", required: true }],
|
|
1483
|
+
pricing: { credits_per_call: 10 },
|
|
1484
|
+
powered_by: [{ provider: "ElevenLabs" }],
|
|
1485
|
+
metadata: { apis_used: ["elevenlabs"], tags: ["tts", "audio", "voice"] }
|
|
1486
|
+
},
|
|
1487
|
+
KLING_API_KEY: {
|
|
1488
|
+
name: "Kling Video Generation",
|
|
1489
|
+
description: "AI video generation via Kling API",
|
|
1490
|
+
level: 1,
|
|
1491
|
+
inputs: [{ name: "prompt", type: "text", required: true }],
|
|
1492
|
+
outputs: [{ name: "video", type: "video", required: true }],
|
|
1493
|
+
pricing: { credits_per_call: 50 },
|
|
1494
|
+
powered_by: [{ provider: "Kling", model: "v1.5" }],
|
|
1495
|
+
metadata: { apis_used: ["kling"], tags: ["video", "generation", "ai"] }
|
|
1496
|
+
},
|
|
1497
|
+
STABILITY_API_KEY: {
|
|
1498
|
+
name: "Stability AI Image Generation",
|
|
1499
|
+
description: "Image generation via Stability AI (Stable Diffusion)",
|
|
1500
|
+
level: 1,
|
|
1501
|
+
inputs: [{ name: "prompt", type: "text", required: true }],
|
|
1502
|
+
outputs: [{ name: "image", type: "image", required: true }],
|
|
1503
|
+
pricing: { credits_per_call: 8 },
|
|
1504
|
+
powered_by: [{ provider: "Stability AI", model: "SDXL" }],
|
|
1505
|
+
metadata: { apis_used: ["stability"], tags: ["image", "generation", "diffusion"] }
|
|
1506
|
+
},
|
|
1507
|
+
REPLICATE_API_TOKEN: {
|
|
1508
|
+
name: "Replicate Model Runner",
|
|
1509
|
+
description: "Run open-source models via Replicate API",
|
|
1510
|
+
level: 1,
|
|
1511
|
+
inputs: [{ name: "input", type: "json", required: true }],
|
|
1512
|
+
outputs: [{ name: "output", type: "json", required: true }],
|
|
1513
|
+
pricing: { credits_per_call: 10 },
|
|
1514
|
+
powered_by: [{ provider: "Replicate" }],
|
|
1515
|
+
metadata: { apis_used: ["replicate"], tags: ["ml", "inference"] }
|
|
1516
|
+
},
|
|
1517
|
+
GOOGLE_API_KEY: {
|
|
1518
|
+
name: "Google AI (Gemini)",
|
|
1519
|
+
description: "Multimodal AI via Google Gemini API",
|
|
1520
|
+
level: 1,
|
|
1521
|
+
inputs: [{ name: "prompt", type: "text", required: true }],
|
|
1522
|
+
outputs: [{ name: "response", type: "text", required: true }],
|
|
1523
|
+
pricing: { credits_per_call: 5 },
|
|
1524
|
+
powered_by: [{ provider: "Google", model: "Gemini" }],
|
|
1525
|
+
metadata: { apis_used: ["google"], tags: ["llm", "multimodal", "text"] }
|
|
1526
|
+
},
|
|
1527
|
+
AZURE_OPENAI_API_KEY: {
|
|
1528
|
+
name: "Azure OpenAI Service",
|
|
1529
|
+
description: "OpenAI models hosted on Azure cloud",
|
|
1530
|
+
level: 1,
|
|
1531
|
+
inputs: [{ name: "prompt", type: "text", required: true }],
|
|
1532
|
+
outputs: [{ name: "completion", type: "text", required: true }],
|
|
1533
|
+
pricing: { credits_per_call: 5 },
|
|
1534
|
+
powered_by: [{ provider: "Azure OpenAI" }],
|
|
1535
|
+
metadata: { apis_used: ["azure-openai"], tags: ["llm", "text", "azure"] }
|
|
1536
|
+
},
|
|
1537
|
+
COHERE_API_KEY: {
|
|
1538
|
+
name: "Cohere Language AI",
|
|
1539
|
+
description: "Text generation and embeddings via Cohere API",
|
|
1540
|
+
level: 1,
|
|
1541
|
+
inputs: [{ name: "text", type: "text", required: true }],
|
|
1542
|
+
outputs: [{ name: "response", type: "text", required: true }],
|
|
1543
|
+
pricing: { credits_per_call: 3 },
|
|
1544
|
+
powered_by: [{ provider: "Cohere" }],
|
|
1545
|
+
metadata: { apis_used: ["cohere"], tags: ["llm", "embeddings", "text"] }
|
|
1546
|
+
},
|
|
1547
|
+
MISTRAL_API_KEY: {
|
|
1548
|
+
name: "Mistral AI",
|
|
1549
|
+
description: "Text generation via Mistral AI API",
|
|
1550
|
+
level: 1,
|
|
1551
|
+
inputs: [{ name: "prompt", type: "text", required: true }],
|
|
1552
|
+
outputs: [{ name: "response", type: "text", required: true }],
|
|
1553
|
+
pricing: { credits_per_call: 4 },
|
|
1554
|
+
powered_by: [{ provider: "Mistral" }],
|
|
1555
|
+
metadata: { apis_used: ["mistral"], tags: ["llm", "text", "generation"] }
|
|
1556
|
+
}
|
|
1557
|
+
};
|
|
1558
|
+
function detectApiKeys(knownKeys) {
|
|
1559
|
+
return knownKeys.filter((key) => key in process.env);
|
|
1560
|
+
}
|
|
1561
|
+
async function isPortOpen(port, host = "127.0.0.1", timeoutMs = 300) {
|
|
1562
|
+
return new Promise((resolve) => {
|
|
1563
|
+
const socket = createConnection({ port, host });
|
|
1564
|
+
const timer = setTimeout(() => {
|
|
1565
|
+
socket.destroy();
|
|
1566
|
+
resolve(false);
|
|
1567
|
+
}, timeoutMs);
|
|
1568
|
+
socket.on("connect", () => {
|
|
1569
|
+
clearTimeout(timer);
|
|
1570
|
+
socket.destroy();
|
|
1571
|
+
resolve(true);
|
|
1572
|
+
});
|
|
1573
|
+
socket.on("error", () => {
|
|
1574
|
+
clearTimeout(timer);
|
|
1575
|
+
resolve(false);
|
|
1576
|
+
});
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
async function detectOpenPorts(ports) {
|
|
1580
|
+
const results = await Promise.all(
|
|
1581
|
+
ports.map(async (port) => ({ port, open: await isPortOpen(port) }))
|
|
1582
|
+
);
|
|
1583
|
+
return results.filter((r) => r.open).map((r) => r.port);
|
|
1584
|
+
}
|
|
1585
|
+
function buildDraftCard(apiKey, owner) {
|
|
1586
|
+
const template = API_TEMPLATES[apiKey];
|
|
1587
|
+
if (!template) return null;
|
|
1588
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1589
|
+
return {
|
|
1590
|
+
spec_version: "1.0",
|
|
1591
|
+
id: randomUUID6(),
|
|
1592
|
+
owner,
|
|
1593
|
+
name: template.name,
|
|
1594
|
+
description: template.description,
|
|
1595
|
+
level: template.level,
|
|
1596
|
+
inputs: template.inputs,
|
|
1597
|
+
outputs: template.outputs,
|
|
1598
|
+
pricing: template.pricing,
|
|
1599
|
+
availability: { online: true },
|
|
1600
|
+
powered_by: template.powered_by,
|
|
1601
|
+
metadata: {
|
|
1602
|
+
apis_used: template.metadata.apis_used,
|
|
1603
|
+
tags: template.metadata.tags
|
|
1604
|
+
},
|
|
1605
|
+
created_at: now,
|
|
1606
|
+
updated_at: now
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// src/runtime/agent-runtime.ts
|
|
1611
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
1612
|
+
|
|
1613
|
+
// src/skills/executor.ts
|
|
1614
|
+
var SkillExecutor = class {
|
|
1615
|
+
skillMap;
|
|
1616
|
+
modeMap;
|
|
1617
|
+
/**
|
|
1618
|
+
* @param configs - Parsed SkillConfig array (from parseSkillsFile).
|
|
1619
|
+
* @param modes - Map from skill type string to its executor implementation.
|
|
1620
|
+
*/
|
|
1621
|
+
constructor(configs, modes) {
|
|
1622
|
+
this.skillMap = new Map(configs.map((c) => [c.id, c]));
|
|
1623
|
+
this.modeMap = modes;
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Execute a skill by ID with the given input parameters.
|
|
1627
|
+
*
|
|
1628
|
+
* Dispatch order:
|
|
1629
|
+
* 1. Look up skill config by skillId.
|
|
1630
|
+
* 2. Find executor mode by config.type.
|
|
1631
|
+
* 3. Invoke mode.execute(), wrap with latency timing.
|
|
1632
|
+
* 4. Catch any thrown errors and return as ExecutionResult with success:false.
|
|
1633
|
+
*
|
|
1634
|
+
* @param skillId - The ID of the skill to execute.
|
|
1635
|
+
* @param params - Input parameters for the skill.
|
|
1636
|
+
* @returns ExecutionResult including success, result/error, and latency_ms.
|
|
1637
|
+
*/
|
|
1638
|
+
async execute(skillId, params) {
|
|
1639
|
+
const startTime = Date.now();
|
|
1640
|
+
const config = this.skillMap.get(skillId);
|
|
1641
|
+
if (!config) {
|
|
1642
|
+
return {
|
|
1643
|
+
success: false,
|
|
1644
|
+
error: `Skill not found: "${skillId}"`,
|
|
1645
|
+
latency_ms: Date.now() - startTime
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
const mode = this.modeMap.get(config.type);
|
|
1649
|
+
if (!mode) {
|
|
1650
|
+
return {
|
|
1651
|
+
success: false,
|
|
1652
|
+
error: `No executor registered for skill type "${config.type}" (skill: "${skillId}")`,
|
|
1653
|
+
latency_ms: Date.now() - startTime
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
try {
|
|
1657
|
+
const modeResult = await mode.execute(config, params);
|
|
1658
|
+
return {
|
|
1659
|
+
...modeResult,
|
|
1660
|
+
latency_ms: Date.now() - startTime
|
|
1661
|
+
};
|
|
1662
|
+
} catch (err) {
|
|
1663
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1664
|
+
return {
|
|
1665
|
+
success: false,
|
|
1666
|
+
error: message,
|
|
1667
|
+
latency_ms: Date.now() - startTime
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Returns the IDs of all registered skills.
|
|
1673
|
+
*
|
|
1674
|
+
* @returns Array of skill ID strings.
|
|
1675
|
+
*/
|
|
1676
|
+
listSkills() {
|
|
1677
|
+
return Array.from(this.skillMap.keys());
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Returns the SkillConfig for a given skill ID, or undefined if not found.
|
|
1681
|
+
*
|
|
1682
|
+
* @param skillId - The skill ID to look up.
|
|
1683
|
+
* @returns The SkillConfig or undefined.
|
|
1684
|
+
*/
|
|
1685
|
+
getSkillConfig(skillId) {
|
|
1686
|
+
return this.skillMap.get(skillId);
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
function createSkillExecutor(configs, modes) {
|
|
1690
|
+
return new SkillExecutor(configs, modes);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// src/skills/skill-config.ts
|
|
1694
|
+
import { z as z2 } from "zod";
|
|
1695
|
+
import yaml from "js-yaml";
|
|
1696
|
+
var PricingSchema = z2.object({
|
|
1697
|
+
credits_per_call: z2.number().nonnegative(),
|
|
1698
|
+
credits_per_minute: z2.number().nonnegative().optional(),
|
|
1699
|
+
free_tier: z2.number().nonnegative().optional()
|
|
1700
|
+
});
|
|
1701
|
+
var ApiAuthSchema = z2.discriminatedUnion("type", [
|
|
1702
|
+
z2.object({
|
|
1703
|
+
type: z2.literal("bearer"),
|
|
1704
|
+
token: z2.string()
|
|
1705
|
+
}),
|
|
1706
|
+
z2.object({
|
|
1707
|
+
type: z2.literal("apikey"),
|
|
1708
|
+
header: z2.string().default("X-API-Key"),
|
|
1709
|
+
key: z2.string()
|
|
1710
|
+
}),
|
|
1711
|
+
z2.object({
|
|
1712
|
+
type: z2.literal("basic"),
|
|
1713
|
+
username: z2.string(),
|
|
1714
|
+
password: z2.string()
|
|
1715
|
+
})
|
|
1716
|
+
]);
|
|
1717
|
+
var ApiSkillConfigSchema = z2.object({
|
|
1718
|
+
id: z2.string().min(1),
|
|
1719
|
+
type: z2.literal("api"),
|
|
1720
|
+
name: z2.string().min(1),
|
|
1721
|
+
endpoint: z2.string().min(1),
|
|
1722
|
+
method: z2.enum(["GET", "POST", "PUT", "DELETE"]),
|
|
1723
|
+
auth: ApiAuthSchema.optional(),
|
|
1724
|
+
input_mapping: z2.record(z2.string()).default({}),
|
|
1725
|
+
output_mapping: z2.record(z2.string()).default({}),
|
|
1726
|
+
pricing: PricingSchema,
|
|
1727
|
+
timeout_ms: z2.number().positive().default(3e4),
|
|
1728
|
+
retries: z2.number().nonnegative().int().default(0),
|
|
1729
|
+
provider: z2.string().optional()
|
|
1730
|
+
});
|
|
1731
|
+
var PipelineStepSchema = z2.union([
|
|
1732
|
+
z2.object({
|
|
1733
|
+
skill_id: z2.string().min(1),
|
|
1734
|
+
input_mapping: z2.record(z2.string()).default({})
|
|
1735
|
+
}),
|
|
1736
|
+
z2.object({
|
|
1737
|
+
command: z2.string().min(1),
|
|
1738
|
+
input_mapping: z2.record(z2.string()).default({})
|
|
1739
|
+
})
|
|
1740
|
+
]);
|
|
1741
|
+
var PipelineSkillConfigSchema = z2.object({
|
|
1742
|
+
id: z2.string().min(1),
|
|
1743
|
+
type: z2.literal("pipeline"),
|
|
1744
|
+
name: z2.string().min(1),
|
|
1745
|
+
steps: z2.array(PipelineStepSchema).min(1),
|
|
1746
|
+
pricing: PricingSchema,
|
|
1747
|
+
timeout_ms: z2.number().positive().optional()
|
|
1748
|
+
});
|
|
1749
|
+
var OpenClawSkillConfigSchema = z2.object({
|
|
1750
|
+
id: z2.string().min(1),
|
|
1751
|
+
type: z2.literal("openclaw"),
|
|
1752
|
+
name: z2.string().min(1),
|
|
1753
|
+
agent_name: z2.string().min(1),
|
|
1754
|
+
channel: z2.enum(["telegram", "webhook", "process"]),
|
|
1755
|
+
pricing: PricingSchema,
|
|
1756
|
+
timeout_ms: z2.number().positive().optional()
|
|
1757
|
+
});
|
|
1758
|
+
var CommandSkillConfigSchema = z2.object({
|
|
1759
|
+
id: z2.string().min(1),
|
|
1760
|
+
type: z2.literal("command"),
|
|
1761
|
+
name: z2.string().min(1),
|
|
1762
|
+
command: z2.string().min(1),
|
|
1763
|
+
output_type: z2.enum(["json", "text", "file"]),
|
|
1764
|
+
allowed_commands: z2.array(z2.string()).optional(),
|
|
1765
|
+
working_dir: z2.string().optional(),
|
|
1766
|
+
timeout_ms: z2.number().positive().default(3e4),
|
|
1767
|
+
pricing: PricingSchema
|
|
1768
|
+
});
|
|
1769
|
+
var SkillConfigSchema = z2.discriminatedUnion("type", [
|
|
1770
|
+
ApiSkillConfigSchema,
|
|
1771
|
+
PipelineSkillConfigSchema,
|
|
1772
|
+
OpenClawSkillConfigSchema,
|
|
1773
|
+
CommandSkillConfigSchema
|
|
1774
|
+
]);
|
|
1775
|
+
var SkillsFileSchema = z2.object({
|
|
1776
|
+
skills: z2.array(SkillConfigSchema)
|
|
1777
|
+
});
|
|
1778
|
+
function expandEnvVars(value) {
|
|
1779
|
+
return value.replace(/\$\{([^}]+)\}/g, (_match, varName) => {
|
|
1780
|
+
const envValue = process.env[varName];
|
|
1781
|
+
if (envValue === void 0) {
|
|
1782
|
+
throw new Error(`Environment variable "${varName}" is not defined`);
|
|
1783
|
+
}
|
|
1784
|
+
return envValue;
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
function expandEnvVarsDeep(value) {
|
|
1788
|
+
if (typeof value === "string") {
|
|
1789
|
+
return expandEnvVars(value);
|
|
1790
|
+
}
|
|
1791
|
+
if (Array.isArray(value)) {
|
|
1792
|
+
return value.map(expandEnvVarsDeep);
|
|
1793
|
+
}
|
|
1794
|
+
if (value !== null && typeof value === "object") {
|
|
1795
|
+
const result = {};
|
|
1796
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1797
|
+
result[k] = expandEnvVarsDeep(v);
|
|
1798
|
+
}
|
|
1799
|
+
return result;
|
|
1800
|
+
}
|
|
1801
|
+
return value;
|
|
1802
|
+
}
|
|
1803
|
+
function parseSkillsFile(yamlContent) {
|
|
1804
|
+
const raw = yaml.load(yamlContent);
|
|
1805
|
+
const expanded = expandEnvVarsDeep(raw);
|
|
1806
|
+
const result = SkillsFileSchema.parse(expanded);
|
|
1807
|
+
return result.skills;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// src/skills/api-executor.ts
|
|
1811
|
+
function parseMappingTarget(mapping) {
|
|
1812
|
+
const dotIndex = mapping.indexOf(".");
|
|
1813
|
+
if (dotIndex < 0) {
|
|
1814
|
+
throw new Error(`Invalid input_mapping format: "${mapping}" (expected "target.key")`);
|
|
1815
|
+
}
|
|
1816
|
+
const target = mapping.slice(0, dotIndex);
|
|
1817
|
+
const key = mapping.slice(dotIndex + 1);
|
|
1818
|
+
if (!["body", "query", "path", "header"].includes(target)) {
|
|
1819
|
+
throw new Error(
|
|
1820
|
+
`Invalid mapping target "${target}" in "${mapping}" (must be body|query|path|header)`
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
return { target, key };
|
|
1824
|
+
}
|
|
1825
|
+
function extractByPath(obj, dotPath) {
|
|
1826
|
+
const normalizedPath = dotPath.startsWith("response.") ? dotPath.slice("response.".length) : dotPath;
|
|
1827
|
+
const parts = normalizedPath.split(".");
|
|
1828
|
+
let current = obj;
|
|
1829
|
+
for (const part of parts) {
|
|
1830
|
+
if (current === null || typeof current !== "object") {
|
|
1831
|
+
return void 0;
|
|
1832
|
+
}
|
|
1833
|
+
current = current[part];
|
|
1834
|
+
}
|
|
1835
|
+
return current;
|
|
1836
|
+
}
|
|
1837
|
+
function buildAuthHeaders(auth) {
|
|
1838
|
+
if (!auth) return {};
|
|
1839
|
+
switch (auth.type) {
|
|
1840
|
+
case "bearer":
|
|
1841
|
+
return { Authorization: `Bearer ${auth.token}` };
|
|
1842
|
+
case "apikey":
|
|
1843
|
+
return { [auth.header]: auth.key };
|
|
1844
|
+
case "basic": {
|
|
1845
|
+
const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString("base64");
|
|
1846
|
+
return { Authorization: `Basic ${encoded}` };
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
function applyInputMapping(params, mapping) {
|
|
1851
|
+
const body = {};
|
|
1852
|
+
const query = {};
|
|
1853
|
+
const pathParams = {};
|
|
1854
|
+
const headers = {};
|
|
1855
|
+
for (const [paramName, mappingValue] of Object.entries(mapping)) {
|
|
1856
|
+
const value = params[paramName];
|
|
1857
|
+
if (value === void 0) continue;
|
|
1858
|
+
const { target, key } = parseMappingTarget(mappingValue);
|
|
1859
|
+
switch (target) {
|
|
1860
|
+
case "body":
|
|
1861
|
+
body[key] = value;
|
|
1862
|
+
break;
|
|
1863
|
+
case "query":
|
|
1864
|
+
query[key] = String(value);
|
|
1865
|
+
break;
|
|
1866
|
+
case "path":
|
|
1867
|
+
pathParams[key] = String(value);
|
|
1868
|
+
break;
|
|
1869
|
+
case "header":
|
|
1870
|
+
headers[key] = String(value);
|
|
1871
|
+
break;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
return { body, query, pathParams, headers };
|
|
1875
|
+
}
|
|
1876
|
+
function sleep(ms) {
|
|
1877
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1878
|
+
}
|
|
1879
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 503]);
|
|
1880
|
+
var ApiExecutor = class {
|
|
1881
|
+
/**
|
|
1882
|
+
* Execute an API call described by the given skill config.
|
|
1883
|
+
*
|
|
1884
|
+
* @param config - The validated SkillConfig (must be ApiSkillConfig).
|
|
1885
|
+
* @param params - Input parameters to map to the HTTP request.
|
|
1886
|
+
* @returns Partial ExecutionResult (without latency_ms — added by SkillExecutor).
|
|
1887
|
+
*/
|
|
1888
|
+
async execute(config, params) {
|
|
1889
|
+
const apiConfig = config;
|
|
1890
|
+
const { body, query, pathParams, headers: mappedHeaders } = applyInputMapping(
|
|
1891
|
+
params,
|
|
1892
|
+
apiConfig.input_mapping
|
|
1893
|
+
);
|
|
1894
|
+
let url = apiConfig.endpoint;
|
|
1895
|
+
for (const [key, value] of Object.entries(pathParams)) {
|
|
1896
|
+
url = url.replace(`{${key}}`, encodeURIComponent(value));
|
|
1897
|
+
}
|
|
1898
|
+
if (Object.keys(query).length > 0) {
|
|
1899
|
+
const qs = new URLSearchParams(query).toString();
|
|
1900
|
+
url = `${url}?${qs}`;
|
|
1901
|
+
}
|
|
1902
|
+
const authHeaders = buildAuthHeaders(apiConfig.auth);
|
|
1903
|
+
const requestHeaders = {
|
|
1904
|
+
...authHeaders,
|
|
1905
|
+
...mappedHeaders
|
|
1906
|
+
};
|
|
1907
|
+
const hasBody = ["POST", "PUT"].includes(apiConfig.method);
|
|
1908
|
+
if (hasBody) {
|
|
1909
|
+
requestHeaders["Content-Type"] = "application/json";
|
|
1910
|
+
}
|
|
1911
|
+
const maxAttempts = (apiConfig.retries ?? 0) + 1;
|
|
1912
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1913
|
+
if (attempt > 0) {
|
|
1914
|
+
await sleep(100 * Math.pow(2, attempt - 1));
|
|
1915
|
+
}
|
|
1916
|
+
const controller = new AbortController();
|
|
1917
|
+
const timeoutId = setTimeout(
|
|
1918
|
+
() => controller.abort(),
|
|
1919
|
+
apiConfig.timeout_ms ?? 3e4
|
|
1920
|
+
);
|
|
1921
|
+
let response;
|
|
1922
|
+
try {
|
|
1923
|
+
response = await fetch(url, {
|
|
1924
|
+
method: apiConfig.method,
|
|
1925
|
+
headers: requestHeaders,
|
|
1926
|
+
body: hasBody ? JSON.stringify(body) : void 0,
|
|
1927
|
+
signal: controller.signal
|
|
1928
|
+
});
|
|
1929
|
+
} catch (err) {
|
|
1930
|
+
clearTimeout(timeoutId);
|
|
1931
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1932
|
+
const isAbort = err instanceof Error && err.name === "AbortError" || message.toLowerCase().includes("abort");
|
|
1933
|
+
if (isAbort) {
|
|
1934
|
+
return { success: false, error: `Request timeout after ${apiConfig.timeout_ms}ms` };
|
|
1935
|
+
}
|
|
1936
|
+
return { success: false, error: message };
|
|
1937
|
+
} finally {
|
|
1938
|
+
clearTimeout(timeoutId);
|
|
1939
|
+
}
|
|
1940
|
+
if (!response.ok && RETRYABLE_STATUSES.has(response.status) && attempt < maxAttempts - 1) {
|
|
1941
|
+
continue;
|
|
1942
|
+
}
|
|
1943
|
+
if (!response.ok) {
|
|
1944
|
+
return {
|
|
1945
|
+
success: false,
|
|
1946
|
+
error: `HTTP ${response.status} from ${apiConfig.endpoint}`
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
const responseBody = await response.json();
|
|
1950
|
+
const outputMapping = apiConfig.output_mapping;
|
|
1951
|
+
if (Object.keys(outputMapping).length === 0) {
|
|
1952
|
+
return { success: true, result: responseBody };
|
|
1953
|
+
}
|
|
1954
|
+
const mappedOutput = {};
|
|
1955
|
+
for (const [outputKey, path] of Object.entries(outputMapping)) {
|
|
1956
|
+
mappedOutput[outputKey] = extractByPath(responseBody, path);
|
|
1957
|
+
}
|
|
1958
|
+
return { success: true, result: mappedOutput };
|
|
1959
|
+
}
|
|
1960
|
+
return { success: false, error: "Unexpected: retry loop exhausted" };
|
|
1961
|
+
}
|
|
1962
|
+
};
|
|
1963
|
+
|
|
1964
|
+
// src/skills/pipeline-executor.ts
|
|
1965
|
+
import { exec } from "child_process";
|
|
1966
|
+
import { promisify } from "util";
|
|
1967
|
+
|
|
1968
|
+
// src/utils/interpolation.ts
|
|
1969
|
+
function resolvePath(obj, path) {
|
|
1970
|
+
const segments = path.replace(/\[(\d+)\]/g, ".$1").split(".").filter((s) => s.length > 0);
|
|
1971
|
+
let current = obj;
|
|
1972
|
+
for (const segment of segments) {
|
|
1973
|
+
if (current === null || current === void 0) {
|
|
1974
|
+
return void 0;
|
|
1975
|
+
}
|
|
1976
|
+
if (typeof current !== "object") {
|
|
1977
|
+
return void 0;
|
|
1978
|
+
}
|
|
1979
|
+
current = current[segment];
|
|
1980
|
+
}
|
|
1981
|
+
return current;
|
|
1982
|
+
}
|
|
1983
|
+
function interpolate(template, context) {
|
|
1984
|
+
return template.replace(/\$\{([^}]+)\}/g, (_match, expression) => {
|
|
1985
|
+
const resolved = resolvePath(context, expression.trim());
|
|
1986
|
+
if (resolved === void 0 || resolved === null) {
|
|
1987
|
+
return "";
|
|
1988
|
+
}
|
|
1989
|
+
if (typeof resolved === "object") {
|
|
1990
|
+
return JSON.stringify(resolved);
|
|
1991
|
+
}
|
|
1992
|
+
return String(resolved);
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
function interpolateObject(obj, context) {
|
|
1996
|
+
const result = {};
|
|
1997
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1998
|
+
result[key] = interpolateValue(value, context);
|
|
1999
|
+
}
|
|
2000
|
+
return result;
|
|
2001
|
+
}
|
|
2002
|
+
function interpolateValue(value, context) {
|
|
2003
|
+
if (typeof value === "string") {
|
|
2004
|
+
return interpolate(value, context);
|
|
2005
|
+
}
|
|
2006
|
+
if (Array.isArray(value)) {
|
|
2007
|
+
return value.map((item) => interpolateValue(item, context));
|
|
2008
|
+
}
|
|
2009
|
+
if (value !== null && typeof value === "object") {
|
|
2010
|
+
return interpolateObject(value, context);
|
|
2011
|
+
}
|
|
2012
|
+
return value;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
// src/skills/pipeline-executor.ts
|
|
2016
|
+
var execAsync = promisify(exec);
|
|
2017
|
+
var PipelineExecutor = class {
|
|
2018
|
+
/**
|
|
2019
|
+
* @param skillExecutor - The parent SkillExecutor used to dispatch sub-skill calls.
|
|
2020
|
+
*/
|
|
2021
|
+
constructor(skillExecutor) {
|
|
2022
|
+
this.skillExecutor = skillExecutor;
|
|
2023
|
+
}
|
|
2024
|
+
/**
|
|
2025
|
+
* Execute a pipeline skill config sequentially.
|
|
2026
|
+
*
|
|
2027
|
+
* Algorithm:
|
|
2028
|
+
* 1. Initialise context: { params, steps: [], prev: { result: null } }
|
|
2029
|
+
* 2. For each step:
|
|
2030
|
+
* a. Resolve input_mapping keys against current context via interpolateObject.
|
|
2031
|
+
* b. If step has `skill_id`: dispatch via skillExecutor.execute(). On failure → stop.
|
|
2032
|
+
* c. If step has `command`: interpolate command string, run via exec(). On non-zero exit → stop.
|
|
2033
|
+
* d. Store step result in context.steps[i] and context.prev.
|
|
2034
|
+
* 3. Return success with final step result (or null for empty pipeline).
|
|
2035
|
+
*
|
|
2036
|
+
* @param config - The PipelineSkillConfig for this skill.
|
|
2037
|
+
* @param params - Input parameters from the caller.
|
|
2038
|
+
* @returns Partial ExecutionResult (without latency_ms — added by SkillExecutor wrapper).
|
|
2039
|
+
*/
|
|
2040
|
+
async execute(config, params) {
|
|
2041
|
+
const pipelineConfig = config;
|
|
2042
|
+
const steps = pipelineConfig.steps ?? [];
|
|
2043
|
+
if (steps.length === 0) {
|
|
2044
|
+
return { success: true, result: null };
|
|
2045
|
+
}
|
|
2046
|
+
const context = {
|
|
2047
|
+
params,
|
|
2048
|
+
steps: [],
|
|
2049
|
+
prev: { result: null }
|
|
2050
|
+
};
|
|
2051
|
+
for (let i = 0; i < steps.length; i++) {
|
|
2052
|
+
const step = steps[i];
|
|
2053
|
+
if (step === void 0) {
|
|
2054
|
+
return {
|
|
2055
|
+
success: false,
|
|
2056
|
+
error: `Step ${i} failed: step definition is undefined`
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
const resolvedInputs = interpolateObject(
|
|
2060
|
+
step.input_mapping,
|
|
2061
|
+
context
|
|
2062
|
+
);
|
|
2063
|
+
let stepResult;
|
|
2064
|
+
if ("skill_id" in step && step.skill_id) {
|
|
2065
|
+
const subResult = await this.skillExecutor.execute(
|
|
2066
|
+
step.skill_id,
|
|
2067
|
+
resolvedInputs
|
|
2068
|
+
);
|
|
2069
|
+
if (!subResult.success) {
|
|
2070
|
+
return {
|
|
2071
|
+
success: false,
|
|
2072
|
+
error: `Step ${i} failed: ${subResult.error ?? "unknown error"}`
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
stepResult = subResult.result;
|
|
2076
|
+
} else if ("command" in step && step.command) {
|
|
2077
|
+
const interpolatedCommand = interpolate(
|
|
2078
|
+
step.command,
|
|
2079
|
+
context
|
|
2080
|
+
);
|
|
2081
|
+
try {
|
|
2082
|
+
const { stdout } = await execAsync(interpolatedCommand, { timeout: 3e4 });
|
|
2083
|
+
stepResult = stdout.trim();
|
|
2084
|
+
} catch (err) {
|
|
2085
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2086
|
+
return {
|
|
2087
|
+
success: false,
|
|
2088
|
+
error: `Step ${i} failed: ${message}`
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
} else {
|
|
2092
|
+
return {
|
|
2093
|
+
success: false,
|
|
2094
|
+
error: `Step ${i} failed: step must have either "skill_id" or "command"`
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
context.steps.push({ result: stepResult });
|
|
2098
|
+
context.prev = { result: stepResult };
|
|
2099
|
+
}
|
|
2100
|
+
const lastStep = context.steps[context.steps.length - 1];
|
|
2101
|
+
return {
|
|
2102
|
+
success: true,
|
|
2103
|
+
result: lastStep !== void 0 ? lastStep.result : null
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
};
|
|
2107
|
+
|
|
2108
|
+
// src/skills/openclaw-bridge.ts
|
|
2109
|
+
import { execSync } from "child_process";
|
|
2110
|
+
var DEFAULT_BASE_URL = "http://localhost:3000";
|
|
2111
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
2112
|
+
function buildPayload(config, params) {
|
|
2113
|
+
return {
|
|
2114
|
+
task: config.name,
|
|
2115
|
+
params,
|
|
2116
|
+
source: "agentbnb",
|
|
2117
|
+
skill_id: config.id
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
async function executeWebhook(config, payload) {
|
|
2121
|
+
const baseUrl = process.env["OPENCLAW_BASE_URL"] ?? DEFAULT_BASE_URL;
|
|
2122
|
+
const url = `${baseUrl}/openclaw/${config.agent_name}/task`;
|
|
2123
|
+
const timeoutMs = config.timeout_ms ?? DEFAULT_TIMEOUT_MS;
|
|
2124
|
+
const controller = new AbortController();
|
|
2125
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2126
|
+
try {
|
|
2127
|
+
const response = await fetch(url, {
|
|
2128
|
+
method: "POST",
|
|
2129
|
+
headers: { "Content-Type": "application/json" },
|
|
2130
|
+
body: JSON.stringify(payload),
|
|
2131
|
+
signal: controller.signal
|
|
2132
|
+
});
|
|
2133
|
+
if (!response.ok) {
|
|
2134
|
+
return {
|
|
2135
|
+
success: false,
|
|
2136
|
+
error: `Webhook returned HTTP ${response.status}: ${response.statusText}`
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
const result = await response.json();
|
|
2140
|
+
return { success: true, result };
|
|
2141
|
+
} catch (err) {
|
|
2142
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
2143
|
+
return {
|
|
2144
|
+
success: false,
|
|
2145
|
+
error: `OpenClaw webhook timed out after ${timeoutMs}ms`
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2149
|
+
return { success: false, error: message };
|
|
2150
|
+
} finally {
|
|
2151
|
+
clearTimeout(timer);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
function executeProcess(config, payload) {
|
|
2155
|
+
const timeoutMs = config.timeout_ms ?? DEFAULT_TIMEOUT_MS;
|
|
2156
|
+
const inputJson = JSON.stringify(payload);
|
|
2157
|
+
const cmd = `openclaw run ${config.agent_name} --input '${inputJson}'`;
|
|
2158
|
+
try {
|
|
2159
|
+
const stdout = execSync(cmd, { timeout: timeoutMs });
|
|
2160
|
+
const text = stdout.toString().trim();
|
|
2161
|
+
const result = JSON.parse(text);
|
|
2162
|
+
return { success: true, result };
|
|
2163
|
+
} catch (err) {
|
|
2164
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2165
|
+
return { success: false, error: message };
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
async function executeTelegram(config, payload) {
|
|
2169
|
+
const token = process.env["TELEGRAM_BOT_TOKEN"];
|
|
2170
|
+
if (!token) {
|
|
2171
|
+
return {
|
|
2172
|
+
success: false,
|
|
2173
|
+
error: "TELEGRAM_BOT_TOKEN environment variable is not set"
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
const chatId = process.env["TELEGRAM_CHAT_ID"];
|
|
2177
|
+
if (!chatId) {
|
|
2178
|
+
return {
|
|
2179
|
+
success: false,
|
|
2180
|
+
error: "TELEGRAM_CHAT_ID environment variable is not set"
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
const text = `[AgentBnB] Skill: ${config.name} (${config.id})
|
|
2184
|
+
Agent: ${config.agent_name}
|
|
2185
|
+
Params: ${JSON.stringify(payload.params ?? {})}`;
|
|
2186
|
+
const url = `https://api.telegram.org/bot${token}/sendMessage`;
|
|
2187
|
+
try {
|
|
2188
|
+
await fetch(url, {
|
|
2189
|
+
method: "POST",
|
|
2190
|
+
headers: { "Content-Type": "application/json" },
|
|
2191
|
+
body: JSON.stringify({ chat_id: chatId, text })
|
|
2192
|
+
});
|
|
2193
|
+
return {
|
|
2194
|
+
success: true,
|
|
2195
|
+
result: { sent: true, channel: "telegram" }
|
|
2196
|
+
};
|
|
2197
|
+
} catch (err) {
|
|
2198
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2199
|
+
return { success: false, error: message };
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
var OpenClawBridge = class {
|
|
2203
|
+
/**
|
|
2204
|
+
* Execute a skill with the given config and input parameters.
|
|
2205
|
+
*
|
|
2206
|
+
* @param config - The SkillConfig for this skill (must be type 'openclaw').
|
|
2207
|
+
* @param params - Input parameters passed by the caller.
|
|
2208
|
+
* @returns Partial ExecutionResult without latency_ms.
|
|
2209
|
+
*/
|
|
2210
|
+
async execute(config, params) {
|
|
2211
|
+
const ocConfig = config;
|
|
2212
|
+
const payload = buildPayload(ocConfig, params);
|
|
2213
|
+
switch (ocConfig.channel) {
|
|
2214
|
+
case "webhook":
|
|
2215
|
+
return executeWebhook(ocConfig, payload);
|
|
2216
|
+
case "process":
|
|
2217
|
+
return executeProcess(ocConfig, payload);
|
|
2218
|
+
case "telegram":
|
|
2219
|
+
return executeTelegram(ocConfig, payload);
|
|
2220
|
+
default: {
|
|
2221
|
+
const unknownChannel = ocConfig.channel;
|
|
2222
|
+
return {
|
|
2223
|
+
success: false,
|
|
2224
|
+
error: `Unknown OpenClaw channel: "${String(unknownChannel)}"`
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
};
|
|
2230
|
+
|
|
2231
|
+
// src/skills/command-executor.ts
|
|
2232
|
+
import { exec as exec2 } from "child_process";
|
|
2233
|
+
function execAsync2(command, options) {
|
|
2234
|
+
return new Promise((resolve, reject) => {
|
|
2235
|
+
exec2(command, options, (error, stdout, stderr) => {
|
|
2236
|
+
const stdoutStr = typeof stdout === "string" ? stdout : stdout.toString();
|
|
2237
|
+
const stderrStr = typeof stderr === "string" ? stderr : stderr.toString();
|
|
2238
|
+
if (error) {
|
|
2239
|
+
const enriched = Object.assign(error, { stderr: stderrStr });
|
|
2240
|
+
reject(enriched);
|
|
2241
|
+
} else {
|
|
2242
|
+
resolve({ stdout: stdoutStr, stderr: stderrStr });
|
|
2243
|
+
}
|
|
2244
|
+
});
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
var CommandExecutor = class {
|
|
2248
|
+
/**
|
|
2249
|
+
* Execute a command skill with the provided parameters.
|
|
2250
|
+
*
|
|
2251
|
+
* Steps:
|
|
2252
|
+
* 1. Security check: base command must be in `allowed_commands` if set.
|
|
2253
|
+
* 2. Interpolate `config.command` using `{ params }` context.
|
|
2254
|
+
* 3. Run via `child_process.exec` with timeout and cwd.
|
|
2255
|
+
* 4. Parse stdout based on `output_type`: text | json | file.
|
|
2256
|
+
*
|
|
2257
|
+
* @param config - Validated CommandSkillConfig.
|
|
2258
|
+
* @param params - Input parameters passed by the caller.
|
|
2259
|
+
* @returns Partial ExecutionResult (without latency_ms).
|
|
2260
|
+
*/
|
|
2261
|
+
async execute(config, params) {
|
|
2262
|
+
const cmdConfig = config;
|
|
2263
|
+
const baseCommand = cmdConfig.command.trim().split(/\s+/)[0] ?? "";
|
|
2264
|
+
if (cmdConfig.allowed_commands && cmdConfig.allowed_commands.length > 0) {
|
|
2265
|
+
if (!cmdConfig.allowed_commands.includes(baseCommand)) {
|
|
2266
|
+
return {
|
|
2267
|
+
success: false,
|
|
2268
|
+
error: `Command not allowed: "${baseCommand}". Allowed: ${cmdConfig.allowed_commands.join(", ")}`
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
const interpolatedCommand = interpolate(cmdConfig.command, { params });
|
|
2273
|
+
const timeout = cmdConfig.timeout_ms ?? 3e4;
|
|
2274
|
+
const cwd = cmdConfig.working_dir ?? process.cwd();
|
|
2275
|
+
let stdout;
|
|
2276
|
+
try {
|
|
2277
|
+
const result = await execAsync2(interpolatedCommand, {
|
|
2278
|
+
timeout,
|
|
2279
|
+
cwd,
|
|
2280
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
2281
|
+
// 10 MB
|
|
2282
|
+
shell: "/bin/sh"
|
|
2283
|
+
});
|
|
2284
|
+
stdout = result.stdout;
|
|
2285
|
+
} catch (err) {
|
|
2286
|
+
if (err instanceof Error) {
|
|
2287
|
+
const message = err.message;
|
|
2288
|
+
const stderrContent = err.stderr ?? "";
|
|
2289
|
+
if (message.includes("timed out") || message.includes("ETIMEDOUT") || err.code === "ETIMEDOUT") {
|
|
2290
|
+
return {
|
|
2291
|
+
success: false,
|
|
2292
|
+
error: `Command timed out after ${timeout}ms`
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
return {
|
|
2296
|
+
success: false,
|
|
2297
|
+
error: stderrContent.trim() || message
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
return {
|
|
2301
|
+
success: false,
|
|
2302
|
+
error: String(err)
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
const rawOutput = stdout.trim();
|
|
2306
|
+
switch (cmdConfig.output_type) {
|
|
2307
|
+
case "text":
|
|
2308
|
+
return { success: true, result: rawOutput };
|
|
2309
|
+
case "json": {
|
|
2310
|
+
try {
|
|
2311
|
+
const parsed = JSON.parse(rawOutput);
|
|
2312
|
+
return { success: true, result: parsed };
|
|
2313
|
+
} catch {
|
|
2314
|
+
return {
|
|
2315
|
+
success: false,
|
|
2316
|
+
error: `Failed to parse JSON output: ${rawOutput.slice(0, 100)}`
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
case "file":
|
|
2321
|
+
return { success: true, result: { file_path: rawOutput } };
|
|
2322
|
+
default:
|
|
2323
|
+
return {
|
|
2324
|
+
success: false,
|
|
2325
|
+
error: `Unknown output_type: ${String(cmdConfig.output_type)}`
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
};
|
|
2330
|
+
|
|
2331
|
+
// src/runtime/agent-runtime.ts
|
|
2332
|
+
var AgentRuntime = class {
|
|
2333
|
+
/** The registry SQLite database instance */
|
|
2334
|
+
registryDb;
|
|
2335
|
+
/** The credit SQLite database instance */
|
|
2336
|
+
creditDb;
|
|
2337
|
+
/** The agent owner identifier */
|
|
2338
|
+
owner;
|
|
2339
|
+
/** Registered background Cron jobs */
|
|
2340
|
+
jobs = [];
|
|
2341
|
+
/**
|
|
2342
|
+
* The SkillExecutor instance, populated by start() if skillsYamlPath is set.
|
|
2343
|
+
* Undefined if no skills.yaml was provided or the file does not exist.
|
|
2344
|
+
*/
|
|
2345
|
+
skillExecutor;
|
|
2346
|
+
draining = false;
|
|
2347
|
+
orphanedEscrowAgeMinutes;
|
|
2348
|
+
skillsYamlPath;
|
|
2349
|
+
/**
|
|
2350
|
+
* Creates a new AgentRuntime instance.
|
|
2351
|
+
* Opens both databases with WAL mode, foreign_keys=ON, and busy_timeout=5000.
|
|
2352
|
+
* Schema migrations are applied via openDatabase() and openCreditDb().
|
|
2353
|
+
*
|
|
2354
|
+
* @param options - Runtime configuration options.
|
|
2355
|
+
*/
|
|
2356
|
+
constructor(options) {
|
|
2357
|
+
this.owner = options.owner;
|
|
2358
|
+
this.orphanedEscrowAgeMinutes = options.orphanedEscrowAgeMinutes ?? 10;
|
|
2359
|
+
this.skillsYamlPath = options.skillsYamlPath;
|
|
2360
|
+
this.registryDb = openDatabase(options.registryDbPath);
|
|
2361
|
+
this.creditDb = openCreditDb(options.creditDbPath);
|
|
2362
|
+
this.registryDb.pragma("busy_timeout = 5000");
|
|
2363
|
+
this.creditDb.pragma("busy_timeout = 5000");
|
|
2364
|
+
}
|
|
2365
|
+
/**
|
|
2366
|
+
* Registers a Cron job to be managed by this runtime.
|
|
2367
|
+
* Registered jobs will be stopped automatically on shutdown().
|
|
2368
|
+
*
|
|
2369
|
+
* @param job - The Cron job instance to register.
|
|
2370
|
+
*/
|
|
2371
|
+
registerJob(job) {
|
|
2372
|
+
this.jobs.push(job);
|
|
2373
|
+
}
|
|
2374
|
+
/**
|
|
2375
|
+
* Starts the runtime.
|
|
2376
|
+
* Recovers orphaned escrows (held escrows older than orphanedEscrowAgeMinutes).
|
|
2377
|
+
* If skillsYamlPath is set and the file exists, initializes SkillExecutor with
|
|
2378
|
+
* all four executor modes (api, pipeline, openclaw, command).
|
|
2379
|
+
*
|
|
2380
|
+
* Call this after creating the runtime and before accepting requests.
|
|
2381
|
+
*/
|
|
2382
|
+
async start() {
|
|
2383
|
+
await this.recoverOrphanedEscrows();
|
|
2384
|
+
await this.initSkillExecutor();
|
|
2385
|
+
}
|
|
2386
|
+
/**
|
|
2387
|
+
* Initializes SkillExecutor from skills.yaml if skillsYamlPath is configured
|
|
2388
|
+
* and the file exists on disk.
|
|
2389
|
+
*
|
|
2390
|
+
* Uses a mutable Map to handle the PipelineExecutor circular dependency:
|
|
2391
|
+
* 1. Create an empty modes Map and a SkillExecutor (holds Map reference).
|
|
2392
|
+
* 2. Create PipelineExecutor passing the SkillExecutor (for sub-skill dispatch).
|
|
2393
|
+
* 3. Populate the Map with all 4 modes — SkillExecutor sees them via reference.
|
|
2394
|
+
*/
|
|
2395
|
+
async initSkillExecutor() {
|
|
2396
|
+
if (!this.skillsYamlPath || !existsSync4(this.skillsYamlPath)) {
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
const yamlContent = readFileSync4(this.skillsYamlPath, "utf8");
|
|
2400
|
+
const configs = parseSkillsFile(yamlContent);
|
|
2401
|
+
const modes = /* @__PURE__ */ new Map();
|
|
2402
|
+
const executor = createSkillExecutor(configs, modes);
|
|
2403
|
+
const pipelineExecutor = new PipelineExecutor(executor);
|
|
2404
|
+
modes.set("api", new ApiExecutor());
|
|
2405
|
+
modes.set("pipeline", pipelineExecutor);
|
|
2406
|
+
modes.set("openclaw", new OpenClawBridge());
|
|
2407
|
+
modes.set("command", new CommandExecutor());
|
|
2408
|
+
this.skillExecutor = executor;
|
|
2409
|
+
}
|
|
2410
|
+
/**
|
|
2411
|
+
* Recovers orphaned escrows by releasing them.
|
|
2412
|
+
* Orphaned escrows are 'held' escrows older than the configured age threshold.
|
|
2413
|
+
* Errors during individual release are swallowed (escrow may have settled between query and release).
|
|
2414
|
+
*/
|
|
2415
|
+
async recoverOrphanedEscrows() {
|
|
2416
|
+
const cutoff = new Date(
|
|
2417
|
+
Date.now() - this.orphanedEscrowAgeMinutes * 60 * 1e3
|
|
2418
|
+
).toISOString();
|
|
2419
|
+
const orphaned = this.creditDb.prepare(
|
|
2420
|
+
"SELECT id FROM credit_escrow WHERE status = 'held' AND created_at < ?"
|
|
2421
|
+
).all(cutoff);
|
|
2422
|
+
for (const row of orphaned) {
|
|
2423
|
+
try {
|
|
2424
|
+
releaseEscrow(this.creditDb, row.id);
|
|
2425
|
+
} catch {
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
/**
|
|
2430
|
+
* Shuts down the runtime gracefully.
|
|
2431
|
+
* Sets draining flag, stops all registered Cron jobs, and closes both databases.
|
|
2432
|
+
* Idempotent — safe to call multiple times.
|
|
2433
|
+
*/
|
|
2434
|
+
async shutdown() {
|
|
2435
|
+
if (this.draining) {
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
this.draining = true;
|
|
2439
|
+
for (const job of this.jobs) {
|
|
2440
|
+
job.stop();
|
|
2441
|
+
}
|
|
2442
|
+
try {
|
|
2443
|
+
this.registryDb.close();
|
|
2444
|
+
} catch {
|
|
2445
|
+
}
|
|
2446
|
+
try {
|
|
2447
|
+
this.creditDb.close();
|
|
2448
|
+
} catch {
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* Returns true if the runtime is shutting down or has shut down.
|
|
2453
|
+
* Background handlers should check this before processing new requests.
|
|
2454
|
+
*/
|
|
2455
|
+
get isDraining() {
|
|
2456
|
+
return this.draining;
|
|
2457
|
+
}
|
|
2458
|
+
};
|
|
2459
|
+
|
|
2460
|
+
// src/gateway/server.ts
|
|
2461
|
+
import Fastify from "fastify";
|
|
2462
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
2463
|
+
var VERSION = "0.0.1";
|
|
2464
|
+
function createGatewayServer(opts) {
|
|
2465
|
+
const {
|
|
2466
|
+
registryDb,
|
|
2467
|
+
creditDb,
|
|
2468
|
+
tokens,
|
|
2469
|
+
handlerUrl,
|
|
2470
|
+
timeoutMs = 3e4,
|
|
2471
|
+
silent = false,
|
|
2472
|
+
skillExecutor
|
|
2473
|
+
} = opts;
|
|
2474
|
+
const fastify = Fastify({ logger: !silent });
|
|
2475
|
+
const tokenSet = new Set(tokens);
|
|
2476
|
+
fastify.addHook("onRequest", async (request, reply) => {
|
|
2477
|
+
if (request.method === "GET" && request.url === "/health") return;
|
|
2478
|
+
const auth = request.headers.authorization;
|
|
2479
|
+
if (!auth || !auth.startsWith("Bearer ")) {
|
|
2480
|
+
await reply.status(401).send({
|
|
2481
|
+
jsonrpc: "2.0",
|
|
2482
|
+
id: null,
|
|
2483
|
+
error: { code: -32e3, message: "Unauthorized: missing token" }
|
|
2484
|
+
});
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
const token = auth.slice("Bearer ".length).trim();
|
|
2488
|
+
if (!tokenSet.has(token)) {
|
|
2489
|
+
await reply.status(401).send({
|
|
2490
|
+
jsonrpc: "2.0",
|
|
2491
|
+
id: null,
|
|
2492
|
+
error: { code: -32e3, message: "Unauthorized: invalid token" }
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
});
|
|
2496
|
+
fastify.get("/health", async () => {
|
|
2497
|
+
return { status: "ok", version: VERSION, uptime: process.uptime() };
|
|
2498
|
+
});
|
|
2499
|
+
fastify.post("/rpc", async (request, reply) => {
|
|
2500
|
+
const body = request.body;
|
|
2501
|
+
if (body.jsonrpc !== "2.0" || !body.method) {
|
|
2502
|
+
return reply.status(400).send({
|
|
2503
|
+
jsonrpc: "2.0",
|
|
2504
|
+
id: body.id ?? null,
|
|
2505
|
+
error: { code: -32600, message: "Invalid Request" }
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
const id = body.id ?? null;
|
|
2509
|
+
if (body.method !== "capability.execute") {
|
|
2510
|
+
return reply.send({
|
|
2511
|
+
jsonrpc: "2.0",
|
|
2512
|
+
id,
|
|
2513
|
+
error: { code: -32601, message: "Method not found" }
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2516
|
+
const params = body.params ?? {};
|
|
2517
|
+
const cardId = params.card_id;
|
|
2518
|
+
const skillId = params.skill_id;
|
|
2519
|
+
if (!cardId) {
|
|
2520
|
+
return reply.send({
|
|
2521
|
+
jsonrpc: "2.0",
|
|
2522
|
+
id,
|
|
2523
|
+
error: { code: -32602, message: "Invalid params: card_id required" }
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
const card = getCard(registryDb, cardId);
|
|
2527
|
+
if (!card) {
|
|
2528
|
+
return reply.send({
|
|
2529
|
+
jsonrpc: "2.0",
|
|
2530
|
+
id,
|
|
2531
|
+
error: { code: -32602, message: `Card not found: ${cardId}` }
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
const requester = params.requester ?? "unknown";
|
|
2535
|
+
let creditsNeeded;
|
|
2536
|
+
let cardName;
|
|
2537
|
+
let resolvedSkillId;
|
|
2538
|
+
const rawCard = card;
|
|
2539
|
+
if (Array.isArray(rawCard["skills"])) {
|
|
2540
|
+
const v2card = card;
|
|
2541
|
+
const skill = skillId ? v2card.skills.find((s) => s.id === skillId) : v2card.skills[0];
|
|
2542
|
+
if (!skill) {
|
|
2543
|
+
return reply.send({
|
|
2544
|
+
jsonrpc: "2.0",
|
|
2545
|
+
id,
|
|
2546
|
+
error: { code: -32602, message: `Skill not found: ${skillId}` }
|
|
2547
|
+
});
|
|
2548
|
+
}
|
|
2549
|
+
creditsNeeded = skill.pricing.credits_per_call;
|
|
2550
|
+
cardName = skill.name;
|
|
2551
|
+
resolvedSkillId = skill.id;
|
|
2552
|
+
} else {
|
|
2553
|
+
creditsNeeded = card.pricing.credits_per_call;
|
|
2554
|
+
cardName = card.name;
|
|
2555
|
+
}
|
|
2556
|
+
let escrowId;
|
|
2557
|
+
try {
|
|
2558
|
+
const balance = getBalance(creditDb, requester);
|
|
2559
|
+
if (balance < creditsNeeded) {
|
|
2560
|
+
return reply.send({
|
|
2561
|
+
jsonrpc: "2.0",
|
|
2562
|
+
id,
|
|
2563
|
+
error: { code: -32603, message: "Insufficient credits" }
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
escrowId = holdEscrow(creditDb, requester, creditsNeeded, cardId);
|
|
2567
|
+
} catch (err) {
|
|
2568
|
+
const msg = err instanceof AgentBnBError ? err.message : "Failed to hold escrow";
|
|
2569
|
+
return reply.send({
|
|
2570
|
+
jsonrpc: "2.0",
|
|
2571
|
+
id,
|
|
2572
|
+
error: { code: -32603, message: msg }
|
|
2573
|
+
});
|
|
2574
|
+
}
|
|
2575
|
+
const startMs = Date.now();
|
|
2576
|
+
if (skillExecutor) {
|
|
2577
|
+
const targetSkillId = resolvedSkillId ?? skillId ?? cardId;
|
|
2578
|
+
let execResult;
|
|
2579
|
+
try {
|
|
2580
|
+
execResult = await skillExecutor.execute(targetSkillId, params);
|
|
2581
|
+
} catch (err) {
|
|
2582
|
+
releaseEscrow(creditDb, escrowId);
|
|
2583
|
+
updateReputation(registryDb, cardId, false, Date.now() - startMs);
|
|
2584
|
+
try {
|
|
2585
|
+
insertRequestLog(registryDb, {
|
|
2586
|
+
id: randomUUID7(),
|
|
2587
|
+
card_id: cardId,
|
|
2588
|
+
card_name: cardName,
|
|
2589
|
+
skill_id: resolvedSkillId,
|
|
2590
|
+
requester,
|
|
2591
|
+
status: "failure",
|
|
2592
|
+
latency_ms: Date.now() - startMs,
|
|
2593
|
+
credits_charged: 0,
|
|
2594
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2595
|
+
});
|
|
2596
|
+
} catch {
|
|
2597
|
+
}
|
|
2598
|
+
const message = err instanceof Error ? err.message : "Execution error";
|
|
2599
|
+
return reply.send({
|
|
2600
|
+
jsonrpc: "2.0",
|
|
2601
|
+
id,
|
|
2602
|
+
error: { code: -32603, message }
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
if (!execResult.success) {
|
|
2606
|
+
releaseEscrow(creditDb, escrowId);
|
|
2607
|
+
updateReputation(registryDb, cardId, false, execResult.latency_ms);
|
|
2608
|
+
try {
|
|
2609
|
+
insertRequestLog(registryDb, {
|
|
2610
|
+
id: randomUUID7(),
|
|
2611
|
+
card_id: cardId,
|
|
2612
|
+
card_name: cardName,
|
|
2613
|
+
skill_id: resolvedSkillId,
|
|
2614
|
+
requester,
|
|
2615
|
+
status: "failure",
|
|
2616
|
+
latency_ms: execResult.latency_ms,
|
|
2617
|
+
credits_charged: 0,
|
|
2618
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2619
|
+
});
|
|
2620
|
+
} catch {
|
|
2621
|
+
}
|
|
2622
|
+
return reply.send({
|
|
2623
|
+
jsonrpc: "2.0",
|
|
2624
|
+
id,
|
|
2625
|
+
error: { code: -32603, message: execResult.error ?? "Execution failed" }
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
settleEscrow(creditDb, escrowId, card.owner);
|
|
2629
|
+
updateReputation(registryDb, cardId, true, execResult.latency_ms);
|
|
2630
|
+
try {
|
|
2631
|
+
insertRequestLog(registryDb, {
|
|
2632
|
+
id: randomUUID7(),
|
|
2633
|
+
card_id: cardId,
|
|
2634
|
+
card_name: cardName,
|
|
2635
|
+
skill_id: resolvedSkillId,
|
|
2636
|
+
requester,
|
|
2637
|
+
status: "success",
|
|
2638
|
+
latency_ms: execResult.latency_ms,
|
|
2639
|
+
credits_charged: creditsNeeded,
|
|
2640
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2641
|
+
});
|
|
2642
|
+
} catch {
|
|
2643
|
+
}
|
|
2644
|
+
return reply.send({ jsonrpc: "2.0", id, result: execResult.result });
|
|
2645
|
+
}
|
|
2646
|
+
const controller = new AbortController();
|
|
2647
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2648
|
+
try {
|
|
2649
|
+
const response = await fetch(handlerUrl, {
|
|
2650
|
+
method: "POST",
|
|
2651
|
+
headers: { "Content-Type": "application/json" },
|
|
2652
|
+
body: JSON.stringify({ card_id: cardId, skill_id: resolvedSkillId, params }),
|
|
2653
|
+
signal: controller.signal
|
|
2654
|
+
});
|
|
2655
|
+
clearTimeout(timer);
|
|
2656
|
+
if (!response.ok) {
|
|
2657
|
+
releaseEscrow(creditDb, escrowId);
|
|
2658
|
+
updateReputation(registryDb, cardId, false, Date.now() - startMs);
|
|
2659
|
+
try {
|
|
2660
|
+
insertRequestLog(registryDb, {
|
|
2661
|
+
id: randomUUID7(),
|
|
2662
|
+
card_id: cardId,
|
|
2663
|
+
card_name: cardName,
|
|
2664
|
+
skill_id: resolvedSkillId,
|
|
2665
|
+
requester,
|
|
2666
|
+
status: "failure",
|
|
2667
|
+
latency_ms: Date.now() - startMs,
|
|
2668
|
+
credits_charged: 0,
|
|
2669
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2670
|
+
});
|
|
2671
|
+
} catch {
|
|
2672
|
+
}
|
|
2673
|
+
return reply.send({
|
|
2674
|
+
jsonrpc: "2.0",
|
|
2675
|
+
id,
|
|
2676
|
+
error: { code: -32603, message: `Handler returned ${response.status}` }
|
|
2677
|
+
});
|
|
2678
|
+
}
|
|
2679
|
+
const result = await response.json();
|
|
2680
|
+
settleEscrow(creditDb, escrowId, card.owner);
|
|
2681
|
+
updateReputation(registryDb, cardId, true, Date.now() - startMs);
|
|
2682
|
+
try {
|
|
2683
|
+
insertRequestLog(registryDb, {
|
|
2684
|
+
id: randomUUID7(),
|
|
2685
|
+
card_id: cardId,
|
|
2686
|
+
card_name: cardName,
|
|
2687
|
+
skill_id: resolvedSkillId,
|
|
2688
|
+
requester,
|
|
2689
|
+
status: "success",
|
|
2690
|
+
latency_ms: Date.now() - startMs,
|
|
2691
|
+
credits_charged: creditsNeeded,
|
|
2692
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2693
|
+
});
|
|
2694
|
+
} catch {
|
|
2695
|
+
}
|
|
2696
|
+
return reply.send({ jsonrpc: "2.0", id, result });
|
|
2697
|
+
} catch (err) {
|
|
2698
|
+
clearTimeout(timer);
|
|
2699
|
+
releaseEscrow(creditDb, escrowId);
|
|
2700
|
+
updateReputation(registryDb, cardId, false, Date.now() - startMs);
|
|
2701
|
+
const isTimeout = err instanceof Error && err.name === "AbortError";
|
|
2702
|
+
try {
|
|
2703
|
+
insertRequestLog(registryDb, {
|
|
2704
|
+
id: randomUUID7(),
|
|
2705
|
+
card_id: cardId,
|
|
2706
|
+
card_name: cardName,
|
|
2707
|
+
skill_id: resolvedSkillId,
|
|
2708
|
+
requester,
|
|
2709
|
+
status: isTimeout ? "timeout" : "failure",
|
|
2710
|
+
latency_ms: Date.now() - startMs,
|
|
2711
|
+
credits_charged: 0,
|
|
2712
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2713
|
+
});
|
|
2714
|
+
} catch {
|
|
2715
|
+
}
|
|
2716
|
+
return reply.send({
|
|
2717
|
+
jsonrpc: "2.0",
|
|
2718
|
+
id,
|
|
2719
|
+
error: { code: -32603, message: isTimeout ? "Execution timeout" : "Handler error" }
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2722
|
+
});
|
|
2723
|
+
return fastify;
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
// src/registry/server.ts
|
|
2727
|
+
import Fastify2 from "fastify";
|
|
2728
|
+
import cors from "@fastify/cors";
|
|
2729
|
+
import fastifyStatic from "@fastify/static";
|
|
2730
|
+
import { join as join4, dirname } from "path";
|
|
2731
|
+
import { fileURLToPath } from "url";
|
|
2732
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2733
|
+
function stripInternal(card) {
|
|
2734
|
+
const { _internal: _, ...publicCard } = card;
|
|
2735
|
+
return publicCard;
|
|
2736
|
+
}
|
|
2737
|
+
function createRegistryServer(opts) {
|
|
2738
|
+
const { registryDb: db, silent = false } = opts;
|
|
2739
|
+
const server = Fastify2({ logger: !silent });
|
|
2740
|
+
void server.register(cors, {
|
|
2741
|
+
origin: true,
|
|
2742
|
+
methods: ["GET", "POST", "PATCH", "OPTIONS"],
|
|
2743
|
+
allowedHeaders: ["Content-Type", "Authorization"]
|
|
2744
|
+
});
|
|
2745
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
2746
|
+
const __dirname = dirname(__filename);
|
|
2747
|
+
const hubDistCandidates = [
|
|
2748
|
+
join4(__dirname, "../../hub/dist"),
|
|
2749
|
+
// When running from dist/registry/server.js
|
|
2750
|
+
join4(__dirname, "../../../hub/dist")
|
|
2751
|
+
// Fallback for alternative layouts
|
|
2752
|
+
];
|
|
2753
|
+
const hubDistDir = hubDistCandidates.find((p) => existsSync5(p));
|
|
2754
|
+
if (hubDistDir) {
|
|
2755
|
+
void server.register(fastifyStatic, {
|
|
2756
|
+
root: hubDistDir,
|
|
2757
|
+
prefix: "/hub/"
|
|
2758
|
+
});
|
|
2759
|
+
server.get("/hub", async (_request, reply) => {
|
|
2760
|
+
return reply.redirect("/hub/");
|
|
2761
|
+
});
|
|
2762
|
+
server.setNotFoundHandler(async (request, reply) => {
|
|
2763
|
+
if (request.url.startsWith("/hub/")) {
|
|
2764
|
+
return reply.sendFile("index.html");
|
|
2765
|
+
}
|
|
2766
|
+
return reply.code(404).send({ error: "Not found" });
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2769
|
+
server.get("/health", async (_request, reply) => {
|
|
2770
|
+
return reply.send({ status: "ok" });
|
|
2771
|
+
});
|
|
2772
|
+
server.get("/cards", async (request, reply) => {
|
|
2773
|
+
const query = request.query;
|
|
2774
|
+
const q = query.q?.trim() ?? "";
|
|
2775
|
+
const levelRaw = query.level !== void 0 ? parseInt(query.level, 10) : void 0;
|
|
2776
|
+
const level = levelRaw === 1 || levelRaw === 2 || levelRaw === 3 ? levelRaw : void 0;
|
|
2777
|
+
const onlineRaw = query.online;
|
|
2778
|
+
const online = onlineRaw === "true" ? true : onlineRaw === "false" ? false : void 0;
|
|
2779
|
+
const tag = query.tag?.trim();
|
|
2780
|
+
const minSuccessRate = query.min_success_rate !== void 0 ? parseFloat(query.min_success_rate) : void 0;
|
|
2781
|
+
const maxLatencyMs = query.max_latency_ms !== void 0 ? parseFloat(query.max_latency_ms) : void 0;
|
|
2782
|
+
const sort = query.sort;
|
|
2783
|
+
const rawLimit = query.limit !== void 0 ? parseInt(query.limit, 10) : 20;
|
|
2784
|
+
const limit = Math.min(isNaN(rawLimit) || rawLimit < 1 ? 20 : rawLimit, 100);
|
|
2785
|
+
const rawOffset = query.offset !== void 0 ? parseInt(query.offset, 10) : 0;
|
|
2786
|
+
const offset = isNaN(rawOffset) || rawOffset < 0 ? 0 : rawOffset;
|
|
2787
|
+
let cards;
|
|
2788
|
+
if (q.length > 0) {
|
|
2789
|
+
cards = searchCards(db, q, { level, online });
|
|
2790
|
+
} else {
|
|
2791
|
+
cards = filterCards(db, { level, online });
|
|
2792
|
+
}
|
|
2793
|
+
if (tag !== void 0 && tag.length > 0) {
|
|
2794
|
+
cards = cards.filter((c) => c.metadata?.tags?.includes(tag));
|
|
2795
|
+
}
|
|
2796
|
+
if (minSuccessRate !== void 0 && !isNaN(minSuccessRate)) {
|
|
2797
|
+
cards = cards.filter(
|
|
2798
|
+
(c) => (c.metadata?.success_rate ?? -1) >= minSuccessRate
|
|
2799
|
+
);
|
|
2800
|
+
}
|
|
2801
|
+
if (maxLatencyMs !== void 0 && !isNaN(maxLatencyMs)) {
|
|
2802
|
+
cards = cards.filter(
|
|
2803
|
+
(c) => (c.metadata?.avg_latency_ms ?? Infinity) <= maxLatencyMs
|
|
2804
|
+
);
|
|
2805
|
+
}
|
|
2806
|
+
if (sort === "success_rate") {
|
|
2807
|
+
cards = [...cards].sort((a, b) => {
|
|
2808
|
+
const aRate = a.metadata?.success_rate ?? -1;
|
|
2809
|
+
const bRate = b.metadata?.success_rate ?? -1;
|
|
2810
|
+
return bRate - aRate;
|
|
2811
|
+
});
|
|
2812
|
+
} else if (sort === "latency") {
|
|
2813
|
+
cards = [...cards].sort((a, b) => {
|
|
2814
|
+
const aLatency = a.metadata?.avg_latency_ms ?? Infinity;
|
|
2815
|
+
const bLatency = b.metadata?.avg_latency_ms ?? Infinity;
|
|
2816
|
+
return aLatency - bLatency;
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
const total = cards.length;
|
|
2820
|
+
const items = cards.slice(offset, offset + limit).map(stripInternal);
|
|
2821
|
+
const result = { total, limit, offset, items };
|
|
2822
|
+
return reply.send(result);
|
|
2823
|
+
});
|
|
2824
|
+
server.get("/cards/:id", async (request, reply) => {
|
|
2825
|
+
const { id } = request.params;
|
|
2826
|
+
const card = getCard(db, id);
|
|
2827
|
+
if (!card) {
|
|
2828
|
+
return reply.code(404).send({ error: "Not found" });
|
|
2829
|
+
}
|
|
2830
|
+
return reply.send(stripInternal(card));
|
|
2831
|
+
});
|
|
2832
|
+
server.get("/api/agents", async (_request, reply) => {
|
|
2833
|
+
const allCards = listCards(db);
|
|
2834
|
+
const ownerMap = /* @__PURE__ */ new Map();
|
|
2835
|
+
for (const card of allCards) {
|
|
2836
|
+
const existing = ownerMap.get(card.owner) ?? [];
|
|
2837
|
+
existing.push(card);
|
|
2838
|
+
ownerMap.set(card.owner, existing);
|
|
2839
|
+
}
|
|
2840
|
+
const creditsStmt = db.prepare(`
|
|
2841
|
+
SELECT cc.owner,
|
|
2842
|
+
SUM(CASE WHEN rl.status = 'success' THEN rl.credits_charged ELSE 0 END) as credits_earned
|
|
2843
|
+
FROM capability_cards cc
|
|
2844
|
+
LEFT JOIN request_log rl ON rl.card_id = cc.id
|
|
2845
|
+
GROUP BY cc.owner
|
|
2846
|
+
`);
|
|
2847
|
+
const creditsRows = creditsStmt.all();
|
|
2848
|
+
const creditsMap = new Map(creditsRows.map((r) => [r.owner, r.credits_earned ?? 0]));
|
|
2849
|
+
const agents = Array.from(ownerMap.entries()).map(([owner, cards]) => {
|
|
2850
|
+
const skillCount = cards.reduce((sum, card) => sum + (card.skills?.length ?? 1), 0);
|
|
2851
|
+
const successRates = cards.map((c) => c.metadata?.success_rate).filter((r) => r != null);
|
|
2852
|
+
const avgSuccessRate = successRates.length > 0 ? successRates.reduce((a, b) => a + b, 0) / successRates.length : null;
|
|
2853
|
+
const memberStmt = db.prepare(
|
|
2854
|
+
"SELECT MIN(created_at) as earliest FROM capability_cards WHERE owner = ?"
|
|
2855
|
+
);
|
|
2856
|
+
const memberRow = memberStmt.get(owner);
|
|
2857
|
+
return {
|
|
2858
|
+
owner,
|
|
2859
|
+
skill_count: skillCount,
|
|
2860
|
+
success_rate: avgSuccessRate,
|
|
2861
|
+
total_earned: creditsMap.get(owner) ?? 0,
|
|
2862
|
+
member_since: memberRow?.earliest ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2863
|
+
};
|
|
2864
|
+
});
|
|
2865
|
+
agents.sort((a, b) => {
|
|
2866
|
+
const aRate = a.success_rate ?? -1;
|
|
2867
|
+
const bRate = b.success_rate ?? -1;
|
|
2868
|
+
if (bRate !== aRate) return bRate - aRate;
|
|
2869
|
+
return b.total_earned - a.total_earned;
|
|
2870
|
+
});
|
|
2871
|
+
return reply.send({ items: agents, total: agents.length });
|
|
2872
|
+
});
|
|
2873
|
+
server.get("/api/agents/:owner", async (request, reply) => {
|
|
2874
|
+
const { owner } = request.params;
|
|
2875
|
+
const ownerCards = listCards(db, owner);
|
|
2876
|
+
if (ownerCards.length === 0) {
|
|
2877
|
+
return reply.status(404).send({ error: "Agent not found" });
|
|
2878
|
+
}
|
|
2879
|
+
const skillCount = ownerCards.reduce((sum, card) => sum + (card.skills?.length ?? 1), 0);
|
|
2880
|
+
const successRates = ownerCards.map((c) => c.metadata?.success_rate).filter((r) => r != null);
|
|
2881
|
+
const avgSuccessRate = successRates.length > 0 ? successRates.reduce((a, b) => a + b, 0) / successRates.length : null;
|
|
2882
|
+
const creditsStmt = db.prepare(`
|
|
2883
|
+
SELECT SUM(CASE WHEN rl.status = 'success' THEN rl.credits_charged ELSE 0 END) as credits_earned
|
|
2884
|
+
FROM capability_cards cc
|
|
2885
|
+
LEFT JOIN request_log rl ON rl.card_id = cc.id
|
|
2886
|
+
WHERE cc.owner = ?
|
|
2887
|
+
`);
|
|
2888
|
+
const creditsRow = creditsStmt.get(owner);
|
|
2889
|
+
const memberStmt = db.prepare(
|
|
2890
|
+
"SELECT MIN(created_at) as earliest FROM capability_cards WHERE owner = ?"
|
|
2891
|
+
);
|
|
2892
|
+
const memberRow = memberStmt.get(owner);
|
|
2893
|
+
const activityStmt = db.prepare(`
|
|
2894
|
+
SELECT rl.id, rl.card_name, rl.requester, rl.status, rl.credits_charged, rl.created_at
|
|
2895
|
+
FROM request_log rl
|
|
2896
|
+
INNER JOIN capability_cards cc ON rl.card_id = cc.id
|
|
2897
|
+
WHERE cc.owner = ?
|
|
2898
|
+
ORDER BY rl.created_at DESC
|
|
2899
|
+
LIMIT 10
|
|
2900
|
+
`);
|
|
2901
|
+
const recentActivity = activityStmt.all(owner);
|
|
2902
|
+
const profile = {
|
|
2903
|
+
owner,
|
|
2904
|
+
skill_count: skillCount,
|
|
2905
|
+
success_rate: avgSuccessRate,
|
|
2906
|
+
total_earned: creditsRow?.credits_earned ?? 0,
|
|
2907
|
+
member_since: memberRow?.earliest ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2908
|
+
};
|
|
2909
|
+
return reply.send({
|
|
2910
|
+
profile,
|
|
2911
|
+
skills: ownerCards,
|
|
2912
|
+
recent_activity: recentActivity
|
|
2913
|
+
});
|
|
2914
|
+
});
|
|
2915
|
+
server.get("/api/activity", async (request, reply) => {
|
|
2916
|
+
const query = request.query;
|
|
2917
|
+
const rawLimit = query.limit !== void 0 ? parseInt(query.limit, 10) : 20;
|
|
2918
|
+
const limit = Math.min(isNaN(rawLimit) || rawLimit < 1 ? 20 : rawLimit, 100);
|
|
2919
|
+
const since = query.since?.trim() || void 0;
|
|
2920
|
+
const items = getActivityFeed(db, limit, since);
|
|
2921
|
+
return reply.send({ items, total: items.length, limit });
|
|
2922
|
+
});
|
|
2923
|
+
if (opts.ownerApiKey && opts.ownerName) {
|
|
2924
|
+
const ownerApiKey = opts.ownerApiKey;
|
|
2925
|
+
const ownerName = opts.ownerName;
|
|
2926
|
+
void server.register(async (ownerRoutes) => {
|
|
2927
|
+
ownerRoutes.addHook("onRequest", async (request, reply) => {
|
|
2928
|
+
const auth = request.headers.authorization;
|
|
2929
|
+
const token = auth?.startsWith("Bearer ") ? auth.slice(7).trim() : null;
|
|
2930
|
+
if (!token || token !== ownerApiKey) {
|
|
2931
|
+
return reply.status(401).send({ error: "Unauthorized" });
|
|
2932
|
+
}
|
|
2933
|
+
});
|
|
2934
|
+
ownerRoutes.get("/me", async (_request, reply) => {
|
|
2935
|
+
const balance = opts.creditDb ? getBalance(opts.creditDb, ownerName) : 0;
|
|
2936
|
+
return reply.send({ owner: ownerName, balance });
|
|
2937
|
+
});
|
|
2938
|
+
ownerRoutes.get("/requests", async (request, reply) => {
|
|
2939
|
+
const query = request.query;
|
|
2940
|
+
const rawLimit = query.limit !== void 0 ? parseInt(query.limit, 10) : 10;
|
|
2941
|
+
const limit = Math.min(isNaN(rawLimit) || rawLimit < 1 ? 10 : rawLimit, 100);
|
|
2942
|
+
const sinceRaw = query.since;
|
|
2943
|
+
const validSince = ["24h", "7d", "30d"];
|
|
2944
|
+
const since = sinceRaw && validSince.includes(sinceRaw) ? sinceRaw : void 0;
|
|
2945
|
+
const items = getRequestLog(db, limit, since);
|
|
2946
|
+
return reply.send({ items, limit });
|
|
2947
|
+
});
|
|
2948
|
+
ownerRoutes.get("/draft", async (_request, reply) => {
|
|
2949
|
+
const detectedKeys = detectApiKeys(KNOWN_API_KEYS);
|
|
2950
|
+
const cards = detectedKeys.map((key) => buildDraftCard(key, ownerName)).filter((card) => card !== null);
|
|
2951
|
+
return reply.send({ cards });
|
|
2952
|
+
});
|
|
2953
|
+
ownerRoutes.post("/cards/:id/toggle-online", async (request, reply) => {
|
|
2954
|
+
const { id } = request.params;
|
|
2955
|
+
const card = getCard(db, id);
|
|
2956
|
+
if (!card) {
|
|
2957
|
+
return reply.code(404).send({ error: "Not found" });
|
|
2958
|
+
}
|
|
2959
|
+
try {
|
|
2960
|
+
const newOnline = !card.availability.online;
|
|
2961
|
+
updateCard(db, id, ownerName, {
|
|
2962
|
+
availability: { ...card.availability, online: newOnline }
|
|
2963
|
+
});
|
|
2964
|
+
return reply.send({ ok: true, online: newOnline });
|
|
2965
|
+
} catch (err) {
|
|
2966
|
+
if (err instanceof AgentBnBError && err.code === "FORBIDDEN") {
|
|
2967
|
+
return reply.code(403).send({ error: "Forbidden" });
|
|
2968
|
+
}
|
|
2969
|
+
throw err;
|
|
2970
|
+
}
|
|
2971
|
+
});
|
|
2972
|
+
ownerRoutes.patch("/cards/:id", async (request, reply) => {
|
|
2973
|
+
const { id } = request.params;
|
|
2974
|
+
const body = request.body;
|
|
2975
|
+
const updates = {};
|
|
2976
|
+
if (body.description !== void 0) updates.description = body.description;
|
|
2977
|
+
if (body.pricing !== void 0) updates.pricing = body.pricing;
|
|
2978
|
+
try {
|
|
2979
|
+
updateCard(db, id, ownerName, updates);
|
|
2980
|
+
return reply.send({ ok: true });
|
|
2981
|
+
} catch (err) {
|
|
2982
|
+
if (err instanceof AgentBnBError) {
|
|
2983
|
+
if (err.code === "FORBIDDEN") {
|
|
2984
|
+
return reply.code(403).send({ error: "Forbidden" });
|
|
2985
|
+
}
|
|
2986
|
+
if (err.code === "NOT_FOUND") {
|
|
2987
|
+
return reply.code(404).send({ error: "Not found" });
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
throw err;
|
|
2991
|
+
}
|
|
2992
|
+
});
|
|
2993
|
+
ownerRoutes.get("/me/pending-requests", async (_request, reply) => {
|
|
2994
|
+
const rows = listPendingRequests(db);
|
|
2995
|
+
return reply.send(rows);
|
|
2996
|
+
});
|
|
2997
|
+
ownerRoutes.post("/me/pending-requests/:id/approve", async (request, reply) => {
|
|
2998
|
+
const { id } = request.params;
|
|
2999
|
+
try {
|
|
3000
|
+
resolvePendingRequest(db, id, "approved");
|
|
3001
|
+
return reply.send({ status: "approved", id });
|
|
3002
|
+
} catch (err) {
|
|
3003
|
+
if (err instanceof AgentBnBError) return reply.status(404).send({ error: err.message });
|
|
3004
|
+
throw err;
|
|
3005
|
+
}
|
|
3006
|
+
});
|
|
3007
|
+
ownerRoutes.post("/me/pending-requests/:id/reject", async (request, reply) => {
|
|
3008
|
+
const { id } = request.params;
|
|
3009
|
+
try {
|
|
3010
|
+
resolvePendingRequest(db, id, "rejected");
|
|
3011
|
+
return reply.send({ status: "rejected", id });
|
|
3012
|
+
} catch (err) {
|
|
3013
|
+
if (err instanceof AgentBnBError) return reply.status(404).send({ error: err.message });
|
|
3014
|
+
throw err;
|
|
3015
|
+
}
|
|
3016
|
+
});
|
|
3017
|
+
ownerRoutes.get("/me/transactions", async (request, reply) => {
|
|
3018
|
+
const query = request.query;
|
|
3019
|
+
const rawLimit = query.limit !== void 0 ? parseInt(query.limit, 10) : 20;
|
|
3020
|
+
const limit = Math.min(isNaN(rawLimit) || rawLimit < 1 ? 20 : rawLimit, 100);
|
|
3021
|
+
if (!opts.creditDb) {
|
|
3022
|
+
return reply.send({ items: [], limit });
|
|
3023
|
+
}
|
|
3024
|
+
const items = getTransactions(opts.creditDb, ownerName, limit);
|
|
3025
|
+
return reply.send({ items, limit });
|
|
3026
|
+
});
|
|
3027
|
+
});
|
|
3028
|
+
}
|
|
3029
|
+
return server;
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
// src/discovery/mdns.ts
|
|
3033
|
+
import { Bonjour } from "bonjour-service";
|
|
3034
|
+
var bonjourInstance = null;
|
|
3035
|
+
function getBonjour() {
|
|
3036
|
+
if (bonjourInstance === null) {
|
|
3037
|
+
bonjourInstance = new Bonjour();
|
|
3038
|
+
}
|
|
3039
|
+
return bonjourInstance;
|
|
3040
|
+
}
|
|
3041
|
+
function announceGateway(owner, port, metadata) {
|
|
3042
|
+
const bonjour = getBonjour();
|
|
3043
|
+
const txt = {
|
|
3044
|
+
owner,
|
|
3045
|
+
version: "1.0",
|
|
3046
|
+
...metadata
|
|
3047
|
+
};
|
|
3048
|
+
bonjour.publish({
|
|
3049
|
+
name: owner,
|
|
3050
|
+
type: "agentbnb",
|
|
3051
|
+
port,
|
|
3052
|
+
txt
|
|
3053
|
+
});
|
|
3054
|
+
}
|
|
3055
|
+
function discoverLocalAgents(onFound, onDown) {
|
|
3056
|
+
const bonjour = getBonjour();
|
|
3057
|
+
const browser = bonjour.find({ type: "agentbnb" });
|
|
3058
|
+
browser.on("up", (service) => {
|
|
3059
|
+
const addresses = service.addresses ?? [];
|
|
3060
|
+
const ipv4Addresses = addresses.filter((addr) => !addr.includes(":"));
|
|
3061
|
+
const host = ipv4Addresses.length > 0 ? ipv4Addresses[0] : service.host;
|
|
3062
|
+
const url = `http://${host}:${service.port}`;
|
|
3063
|
+
const owner = service.txt?.owner ?? service.name;
|
|
3064
|
+
onFound({
|
|
3065
|
+
name: service.name,
|
|
3066
|
+
url,
|
|
3067
|
+
owner
|
|
3068
|
+
});
|
|
3069
|
+
});
|
|
3070
|
+
if (onDown) {
|
|
3071
|
+
browser.on("down", (service) => {
|
|
3072
|
+
const addresses = service.addresses ?? [];
|
|
3073
|
+
const ipv4Addresses = addresses.filter((addr) => !addr.includes(":"));
|
|
3074
|
+
const host = ipv4Addresses.length > 0 ? ipv4Addresses[0] : service.host;
|
|
3075
|
+
const url = `http://${host}:${service.port}`;
|
|
3076
|
+
const owner = service.txt?.owner ?? service.name;
|
|
3077
|
+
onDown({
|
|
3078
|
+
name: service.name,
|
|
3079
|
+
url,
|
|
3080
|
+
owner
|
|
3081
|
+
});
|
|
3082
|
+
});
|
|
3083
|
+
}
|
|
3084
|
+
return {
|
|
3085
|
+
stop: () => browser.stop()
|
|
3086
|
+
};
|
|
3087
|
+
}
|
|
3088
|
+
async function stopAnnouncement() {
|
|
3089
|
+
if (bonjourInstance === null) {
|
|
3090
|
+
return;
|
|
3091
|
+
}
|
|
3092
|
+
const instance = bonjourInstance;
|
|
3093
|
+
bonjourInstance = null;
|
|
3094
|
+
await new Promise((resolve) => {
|
|
3095
|
+
instance.unpublishAll(() => {
|
|
3096
|
+
instance.destroy();
|
|
3097
|
+
resolve();
|
|
3098
|
+
});
|
|
3099
|
+
});
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
// src/openclaw/soul-sync.ts
|
|
3103
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
3104
|
+
|
|
3105
|
+
// src/skills/publish-capability.ts
|
|
3106
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
3107
|
+
function parseSoulMd(content) {
|
|
3108
|
+
const lines = content.split("\n");
|
|
3109
|
+
let name = "";
|
|
3110
|
+
let description = "";
|
|
3111
|
+
const capabilities = [];
|
|
3112
|
+
const unknownSections = [];
|
|
3113
|
+
let currentSection = null;
|
|
3114
|
+
let currentCapabilityName = "";
|
|
3115
|
+
let currentCapabilityLines = [];
|
|
3116
|
+
let descriptionLines = [];
|
|
3117
|
+
let pastFirstH1 = false;
|
|
3118
|
+
let pastFirstH2 = false;
|
|
3119
|
+
const flushCapability = () => {
|
|
3120
|
+
if (currentCapabilityName) {
|
|
3121
|
+
capabilities.push({
|
|
3122
|
+
name: currentCapabilityName,
|
|
3123
|
+
description: currentCapabilityLines.join(" ").trim()
|
|
3124
|
+
});
|
|
3125
|
+
currentCapabilityName = "";
|
|
3126
|
+
currentCapabilityLines = [];
|
|
3127
|
+
}
|
|
3128
|
+
};
|
|
3129
|
+
for (const line of lines) {
|
|
3130
|
+
const trimmed = line.trim();
|
|
3131
|
+
if (/^# /.test(trimmed) && !pastFirstH1) {
|
|
3132
|
+
name = trimmed.slice(2).trim();
|
|
3133
|
+
pastFirstH1 = true;
|
|
3134
|
+
currentSection = "preamble";
|
|
3135
|
+
continue;
|
|
3136
|
+
}
|
|
3137
|
+
if (/^## /.test(trimmed)) {
|
|
3138
|
+
flushCapability();
|
|
3139
|
+
const capName = trimmed.slice(3).trim();
|
|
3140
|
+
currentCapabilityName = capName;
|
|
3141
|
+
currentSection = "capability";
|
|
3142
|
+
pastFirstH2 = true;
|
|
3143
|
+
continue;
|
|
3144
|
+
}
|
|
3145
|
+
if (/^#{3,} /.test(trimmed)) {
|
|
3146
|
+
const sectionName = trimmed.replace(/^#+\s*/, "");
|
|
3147
|
+
if (!unknownSections.includes(sectionName)) {
|
|
3148
|
+
unknownSections.push(sectionName);
|
|
3149
|
+
}
|
|
3150
|
+
continue;
|
|
3151
|
+
}
|
|
3152
|
+
if (trimmed === "") continue;
|
|
3153
|
+
if (currentSection === "preamble" && !pastFirstH2) {
|
|
3154
|
+
descriptionLines.push(trimmed);
|
|
3155
|
+
} else if (currentSection === "capability") {
|
|
3156
|
+
currentCapabilityLines.push(trimmed);
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
flushCapability();
|
|
3160
|
+
if (descriptionLines.length > 0) {
|
|
3161
|
+
description = descriptionLines[0] ?? "";
|
|
3162
|
+
}
|
|
3163
|
+
return {
|
|
3164
|
+
name,
|
|
3165
|
+
description,
|
|
3166
|
+
level: 2,
|
|
3167
|
+
capabilities,
|
|
3168
|
+
unknownSections
|
|
3169
|
+
};
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
// src/openclaw/soul-sync.ts
|
|
3173
|
+
function parseSoulMdV2(content) {
|
|
3174
|
+
const parsed = parseSoulMd(content);
|
|
3175
|
+
const skills = parsed.capabilities.map((cap) => {
|
|
3176
|
+
const sanitizedId = cap.name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
3177
|
+
const id = sanitizedId.length > 0 ? sanitizedId : randomUUID9();
|
|
3178
|
+
return {
|
|
3179
|
+
id,
|
|
3180
|
+
name: cap.name,
|
|
3181
|
+
description: (cap.description.slice(0, 500) || cap.name).slice(0, 500),
|
|
3182
|
+
level: 2,
|
|
3183
|
+
inputs: [
|
|
3184
|
+
{
|
|
3185
|
+
name: "input",
|
|
3186
|
+
type: "text",
|
|
3187
|
+
description: "Input for the skill",
|
|
3188
|
+
required: true
|
|
3189
|
+
}
|
|
3190
|
+
],
|
|
3191
|
+
outputs: [
|
|
3192
|
+
{
|
|
3193
|
+
name: "output",
|
|
3194
|
+
type: "text",
|
|
3195
|
+
description: "Output from the skill",
|
|
3196
|
+
required: true
|
|
3197
|
+
}
|
|
3198
|
+
],
|
|
3199
|
+
pricing: { credits_per_call: 10 },
|
|
3200
|
+
availability: { online: true }
|
|
3201
|
+
};
|
|
3202
|
+
});
|
|
3203
|
+
return {
|
|
3204
|
+
agentName: parsed.name || "Unknown Agent",
|
|
3205
|
+
description: parsed.description,
|
|
3206
|
+
skills
|
|
3207
|
+
};
|
|
3208
|
+
}
|
|
3209
|
+
function publishFromSoulV2(db, soulContent, owner) {
|
|
3210
|
+
const { agentName, skills } = parseSoulMdV2(soulContent);
|
|
3211
|
+
if (skills.length === 0) {
|
|
3212
|
+
throw new AgentBnBError("SOUL.md has no H2 sections", "VALIDATION_ERROR");
|
|
3213
|
+
}
|
|
3214
|
+
const existingCards = listCards(db, owner);
|
|
3215
|
+
const existingV2 = existingCards.find(
|
|
3216
|
+
(c) => c.spec_version === "2.0"
|
|
3217
|
+
);
|
|
3218
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3219
|
+
const cardId = existingV2?.id ?? randomUUID9();
|
|
3220
|
+
const card = {
|
|
3221
|
+
spec_version: "2.0",
|
|
3222
|
+
id: cardId,
|
|
3223
|
+
owner,
|
|
3224
|
+
agent_name: agentName,
|
|
3225
|
+
skills,
|
|
3226
|
+
availability: { online: true },
|
|
3227
|
+
created_at: existingV2?.created_at ?? now,
|
|
3228
|
+
updated_at: now
|
|
3229
|
+
};
|
|
3230
|
+
CapabilityCardV2Schema.parse(card);
|
|
3231
|
+
if (existingV2) {
|
|
3232
|
+
db.prepare(
|
|
3233
|
+
"UPDATE capability_cards SET data = ?, updated_at = ? WHERE id = ?"
|
|
3234
|
+
).run(JSON.stringify(card), now, cardId);
|
|
3235
|
+
} else {
|
|
3236
|
+
db.prepare(
|
|
3237
|
+
"INSERT INTO capability_cards (id, owner, data, created_at, updated_at) VALUES (?, ?, ?, ?, ?)"
|
|
3238
|
+
).run(cardId, owner, JSON.stringify(card), now, now);
|
|
3239
|
+
}
|
|
3240
|
+
return card;
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
// src/openclaw/heartbeat-writer.ts
|
|
3244
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync6 } from "fs";
|
|
3245
|
+
var HEARTBEAT_MARKER_START = "<!-- agentbnb:start -->";
|
|
3246
|
+
var HEARTBEAT_MARKER_END = "<!-- agentbnb:end -->";
|
|
3247
|
+
function generateHeartbeatSection(autonomy, budget) {
|
|
3248
|
+
const t1 = autonomy.tier1_max_credits;
|
|
3249
|
+
const t2 = autonomy.tier2_max_credits;
|
|
3250
|
+
const reserve = budget.reserve_credits;
|
|
3251
|
+
return [
|
|
3252
|
+
HEARTBEAT_MARKER_START,
|
|
3253
|
+
"## AgentBnB Protocol",
|
|
3254
|
+
"",
|
|
3255
|
+
"You are a participant in the AgentBnB capability sharing network.",
|
|
3256
|
+
"",
|
|
3257
|
+
"### Sharing Rules",
|
|
3258
|
+
"- When your idle_rate exceeds 70%, automatically share idle capabilities",
|
|
3259
|
+
"- Accept incoming requests that match your published Capability Card",
|
|
3260
|
+
"- Track credit earnings and report to owner weekly",
|
|
3261
|
+
"",
|
|
3262
|
+
"### Requesting Rules",
|
|
3263
|
+
"- When you encounter a task you cannot complete with local skills:",
|
|
3264
|
+
" 1. Query AgentBnB network for matching capabilities",
|
|
3265
|
+
" 2. If found and credit sufficient, automatically request",
|
|
3266
|
+
" 3. Integrate result into your current workflow",
|
|
3267
|
+
"- Budget limits:",
|
|
3268
|
+
` - < ${t1} credits: auto-execute, no notification`,
|
|
3269
|
+
` - ${t1}-${t2} credits: execute, notify owner after`,
|
|
3270
|
+
` - > ${t2} credits: ask owner before executing`,
|
|
3271
|
+
"",
|
|
3272
|
+
"### Credit Management",
|
|
3273
|
+
`- Maintain minimum balance of ${reserve} credits (reserve for emergencies)`,
|
|
3274
|
+
`- If balance drops below ${reserve}, increase sharing priority`,
|
|
3275
|
+
"- If balance exceeds 500, notify owner of surplus",
|
|
3276
|
+
HEARTBEAT_MARKER_END
|
|
3277
|
+
].join("\n");
|
|
3278
|
+
}
|
|
3279
|
+
function injectHeartbeatSection(heartbeatPath, section) {
|
|
3280
|
+
if (!existsSync6(heartbeatPath)) {
|
|
3281
|
+
writeFileSync4(heartbeatPath, section + "\n", "utf-8");
|
|
3282
|
+
return;
|
|
3283
|
+
}
|
|
3284
|
+
let content = readFileSync5(heartbeatPath, "utf-8");
|
|
3285
|
+
const startIdx = content.indexOf(HEARTBEAT_MARKER_START);
|
|
3286
|
+
const endIdx = content.indexOf(HEARTBEAT_MARKER_END);
|
|
3287
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
3288
|
+
content = content.slice(0, startIdx) + section + content.slice(endIdx + HEARTBEAT_MARKER_END.length);
|
|
3289
|
+
} else {
|
|
3290
|
+
content = content + "\n" + section + "\n";
|
|
3291
|
+
}
|
|
3292
|
+
writeFileSync4(heartbeatPath, content, "utf-8");
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
// src/openclaw/skill.ts
|
|
3296
|
+
function getOpenClawStatus(config, db, creditDb) {
|
|
3297
|
+
const autonomy = config.autonomy ?? DEFAULT_AUTONOMY_CONFIG;
|
|
3298
|
+
const budget = config.budget ?? DEFAULT_BUDGET_CONFIG;
|
|
3299
|
+
const balance = getBalance(creditDb, config.owner);
|
|
3300
|
+
const allCards = listCards(db, config.owner);
|
|
3301
|
+
const skills = [];
|
|
3302
|
+
for (const card of allCards) {
|
|
3303
|
+
const anyCard = card;
|
|
3304
|
+
if (anyCard.spec_version !== "2.0" || !Array.isArray(anyCard.skills)) continue;
|
|
3305
|
+
for (const skill of anyCard.skills) {
|
|
3306
|
+
const internal = skill["_internal"] ?? {};
|
|
3307
|
+
const idleRate = typeof internal["idle_rate"] === "number" ? internal["idle_rate"] : null;
|
|
3308
|
+
const availability = skill["availability"];
|
|
3309
|
+
const online = availability?.online ?? false;
|
|
3310
|
+
skills.push({
|
|
3311
|
+
id: String(skill["id"] ?? ""),
|
|
3312
|
+
name: String(skill["name"] ?? ""),
|
|
3313
|
+
idle_rate: idleRate,
|
|
3314
|
+
online
|
|
3315
|
+
});
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
return {
|
|
3319
|
+
installed: true,
|
|
3320
|
+
owner: config.owner,
|
|
3321
|
+
gateway_url: config.gateway_url,
|
|
3322
|
+
tier: autonomy,
|
|
3323
|
+
balance,
|
|
3324
|
+
reserve: budget.reserve_credits,
|
|
3325
|
+
skills
|
|
3326
|
+
};
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
// src/cli/index.ts
|
|
3330
|
+
var require2 = createRequire(import.meta.url);
|
|
3331
|
+
var pkg = require2("../../package.json");
|
|
3332
|
+
async function confirm(question) {
|
|
3333
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3334
|
+
try {
|
|
3335
|
+
return await new Promise((resolve) => {
|
|
3336
|
+
rl.question(question, (answer) => {
|
|
3337
|
+
resolve(answer.trim().toLowerCase() === "y");
|
|
3338
|
+
});
|
|
3339
|
+
});
|
|
3340
|
+
} finally {
|
|
3341
|
+
rl.close();
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
function getLanIp() {
|
|
3345
|
+
const nets = networkInterfaces();
|
|
3346
|
+
for (const ifaces of Object.values(nets)) {
|
|
3347
|
+
for (const iface of ifaces ?? []) {
|
|
3348
|
+
if (iface.family === "IPv4" && !iface.internal) return iface.address;
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
return "localhost";
|
|
3352
|
+
}
|
|
3353
|
+
var program = new Command();
|
|
3354
|
+
program.name("agentbnb").description("P2P Agent Capability Sharing Protocol \u2014 Airbnb for AI agent pipelines").version(pkg.version);
|
|
3355
|
+
program.command("init").description("Initialize AgentBnB config and create agent identity").option("--owner <name>", "Agent owner name").option("--port <port>", "Gateway port", "7700").option("--host <ip>", "Override gateway host IP (default: auto-detected LAN IP)").option("--yes", "Auto-confirm all draft cards (non-interactive)").option("--no-detect", "Skip API key detection").option("--json", "Output as JSON").action(async (opts) => {
|
|
3356
|
+
const owner = opts.owner ?? `agent-${randomBytes(4).toString("hex")}`;
|
|
3357
|
+
const token = randomBytes(32).toString("hex");
|
|
3358
|
+
const configDir = getConfigDir();
|
|
3359
|
+
const dbPath = join5(configDir, "registry.db");
|
|
3360
|
+
const creditDbPath = join5(configDir, "credit.db");
|
|
3361
|
+
const port = parseInt(opts.port, 10);
|
|
3362
|
+
const ip = opts.host ?? getLanIp();
|
|
3363
|
+
const existingConfig = loadConfig();
|
|
3364
|
+
const api_key = existingConfig?.api_key ?? randomBytes(32).toString("hex");
|
|
3365
|
+
const config = {
|
|
3366
|
+
owner,
|
|
3367
|
+
gateway_url: `http://${ip}:${port}`,
|
|
3368
|
+
gateway_port: port,
|
|
3369
|
+
db_path: dbPath,
|
|
3370
|
+
credit_db_path: creditDbPath,
|
|
3371
|
+
token,
|
|
3372
|
+
api_key
|
|
3373
|
+
};
|
|
3374
|
+
saveConfig(config);
|
|
3375
|
+
let keypairStatus = "existing";
|
|
3376
|
+
try {
|
|
3377
|
+
loadKeyPair(configDir);
|
|
3378
|
+
} catch {
|
|
3379
|
+
const keys = generateKeyPair();
|
|
3380
|
+
saveKeyPair(configDir, keys);
|
|
3381
|
+
keypairStatus = "generated";
|
|
3382
|
+
}
|
|
3383
|
+
const creditDb = openCreditDb(creditDbPath);
|
|
3384
|
+
bootstrapAgent(creditDb, owner, 100);
|
|
3385
|
+
creditDb.close();
|
|
3386
|
+
const skipDetect = opts.detect === false;
|
|
3387
|
+
let detectedKeys = [];
|
|
3388
|
+
let detectedPorts = [];
|
|
3389
|
+
const publishedCards = [];
|
|
3390
|
+
if (!skipDetect) {
|
|
3391
|
+
detectedKeys = detectApiKeys(KNOWN_API_KEYS);
|
|
3392
|
+
detectedPorts = await detectOpenPorts([7700, 7701, 8080, 3e3, 8e3, 11434]);
|
|
3393
|
+
if (detectedKeys.length > 0) {
|
|
3394
|
+
if (!opts.json) {
|
|
3395
|
+
console.log(`
|
|
3396
|
+
Detected ${detectedKeys.length} API key${detectedKeys.length > 1 ? "s" : ""}: ${detectedKeys.join(", ")}`);
|
|
3397
|
+
}
|
|
3398
|
+
if (detectedPorts.length > 0 && !opts.json) {
|
|
3399
|
+
console.log(`Found services on ports: ${detectedPorts.join(", ")}`);
|
|
3400
|
+
}
|
|
3401
|
+
const drafts = detectedKeys.map((key) => buildDraftCard(key, owner)).filter((card) => card !== null);
|
|
3402
|
+
if (opts.yes) {
|
|
3403
|
+
const db = openDatabase(dbPath);
|
|
3404
|
+
try {
|
|
3405
|
+
for (const card of drafts) {
|
|
3406
|
+
insertCard(db, card);
|
|
3407
|
+
publishedCards.push({ id: card.id, name: card.name });
|
|
3408
|
+
if (!opts.json) {
|
|
3409
|
+
console.log(`Published: ${card.name} (${card.id})`);
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
} finally {
|
|
3413
|
+
db.close();
|
|
3414
|
+
}
|
|
3415
|
+
} else if (process.stdout.isTTY) {
|
|
3416
|
+
const db = openDatabase(dbPath);
|
|
3417
|
+
try {
|
|
3418
|
+
for (const card of drafts) {
|
|
3419
|
+
const yes = await confirm(`Publish "${card.name}"? [y/N] `);
|
|
3420
|
+
if (yes) {
|
|
3421
|
+
insertCard(db, card);
|
|
3422
|
+
publishedCards.push({ id: card.id, name: card.name });
|
|
3423
|
+
console.log(`Published: ${card.name} (${card.id})`);
|
|
3424
|
+
} else {
|
|
3425
|
+
console.log(`Skipped: ${card.name}`);
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
} finally {
|
|
3429
|
+
db.close();
|
|
3430
|
+
}
|
|
3431
|
+
} else {
|
|
3432
|
+
if (!opts.json) {
|
|
3433
|
+
console.log("Non-interactive environment detected. Re-run with --yes to auto-publish draft cards.");
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
} else {
|
|
3437
|
+
if (!opts.json) {
|
|
3438
|
+
console.log("\nNo API keys detected. You can manually publish cards with `agentbnb publish`.");
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
if (opts.json) {
|
|
3443
|
+
const jsonOutput = {
|
|
3444
|
+
success: true,
|
|
3445
|
+
owner,
|
|
3446
|
+
config_dir: configDir,
|
|
3447
|
+
token,
|
|
3448
|
+
gateway_url: config.gateway_url,
|
|
3449
|
+
keypair: keypairStatus
|
|
3450
|
+
};
|
|
3451
|
+
if (!skipDetect) {
|
|
3452
|
+
jsonOutput.detected_keys = detectedKeys;
|
|
3453
|
+
jsonOutput.published_cards = publishedCards;
|
|
3454
|
+
}
|
|
3455
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
3456
|
+
} else {
|
|
3457
|
+
console.log(`AgentBnB initialized.`);
|
|
3458
|
+
console.log(` Owner: ${owner}`);
|
|
3459
|
+
console.log(` Token: ${token}`);
|
|
3460
|
+
console.log(` Config: ${configDir}/config.json`);
|
|
3461
|
+
console.log(` Credits: 100 (starter grant)`);
|
|
3462
|
+
console.log(` Keypair: ${keypairStatus === "generated" ? "generated (Ed25519)" : "preserved (existing)"}`);
|
|
3463
|
+
console.log(` Gateway: http://${ip}:${port}`);
|
|
3464
|
+
}
|
|
3465
|
+
});
|
|
3466
|
+
program.command("publish <card.json>").description("Publish a Capability Card to the registry").option("--json", "Output as JSON").action(async (cardPath, opts) => {
|
|
3467
|
+
const config = loadConfig();
|
|
3468
|
+
if (!config) {
|
|
3469
|
+
console.error("Error: not initialized. Run `agentbnb init` first.");
|
|
3470
|
+
process.exit(1);
|
|
3471
|
+
}
|
|
3472
|
+
let raw;
|
|
3473
|
+
try {
|
|
3474
|
+
raw = readFileSync6(cardPath, "utf-8");
|
|
3475
|
+
} catch {
|
|
3476
|
+
console.error(`Error: cannot read file: ${cardPath}`);
|
|
3477
|
+
process.exit(1);
|
|
3478
|
+
}
|
|
3479
|
+
let parsed;
|
|
3480
|
+
try {
|
|
3481
|
+
parsed = JSON.parse(raw);
|
|
3482
|
+
} catch {
|
|
3483
|
+
console.error("Error: invalid JSON in card file.");
|
|
3484
|
+
process.exit(1);
|
|
3485
|
+
}
|
|
3486
|
+
const result = CapabilityCardSchema.safeParse(parsed);
|
|
3487
|
+
if (!result.success) {
|
|
3488
|
+
if (opts.json) {
|
|
3489
|
+
console.log(JSON.stringify({ success: false, errors: result.error.issues }, null, 2));
|
|
3490
|
+
} else {
|
|
3491
|
+
console.error("Error: card validation failed:");
|
|
3492
|
+
for (const issue of result.error.issues) {
|
|
3493
|
+
console.error(` - ${issue.path.join(".")}: ${issue.message}`);
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
process.exit(1);
|
|
3497
|
+
}
|
|
3498
|
+
const db = openDatabase(config.db_path);
|
|
3499
|
+
try {
|
|
3500
|
+
insertCard(db, result.data);
|
|
3501
|
+
} finally {
|
|
3502
|
+
db.close();
|
|
3503
|
+
}
|
|
3504
|
+
if (opts.json) {
|
|
3505
|
+
console.log(JSON.stringify({ success: true, id: result.data.id, name: result.data.name }, null, 2));
|
|
3506
|
+
} else {
|
|
3507
|
+
console.log(`Published: ${result.data.name} (${result.data.id})`);
|
|
3508
|
+
}
|
|
3509
|
+
});
|
|
3510
|
+
program.command("discover [query]").description("Search available capabilities in the registry").option("--level <level>", "Filter by level (1, 2, or 3)").option("--online", "Only show online capabilities").option("--local", "Browse for agents on the local network via mDNS").option("--registry <url>", "Remote registry URL to query (e.g., http://host:7701)").option("--tag <tag>", "Filter by metadata tag").option("--json", "Output as JSON").action(async (query, opts) => {
|
|
3511
|
+
if (opts.local) {
|
|
3512
|
+
const discovered = [];
|
|
3513
|
+
const browser = discoverLocalAgents((agent) => {
|
|
3514
|
+
discovered.push(agent);
|
|
3515
|
+
});
|
|
3516
|
+
await new Promise((resolve) => setTimeout(resolve, 3e3));
|
|
3517
|
+
browser.stop();
|
|
3518
|
+
if (opts.json) {
|
|
3519
|
+
console.log(JSON.stringify(discovered, null, 2));
|
|
3520
|
+
return;
|
|
3521
|
+
}
|
|
3522
|
+
if (discovered.length === 0) {
|
|
3523
|
+
console.log("No agents found on local network.");
|
|
3524
|
+
return;
|
|
3525
|
+
}
|
|
3526
|
+
const col2 = (s, w) => s.slice(0, w).padEnd(w);
|
|
3527
|
+
console.log(col2("Name", 24) + " " + col2("URL", 32) + " " + col2("Owner", 20));
|
|
3528
|
+
console.log("-".repeat(80));
|
|
3529
|
+
for (const agent of discovered) {
|
|
3530
|
+
console.log(col2(agent.name, 24) + " " + col2(agent.url, 32) + " " + col2(agent.owner, 20));
|
|
3531
|
+
}
|
|
3532
|
+
console.log(`
|
|
3533
|
+
${discovered.length} agent(s) found on local network`);
|
|
3534
|
+
return;
|
|
3535
|
+
}
|
|
3536
|
+
const config = loadConfig();
|
|
3537
|
+
if (!config) {
|
|
3538
|
+
console.error("Error: not initialized. Run `agentbnb init` first.");
|
|
3539
|
+
process.exit(1);
|
|
3540
|
+
}
|
|
3541
|
+
const db = openDatabase(config.db_path);
|
|
3542
|
+
let localCards;
|
|
3543
|
+
try {
|
|
3544
|
+
const level = opts.level ? parseInt(opts.level, 10) : void 0;
|
|
3545
|
+
const filters = { level, online: opts.online };
|
|
3546
|
+
if (query && query.trim().length > 0) {
|
|
3547
|
+
localCards = searchCards(db, query, filters);
|
|
3548
|
+
} else {
|
|
3549
|
+
localCards = filterCards(db, filters);
|
|
3550
|
+
}
|
|
3551
|
+
} finally {
|
|
3552
|
+
db.close();
|
|
3553
|
+
}
|
|
3554
|
+
if (opts.tag) {
|
|
3555
|
+
localCards = localCards.filter((c) => c.metadata?.tags?.includes(opts.tag));
|
|
3556
|
+
}
|
|
3557
|
+
localCards = localCards.map(({ _internal: _, ...rest }) => rest);
|
|
3558
|
+
const registryUrl = opts.registry ?? config.registry ?? void 0;
|
|
3559
|
+
const isExplicitRegistry = Boolean(opts.registry);
|
|
3560
|
+
let outputCards;
|
|
3561
|
+
let hasRemote = false;
|
|
3562
|
+
if (registryUrl) {
|
|
3563
|
+
try {
|
|
3564
|
+
let remoteCards = await fetchRemoteCards(registryUrl, {
|
|
3565
|
+
q: query,
|
|
3566
|
+
level: opts.level ? parseInt(opts.level, 10) : void 0,
|
|
3567
|
+
online: opts.online,
|
|
3568
|
+
tag: opts.tag
|
|
3569
|
+
});
|
|
3570
|
+
remoteCards = remoteCards.map(({ _internal: _, ...rest }) => rest);
|
|
3571
|
+
hasRemote = true;
|
|
3572
|
+
outputCards = mergeResults(localCards, remoteCards, Boolean(query && query.trim().length > 0));
|
|
3573
|
+
} catch (err) {
|
|
3574
|
+
if (isExplicitRegistry) {
|
|
3575
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3576
|
+
console.error(msg);
|
|
3577
|
+
process.exit(1);
|
|
3578
|
+
return;
|
|
3579
|
+
} else {
|
|
3580
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3581
|
+
console.error(`Warning: ${msg}`);
|
|
3582
|
+
outputCards = localCards.map((c) => ({ ...c, source: "local" }));
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
} else {
|
|
3586
|
+
outputCards = localCards;
|
|
3587
|
+
}
|
|
3588
|
+
if (opts.json) {
|
|
3589
|
+
console.log(JSON.stringify(outputCards, null, 2));
|
|
3590
|
+
return;
|
|
3591
|
+
}
|
|
3592
|
+
if (outputCards.length === 0) {
|
|
3593
|
+
console.log("No capabilities found.");
|
|
3594
|
+
return;
|
|
3595
|
+
}
|
|
3596
|
+
const col = (s, w) => s.slice(0, w).padEnd(w);
|
|
3597
|
+
if (hasRemote) {
|
|
3598
|
+
console.log(
|
|
3599
|
+
col("ID", 16) + " " + col("Name", 28) + " " + col("Lvl", 3) + " " + col("Credits", 7) + " " + col("Online", 6) + " " + col("Source", 8)
|
|
3600
|
+
);
|
|
3601
|
+
console.log("-".repeat(80));
|
|
3602
|
+
for (const card of outputCards) {
|
|
3603
|
+
const shortId = card.id.slice(0, 8) + "...";
|
|
3604
|
+
const source = "source" in card ? card.source : "local";
|
|
3605
|
+
const sourceTag = source === "remote" ? "[remote]" : "[local]";
|
|
3606
|
+
console.log(
|
|
3607
|
+
col(shortId, 16) + " " + col(card.name, 28) + " " + col(String(card.level), 3) + " " + col(String(card.pricing.credits_per_call), 7) + " " + col(card.availability.online ? "yes" : "no", 6) + " " + col(sourceTag, 8)
|
|
3608
|
+
);
|
|
3609
|
+
}
|
|
3610
|
+
} else {
|
|
3611
|
+
console.log(
|
|
3612
|
+
col("ID", 16) + " " + col("Name", 32) + " " + col("Lvl", 3) + " " + col("Credits", 7) + " " + col("Online", 6)
|
|
3613
|
+
);
|
|
3614
|
+
console.log("-".repeat(72));
|
|
3615
|
+
for (const card of outputCards) {
|
|
3616
|
+
const shortId = card.id.slice(0, 8) + "...";
|
|
3617
|
+
console.log(
|
|
3618
|
+
col(shortId, 16) + " " + col(card.name, 32) + " " + col(String(card.level), 3) + " " + col(String(card.pricing.credits_per_call), 7) + " " + col(card.availability.online ? "yes" : "no", 6)
|
|
3619
|
+
);
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
console.log(`
|
|
3623
|
+
${outputCards.length} result(s)`);
|
|
3624
|
+
});
|
|
3625
|
+
program.command("request [card-id]").description("Request a capability from another agent \u2014 direct (card-id) or auto (--query)").option("--params <json>", "Input parameters as JSON string", "{}").option("--peer <name>", "Peer name to send request to (resolves URL+token from peer registry)").option("--query <text>", "Search query for capability gap (triggers auto-request flow)").option("--max-cost <credits>", "Maximum credits to spend on auto-request (default: 50)").option("--json", "Output as JSON").action(async (cardId, opts) => {
|
|
3626
|
+
const config = loadConfig();
|
|
3627
|
+
if (!config) {
|
|
3628
|
+
console.error("Error: not initialized. Run `agentbnb init` first.");
|
|
3629
|
+
process.exit(1);
|
|
3630
|
+
}
|
|
3631
|
+
if (opts.query) {
|
|
3632
|
+
let queryParams;
|
|
3633
|
+
if (opts.params && opts.params !== "{}") {
|
|
3634
|
+
try {
|
|
3635
|
+
queryParams = JSON.parse(opts.params);
|
|
3636
|
+
} catch {
|
|
3637
|
+
console.error("Error: --params must be valid JSON.");
|
|
3638
|
+
process.exit(1);
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
const registryDb = openDatabase(join5(getConfigDir(), "registry.db"));
|
|
3642
|
+
const creditDb = openCreditDb(join5(getConfigDir(), "credit.db"));
|
|
3643
|
+
registryDb.pragma("busy_timeout = 5000");
|
|
3644
|
+
creditDb.pragma("busy_timeout = 5000");
|
|
3645
|
+
try {
|
|
3646
|
+
const budgetManager = new BudgetManager(creditDb, config.owner, config.budget ?? DEFAULT_BUDGET_CONFIG);
|
|
3647
|
+
const requestor = new AutoRequestor({
|
|
3648
|
+
owner: config.owner,
|
|
3649
|
+
registryDb,
|
|
3650
|
+
creditDb,
|
|
3651
|
+
autonomyConfig: config.autonomy ?? DEFAULT_AUTONOMY_CONFIG,
|
|
3652
|
+
budgetManager
|
|
3653
|
+
});
|
|
3654
|
+
const result = await requestor.requestWithAutonomy({
|
|
3655
|
+
query: opts.query,
|
|
3656
|
+
maxCostCredits: Number(opts.maxCost ?? 50),
|
|
3657
|
+
params: queryParams
|
|
3658
|
+
});
|
|
3659
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3660
|
+
} finally {
|
|
3661
|
+
registryDb.close();
|
|
3662
|
+
creditDb.close();
|
|
3663
|
+
}
|
|
3664
|
+
return;
|
|
3665
|
+
}
|
|
3666
|
+
if (!cardId) {
|
|
3667
|
+
console.error("Error: provide a <card-id> for direct request, or use --query for auto-request.");
|
|
3668
|
+
process.exit(1);
|
|
3669
|
+
}
|
|
3670
|
+
let params;
|
|
3671
|
+
try {
|
|
3672
|
+
params = JSON.parse(opts.params);
|
|
3673
|
+
} catch {
|
|
3674
|
+
console.error("Error: --params must be valid JSON.");
|
|
3675
|
+
process.exit(1);
|
|
3676
|
+
}
|
|
3677
|
+
let gatewayUrl;
|
|
3678
|
+
let token;
|
|
3679
|
+
if (opts.peer) {
|
|
3680
|
+
const peer = findPeer(opts.peer);
|
|
3681
|
+
if (!peer) {
|
|
3682
|
+
console.error(`Error: Peer not found: ${opts.peer}. Run \`agentbnb peers\` to see registered peers.`);
|
|
3683
|
+
process.exit(1);
|
|
3684
|
+
}
|
|
3685
|
+
gatewayUrl = peer.url;
|
|
3686
|
+
token = peer.token;
|
|
3687
|
+
} else {
|
|
3688
|
+
gatewayUrl = config.gateway_url;
|
|
3689
|
+
token = config.token;
|
|
3690
|
+
}
|
|
3691
|
+
try {
|
|
3692
|
+
const result = await requestCapability({
|
|
3693
|
+
gatewayUrl,
|
|
3694
|
+
token,
|
|
3695
|
+
cardId,
|
|
3696
|
+
params
|
|
3697
|
+
});
|
|
3698
|
+
if (opts.json) {
|
|
3699
|
+
console.log(JSON.stringify({ success: true, result }, null, 2));
|
|
3700
|
+
} else {
|
|
3701
|
+
console.log("Result:");
|
|
3702
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3703
|
+
}
|
|
3704
|
+
} catch (err) {
|
|
3705
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3706
|
+
if (opts.json) {
|
|
3707
|
+
console.log(JSON.stringify({ success: false, error: msg }, null, 2));
|
|
3708
|
+
} else {
|
|
3709
|
+
console.error(`Error: ${msg}`);
|
|
3710
|
+
}
|
|
3711
|
+
process.exit(1);
|
|
3712
|
+
}
|
|
3713
|
+
});
|
|
3714
|
+
program.command("status").description("Show credit balance and recent transactions").option("--json", "Output as JSON").action(async (opts) => {
|
|
3715
|
+
const config = loadConfig();
|
|
3716
|
+
if (!config) {
|
|
3717
|
+
console.error("Error: not initialized. Run `agentbnb init` first.");
|
|
3718
|
+
process.exit(1);
|
|
3719
|
+
}
|
|
3720
|
+
const creditDb = openCreditDb(config.credit_db_path);
|
|
3721
|
+
let balance;
|
|
3722
|
+
let transactions;
|
|
3723
|
+
let heldEscrows;
|
|
3724
|
+
try {
|
|
3725
|
+
balance = getBalance(creditDb, config.owner);
|
|
3726
|
+
transactions = getTransactions(creditDb, config.owner, 5);
|
|
3727
|
+
heldEscrows = creditDb.prepare("SELECT id, amount, card_id, created_at FROM credit_escrow WHERE owner = ? AND status = ?").all(config.owner, "held");
|
|
3728
|
+
} finally {
|
|
3729
|
+
creditDb.close();
|
|
3730
|
+
}
|
|
3731
|
+
if (opts.json) {
|
|
3732
|
+
console.log(JSON.stringify({ owner: config.owner, balance, held_escrows: heldEscrows, recent_transactions: transactions }, null, 2));
|
|
3733
|
+
return;
|
|
3734
|
+
}
|
|
3735
|
+
console.log(`Owner: ${config.owner}`);
|
|
3736
|
+
console.log(`Balance: ${balance} credits`);
|
|
3737
|
+
if (heldEscrows.length > 0) {
|
|
3738
|
+
console.log(`
|
|
3739
|
+
Active Escrows (${heldEscrows.length}):`);
|
|
3740
|
+
for (const e of heldEscrows) {
|
|
3741
|
+
console.log(` ${e.id.slice(0, 8)}... ${e.amount} credits card=${e.card_id.slice(0, 8)}...`);
|
|
3742
|
+
}
|
|
3743
|
+
} else {
|
|
3744
|
+
console.log("Active Escrows: none");
|
|
3745
|
+
}
|
|
3746
|
+
if (transactions.length > 0) {
|
|
3747
|
+
console.log("\nRecent Transactions:");
|
|
3748
|
+
for (const tx of transactions) {
|
|
3749
|
+
const sign2 = tx.amount > 0 ? "+" : "";
|
|
3750
|
+
console.log(` ${tx.created_at.slice(0, 19)} ${sign2}${tx.amount} ${tx.reason}`);
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
});
|
|
3754
|
+
program.command("serve").description("Start the AgentBnB gateway server").option("--port <port>", "Port to listen on (overrides config)").option("--handler-url <url>", "Local capability handler URL", "http://localhost:8080").option("--skills-yaml <path>", "Path to skills.yaml (default: ~/.agentbnb/skills.yaml)").option("--registry-port <port>", "Public registry API port (0 to disable)", "7701").option("--announce", "Announce this gateway on the local network via mDNS").action(async (opts) => {
|
|
3755
|
+
const config = loadConfig();
|
|
3756
|
+
if (!config) {
|
|
3757
|
+
console.error("Error: not initialized. Run `agentbnb init` first.");
|
|
3758
|
+
process.exit(1);
|
|
3759
|
+
}
|
|
3760
|
+
const port = opts.port ? parseInt(opts.port, 10) : config.gateway_port;
|
|
3761
|
+
const registryPort = parseInt(opts.registryPort, 10);
|
|
3762
|
+
const skillsYamlPath = opts.skillsYaml ?? join5(homedir2(), ".agentbnb", "skills.yaml");
|
|
3763
|
+
const runtime = new AgentRuntime({
|
|
3764
|
+
registryDbPath: config.db_path,
|
|
3765
|
+
creditDbPath: config.credit_db_path,
|
|
3766
|
+
owner: config.owner,
|
|
3767
|
+
skillsYamlPath
|
|
3768
|
+
});
|
|
3769
|
+
await runtime.start();
|
|
3770
|
+
if (runtime.skillExecutor) {
|
|
3771
|
+
console.log(`SkillExecutor initialized from ${skillsYamlPath}`);
|
|
3772
|
+
}
|
|
3773
|
+
const autonomyConfig = config.autonomy ?? DEFAULT_AUTONOMY_CONFIG;
|
|
3774
|
+
const idleMonitor = new IdleMonitor({
|
|
3775
|
+
owner: config.owner,
|
|
3776
|
+
db: runtime.registryDb,
|
|
3777
|
+
autonomyConfig
|
|
3778
|
+
});
|
|
3779
|
+
const idleJob = idleMonitor.start();
|
|
3780
|
+
runtime.registerJob(idleJob);
|
|
3781
|
+
console.log("IdleMonitor started (60s poll interval, 70% idle threshold)");
|
|
3782
|
+
const server = createGatewayServer({
|
|
3783
|
+
port,
|
|
3784
|
+
registryDb: runtime.registryDb,
|
|
3785
|
+
creditDb: runtime.creditDb,
|
|
3786
|
+
tokens: [config.token],
|
|
3787
|
+
handlerUrl: opts.handlerUrl,
|
|
3788
|
+
skillExecutor: runtime.skillExecutor
|
|
3789
|
+
});
|
|
3790
|
+
let registryServer = null;
|
|
3791
|
+
const gracefulShutdown = async () => {
|
|
3792
|
+
console.log("\nShutting down...");
|
|
3793
|
+
if (opts.announce) {
|
|
3794
|
+
await stopAnnouncement();
|
|
3795
|
+
}
|
|
3796
|
+
if (registryServer) {
|
|
3797
|
+
await registryServer.close();
|
|
3798
|
+
}
|
|
3799
|
+
await server.close();
|
|
3800
|
+
await runtime.shutdown();
|
|
3801
|
+
process.exit(0);
|
|
3802
|
+
};
|
|
3803
|
+
process.on("SIGINT", () => {
|
|
3804
|
+
void gracefulShutdown();
|
|
3805
|
+
});
|
|
3806
|
+
process.on("SIGTERM", () => {
|
|
3807
|
+
void gracefulShutdown();
|
|
3808
|
+
});
|
|
3809
|
+
try {
|
|
3810
|
+
await server.listen({ port, host: "0.0.0.0" });
|
|
3811
|
+
console.log(`Gateway running on port ${port}`);
|
|
3812
|
+
if (registryPort > 0) {
|
|
3813
|
+
if (!config.api_key) {
|
|
3814
|
+
console.warn("No API key found. Run `agentbnb init` to enable dashboard features.");
|
|
3815
|
+
}
|
|
3816
|
+
registryServer = createRegistryServer({
|
|
3817
|
+
registryDb: runtime.registryDb,
|
|
3818
|
+
silent: false,
|
|
3819
|
+
ownerName: config.owner,
|
|
3820
|
+
ownerApiKey: config.api_key,
|
|
3821
|
+
creditDb: runtime.creditDb
|
|
3822
|
+
});
|
|
3823
|
+
await registryServer.listen({ port: registryPort, host: "0.0.0.0" });
|
|
3824
|
+
console.log(`Registry API: http://0.0.0.0:${registryPort}/cards`);
|
|
3825
|
+
}
|
|
3826
|
+
if (opts.announce) {
|
|
3827
|
+
announceGateway(config.owner, port);
|
|
3828
|
+
console.log("Announcing on local network via mDNS");
|
|
3829
|
+
}
|
|
3830
|
+
} catch (err) {
|
|
3831
|
+
console.error("Failed to start:", err);
|
|
3832
|
+
if (registryServer) {
|
|
3833
|
+
await registryServer.close().catch(() => {
|
|
3834
|
+
});
|
|
3835
|
+
}
|
|
3836
|
+
await runtime.shutdown();
|
|
3837
|
+
process.exit(1);
|
|
3838
|
+
}
|
|
3839
|
+
});
|
|
3840
|
+
program.command("connect <name> <url> <token>").description("Register a remote peer agent (store URL + token for reuse)").option("--json", "Output as JSON").action(async (name, url, token, opts) => {
|
|
3841
|
+
const config = loadConfig();
|
|
3842
|
+
if (!config) {
|
|
3843
|
+
console.error("Error: not initialized. Run `agentbnb init` first.");
|
|
3844
|
+
process.exit(1);
|
|
3845
|
+
}
|
|
3846
|
+
savePeer({ name, url, token, added_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
3847
|
+
if (opts.json) {
|
|
3848
|
+
console.log(JSON.stringify({ success: true, name, url }, null, 2));
|
|
3849
|
+
} else {
|
|
3850
|
+
console.log(`Connected to peer: ${name} at ${url}`);
|
|
3851
|
+
}
|
|
3852
|
+
});
|
|
3853
|
+
var peersCommand = program.command("peers").description("List registered peer agents");
|
|
3854
|
+
peersCommand.option("--json", "Output as JSON").action(async (opts) => {
|
|
3855
|
+
const peers = loadPeers();
|
|
3856
|
+
if (opts.json) {
|
|
3857
|
+
console.log(JSON.stringify(peers, null, 2));
|
|
3858
|
+
return;
|
|
3859
|
+
}
|
|
3860
|
+
if (peers.length === 0) {
|
|
3861
|
+
console.log("No peers registered. Use `agentbnb connect` to add one.");
|
|
3862
|
+
return;
|
|
3863
|
+
}
|
|
3864
|
+
const col = (s, w) => s.slice(0, w).padEnd(w);
|
|
3865
|
+
console.log(col("Name", 20) + " " + col("URL", 36) + " " + col("Added", 20));
|
|
3866
|
+
console.log("-".repeat(80));
|
|
3867
|
+
for (const peer of peers) {
|
|
3868
|
+
console.log(col(peer.name, 20) + " " + col(peer.url, 36) + " " + col(peer.added_at.slice(0, 19), 20));
|
|
3869
|
+
}
|
|
3870
|
+
console.log(`
|
|
3871
|
+
${peers.length} peer(s)`);
|
|
3872
|
+
});
|
|
3873
|
+
peersCommand.command("remove <name>").description("Remove a registered peer").action(async (name) => {
|
|
3874
|
+
const removed = removePeer(name);
|
|
3875
|
+
if (removed) {
|
|
3876
|
+
console.log(`Peer removed: ${name}`);
|
|
3877
|
+
} else {
|
|
3878
|
+
console.error(`Peer not found: ${name}`);
|
|
3879
|
+
process.exit(1);
|
|
3880
|
+
}
|
|
3881
|
+
});
|
|
3882
|
+
var configCmd = program.command("config").description("Get or set AgentBnB configuration values");
|
|
3883
|
+
configCmd.command("set <key> <value>").description("Set a configuration value").action((key, value) => {
|
|
3884
|
+
const allowedKeys = ["registry", "tier1", "tier2", "reserve", "idle-threshold"];
|
|
3885
|
+
if (!allowedKeys.includes(key)) {
|
|
3886
|
+
console.error(`Unknown config key: ${key}. Valid keys: ${allowedKeys.join(", ")}`);
|
|
3887
|
+
process.exit(1);
|
|
3888
|
+
}
|
|
3889
|
+
const config = loadConfig();
|
|
3890
|
+
if (!config) {
|
|
3891
|
+
console.error("Error: not initialized. Run `agentbnb init` first.");
|
|
3892
|
+
process.exit(1);
|
|
3893
|
+
}
|
|
3894
|
+
if (key === "tier1" || key === "tier2") {
|
|
3895
|
+
const parsed = parseInt(value, 10);
|
|
3896
|
+
if (isNaN(parsed) || parsed < 0) {
|
|
3897
|
+
console.error(`Error: ${key} must be a non-negative integer, got: ${value}`);
|
|
3898
|
+
process.exit(1);
|
|
3899
|
+
}
|
|
3900
|
+
if (!config.autonomy) {
|
|
3901
|
+
config.autonomy = { ...DEFAULT_AUTONOMY_CONFIG };
|
|
3902
|
+
}
|
|
3903
|
+
if (key === "tier1") {
|
|
3904
|
+
config.autonomy.tier1_max_credits = parsed;
|
|
3905
|
+
if (parsed >= config.autonomy.tier2_max_credits && config.autonomy.tier2_max_credits > 0) {
|
|
3906
|
+
console.warn(
|
|
3907
|
+
`Warning: tier1 (${parsed}) >= tier2 (${config.autonomy.tier2_max_credits}). Tier 2 will never be reached \u2014 consider increasing tier2.`
|
|
3908
|
+
);
|
|
3909
|
+
}
|
|
3910
|
+
saveConfig(config);
|
|
3911
|
+
console.log(`Set tier1 = ${parsed} (auto-execute threshold: <${parsed} credits)`);
|
|
3912
|
+
} else {
|
|
3913
|
+
config.autonomy.tier2_max_credits = parsed;
|
|
3914
|
+
if (config.autonomy.tier1_max_credits >= parsed && parsed > 0) {
|
|
3915
|
+
console.warn(
|
|
3916
|
+
`Warning: tier2 (${parsed}) <= tier1 (${config.autonomy.tier1_max_credits}). Tier 2 will never be reached \u2014 consider decreasing tier1.`
|
|
3917
|
+
);
|
|
3918
|
+
}
|
|
3919
|
+
saveConfig(config);
|
|
3920
|
+
console.log(`Set tier2 = ${parsed} (notify threshold: <${parsed} credits)`);
|
|
3921
|
+
}
|
|
3922
|
+
return;
|
|
3923
|
+
}
|
|
3924
|
+
if (key === "reserve") {
|
|
3925
|
+
const parsed = parseInt(value, 10);
|
|
3926
|
+
if (isNaN(parsed) || parsed < 0) {
|
|
3927
|
+
console.error(`Error: reserve must be a non-negative integer, got: ${value}`);
|
|
3928
|
+
process.exit(1);
|
|
3929
|
+
}
|
|
3930
|
+
if (!config.budget) {
|
|
3931
|
+
config.budget = { ...DEFAULT_BUDGET_CONFIG };
|
|
3932
|
+
}
|
|
3933
|
+
config.budget.reserve_credits = parsed;
|
|
3934
|
+
saveConfig(config);
|
|
3935
|
+
console.log(`Set reserve = ${parsed} (credit reserve floor: ${parsed} credits)`);
|
|
3936
|
+
return;
|
|
3937
|
+
}
|
|
3938
|
+
if (key === "idle-threshold") {
|
|
3939
|
+
const parsed = parseFloat(value);
|
|
3940
|
+
if (isNaN(parsed) || parsed < 0 || parsed > 1) {
|
|
3941
|
+
console.error("Error: idle-threshold must be a number between 0 and 1");
|
|
3942
|
+
process.exit(1);
|
|
3943
|
+
}
|
|
3944
|
+
config["idle_threshold"] = parsed;
|
|
3945
|
+
saveConfig(config);
|
|
3946
|
+
console.log(`Set idle-threshold = ${parsed} (idle rate threshold for auto-share)`);
|
|
3947
|
+
return;
|
|
3948
|
+
}
|
|
3949
|
+
config[key] = value;
|
|
3950
|
+
saveConfig(config);
|
|
3951
|
+
console.log(`Set ${key} = ${value}`);
|
|
3952
|
+
});
|
|
3953
|
+
configCmd.command("get <key>").description("Get a configuration value").action((key) => {
|
|
3954
|
+
const config = loadConfig();
|
|
3955
|
+
if (!config) {
|
|
3956
|
+
console.error("Error: not initialized. Run `agentbnb init` first.");
|
|
3957
|
+
process.exit(1);
|
|
3958
|
+
}
|
|
3959
|
+
if (key === "tier1") {
|
|
3960
|
+
console.log(String(config.autonomy?.tier1_max_credits ?? 0));
|
|
3961
|
+
return;
|
|
3962
|
+
}
|
|
3963
|
+
if (key === "tier2") {
|
|
3964
|
+
console.log(String(config.autonomy?.tier2_max_credits ?? 0));
|
|
3965
|
+
return;
|
|
3966
|
+
}
|
|
3967
|
+
if (key === "reserve") {
|
|
3968
|
+
console.log(String(config.budget?.reserve_credits ?? DEFAULT_BUDGET_CONFIG.reserve_credits));
|
|
3969
|
+
return;
|
|
3970
|
+
}
|
|
3971
|
+
if (key === "idle-threshold") {
|
|
3972
|
+
const val = config["idle_threshold"];
|
|
3973
|
+
console.log(val !== void 0 ? String(val) : "0.70");
|
|
3974
|
+
return;
|
|
3975
|
+
}
|
|
3976
|
+
const value = config[key];
|
|
3977
|
+
console.log(value !== void 0 ? String(value) : "(not set)");
|
|
3978
|
+
});
|
|
3979
|
+
var openclaw = program.command("openclaw").description("OpenClaw integration commands");
|
|
3980
|
+
openclaw.command("sync").description("Read SOUL.md and publish/update a v2.0 capability card").option("--soul-path <path>", "Path to SOUL.md", "./SOUL.md").action(async (opts) => {
|
|
3981
|
+
const config = loadConfig();
|
|
3982
|
+
if (!config) {
|
|
3983
|
+
console.error("Error: not initialized. Run `agentbnb init` first.");
|
|
3984
|
+
process.exit(1);
|
|
3985
|
+
}
|
|
3986
|
+
let content;
|
|
3987
|
+
try {
|
|
3988
|
+
content = readFileSync6(opts.soulPath, "utf-8");
|
|
3989
|
+
} catch {
|
|
3990
|
+
console.error(`Error: cannot read SOUL.md at ${opts.soulPath}`);
|
|
3991
|
+
process.exit(1);
|
|
3992
|
+
}
|
|
3993
|
+
const db = openDatabase(config.db_path);
|
|
3994
|
+
try {
|
|
3995
|
+
const card = publishFromSoulV2(db, content, config.owner);
|
|
3996
|
+
console.log(`Published card ${card.id} with ${card.skills.length} skill(s)`);
|
|
3997
|
+
} catch (err) {
|
|
3998
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3999
|
+
console.error(`Error: ${msg}`);
|
|
4000
|
+
process.exit(1);
|
|
4001
|
+
} finally {
|
|
4002
|
+
db.close();
|
|
4003
|
+
}
|
|
4004
|
+
});
|
|
4005
|
+
openclaw.command("status").description("Show OpenClaw integration status, tier config, and skill idle rates").action(async () => {
|
|
4006
|
+
const config = loadConfig();
|
|
4007
|
+
if (!config) {
|
|
4008
|
+
console.error("Error: not initialized. Run `agentbnb init` first.");
|
|
4009
|
+
process.exit(1);
|
|
4010
|
+
}
|
|
4011
|
+
const db = openDatabase(config.db_path);
|
|
4012
|
+
const creditDb = openCreditDb(config.credit_db_path);
|
|
4013
|
+
try {
|
|
4014
|
+
const status = getOpenClawStatus(config, db, creditDb);
|
|
4015
|
+
console.log("AgentBnB OpenClaw Status");
|
|
4016
|
+
console.log(`Owner: ${status.owner}`);
|
|
4017
|
+
console.log(`Gateway: ${status.gateway_url}`);
|
|
4018
|
+
console.log(`Tier 1 (auto): < ${status.tier.tier1_max_credits} credits`);
|
|
4019
|
+
console.log(`Tier 2 (notify): ${status.tier.tier1_max_credits}-${status.tier.tier2_max_credits} credits`);
|
|
4020
|
+
console.log(`Tier 3 (ask): > ${status.tier.tier2_max_credits} credits`);
|
|
4021
|
+
console.log(`Balance: ${status.balance} credits`);
|
|
4022
|
+
console.log(`Reserve: ${status.reserve} credits`);
|
|
4023
|
+
console.log(`Skills: ${status.skills.length}`);
|
|
4024
|
+
for (const skill of status.skills) {
|
|
4025
|
+
console.log(` - ${skill.id}: ${skill.name} (idle: ${skill.idle_rate ?? "N/A"}, online: ${skill.online})`);
|
|
4026
|
+
}
|
|
4027
|
+
} finally {
|
|
4028
|
+
db.close();
|
|
4029
|
+
creditDb.close();
|
|
4030
|
+
}
|
|
4031
|
+
});
|
|
4032
|
+
openclaw.command("rules").description("Print HEARTBEAT.md rules block (or inject into a file with --inject)").option("--inject <path>", "Path to HEARTBEAT.md file to patch with rules block").action(async (opts) => {
|
|
4033
|
+
const config = loadConfig();
|
|
4034
|
+
if (!config) {
|
|
4035
|
+
console.error("Error: not initialized. Run `agentbnb init` first.");
|
|
4036
|
+
process.exit(1);
|
|
4037
|
+
}
|
|
4038
|
+
const autonomy = config.autonomy ?? DEFAULT_AUTONOMY_CONFIG;
|
|
4039
|
+
const budget = config.budget ?? DEFAULT_BUDGET_CONFIG;
|
|
4040
|
+
const section = generateHeartbeatSection(autonomy, budget);
|
|
4041
|
+
if (opts.inject) {
|
|
4042
|
+
injectHeartbeatSection(opts.inject, section);
|
|
4043
|
+
console.log(`Injected AgentBnB rules into ${opts.inject}`);
|
|
4044
|
+
} else {
|
|
4045
|
+
console.log(section);
|
|
4046
|
+
}
|
|
4047
|
+
});
|
|
4048
|
+
await program.parseAsync(process.argv);
|