agentbnb 4.0.0 → 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.
- package/README.md +2 -0
- package/dist/{card-IE5UV5QX.js → card-RSGDCHCV.js} +11 -4
- package/dist/chunk-3MJT4PZG.js +50 -0
- package/dist/{chunk-HEVXCYCY.js → chunk-4P3EMGL4.js} +61 -24
- package/dist/chunk-5AH3CMOX.js +62 -0
- package/dist/{chunk-QO67IGCW.js → chunk-5KFI5X7B.js} +1 -1
- package/dist/chunk-75OC6E4F.js +33 -0
- package/dist/{chunk-CUVIWPQO.js → chunk-7NA43XCG.js} +7 -6
- package/dist/{conduct-IQYAT6ZU.js → chunk-BH6WGYFB.js} +70 -33
- package/dist/{chunk-QVV2P3FN.js → chunk-DNWT5FZQ.js} +22 -2
- package/dist/chunk-FF226TIV.js +148 -0
- package/dist/{chunk-UJWYE7VL.js → chunk-GGYC5U2Z.js} +28 -111
- package/dist/chunk-HH24WMFN.js +373 -0
- package/dist/{websocket-client-5TIQDYQ4.js → chunk-JOY533UH.js} +38 -4
- package/dist/chunk-QITOPASZ.js +96 -0
- package/dist/{chunk-3Y36WQDV.js → chunk-QT7TEVNV.js} +14 -2
- package/dist/{chunk-UOGDK2S2.js → chunk-T7NS2J2B.js} +1 -1
- package/dist/{chunk-XA63SD4T.js → chunk-WGZ5AGOX.js} +37 -0
- package/dist/{chunk-RSX4SCPN.js → chunk-XND2DWTZ.js} +4 -3
- package/dist/cli/index.js +2924 -835
- package/dist/{client-IOTK6GOS.js → client-T5MTY3CS.js} +3 -3
- package/dist/conduct-GZQNFTRP.js +19 -0
- package/dist/conduct-N52JX7RT.js +52 -0
- package/dist/{conductor-mode-XU7ONJWC.js → conductor-mode-XUWGR4ZE.js} +16 -9
- package/dist/execute-PNGQOMYO.js +10 -0
- package/dist/index.d.ts +1148 -915
- package/dist/index.js +589 -127
- package/dist/{peers-G36URZYB.js → peers-K7FSHPN3.js} +2 -1
- package/dist/request-4GQSSM4B.js +196 -0
- package/dist/serve-skill-TPHZH6BS.js +104 -0
- package/dist/server-365V3GYD.js +295 -0
- package/dist/websocket-client-6IIDGXKB.js +7 -0
- package/package.json +3 -6
- package/skills/agentbnb/HEARTBEAT.rules.md +47 -0
- package/skills/agentbnb/SKILL.md +166 -0
- package/skills/agentbnb/auto-request.ts +14 -0
- package/skills/agentbnb/auto-share.ts +10 -0
- package/skills/agentbnb/bootstrap.test.ts +323 -0
- package/skills/agentbnb/bootstrap.ts +126 -0
- package/skills/agentbnb/credit-mgr.ts +11 -0
- package/skills/agentbnb/gateway.ts +12 -0
- package/skills/agentbnb/install.sh +210 -0
- package/dist/chunk-BEI5MTNZ.js +0 -91
- package/dist/execute-GDGBU6DJ.js +0 -10
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentBnB Bootstrap — single-call OpenClaw skill lifecycle entry point.
|
|
3
|
+
*
|
|
4
|
+
* Usage: `const ctx = await activate({ owner: 'alice', soulMdPath: './SOUL.md' });`
|
|
5
|
+
* Teardown: `await deactivate(ctx);`
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import type { FastifyInstance } from 'fastify';
|
|
13
|
+
|
|
14
|
+
import { AgentRuntime } from '../../src/runtime/agent-runtime.js';
|
|
15
|
+
import { publishFromSoulV2 } from '../../src/openclaw/soul-sync.js';
|
|
16
|
+
import { createGatewayServer } from '../../src/gateway/server.js';
|
|
17
|
+
import { IdleMonitor } from '../../src/autonomy/idle-monitor.js';
|
|
18
|
+
import { DEFAULT_AUTONOMY_CONFIG } from '../../src/autonomy/tiers.js';
|
|
19
|
+
import { AgentBnBError } from '../../src/types/index.js';
|
|
20
|
+
import { ensureIdentity, type AgentIdentity } from '../../src/identity/identity.js';
|
|
21
|
+
import type { AutonomyConfig } from '../../src/autonomy/tiers.js';
|
|
22
|
+
import type { CapabilityCardV2 } from '../../src/types/index.js';
|
|
23
|
+
|
|
24
|
+
/** Configuration for bringing an AgentBnB agent online. */
|
|
25
|
+
export interface BootstrapConfig {
|
|
26
|
+
/** Agent owner identifier. */
|
|
27
|
+
owner: string;
|
|
28
|
+
/** Absolute path to SOUL.md. */
|
|
29
|
+
soulMdPath: string;
|
|
30
|
+
/** Registry DB path. Defaults to ~/.agentbnb/registry.db */
|
|
31
|
+
registryDbPath?: string;
|
|
32
|
+
/** Credit DB path. Defaults to ~/.agentbnb/credit.db */
|
|
33
|
+
creditDbPath?: string;
|
|
34
|
+
/** Gateway port. Defaults to 7700. */
|
|
35
|
+
gatewayPort?: number;
|
|
36
|
+
/** Bearer token for gateway auth. Defaults to a random UUID. */
|
|
37
|
+
gatewayToken?: string;
|
|
38
|
+
/** Handler URL for capability forwarding. Defaults to http://localhost:{gatewayPort}. */
|
|
39
|
+
handlerUrl?: string;
|
|
40
|
+
/** Autonomy tier config. Defaults to DEFAULT_AUTONOMY_CONFIG (Tier 3). */
|
|
41
|
+
autonomyConfig?: AutonomyConfig;
|
|
42
|
+
/** Suppress gateway logs. Defaults to false. */
|
|
43
|
+
silent?: boolean;
|
|
44
|
+
/** When true, ensures identity.json exists on activate. Defaults to true. */
|
|
45
|
+
identityRequired?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Live handles returned by activate(). Pass to deactivate() for clean teardown. */
|
|
49
|
+
export interface BootstrapContext {
|
|
50
|
+
/** AgentRuntime managing DBs and background job lifecycle. */
|
|
51
|
+
runtime: AgentRuntime;
|
|
52
|
+
/** Fastify gateway HTTP server instance. */
|
|
53
|
+
gateway: FastifyInstance;
|
|
54
|
+
/** IdleMonitor background loop. */
|
|
55
|
+
idleMonitor: IdleMonitor;
|
|
56
|
+
/** Published CapabilityCard derived from SOUL.md. */
|
|
57
|
+
card: CapabilityCardV2;
|
|
58
|
+
/** Agent identity (created/loaded during activation). */
|
|
59
|
+
identity: AgentIdentity | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Brings an agent fully online: Runtime -> publishCard -> gateway.listen -> IdleMonitor.
|
|
64
|
+
* @throws {AgentBnBError} FILE_NOT_FOUND if SOUL.md does not exist.
|
|
65
|
+
*/
|
|
66
|
+
export async function activate(config: BootstrapConfig): Promise<BootstrapContext> {
|
|
67
|
+
const {
|
|
68
|
+
owner,
|
|
69
|
+
soulMdPath,
|
|
70
|
+
registryDbPath = join(homedir(), '.agentbnb', 'registry.db'),
|
|
71
|
+
creditDbPath = join(homedir(), '.agentbnb', 'credit.db'),
|
|
72
|
+
gatewayPort = 7700,
|
|
73
|
+
gatewayToken = randomUUID(),
|
|
74
|
+
autonomyConfig = DEFAULT_AUTONOMY_CONFIG,
|
|
75
|
+
silent = false,
|
|
76
|
+
} = config;
|
|
77
|
+
|
|
78
|
+
const identityRequired = config.identityRequired ?? false;
|
|
79
|
+
const handlerUrl = config.handlerUrl ?? `http://localhost:${gatewayPort}`;
|
|
80
|
+
|
|
81
|
+
if (!existsSync(soulMdPath)) {
|
|
82
|
+
throw new AgentBnBError(`SOUL.md not found at path: ${soulMdPath}`, 'FILE_NOT_FOUND');
|
|
83
|
+
}
|
|
84
|
+
const soulContent = readFileSync(soulMdPath, 'utf8');
|
|
85
|
+
|
|
86
|
+
const runtime = new AgentRuntime({ registryDbPath, creditDbPath, owner });
|
|
87
|
+
await runtime.start();
|
|
88
|
+
|
|
89
|
+
// Ensure agent identity exists (idempotent — preserves existing identity)
|
|
90
|
+
let identity: AgentIdentity | null = null;
|
|
91
|
+
if (identityRequired) {
|
|
92
|
+
const configDir = join(homedir(), '.agentbnb');
|
|
93
|
+
identity = ensureIdentity(configDir, owner);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const card = publishFromSoulV2(runtime.registryDb, soulContent, owner);
|
|
97
|
+
|
|
98
|
+
const gateway = createGatewayServer({
|
|
99
|
+
port: gatewayPort,
|
|
100
|
+
registryDb: runtime.registryDb,
|
|
101
|
+
creditDb: runtime.creditDb,
|
|
102
|
+
tokens: [gatewayToken],
|
|
103
|
+
handlerUrl,
|
|
104
|
+
silent,
|
|
105
|
+
});
|
|
106
|
+
await gateway.listen({ port: gatewayPort, host: '0.0.0.0' });
|
|
107
|
+
|
|
108
|
+
const idleMonitor = new IdleMonitor({ owner, db: runtime.registryDb, autonomyConfig });
|
|
109
|
+
const idleJob = idleMonitor.start();
|
|
110
|
+
runtime.registerJob(idleJob);
|
|
111
|
+
|
|
112
|
+
return { runtime, gateway, idleMonitor, card, identity };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Tears down all active components: gateway.close() then runtime.shutdown().
|
|
117
|
+
* Idempotent — safe to call multiple times.
|
|
118
|
+
*/
|
|
119
|
+
export async function deactivate(ctx: BootstrapContext): Promise<void> {
|
|
120
|
+
try {
|
|
121
|
+
await ctx.gateway.close();
|
|
122
|
+
await ctx.runtime.shutdown();
|
|
123
|
+
} catch {
|
|
124
|
+
// Swallow errors — idempotent teardown
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentBnB Credit Management adapter for OpenClaw skill integration.
|
|
3
|
+
* Delegates to BudgetManager and getBalance from src/credit/.
|
|
4
|
+
* This is a thin re-export wrapper — no business logic lives here.
|
|
5
|
+
*/
|
|
6
|
+
import { BudgetManager, DEFAULT_BUDGET_CONFIG } from '../../src/credit/budget.js';
|
|
7
|
+
import { getBalance } from '../../src/credit/ledger.js';
|
|
8
|
+
import type { BudgetConfig } from '../../src/credit/budget.js';
|
|
9
|
+
|
|
10
|
+
export { BudgetManager, DEFAULT_BUDGET_CONFIG, getBalance };
|
|
11
|
+
export type { BudgetConfig };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentBnB Gateway adapter for OpenClaw skill integration.
|
|
3
|
+
* Delegates to AgentRuntime and createGatewayServer from src/.
|
|
4
|
+
* This is a thin re-export wrapper — no business logic lives here.
|
|
5
|
+
*/
|
|
6
|
+
import { AgentRuntime } from '../../src/runtime/agent-runtime.js';
|
|
7
|
+
import { createGatewayServer } from '../../src/gateway/server.js';
|
|
8
|
+
import type { RuntimeOptions } from '../../src/runtime/agent-runtime.js';
|
|
9
|
+
import type { GatewayOptions } from '../../src/gateway/server.js';
|
|
10
|
+
|
|
11
|
+
export { AgentRuntime, createGatewayServer };
|
|
12
|
+
export type { RuntimeOptions, GatewayOptions };
|