@agentmeshhq/agent 0.1.12 → 0.1.13
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/__tests__/sandbox.test.d.ts +1 -0
- package/dist/__tests__/sandbox.test.js +362 -0
- package/dist/__tests__/sandbox.test.js.map +1 -0
- package/dist/cli/inbox.d.ts +5 -0
- package/dist/cli/inbox.js +123 -0
- package/dist/cli/inbox.js.map +1 -0
- package/dist/cli/index.js +5 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/issue.d.ts +42 -0
- package/dist/cli/issue.js +297 -0
- package/dist/cli/issue.js.map +1 -0
- package/dist/cli/ready.d.ts +5 -0
- package/dist/cli/ready.js +131 -0
- package/dist/cli/ready.js.map +1 -0
- package/dist/cli/start.d.ts +8 -0
- package/dist/cli/start.js +9 -0
- package/dist/cli/start.js.map +1 -1
- package/dist/cli/sync.d.ts +8 -0
- package/dist/cli/sync.js +154 -0
- package/dist/cli/sync.js.map +1 -0
- package/dist/core/daemon.d.ts +19 -0
- package/dist/core/daemon.js +96 -5
- package/dist/core/daemon.js.map +1 -1
- package/dist/core/issue-cache.d.ts +44 -0
- package/dist/core/issue-cache.js +75 -0
- package/dist/core/issue-cache.js.map +1 -0
- package/dist/core/registry.d.ts +5 -0
- package/dist/core/registry.js +8 -1
- package/dist/core/registry.js.map +1 -1
- package/dist/core/sandbox.d.ts +127 -0
- package/dist/core/sandbox.js +377 -0
- package/dist/core/sandbox.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/sandbox.test.ts +435 -0
- package/src/cli/index.ts +8 -1
- package/src/cli/start.ts +14 -0
- package/src/core/daemon.ts +114 -6
- package/src/core/registry.ts +14 -1
- package/src/core/sandbox.ts +505 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { DockerSandbox, type SandboxConfig, type SandboxMountPolicy } from "../core/sandbox.js";
|
|
5
|
+
|
|
6
|
+
// Mock child_process
|
|
7
|
+
vi.mock("node:child_process", () => ({
|
|
8
|
+
execSync: vi.fn(),
|
|
9
|
+
spawn: vi.fn(() => ({
|
|
10
|
+
on: vi.fn(),
|
|
11
|
+
unref: vi.fn(),
|
|
12
|
+
pid: 12345,
|
|
13
|
+
})),
|
|
14
|
+
spawnSync: vi.fn(() => ({
|
|
15
|
+
status: 0,
|
|
16
|
+
stdout: "mock-container-id\n",
|
|
17
|
+
stderr: "",
|
|
18
|
+
})),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// Mock fs
|
|
22
|
+
vi.mock("node:fs", () => ({
|
|
23
|
+
default: {
|
|
24
|
+
existsSync: vi.fn(() => true),
|
|
25
|
+
},
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
describe("DockerSandbox", () => {
|
|
29
|
+
const defaultConfig: SandboxConfig = {
|
|
30
|
+
agentName: "test-agent",
|
|
31
|
+
image: "agentmesh/agent-sandbox:latest",
|
|
32
|
+
workspacePath: "/tmp/workspace",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.clearAllMocks();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("constructor", () => {
|
|
40
|
+
it("should create sandbox with default values", () => {
|
|
41
|
+
const sandbox = new DockerSandbox(defaultConfig);
|
|
42
|
+
|
|
43
|
+
expect(sandbox.getContainerName()).toMatch(/^agentmesh-sandbox-test-agent-[a-f0-9]{8}$/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should merge config with defaults", () => {
|
|
47
|
+
const config: SandboxConfig = {
|
|
48
|
+
...defaultConfig,
|
|
49
|
+
cpuLimit: "0.5",
|
|
50
|
+
memoryLimit: "1g",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const sandbox = new DockerSandbox(config);
|
|
54
|
+
expect(sandbox).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("validateMountPolicy", () => {
|
|
59
|
+
it("should deny sensitive paths by default", () => {
|
|
60
|
+
const sandbox = new DockerSandbox({
|
|
61
|
+
...defaultConfig,
|
|
62
|
+
workspacePath: path.join(os.homedir(), ".ssh"),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(() => sandbox.validateMountPolicy()).toThrow(/Mount denied.*\.ssh/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should deny ~/.gnupg", () => {
|
|
69
|
+
const sandbox = new DockerSandbox({
|
|
70
|
+
...defaultConfig,
|
|
71
|
+
workspacePath: path.join(os.homedir(), ".gnupg"),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(() => sandbox.validateMountPolicy()).toThrow(/Mount denied.*\.gnupg/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should deny ~/.aws", () => {
|
|
78
|
+
const sandbox = new DockerSandbox({
|
|
79
|
+
...defaultConfig,
|
|
80
|
+
workspacePath: path.join(os.homedir(), ".aws"),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(() => sandbox.validateMountPolicy()).toThrow(/Mount denied.*\.aws/);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should deny ~/.config/gcloud", () => {
|
|
87
|
+
const sandbox = new DockerSandbox({
|
|
88
|
+
...defaultConfig,
|
|
89
|
+
workspacePath: path.join(os.homedir(), ".config/gcloud"),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(() => sandbox.validateMountPolicy()).toThrow(/Mount denied.*gcloud/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should deny ~/.kube", () => {
|
|
96
|
+
const sandbox = new DockerSandbox({
|
|
97
|
+
...defaultConfig,
|
|
98
|
+
workspacePath: path.join(os.homedir(), ".kube"),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(() => sandbox.validateMountPolicy()).toThrow(/Mount denied.*\.kube/);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should deny ~/.docker", () => {
|
|
105
|
+
const sandbox = new DockerSandbox({
|
|
106
|
+
...defaultConfig,
|
|
107
|
+
workspacePath: path.join(os.homedir(), ".docker"),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(() => sandbox.validateMountPolicy()).toThrow(/Mount denied.*\.docker/);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should deny /var/run/docker.sock", () => {
|
|
114
|
+
const sandbox = new DockerSandbox({
|
|
115
|
+
...defaultConfig,
|
|
116
|
+
workspacePath: "/var/run/docker.sock",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(() => sandbox.validateMountPolicy()).toThrow(/Mount denied.*docker\.sock/);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should allow regular workspace paths", () => {
|
|
123
|
+
const sandbox = new DockerSandbox({
|
|
124
|
+
...defaultConfig,
|
|
125
|
+
workspacePath: "/tmp/workspace",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(() => sandbox.validateMountPolicy()).not.toThrow();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should enforce custom allowed paths when specified", () => {
|
|
132
|
+
const sandbox = new DockerSandbox({
|
|
133
|
+
...defaultConfig,
|
|
134
|
+
workspacePath: "/home/user/random",
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const policy: SandboxMountPolicy = {
|
|
138
|
+
allowedPaths: ["/home/user/allowed-only"],
|
|
139
|
+
deniedPaths: [],
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
expect(() => sandbox.validateMountPolicy(policy)).toThrow(/not in allowed paths/);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should allow path within custom allowed paths", () => {
|
|
146
|
+
const sandbox = new DockerSandbox({
|
|
147
|
+
...defaultConfig,
|
|
148
|
+
workspacePath: "/home/user/projects/myrepo",
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const policy: SandboxMountPolicy = {
|
|
152
|
+
allowedPaths: ["/home/user/projects"],
|
|
153
|
+
deniedPaths: [],
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
expect(() => sandbox.validateMountPolicy(policy)).not.toThrow();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("checkDockerAvailable", () => {
|
|
161
|
+
it("should return true when docker is available", async () => {
|
|
162
|
+
const { spawnSync } = await import("node:child_process");
|
|
163
|
+
vi.mocked(spawnSync).mockReturnValueOnce({
|
|
164
|
+
status: 0,
|
|
165
|
+
stdout: "Docker version 24.0.0",
|
|
166
|
+
stderr: "",
|
|
167
|
+
pid: 1,
|
|
168
|
+
output: [],
|
|
169
|
+
signal: null,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(DockerSandbox.checkDockerAvailable()).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("should return false when docker is not available", async () => {
|
|
176
|
+
const { spawnSync } = await import("node:child_process");
|
|
177
|
+
vi.mocked(spawnSync).mockReturnValueOnce({
|
|
178
|
+
status: 1,
|
|
179
|
+
stdout: "",
|
|
180
|
+
stderr: "docker: command not found",
|
|
181
|
+
pid: 1,
|
|
182
|
+
output: [],
|
|
183
|
+
signal: null,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(DockerSandbox.checkDockerAvailable()).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("listAll", () => {
|
|
191
|
+
it("should parse docker ps output correctly", async () => {
|
|
192
|
+
const { spawnSync } = await import("node:child_process");
|
|
193
|
+
vi.mocked(spawnSync).mockReturnValueOnce({
|
|
194
|
+
status: 0,
|
|
195
|
+
stdout:
|
|
196
|
+
"abc123|agentmesh-sandbox-agent1-12345678|Up 5 minutes|agentmesh/agent-sandbox:latest\ndef456|agentmesh-sandbox-agent2-87654321|Exited (0)|agentmesh/agent-sandbox:latest",
|
|
197
|
+
stderr: "",
|
|
198
|
+
pid: 1,
|
|
199
|
+
output: [],
|
|
200
|
+
signal: null,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const containers = DockerSandbox.listAll();
|
|
204
|
+
|
|
205
|
+
expect(containers).toHaveLength(2);
|
|
206
|
+
expect(containers[0]).toEqual({
|
|
207
|
+
id: "abc123",
|
|
208
|
+
name: "agentmesh-sandbox-agent1-12345678",
|
|
209
|
+
status: "Up 5 minutes",
|
|
210
|
+
image: "agentmesh/agent-sandbox:latest",
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("should return empty array when no containers found", async () => {
|
|
215
|
+
const { spawnSync } = await import("node:child_process");
|
|
216
|
+
vi.mocked(spawnSync).mockReturnValueOnce({
|
|
217
|
+
status: 0,
|
|
218
|
+
stdout: "",
|
|
219
|
+
stderr: "",
|
|
220
|
+
pid: 1,
|
|
221
|
+
output: [],
|
|
222
|
+
signal: null,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const containers = DockerSandbox.listAll();
|
|
226
|
+
expect(containers).toEqual([]);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("findExisting", () => {
|
|
231
|
+
it("should find existing container for agent", async () => {
|
|
232
|
+
const { spawnSync } = await import("node:child_process");
|
|
233
|
+
vi.mocked(spawnSync).mockReturnValueOnce({
|
|
234
|
+
status: 0,
|
|
235
|
+
stdout: "abc123def456\n",
|
|
236
|
+
stderr: "",
|
|
237
|
+
pid: 1,
|
|
238
|
+
output: [],
|
|
239
|
+
signal: null,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const containerId = DockerSandbox.findExisting("test-agent");
|
|
243
|
+
expect(containerId).toBe("abc123def456");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("should return null when no container exists", async () => {
|
|
247
|
+
const { spawnSync } = await import("node:child_process");
|
|
248
|
+
vi.mocked(spawnSync).mockReturnValueOnce({
|
|
249
|
+
status: 0,
|
|
250
|
+
stdout: "\n",
|
|
251
|
+
stderr: "",
|
|
252
|
+
pid: 1,
|
|
253
|
+
output: [],
|
|
254
|
+
signal: null,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const containerId = DockerSandbox.findExisting("nonexistent");
|
|
258
|
+
expect(containerId).toBeNull();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("container lifecycle", () => {
|
|
263
|
+
it("should build correct docker run arguments", async () => {
|
|
264
|
+
const { spawnSync } = await import("node:child_process");
|
|
265
|
+
const mockSpawnSync = vi.mocked(spawnSync);
|
|
266
|
+
|
|
267
|
+
// Mock image inspect (image exists)
|
|
268
|
+
mockSpawnSync.mockReturnValueOnce({
|
|
269
|
+
status: 0,
|
|
270
|
+
stdout: "{}",
|
|
271
|
+
stderr: "",
|
|
272
|
+
pid: 1,
|
|
273
|
+
output: [],
|
|
274
|
+
signal: null,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Mock docker run
|
|
278
|
+
mockSpawnSync.mockReturnValueOnce({
|
|
279
|
+
status: 0,
|
|
280
|
+
stdout: "container-id-12345\n",
|
|
281
|
+
stderr: "",
|
|
282
|
+
pid: 1,
|
|
283
|
+
output: [],
|
|
284
|
+
signal: null,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const sandbox = new DockerSandbox({
|
|
288
|
+
agentName: "test",
|
|
289
|
+
image: "agentmesh/agent-sandbox:latest",
|
|
290
|
+
workspacePath: "/tmp/workspace",
|
|
291
|
+
cpuLimit: "2",
|
|
292
|
+
memoryLimit: "4g",
|
|
293
|
+
env: {
|
|
294
|
+
AGENT_TOKEN: "test-token",
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await sandbox.pullImage();
|
|
299
|
+
await sandbox.start();
|
|
300
|
+
|
|
301
|
+
// Find the docker run call
|
|
302
|
+
const runCall = mockSpawnSync.mock.calls.find(
|
|
303
|
+
(call) => call[0] === "docker" && call[1]?.[0] === "run",
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
expect(runCall).toBeDefined();
|
|
307
|
+
const args = runCall![1] as string[];
|
|
308
|
+
|
|
309
|
+
// Check key arguments
|
|
310
|
+
expect(args).toContain("-d");
|
|
311
|
+
expect(args).toContain("--cpus");
|
|
312
|
+
expect(args).toContain("2");
|
|
313
|
+
expect(args).toContain("--memory");
|
|
314
|
+
expect(args).toContain("4g");
|
|
315
|
+
expect(args).toContain("--user");
|
|
316
|
+
expect(args).toContain("1000:1000");
|
|
317
|
+
expect(args).toContain("-e");
|
|
318
|
+
expect(args).toContain("AGENT_TOKEN=test-token");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("should get container status", async () => {
|
|
322
|
+
const { spawnSync } = await import("node:child_process");
|
|
323
|
+
vi.mocked(spawnSync).mockReturnValueOnce({
|
|
324
|
+
status: 0,
|
|
325
|
+
stdout: "true|running|healthy",
|
|
326
|
+
stderr: "",
|
|
327
|
+
pid: 1,
|
|
328
|
+
output: [],
|
|
329
|
+
signal: null,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const sandbox = new DockerSandbox(defaultConfig);
|
|
333
|
+
// @ts-expect-error - accessing private property for testing
|
|
334
|
+
sandbox.containerId = "test-container";
|
|
335
|
+
|
|
336
|
+
const status = sandbox.getStatus();
|
|
337
|
+
|
|
338
|
+
expect(status).toEqual({
|
|
339
|
+
running: true,
|
|
340
|
+
status: "running",
|
|
341
|
+
health: "healthy",
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("should return not found status when container not started", async () => {
|
|
346
|
+
const { spawnSync } = await import("node:child_process");
|
|
347
|
+
vi.mocked(spawnSync).mockReturnValueOnce({
|
|
348
|
+
status: 1, // docker inspect fails for non-existent container
|
|
349
|
+
stdout: "",
|
|
350
|
+
stderr: "Error: No such container",
|
|
351
|
+
pid: 1,
|
|
352
|
+
output: [],
|
|
353
|
+
signal: null,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const sandbox = new DockerSandbox(defaultConfig);
|
|
357
|
+
const status = sandbox.getStatus();
|
|
358
|
+
|
|
359
|
+
expect(status).toEqual({
|
|
360
|
+
running: false,
|
|
361
|
+
status: "not found",
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe("resource limits", () => {
|
|
367
|
+
it("should apply default CPU limit of 1", () => {
|
|
368
|
+
const sandbox = new DockerSandbox(defaultConfig);
|
|
369
|
+
// The default is applied in constructor
|
|
370
|
+
expect(sandbox).toBeDefined();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("should apply default memory limit of 2g", () => {
|
|
374
|
+
const sandbox = new DockerSandbox(defaultConfig);
|
|
375
|
+
expect(sandbox).toBeDefined();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("should allow custom resource limits", () => {
|
|
379
|
+
const sandbox = new DockerSandbox({
|
|
380
|
+
...defaultConfig,
|
|
381
|
+
cpuLimit: "0.5",
|
|
382
|
+
memoryLimit: "512m",
|
|
383
|
+
});
|
|
384
|
+
expect(sandbox).toBeDefined();
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
describe("serve mode", () => {
|
|
389
|
+
it("should configure serve mode with port", async () => {
|
|
390
|
+
const { spawnSync } = await import("node:child_process");
|
|
391
|
+
const mockSpawnSync = vi.mocked(spawnSync);
|
|
392
|
+
|
|
393
|
+
// Mock image inspect
|
|
394
|
+
mockSpawnSync.mockReturnValueOnce({
|
|
395
|
+
status: 0,
|
|
396
|
+
stdout: "{}",
|
|
397
|
+
stderr: "",
|
|
398
|
+
pid: 1,
|
|
399
|
+
output: [],
|
|
400
|
+
signal: null,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Mock docker run
|
|
404
|
+
mockSpawnSync.mockReturnValueOnce({
|
|
405
|
+
status: 0,
|
|
406
|
+
stdout: "container-id\n",
|
|
407
|
+
stderr: "",
|
|
408
|
+
pid: 1,
|
|
409
|
+
output: [],
|
|
410
|
+
signal: null,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const sandbox = new DockerSandbox({
|
|
414
|
+
...defaultConfig,
|
|
415
|
+
serveMode: true,
|
|
416
|
+
servePort: 3001,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
await sandbox.pullImage();
|
|
420
|
+
await sandbox.start();
|
|
421
|
+
|
|
422
|
+
const runCall = mockSpawnSync.mock.calls.find(
|
|
423
|
+
(call) => call[0] === "docker" && call[1]?.[0] === "run",
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
expect(runCall).toBeDefined();
|
|
427
|
+
const args = runCall![1] as string[];
|
|
428
|
+
|
|
429
|
+
expect(args).toContain("-p");
|
|
430
|
+
expect(args).toContain("3001:3001");
|
|
431
|
+
expect(args).toContain("opencode");
|
|
432
|
+
expect(args).toContain("serve");
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
});
|
package/src/cli/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { contextCmd } from "./context.js";
|
|
|
10
10
|
import { deploy } from "./deploy.js";
|
|
11
11
|
import { init } from "./init.js";
|
|
12
12
|
import { list } from "./list.js";
|
|
13
|
-
import {
|
|
13
|
+
import { localDown, localLogs, localStatus, localUp } from "./local.js";
|
|
14
14
|
import { logs } from "./logs.js";
|
|
15
15
|
import { migrate } from "./migrate.js";
|
|
16
16
|
import { nudge } from "./nudge.js";
|
|
@@ -57,6 +57,13 @@ program
|
|
|
57
57
|
.option("--auto-setup", "Auto-clone repository for project assignments")
|
|
58
58
|
.option("--serve", "Run opencode serve instead of tmux TUI (for Integration Service)")
|
|
59
59
|
.option("--serve-port <port>", "Port for opencode serve (default: 3001)", "3001")
|
|
60
|
+
.option("--sandbox", "Run agent in Docker sandbox container with filesystem isolation")
|
|
61
|
+
.option(
|
|
62
|
+
"--sandbox-image <image>",
|
|
63
|
+
"Docker image for sandbox (default: agentmesh/agent-sandbox:latest)",
|
|
64
|
+
)
|
|
65
|
+
.option("--sandbox-cpu <limit>", "CPU limit for sandbox (default: 1)")
|
|
66
|
+
.option("--sandbox-memory <limit>", "Memory limit for sandbox (default: 2g)")
|
|
60
67
|
.action(async (options) => {
|
|
61
68
|
try {
|
|
62
69
|
// Parse serve port as number
|
package/src/cli/start.ts
CHANGED
|
@@ -18,6 +18,14 @@ export interface StartOptions {
|
|
|
18
18
|
serve?: boolean;
|
|
19
19
|
/** Port for opencode serve (default: 3001) */
|
|
20
20
|
servePort?: number;
|
|
21
|
+
/** Run agent in Docker sandbox container */
|
|
22
|
+
sandbox?: boolean;
|
|
23
|
+
/** Docker image for sandbox */
|
|
24
|
+
sandboxImage?: string;
|
|
25
|
+
/** CPU limit for sandbox */
|
|
26
|
+
sandboxCpu?: string;
|
|
27
|
+
/** Memory limit for sandbox */
|
|
28
|
+
sandboxMemory?: string;
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
export async function start(options: StartOptions): Promise<void> {
|
|
@@ -78,6 +86,12 @@ export async function start(options: StartOptions): Promise<void> {
|
|
|
78
86
|
args.push("--serve");
|
|
79
87
|
if (options.servePort) args.push("--serve-port", String(options.servePort));
|
|
80
88
|
}
|
|
89
|
+
if (options.sandbox) {
|
|
90
|
+
args.push("--sandbox");
|
|
91
|
+
if (options.sandboxImage) args.push("--sandbox-image", options.sandboxImage);
|
|
92
|
+
if (options.sandboxCpu) args.push("--sandbox-cpu", options.sandboxCpu);
|
|
93
|
+
if (options.sandboxMemory) args.push("--sandbox-memory", options.sandboxMemory);
|
|
94
|
+
}
|
|
81
95
|
|
|
82
96
|
// Spawn detached background process
|
|
83
97
|
const child = spawn("node", [cliPath, ...args], {
|
package/src/core/daemon.ts
CHANGED
|
@@ -13,13 +13,14 @@ import type { AgentConfig, Config } from "../config/schema.js";
|
|
|
13
13
|
import { loadContext, loadOrCreateContext, saveContext } from "../context/index.js";
|
|
14
14
|
import { Heartbeat } from "./heartbeat.js";
|
|
15
15
|
import { handleWebSocketEvent, injectRestoredContext, injectStartupMessage } from "./injector.js";
|
|
16
|
-
import { checkInbox, fetchAssignments, registerAgent } from "./registry.js";
|
|
16
|
+
import { checkInbox, fetchAssignments, registerAgent, type ServerContext } from "./registry.js";
|
|
17
17
|
import {
|
|
18
18
|
buildRunnerConfig,
|
|
19
19
|
detectRunner,
|
|
20
20
|
getRunnerDisplayName,
|
|
21
21
|
type RunnerConfig,
|
|
22
22
|
} from "./runner.js";
|
|
23
|
+
import { DockerSandbox } from "./sandbox.js";
|
|
23
24
|
import {
|
|
24
25
|
captureSessionContext,
|
|
25
26
|
createSession,
|
|
@@ -44,6 +45,14 @@ export interface DaemonOptions {
|
|
|
44
45
|
serve?: boolean;
|
|
45
46
|
/** Port for opencode serve (default: 3001) */
|
|
46
47
|
servePort?: number;
|
|
48
|
+
/** Run agent in Docker sandbox container */
|
|
49
|
+
sandbox?: boolean;
|
|
50
|
+
/** Docker image for sandbox (default: agentmesh/agent-sandbox:latest) */
|
|
51
|
+
sandboxImage?: string;
|
|
52
|
+
/** CPU limit for sandbox (default: 1) */
|
|
53
|
+
sandboxCpu?: string;
|
|
54
|
+
/** Memory limit for sandbox (default: 2g) */
|
|
55
|
+
sandboxMemory?: string;
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
export class AgentDaemon {
|
|
@@ -62,6 +71,12 @@ export class AgentDaemon {
|
|
|
62
71
|
private serveMode: boolean;
|
|
63
72
|
private servePort: number;
|
|
64
73
|
private serveProcess: ChildProcess | null = null;
|
|
74
|
+
private serverContext: ServerContext | undefined;
|
|
75
|
+
private sandboxMode: boolean;
|
|
76
|
+
private sandboxImage: string;
|
|
77
|
+
private sandboxCpu: string;
|
|
78
|
+
private sandboxMemory: string;
|
|
79
|
+
private sandbox: DockerSandbox | null = null;
|
|
65
80
|
|
|
66
81
|
constructor(options: DaemonOptions) {
|
|
67
82
|
const config = loadConfig();
|
|
@@ -94,6 +109,10 @@ export class AgentDaemon {
|
|
|
94
109
|
this.agentConfig = agentConfig;
|
|
95
110
|
this.serveMode = options.serve === true;
|
|
96
111
|
this.servePort = options.servePort || 3001;
|
|
112
|
+
this.sandboxMode = options.sandbox === true;
|
|
113
|
+
this.sandboxImage = options.sandboxImage || "agentmesh/agent-sandbox:latest";
|
|
114
|
+
this.sandboxCpu = options.sandboxCpu || "1";
|
|
115
|
+
this.sandboxMemory = options.sandboxMemory || "2g";
|
|
97
116
|
|
|
98
117
|
// Build runner configuration with model resolution
|
|
99
118
|
this.runnerConfig = buildRunnerConfig({
|
|
@@ -128,18 +147,29 @@ export class AgentDaemon {
|
|
|
128
147
|
agentId: existingState?.agentId || this.agentConfig.agentId,
|
|
129
148
|
agentName: this.agentName,
|
|
130
149
|
model: this.agentConfig.model || this.config.defaults.model,
|
|
150
|
+
restoreContext: this.shouldRestoreContext,
|
|
131
151
|
});
|
|
132
152
|
|
|
133
153
|
this.agentId = registration.agentId;
|
|
134
154
|
this.token = registration.token;
|
|
135
155
|
|
|
136
|
-
|
|
156
|
+
if (registration.status === "re-registered") {
|
|
157
|
+
console.log(`Re-registered as: ${this.agentId}`);
|
|
158
|
+
if (registration.context && Object.keys(registration.context).length > 0) {
|
|
159
|
+
this.serverContext = registration.context;
|
|
160
|
+
console.log(`Server context restored: ${Object.keys(registration.context).join(", ")}`);
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
console.log(`Registered as: ${this.agentId}`);
|
|
164
|
+
}
|
|
137
165
|
|
|
138
166
|
// Check assignments and auto-setup workdir if needed (before creating tmux session)
|
|
139
167
|
await this.checkAssignments();
|
|
140
168
|
|
|
141
|
-
//
|
|
142
|
-
if (this.
|
|
169
|
+
// Choose runtime mode: sandbox > serve > tmux
|
|
170
|
+
if (this.sandboxMode) {
|
|
171
|
+
await this.startSandboxMode();
|
|
172
|
+
} else if (this.serveMode) {
|
|
143
173
|
await this.startServeMode();
|
|
144
174
|
} else {
|
|
145
175
|
// Check if session already exists
|
|
@@ -337,8 +367,12 @@ Nudge agent:
|
|
|
337
367
|
this.ws = null;
|
|
338
368
|
}
|
|
339
369
|
|
|
340
|
-
// Stop serve process or destroy tmux session
|
|
341
|
-
if (this.
|
|
370
|
+
// Stop sandbox, serve process, or destroy tmux session
|
|
371
|
+
if (this.sandboxMode && this.sandbox) {
|
|
372
|
+
console.log("Stopping sandbox container...");
|
|
373
|
+
await this.sandbox.destroy();
|
|
374
|
+
this.sandbox = null;
|
|
375
|
+
} else if (this.serveMode && this.serveProcess) {
|
|
342
376
|
console.log("Stopping opencode serve...");
|
|
343
377
|
this.serveProcess.kill("SIGTERM");
|
|
344
378
|
this.serveProcess = null;
|
|
@@ -409,6 +443,80 @@ Nudge agent:
|
|
|
409
443
|
console.log(`opencode serve started on http://0.0.0.0:${this.servePort}`);
|
|
410
444
|
}
|
|
411
445
|
|
|
446
|
+
/**
|
|
447
|
+
* Starts agent in Docker sandbox mode
|
|
448
|
+
* Provides filesystem isolation with only workspace mounted
|
|
449
|
+
*/
|
|
450
|
+
private async startSandboxMode(): Promise<void> {
|
|
451
|
+
console.log("Starting in Docker sandbox mode...");
|
|
452
|
+
|
|
453
|
+
// Check Docker availability
|
|
454
|
+
if (!DockerSandbox.checkDockerAvailable()) {
|
|
455
|
+
throw new Error(
|
|
456
|
+
"Docker is not available. Install Docker or use --sandbox host to run on host.",
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const workdir = this.agentConfig.workdir || process.cwd();
|
|
461
|
+
|
|
462
|
+
// Check for existing sandbox container
|
|
463
|
+
const existingContainer = DockerSandbox.findExisting(this.agentName);
|
|
464
|
+
if (existingContainer) {
|
|
465
|
+
console.log(`Found existing sandbox container: ${existingContainer}`);
|
|
466
|
+
console.log("Stop it with: agentmesh stop " + this.agentName);
|
|
467
|
+
throw new Error("Sandbox container already exists");
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Create sandbox configuration
|
|
471
|
+
this.sandbox = new DockerSandbox({
|
|
472
|
+
agentName: this.agentName,
|
|
473
|
+
image: this.sandboxImage,
|
|
474
|
+
workspacePath: workdir,
|
|
475
|
+
cpuLimit: this.sandboxCpu,
|
|
476
|
+
memoryLimit: this.sandboxMemory,
|
|
477
|
+
env: {
|
|
478
|
+
...this.runnerConfig.env,
|
|
479
|
+
AGENT_TOKEN: this.token!,
|
|
480
|
+
AGENTMESH_AGENT_ID: this.agentId!,
|
|
481
|
+
},
|
|
482
|
+
serveMode: this.serveMode,
|
|
483
|
+
servePort: this.servePort,
|
|
484
|
+
networkMode: "bridge",
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// Validate mount policy (will throw if denied)
|
|
488
|
+
this.sandbox.validateMountPolicy();
|
|
489
|
+
|
|
490
|
+
// Pull image if needed
|
|
491
|
+
await this.sandbox.pullImage();
|
|
492
|
+
|
|
493
|
+
// Start container
|
|
494
|
+
await this.sandbox.start();
|
|
495
|
+
|
|
496
|
+
console.log(`
|
|
497
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
498
|
+
🐳 SANDBOX MODE ACTIVE
|
|
499
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
500
|
+
|
|
501
|
+
Container: ${this.sandbox.getContainerName()}
|
|
502
|
+
Image: ${this.sandboxImage}
|
|
503
|
+
Workspace: ${workdir} -> /workspace
|
|
504
|
+
CPU: ${this.sandboxCpu} core(s)
|
|
505
|
+
Memory: ${this.sandboxMemory}
|
|
506
|
+
|
|
507
|
+
The agent is running in an isolated Docker container.
|
|
508
|
+
Only the workspace directory is accessible.
|
|
509
|
+
|
|
510
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
511
|
+
`);
|
|
512
|
+
|
|
513
|
+
// Start opencode in the container
|
|
514
|
+
if (!this.serveMode) {
|
|
515
|
+
console.log("Starting opencode in sandbox container...");
|
|
516
|
+
await this.sandbox.spawnOpencode();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
412
520
|
/**
|
|
413
521
|
* Saves the current agent context to disk
|
|
414
522
|
*/
|