claw-harness 0.1.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.
Files changed (102) hide show
  1. package/README.md +152 -0
  2. package/dist/agent-client.d.ts +17 -0
  3. package/dist/agent-client.d.ts.map +1 -0
  4. package/dist/agent-client.js +79 -0
  5. package/dist/agent-client.js.map +1 -0
  6. package/dist/agent-client.test.d.ts +2 -0
  7. package/dist/agent-client.test.d.ts.map +1 -0
  8. package/dist/agent-client.test.js +57 -0
  9. package/dist/agent-client.test.js.map +1 -0
  10. package/dist/bench.d.ts +45 -0
  11. package/dist/bench.d.ts.map +1 -0
  12. package/dist/bench.js +133 -0
  13. package/dist/bench.js.map +1 -0
  14. package/dist/bench.test.d.ts +2 -0
  15. package/dist/bench.test.d.ts.map +1 -0
  16. package/dist/bench.test.js +95 -0
  17. package/dist/bench.test.js.map +1 -0
  18. package/dist/bin/clawbench.d.ts +11 -0
  19. package/dist/bin/clawbench.d.ts.map +1 -0
  20. package/dist/bin/clawbench.js +237 -0
  21. package/dist/bin/clawbench.js.map +1 -0
  22. package/dist/bot.d.ts +36 -0
  23. package/dist/bot.d.ts.map +1 -0
  24. package/dist/bot.js +74 -0
  25. package/dist/bot.js.map +1 -0
  26. package/dist/bot.test.d.ts +2 -0
  27. package/dist/bot.test.d.ts.map +1 -0
  28. package/dist/bot.test.js +109 -0
  29. package/dist/bot.test.js.map +1 -0
  30. package/dist/cost-tracker.d.ts +10 -0
  31. package/dist/cost-tracker.d.ts.map +1 -0
  32. package/dist/cost-tracker.js +81 -0
  33. package/dist/cost-tracker.js.map +1 -0
  34. package/dist/cost-tracker.test.d.ts +2 -0
  35. package/dist/cost-tracker.test.d.ts.map +1 -0
  36. package/dist/cost-tracker.test.js +75 -0
  37. package/dist/cost-tracker.test.js.map +1 -0
  38. package/dist/docker-gateway.d.ts +40 -0
  39. package/dist/docker-gateway.d.ts.map +1 -0
  40. package/dist/docker-gateway.js +172 -0
  41. package/dist/docker-gateway.js.map +1 -0
  42. package/dist/docker-gateway.test.d.ts +2 -0
  43. package/dist/docker-gateway.test.d.ts.map +1 -0
  44. package/dist/docker-gateway.test.js +116 -0
  45. package/dist/docker-gateway.test.js.map +1 -0
  46. package/dist/gateway.d.ts +32 -0
  47. package/dist/gateway.d.ts.map +1 -0
  48. package/dist/gateway.js +142 -0
  49. package/dist/gateway.js.map +1 -0
  50. package/dist/gateway.test.d.ts +2 -0
  51. package/dist/gateway.test.d.ts.map +1 -0
  52. package/dist/gateway.test.js +19 -0
  53. package/dist/gateway.test.js.map +1 -0
  54. package/dist/index.d.ts +30 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +28 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/runner.d.ts +13 -0
  59. package/dist/runner.d.ts.map +1 -0
  60. package/dist/runner.js +213 -0
  61. package/dist/runner.js.map +1 -0
  62. package/dist/runner.test.d.ts +2 -0
  63. package/dist/runner.test.d.ts.map +1 -0
  64. package/dist/runner.test.js +298 -0
  65. package/dist/runner.test.js.map +1 -0
  66. package/dist/scenario-loader.d.ts +6 -0
  67. package/dist/scenario-loader.d.ts.map +1 -0
  68. package/dist/scenario-loader.js +77 -0
  69. package/dist/scenario-loader.js.map +1 -0
  70. package/dist/scenario-loader.test.d.ts +2 -0
  71. package/dist/scenario-loader.test.d.ts.map +1 -0
  72. package/dist/scenario-loader.test.js +213 -0
  73. package/dist/scenario-loader.test.js.map +1 -0
  74. package/dist/types.d.ts +155 -0
  75. package/dist/types.d.ts.map +1 -0
  76. package/dist/types.js +5 -0
  77. package/dist/types.js.map +1 -0
  78. package/dist/utils.d.ts +27 -0
  79. package/dist/utils.d.ts.map +1 -0
  80. package/dist/utils.js +106 -0
  81. package/dist/utils.js.map +1 -0
  82. package/dist/utils.test.d.ts +2 -0
  83. package/dist/utils.test.d.ts.map +1 -0
  84. package/dist/utils.test.js +55 -0
  85. package/dist/utils.test.js.map +1 -0
  86. package/dist/workspace.d.ts +33 -0
  87. package/dist/workspace.d.ts.map +1 -0
  88. package/dist/workspace.js +198 -0
  89. package/dist/workspace.js.map +1 -0
  90. package/dist/workspace.test.d.ts +2 -0
  91. package/dist/workspace.test.d.ts.map +1 -0
  92. package/dist/workspace.test.js +68 -0
  93. package/dist/workspace.test.js.map +1 -0
  94. package/docker/Dockerfile +12 -0
  95. package/docker/entrypoint.sh +6 -0
  96. package/package.json +49 -0
  97. package/presets/configs/default.json5 +28 -0
  98. package/presets/configs/minimal.json5 +22 -0
  99. package/presets/personas/curious.md +5 -0
  100. package/presets/personas/friendly.md +5 -0
  101. package/presets/personas/terse.md +3 -0
  102. package/presets/scenarios/example-chat.yaml +71 -0
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { deepMerge, parseDuration, getPackageRoot } from './utils.js';
3
+ import { existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ describe('deepMerge', () => {
6
+ it('merges flat objects', () => {
7
+ expect(deepMerge({ a: 1 }, { b: 2 })).toEqual({ a: 1, b: 2 });
8
+ });
9
+ it('merges nested objects', () => {
10
+ const target = { gateway: { port: 8080, auth: { token: 'old' } } };
11
+ const source = { gateway: { auth: { token: 'new' } } };
12
+ expect(deepMerge(target, source)).toEqual({
13
+ gateway: { port: 8080, auth: { token: 'new' } },
14
+ });
15
+ });
16
+ it('replaces arrays instead of concatenating', () => {
17
+ const target = { list: [1, 2, 3] };
18
+ const source = { list: [4, 5] };
19
+ expect(deepMerge(target, source)).toEqual({ list: [4, 5] });
20
+ });
21
+ it('skips undefined values in source', () => {
22
+ const target = { a: 1, b: 2 };
23
+ const source = { a: undefined, b: 3 };
24
+ expect(deepMerge(target, source)).toEqual({ a: 1, b: 3 });
25
+ });
26
+ it('does not mutate the target', () => {
27
+ const target = { a: 1, nested: { b: 2 } };
28
+ const original = { a: 1, nested: { b: 2 } };
29
+ deepMerge(target, { a: 99 });
30
+ expect(target).toEqual(original);
31
+ });
32
+ });
33
+ describe('parseDuration', () => {
34
+ it('parses milliseconds', () => {
35
+ expect(parseDuration('500ms')).toBe(500);
36
+ });
37
+ it('parses seconds', () => {
38
+ expect(parseDuration('30s')).toBe(30_000);
39
+ });
40
+ it('parses minutes', () => {
41
+ expect(parseDuration('2m')).toBe(120_000);
42
+ });
43
+ it('returns 60000 for invalid input', () => {
44
+ expect(parseDuration('invalid')).toBe(60_000);
45
+ expect(parseDuration('')).toBe(60_000);
46
+ expect(parseDuration('10x')).toBe(60_000);
47
+ });
48
+ });
49
+ describe('getPackageRoot', () => {
50
+ it('returns a path containing package.json', async () => {
51
+ const root = await getPackageRoot();
52
+ expect(existsSync(join(root, 'package.json'))).toBe(true);
53
+ });
54
+ });
55
+ //# sourceMappingURL=utils.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.test.js","sourceRoot":"","sources":["../src/utils.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Workspace — Creates and manages isolated OpenClaw workspace directories.
3
+ *
4
+ * Each bot gets a profile directory at ~/.openclaw-claw-harness-<id>/
5
+ * containing openclaw.json, USER.md, and skills.
6
+ */
7
+ import type { BotConfig } from './types.js';
8
+ import type { BotRuntimeConfig } from './bot.js';
9
+ export declare class Workspace {
10
+ readonly profileName: string;
11
+ private _profileDir;
12
+ private workspaceSubdir;
13
+ private skillsDir;
14
+ get profileDir(): string;
15
+ constructor(botId: string, baseDir: string);
16
+ /**
17
+ * Create the workspace directory structure and populate files.
18
+ */
19
+ setup(config: BotConfig, runtime: BotRuntimeConfig): Promise<void>;
20
+ /**
21
+ * Remove the workspace directory.
22
+ */
23
+ cleanup(): Promise<void>;
24
+ private buildConfig;
25
+ private loadPreset;
26
+ private installSkill;
27
+ /**
28
+ * Resolve content that could be a file path or inline string.
29
+ * Tries: absolute path, cwd-relative, package-root-relative, then inline.
30
+ */
31
+ resolveContent(pathOrContent: string): Promise<string>;
32
+ }
33
+ //# sourceMappingURL=workspace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAGhD,qBAAa,SAAS;IACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,SAAS,CAAQ;IAEzB,IAAI,UAAU,IAAI,MAAM,CAA4B;gBAExC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;IAQ1C;;OAEG;IACG,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IA2CxE;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YAQhB,WAAW;YA2DX,UAAU;YAyBV,YAAY;IA4B1B;;;OAGG;IACG,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CA4B7D"}
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Workspace — Creates and manages isolated OpenClaw workspace directories.
3
+ *
4
+ * Each bot gets a profile directory at ~/.openclaw-claw-harness-<id>/
5
+ * containing openclaw.json, USER.md, and skills.
6
+ */
7
+ import { mkdir, writeFile, rm, readFile, access } from 'node:fs/promises';
8
+ import { join, resolve, isAbsolute } from 'node:path';
9
+ import JSON5 from 'json5';
10
+ import { deepMerge, getPackageRoot } from './utils.js';
11
+ export class Workspace {
12
+ profileName;
13
+ _profileDir;
14
+ workspaceSubdir;
15
+ skillsDir;
16
+ get profileDir() { return this._profileDir; }
17
+ constructor(botId, baseDir) {
18
+ this.profileName = `claw-harness-${botId}`;
19
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? '/tmp';
20
+ this._profileDir = join(home, `.openclaw-${this.profileName}`);
21
+ this.workspaceSubdir = join(this._profileDir, 'workspace');
22
+ this.skillsDir = join(this.workspaceSubdir, 'skills');
23
+ }
24
+ /**
25
+ * Create the workspace directory structure and populate files.
26
+ */
27
+ async setup(config, runtime) {
28
+ // Create directory structure
29
+ await mkdir(this.skillsDir, { recursive: true });
30
+ // Write openclaw.json
31
+ const openclawConfig = await this.buildConfig(config, runtime);
32
+ await writeFile(join(this._profileDir, 'openclaw.json'), JSON.stringify(openclawConfig, null, 2));
33
+ // Write .env file with API keys so the agent can find them
34
+ const envLines = [];
35
+ if (runtime.anthropicApiKey) {
36
+ envLines.push(`ANTHROPIC_API_KEY=${runtime.anthropicApiKey}`);
37
+ }
38
+ if (runtime.openaiApiKey) {
39
+ envLines.push(`OPENAI_API_KEY=${runtime.openaiApiKey}`);
40
+ }
41
+ if (envLines.length > 0) {
42
+ await writeFile(join(this._profileDir, '.env'), envLines.join('\n') + '\n');
43
+ }
44
+ // Write USER.md (persona)
45
+ if (config.userMd) {
46
+ const content = await this.resolveContent(config.userMd);
47
+ await writeFile(join(this.workspaceSubdir, 'USER.md'), content);
48
+ }
49
+ // Write SOUL.md (personality)
50
+ if (config.soulMd) {
51
+ const content = await this.resolveContent(config.soulMd);
52
+ await writeFile(join(this.workspaceSubdir, 'SOUL.md'), content);
53
+ }
54
+ // Install skills
55
+ if (config.skills) {
56
+ for (const skill of config.skills) {
57
+ await this.installSkill(skill);
58
+ }
59
+ }
60
+ }
61
+ /**
62
+ * Remove the workspace directory.
63
+ */
64
+ async cleanup() {
65
+ try {
66
+ await rm(this._profileDir, { recursive: true, force: true });
67
+ }
68
+ catch {
69
+ // Best-effort cleanup
70
+ }
71
+ }
72
+ async buildConfig(config, runtime) {
73
+ // Load preset if specified
74
+ let base = {};
75
+ if (config.preset) {
76
+ base = await this.loadPreset(config.preset);
77
+ }
78
+ // Build the config, merging preset with overrides
79
+ const token = `claw-harness-${Date.now()}-${Math.random().toString(36).slice(2)}`;
80
+ // Build agents defaults with optional web_fetch allowlist
81
+ const agentsDefaults = {};
82
+ if (config.model) {
83
+ agentsDefaults.model = { primary: config.model };
84
+ }
85
+ if (config.allowedHosts && config.allowedHosts.length > 0) {
86
+ agentsDefaults.web_fetch = {
87
+ allowed_hosts: config.allowedHosts,
88
+ };
89
+ }
90
+ let result = deepMerge(base, {
91
+ gateway: {
92
+ mode: 'local',
93
+ port: runtime.port,
94
+ auth: { token },
95
+ http: {
96
+ endpoints: {
97
+ chatCompletions: { enabled: true },
98
+ },
99
+ },
100
+ },
101
+ agents: {
102
+ defaults: agentsDefaults,
103
+ list: [{ id: 'main', default: true }],
104
+ },
105
+ skills: {
106
+ allowBundled: [],
107
+ },
108
+ });
109
+ // Docker mode: enable headless browser with Chromium
110
+ if (runtime.mode === 'docker') {
111
+ result = deepMerge(result, {
112
+ browser: { enabled: true, headless: true, noSandbox: true },
113
+ });
114
+ }
115
+ // Apply user-specified overrides
116
+ if (config.configOverrides) {
117
+ return deepMerge(result, config.configOverrides);
118
+ }
119
+ return result;
120
+ }
121
+ async loadPreset(name) {
122
+ const pkgRoot = await getPackageRoot();
123
+ // Search order: cwd presets -> package presets
124
+ const presetsLocations = [
125
+ join(process.cwd(), 'presets', 'configs', `${name}.json5`),
126
+ join(process.cwd(), 'presets', 'configs', `${name}.json`),
127
+ join(pkgRoot, 'presets', 'configs', `${name}.json5`),
128
+ join(pkgRoot, 'presets', 'configs', `${name}.json`),
129
+ ];
130
+ for (const loc of presetsLocations) {
131
+ try {
132
+ await access(loc);
133
+ const content = await readFile(loc, 'utf-8');
134
+ return JSON5.parse(content);
135
+ }
136
+ catch {
137
+ continue;
138
+ }
139
+ }
140
+ console.warn(`Warning: preset "${name}" not found, using empty config`);
141
+ return {};
142
+ }
143
+ async installSkill(skill) {
144
+ const skillName = skill.name ?? 'skill';
145
+ const skillDir = join(this.skillsDir, skillName);
146
+ await mkdir(skillDir, { recursive: true });
147
+ let content;
148
+ if (skill.url) {
149
+ // Fetch from URL with timeout
150
+ const response = await fetch(skill.url, {
151
+ signal: AbortSignal.timeout(30_000),
152
+ });
153
+ if (!response.ok) {
154
+ throw new Error(`Failed to fetch skill from ${skill.url}: ${response.status}`);
155
+ }
156
+ content = await response.text();
157
+ }
158
+ else if (skill.path) {
159
+ // Read from local file
160
+ content = await readFile(skill.path, 'utf-8');
161
+ }
162
+ else {
163
+ throw new Error('Skill must have either url or path');
164
+ }
165
+ await writeFile(join(skillDir, 'SKILL.md'), content);
166
+ }
167
+ /**
168
+ * Resolve content that could be a file path or inline string.
169
+ * Tries: absolute path, cwd-relative, package-root-relative, then inline.
170
+ */
171
+ async resolveContent(pathOrContent) {
172
+ // If it contains newlines or doesn't look like a path, treat as inline content
173
+ if (pathOrContent.includes('\n') || !pathOrContent.match(/\.\w+$/)) {
174
+ return pathOrContent;
175
+ }
176
+ // Try resolving as file path from multiple locations
177
+ const candidates = [];
178
+ if (isAbsolute(pathOrContent)) {
179
+ candidates.push(pathOrContent);
180
+ }
181
+ else {
182
+ candidates.push(resolve(process.cwd(), pathOrContent));
183
+ const pkgRoot = await getPackageRoot();
184
+ candidates.push(resolve(pkgRoot, pathOrContent));
185
+ }
186
+ for (const candidate of candidates) {
187
+ try {
188
+ return await readFile(candidate, 'utf-8');
189
+ }
190
+ catch {
191
+ continue;
192
+ }
193
+ }
194
+ // Fall back to treating it as inline content
195
+ return pathOrContent;
196
+ }
197
+ }
198
+ //# sourceMappingURL=workspace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.js","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=workspace.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.test.d.ts","sourceRoot":"","sources":["../src/workspace.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,68 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { Workspace } from './workspace.js';
3
+ import { mkdir, writeFile, readFile, rm } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ const TEST_DIR = join(tmpdir(), 'claw-harness-workspace-test');
7
+ describe('Workspace', () => {
8
+ beforeEach(async () => {
9
+ await mkdir(TEST_DIR, { recursive: true });
10
+ });
11
+ afterEach(async () => {
12
+ await rm(TEST_DIR, { recursive: true, force: true });
13
+ // Clean up any test profile directories
14
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? '/tmp';
15
+ await rm(join(home, '.openclaw-claw-harness-test-ws'), { recursive: true, force: true }).catch(() => { });
16
+ });
17
+ it('resolveContent returns inline content for strings with newlines', async () => {
18
+ const ws = new Workspace('test-ws', TEST_DIR);
19
+ const result = await ws.resolveContent('line one\nline two');
20
+ expect(result).toBe('line one\nline two');
21
+ });
22
+ it('resolveContent returns inline content for strings without file extension', async () => {
23
+ const ws = new Workspace('test-ws', TEST_DIR);
24
+ const result = await ws.resolveContent('You are a friendly bot');
25
+ expect(result).toBe('You are a friendly bot');
26
+ });
27
+ it('resolveContent reads file content for valid paths', async () => {
28
+ const testFile = join(TEST_DIR, 'test-persona.md');
29
+ await writeFile(testFile, 'I am a test persona');
30
+ const ws = new Workspace('test-ws', TEST_DIR);
31
+ const result = await ws.resolveContent(testFile);
32
+ expect(result).toBe('I am a test persona');
33
+ });
34
+ it('resolveContent falls back to inline for missing file paths', async () => {
35
+ const ws = new Workspace('test-ws', TEST_DIR);
36
+ const result = await ws.resolveContent('/nonexistent/file.md');
37
+ expect(result).toBe('/nonexistent/file.md');
38
+ });
39
+ it('installSkill throws for skill with no url or path', async () => {
40
+ const ws = new Workspace('test-ws', TEST_DIR);
41
+ const runtime = { mode: 'local', port: 18800, workspaceDir: TEST_DIR, anthropicApiKey: 'test', openaiApiKey: '' };
42
+ await ws.setup({ skills: [{}] }, runtime).catch(err => {
43
+ expect(err.message).toBe('Skill must have either url or path');
44
+ });
45
+ });
46
+ it('profileDir getter returns the profile directory path', () => {
47
+ const ws = new Workspace('test-ws', TEST_DIR);
48
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? '/tmp';
49
+ expect(ws.profileDir).toBe(join(home, '.openclaw-claw-harness-test-ws'));
50
+ });
51
+ it('docker mode includes browser config in openclaw.json', async () => {
52
+ const ws = new Workspace('test-ws', TEST_DIR);
53
+ const runtime = { mode: 'docker', port: 18800, workspaceDir: TEST_DIR, anthropicApiKey: 'test', openaiApiKey: '' };
54
+ await ws.setup({}, runtime);
55
+ const configPath = join(ws.profileDir, 'openclaw.json');
56
+ const config = JSON.parse(await readFile(configPath, 'utf-8'));
57
+ expect(config.browser).toEqual({ enabled: true, headless: true, noSandbox: true });
58
+ });
59
+ it('local mode does not include browser config in openclaw.json', async () => {
60
+ const ws = new Workspace('test-ws', TEST_DIR);
61
+ const runtime = { mode: 'local', port: 18800, workspaceDir: TEST_DIR, anthropicApiKey: 'test', openaiApiKey: '' };
62
+ await ws.setup({}, runtime);
63
+ const configPath = join(ws.profileDir, 'openclaw.json');
64
+ const config = JSON.parse(await readFile(configPath, 'utf-8'));
65
+ expect(config.browser).toBeUndefined();
66
+ });
67
+ });
68
+ //# sourceMappingURL=workspace.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.test.js","sourceRoot":"","sources":["../src/workspace.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ FROM node:22-slim
2
+ RUN apt-get update && apt-get install -y --no-install-recommends \
3
+ chromium xvfb fonts-liberation libatk-bridge2.0-0 libdrm2 \
4
+ libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 \
5
+ libcairo2 libasound2 libnspr4 libnss3 libxss1 libxtst6 \
6
+ && rm -rf /var/lib/apt/lists/*
7
+ RUN npm install -g openclaw@latest
8
+ ENV CHROME_PATH=/usr/bin/chromium
9
+ ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
10
+ COPY entrypoint.sh /entrypoint.sh
11
+ RUN chmod +x /entrypoint.sh
12
+ ENTRYPOINT ["/entrypoint.sh"]
@@ -0,0 +1,6 @@
1
+ #!/bin/bash
2
+ set -e
3
+ Xvfb :99 -screen 0 1280x1024x24 -nolisten tcp &
4
+ sleep 0.5
5
+ export DISPLAY=:99
6
+ exec "$@"
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "claw-harness",
3
+ "version": "0.1.0",
4
+ "description": "Testing framework for OpenClaw bots. Spin up real agents, load skills, drive multi-turn prompts, and capture results.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "claw-harness": "dist/bin/clawbench.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "dev": "tsc --watch",
14
+ "test": "vitest",
15
+ "lint": "tsc --noEmit",
16
+ "postbuild": "chmod +x dist/bin/clawbench.js"
17
+ },
18
+ "keywords": [
19
+ "openclaw",
20
+ "testing",
21
+ "ai-agents",
22
+ "benchmark",
23
+ "bot-testing",
24
+ "skill-testing"
25
+ ],
26
+ "files": ["dist", "presets", "docker"],
27
+ "author": "Connor Daly connor@iceblock.ventures",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/dasconnor/claw-harness.git"
31
+ },
32
+ "homepage": "https://github.com/dasconnor/claw-harness#readme",
33
+ "bugs": {
34
+ "url": "https://github.com/dasconnor/claw-harness/issues"
35
+ },
36
+ "license": "MIT",
37
+ "engines": {
38
+ "node": ">=22"
39
+ },
40
+ "dependencies": {
41
+ "json5": "^2.2.3",
42
+ "yaml": "^2.7.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^22.0.0",
46
+ "typescript": "^5.7.0",
47
+ "vitest": "^3.0.0"
48
+ }
49
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ // Claw Harness default bot configuration
3
+ // Merged into each bot's openclaw.json
4
+ "gateway": {
5
+ "mode": "local",
6
+ // port is set dynamically by Claw Harness
7
+ "auth": {
8
+ // token is set dynamically by Claw Harness
9
+ },
10
+ "http": {
11
+ "endpoints": {
12
+ "chatCompletions": { "enabled": true }
13
+ }
14
+ }
15
+ },
16
+ "agents": {
17
+ "defaults": {
18
+ "model": { "primary": "anthropic/claude-haiku-4-5-20251001" }
19
+ },
20
+ "list": [
21
+ { "id": "main", "default": true }
22
+ ]
23
+ },
24
+ "skills": {
25
+ // Disable all bundled skills so only test skills are active
26
+ "allowBundled": []
27
+ }
28
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ // Minimal config — restricted tools, lower cost
3
+ "gateway": {
4
+ "mode": "local",
5
+ "http": {
6
+ "endpoints": {
7
+ "chatCompletions": { "enabled": true }
8
+ }
9
+ }
10
+ },
11
+ "agents": {
12
+ "defaults": {
13
+ "model": { "primary": "anthropic/claude-haiku-4-5-20251001" }
14
+ },
15
+ "list": [
16
+ { "id": "main", "default": true }
17
+ ]
18
+ },
19
+ "skills": {
20
+ "allowBundled": []
21
+ }
22
+ }
@@ -0,0 +1,5 @@
1
+ # About Me
2
+
3
+ I'm a curious, thoughtful AI bot. I ask interesting questions and engage deeply with conversation partners. I prefer substance over small talk and enjoy exploring ideas.
4
+
5
+ When interacting with platforms or services, I read the documentation carefully and think through my actions before taking them.
@@ -0,0 +1,5 @@
1
+ # About Me
2
+
3
+ I'm a friendly, outgoing AI bot. I enjoy meeting other bots and having casual conversations. I'm enthusiastic, ask follow-up questions, and like to share interesting observations.
4
+
5
+ When interacting with platforms or services, I explore them naturally — trying things out, reading documentation, and figuring out how things work on my own.
@@ -0,0 +1,3 @@
1
+ # About Me
2
+
3
+ I'm a concise, technical bot. I keep responses brief and to the point. I value efficiency and clarity over chattiness.
@@ -0,0 +1,71 @@
1
+ # Example: Two bots discover a site and have a conversation
2
+ #
3
+ # Usage:
4
+ # claw-harness run presets/scenarios/example-chat.yaml
5
+ #
6
+ # Prerequisites:
7
+ # - OpenClaw installed (`npm install -g openclaw@latest`)
8
+ # - ANTHROPIC_API_KEY set in environment
9
+ # - Target app running at base_url
10
+
11
+ name: "Example Chat Test"
12
+ description: "Two bots read a skill.md, register, and have a conversation"
13
+
14
+ target:
15
+ base_url: "http://localhost:3000"
16
+
17
+ bots:
18
+ alpha:
19
+ preset: default
20
+ model: anthropic/claude-haiku-4-5-20251001
21
+ user_md: presets/personas/friendly.md
22
+ skills:
23
+ - url: "http://localhost:3000/skill.md"
24
+ name: target-app
25
+
26
+ beta:
27
+ preset: default
28
+ model: anthropic/claude-haiku-4-5-20251001
29
+ user_md: presets/personas/curious.md
30
+ skills:
31
+ - url: "http://localhost:3000/skill.md"
32
+ name: target-app
33
+
34
+ steps:
35
+ # Both bots register
36
+ - bot: alpha
37
+ prompt: >
38
+ Read the skill documentation to understand how this platform works.
39
+ Then register yourself as a bot on the platform.
40
+ timeout: 60s
41
+
42
+ - bot: beta
43
+ prompt: >
44
+ Read the skill documentation to understand how this platform works.
45
+ Then register yourself as a bot on the platform.
46
+ timeout: 60s
47
+
48
+ # Alpha joins a lounge
49
+ - bot: alpha
50
+ prompt: >
51
+ Browse the platform for active lounges or events.
52
+ Join one and introduce yourself.
53
+ timeout: 60s
54
+
55
+ # Beta follows
56
+ - bot: beta
57
+ prompt: >
58
+ Browse the platform for active lounges or events.
59
+ If you see a lounge with another bot, join it and start a conversation.
60
+ timeout: 60s
61
+
62
+ # Back-and-forth conversation
63
+ - repeat: 3
64
+ interval: 15s
65
+ steps:
66
+ - bot: alpha
67
+ prompt: "Check for new messages and respond to continue the conversation."
68
+ timeout: 30s
69
+ - bot: beta
70
+ prompt: "Check for new messages and respond to continue the conversation."
71
+ timeout: 30s