@vellumai/assistant 0.3.19 → 0.3.21

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 (199) hide show
  1. package/ARCHITECTURE.md +151 -15
  2. package/Dockerfile +1 -0
  3. package/README.md +40 -4
  4. package/bun.lock +139 -2
  5. package/docs/architecture/integrations.md +7 -11
  6. package/package.json +2 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +54 -0
  8. package/src/__tests__/approval-primitive.test.ts +540 -0
  9. package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
  10. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
  11. package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
  12. package/src/__tests__/call-controller.test.ts +439 -108
  13. package/src/__tests__/channel-invite-transport.test.ts +264 -0
  14. package/src/__tests__/cli.test.ts +42 -1
  15. package/src/__tests__/config-schema.test.ts +11 -127
  16. package/src/__tests__/config-watcher.test.ts +0 -8
  17. package/src/__tests__/daemon-lifecycle.test.ts +1 -0
  18. package/src/__tests__/daemon-server-session-init.test.ts +8 -2
  19. package/src/__tests__/diff.test.ts +22 -0
  20. package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
  21. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +300 -32
  22. package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
  23. package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
  24. package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
  25. package/src/__tests__/guardian-dispatch.test.ts +124 -0
  26. package/src/__tests__/guardian-grant-minting.test.ts +6 -17
  27. package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
  28. package/src/__tests__/invite-redemption-service.test.ts +306 -0
  29. package/src/__tests__/ipc-snapshot.test.ts +57 -0
  30. package/src/__tests__/notification-decision-fallback.test.ts +88 -0
  31. package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
  32. package/src/__tests__/sandbox-host-parity.test.ts +6 -13
  33. package/src/__tests__/scoped-approval-grants.test.ts +6 -6
  34. package/src/__tests__/scoped-grant-security-matrix.test.ts +5 -4
  35. package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
  36. package/src/__tests__/session-load-history-repair.test.ts +169 -2
  37. package/src/__tests__/session-runtime-assembly.test.ts +33 -5
  38. package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
  39. package/src/__tests__/skill-feature-flags.test.ts +188 -0
  40. package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
  41. package/src/__tests__/skill-mirror-parity.test.ts +1 -0
  42. package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
  43. package/src/__tests__/system-prompt.test.ts +1 -1
  44. package/src/__tests__/terminal-sandbox.test.ts +142 -9
  45. package/src/__tests__/terminal-tools.test.ts +2 -93
  46. package/src/__tests__/thread-seed-composer.test.ts +18 -0
  47. package/src/__tests__/tool-approval-handler.test.ts +350 -0
  48. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
  49. package/src/__tests__/voice-scoped-grant-consumer.test.ts +46 -84
  50. package/src/agent/loop.ts +36 -1
  51. package/src/approvals/approval-primitive.ts +381 -0
  52. package/src/approvals/guardian-decision-primitive.ts +191 -0
  53. package/src/calls/call-controller.ts +252 -209
  54. package/src/calls/call-domain.ts +44 -6
  55. package/src/calls/guardian-dispatch.ts +48 -0
  56. package/src/calls/types.ts +1 -1
  57. package/src/calls/voice-session-bridge.ts +46 -30
  58. package/src/cli/core-commands.ts +0 -4
  59. package/src/cli/mcp.ts +58 -0
  60. package/src/cli.ts +76 -34
  61. package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
  62. package/src/config/assistant-feature-flags.ts +162 -0
  63. package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
  64. package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
  65. package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
  66. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  67. package/src/config/bundled-skills/reminder/SKILL.md +49 -2
  68. package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
  69. package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
  70. package/src/config/core-schema.ts +1 -1
  71. package/src/config/env-registry.ts +10 -0
  72. package/src/config/feature-flag-registry.json +61 -0
  73. package/src/config/loader.ts +22 -1
  74. package/src/config/mcp-schema.ts +46 -0
  75. package/src/config/sandbox-schema.ts +0 -39
  76. package/src/config/schema.ts +18 -2
  77. package/src/config/skill-state.ts +34 -0
  78. package/src/config/skills-schema.ts +0 -1
  79. package/src/config/skills.ts +9 -0
  80. package/src/config/system-prompt.ts +110 -46
  81. package/src/config/templates/SOUL.md +1 -1
  82. package/src/config/types.ts +19 -1
  83. package/src/config/vellum-skills/catalog.json +1 -1
  84. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
  85. package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
  86. package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -5
  87. package/src/config/vellum-skills/trusted-contacts/SKILL.md +105 -3
  88. package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
  89. package/src/daemon/config-watcher.ts +0 -1
  90. package/src/daemon/daemon-control.ts +1 -1
  91. package/src/daemon/guardian-invite-intent.ts +124 -0
  92. package/src/daemon/handlers/avatar.ts +68 -0
  93. package/src/daemon/handlers/browser.ts +2 -2
  94. package/src/daemon/handlers/guardian-actions.ts +120 -0
  95. package/src/daemon/handlers/index.ts +4 -0
  96. package/src/daemon/handlers/sessions.ts +19 -0
  97. package/src/daemon/handlers/shared.ts +3 -1
  98. package/src/daemon/install-cli-launchers.ts +58 -13
  99. package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
  100. package/src/daemon/ipc-contract/sessions.ts +8 -2
  101. package/src/daemon/ipc-contract/settings.ts +25 -2
  102. package/src/daemon/ipc-contract-inventory.json +10 -0
  103. package/src/daemon/ipc-contract.ts +4 -0
  104. package/src/daemon/lifecycle.ts +14 -2
  105. package/src/daemon/main.ts +1 -0
  106. package/src/daemon/providers-setup.ts +26 -1
  107. package/src/daemon/server.ts +1 -0
  108. package/src/daemon/session-lifecycle.ts +52 -7
  109. package/src/daemon/session-memory.ts +45 -0
  110. package/src/daemon/session-process.ts +258 -432
  111. package/src/daemon/session-runtime-assembly.ts +12 -0
  112. package/src/daemon/session-skill-tools.ts +14 -1
  113. package/src/daemon/session-tool-setup.ts +5 -0
  114. package/src/daemon/session.ts +11 -0
  115. package/src/daemon/shutdown-handlers.ts +11 -0
  116. package/src/daemon/tool-side-effects.ts +35 -9
  117. package/src/index.ts +2 -2
  118. package/src/mcp/client.ts +152 -0
  119. package/src/mcp/manager.ts +139 -0
  120. package/src/memory/conversation-display-order-migration.ts +44 -0
  121. package/src/memory/conversation-queries.ts +2 -0
  122. package/src/memory/conversation-store.ts +91 -0
  123. package/src/memory/db-init.ts +5 -1
  124. package/src/memory/embedding-local.ts +13 -8
  125. package/src/memory/guardian-action-store.ts +125 -2
  126. package/src/memory/ingress-invite-store.ts +95 -1
  127. package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
  128. package/src/memory/migrations/index.ts +2 -1
  129. package/src/memory/schema.ts +5 -1
  130. package/src/memory/scoped-approval-grants.ts +14 -5
  131. package/src/messaging/providers/slack/client.ts +12 -0
  132. package/src/messaging/providers/slack/types.ts +5 -0
  133. package/src/notifications/decision-engine.ts +49 -12
  134. package/src/notifications/emit-signal.ts +7 -0
  135. package/src/notifications/signal.ts +7 -0
  136. package/src/notifications/thread-seed-composer.ts +2 -1
  137. package/src/runtime/channel-approval-types.ts +16 -6
  138. package/src/runtime/channel-approvals.ts +19 -15
  139. package/src/runtime/channel-invite-transport.ts +85 -0
  140. package/src/runtime/channel-invite-transports/telegram.ts +105 -0
  141. package/src/runtime/guardian-action-grant-minter.ts +92 -35
  142. package/src/runtime/guardian-action-message-composer.ts +30 -0
  143. package/src/runtime/guardian-decision-types.ts +91 -0
  144. package/src/runtime/http-server.ts +23 -1
  145. package/src/runtime/ingress-service.ts +22 -0
  146. package/src/runtime/invite-redemption-service.ts +181 -0
  147. package/src/runtime/invite-redemption-templates.ts +39 -0
  148. package/src/runtime/routes/call-routes.ts +2 -1
  149. package/src/runtime/routes/guardian-action-routes.ts +206 -0
  150. package/src/runtime/routes/guardian-approval-interception.ts +66 -190
  151. package/src/runtime/routes/identity-routes.ts +73 -0
  152. package/src/runtime/routes/inbound-message-handler.ts +486 -394
  153. package/src/runtime/routes/pairing-routes.ts +4 -0
  154. package/src/security/encrypted-store.ts +31 -17
  155. package/src/security/keychain.ts +176 -2
  156. package/src/security/secure-keys.ts +97 -0
  157. package/src/security/tool-approval-digest.ts +1 -1
  158. package/src/tools/browser/browser-execution.ts +2 -2
  159. package/src/tools/browser/browser-manager.ts +46 -32
  160. package/src/tools/browser/browser-screencast.ts +2 -2
  161. package/src/tools/calls/call-start.ts +1 -1
  162. package/src/tools/executor.ts +22 -17
  163. package/src/tools/mcp/mcp-tool-factory.ts +100 -0
  164. package/src/tools/network/script-proxy/session-manager.ts +1 -5
  165. package/src/tools/registry.ts +64 -1
  166. package/src/tools/skills/load.ts +22 -8
  167. package/src/tools/system/avatar-generator.ts +119 -0
  168. package/src/tools/system/navigate-settings.ts +65 -0
  169. package/src/tools/system/open-system-settings.ts +75 -0
  170. package/src/tools/system/voice-config.ts +121 -32
  171. package/src/tools/terminal/backends/native.ts +40 -19
  172. package/src/tools/terminal/backends/types.ts +3 -3
  173. package/src/tools/terminal/parser.ts +1 -1
  174. package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
  175. package/src/tools/terminal/sandbox.ts +1 -12
  176. package/src/tools/terminal/shell.ts +3 -31
  177. package/src/tools/tool-approval-handler.ts +141 -3
  178. package/src/tools/tool-manifest.ts +6 -0
  179. package/src/tools/types.ts +10 -2
  180. package/src/util/diff.ts +36 -13
  181. package/Dockerfile.sandbox +0 -5
  182. package/src/__tests__/doordash-client.test.ts +0 -187
  183. package/src/__tests__/doordash-session.test.ts +0 -154
  184. package/src/__tests__/signup-e2e.test.ts +0 -354
  185. package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
  186. package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
  187. package/src/cli/doordash.ts +0 -1057
  188. package/src/config/bundled-skills/doordash/SKILL.md +0 -163
  189. package/src/config/templates/LOOKS.md +0 -25
  190. package/src/doordash/cart-queries.ts +0 -787
  191. package/src/doordash/client.ts +0 -1016
  192. package/src/doordash/order-queries.ts +0 -85
  193. package/src/doordash/queries.ts +0 -13
  194. package/src/doordash/query-extractor.ts +0 -94
  195. package/src/doordash/search-queries.ts +0 -203
  196. package/src/doordash/session.ts +0 -84
  197. package/src/doordash/store-queries.ts +0 -246
  198. package/src/doordash/types.ts +0 -367
  199. package/src/tools/terminal/backends/docker.ts +0 -379
