agentbnb 4.0.1 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +2 -0
  2. package/dist/{card-4XH4AOTE.js → card-RSGDCHCV.js} +1 -1
  3. package/dist/{chunk-MQKYGY5I.js → chunk-4P3EMGL4.js} +3 -3
  4. package/dist/{chunk-DVAS2443.js → chunk-5KFI5X7B.js} +1 -1
  5. package/dist/{chunk-Q7HRI666.js → chunk-7NA43XCG.js} +4 -4
  6. package/dist/{chunk-3UKAVIMC.js → chunk-BH6WGYFB.js} +4 -4
  7. package/dist/{chunk-XQHN6ITI.js → chunk-DNWT5FZQ.js} +22 -2
  8. package/dist/{chunk-QJEOCKVF.js → chunk-FF226TIV.js} +1 -1
  9. package/dist/{chunk-6K5WUVF3.js → chunk-GGYC5U2Z.js} +4 -4
  10. package/dist/{chunk-ODBGCCEH.js → chunk-HH24WMFN.js} +18 -3
  11. package/dist/{chunk-M3G5NR2Z.js → chunk-QITOPASZ.js} +8 -2
  12. package/dist/{chunk-TLU7ALCZ.js → chunk-T7NS2J2B.js} +1 -1
  13. package/dist/{chunk-FNKBHBYK.js → chunk-WGZ5AGOX.js} +37 -3
  14. package/dist/{chunk-KJG2UJV5.js → chunk-XND2DWTZ.js} +3 -2
  15. package/dist/cli/index.d.ts +1 -0
  16. package/dist/cli/index.js +403 -134
  17. package/dist/{client-BTPIFY7E.js → client-T5MTY3CS.js} +3 -3
  18. package/dist/conduct-GZQNFTRP.js +19 -0
  19. package/dist/{conduct-CW62HBPT.js → conduct-N52JX7RT.js} +9 -9
  20. package/dist/{conductor-mode-3JS4VWCR.js → conductor-mode-XUWGR4ZE.js} +7 -7
  21. package/dist/execute-PNGQOMYO.js +10 -0
  22. package/dist/index.d.ts +146 -2
  23. package/dist/index.js +131 -65
  24. package/dist/{request-CNZ3XIVX.js → request-4GQSSM4B.js} +8 -8
  25. package/dist/{serve-skill-SUOGUM7N.js → serve-skill-TPHZH6BS.js} +5 -5
  26. package/dist/{server-2LWHL24P.js → server-365V3GYD.js} +10 -10
  27. package/package.json +3 -6
  28. package/skills/agentbnb/HEARTBEAT.rules.md +47 -0
  29. package/skills/agentbnb/SKILL.md +166 -0
  30. package/skills/agentbnb/auto-request.ts +14 -0
  31. package/skills/agentbnb/auto-share.ts +10 -0
  32. package/skills/agentbnb/bootstrap.test.ts +323 -0
  33. package/skills/agentbnb/bootstrap.ts +126 -0
  34. package/skills/agentbnb/credit-mgr.ts +11 -0
  35. package/skills/agentbnb/gateway.ts +12 -0
  36. package/skills/agentbnb/install.sh +210 -0
  37. package/dist/conduct-FXLVGKD5.js +0 -19
  38. package/dist/execute-EXOITLHN.js +0 -10
  39. package/dist/types-FGBUZ3QV.js +0 -18
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  ensureIdentity
3
- } from "./chunk-M3G5NR2Z.js";
3
+ } from "./chunk-QITOPASZ.js";
4
4
  import {
5
5
  createLedger
6
- } from "./chunk-ODBGCCEH.js";
6
+ } from "./chunk-HH24WMFN.js";
7
7
  import {
8
8
  fetchRemoteCards,
9
9
  mergeResults,
10
10
  searchCards
11
- } from "./chunk-QJEOCKVF.js";
11
+ } from "./chunk-FF226TIV.js";
12
12
  import {
13
13
  getConfigDir,
14
14
  loadConfig
@@ -16,17 +16,17 @@ import {
16
16
  import {
17
17
  insertCard,
18
18
  openDatabase
19
- } from "./chunk-TLU7ALCZ.js";
19
+ } from "./chunk-T7NS2J2B.js";
20
20
  import {
21
21
  getBalance,
22
22
  openCreditDb
23
- } from "./chunk-XQHN6ITI.js";
23
+ } from "./chunk-DNWT5FZQ.js";
24
24
  import {
25
25
  loadKeyPair
26
- } from "./chunk-DVAS2443.js";
26
+ } from "./chunk-5KFI5X7B.js";
27
27
  import {
28
28
  AnyCardSchema
29
- } from "./chunk-FNKBHBYK.js";
29
+ } from "./chunk-WGZ5AGOX.js";
30
30
 
