@agentmeshhq/agent 0.1.11 → 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.
Files changed (74) hide show
  1. package/dist/__tests__/injector.test.d.ts +1 -0
  2. package/dist/__tests__/injector.test.js +26 -0
  3. package/dist/__tests__/injector.test.js.map +1 -0
  4. package/dist/__tests__/sandbox.test.d.ts +1 -0
  5. package/dist/__tests__/sandbox.test.js +362 -0
  6. package/dist/__tests__/sandbox.test.js.map +1 -0
  7. package/dist/cli/build.d.ts +6 -0
  8. package/dist/cli/build.js +111 -0
  9. package/dist/cli/build.js.map +1 -0
  10. package/dist/cli/deploy.d.ts +9 -0
  11. package/dist/cli/deploy.js +130 -0
  12. package/dist/cli/deploy.js.map +1 -0
  13. package/dist/cli/inbox.d.ts +5 -0
  14. package/dist/cli/inbox.js +123 -0
  15. package/dist/cli/inbox.js.map +1 -0
  16. package/dist/cli/index.js +159 -0
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/cli/issue.d.ts +42 -0
  19. package/dist/cli/issue.js +297 -0
  20. package/dist/cli/issue.js.map +1 -0
  21. package/dist/cli/local.d.ts +9 -0
  22. package/dist/cli/local.js +139 -0
  23. package/dist/cli/local.js.map +1 -0
  24. package/dist/cli/migrate.d.ts +8 -0
  25. package/dist/cli/migrate.js +167 -0
  26. package/dist/cli/migrate.js.map +1 -0
  27. package/dist/cli/ready.d.ts +5 -0
  28. package/dist/cli/ready.js +131 -0
  29. package/dist/cli/ready.js.map +1 -0
  30. package/dist/cli/slack.d.ts +3 -0
  31. package/dist/cli/slack.js +57 -0
  32. package/dist/cli/slack.js.map +1 -0
  33. package/dist/cli/start.d.ts +12 -0
  34. package/dist/cli/start.js +14 -0
  35. package/dist/cli/start.js.map +1 -1
  36. package/dist/cli/sync.d.ts +8 -0
  37. package/dist/cli/sync.js +154 -0
  38. package/dist/cli/sync.js.map +1 -0
  39. package/dist/cli/test.d.ts +8 -0
  40. package/dist/cli/test.js +110 -0
  41. package/dist/cli/test.js.map +1 -0
  42. package/dist/core/daemon.d.ts +31 -0
  43. package/dist/core/daemon.js +188 -28
  44. package/dist/core/daemon.js.map +1 -1
  45. package/dist/core/injector.d.ts +6 -1
  46. package/dist/core/injector.js +64 -1
  47. package/dist/core/injector.js.map +1 -1
  48. package/dist/core/issue-cache.d.ts +44 -0
  49. package/dist/core/issue-cache.js +75 -0
  50. package/dist/core/issue-cache.js.map +1 -0
  51. package/dist/core/registry.d.ts +5 -0
  52. package/dist/core/registry.js +8 -1
  53. package/dist/core/registry.js.map +1 -1
  54. package/dist/core/sandbox.d.ts +127 -0
  55. package/dist/core/sandbox.js +377 -0
  56. package/dist/core/sandbox.js.map +1 -0
  57. package/dist/core/tmux.js +8 -10
  58. package/dist/core/tmux.js.map +1 -1
  59. package/package.json +1 -1
  60. package/src/__tests__/injector.test.ts +29 -0
  61. package/src/__tests__/sandbox.test.ts +435 -0
  62. package/src/cli/build.ts +137 -0
  63. package/src/cli/deploy.ts +153 -0
  64. package/src/cli/index.ts +163 -0
  65. package/src/cli/local.ts +174 -0
  66. package/src/cli/migrate.ts +210 -0
  67. package/src/cli/slack.ts +69 -0
  68. package/src/cli/start.ts +22 -0
  69. package/src/cli/test.ts +141 -0
  70. package/src/core/daemon.ts +228 -37
  71. package/src/core/injector.ts +98 -1
  72. package/src/core/registry.ts +14 -1
  73. package/src/core/sandbox.ts +505 -0
  74. package/src/core/tmux.ts +9 -11
