create-ironclaws 1.0.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/README.md +101 -0
- package/bin/create.js +394 -0
- package/package.json +33 -0
- package/template/.env.example +38 -0
- package/template/CLAUDE.md +104 -0
- package/template/agent-credentials.yaml +33 -0
- package/template/agents.yaml +22 -0
- package/template/container/Dockerfile +70 -0
- package/template/container/Dockerfile.argus +34 -0
- package/template/container/agent-runner/package-lock.json +1524 -0
- package/template/container/agent-runner/package.json +23 -0
- package/template/container/agent-runner/src/index.ts +630 -0
- package/template/container/agent-runner/src/ipc-mcp-stdio.ts +339 -0
- package/template/container/agent-runner/tsconfig.json +15 -0
- package/template/container/build-argus.sh +25 -0
- package/template/container/build.sh +23 -0
- package/template/container/skills/agent-browser/SKILL.md +159 -0
- package/template/container/skills/agent-status/SKILL.md +69 -0
- package/template/container/skills/capabilities/SKILL.md +100 -0
- package/template/container/skills/edit-agent/SKILL.md +93 -0
- package/template/container/skills/slack-formatting/SKILL.md +92 -0
- package/template/container/skills/status/SKILL.md +104 -0
- package/template/container/tools/elastic_query.py +161 -0
- package/template/container/tools/gdrive_tool.py +185 -0
- package/template/container/tools/jira_tool.py +433 -0
- package/template/container/tools/slack_history_tool.py +144 -0
- package/template/container/tools/youtube_tool.py +174 -0
- package/template/docker-compose.yml +54 -0
- package/template/docs/how-it-works.md +496 -0
- package/template/eslint.config.js +32 -0
- package/template/groups/forge/CLAUDE.md +107 -0
- package/template/package-lock.json +5278 -0
- package/template/package.json +52 -0
- package/template/scripts/github-app-token.py +58 -0
- package/template/scripts/register-expense-agent.sh +121 -0
- package/template/scripts/run-migrations.ts +105 -0
- package/template/scripts/setup-onecli-secrets.sh +252 -0
- package/template/setup-agents.sh +142 -0
- package/template/src/channels/index.ts +13 -0
- package/template/src/channels/registry.test.ts +42 -0
- package/template/src/channels/registry.ts +28 -0
- package/template/src/channels/slack.test.ts +859 -0
- package/template/src/channels/slack.ts +373 -0
- package/template/src/claw-skill.test.ts +45 -0
- package/template/src/config.ts +94 -0
- package/template/src/container-runner.test.ts +221 -0
- package/template/src/container-runner.ts +1029 -0
- package/template/src/container-runtime.test.ts +149 -0
- package/template/src/container-runtime.ts +124 -0
- package/template/src/db-migration.test.ts +67 -0
- package/template/src/db.test.ts +484 -0
- package/template/src/db.ts +837 -0
- package/template/src/env.ts +42 -0
- package/template/src/formatting.test.ts +294 -0
- package/template/src/github-token.ts +48 -0
- package/template/src/google-token.ts +75 -0
- package/template/src/group-folder.test.ts +43 -0
- package/template/src/group-folder.ts +44 -0
- package/template/src/group-queue.test.ts +484 -0
- package/template/src/group-queue.ts +363 -0
- package/template/src/http-server.ts +343 -0
- package/template/src/index.ts +960 -0
- package/template/src/ipc-auth.test.ts +679 -0
- package/template/src/ipc.ts +548 -0
- package/template/src/logger.ts +16 -0
- package/template/src/mount-security.ts +421 -0
- package/template/src/network-policy.ts +119 -0
- package/template/src/remote-control.test.ts +397 -0
- package/template/src/remote-control.ts +224 -0
- package/template/src/router.ts +52 -0
- package/template/src/routing.test.ts +170 -0
- package/template/src/sender-allowlist.test.ts +216 -0
- package/template/src/sender-allowlist.ts +128 -0
- package/template/src/task-scheduler.test.ts +129 -0
- package/template/src/task-scheduler.ts +290 -0
- package/template/src/timezone.test.ts +73 -0
- package/template/src/timezone.ts +37 -0
- package/template/src/types.ts +114 -0
- package/template/src/worktree.ts +206 -0
- package/template/tsconfig.json +20 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { PassThrough } from 'stream';
|
|
4
|
+
|
|
5
|
+
// Sentinel markers must match container-runner.ts
|
|
6
|
+
const OUTPUT_START_MARKER = '---NANOCLAW_OUTPUT_START---';
|
|
7
|
+
const OUTPUT_END_MARKER = '---NANOCLAW_OUTPUT_END---';
|
|
8
|
+
|
|
9
|
+
// Mock config
|
|
10
|
+
vi.mock('./config.js', () => ({
|
|
11
|
+
CONTAINER_IMAGE: 'nanoclaw-agent:latest',
|
|
12
|
+
CONTAINER_MAX_OUTPUT_SIZE: 10485760,
|
|
13
|
+
CONTAINER_TIMEOUT: 1800000, // 30min
|
|
14
|
+
DATA_DIR: '/tmp/nanoclaw-test-data',
|
|
15
|
+
GROUPS_DIR: '/tmp/nanoclaw-test-groups',
|
|
16
|
+
IDLE_TIMEOUT: 1800000, // 30min
|
|
17
|
+
ONECLI_URL: 'http://localhost:10254',
|
|
18
|
+
TIMEZONE: 'America/Los_Angeles',
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// Mock logger
|
|
22
|
+
vi.mock('./logger.js', () => ({
|
|
23
|
+
logger: {
|
|
24
|
+
debug: vi.fn(),
|
|
25
|
+
info: vi.fn(),
|
|
26
|
+
warn: vi.fn(),
|
|
27
|
+
error: vi.fn(),
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
// Mock fs
|
|
32
|
+
vi.mock('fs', async () => {
|
|
33
|
+
const actual = await vi.importActual<typeof import('fs')>('fs');
|
|
34
|
+
return {
|
|
35
|
+
...actual,
|
|
36
|
+
default: {
|
|
37
|
+
...actual,
|
|
38
|
+
existsSync: vi.fn(() => false),
|
|
39
|
+
mkdirSync: vi.fn(),
|
|
40
|
+
writeFileSync: vi.fn(),
|
|
41
|
+
readFileSync: vi.fn(() => ''),
|
|
42
|
+
readdirSync: vi.fn(() => []),
|
|
43
|
+
statSync: vi.fn(() => ({ isDirectory: () => false })),
|
|
44
|
+
copyFileSync: vi.fn(),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Mock mount-security
|
|
50
|
+
vi.mock('./mount-security.js', () => ({
|
|
51
|
+
validateAdditionalMounts: vi.fn(() => []),
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
// Mock OneCLI SDK
|
|
55
|
+
vi.mock('@onecli-sh/sdk', () => ({
|
|
56
|
+
OneCLI: class {
|
|
57
|
+
applyContainerConfig = vi.fn().mockResolvedValue(true);
|
|
58
|
+
createAgent = vi.fn().mockResolvedValue({ id: 'test' });
|
|
59
|
+
ensureAgent = vi
|
|
60
|
+
.fn()
|
|
61
|
+
.mockResolvedValue({ name: 'test', identifier: 'test', created: true });
|
|
62
|
+
},
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
// Create a controllable fake ChildProcess
|
|
66
|
+
function createFakeProcess() {
|
|
67
|
+
const proc = new EventEmitter() as EventEmitter & {
|
|
68
|
+
stdin: PassThrough;
|
|
69
|
+
stdout: PassThrough;
|
|
70
|
+
stderr: PassThrough;
|
|
71
|
+
kill: ReturnType<typeof vi.fn>;
|
|
72
|
+
pid: number;
|
|
73
|
+
};
|
|
74
|
+
proc.stdin = new PassThrough();
|
|
75
|
+
proc.stdout = new PassThrough();
|
|
76
|
+
proc.stderr = new PassThrough();
|
|
77
|
+
proc.kill = vi.fn();
|
|
78
|
+
proc.pid = 12345;
|
|
79
|
+
return proc;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let fakeProc: ReturnType<typeof createFakeProcess>;
|
|
83
|
+
|
|
84
|
+
// Mock child_process.spawn
|
|
85
|
+
vi.mock('child_process', async () => {
|
|
86
|
+
const actual =
|
|
87
|
+
await vi.importActual<typeof import('child_process')>('child_process');
|
|
88
|
+
return {
|
|
89
|
+
...actual,
|
|
90
|
+
spawn: vi.fn(() => fakeProc),
|
|
91
|
+
exec: vi.fn(
|
|
92
|
+
(_cmd: string, _opts: unknown, cb?: (err: Error | null) => void) => {
|
|
93
|
+
if (cb) cb(null);
|
|
94
|
+
return new EventEmitter();
|
|
95
|
+
},
|
|
96
|
+
),
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
import { runContainerAgent, ContainerOutput } from './container-runner.js';
|
|
101
|
+
import type { RegisteredGroup } from './types.js';
|
|
102
|
+
|
|
103
|
+
const testGroup: RegisteredGroup = {
|
|
104
|
+
name: 'Test Group',
|
|
105
|
+
folder: 'test-group',
|
|
106
|
+
trigger: '@Andy',
|
|
107
|
+
added_at: new Date().toISOString(),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const testInput = {
|
|
111
|
+
prompt: 'Hello',
|
|
112
|
+
groupFolder: 'test-group',
|
|
113
|
+
chatJid: 'test@g.us',
|
|
114
|
+
isMain: false,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
function emitOutputMarker(
|
|
118
|
+
proc: ReturnType<typeof createFakeProcess>,
|
|
119
|
+
output: ContainerOutput,
|
|
120
|
+
) {
|
|
121
|
+
const json = JSON.stringify(output);
|
|
122
|
+
proc.stdout.push(`${OUTPUT_START_MARKER}\n${json}\n${OUTPUT_END_MARKER}\n`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
describe('container-runner timeout behavior', () => {
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
vi.useFakeTimers();
|
|
128
|
+
fakeProc = createFakeProcess();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
afterEach(() => {
|
|
132
|
+
vi.useRealTimers();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('timeout after output resolves as success', async () => {
|
|
136
|
+
const onOutput = vi.fn(async () => {});
|
|
137
|
+
const resultPromise = runContainerAgent(
|
|
138
|
+
testGroup,
|
|
139
|
+
testInput,
|
|
140
|
+
() => {},
|
|
141
|
+
onOutput,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Emit output with a result
|
|
145
|
+
emitOutputMarker(fakeProc, {
|
|
146
|
+
status: 'success',
|
|
147
|
+
result: 'Here is my response',
|
|
148
|
+
newSessionId: 'session-123',
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Let output processing settle
|
|
152
|
+
await vi.advanceTimersByTimeAsync(10);
|
|
153
|
+
|
|
154
|
+
// Fire the hard timeout (IDLE_TIMEOUT + 30s = 1830000ms)
|
|
155
|
+
await vi.advanceTimersByTimeAsync(1830000);
|
|
156
|
+
|
|
157
|
+
// Emit close event (as if container was stopped by the timeout)
|
|
158
|
+
fakeProc.emit('close', 137);
|
|
159
|
+
|
|
160
|
+
// Let the promise resolve
|
|
161
|
+
await vi.advanceTimersByTimeAsync(10);
|
|
162
|
+
|
|
163
|
+
const result = await resultPromise;
|
|
164
|
+
expect(result.status).toBe('success');
|
|
165
|
+
expect(result.newSessionId).toBe('session-123');
|
|
166
|
+
expect(onOutput).toHaveBeenCalledWith(
|
|
167
|
+
expect.objectContaining({ result: 'Here is my response' }),
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('timeout with no output resolves as error', async () => {
|
|
172
|
+
const onOutput = vi.fn(async () => {});
|
|
173
|
+
const resultPromise = runContainerAgent(
|
|
174
|
+
testGroup,
|
|
175
|
+
testInput,
|
|
176
|
+
() => {},
|
|
177
|
+
onOutput,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// No output emitted — fire the hard timeout
|
|
181
|
+
await vi.advanceTimersByTimeAsync(1830000);
|
|
182
|
+
|
|
183
|
+
// Emit close event
|
|
184
|
+
fakeProc.emit('close', 137);
|
|
185
|
+
|
|
186
|
+
await vi.advanceTimersByTimeAsync(10);
|
|
187
|
+
|
|
188
|
+
const result = await resultPromise;
|
|
189
|
+
expect(result.status).toBe('error');
|
|
190
|
+
expect(result.error).toContain('timed out');
|
|
191
|
+
expect(onOutput).not.toHaveBeenCalled();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('normal exit after output resolves as success', async () => {
|
|
195
|
+
const onOutput = vi.fn(async () => {});
|
|
196
|
+
const resultPromise = runContainerAgent(
|
|
197
|
+
testGroup,
|
|
198
|
+
testInput,
|
|
199
|
+
() => {},
|
|
200
|
+
onOutput,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Emit output
|
|
204
|
+
emitOutputMarker(fakeProc, {
|
|
205
|
+
status: 'success',
|
|
206
|
+
result: 'Done',
|
|
207
|
+
newSessionId: 'session-456',
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await vi.advanceTimersByTimeAsync(10);
|
|
211
|
+
|
|
212
|
+
// Normal exit (no timeout)
|
|
213
|
+
fakeProc.emit('close', 0);
|
|
214
|
+
|
|
215
|
+
await vi.advanceTimersByTimeAsync(10);
|
|
216
|
+
|
|
217
|
+
const result = await resultPromise;
|
|
218
|
+
expect(result.status).toBe('success');
|
|
219
|
+
expect(result.newSessionId).toBe('session-456');
|
|
220
|
+
});
|
|
221
|
+
});
|