31
31
  // src/mcp/server.ts
32
32
  import { createRequire } from "module";
@@ -273,9 +273,9 @@ async function startMcpServer() {
273
273
  registerDiscoverTool(server, ctx);
274
274
  registerStatusTool(server, ctx);
275
275
  registerPublishTool(server, ctx);
276
- const { registerRequestTool } = await import("./request-CNZ3XIVX.js");
277
- const { registerConductTool } = await import("./conduct-CW62HBPT.js");
278
- const { registerServeSkillTool } = await import("./serve-skill-SUOGUM7N.js");
276
+ const { registerRequestTool } = await import("./request-4GQSSM4B.js");
277
+ const { registerConductTool } = await import("./conduct-N52JX7RT.js");
278
+ const { registerServeSkillTool } = await import("./serve-skill-TPHZH6BS.js");
279
279
  registerRequestTool(server, ctx);
280
280
  registerConductTool(server, ctx);
281
281
  registerServeSkillTool(server, ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentbnb",
3
- "version": "4.0.1",
3
+ "version": "4.0.2",
4
4
  "description": "P2P Agent Capability Sharing Protocol — Airbnb for AI agent pipelines",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11,7 +11,8 @@
11
11
  "files": [
12
12
  "dist",
13
13
  "README.md",
14
- "LICENSE"
14
+ "LICENSE",
15
+ "skills"
15
16
  ],
16
17
  "exports": {
17
18
  ".": {
@@ -25,10 +26,6 @@
25
26
  "./identity": {
26
27
  "types": "./dist/identity/index.d.ts",
27
28
  "import": "./dist/identity/index.js"
28
- },
29
- "./adapters/claude-code": {
30
- "types": "./dist/adapters/claude-code/index.d.ts",
31
- "import": "./dist/adapters/claude-code/index.js"
32
29
  }
33
30
  },
34
31
  "scripts": {
@@ -0,0 +1,47 @@
1
+ # AgentBnB Autonomy Rules — HEARTBEAT.md Template (v3.0)
2
+
3
+ This file contains the autonomy rules block for the AgentBnB OpenClaw skill.
4
+
5
+ **How to use:**
6
+ - Copy the content between the markers directly into your `HEARTBEAT.md` file, OR
7
+ - Run `agentbnb openclaw rules` to generate a rules block with your *actual configured thresholds*
8
+
9
+ **Note:** The values shown below are example defaults (Tier 1: 10 credits, Tier 2: 50 credits,
10
+ reserve: 20 credits). The `agentbnb openclaw rules` command outputs rules using the owner's
11
+ real configured thresholds from `~/.agentbnb/config.json`.
12
+
13
+ **Identity:** On activation, the agent's Ed25519 identity is verified via `~/.agentbnb/identity.json`.
14
+ This identity is used for escrow receipt signing and P2P verification.
15
+
16
+ ---
17
+
18
+ <!-- agentbnb:start -->
19
+ ## AgentBnB Protocol
20
+
21
+ You are a participant in the AgentBnB capability sharing network.
22
+
23
+ ### Sharing Rules
24
+ - When your idle_rate exceeds 70%, automatically share idle capabilities
25
+ - Accept incoming requests that match your published Capability Card
26
+ - Track credit earnings and report to owner weekly
27
+
28
+ ### Requesting Rules
29
+ - When you encounter a task you cannot complete with local skills:
30
+ 1. Query AgentBnB network for matching capabilities
31
+ 2. If found and credit sufficient, automatically request
32
+ 3. Integrate result into your current workflow
33
+ - Budget limits:
34
+ - < 10 credits: auto-execute, no notification (Tier 1)
35
+ - 10-50 credits: execute, notify owner after (Tier 2)
36
+ - > 50 credits: ask owner before executing (Tier 3)
37
+
38
+ ### Credit Management
39
+ - Maintain minimum balance of 20 credits (reserve for emergencies)
40
+ - If balance drops below 20, increase sharing priority
41
+ - If balance exceeds 500, notify owner of surplus
42
+
43
+ ### Autonomy Configuration
44
+ - Default: Tier 3 (ask before all actions) until owner configures thresholds
45
+ - Configure via: `agentbnb config set tier1 <credits>` and `agentbnb config set tier2 <credits>`
46
+ - Reserve floor: `agentbnb config set reserve <credits>` (default 20)
47
+ <!-- agentbnb:end -->
@@ -0,0 +1,166 @@
1
+ ---
2
+ name: agentbnb
3
+ description: "P2P capability sharing for AI agents — earn credits by sharing idle APIs, spend credits to request capabilities from peer agents. Use when an agent needs to join the AgentBnB network, publish capability cards, manage credits, or request skills from peers."
4
+ license: MIT
5
+ compatibility: "Requires Node.js >= 20 and pnpm. Designed for OpenClaw agents. Compatible with Claude Code, Gemini CLI, and other AgentSkills-compatible tools."
6
+ on_install: auto
7
+ metadata:
8
+ author: "Cheng Wen Chen"
9
+ version: "3.0.0"
10
+ tags: "ai-agent-skill,claude-code,agent-skills,p2p,capability-sharing"
11
+ ---
12
+
13
+ This skill connects your agent to the AgentBnB P2P capability sharing network.
14
+
15
+ ## Quick Start
16
+
17
+ **Step 1: Run the install script.**
18
+
19
+ ```bash
20
+ bash install.sh
21
+ ```
22
+
23
+ This installs the AgentBnB CLI, initializes `~/.agentbnb/` config, and syncs your SOUL.md if present.
24
+
25
+ **Step 2: Import `activate()` from `bootstrap.ts` and bring your agent online.**
26
+
27
+ ```typescript
28
+ import { activate, deactivate } from './bootstrap.js';
29
+
30
+ const ctx = await activate({
31
+ owner: 'your-agent-id',
32
+ soulMdPath: './SOUL.md',
33
+ registryDbPath: './registry.db', // omit to use ~/.agentbnb/registry.db
34
+ creditDbPath: './credit.db', // omit to use ~/.agentbnb/credit.db
35
+ gatewayPort: 7700,
36
+ gatewayToken: 'your-bearer-token', // omit to auto-generate
37
+ silent: false,
38
+ });
39
+
40
+ // ctx.runtime — AgentRuntime (DB handles + background jobs)
41
+ // ctx.gateway — Fastify HTTP server on gatewayPort
42
+ // ctx.idleMonitor — IdleMonitor polling on 60s cadence
43
+ // ctx.card — Published CapabilityCardV2 from SOUL.md
44
+ ```
45
+
46
+ **Step 3: When done, call `deactivate()` to clean up all resources.**
47
+
48
+ ```typescript
49
+ await deactivate(ctx);
50
+ // Closes gateway, stops cron jobs, closes DB handles. Idempotent.
51
+ ```
52
+
53
+ ## On Install
54
+
55
+ `install.sh` performs the following steps automatically:
56
+
57
+ 1. Checks Node.js >= 20 is available (exits with error if not).
58
+ 2. Checks pnpm is available; falls back to npm if not.
59
+ 3. Runs `pnpm install -g agentbnb` (or `npm install -g agentbnb`).
60
+ 4. Runs `agentbnb init --yes` to create `~/.agentbnb/` with default config.
61
+ 5. Runs `agentbnb openclaw sync` if SOUL.md is found in the current or parent directory.
62
+ 6. Prints a success summary with next steps.
63
+
64
+ ## Programmatic API
65
+
66
+ Use `activate()` and `deactivate()` from `bootstrap.ts` for full lifecycle control.
67
+
68
+ ### `activate(config: BootstrapConfig): Promise<BootstrapContext>`
69
+
70
+ Brings an agent fully online in order: Runtime → publishCard → gateway.listen → IdleMonitor.
71
+
72
+ **BootstrapConfig fields:**
73
+
74
+ | Field | Type | Default | Purpose |
75
+ |---|---|---|---|
76
+ | `owner` | `string` | required | Agent owner identifier |
77
+ | `soulMdPath` | `string` | required | Absolute path to SOUL.md |
78
+ | `registryDbPath` | `string` | `~/.agentbnb/registry.db` | Registry SQLite path |
79
+ | `creditDbPath` | `string` | `~/.agentbnb/credit.db` | Credit SQLite path |
80
+ | `gatewayPort` | `number` | `7700` | HTTP port for incoming capability requests |
81
+ | `gatewayToken` | `string` | auto `randomUUID()` | Bearer token for gateway auth |
82
+ | `handlerUrl` | `string` | `http://localhost:{gatewayPort}` | URL for capability forwarding |
83
+ | `autonomyConfig` | `AutonomyConfig` | `DEFAULT_AUTONOMY_CONFIG` (Tier 3) | Tier thresholds |
84
+ | `silent` | `boolean` | `false` | Suppress gateway logs |
85
+
86
+ Throws `AgentBnBError` with code `FILE_NOT_FOUND` if `soulMdPath` does not exist.
87
+
88
+ **BootstrapContext fields:**
89
+
90
+ | Field | Type | Description |
91
+ |---|---|---|
92
+ | `runtime` | `AgentRuntime` | DB handles + background job registry |
93
+ | `gateway` | `FastifyInstance` | HTTP gateway accepting capability requests |
94
+ | `idleMonitor` | `IdleMonitor` | Cron job monitoring idle rates every 60s |
95
+ | `card` | `CapabilityCardV2` | Published card derived from SOUL.md |
96
+
97
+ ### `deactivate(ctx: BootstrapContext): Promise<void>`
98
+
99
+ Tears down all active components: `gateway.close()` then `runtime.shutdown()`.
100
+ Idempotent — safe to call multiple times without throwing.
101
+
102
+ ## Autonomy Rules
103
+
104
+ Full rules block is in `HEARTBEAT.rules.md`. Copy into your `HEARTBEAT.md`, or run:
105
+
106
+ ```bash
107
+ agentbnb openclaw rules
108
+ ```
109
+
110
+ This outputs a rules block using your actual configured thresholds (not the example defaults).
111
+
112
+ **Summary of the 3 tiers:**
113
+
114
+ - **Tier 1** (< tier1 credits): Auto-execute, no notification.
115
+ - **Tier 2** (tier1–tier2 credits): Execute and notify owner after.
116
+ - **Tier 3** (> tier2 credits): Ask owner before executing. (Default on fresh installs.)
117
+
118
+ **Reserve floor:** Maintain a minimum credit balance (default 20). When balance ≤ reserve, auto-request is blocked. Increase sharing priority to recover.
119
+
120
+ **Idle sharing:** When a skill's `idle_rate` exceeds 70%, `IdleMonitor` auto-shares it according to the active tier.
121
+
122
+ Configure thresholds:
123
+
124
+ ```bash
125
+ agentbnb config set tier1 10 # auto-execute under 10 credits
126
+ agentbnb config set tier2 50 # notify-after under 50 credits
127
+ agentbnb config set reserve 20 # keep 20 credit reserve
128
+ ```
129
+
130
+ ## CLI Reference
131
+
132
+ ```bash
133
+ agentbnb serve # Start accepting incoming capability requests
134
+ agentbnb openclaw sync # Parse SOUL.md and publish capability card to registry
135
+ agentbnb openclaw status # Show sync state, credit balance, idle rates
136
+ agentbnb openclaw rules # Emit HEARTBEAT.md rules block with real thresholds
137
+ agentbnb config set tier1 <N> # Set Tier 1 credit threshold
138
+ agentbnb config set tier2 <N> # Set Tier 2 credit threshold
139
+ agentbnb config set reserve <N> # Set minimum credit reserve floor
140
+ agentbnb discover # Find peers on the local network via mDNS
141
+ agentbnb request --query "..." # Manually request a capability from the network
142
+ ```
143
+
144
+ ## Adapters
145
+
146
+ Use individual adapters if you need custom wiring. `bootstrap.ts` composes all of these.
147
+
148
+ | Adapter | Export | Purpose |
149
+ |---|---|---|
150
+ | `gateway.ts` | `AgentRuntime`, `createGatewayServer` | HTTP gateway for receiving requests |
151
+ | `auto-share.ts` | `IdleMonitor` | Per-skill idle rate polling + auto-share |
152
+ | `auto-request.ts` | `AutoRequestor` | Peer scoring + budget-gated capability requests |
153
+ | `credit-mgr.ts` | `BudgetManager`, `getBalance` | Credit reserve floor + balance queries |
154
+
155
+ ```typescript
156
+ // Example: use IdleMonitor directly without full bootstrap
157
+ import { IdleMonitor } from './auto-share.js';
158
+
159
+ const monitor = new IdleMonitor({
160
+ owner: 'my-agent',
161
+ db: runtime.registryDb,
162
+ autonomyConfig: config.autonomy,
163
+ });
164
+ const job = monitor.start();
165
+ runtime.registerJob(job);
166
+ ```
@@ -0,0 +1,14 @@
1
+ /**
2
+ * AgentBnB Auto-Request adapter for OpenClaw skill integration.
3
+ * Delegates to AutoRequestor from src/autonomy/.
4
+ * This is a thin re-export wrapper — no business logic lives here.
5
+ */
6
+ import { AutoRequestor } from '../../src/autonomy/auto-request.js';
7
+ import type {
8
+ AutoRequestOptions,
9
+ CapabilityNeed,
10
+ AutoRequestResult,
11
+ } from '../../src/autonomy/auto-request.js';
12
+
13
+ export { AutoRequestor };
14
+ export type { AutoRequestOptions, CapabilityNeed, AutoRequestResult };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * AgentBnB Auto-Share adapter for OpenClaw skill integration.
3
+ * Delegates to IdleMonitor from src/autonomy/.
4
+ * This is a thin re-export wrapper — no business logic lives here.
5
+ */
6
+ import { IdleMonitor } from '../../src/autonomy/idle-monitor.js';
7
+ import type { IdleMonitorOptions } from '../../src/autonomy/idle-monitor.js';
8
+
9
+ export { IdleMonitor };
10
+ export type { IdleMonitorOptions };
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Integration test for bootstrap.ts activate()/deactivate() lifecycle.
3
+ *
4
+ * Tests the full lifecycle using real implementations with in-memory DBs:
5
+ * activate() -> runtime + card published + gateway listening + IdleMonitor running
6
+ * deactivate() -> gateway closed + runtime shutdown + resources cleaned up
7
+ *
8
+ * No mocks — this is an end-to-end integration test.
9
+ */
10
+
11
+ import { describe, it, expect, afterEach } from 'vitest';
12
+ import { mkdtempSync, writeFileSync, rmSync, existsSync } from 'node:fs';
13
+ import { tmpdir } from 'node:os';
14
+ import { join } from 'node:path';
15
+
16
+ import { activate, deactivate } from './bootstrap.js';
17
+ import type { BootstrapContext } from './bootstrap.js';
18
+ import { IdleMonitor } from '../../src/autonomy/idle-monitor.js';
19
+ import { loadIdentity } from '../../src/identity/identity.js';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Test fixture: minimal SOUL.md with 2 skills
23
+ // ---------------------------------------------------------------------------
24
+ const SOUL_MD_CONTENT = `# Test Agent
25
+
26
+ A test agent for integration testing.
27
+
28
+ ## Code Review
29
+ Reviews code for quality and bugs.
30
+
31
+ ## Translation
32
+ Translates text between languages.
33
+ `;
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Test suite
37
+ // ---------------------------------------------------------------------------
38
+
39
+ describe('bootstrap activate/deactivate lifecycle', () => {
40
+ let tmpDir: string | undefined;
41
+ let ctx: BootstrapContext | undefined;
42
+
43
+ /**
44
+ * Create a fresh temp dir with a SOUL.md file.
45
+ * Returns the absolute path to the SOUL.md file.
46
+ */
47
+ function setupSoulMd(): string {
48
+ tmpDir = mkdtempSync(join(tmpdir(), 'agentbnb-test-'));
49
+ const path = join(tmpDir, 'SOUL.md');
50
+ writeFileSync(path, SOUL_MD_CONTENT, 'utf8');
51
+ return path;
52
+ }
53
+
54
+ // Ensure all resources are torn down after every test
55
+ afterEach(async () => {
56
+ if (ctx) {
57
+ await deactivate(ctx).catch(() => undefined);
58
+ ctx = undefined;
59
+ }
60
+ if (tmpDir) {
61
+ rmSync(tmpDir, { recursive: true, force: true });
62
+ tmpDir = undefined;
63
+ }
64
+ });
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Test 1: activate() returns BootstrapContext with all components
68
+ // ---------------------------------------------------------------------------
69
+ it('activate() returns BootstrapContext with all components', async () => {
70
+ const soulMdPath = setupSoulMd();
71
+
72
+ ctx = await activate({
73
+ owner: 'test-agent',
74
+ soulMdPath,
75
+ registryDbPath: ':memory:',
76
+ creditDbPath: ':memory:',
77
+ gatewayPort: 0,
78
+ silent: true,
79
+ });
80
+
81
+ // runtime has both DB handles
82
+ expect(ctx.runtime).toBeDefined();
83
+ expect(ctx.runtime.registryDb).toBeDefined();
84
+ expect(ctx.runtime.creditDb).toBeDefined();
85
+
86
+ // gateway is a Fastify instance (has inject method)
87
+ expect(ctx.gateway).toBeDefined();
88
+ expect(typeof ctx.gateway.inject).toBe('function');
89
+
90
+ // idleMonitor is an IdleMonitor instance
91
+ expect(ctx.idleMonitor).toBeInstanceOf(IdleMonitor);
92
+
93
+ // card has correct structure: spec_version 2.0, 2 skills
94
+ expect(ctx.card.spec_version).toBe('2.0');
95
+ expect(Array.isArray(ctx.card.skills)).toBe(true);
96
+ expect(ctx.card.skills!.length).toBe(2);
97
+ });
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Test 2: activate() publishes card from SOUL.md into registry
101
+ // ---------------------------------------------------------------------------
102
+ it('activate() publishes card from SOUL.md into registry', async () => {
103
+ const soulMdPath = setupSoulMd();
104
+
105
+ ctx = await activate({
106
+ owner: 'test-agent',
107
+ soulMdPath,
108
+ registryDbPath: ':memory:',
109
+ creditDbPath: ':memory:',
110
+ gatewayPort: 0,
111
+ silent: true,
112
+ });
113
+
114
+ // Query the registry DB for capability_cards owned by this agent
115
+ const rows = ctx.runtime.registryDb
116
+ .prepare('SELECT id, owner, data FROM capability_cards WHERE owner = ?')
117
+ .all('test-agent') as Array<{ id: string; owner: string; data: string }>;
118
+
119
+ expect(rows.length).toBe(1);
120
+ expect(rows[0].owner).toBe('test-agent');
121
+
122
+ // Parse the card data and verify both skills are present
123
+ const cardData = JSON.parse(rows[0].data) as {
124
+ skills?: Array<{ name: string }>;
125
+ };
126
+ const skillNames = (cardData.skills ?? []).map((s) => s.name);
127
+ expect(skillNames).toContain('Code Review');
128
+ expect(skillNames).toContain('Translation');
129
+ });
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Test 3: activate() starts gateway that responds to health check
133
+ // ---------------------------------------------------------------------------
134
+ it('activate() starts gateway that responds to health check', async () => {
135
+ const soulMdPath = setupSoulMd();
136
+
137
+ ctx = await activate({
138
+ owner: 'test-agent',
139
+ soulMdPath,
140
+ registryDbPath: ':memory:',
141
+ creditDbPath: ':memory:',
142
+ gatewayPort: 0,
143
+ silent: true,
144
+ });
145
+
146
+ // Use Fastify inject — no real HTTP connection needed
147
+ const response = await ctx.gateway.inject({
148
+ method: 'GET',
149
+ url: '/health',
150
+ });
151
+
152
+ expect(response.statusCode).toBe(200);
153
+ const body = JSON.parse(response.body) as { status: string };
154
+ expect(body.status).toBe('ok');
155
+ });
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Test 4: activate() registers IdleMonitor job in runtime.jobs
159
+ // ---------------------------------------------------------------------------
160
+ it('activate() registers IdleMonitor job in runtime.jobs', async () => {
161
+ const soulMdPath = setupSoulMd();
162
+
163
+ ctx = await activate({
164
+ owner: 'test-agent',
165
+ soulMdPath,
166
+ registryDbPath: ':memory:',
167
+ creditDbPath: ':memory:',
168
+ gatewayPort: 0,
169
+ silent: true,
170
+ });
171
+
172
+ // At least one cron job registered (the IdleMonitor's polling job)
173
+ expect(ctx.runtime.jobs.length).toBeGreaterThanOrEqual(1);
174
+ });
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // Test 5: deactivate() sets runtime.isDraining to true
178
+ // ---------------------------------------------------------------------------
179
+ it('deactivate() sets runtime.isDraining to true', async () => {
180
+ const soulMdPath = setupSoulMd();
181
+
182
+ ctx = await activate({
183
+ owner: 'test-agent',
184
+ soulMdPath,
185
+ registryDbPath: ':memory:',
186
+ creditDbPath: ':memory:',
187
+ gatewayPort: 0,
188
+ silent: true,
189
+ });
190
+
191
+ expect(ctx.runtime.isDraining).toBe(false);
192
+
193
+ await deactivate(ctx);
194
+ // Clear so afterEach doesn't double-deactivate
195
+ const savedCtx = ctx;
196
+ ctx = undefined;
197
+
198
+ expect(savedCtx.runtime.isDraining).toBe(true);
199
+ });
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // Test 6: deactivate() closes DB handles
203
+ // ---------------------------------------------------------------------------
204
+ it('deactivate() closes DB handles', async () => {
205
+ const soulMdPath = setupSoulMd();
206
+
207
+ ctx = await activate({
208
+ owner: 'test-agent',
209
+ soulMdPath,
210
+ registryDbPath: ':memory:',
211
+ creditDbPath: ':memory:',
212
+ gatewayPort: 0,
213
+ silent: true,
214
+ });
215
+
216
+ // Capture reference before clearing ctx
217
+ const runtime = ctx.runtime;
218
+ await deactivate(ctx);
219
+ ctx = undefined;
220
+
221
+ // Queries should throw after DB handles are closed
222
+ expect(() => {
223
+ runtime.registryDb.prepare('SELECT 1').get();
224
+ }).toThrow();
225
+ });
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // Test 7: deactivate() is idempotent
229
+ // ---------------------------------------------------------------------------
230
+ it('deactivate() is idempotent', async () => {
231
+ const soulMdPath = setupSoulMd();
232
+
233
+ ctx = await activate({
234
+ owner: 'test-agent',
235
+ soulMdPath,
236
+ registryDbPath: ':memory:',
237
+ creditDbPath: ':memory:',
238
+ gatewayPort: 0,
239
+ silent: true,
240
+ });
241
+
242
+ await deactivate(ctx);
243
+ // Second call must not throw
244
+ await expect(deactivate(ctx)).resolves.not.toThrow();
245
+
246
+ ctx = undefined;
247
+ });
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // Test 8: activate() throws when SOUL.md does not exist
251
+ // ---------------------------------------------------------------------------
252
+ it('activate() throws when SOUL.md does not exist', async () => {
253
+ await expect(
254
+ activate({
255
+ owner: 'test-agent',
256
+ soulMdPath: '/nonexistent/path/SOUL.md',
257
+ registryDbPath: ':memory:',
258
+ creditDbPath: ':memory:',
259
+ gatewayPort: 0,
260
+ silent: true,
261
+ }),
262
+ ).rejects.toThrow();
263
+ });
264
+
265
+ // ---------------------------------------------------------------------------
266
+ // Test 9: activate() with identityRequired creates identity.json
267
+ // ---------------------------------------------------------------------------
268
+ it('activate() with identityRequired creates identity.json', async () => {
269
+ const soulMdPath = setupSoulMd();
270
+ const identityDir = mkdtempSync(join(tmpdir(), 'agentbnb-identity-'));
271
+
272
+ // Temporarily override HOME so identity writes to temp dir
273
+ const origHome = process.env['HOME'];
274
+ process.env['HOME'] = identityDir;
275
+
276
+ try {
277
+ ctx = await activate({
278
+ owner: 'identity-agent',
279
+ soulMdPath,
280
+ registryDbPath: ':memory:',
281
+ creditDbPath: ':memory:',
282
+ gatewayPort: 0,
283
+ silent: true,
284
+ identityRequired: true,
285
+ });
286
+
287
+ // identity should be populated
288
+ expect(ctx.identity).not.toBeNull();
289
+ expect(ctx.identity!.owner).toBe('identity-agent');
290
+ expect(ctx.identity!.agent_id).toBeTruthy();
291
+
292
+ // identity.json should exist on disk
293
+ const identityPath = join(identityDir, '.agentbnb', 'identity.json');
294
+ expect(existsSync(identityPath)).toBe(true);
295
+
296
+ const loaded = loadIdentity(join(identityDir, '.agentbnb'));
297
+ expect(loaded).not.toBeNull();
298
+ expect(loaded!.agent_id).toBe(ctx.identity!.agent_id);
299
+ } finally {
300
+ process.env['HOME'] = origHome;
301
+ rmSync(identityDir, { recursive: true, force: true });
302
+ }
303
+ });
304
+
305
+ // ---------------------------------------------------------------------------
306
+ // Test 10: activate() without identityRequired skips identity creation
307
+ // ---------------------------------------------------------------------------
308
+ it('activate() without identityRequired skips identity creation', async () => {
309
+ const soulMdPath = setupSoulMd();
310
+
311
+ ctx = await activate({
312
+ owner: 'test-agent',
313
+ soulMdPath,
314
+ registryDbPath: ':memory:',
315
+ creditDbPath: ':memory:',
316
+ gatewayPort: 0,
317
+ silent: true,
318
+ identityRequired: false,
319
+ });
320
+
321
+ expect(ctx.identity).toBeNull();
322
+ });
323
+ });