@@ -0,0 +1,505 @@
1
+ /**
2
+ * Docker Sandbox Runtime
3
+ *
4
+ * Provides containerized execution of agents with filesystem isolation.
5
+ * Each agent runs in its own container with only the allowed workspace mounted.
6
+ */
7
+
8
+ import { type ChildProcess, execSync, spawn, spawnSync } from "node:child_process";
9
+ import crypto from "node:crypto";
10
+ import fs from "node:fs";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+
14
+ export interface SandboxConfig {
15
+ /** Agent name (used for container naming) */
16
+ agentName: string;
17
+ /** Image to use for the container */
18
+ image: string;
19
+ /** Workspace path on host to mount */
20
+ workspacePath: string;
21
+ /** Container mount point for workspace (default: /workspace) */
22
+ containerWorkdir?: string;
23
+ /** CPU limit (e.g., "0.5" for half a CPU) */
24
+ cpuLimit?: string;
25
+ /** Memory limit (e.g., "512m", "1g") */
26
+ memoryLimit?: string;
27
+ /** Environment variables to pass to container */
28
+ env?: Record<string, string>;
29
+ /** Network mode (default: bridge) */
30
+ networkMode?: string;
31
+ /** Additional mounts (host:container format) */
32
+ additionalMounts?: string[];
33
+ /** Run opencode serve instead of TUI */
34
+ serveMode?: boolean;
35
+ /** Port for serve mode */
36
+ servePort?: number;
37
+ }
38
+
39
+ export interface SandboxMountPolicy {
40
+ /** Paths that are allowed to be mounted */
41
+ allowedPaths: string[];
42
+ /** Paths that are explicitly denied (checked first) */
43
+ deniedPaths: string[];
44
+ }
45
+
46
+ // Default mount policy - deny sensitive paths
47
+ const DEFAULT_DENY_PATHS = [
48
+ "~/.ssh",
49
+ "~/.gnupg",
50
+ "~/.aws",
51
+ "~/.config/gcloud",
52
+ "~/.kube",
53
+ "~/.docker",
54
+ "/etc/passwd",
55
+ "/etc/shadow",
56
+ "/etc/hosts",
57
+ "/var/run/docker.sock",
58
+ ];
59
+
60
+ export class DockerSandbox {
61
+ private config: SandboxConfig;
62
+ private containerId: string | null = null;
63
+ private containerName: string;
64
+ private process: ChildProcess | null = null;
65
+
66
+ constructor(config: SandboxConfig) {
67
+ this.config = {
68
+ containerWorkdir: "/workspace",
69
+ cpuLimit: "1",
70
+ memoryLimit: "2g",
71
+ networkMode: "bridge",
72
+ ...config,
73
+ };
74
+ // Generate unique container name
75
+ const suffix = crypto.randomBytes(4).toString("hex");
76
+ this.containerName = `agentmesh-sandbox-${config.agentName}-${suffix}`;
77
+ }
78
+
79
+ /**
80
+ * Validates that the workspace path is allowed based on policy
81
+ */
82
+ validateMountPolicy(policy?: SandboxMountPolicy): void {
83
+ const workspacePath = this.expandPath(this.config.workspacePath);
84
+
85
+ // Check denied paths first
86
+ const deniedPaths = policy?.deniedPaths || DEFAULT_DENY_PATHS;
87
+ for (const denied of deniedPaths) {
88
+ const expandedDenied = this.expandPath(denied);
89
+ if (workspacePath.startsWith(expandedDenied) || workspacePath === expandedDenied) {
90
+ throw new Error(`Mount denied: ${workspacePath} matches denied path ${denied}`);
91
+ }
92
+ }
93
+
94
+ // Check allowed paths if specified
95
+ if (policy?.allowedPaths && policy.allowedPaths.length > 0) {
96
+ const isAllowed = policy.allowedPaths.some((allowed) => {
97
+ const expandedAllowed = this.expandPath(allowed);
98
+ return workspacePath.startsWith(expandedAllowed);
99
+ });
100
+ if (!isAllowed) {
101
+ throw new Error(`Mount denied: ${workspacePath} is not in allowed paths`);
102
+ }
103
+ }
104
+
105
+ // Verify path exists
106
+ if (!fs.existsSync(workspacePath)) {
107
+ throw new Error(`Workspace path does not exist: ${workspacePath}`);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Expands ~ to home directory
113
+ */
114
+ private expandPath(p: string): string {
115
+ if (p.startsWith("~")) {
116
+ return path.join(os.homedir(), p.slice(1));
117
+ }
118
+ return path.resolve(p);
119
+ }
120
+
121
+ /**
122
+ * Checks if Docker is available
123
+ */
124
+ static checkDockerAvailable(): boolean {
125
+ try {
126
+ const result = spawnSync("docker", ["--version"], {
127
+ encoding: "utf-8",
128
+ timeout: 5000,
129
+ });
130
+ return result.status === 0;
131
+ } catch {
132
+ return false;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Pulls the sandbox image if not present
138
+ */
139
+ async pullImage(): Promise<void> {
140
+ console.log(`Checking for image: ${this.config.image}`);
141
+
142
+ // Check if image exists locally
143
+ const result = spawnSync("docker", ["image", "inspect", this.config.image], {
144
+ encoding: "utf-8",
145
+ });
146
+
147
+ if (result.status === 0) {
148
+ console.log("Image found locally");
149
+ return;
150
+ }
151
+
152
+ console.log(`Pulling image: ${this.config.image}...`);
153
+ const pullResult = spawnSync("docker", ["pull", this.config.image], {
154
+ encoding: "utf-8",
155
+ stdio: "inherit",
156
+ });
157
+
158
+ if (pullResult.status !== 0) {
159
+ throw new Error(`Failed to pull image: ${this.config.image}`);
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Creates and starts the sandbox container
165
+ */
166
+ async start(): Promise<string> {
167
+ // Validate mount policy
168
+ this.validateMountPolicy();
169
+
170
+ const workspacePath = this.expandPath(this.config.workspacePath);
171
+
172
+ // Build docker run arguments
173
+ const args: string[] = [
174
+ "run",
175
+ "-d", // Detached
176
+ "--name",
177
+ this.containerName,
178
+ "--hostname",
179
+ `sandbox-${this.config.agentName}`,
180
+
181
+ // Resource limits
182
+ "--cpus",
183
+ this.config.cpuLimit!,
184
+ "--memory",
185
+ this.config.memoryLimit!,
186
+
187
+ // Security: run as non-root
188
+ "--user",
189
+ "1000:1000",
190
+
191
+ // Network
192
+ "--network",
193
+ this.config.networkMode!,
194
+
195
+ // Mount workspace
196
+ "-v",
197
+ `${workspacePath}:${this.config.containerWorkdir}:rw`,
198
+
199
+ // Working directory
200
+ "-w",
201
+ this.config.containerWorkdir!,
202
+ ];
203
+
204
+ // Add environment variables
205
+ if (this.config.env) {
206
+ for (const [key, value] of Object.entries(this.config.env)) {
207
+ args.push("-e", `${key}=${value}`);
208
+ }
209
+ }
210
+
211
+ // Add additional mounts
212
+ if (this.config.additionalMounts) {
213
+ for (const mount of this.config.additionalMounts) {
214
+ args.push("-v", mount);
215
+ }
216
+ }
217
+
218
+ // Expose port for serve mode
219
+ if (this.config.serveMode && this.config.servePort) {
220
+ args.push("-p", `${this.config.servePort}:${this.config.servePort}`);
221
+ }
222
+
223
+ // Image and command
224
+ args.push(this.config.image);
225
+
226
+ // Command: either serve mode or keep container alive for tmux-style attach
227
+ if (this.config.serveMode) {
228
+ args.push(
229
+ "opencode",
230
+ "serve",
231
+ "--port",
232
+ String(this.config.servePort || 3001),
233
+ "--hostname",
234
+ "0.0.0.0",
235
+ );
236
+ } else {
237
+ // Keep container running for attach capability
238
+ args.push("tail", "-f", "/dev/null");
239
+ }
240
+
241
+ console.log(`Starting sandbox container: ${this.containerName}`);
242
+
243
+ const result = spawnSync("docker", args, {
244
+ encoding: "utf-8",
245
+ });
246
+
247
+ if (result.status !== 0) {
248
+ throw new Error(`Failed to start container: ${result.stderr || result.stdout}`);
249
+ }
250
+
251
+ this.containerId = result.stdout.trim();
252
+ console.log(`Container started: ${this.containerId.substring(0, 12)}`);
253
+
254
+ return this.containerId;
255
+ }
256
+
257
+ /**
258
+ * Executes a command in the running container
259
+ */
260
+ async exec(command: string[], options?: { interactive?: boolean }): Promise<string> {
261
+ if (!this.containerId && !this.containerName) {
262
+ throw new Error("Container not started");
263
+ }
264
+
265
+ const target = this.containerId || this.containerName;
266
+ const args = ["exec"];
267
+
268
+ if (options?.interactive) {
269
+ args.push("-it");
270
+ }
271
+
272
+ args.push(target, ...command);
273
+
274
+ const result = spawnSync("docker", args, {
275
+ encoding: "utf-8",
276
+ stdio: options?.interactive ? "inherit" : "pipe",
277
+ });
278
+
279
+ if (result.status !== 0 && !options?.interactive) {
280
+ throw new Error(`Exec failed: ${result.stderr || result.stdout}`);
281
+ }
282
+
283
+ return result.stdout || "";
284
+ }
285
+
286
+ /**
287
+ * Spawns opencode in the container (for non-serve mode)
288
+ */
289
+ async spawnOpencode(): Promise<ChildProcess> {
290
+ if (!this.containerId && !this.containerName) {
291
+ throw new Error("Container not started");
292
+ }
293
+
294
+ const target = this.containerId || this.containerName;
295
+
296
+ // Build environment args
297
+ const envArgs: string[] = [];
298
+ if (this.config.env) {
299
+ for (const [key, value] of Object.entries(this.config.env)) {
300
+ envArgs.push("-e", `${key}=${value}`);
301
+ }
302
+ }
303
+
304
+ this.process = spawn("docker", ["exec", "-it", ...envArgs, target, "opencode"], {
305
+ stdio: "inherit",
306
+ });
307
+
308
+ return this.process;
309
+ }
310
+
311
+ /**
312
+ * Attaches to the container's opencode process
313
+ */
314
+ attach(): void {
315
+ if (!this.containerId && !this.containerName) {
316
+ throw new Error("Container not started");
317
+ }
318
+
319
+ const target = this.containerId || this.containerName;
320
+
321
+ // Exec into container interactively
322
+ execSync(`docker exec -it ${target} opencode`, {
323
+ stdio: "inherit",
324
+ });
325
+ }
326
+
327
+ /**
328
+ * Gets container logs
329
+ */
330
+ getLogs(lines = 100): string {
331
+ if (!this.containerId && !this.containerName) {
332
+ throw new Error("Container not started");
333
+ }
334
+
335
+ const target = this.containerId || this.containerName;
336
+ const result = spawnSync("docker", ["logs", "--tail", String(lines), target], {
337
+ encoding: "utf-8",
338
+ });
339
+
340
+ return result.stdout + result.stderr;
341
+ }
342
+
343
+ /**
344
+ * Follows container logs
345
+ */
346
+ followLogs(): void {
347
+ if (!this.containerId && !this.containerName) {
348
+ throw new Error("Container not started");
349
+ }
350
+
351
+ const target = this.containerId || this.containerName;
352
+ execSync(`docker logs -f ${target}`, { stdio: "inherit" });
353
+ }
354
+
355
+ /**
356
+ * Checks if container is running
357
+ */
358
+ isRunning(): boolean {
359
+ if (!this.containerId && !this.containerName) {
360
+ return false;
361
+ }
362
+
363
+ const target = this.containerId || this.containerName;
364
+ const result = spawnSync("docker", ["inspect", "-f", "{{.State.Running}}", target], {
365
+ encoding: "utf-8",
366
+ });
367
+
368
+ return result.stdout.trim() === "true";
369
+ }
370
+
371
+ /**
372
+ * Gets container status
373
+ */
374
+ getStatus(): { running: boolean; status: string; health?: string } {
375
+ if (!this.containerId && !this.containerName) {
376
+ return { running: false, status: "not created" };
377
+ }
378
+
379
+ const target = this.containerId || this.containerName;
380
+ const result = spawnSync(
381
+ "docker",
382
+ ["inspect", "-f", "{{.State.Running}}|{{.State.Status}}|{{.State.Health.Status}}", target],
383
+ { encoding: "utf-8" },
384
+ );
385
+
386
+ if (result.status !== 0) {
387
+ return { running: false, status: "not found" };
388
+ }
389
+
390
+ const [running, status, health] = result.stdout.trim().split("|");
391
+ return {
392
+ running: running === "true",
393
+ status,
394
+ health: health || undefined,
395
+ };
396
+ }
397
+
398
+ /**
399
+ * Stops the container
400
+ */
401
+ async stop(): Promise<void> {
402
+ if (!this.containerId && !this.containerName) {
403
+ return;
404
+ }
405
+
406
+ const target = this.containerId || this.containerName;
407
+ console.log(`Stopping container: ${target}`);
408
+
409
+ spawnSync("docker", ["stop", "-t", "10", target], {
410
+ encoding: "utf-8",
411
+ });
412
+ }
413
+
414
+ /**
415
+ * Removes the container
416
+ */
417
+ async destroy(): Promise<void> {
418
+ if (!this.containerId && !this.containerName) {
419
+ return;
420
+ }
421
+
422
+ const target = this.containerId || this.containerName;
423
+ console.log(`Destroying container: ${target}`);
424
+
425
+ // Stop if running
426
+ await this.stop();
427
+
428
+ // Remove
429
+ spawnSync("docker", ["rm", "-f", target], {
430
+ encoding: "utf-8",
431
+ });
432
+
433
+ this.containerId = null;
434
+ }
435
+
436
+ /**
437
+ * Gets container name
438
+ */
439
+ getContainerName(): string {
440
+ return this.containerName;
441
+ }
442
+
443
+ /**
444
+ * Gets container ID
445
+ */
446
+ getContainerId(): string | null {
447
+ return this.containerId;
448
+ }
449
+
450
+ /**
451
+ * Finds existing sandbox container for an agent
452
+ */
453
+ static findExisting(agentName: string): string | null {
454
+ const result = spawnSync(
455
+ "docker",
456
+ [
457
+ "ps",
458
+ "-aq",
459
+ "--filter",
460
+ `name=agentmesh-sandbox-${agentName}-`,
461
+ "--filter",
462
+ "status=running",
463
+ ],
464
+ { encoding: "utf-8" },
465
+ );
466
+
467
+ const containerId = result.stdout.trim().split("\n")[0];
468
+ return containerId || null;
469
+ }
470
+
471
+ /**
472
+ * Lists all sandbox containers
473
+ */
474
+ static listAll(): Array<{
475
+ id: string;
476
+ name: string;
477
+ status: string;
478
+ image: string;
479
+ }> {
480
+ const result = spawnSync(
481
+ "docker",
482
+ [
483
+ "ps",
484
+ "-a",
485
+ "--filter",
486
+ "name=agentmesh-sandbox-",
487
+ "--format",
488
+ "{{.ID}}|{{.Names}}|{{.Status}}|{{.Image}}",
489
+ ],
490
+ { encoding: "utf-8" },
491
+ );
492
+
493
+ if (result.status !== 0 || !result.stdout.trim()) {
494
+ return [];
495
+ }
496
+
497
+ return result.stdout
498
+ .trim()
499
+ .split("\n")
500
+ .map((line) => {
501
+ const [id, name, status, image] = line.split("|");
502
+ return { id, name, status, image };
503
+ });
504
+ }
505
+ }
package/src/core/tmux.ts CHANGED
@@ -56,7 +56,8 @@ export function createSession(
56
56
 
57
57
  const fullCommand = `${envPrefix}${command}`;
58
58
 
59
- const args = ["new-session", "-d", "-s", sessionName];
59
+ // Set reasonable terminal size for TUI applications
60
+ const args = ["new-session", "-d", "-s", sessionName, "-x", "200", "-y", "50"];
60
61
 
61
62
  if (workdir) {
62
63
  args.push("-c", workdir);
@@ -128,16 +129,13 @@ export function sendKeys(agentName: string, message: string): boolean {
128
129
  }
129
130
 
130
131
  try {
131
- // Escape special characters for tmux
132
- const escapedMessage = message
133
- .replace(/\\/g, "\\\\")
134
- .replace(/"/g, '\\"')
135
- .replace(/\$/g, "\\$")
136
- .replace(/`/g, "\\`");
137
-
138
- // Send the message, then press Enter separately to submit to the AI
139
- execSync(`tmux send-keys -t "${sessionName}" "${escapedMessage}"`);
140
- execSync(`tmux send-keys -t "${sessionName}" Enter`);
132
+ // Replace newlines with " | " to keep message on single line (newlines would act as Enter)
133
+ const cleanMessage = message.replace(/\n/g, " | ");
134
+
135
+ // Use execFileSync with array args to avoid shell escaping issues
136
+ // The -l flag sends keys literally
137
+ execFileSync("tmux", ["send-keys", "-t", sessionName, "-l", cleanMessage]);
138
+ execFileSync("tmux", ["send-keys", "-t", sessionName, "Enter"]);
141
139
  return true;
142
140
  } catch (error) {
143
141
  console.error(`Failed to send keys: ${error}`);