@@ -1,379 +0,0 @@
1
- import { execFileSync } from 'node:child_process';
2
- import { existsSync, realpathSync } from 'node:fs';
3
- import { dirname, posix,relative, resolve } from 'node:path';
4
-
5
- import type { DockerConfig } from '../../../config/types.js';
6
- import { ToolError } from '../../../util/errors.js';
7
- import { getLogger } from '../../../util/logger.js';
8
- import type { SandboxBackend, SandboxResult, WrapOptions } from './types.js';
9
-
10
- const log = getLogger('docker-sandbox');
11
-
12
- export const DEFAULT_SANDBOX_IMAGE = 'vellum-sandbox:latest';
13
-
14
- /**
15
- * Fallback defaults when DockerBackend is constructed without explicit config.
16
- * Must stay in sync with DockerConfigSchema defaults in config/schema.ts.
17
- */
18
- const DEFAULTS: Required<DockerConfig> = {
19
- image: DEFAULT_SANDBOX_IMAGE,
20
- shell: 'bash',
21
- cpus: 1,
22
- memoryMb: 512,
23
- pidsLimit: 256,
24
- network: 'none',
25
- };
26
-
27
- /**
28
- * Characters that are dangerous in Docker mount arguments or shell commands.
29
- * Commas are included because Docker's --mount flag uses them as field
30
- * delimiters — a path containing a comma could inject extra key=value pairs
31
- * (e.g. overriding dst= or src=) into the mount specification.
32
- */
33
- const UNSAFE_PATH_CHARS = /[\x00\n\r,]/;
34
-
35
- /**
36
- * Cache positive preflight results only, matching the bwrap pattern in native.ts.
37
- * Negative results are not cached so that installing/starting Docker after the
38
- * daemon starts takes effect without a restart.
39
- */
40
- let dockerCliAvailable = false;
41
- let dockerDaemonReachable = false;
42
- const imageAvailableCache = new Set<string>();
43
- const mountProbeCache = new Set<string>();
44
- /**
45
- * Maps (image, shell, configHash) → resolved shell path.
46
- * The config hash is included so that any DockerConfig change
47
- * (e.g. swapping image tags, changing resource limits) invalidates the cache.
48
- */
49
- const shellResolvedCache = new Map<string, string>();
50
-
51
- /** Exported for tests to reset cached state between runs. */
52
- export function _resetDockerChecks(): void {
53
- dockerCliAvailable = false;
54
- dockerDaemonReachable = false;
55
- imageAvailableCache.clear();
56
- mountProbeCache.clear();
57
- shellResolvedCache.clear();
58
- }
59
-
60
- function checkDockerCli(): void {
61
- if (dockerCliAvailable) return;
62
- try {
63
- execFileSync('docker', ['--version'], { stdio: 'ignore', timeout: 5000 });
64
- dockerCliAvailable = true;
65
- } catch {
66
- throw new ToolError(
67
- 'Docker CLI is not installed or not in PATH. Install Docker: https://docs.docker.com/get-docker/',
68
- 'bash',
69
- );
70
- }
71
- }
72
-
73
- function checkDockerDaemon(): void {
74
- if (dockerDaemonReachable) return;
75
- try {
76
- execFileSync('docker', ['info'], { stdio: 'ignore', timeout: 10000 });
77
- dockerDaemonReachable = true;
78
- } catch {
79
- throw new ToolError(
80
- 'Docker daemon is not running. Start Docker Desktop or run "sudo systemctl start docker".',
81
- 'bash',
82
- );
83
- }
84
- }
85
-
86
- /**
87
- * Resolve the path to Dockerfile.sandbox relative to this source file.
88
- * Works in both development (source layout) and bundled environments.
89
- *
90
- * In compiled Bun binaries, import.meta.dirname resolves into the virtual
91
- * $bunfs filesystem, so the Dockerfile won't exist there. Fall back to
92
- * looking next to the compiled binary (process.execPath) in that case.
93
- */
94
- function getSandboxDockerfilePath(): string {
95
- const dir = import.meta.dirname ?? __dirname;
96
- const sourcePath = resolve(dir, '../../../../Dockerfile.sandbox');
97
-
98
- // In compiled Bun binaries, dir points into /$bunfs/ which is virtual.
99
- // Fall back to looking next to the compiled binary itself.
100
- if (!existsSync(sourcePath) && dir.startsWith('/$bunfs/')) {
101
- return resolve(dirname(process.execPath), 'Dockerfile.sandbox');
102
- }
103
-
104
- return sourcePath;
105
- }
106
-
107
- function checkImageAvailable(image: string): void {
108
- if (imageAvailableCache.has(image)) return;
109
- try {
110
- // Use execFileSync to avoid shell interpolation of the image name.
111
- execFileSync('docker', ['image', 'inspect', image], { stdio: 'ignore', timeout: 10000 });
112
- imageAvailableCache.add(image);
113
- return;
114
- } catch {
115
- // Image not available locally — try to build or pull it.
116
- }
117
-
118
- // For the default sandbox image, build from Dockerfile.sandbox instead of pulling.
119
- if (image === DEFAULT_SANDBOX_IMAGE) {
120
- const dockerfile = getSandboxDockerfilePath();
121
- if (existsSync(dockerfile)) {
122
- log.info(`Building sandbox image "${image}" from ${dockerfile}...`);
123
- try {
124
- // --no-cache avoids stale apt-get layers with expired GPG signatures.
125
- execFileSync('docker', ['build', '--no-cache', '-t', image, '-f', dockerfile, '.'], {
126
- stdio: ['ignore', 'ignore', 'pipe'],
127
- timeout: 120000,
128
- cwd: resolve(dockerfile, '..'),
129
- });
130
- imageAvailableCache.add(image);
131
- return;
132
- } catch (err: unknown) {
133
- const stderr = err instanceof Error && 'stderr' in err
134
- ? String((err as { stderr: unknown }).stderr).trim()
135
- : '';
136
- const detail = stderr ? `\n\nBuild output:\n${stderr}` : '';
137
- throw new ToolError(
138
- `Failed to build sandbox image "${image}" from ${dockerfile}. ` +
139
- 'Check Docker is running and try building manually: ' +
140
- `docker build --no-cache -t ${image} -f ${dockerfile} ${resolve(dockerfile, '..')}` +
141
- detail,
142
- 'bash',
143
- );
144
- }
145
- }
146
-
147
- // Dockerfile not found — can't build the local-only image and pulling won't work.
148
- throw new ToolError(
149
- `Cannot find Dockerfile.sandbox to build "${image}". ` +
150
- 'This image is built locally and is not available from a registry. ' +
151
- 'If you have the Vellum source tree, build it manually:\n' +
152
- ' docker build --no-cache -t vellum-sandbox:latest -f assistant/Dockerfile.sandbox assistant\n' +
153
- 'Or set sandbox.docker.image to a different image in your config.',
154
- 'bash',
155
- );
156
- }
157
-
158
- log.info(`Docker image "${image}" not found locally, pulling...`);
159
- try {
160
- execFileSync('docker', ['pull', image], { stdio: 'ignore', timeout: 120000 });
161
- imageAvailableCache.add(image);
162
- } catch {
163
- throw new ToolError(
164
- `Failed to pull Docker image "${image}". Check your network connection or pull it manually: docker pull ${image}`,
165
- 'bash',
166
- );
167
- }
168
- }
169
-
170
- function checkMountProbe(sandboxRoot: string, image: string): void {
171
- const cacheKey = `${sandboxRoot}\0${image}`;
172
- if (mountProbeCache.has(cacheKey)) return;
173
- try {
174
- execFileSync(
175
- 'docker',
176
- [
177
- 'run', '--rm',
178
- '--mount', `type=bind,src=${sandboxRoot},dst=/workspace`,
179
- image, 'test', '-w', '/workspace',
180
- ],
181
- { stdio: 'ignore', timeout: 15000 },
182
- );
183
- mountProbeCache.add(cacheKey);
184
- } catch {
185
- throw new ToolError(
186
- 'Cannot bind-mount the sandbox root into a Docker container or /workspace is not writable. ' +
187
- 'If using Docker Desktop, enable file sharing for this path in Settings > Resources > File Sharing.',
188
- 'bash',
189
- );
190
- }
191
- }
192
-
193
- /**
194
- * Verify the configured shell exists in the image. If the requested shell
195
- * (e.g. 'bash') is missing, fall back to 'sh' which is available on virtually
196
- * every Linux image. If neither exists the image is too minimal to use.
197
- */
198
- function resolveShell(image: string, shell: string, configHash: string): string {
199
- const cacheKey = `${image}\0${shell}\0${configHash}`;
200
- const cached = shellResolvedCache.get(cacheKey);
201
- if (cached) return cached;
202
-
203
- // Try the configured shell first.
204
- try {
205
- execFileSync('docker', ['run', '--rm', image, shell, '-c', 'true'], {
206
- stdio: 'ignore',
207
- timeout: 15000,
208
- });
209
- shellResolvedCache.set(cacheKey, shell);
210
- return shell;
211
- } catch {
212
- // configured shell not available — try sh fallback
213
- }
214
-
215
- if (shell === 'sh') {
216
- throw new ToolError(
217
- `Shell "sh" is not available in Docker image "${image}". The image may be too minimal for sandbox use.`,
218
- 'bash',
219
- );
220
- }
221
-
222
- try {
223
- execFileSync('docker', ['run', '--rm', image, 'sh', '-c', 'true'], {
224
- stdio: 'ignore',
225
- timeout: 15000,
226
- });
227
- log.warn(`Shell "${shell}" not found in image "${image}", falling back to "sh"`);
228
- shellResolvedCache.set(cacheKey, 'sh');
229
- return 'sh';
230
- } catch {
231
- throw new ToolError(
232
- `Neither "${shell}" nor "sh" is available in Docker image "${image}". ` +
233
- 'Choose a different image or set sandbox.docker.shell to a shell that exists in the image.',
234
- 'bash',
235
- );
236
- }
237
- }
238
-
239
- /**
240
- * Validate that a path is safe to use in Docker mount arguments.
241
- * Rejects paths containing null bytes, newlines, or carriage returns which
242
- * could cause argument injection or parsing issues.
243
- */
244
- function validatePathSafety(path: string, label: string): void {
245
- if (UNSAFE_PATH_CHARS.test(path)) {
246
- throw new ToolError(
247
- `${label} contains characters that are unsafe for Docker mount arguments. ` +
248
- 'Refusing to execute. Remove null bytes, newlines, carriage returns, or commas from the path.',
249
- 'bash',
250
- );
251
- }
252
- }
253
-
254
- /**
255
- * Docker sandbox backend that wraps commands in ephemeral containers.
256
- *
257
- * Each invocation produces a single `docker run --rm` command — no long-lived
258
- * container state. The sandbox filesystem root is bind-mounted to /workspace
259
- * and the host UID:GID is forwarded to prevent permission drift.
260
- *
261
- * On first use, runs preflight checks (CLI, daemon, image, mount probe) and
262
- * fails closed with actionable error messages if any check fails.
263
- */
264
- export class DockerBackend implements SandboxBackend {
265
- private readonly sandboxRoot: string;
266
- private readonly config: Required<DockerConfig>;
267
- private readonly configHash: string;
268
- private readonly uid: number;
269
- private readonly gid: number;
270
-
271
- constructor(
272
- sandboxRoot: string,
273
- config?: Partial<Required<DockerConfig>>,
274
- uid?: number,
275
- gid?: number,
276
- ) {
277
- // Resolve to an absolute path first, then follow symlinks.
278
- // This prevents path traversal via ../.. or symlink tricks.
279
- const resolved = resolve(sandboxRoot);
280
- this.sandboxRoot = realpathSync(resolved);
281
- validatePathSafety(this.sandboxRoot, 'Sandbox root');
282
- this.config = { ...DEFAULTS, ...config };
283
- this.configHash = JSON.stringify(this.config);
284
- if (uid != null) {
285
- this.uid = uid;
286
- } else if (process.getuid) {
287
- this.uid = process.getuid();
288
- } else {
289
- throw new ToolError(
290
- 'Docker sandbox requires POSIX UID/GID APIs (process.getuid/getgid) which are not available on this platform.',
291
- 'bash',
292
- );
293
- }
294
- this.gid = gid ?? (process.getgid ? process.getgid() : this.uid);
295
- }
296
-
297
- /**
298
- * Run preflight checks in dependency order. Each check is cached
299
- * on success; failures re-check on every call. Returns the resolved
300
- * shell (may differ from config if the configured shell is missing).
301
- */
302
- preflight(): string {
303
- checkDockerCli();
304
- checkDockerDaemon();
305
- checkImageAvailable(this.config.image);
306
- checkMountProbe(this.sandboxRoot, this.config.image);
307
- return resolveShell(this.config.image, this.config.shell, this.configHash);
308
- }
309
-
310
- wrap(command: string, workingDir: string, options?: WrapOptions): SandboxResult {
311
- // Preflight: fail closed if Docker is not usable.
312
- const shell = this.preflight();
313
-
314
- // Resolve + follow symlinks for the working directory.
315
- const resolved = resolve(workingDir);
316
- const realWorkDir = realpathSync(resolved);
317
- const realRoot = this.sandboxRoot;
318
-
319
- // Validate path safety for mount/workdir args.
320
- validatePathSafety(realWorkDir, 'Working directory');
321
-
322
- // Fail closed: working dir must be inside sandbox root.
323
- if (!realWorkDir.startsWith(realRoot + '/') && realWorkDir !== realRoot) {
324
- log.error(
325
- 'Working directory is outside sandbox root — refusing to execute',
326
- );
327
- throw new ToolError(
328
- 'Working directory is outside the sandbox root. Refusing to execute.',
329
- 'bash',
330
- );
331
- }
332
-
333
- // Map host working dir to container path under /workspace.
334
- const relPath = relative(realRoot, realWorkDir);
335
- const containerWorkDir =
336
- relPath === '' ? '/workspace' : posix.join('/workspace', relPath);
337
-
338
- const { image, cpus, memoryMb, pidsLimit, network } = this.config;
339
-
340
- // Per-invocation network override: proxied mode needs bridge networking
341
- // so the container can reach the proxy on the host. Default ('off' or
342
- // undefined) preserves the config-level network setting.
343
- const effectiveNetwork =
344
- options?.networkMode === 'proxied' ? 'bridge' : network;
345
-
346
- // Every flag is a separate argv segment — no shell interpolation occurs.
347
- const args: string[] = [
348
- 'run',
349
- '--rm',
350
- `--network=${effectiveNetwork}`,
351
- // When proxied, map host.docker.internal to the host machine so the
352
- // container can reach the proxy daemon listening on the host loopback.
353
- ...(options?.networkMode === 'proxied'
354
- ? ['--add-host=host.docker.internal:host-gateway']
355
- : []),
356
- `--cpus=${cpus}`,
357
- `--memory=${memoryMb}m`,
358
- `--pids-limit=${pidsLimit}`,
359
- '--cap-drop=ALL',
360
- '--security-opt=no-new-privileges',
361
- // Read-only container root prevents writes outside explicit mounts.
362
- '--read-only',
363
- // Writable tmpfs for /tmp — required for shell behavior, temp files, etc.
364
- '--tmpfs', '/tmp:rw,nosuid,nodev,noexec',
365
- '--mount',
366
- `type=bind,src=${realRoot},dst=/workspace`,
367
- '--workdir',
368
- containerWorkDir,
369
- '--user',
370
- `${this.uid}:${this.gid}`,
371
- image,
372
- shell,
373
- '-c',
374
- command,
375
- ];
376
-
377
- return { command: 'docker', args, sandboxed: true };
378
- }
379
- }