agentbnb 7.0.0 → 8.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/{conductor-mode-2GSLHVN6.js → chunk-P4LOYSLA.js} +624 -260
- package/dist/cli/index.js +15 -5
- package/dist/conductor-mode-TFCVCQHU.js +266 -0
- package/dist/{server-MHMAYXWZ.js → server-LMY2A3GT.js} +2 -4
- package/dist/skills/agentbnb/bootstrap.js +775 -28
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/agentbnb/bootstrap.test.ts +94 -5
- package/skills/agentbnb/bootstrap.ts +139 -17
- package/skills/agentbnb/openclaw-tools.test.ts +328 -0
- package/skills/agentbnb/openclaw-tools.ts +297 -0
- package/dist/chunk-B2VJTKO5.js +0 -393
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "agentbnb",
|
|
3
3
|
"name": "AgentBnB",
|
|
4
4
|
"description": "Where AI agents hire AI agents — join the network, hire agent specialists, earn credits, and coordinate with peers. Agents run autonomously on their own will.",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "8.2.0",
|
|
6
6
|
"kind": "tools",
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED
|
@@ -55,7 +55,8 @@ vi.mock('../../src/registry/store.js', () => ({
|
|
|
55
55
|
|
|
56
56
|
import { loadConfig } from '../../src/cli/config.js';
|
|
57
57
|
import { activate, deactivate } from './bootstrap.js';
|
|
58
|
-
import
|
|
58
|
+
import bootstrapDefault from './bootstrap.js';
|
|
59
|
+
import type { BootstrapContext, OnboardDeps } from './bootstrap.js';
|
|
59
60
|
|
|
60
61
|
const mockLoadConfig = vi.mocked(loadConfig);
|
|
61
62
|
|
|
@@ -104,16 +105,71 @@ describe('bootstrap activate/deactivate lifecycle', () => {
|
|
|
104
105
|
});
|
|
105
106
|
|
|
106
107
|
// ---------------------------------------------------------------------------
|
|
107
|
-
// Test 1:
|
|
108
|
+
// Test 1: INIT_FAILED when CLI not found and config missing
|
|
108
109
|
// ---------------------------------------------------------------------------
|
|
109
|
-
it('activate() throws
|
|
110
|
+
it('activate() throws INIT_FAILED when CLI not found and config missing', async () => {
|
|
110
111
|
mockLoadConfig.mockReturnValue(null);
|
|
112
|
+
const deps: OnboardDeps = {
|
|
113
|
+
findCli: () => null,
|
|
114
|
+
runCommand: vi.fn(),
|
|
115
|
+
};
|
|
111
116
|
|
|
112
|
-
await expect(activate()).rejects.toMatchObject({
|
|
113
|
-
code: '
|
|
117
|
+
await expect(activate({}, deps)).rejects.toMatchObject({
|
|
118
|
+
code: 'INIT_FAILED',
|
|
114
119
|
});
|
|
115
120
|
});
|
|
116
121
|
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Test 1b: Auto-onboard runs init + openclaw sync when config missing
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
it('activate() auto-onboards when config missing and CLI available', async () => {
|
|
126
|
+
mockLoadConfig.mockReturnValueOnce(null).mockReturnValue(MINIMAL_CONFIG as ReturnType<typeof loadConfig>);
|
|
127
|
+
const mockRun = vi.fn().mockResolvedValue({ stdout: '', stderr: '' });
|
|
128
|
+
const deps: OnboardDeps = {
|
|
129
|
+
findCli: () => '/usr/local/bin/agentbnb',
|
|
130
|
+
runCommand: mockRun,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
ctx = await activate({}, deps);
|
|
134
|
+
|
|
135
|
+
expect(mockRun).toHaveBeenCalledTimes(2);
|
|
136
|
+
expect(mockRun.mock.calls[0][0]).toMatch(/agentbnb init --owner .* --yes --no-detect/);
|
|
137
|
+
expect(mockRun.mock.calls[1][0]).toBe('agentbnb openclaw sync');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Test 1c: Auto-onboard continues if openclaw sync fails
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
it('activate() continues if openclaw sync fails during auto-onboard', async () => {
|
|
144
|
+
mockLoadConfig.mockReturnValueOnce(null).mockReturnValue(MINIMAL_CONFIG as ReturnType<typeof loadConfig>);
|
|
145
|
+
const mockRun = vi.fn()
|
|
146
|
+
.mockResolvedValueOnce({ stdout: '', stderr: '' })
|
|
147
|
+
.mockRejectedValueOnce(new Error('SOUL.md not found'));
|
|
148
|
+
const deps: OnboardDeps = {
|
|
149
|
+
findCli: () => '/usr/local/bin/agentbnb',
|
|
150
|
+
runCommand: mockRun,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
ctx = await activate({}, deps);
|
|
154
|
+
|
|
155
|
+
expect(ctx.startDisposition).toBe('started');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Test 1d: Skips auto-onboard when config already exists
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
it('activate() skips auto-onboard when config already exists', async () => {
|
|
162
|
+
const mockRun = vi.fn();
|
|
163
|
+
const deps: OnboardDeps = {
|
|
164
|
+
findCli: () => '/usr/local/bin/agentbnb',
|
|
165
|
+
runCommand: mockRun,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
ctx = await activate({}, deps);
|
|
169
|
+
|
|
170
|
+
expect(mockRun).not.toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
|
|
117
173
|
// ---------------------------------------------------------------------------
|
|
118
174
|
// Test 2: BootstrapContext shape
|
|
119
175
|
// ---------------------------------------------------------------------------
|
|
@@ -230,3 +286,36 @@ describe('bootstrap activate/deactivate lifecycle', () => {
|
|
|
230
286
|
expect(process.listenerCount('SIGTERM')).toBeLessThan(sigtermAfterActivate);
|
|
231
287
|
});
|
|
232
288
|
});
|
|
289
|
+
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// Default export (OpenClaw plugin definition)
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
describe('bootstrap default export (OpenClaw plugin definition)', () => {
|
|
295
|
+
it('has id, name, description, and register', () => {
|
|
296
|
+
expect(bootstrapDefault).toHaveProperty('id', 'agentbnb');
|
|
297
|
+
expect(bootstrapDefault).toHaveProperty('name', 'AgentBnB');
|
|
298
|
+
expect(bootstrapDefault).toHaveProperty('description');
|
|
299
|
+
expect(typeof bootstrapDefault.description).toBe('string');
|
|
300
|
+
expect(bootstrapDefault).toHaveProperty('register');
|
|
301
|
+
expect(typeof bootstrapDefault.register).toBe('function');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('register() calls api.registerTool with a factory', () => {
|
|
305
|
+
const mockRegisterTool = vi.fn();
|
|
306
|
+
bootstrapDefault.register({ registerTool: mockRegisterTool });
|
|
307
|
+
expect(mockRegisterTool).toHaveBeenCalledTimes(1);
|
|
308
|
+
expect(typeof mockRegisterTool.mock.calls[0][0]).toBe('function');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('factory returns 5 tools', () => {
|
|
312
|
+
let factory: ((ctx: { workspaceDir?: string; agentDir?: string }) => unknown[]) | undefined;
|
|
313
|
+
const mockRegisterTool = vi.fn((fn: typeof factory) => { factory = fn; });
|
|
314
|
+
bootstrapDefault.register({ registerTool: mockRegisterTool });
|
|
315
|
+
|
|
316
|
+
// Call the factory with a mock context — it will fail on config read,
|
|
317
|
+
// so we mock createAllTools indirectly by checking the factory is callable
|
|
318
|
+
expect(factory).toBeDefined();
|
|
319
|
+
expect(typeof factory).toBe('function');
|
|
320
|
+
});
|
|
321
|
+
});
|
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
* Teardown: `await deactivate(ctx);`
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { join } from 'node:path';
|
|
11
|
+
import { join, basename, dirname } from 'node:path';
|
|
12
12
|
import { existsSync } from 'node:fs';
|
|
13
13
|
import { homedir } from 'node:os';
|
|
14
|
-
import { spawnSync } from 'node:child_process';
|
|
14
|
+
import { spawnSync, exec } from 'node:child_process';
|
|
15
|
+
import { promisify } from 'node:util';
|
|
15
16
|
import { randomUUID } from 'node:crypto';
|
|
16
17
|
|
|
18
|
+
const execAsync = promisify(exec);
|
|
19
|
+
|
|
17
20
|
/**
|
|
18
21
|
* Derives a workspace-specific AGENTBNB_DIR using a priority chain:
|
|
19
22
|
*
|
|
@@ -170,6 +173,109 @@ function registerDecomposerCard(configDir: string, owner: string): void {
|
|
|
170
173
|
}
|
|
171
174
|
}
|
|
172
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Checks if the `agentbnb` CLI is available in PATH.
|
|
178
|
+
* @returns Absolute path to the CLI, or null if not found.
|
|
179
|
+
*/
|
|
180
|
+
export function findCli(): string | null {
|
|
181
|
+
const result = spawnSync('which', ['agentbnb'], { encoding: 'utf-8', stdio: 'pipe' });
|
|
182
|
+
if (result.status === 0 && result.stdout.trim()) {
|
|
183
|
+
return result.stdout.trim();
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Runs a shell command asynchronously. Exported for test injection.
|
|
190
|
+
*/
|
|
191
|
+
export async function runCommand(cmd: string, env: Record<string, string | undefined>): Promise<{ stdout: string; stderr: string }> {
|
|
192
|
+
return execAsync(cmd, { env });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Derives a human-readable agent name from the config directory path.
|
|
197
|
+
* If configDir is `~/.openclaw/agents/genesis-bot/.agentbnb`, returns "genesis-bot".
|
|
198
|
+
* Falls back to a random identifier.
|
|
199
|
+
*/
|
|
200
|
+
function deriveAgentName(configDir: string): string {
|
|
201
|
+
// configDir is typically <workspace>/.agentbnb — parent dir is the agent workspace
|
|
202
|
+
const parent = basename(dirname(configDir));
|
|
203
|
+
if (parent && parent !== '.' && parent !== '.agentbnb' && parent !== homedir().split('/').pop()) {
|
|
204
|
+
return parent;
|
|
205
|
+
}
|
|
206
|
+
return `agent-${randomUUID().slice(0, 8)}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* First-time auto-onboarding: initializes identity, publishes capabilities, and
|
|
211
|
+
* grants the Demand Voucher (50 credits). Called when activate() detects no config.json.
|
|
212
|
+
*
|
|
213
|
+
* Steps:
|
|
214
|
+
* 1. Check agentbnb CLI is available
|
|
215
|
+
* 2. Run `agentbnb init --owner <name> --yes --no-detect` (keypair + config)
|
|
216
|
+
* 3. Run `agentbnb openclaw sync` (publish SOUL.md capabilities)
|
|
217
|
+
* 4. Demand Voucher is auto-issued by the registry on first registration
|
|
218
|
+
*
|
|
219
|
+
* @param configDir - The AGENTBNB_DIR for this agent.
|
|
220
|
+
* @returns The loaded AgentBnBConfig after init.
|
|
221
|
+
* @throws {AgentBnBError} INIT_FAILED if CLI not found or init fails.
|
|
222
|
+
*/
|
|
223
|
+
/** Injectable dependencies for autoOnboard (test seam). */
|
|
224
|
+
export interface OnboardDeps {
|
|
225
|
+
findCli: () => string | null;
|
|
226
|
+
runCommand: (cmd: string, env: Record<string, string | undefined>) => Promise<{ stdout: string; stderr: string }>;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Default production dependencies. */
|
|
230
|
+
const defaultDeps: OnboardDeps = { findCli, runCommand };
|
|
231
|
+
|
|
232
|
+
async function autoOnboard(configDir: string, deps: OnboardDeps = defaultDeps): Promise<import('./../../src/cli/config.js').AgentBnBConfig> {
|
|
233
|
+
process.stderr.write('[agentbnb] First-time setup: initializing agent identity...\n');
|
|
234
|
+
|
|
235
|
+
// Step 0: Check CLI exists
|
|
236
|
+
const cliPath = deps.findCli();
|
|
237
|
+
if (!cliPath) {
|
|
238
|
+
process.stderr.write('[agentbnb] CLI not found. Run: npm install -g agentbnb\n');
|
|
239
|
+
throw new AgentBnBError(
|
|
240
|
+
'agentbnb CLI not found in PATH. Install with: npm install -g agentbnb',
|
|
241
|
+
'INIT_FAILED',
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const env = { ...process.env, AGENTBNB_DIR: configDir };
|
|
246
|
+
const agentName = deriveAgentName(configDir);
|
|
247
|
+
|
|
248
|
+
// Step 1: Initialize identity (keypair + config.json + credit bootstrap)
|
|
249
|
+
try {
|
|
250
|
+
await deps.runCommand(`agentbnb init --owner "${agentName}" --yes --no-detect`, env);
|
|
251
|
+
process.stderr.write(`[agentbnb] Agent "${agentName}" initialized.\n`);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
254
|
+
throw new AgentBnBError(`Auto-init failed: ${msg}`, 'INIT_FAILED');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Step 2: Publish capabilities from SOUL.md (if it exists)
|
|
258
|
+
try {
|
|
259
|
+
await deps.runCommand('agentbnb openclaw sync', env);
|
|
260
|
+
process.stderr.write('[agentbnb] Capabilities published from SOUL.md.\n');
|
|
261
|
+
} catch {
|
|
262
|
+
// Non-fatal: SOUL.md may not exist yet, or sync may fail for other reasons.
|
|
263
|
+
// Agent is still initialized and can publish capabilities later.
|
|
264
|
+
process.stderr.write('[agentbnb] Note: openclaw sync skipped (SOUL.md may not exist yet).\n');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Step 3: Demand Voucher (50 credits) is auto-issued by bootstrapAgent() during init.
|
|
268
|
+
// No action needed here.
|
|
269
|
+
|
|
270
|
+
const config = loadConfig();
|
|
271
|
+
if (!config) {
|
|
272
|
+
throw new AgentBnBError('AgentBnB config still not found after auto-init', 'CONFIG_NOT_FOUND');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
process.stderr.write('[agentbnb] Agent initialized and published to AgentBnB network.\n');
|
|
276
|
+
return config;
|
|
277
|
+
}
|
|
278
|
+
|
|
173
279
|
/**
|
|
174
280
|
* Brings an AgentBnB node online (idempotent — safe to call when already running).
|
|
175
281
|
* Registers SIGTERM/SIGINT handlers that conditionally stop the node on process exit.
|
|
@@ -179,7 +285,7 @@ function registerDecomposerCard(configDir: string, owner: string): void {
|
|
|
179
285
|
* TODO: Once ServiceCoordinator gains its own signal handling, remove the handlers
|
|
180
286
|
* registered here to avoid double-handler conflicts. Track in Layer A implementation.
|
|
181
287
|
*/
|
|
182
|
-
export async function activate(config: BootstrapConfig = {}): Promise<BootstrapContext> {
|
|
288
|
+
export async function activate(config: BootstrapConfig = {}, _onboardDeps?: OnboardDeps): Promise<BootstrapContext> {
|
|
183
289
|
// Per-workspace isolation: determine the correct config directory.
|
|
184
290
|
// Priority: config.agentDir > config.workspaceDir/.agentbnb > AGENTBNB_DIR env > resolveWorkspaceDir()
|
|
185
291
|
if (config.agentDir) {
|
|
@@ -211,20 +317,8 @@ export async function activate(config: BootstrapConfig = {}): Promise<BootstrapC
|
|
|
211
317
|
|
|
212
318
|
let agentConfig = loadConfig();
|
|
213
319
|
if (!agentConfig) {
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
stdio: 'pipe',
|
|
217
|
-
env: { ...process.env },
|
|
218
|
-
encoding: 'utf-8',
|
|
219
|
-
});
|
|
220
|
-
if (result.error || result.status !== 0) {
|
|
221
|
-
const msg = result.error?.message ?? (result.stderr as string | null)?.trim() ?? 'agentbnb init failed';
|
|
222
|
-
throw new AgentBnBError(`Auto-init failed: ${msg}`, 'INIT_FAILED');
|
|
223
|
-
}
|
|
224
|
-
agentConfig = loadConfig();
|
|
225
|
-
if (!agentConfig) {
|
|
226
|
-
throw new AgentBnBError('AgentBnB config still not found after auto-init', 'CONFIG_NOT_FOUND');
|
|
227
|
-
}
|
|
320
|
+
// First-time setup: auto-onboard this agent onto the AgentBnB network.
|
|
321
|
+
agentConfig = await autoOnboard(configDir, _onboardDeps);
|
|
228
322
|
}
|
|
229
323
|
|
|
230
324
|
// Print startup diagnostic so it's always visible in agent logs.
|
|
@@ -293,3 +387,31 @@ export async function deactivate(ctx: BootstrapContext): Promise<void> {
|
|
|
293
387
|
}
|
|
294
388
|
}
|
|
295
389
|
}
|
|
390
|
+
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
// OpenClaw Plugin Definition — default export
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
import { createAllTools, resetContextCache } from './openclaw-tools.js';
|
|
396
|
+
import type { AgentTool } from './openclaw-tools.js';
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* OpenClaw plugin definition. When OpenClaw loads this plugin, `register()`
|
|
400
|
+
* is called to register the AgentBnB tools with the bot's LLM.
|
|
401
|
+
*
|
|
402
|
+
* The existing named exports (activate, deactivate, findCli, runCommand)
|
|
403
|
+
* remain unchanged — they handle the node lifecycle. This default export
|
|
404
|
+
* adds tool registration so the LLM can discover, hire, and orchestrate agents.
|
|
405
|
+
*/
|
|
406
|
+
export default {
|
|
407
|
+
id: 'agentbnb',
|
|
408
|
+
name: 'AgentBnB',
|
|
409
|
+
description:
|
|
410
|
+
'Where AI agents hire AI agents — discover, request, and orchestrate agent capabilities.',
|
|
411
|
+
|
|
412
|
+
register(api: { registerTool: (factory: (toolCtx: { workspaceDir?: string; agentDir?: string }) => AgentTool[]) => void }) {
|
|
413
|
+
api.registerTool(
|
|
414
|
+
(toolCtx: { workspaceDir?: string; agentDir?: string }) => createAllTools(toolCtx),
|
|
415
|
+
);
|
|
416
|
+
},
|
|
417
|
+
};
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for openclaw-tools.ts — OpenClaw tool factory.
|
|
3
|
+
*
|
|
4
|
+
* Tests verify:
|
|
5
|
+
* - Tool factory returns 5 tools with correct shape
|
|
6
|
+
* - Tool names are kebab-case with agentbnb- prefix
|
|
7
|
+
* - Parameter schemas are valid JSON Schema
|
|
8
|
+
* - Result conversion from MCP format to AgentToolResult
|
|
9
|
+
* - Context caching and multi-agent isolation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
13
|
+
import { mkdtempSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
createAllTools,
|
|
19
|
+
createDiscoverTool,
|
|
20
|
+
createRequestTool,
|
|
21
|
+
createConductTool,
|
|
22
|
+
createStatusTool,
|
|
23
|
+
createPublishTool,
|
|
24
|
+
toAgentToolResult,
|
|
25
|
+
buildMcpContext,
|
|
26
|
+
resolveConfigDir,
|
|
27
|
+
resetContextCache,
|
|
28
|
+
} from './openclaw-tools.js';
|
|
29
|
+
import type { AgentTool, OpenClawToolContext } from './openclaw-tools.js';
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Mocks
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
vi.mock('../../src/identity/identity.js', () => ({
|
|
36
|
+
ensureIdentity: vi.fn((_dir: string, owner: string) => ({
|
|
37
|
+
agent_id: 'mock-agent-id',
|
|
38
|
+
owner,
|
|
39
|
+
public_key: 'deadbeef'.repeat(8),
|
|
40
|
+
created_at: '2026-01-01T00:00:00.000Z',
|
|
41
|
+
})),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
vi.mock('../../src/mcp/tools/discover.js', () => ({
|
|
45
|
+
handleDiscover: vi.fn(async () => ({
|
|
46
|
+
content: [{ type: 'text', text: JSON.stringify({ results: [], count: 0 }) }],
|
|
47
|
+
})),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
vi.mock('../../src/mcp/tools/request.js', () => ({
|
|
51
|
+
handleRequest: vi.fn(async () => ({
|
|
52
|
+
content: [{ type: 'text', text: JSON.stringify({ success: true }) }],
|
|
53
|
+
})),
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
vi.mock('../../src/mcp/tools/conduct.js', () => ({
|
|
57
|
+
handleConduct: vi.fn(async () => ({
|
|
58
|
+
content: [{ type: 'text', text: JSON.stringify({ success: true, plan: [] }) }],
|
|
59
|
+
})),
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
vi.mock('../../src/mcp/tools/status.js', () => ({
|
|
63
|
+
handleStatus: vi.fn(async () => ({
|
|
64
|
+
content: [{ type: 'text', text: JSON.stringify({ agent_id: 'mock', balance: 50 }) }],
|
|
65
|
+
})),
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
vi.mock('../../src/mcp/tools/publish.js', () => ({
|
|
69
|
+
handlePublish: vi.fn(async () => ({
|
|
70
|
+
content: [{ type: 'text', text: JSON.stringify({ success: true, card_id: 'abc' }) }],
|
|
71
|
+
})),
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Helpers
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Creates a temp dir ending with `.agentbnb` containing config.json.
|
|
80
|
+
* resolveConfigDir() uses the dir as-is when it ends with `.agentbnb`.
|
|
81
|
+
*/
|
|
82
|
+
function createTempConfigDir(): string {
|
|
83
|
+
const parent = mkdtempSync(join(tmpdir(), 'agentbnb-test-'));
|
|
84
|
+
const configDir = join(parent, '.agentbnb');
|
|
85
|
+
mkdirSync(configDir, { recursive: true });
|
|
86
|
+
writeFileSync(
|
|
87
|
+
join(configDir, 'config.json'),
|
|
88
|
+
JSON.stringify({
|
|
89
|
+
owner: 'test-agent',
|
|
90
|
+
gateway_url: 'http://localhost:7700',
|
|
91
|
+
gateway_port: 7700,
|
|
92
|
+
db_path: ':memory:',
|
|
93
|
+
credit_db_path: ':memory:',
|
|
94
|
+
token: 'test-token',
|
|
95
|
+
registry: 'https://hub.agentbnb.dev',
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
return configDir;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Tests
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
describe('openclaw-tools', () => {
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
resetContextCache();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
afterEach(() => {
|
|
111
|
+
resetContextCache();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// -------------------------------------------------------------------------
|
|
115
|
+
// 1. Tool factory shape
|
|
116
|
+
// -------------------------------------------------------------------------
|
|
117
|
+
describe('createAllTools()', () => {
|
|
118
|
+
it('returns array of 5 tools', () => {
|
|
119
|
+
const configDir = createTempConfigDir();
|
|
120
|
+
const tools = createAllTools({ agentDir: configDir });
|
|
121
|
+
expect(tools).toHaveLength(5);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('each tool has required properties', () => {
|
|
125
|
+
const configDir = createTempConfigDir();
|
|
126
|
+
const tools = createAllTools({ agentDir: configDir });
|
|
127
|
+
for (const tool of tools) {
|
|
128
|
+
expect(tool).toHaveProperty('name');
|
|
129
|
+
expect(tool).toHaveProperty('label');
|
|
130
|
+
expect(tool).toHaveProperty('description');
|
|
131
|
+
expect(tool).toHaveProperty('parameters');
|
|
132
|
+
expect(tool).toHaveProperty('execute');
|
|
133
|
+
expect(typeof tool.name).toBe('string');
|
|
134
|
+
expect(typeof tool.label).toBe('string');
|
|
135
|
+
expect(typeof tool.description).toBe('string');
|
|
136
|
+
expect(typeof tool.parameters).toBe('object');
|
|
137
|
+
expect(typeof tool.execute).toBe('function');
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// -------------------------------------------------------------------------
|
|
143
|
+
// 2. Tool names — kebab-case with agentbnb- prefix
|
|
144
|
+
// -------------------------------------------------------------------------
|
|
145
|
+
describe('tool names', () => {
|
|
146
|
+
it('all names follow agentbnb-<action> pattern', () => {
|
|
147
|
+
const configDir = createTempConfigDir();
|
|
148
|
+
const tools = createAllTools({ agentDir: configDir });
|
|
149
|
+
const names = tools.map((t) => t.name);
|
|
150
|
+
|
|
151
|
+
expect(names).toEqual([
|
|
152
|
+
'agentbnb-discover',
|
|
153
|
+
'agentbnb-request',
|
|
154
|
+
'agentbnb-conduct',
|
|
155
|
+
'agentbnb-status',
|
|
156
|
+
'agentbnb-publish',
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
for (const name of names) {
|
|
160
|
+
expect(name).toMatch(/^agentbnb-[a-z]+$/);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// -------------------------------------------------------------------------
|
|
166
|
+
// 3. Parameter schemas
|
|
167
|
+
// -------------------------------------------------------------------------
|
|
168
|
+
describe('parameter schemas', () => {
|
|
169
|
+
it('discover requires query', () => {
|
|
170
|
+
const configDir = createTempConfigDir();
|
|
171
|
+
const tool = createDiscoverTool({ agentDir: configDir });
|
|
172
|
+
const schema = tool.parameters as { required: string[]; properties: Record<string, unknown> };
|
|
173
|
+
expect(schema.required).toContain('query');
|
|
174
|
+
expect(schema.properties).toHaveProperty('query');
|
|
175
|
+
expect(schema.properties).toHaveProperty('level');
|
|
176
|
+
expect(schema.properties).toHaveProperty('online_only');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('request has no required fields', () => {
|
|
180
|
+
const configDir = createTempConfigDir();
|
|
181
|
+
const tool = createRequestTool({ agentDir: configDir });
|
|
182
|
+
const schema = tool.parameters as { required: string[] };
|
|
183
|
+
expect(schema.required).toEqual([]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('conduct requires task', () => {
|
|
187
|
+
const configDir = createTempConfigDir();
|
|
188
|
+
const tool = createConductTool({ agentDir: configDir });
|
|
189
|
+
const schema = tool.parameters as { required: string[] };
|
|
190
|
+
expect(schema.required).toContain('task');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('status has no required fields', () => {
|
|
194
|
+
const configDir = createTempConfigDir();
|
|
195
|
+
const tool = createStatusTool({ agentDir: configDir });
|
|
196
|
+
const schema = tool.parameters as { required: string[]; properties: Record<string, unknown> };
|
|
197
|
+
expect(schema.required).toEqual([]);
|
|
198
|
+
expect(Object.keys(schema.properties)).toHaveLength(0);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('publish requires card_json', () => {
|
|
202
|
+
const configDir = createTempConfigDir();
|
|
203
|
+
const tool = createPublishTool({ agentDir: configDir });
|
|
204
|
+
const schema = tool.parameters as { required: string[] };
|
|
205
|
+
expect(schema.required).toContain('card_json');
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// -------------------------------------------------------------------------
|
|
210
|
+
// 4. Result conversion
|
|
211
|
+
// -------------------------------------------------------------------------
|
|
212
|
+
describe('toAgentToolResult()', () => {
|
|
213
|
+
it('converts MCP format to AgentToolResult with parsed details', () => {
|
|
214
|
+
const mcpResult = {
|
|
215
|
+
content: [{ type: 'text' as const, text: '{"results":[],"count":0}' }],
|
|
216
|
+
};
|
|
217
|
+
const result = toAgentToolResult(mcpResult);
|
|
218
|
+
expect(result.content).toBe('{"results":[],"count":0}');
|
|
219
|
+
expect(result.details).toEqual({ results: [], count: 0 });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('handles non-JSON text gracefully', () => {
|
|
223
|
+
const mcpResult = {
|
|
224
|
+
content: [{ type: 'text' as const, text: 'not json' }],
|
|
225
|
+
};
|
|
226
|
+
const result = toAgentToolResult(mcpResult);
|
|
227
|
+
expect(result.content).toBe('not json');
|
|
228
|
+
expect(result.details).toBeUndefined();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('handles empty content array', () => {
|
|
232
|
+
const mcpResult = { content: [] as Array<{ type: string; text: string }> };
|
|
233
|
+
const result = toAgentToolResult(mcpResult);
|
|
234
|
+
expect(result.content).toBe('{}');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// -------------------------------------------------------------------------
|
|
239
|
+
// 5. Context caching
|
|
240
|
+
// -------------------------------------------------------------------------
|
|
241
|
+
describe('context caching', () => {
|
|
242
|
+
it('same configDir reuses cached context', () => {
|
|
243
|
+
const configDir = createTempConfigDir();
|
|
244
|
+
const ctx1 = buildMcpContext({ agentDir: configDir });
|
|
245
|
+
const ctx2 = buildMcpContext({ agentDir: configDir });
|
|
246
|
+
expect(ctx1).toBe(ctx2); // same reference
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('resetContextCache() clears the cache', () => {
|
|
250
|
+
const configDir = createTempConfigDir();
|
|
251
|
+
const ctx1 = buildMcpContext({ agentDir: configDir });
|
|
252
|
+
resetContextCache();
|
|
253
|
+
const ctx2 = buildMcpContext({ agentDir: configDir });
|
|
254
|
+
expect(ctx1).not.toBe(ctx2); // different reference
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// -------------------------------------------------------------------------
|
|
259
|
+
// 6. Multi-agent isolation
|
|
260
|
+
// -------------------------------------------------------------------------
|
|
261
|
+
describe('multi-agent isolation', () => {
|
|
262
|
+
it('different workspaceDir values produce different contexts', () => {
|
|
263
|
+
const dir1 = createTempConfigDir();
|
|
264
|
+
const dir2 = createTempConfigDir();
|
|
265
|
+
|
|
266
|
+
const ctx1 = buildMcpContext({ agentDir: dir1 });
|
|
267
|
+
const ctx2 = buildMcpContext({ agentDir: dir2 });
|
|
268
|
+
|
|
269
|
+
expect(ctx1).not.toBe(ctx2);
|
|
270
|
+
expect(ctx1.configDir).not.toBe(ctx2.configDir);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// -------------------------------------------------------------------------
|
|
275
|
+
// 7. resolveConfigDir
|
|
276
|
+
// -------------------------------------------------------------------------
|
|
277
|
+
describe('resolveConfigDir()', () => {
|
|
278
|
+
it('uses agentDir directly if it ends with .agentbnb', () => {
|
|
279
|
+
const dir = resolveConfigDir({ agentDir: '/tmp/test/.agentbnb' });
|
|
280
|
+
expect(dir).toBe('/tmp/test/.agentbnb');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('appends .agentbnb to agentDir if it does not end with .agentbnb', () => {
|
|
284
|
+
const dir = resolveConfigDir({ agentDir: '/tmp/test-agent' });
|
|
285
|
+
expect(dir).toBe('/tmp/test-agent/.agentbnb');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('derives from workspaceDir when agentDir is not set', () => {
|
|
289
|
+
const dir = resolveConfigDir({ workspaceDir: '/tmp/workspace' });
|
|
290
|
+
expect(dir).toBe('/tmp/workspace/.agentbnb');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('falls back to ~/.agentbnb when neither is set', () => {
|
|
294
|
+
const dir = resolveConfigDir({});
|
|
295
|
+
expect(dir).toMatch(/\.agentbnb$/);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// -------------------------------------------------------------------------
|
|
300
|
+
// 8. buildMcpContext error on missing config
|
|
301
|
+
// -------------------------------------------------------------------------
|
|
302
|
+
describe('buildMcpContext()', () => {
|
|
303
|
+
it('throws when config.json does not exist', () => {
|
|
304
|
+
const dir = mkdtempSync(join(tmpdir(), 'agentbnb-noconfig-'));
|
|
305
|
+
expect(() => buildMcpContext({ agentDir: dir })).toThrow(/not initialized/);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// -------------------------------------------------------------------------
|
|
310
|
+
// 9. Tool execution delegates to MCP handlers
|
|
311
|
+
// -------------------------------------------------------------------------
|
|
312
|
+
describe('tool execution', () => {
|
|
313
|
+
it('discover tool delegates to handleDiscover', async () => {
|
|
314
|
+
const configDir = createTempConfigDir();
|
|
315
|
+
const tool = createDiscoverTool({ agentDir: configDir });
|
|
316
|
+
const result = await tool.execute('call-1', { query: 'stock analysis' });
|
|
317
|
+
expect(result.content).toBeTruthy();
|
|
318
|
+
expect(result.details).toEqual({ results: [], count: 0 });
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('status tool delegates to handleStatus', async () => {
|
|
322
|
+
const configDir = createTempConfigDir();
|
|
323
|
+
const tool = createStatusTool({ agentDir: configDir });
|
|
324
|
+
const result = await tool.execute('call-2', {});
|
|
325
|
+
expect(result.details).toEqual({ agent_id: 'mock', balance: 50 });
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
});
|