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/dist/index.js ADDED
@@ -0,0 +1,894 @@
1
+ // src/types/index.ts
2
+ import { z } from "zod";
3
+ var IOSchemaSchema = z.object({
4
+ name: z.string(),
5
+ type: z.enum(["text", "json", "file", "audio", "image", "video", "stream"]),
6
+ description: z.string().optional(),
7
+ required: z.boolean().default(true),
8
+ schema: z.record(z.unknown()).optional()
9
+ // JSON Schema
10
+ });
11
+ var PoweredBySchema = z.object({
12
+ provider: z.string().min(1),
13
+ model: z.string().optional(),
14
+ tier: z.string().optional()
15
+ });
16
+ var CapabilityCardSchema = z.object({
17
+ spec_version: z.literal("1.0").default("1.0"),
18
+ id: z.string().uuid(),
19
+ owner: z.string().min(1),
20
+ name: z.string().min(1).max(100),
21
+ description: z.string().max(500),
22
+ level: z.union([z.literal(1), z.literal(2), z.literal(3)]),
23
+ inputs: z.array(IOSchemaSchema),
24
+ outputs: z.array(IOSchemaSchema),
25
+ pricing: z.object({
26
+ credits_per_call: z.number().nonnegative(),
27
+ credits_per_minute: z.number().nonnegative().optional(),
28
+ /** Number of free monthly calls. Shown as a "N free/mo" badge in the Hub. */
29
+ free_tier: z.number().nonnegative().optional()
30
+ }),
31
+ availability: z.object({
32
+ online: z.boolean(),
33
+ schedule: z.string().optional()
34
+ // cron expression
35
+ }),
36
+ powered_by: z.array(PoweredBySchema).optional(),
37
+ /**
38
+ * Private per-card metadata. Stripped from all API and CLI responses —
39
+ * never transmitted beyond the local store.
40
+ */
41
+ _internal: z.record(z.unknown()).optional(),
42
+ metadata: z.object({
43
+ apis_used: z.array(z.string()).optional(),
44
+ avg_latency_ms: z.number().nonnegative().optional(),
45
+ success_rate: z.number().min(0).max(1).optional(),
46
+ tags: z.array(z.string()).optional()
47
+ }).optional(),
48
+ created_at: z.string().datetime().optional(),
49
+ updated_at: z.string().datetime().optional()
50
+ });
51
+ var SkillSchema = z.object({
52
+ /** Stable skill identifier, e.g. 'tts-elevenlabs'. Used for gateway routing and idle tracking. */
53
+ id: z.string().min(1),
54
+ name: z.string().min(1).max(100),
55
+ description: z.string().max(500),
56
+ level: z.union([z.literal(1), z.literal(2), z.literal(3)]),
57
+ /** Optional grouping category, e.g. 'tts' | 'video_gen' | 'code_review'. */
58
+ category: z.string().optional(),
59
+ inputs: z.array(IOSchemaSchema),
60
+ outputs: z.array(IOSchemaSchema),
61
+ pricing: z.object({
62
+ credits_per_call: z.number().nonnegative(),
63
+ credits_per_minute: z.number().nonnegative().optional(),
64
+ free_tier: z.number().nonnegative().optional()
65
+ }),
66
+ /** Per-skill online flag — overrides card-level availability for this skill. */
67
+ availability: z.object({ online: z.boolean() }).optional(),
68
+ powered_by: z.array(PoweredBySchema).optional(),
69
+ metadata: z.object({
70
+ apis_used: z.array(z.string()).optional(),
71
+ avg_latency_ms: z.number().nonnegative().optional(),
72
+ success_rate: z.number().min(0).max(1).optional(),
73
+ tags: z.array(z.string()).optional(),
74
+ capacity: z.object({
75
+ calls_per_hour: z.number().positive().default(60)
76
+ }).optional()
77
+ }).optional(),
78
+ /**
79
+ * Private per-skill metadata. Stripped from all API and CLI responses —
80
+ * never transmitted beyond the local store.
81
+ */
82
+ _internal: z.record(z.unknown()).optional()
83
+ });
84
+ var CapabilityCardV2Schema = z.object({
85
+ spec_version: z.literal("2.0"),
86
+ id: z.string().uuid(),
87
+ owner: z.string().min(1),
88
+ /** Agent display name — was 'name' in v1.0. */
89
+ agent_name: z.string().min(1).max(100),
90
+ /** At least one skill is required. */
91
+ skills: z.array(SkillSchema).min(1),
92
+ availability: z.object({
93
+ online: z.boolean(),
94
+ schedule: z.string().optional()
95
+ }),
96
+ /** Optional deployment environment metadata. */
97
+ environment: z.object({
98
+ runtime: z.string(),
99
+ region: z.string().optional()
100
+ }).optional(),
101
+ /**
102
+ * Private per-card metadata. Stripped from all API and CLI responses —
103
+ * never transmitted beyond the local store.
104
+ */
105
+ _internal: z.record(z.unknown()).optional(),
106
+ created_at: z.string().datetime().optional(),
107
+ updated_at: z.string().datetime().optional()
108
+ });
109
+ var AnyCardSchema = z.discriminatedUnion("spec_version", [
110
+ CapabilityCardSchema,
111
+ CapabilityCardV2Schema
112
+ ]);
113
+ var AgentBnBError = class extends Error {
114
+ constructor(message, code) {
115
+ super(message);
116
+ this.code = code;
117
+ this.name = "AgentBnBError";
118
+ }
119
+ };
120
+
121
+ // src/registry/store.ts
122
+ import Database from "better-sqlite3";
123
+
124
+ // src/registry/request-log.ts
125
+ function createRequestLogTable(db) {
126
+ db.exec(`
127
+ CREATE TABLE IF NOT EXISTS request_log (
128
+ id TEXT PRIMARY KEY,
129
+ card_id TEXT NOT NULL,
130
+ card_name TEXT NOT NULL,
131
+ requester TEXT NOT NULL,
132
+ status TEXT NOT NULL CHECK(status IN ('success', 'failure', 'timeout')),
133
+ latency_ms INTEGER NOT NULL,
134
+ credits_charged INTEGER NOT NULL,
135
+ created_at TEXT NOT NULL
136
+ );
137
+
138
+ CREATE INDEX IF NOT EXISTS request_log_created_at_idx
139
+ ON request_log (created_at DESC);
140
+ `);
141
+ try {
142
+ db.exec("ALTER TABLE request_log ADD COLUMN skill_id TEXT");
143
+ } catch {
144
+ }
145
+ try {
146
+ db.exec("ALTER TABLE request_log ADD COLUMN action_type TEXT");
147
+ } catch {
148
+ }
149
+ try {
150
+ db.exec("ALTER TABLE request_log ADD COLUMN tier_invoked INTEGER");
151
+ } catch {
152
+ }
153
+ }
154
+ function insertRequestLog(db, entry) {
155
+ const stmt = db.prepare(`
156
+ INSERT INTO request_log (id, card_id, card_name, requester, status, latency_ms, credits_charged, created_at, skill_id, action_type, tier_invoked)
157
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
158
+ `);
159
+ stmt.run(
160
+ entry.id,
161
+ entry.card_id,
162
+ entry.card_name,
163
+ entry.requester,
164
+ entry.status,
165
+ entry.latency_ms,
166
+ entry.credits_charged,
167
+ entry.created_at,
168
+ entry.skill_id ?? null,
169
+ entry.action_type ?? null,
170
+ entry.tier_invoked ?? null
171
+ );
172
+ }
173
+
174
+ // src/registry/store.ts
175
+ var V2_FTS_TRIGGERS = `
176
+ DROP TRIGGER IF EXISTS cards_ai;
177
+ DROP TRIGGER IF EXISTS cards_au;
178
+ DROP TRIGGER IF EXISTS cards_ad;
179
+
180
+ CREATE TRIGGER cards_ai AFTER INSERT ON capability_cards BEGIN
181
+ INSERT INTO cards_fts(rowid, id, owner, name, description, tags)
182
+ VALUES (
183
+ new.rowid,
184
+ new.id,
185
+ new.owner,
186
+ COALESCE(
187
+ (SELECT group_concat(json_extract(value, '$.name'), ' ')
188
+ FROM json_each(json_extract(new.data, '$.skills'))),
189
+ json_extract(new.data, '$.name'),
190
+ ''
191
+ ),
192
+ COALESCE(
193
+ (SELECT group_concat(json_extract(value, '$.description'), ' ')
194
+ FROM json_each(json_extract(new.data, '$.skills'))),
195
+ json_extract(new.data, '$.description'),
196
+ ''
197
+ ),
198
+ COALESCE(
199
+ (SELECT group_concat(json_extract(value, '$.metadata.tags'), ' ')
200
+ FROM json_each(json_extract(new.data, '$.skills'))),
201
+ (SELECT group_concat(value, ' ')
202
+ FROM json_each(json_extract(new.data, '$.metadata.tags'))),
203
+ ''
204
+ )
205
+ );
206
+ END;
207
+
208
+ CREATE TRIGGER cards_au AFTER UPDATE ON capability_cards BEGIN
209
+ INSERT INTO cards_fts(cards_fts, rowid, id, owner, name, description, tags)
210
+ VALUES (
211
+ 'delete',
212
+ old.rowid,
213
+ old.id,
214
+ old.owner,
215
+ COALESCE(
216
+ (SELECT group_concat(json_extract(value, '$.name'), ' ')
217
+ FROM json_each(json_extract(old.data, '$.skills'))),
218
+ json_extract(old.data, '$.name'),
219
+ ''
220
+ ),
221
+ COALESCE(
222
+ (SELECT group_concat(json_extract(value, '$.description'), ' ')
223
+ FROM json_each(json_extract(old.data, '$.skills'))),
224
+ json_extract(old.data, '$.description'),
225
+ ''
226
+ ),
227
+ COALESCE(
228
+ (SELECT group_concat(json_extract(value, '$.metadata.tags'), ' ')
229
+ FROM json_each(json_extract(old.data, '$.skills'))),
230
+ (SELECT group_concat(value, ' ')
231
+ FROM json_each(json_extract(old.data, '$.metadata.tags'))),
232
+ ''
233
+ )
234
+ );
235
+ INSERT INTO cards_fts(rowid, id, owner, name, description, tags)
236
+ VALUES (
237
+ new.rowid,
238
+ new.id,
239
+ new.owner,
240
+ COALESCE(
241
+ (SELECT group_concat(json_extract(value, '$.name'), ' ')
242
+ FROM json_each(json_extract(new.data, '$.skills'))),
243
+ json_extract(new.data, '$.name'),
244
+ ''
245
+ ),
246
+ COALESCE(
247
+ (SELECT group_concat(json_extract(value, '$.description'), ' ')
248
+ FROM json_each(json_extract(new.data, '$.skills'))),
249
+ json_extract(new.data, '$.description'),
250
+ ''
251
+ ),
252
+ COALESCE(
253
+ (SELECT group_concat(json_extract(value, '$.metadata.tags'), ' ')
254
+ FROM json_each(json_extract(new.data, '$.skills'))),
255
+ (SELECT group_concat(value, ' ')
256
+ FROM json_each(json_extract(new.data, '$.metadata.tags'))),
257
+ ''
258
+ )
259
+ );
260
+ END;
261
+
262
+ CREATE TRIGGER cards_ad AFTER DELETE ON capability_cards BEGIN
263
+ INSERT INTO cards_fts(cards_fts, rowid, id, owner, name, description, tags)
264
+ VALUES (
265
+ 'delete',
266
+ old.rowid,
267
+ old.id,
268
+ old.owner,
269
+ COALESCE(
270
+ (SELECT group_concat(json_extract(value, '$.name'), ' ')
271
+ FROM json_each(json_extract(old.data, '$.skills'))),
272
+ json_extract(old.data, '$.name'),
273
+ ''
274
+ ),
275
+ COALESCE(
276
+ (SELECT group_concat(json_extract(value, '$.description'), ' ')
277
+ FROM json_each(json_extract(old.data, '$.skills'))),
278
+ json_extract(old.data, '$.description'),
279
+ ''
280
+ ),
281
+ COALESCE(
282
+ (SELECT group_concat(json_extract(value, '$.metadata.tags'), ' ')
283
+ FROM json_each(json_extract(old.data, '$.skills'))),
284
+ (SELECT group_concat(value, ' ')
285
+ FROM json_each(json_extract(old.data, '$.metadata.tags'))),
286
+ ''
287
+ )
288
+ );
289
+ END;
290
+ `;
291
+ function openDatabase(path = ":memory:") {
292
+ const db = new Database(path);
293
+ db.pragma("journal_mode = WAL");
294
+ db.pragma("foreign_keys = ON");
295
+ db.exec(`
296
+ CREATE TABLE IF NOT EXISTS capability_cards (
297
+ id TEXT PRIMARY KEY,
298
+ owner TEXT NOT NULL,
299
+ data TEXT NOT NULL,
300
+ created_at TEXT NOT NULL,
301
+ updated_at TEXT NOT NULL
302
+ );
303
+
304
+ CREATE TABLE IF NOT EXISTS pending_requests (
305
+ id TEXT PRIMARY KEY,
306
+ skill_query TEXT NOT NULL,
307
+ max_cost_credits REAL NOT NULL,
308
+ selected_peer TEXT,
309
+ selected_card_id TEXT,
310
+ selected_skill_id TEXT,
311
+ credits REAL NOT NULL,
312
+ status TEXT NOT NULL DEFAULT 'pending',
313
+ params TEXT,
314
+ created_at TEXT NOT NULL,
315
+ resolved_at TEXT
316
+ );
317
+
318
+ CREATE VIRTUAL TABLE IF NOT EXISTS cards_fts USING fts5(
319
+ id UNINDEXED,
320
+ owner,
321
+ name,
322
+ description,
323
+ tags,
324
+ content=""
325
+ );
326
+ `);
327
+ createRequestLogTable(db);
328
+ runMigrations(db);
329
+ return db;
330
+ }
331
+ function runMigrations(db) {
332
+ const version = db.pragma("user_version")[0]?.user_version ?? 0;
333
+ if (version < 2) {
334
+ migrateV1toV2(db);
335
+ }
336
+ }
337
+ function migrateV1toV2(db) {
338
+ const migrate = db.transaction(() => {
339
+ const rows = db.prepare("SELECT rowid, id, data FROM capability_cards").all();
340
+ const now = (/* @__PURE__ */ new Date()).toISOString();
341
+ for (const row of rows) {
342
+ const parsed = JSON.parse(row.data);
343
+ if (parsed["spec_version"] === "2.0") continue;
344
+ const v1 = parsed;
345
+ const v2 = {
346
+ spec_version: "2.0",
347
+ id: v1.id,
348
+ owner: v1.owner,
349
+ agent_name: v1.name,
350
+ skills: [
351
+ {
352
+ id: `skill-${v1.id}`,
353
+ name: v1.name,
354
+ description: v1.description,
355
+ level: v1.level,
356
+ inputs: v1.inputs,
357
+ outputs: v1.outputs,
358
+ pricing: v1.pricing,
359
+ availability: { online: v1.availability.online },
360
+ powered_by: v1.powered_by,
361
+ metadata: v1.metadata,
362
+ _internal: v1._internal
363
+ }
364
+ ],
365
+ availability: v1.availability,
366
+ created_at: v1.created_at,
367
+ updated_at: now
368
+ };
369
+ db.prepare("UPDATE capability_cards SET data = ?, updated_at = ? WHERE id = ?").run(
370
+ JSON.stringify(v2),
371
+ now,
372
+ v2.id
373
+ );
374
+ }
375
+ db.exec(V2_FTS_TRIGGERS);
376
+ db.exec(`INSERT INTO cards_fts(cards_fts) VALUES('delete-all')`);
377
+ const allRows = db.prepare("SELECT rowid, id, owner, data FROM capability_cards").all();
378
+ const ftsInsert = db.prepare(
379
+ "INSERT INTO cards_fts(rowid, id, owner, name, description, tags) VALUES (?, ?, ?, ?, ?, ?)"
380
+ );
381
+ for (const row of allRows) {
382
+ const data = JSON.parse(row.data);
383
+ const skills = data["skills"] ?? [];
384
+ let name;
385
+ let description;
386
+ let tags;
387
+ if (skills.length > 0) {
388
+ name = skills.map((s) => String(s["name"] ?? "")).join(" ");
389
+ description = skills.map((s) => String(s["description"] ?? "")).join(" ");
390
+ tags = skills.flatMap((s) => {
391
+ const meta = s["metadata"];
392
+ return meta?.["tags"] ?? [];
393
+ }).join(" ");
394
+ } else {
395
+ name = String(data["name"] ?? "");
396
+ description = String(data["description"] ?? "");
397
+ const meta = data["metadata"];
398
+ const rawTags = meta?.["tags"] ?? [];
399
+ tags = rawTags.join(" ");
400
+ }
401
+ ftsInsert.run(row.rowid, row.id, row.owner, name, description, tags);
402
+ }
403
+ db.pragma("user_version = 2");
404
+ });
405
+ migrate();
406
+ }
407
+ function insertCard(db, card) {
408
+ const now = (/* @__PURE__ */ new Date()).toISOString();
409
+ const withTimestamps = { ...card, created_at: card.created_at ?? now, updated_at: now };
410
+ const parsed = CapabilityCardSchema.safeParse(withTimestamps);
411
+ if (!parsed.success) {
412
+ throw new AgentBnBError(
413
+ `Card validation failed: ${parsed.error.message}`,
414
+ "VALIDATION_ERROR"
415
+ );
416
+ }
417
+ const stmt = db.prepare(`
418
+ INSERT INTO capability_cards (id, owner, data, created_at, updated_at)
419
+ VALUES (?, ?, ?, ?, ?)
420
+ `);
421
+ stmt.run(
422
+ parsed.data.id,
423
+ parsed.data.owner,
424
+ JSON.stringify(parsed.data),
425
+ parsed.data.created_at ?? now,
426
+ parsed.data.updated_at ?? now
427
+ );
428
+ }
429
+ function getCard(db, id) {
430
+ const stmt = db.prepare("SELECT data FROM capability_cards WHERE id = ?");
431
+ const row = stmt.get(id);
432
+ if (!row) return null;
433
+ return JSON.parse(row.data);
434
+ }
435
+ function updateReputation(db, cardId, success, latencyMs) {
436
+ const existing = getCard(db, cardId);
437
+ if (!existing) return;
438
+ const ALPHA = 0.1;
439
+ const observed = success ? 1 : 0;
440
+ const prevSuccessRate = existing.metadata?.success_rate;
441
+ const prevLatency = existing.metadata?.avg_latency_ms;
442
+ const newSuccessRate = prevSuccessRate === void 0 ? observed : ALPHA * observed + (1 - ALPHA) * prevSuccessRate;
443
+ const newLatency = prevLatency === void 0 ? latencyMs : ALPHA * latencyMs + (1 - ALPHA) * prevLatency;
444
+ const now = (/* @__PURE__ */ new Date()).toISOString();
445
+ const updatedMetadata = {
446
+ ...existing.metadata,
447
+ success_rate: Math.round(newSuccessRate * 1e3) / 1e3,
448
+ avg_latency_ms: Math.round(newLatency)
449
+ };
450
+ const updatedCard = { ...existing, metadata: updatedMetadata, updated_at: now };
451
+ const stmt = db.prepare(`
452
+ UPDATE capability_cards
453
+ SET data = ?, updated_at = ?
454
+ WHERE id = ?
455
+ `);
456
+ stmt.run(JSON.stringify(updatedCard), now, cardId);
457
+ }
458
+
459
+ // src/registry/matcher.ts
460
+ function searchCards(db, query, filters = {}) {
461
+ const words = query.trim().split(/\s+/).map((w) => w.replace(/"/g, "")).filter((w) => w.length > 0);
462
+ if (words.length === 0) return [];
463
+ const ftsQuery = words.map((w) => `"${w}"`).join(" OR ");
464
+ const conditions = [];
465
+ const params = [ftsQuery];
466
+ if (filters.level !== void 0) {
467
+ conditions.push(`json_extract(cc.data, '$.level') = ?`);
468
+ params.push(filters.level);
469
+ }
470
+ if (filters.online !== void 0) {
471
+ conditions.push(`json_extract(cc.data, '$.availability.online') = ?`);
472
+ params.push(filters.online ? 1 : 0);
473
+ }
474
+ const whereClause = conditions.length > 0 ? `AND ${conditions.join(" AND ")}` : "";
475
+ const sql = `
476
+ SELECT cc.data
477
+ FROM capability_cards cc
478
+ JOIN cards_fts ON cc.rowid = cards_fts.rowid
479
+ WHERE cards_fts MATCH ?
480
+ ${whereClause}
481
+ ORDER BY bm25(cards_fts)
482
+ LIMIT 50
483
+ `;
484
+ const stmt = db.prepare(sql);
485
+ const rows = stmt.all(...params);
486
+ const results = rows.map((row) => JSON.parse(row.data));
487
+ if (filters.apis_used && filters.apis_used.length > 0) {
488
+ const requiredApis = filters.apis_used;
489
+ return results.filter((card) => {
490
+ const cardApis = card.metadata?.apis_used ?? [];
491
+ return requiredApis.every((api) => cardApis.includes(api));
492
+ });
493
+ }
494
+ return results;
495
+ }
496
+
497
+ // src/credit/ledger.ts
498
+ import Database2 from "better-sqlite3";
499
+ import { randomUUID } from "crypto";
500
+ var CREDIT_SCHEMA = `
501
+ CREATE TABLE IF NOT EXISTS credit_balances (
502
+ owner TEXT PRIMARY KEY,
503
+ balance INTEGER NOT NULL DEFAULT 0,
504
+ updated_at TEXT NOT NULL
505
+ );
506
+
507
+ CREATE TABLE IF NOT EXISTS credit_transactions (
508
+ id TEXT PRIMARY KEY,
509
+ owner TEXT NOT NULL,
510
+ amount INTEGER NOT NULL,
511
+ reason TEXT NOT NULL,
512
+ reference_id TEXT,
513
+ created_at TEXT NOT NULL
514
+ );
515
+
516
+ CREATE TABLE IF NOT EXISTS credit_escrow (
517
+ id TEXT PRIMARY KEY,
518
+ owner TEXT NOT NULL,
519
+ amount INTEGER NOT NULL,
520
+ card_id TEXT NOT NULL,
521
+ status TEXT NOT NULL DEFAULT 'held',
522
+ created_at TEXT NOT NULL,
523
+ settled_at TEXT
524
+ );
525
+
526
+ CREATE INDEX IF NOT EXISTS idx_transactions_owner ON credit_transactions(owner, created_at);
527
+ CREATE INDEX IF NOT EXISTS idx_escrow_owner ON credit_escrow(owner);
528
+ `;
529
+ function openCreditDb(path = ":memory:") {
530
+ const db = new Database2(path);
531
+ db.pragma("journal_mode = WAL");
532
+ db.pragma("foreign_keys = ON");
533
+ db.exec(CREDIT_SCHEMA);
534
+ return db;
535
+ }
536
+ function getBalance(db, owner) {
537
+ const row = db.prepare("SELECT balance FROM credit_balances WHERE owner = ?").get(owner);
538
+ return row?.balance ?? 0;
539
+ }
540
+
541
+ // src/gateway/server.ts
542
+ import Fastify from "fastify";
543
+ import { randomUUID as randomUUID3 } from "crypto";
544
+
545
+ // src/credit/escrow.ts
546
+ import { randomUUID as randomUUID2 } from "crypto";
547
+ function holdEscrow(db, owner, amount, cardId) {
548
+ const escrowId = randomUUID2();
549
+ const now = (/* @__PURE__ */ new Date()).toISOString();
550
+ const hold = db.transaction(() => {
551
+ const row = db.prepare("SELECT balance FROM credit_balances WHERE owner = ?").get(owner);
552
+ if (!row || row.balance < amount) {
553
+ throw new AgentBnBError("Insufficient credits", "INSUFFICIENT_CREDITS");
554
+ }
555
+ db.prepare(
556
+ "UPDATE credit_balances SET balance = balance - ?, updated_at = ? WHERE owner = ? AND balance >= ?"
557
+ ).run(amount, now, owner, amount);
558
+ db.prepare(
559
+ "INSERT INTO credit_escrow (id, owner, amount, card_id, status, created_at) VALUES (?, ?, ?, ?, ?, ?)"
560
+ ).run(escrowId, owner, amount, cardId, "held", now);
561
+ db.prepare(
562
+ "INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
563
+ ).run(randomUUID2(), owner, -amount, "escrow_hold", escrowId, now);
564
+ });
565
+ hold();
566
+ return escrowId;
567
+ }
568
+ function settleEscrow(db, escrowId, recipientOwner) {
569
+ const now = (/* @__PURE__ */ new Date()).toISOString();
570
+ const settle = db.transaction(() => {
571
+ const escrow = db.prepare("SELECT id, owner, amount, status FROM credit_escrow WHERE id = ?").get(escrowId);
572
+ if (!escrow) {
573
+ throw new AgentBnBError(`Escrow not found: ${escrowId}`, "ESCROW_NOT_FOUND");
574
+ }
575
+ if (escrow.status !== "held") {
576
+ throw new AgentBnBError(
577
+ `Escrow ${escrowId} is already ${escrow.status}`,
578
+ "ESCROW_ALREADY_SETTLED"
579
+ );
580
+ }
581
+ db.prepare(
582
+ "INSERT OR IGNORE INTO credit_balances (owner, balance, updated_at) VALUES (?, 0, ?)"
583
+ ).run(recipientOwner, now);
584
+ db.prepare(
585
+ "UPDATE credit_balances SET balance = balance + ?, updated_at = ? WHERE owner = ?"
586
+ ).run(escrow.amount, now, recipientOwner);
587
+ db.prepare(
588
+ "UPDATE credit_escrow SET status = ?, settled_at = ? WHERE id = ?"
589
+ ).run("settled", now, escrowId);
590
+ db.prepare(
591
+ "INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
592
+ ).run(randomUUID2(), recipientOwner, escrow.amount, "settlement", escrowId, now);
593
+ });
594
+ settle();
595
+ }
596
+ function releaseEscrow(db, escrowId) {
597
+ const now = (/* @__PURE__ */ new Date()).toISOString();
598
+ const release = db.transaction(() => {
599
+ const escrow = db.prepare("SELECT id, owner, amount, status FROM credit_escrow WHERE id = ?").get(escrowId);
600
+ if (!escrow) {
601
+ throw new AgentBnBError(`Escrow not found: ${escrowId}`, "ESCROW_NOT_FOUND");
602
+ }
603
+ if (escrow.status !== "held") {
604
+ throw new AgentBnBError(
605
+ `Escrow ${escrowId} is already ${escrow.status}`,
606
+ "ESCROW_ALREADY_SETTLED"
607
+ );
608
+ }
609
+ db.prepare(
610
+ "UPDATE credit_balances SET balance = balance + ?, updated_at = ? WHERE owner = ?"
611
+ ).run(escrow.amount, now, escrow.owner);
612
+ db.prepare(
613
+ "UPDATE credit_escrow SET status = ?, settled_at = ? WHERE id = ?"
614
+ ).run("released", now, escrowId);
615
+ db.prepare(
616
+ "INSERT INTO credit_transactions (id, owner, amount, reason, reference_id, created_at) VALUES (?, ?, ?, ?, ?, ?)"
617
+ ).run(randomUUID2(), escrow.owner, escrow.amount, "refund", escrowId, now);
618
+ });
619
+ release();
620
+ }
621
+
622
+ // src/gateway/server.ts
623
+ var VERSION = "0.0.1";
624
+ function createGatewayServer(opts) {
625
+ const {
626
+ registryDb,
627
+ creditDb,
628
+ tokens,
629
+ handlerUrl,
630
+ timeoutMs = 3e4,
631
+ silent = false,
632
+ skillExecutor
633
+ } = opts;
634
+ const fastify = Fastify({ logger: !silent });
635
+ const tokenSet = new Set(tokens);
636
+ fastify.addHook("onRequest", async (request, reply) => {
637
+ if (request.method === "GET" && request.url === "/health") return;
638
+ const auth = request.headers.authorization;
639
+ if (!auth || !auth.startsWith("Bearer ")) {
640
+ await reply.status(401).send({
641
+ jsonrpc: "2.0",
642
+ id: null,
643
+ error: { code: -32e3, message: "Unauthorized: missing token" }
644
+ });
645
+ return;
646
+ }
647
+ const token = auth.slice("Bearer ".length).trim();
648
+ if (!tokenSet.has(token)) {
649
+ await reply.status(401).send({
650
+ jsonrpc: "2.0",
651
+ id: null,
652
+ error: { code: -32e3, message: "Unauthorized: invalid token" }
653
+ });
654
+ }
655
+ });
656
+ fastify.get("/health", async () => {
657
+ return { status: "ok", version: VERSION, uptime: process.uptime() };
658
+ });
659
+ fastify.post("/rpc", async (request, reply) => {
660
+ const body = request.body;
661
+ if (body.jsonrpc !== "2.0" || !body.method) {
662
+ return reply.status(400).send({
663
+ jsonrpc: "2.0",
664
+ id: body.id ?? null,
665
+ error: { code: -32600, message: "Invalid Request" }
666
+ });
667
+ }
668
+ const id = body.id ?? null;
669
+ if (body.method !== "capability.execute") {
670
+ return reply.send({
671
+ jsonrpc: "2.0",
672
+ id,
673
+ error: { code: -32601, message: "Method not found" }
674
+ });
675
+ }
676
+ const params = body.params ?? {};
677
+ const cardId = params.card_id;
678
+ const skillId = params.skill_id;
679
+ if (!cardId) {
680
+ return reply.send({
681
+ jsonrpc: "2.0",
682
+ id,
683
+ error: { code: -32602, message: "Invalid params: card_id required" }
684
+ });
685
+ }
686
+ const card = getCard(registryDb, cardId);
687
+ if (!card) {
688
+ return reply.send({
689
+ jsonrpc: "2.0",
690
+ id,
691
+ error: { code: -32602, message: `Card not found: ${cardId}` }
692
+ });
693
+ }
694
+ const requester = params.requester ?? "unknown";
695
+ let creditsNeeded;
696
+ let cardName;
697
+ let resolvedSkillId;
698
+ const rawCard = card;
699
+ if (Array.isArray(rawCard["skills"])) {
700
+ const v2card = card;
701
+ const skill = skillId ? v2card.skills.find((s) => s.id === skillId) : v2card.skills[0];
702
+ if (!skill) {
703
+ return reply.send({
704
+ jsonrpc: "2.0",
705
+ id,
706
+ error: { code: -32602, message: `Skill not found: ${skillId}` }
707
+ });
708
+ }
709
+ creditsNeeded = skill.pricing.credits_per_call;
710
+ cardName = skill.name;
711
+ resolvedSkillId = skill.id;
712
+ } else {
713
+ creditsNeeded = card.pricing.credits_per_call;
714
+ cardName = card.name;
715
+ }
716
+ let escrowId;
717
+ try {
718
+ const balance = getBalance(creditDb, requester);
719
+ if (balance < creditsNeeded) {
720
+ return reply.send({
721
+ jsonrpc: "2.0",
722
+ id,
723
+ error: { code: -32603, message: "Insufficient credits" }
724
+ });
725
+ }
726
+ escrowId = holdEscrow(creditDb, requester, creditsNeeded, cardId);
727
+ } catch (err) {
728
+ const msg = err instanceof AgentBnBError ? err.message : "Failed to hold escrow";
729
+ return reply.send({
730
+ jsonrpc: "2.0",
731
+ id,
732
+ error: { code: -32603, message: msg }
733
+ });
734
+ }
735
+ const startMs = Date.now();
736
+ if (skillExecutor) {
737
+ const targetSkillId = resolvedSkillId ?? skillId ?? cardId;
738
+ let execResult;
739
+ try {
740
+ execResult = await skillExecutor.execute(targetSkillId, params);
741
+ } catch (err) {
742
+ releaseEscrow(creditDb, escrowId);
743
+ updateReputation(registryDb, cardId, false, Date.now() - startMs);
744
+ try {
745
+ insertRequestLog(registryDb, {
746
+ id: randomUUID3(),
747
+ card_id: cardId,
748
+ card_name: cardName,
749
+ skill_id: resolvedSkillId,
750
+ requester,
751
+ status: "failure",
752
+ latency_ms: Date.now() - startMs,
753
+ credits_charged: 0,
754
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
755
+ });
756
+ } catch {
757
+ }
758
+ const message = err instanceof Error ? err.message : "Execution error";
759
+ return reply.send({
760
+ jsonrpc: "2.0",
761
+ id,
762
+ error: { code: -32603, message }
763
+ });
764
+ }
765
+ if (!execResult.success) {
766
+ releaseEscrow(creditDb, escrowId);
767
+ updateReputation(registryDb, cardId, false, execResult.latency_ms);
768
+ try {
769
+ insertRequestLog(registryDb, {
770
+ id: randomUUID3(),
771
+ card_id: cardId,
772
+ card_name: cardName,
773
+ skill_id: resolvedSkillId,
774
+ requester,
775
+ status: "failure",
776
+ latency_ms: execResult.latency_ms,
777
+ credits_charged: 0,
778
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
779
+ });
780
+ } catch {
781
+ }
782
+ return reply.send({
783
+ jsonrpc: "2.0",
784
+ id,
785
+ error: { code: -32603, message: execResult.error ?? "Execution failed" }
786
+ });
787
+ }
788
+ settleEscrow(creditDb, escrowId, card.owner);
789
+ updateReputation(registryDb, cardId, true, execResult.latency_ms);
790
+ try {
791
+ insertRequestLog(registryDb, {
792
+ id: randomUUID3(),
793
+ card_id: cardId,
794
+ card_name: cardName,
795
+ skill_id: resolvedSkillId,
796
+ requester,
797
+ status: "success",
798
+ latency_ms: execResult.latency_ms,
799
+ credits_charged: creditsNeeded,
800
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
801
+ });
802
+ } catch {
803
+ }
804
+ return reply.send({ jsonrpc: "2.0", id, result: execResult.result });
805
+ }
806
+ const controller = new AbortController();
807
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
808
+ try {
809
+ const response = await fetch(handlerUrl, {
810
+ method: "POST",
811
+ headers: { "Content-Type": "application/json" },
812
+ body: JSON.stringify({ card_id: cardId, skill_id: resolvedSkillId, params }),
813
+ signal: controller.signal
814
+ });
815
+ clearTimeout(timer);
816
+ if (!response.ok) {
817
+ releaseEscrow(creditDb, escrowId);
818
+ updateReputation(registryDb, cardId, false, Date.now() - startMs);
819
+ try {
820
+ insertRequestLog(registryDb, {
821
+ id: randomUUID3(),
822
+ card_id: cardId,
823
+ card_name: cardName,
824
+ skill_id: resolvedSkillId,
825
+ requester,
826
+ status: "failure",
827
+ latency_ms: Date.now() - startMs,
828
+ credits_charged: 0,
829
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
830
+ });
831
+ } catch {
832
+ }
833
+ return reply.send({
834
+ jsonrpc: "2.0",
835
+ id,
836
+ error: { code: -32603, message: `Handler returned ${response.status}` }
837
+ });
838
+ }
839
+ const result = await response.json();
840
+ settleEscrow(creditDb, escrowId, card.owner);
841
+ updateReputation(registryDb, cardId, true, Date.now() - startMs);
842
+ try {
843
+ insertRequestLog(registryDb, {
844
+ id: randomUUID3(),
845
+ card_id: cardId,
846
+ card_name: cardName,
847
+ skill_id: resolvedSkillId,
848
+ requester,
849
+ status: "success",
850
+ latency_ms: Date.now() - startMs,
851
+ credits_charged: creditsNeeded,
852
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
853
+ });
854
+ } catch {
855
+ }
856
+ return reply.send({ jsonrpc: "2.0", id, result });
857
+ } catch (err) {
858
+ clearTimeout(timer);
859
+ releaseEscrow(creditDb, escrowId);
860
+ updateReputation(registryDb, cardId, false, Date.now() - startMs);
861
+ const isTimeout = err instanceof Error && err.name === "AbortError";
862
+ try {
863
+ insertRequestLog(registryDb, {
864
+ id: randomUUID3(),
865
+ card_id: cardId,
866
+ card_name: cardName,
867
+ skill_id: resolvedSkillId,
868
+ requester,
869
+ status: isTimeout ? "timeout" : "failure",
870
+ latency_ms: Date.now() - startMs,
871
+ credits_charged: 0,
872
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
873
+ });
874
+ } catch {
875
+ }
876
+ return reply.send({
877
+ jsonrpc: "2.0",
878
+ id,
879
+ error: { code: -32603, message: isTimeout ? "Execution timeout" : "Handler error" }
880
+ });
881
+ }
882
+ });
883
+ return fastify;
884
+ }
885
+ export {
886
+ CapabilityCardSchema,
887
+ createGatewayServer,
888
+ getBalance,
889
+ getCard,
890
+ insertCard,
891
+ openCreditDb,
892
+ openDatabase,
893
+ searchCards
894
+ };