@sysid/sandbox-runtime-improved 0.0.42-sysid.1

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 (83) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +676 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +166 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/index.d.ts +11 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +9 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/sandbox/generate-seccomp-filter.d.ts +71 -0
  12. package/dist/sandbox/generate-seccomp-filter.d.ts.map +1 -0
  13. package/dist/sandbox/generate-seccomp-filter.js +263 -0
  14. package/dist/sandbox/generate-seccomp-filter.js.map +1 -0
  15. package/dist/sandbox/http-proxy.d.ts +19 -0
  16. package/dist/sandbox/http-proxy.d.ts.map +1 -0
  17. package/dist/sandbox/http-proxy.js +295 -0
  18. package/dist/sandbox/http-proxy.js.map +1 -0
  19. package/dist/sandbox/linux-sandbox-utils.d.ts +158 -0
  20. package/dist/sandbox/linux-sandbox-utils.d.ts.map +1 -0
  21. package/dist/sandbox/linux-sandbox-utils.js +875 -0
  22. package/dist/sandbox/linux-sandbox-utils.js.map +1 -0
  23. package/dist/sandbox/macos-sandbox-utils.d.ts +41 -0
  24. package/dist/sandbox/macos-sandbox-utils.d.ts.map +1 -0
  25. package/dist/sandbox/macos-sandbox-utils.js +672 -0
  26. package/dist/sandbox/macos-sandbox-utils.js.map +1 -0
  27. package/dist/sandbox/sandbox-config.d.ts +307 -0
  28. package/dist/sandbox/sandbox-config.d.ts.map +1 -0
  29. package/dist/sandbox/sandbox-config.js +195 -0
  30. package/dist/sandbox/sandbox-config.js.map +1 -0
  31. package/dist/sandbox/sandbox-manager.d.ts +42 -0
  32. package/dist/sandbox/sandbox-manager.d.ts.map +1 -0
  33. package/dist/sandbox/sandbox-manager.js +796 -0
  34. package/dist/sandbox/sandbox-manager.js.map +1 -0
  35. package/dist/sandbox/sandbox-schemas.d.ts +57 -0
  36. package/dist/sandbox/sandbox-schemas.d.ts.map +1 -0
  37. package/dist/sandbox/sandbox-schemas.js +3 -0
  38. package/dist/sandbox/sandbox-schemas.js.map +1 -0
  39. package/dist/sandbox/sandbox-utils.d.ts +116 -0
  40. package/dist/sandbox/sandbox-utils.d.ts.map +1 -0
  41. package/dist/sandbox/sandbox-utils.js +463 -0
  42. package/dist/sandbox/sandbox-utils.js.map +1 -0
  43. package/dist/sandbox/sandbox-violation-store.d.ts +19 -0
  44. package/dist/sandbox/sandbox-violation-store.d.ts.map +1 -0
  45. package/dist/sandbox/sandbox-violation-store.js +54 -0
  46. package/dist/sandbox/sandbox-violation-store.js.map +1 -0
  47. package/dist/sandbox/socks-proxy.d.ts +13 -0
  48. package/dist/sandbox/socks-proxy.d.ts.map +1 -0
  49. package/dist/sandbox/socks-proxy.js +95 -0
  50. package/dist/sandbox/socks-proxy.js.map +1 -0
  51. package/dist/utils/config-loader.d.ts +11 -0
  52. package/dist/utils/config-loader.d.ts.map +1 -0
  53. package/dist/utils/config-loader.js +60 -0
  54. package/dist/utils/config-loader.js.map +1 -0
  55. package/dist/utils/debug.d.ts +7 -0
  56. package/dist/utils/debug.d.ts.map +1 -0
  57. package/dist/utils/debug.js +25 -0
  58. package/dist/utils/debug.js.map +1 -0
  59. package/dist/utils/platform.d.ts +15 -0
  60. package/dist/utils/platform.d.ts.map +1 -0
  61. package/dist/utils/platform.js +49 -0
  62. package/dist/utils/platform.js.map +1 -0
  63. package/dist/utils/ripgrep.d.ts +22 -0
  64. package/dist/utils/ripgrep.d.ts.map +1 -0
  65. package/dist/utils/ripgrep.js +45 -0
  66. package/dist/utils/ripgrep.js.map +1 -0
  67. package/dist/utils/which.d.ts +9 -0
  68. package/dist/utils/which.d.ts.map +1 -0
  69. package/dist/utils/which.js +25 -0
  70. package/dist/utils/which.js.map +1 -0
  71. package/dist/vendor/seccomp/arm64/apply-seccomp +0 -0
  72. package/dist/vendor/seccomp/arm64/unix-block.bpf +0 -0
  73. package/dist/vendor/seccomp/x64/apply-seccomp +0 -0
  74. package/dist/vendor/seccomp/x64/unix-block.bpf +0 -0
  75. package/dist/vendor/seccomp-src/apply-seccomp.c +98 -0
  76. package/dist/vendor/seccomp-src/seccomp-unix-block.c +97 -0
  77. package/package.json +88 -0
  78. package/vendor/seccomp/arm64/apply-seccomp +0 -0
  79. package/vendor/seccomp/arm64/unix-block.bpf +0 -0
  80. package/vendor/seccomp/x64/apply-seccomp +0 -0
  81. package/vendor/seccomp/x64/unix-block.bpf +0 -0
  82. package/vendor/seccomp-src/apply-seccomp.c +98 -0
  83. package/vendor/seccomp-src/seccomp-unix-block.c +97 -0
@@ -0,0 +1,796 @@
1
+ import { createHttpProxyServer } from './http-proxy.js';
2
+ import { createSocksProxyServer } from './socks-proxy.js';
3
+ import { logForDebugging } from '../utils/debug.js';
4
+ import { whichSync } from '../utils/which.js';
5
+ import { cloneDeep } from 'lodash-es';
6
+ import { getPlatform, getWslVersion } from '../utils/platform.js';
7
+ import * as fs from 'fs';
8
+ import { wrapCommandWithSandboxLinux, initializeLinuxNetworkBridge, checkLinuxDependencies, cleanupBwrapMountPoints, } from './linux-sandbox-utils.js';
9
+ import { wrapCommandWithSandboxMacOS, startMacOSSandboxLogMonitor, } from './macos-sandbox-utils.js';
10
+ import { getDefaultWritePaths, containsGlobChars, removeTrailingGlobSuffix, expandGlobPattern, ensureSandboxTmpdir, } from './sandbox-utils.js';
11
+ import { SandboxViolationStore } from './sandbox-violation-store.js';
12
+ import { EOL } from 'node:os';
13
+ // ============================================================================
14
+ // Private Module State
15
+ // ============================================================================
16
+ let config;
17
+ let httpProxyServer;
18
+ let socksProxyServer;
19
+ let managerContext;
20
+ let initializationPromise;
21
+ let cleanupRegistered = false;
22
+ let logMonitorShutdown;
23
+ const sandboxViolationStore = new SandboxViolationStore();
24
+ // ============================================================================
25
+ // Private Helper Functions (not exported)
26
+ // ============================================================================
27
+ function registerCleanup() {
28
+ if (cleanupRegistered) {
29
+ return;
30
+ }
31
+ const cleanupHandler = () => reset().catch(e => {
32
+ logForDebugging(`Cleanup failed in registerCleanup ${e}`, {
33
+ level: 'error',
34
+ });
35
+ });
36
+ process.once('exit', cleanupHandler);
37
+ process.once('SIGINT', cleanupHandler);
38
+ process.once('SIGTERM', cleanupHandler);
39
+ cleanupRegistered = true;
40
+ }
41
+ function matchesDomainPattern(hostname, pattern) {
42
+ // Support wildcard patterns like *.example.com
43
+ // This matches any subdomain but not the base domain itself
44
+ if (pattern.startsWith('*.')) {
45
+ const baseDomain = pattern.substring(2); // Remove '*.'
46
+ return hostname.toLowerCase().endsWith('.' + baseDomain.toLowerCase());
47
+ }
48
+ // Exact match for non-wildcard patterns
49
+ return hostname.toLowerCase() === pattern.toLowerCase();
50
+ }
51
+ async function filterNetworkRequest(port, host, sandboxAskCallback) {
52
+ if (!config) {
53
+ logForDebugging('No config available, denying network request');
54
+ return false;
55
+ }
56
+ // Check denied domains first
57
+ for (const deniedDomain of config.network.deniedDomains) {
58
+ if (matchesDomainPattern(host, deniedDomain)) {
59
+ logForDebugging(`Denied by config rule: ${host}:${port}`);
60
+ return false;
61
+ }
62
+ }
63
+ // Check allowed domains
64
+ for (const allowedDomain of config.network.allowedDomains) {
65
+ if (matchesDomainPattern(host, allowedDomain)) {
66
+ logForDebugging(`Allowed by config rule: ${host}:${port}`);
67
+ return true;
68
+ }
69
+ }
70
+ // No matching rules - ask user or deny
71
+ if (!sandboxAskCallback) {
72
+ logForDebugging(`No matching config rule, denying: ${host}:${port}`);
73
+ return false;
74
+ }
75
+ logForDebugging(`No matching config rule, asking user: ${host}:${port}`);
76
+ try {
77
+ const userAllowed = await sandboxAskCallback({ host, port });
78
+ if (userAllowed) {
79
+ logForDebugging(`User allowed: ${host}:${port}`);
80
+ return true;
81
+ }
82
+ else {
83
+ logForDebugging(`User denied: ${host}:${port}`);
84
+ return false;
85
+ }
86
+ }
87
+ catch (error) {
88
+ logForDebugging(`Error in permission callback: ${error}`, {
89
+ level: 'error',
90
+ });
91
+ return false;
92
+ }
93
+ }
94
+ /**
95
+ * Get the MITM proxy socket path for a given host, if configured.
96
+ * Returns the socket path if the host matches any MITM domain pattern,
97
+ * otherwise returns undefined.
98
+ */
99
+ function getMitmSocketPath(host) {
100
+ if (!config?.network.mitmProxy) {
101
+ return undefined;
102
+ }
103
+ const { socketPath, domains } = config.network.mitmProxy;
104
+ for (const pattern of domains) {
105
+ if (matchesDomainPattern(host, pattern)) {
106
+ logForDebugging(`Host ${host} matches MITM pattern ${pattern}`);
107
+ return socketPath;
108
+ }
109
+ }
110
+ return undefined;
111
+ }
112
+ async function startHttpProxyServer(sandboxAskCallback) {
113
+ httpProxyServer = createHttpProxyServer({
114
+ filter: (port, host) => filterNetworkRequest(port, host, sandboxAskCallback),
115
+ getMitmSocketPath,
116
+ upstreamHttpProxy: config?.network.upstreamHttpProxy
117
+ ? new URL(config.network.upstreamHttpProxy)
118
+ : undefined,
119
+ });
120
+ return new Promise((resolve, reject) => {
121
+ if (!httpProxyServer) {
122
+ reject(new Error('HTTP proxy server undefined before listen'));
123
+ return;
124
+ }
125
+ const server = httpProxyServer;
126
+ server.once('error', reject);
127
+ server.once('listening', () => {
128
+ const address = server.address();
129
+ if (address && typeof address === 'object') {
130
+ server.unref();
131
+ logForDebugging(`HTTP proxy listening on localhost:${address.port}`);
132
+ resolve(address.port);
133
+ }
134
+ else {
135
+ reject(new Error('Failed to get proxy server address'));
136
+ }
137
+ });
138
+ server.listen(0, '127.0.0.1');
139
+ });
140
+ }
141
+ async function startSocksProxyServer(sandboxAskCallback) {
142
+ socksProxyServer = createSocksProxyServer({
143
+ filter: (port, host) => filterNetworkRequest(port, host, sandboxAskCallback),
144
+ });
145
+ return new Promise((resolve, reject) => {
146
+ if (!socksProxyServer) {
147
+ // This is mostly just for the typechecker
148
+ reject(new Error('SOCKS proxy server undefined before listen'));
149
+ return;
150
+ }
151
+ socksProxyServer
152
+ .listen(0, '127.0.0.1')
153
+ .then((port) => {
154
+ socksProxyServer?.unref();
155
+ resolve(port);
156
+ })
157
+ .catch(reject);
158
+ });
159
+ }
160
+ // ============================================================================
161
+ // Public Module Functions (will be exported via namespace)
162
+ // ============================================================================
163
+ async function initialize(runtimeConfig, sandboxAskCallback, enableLogMonitor = false) {
164
+ // Return if already initializing
165
+ if (initializationPromise) {
166
+ await initializationPromise;
167
+ return;
168
+ }
169
+ // Store config for use by other functions
170
+ config = runtimeConfig;
171
+ // Ensure the sandbox TMPDIR directory exists (default: /tmp/claude).
172
+ // mktemp fails silently when TMPDIR is missing, producing an empty string;
173
+ // shell sessions that redirect to that empty path then hang on stdin.
174
+ ensureSandboxTmpdir();
175
+ // Check dependencies
176
+ const deps = checkDependencies();
177
+ if (deps.errors.length > 0) {
178
+ throw new Error(`Sandbox dependencies not available: ${deps.errors.join(', ')}`);
179
+ }
180
+ // Start log monitor for macOS if enabled
181
+ if (enableLogMonitor && getPlatform() === 'macos') {
182
+ logMonitorShutdown = startMacOSSandboxLogMonitor(sandboxViolationStore.addViolation.bind(sandboxViolationStore), config.ignoreViolations);
183
+ logForDebugging('Started macOS sandbox log monitor');
184
+ }
185
+ // Register cleanup handlers first time
186
+ registerCleanup();
187
+ // Initialize network infrastructure
188
+ initializationPromise = (async () => {
189
+ try {
190
+ // Conditionally start proxy servers based on config
191
+ let httpProxyPort;
192
+ if (config.network.httpProxyPort !== undefined) {
193
+ // Use external HTTP proxy (don't start a server)
194
+ httpProxyPort = config.network.httpProxyPort;
195
+ logForDebugging(`Using external HTTP proxy on port ${httpProxyPort}`);
196
+ }
197
+ else {
198
+ // Start local HTTP proxy
199
+ httpProxyPort = await startHttpProxyServer(sandboxAskCallback);
200
+ }
201
+ let socksProxyPort;
202
+ if (config.network.socksProxyPort !== undefined) {
203
+ // Use external SOCKS proxy (don't start a server)
204
+ socksProxyPort = config.network.socksProxyPort;
205
+ logForDebugging(`Using external SOCKS proxy on port ${socksProxyPort}`);
206
+ }
207
+ else {
208
+ // Start local SOCKS proxy
209
+ socksProxyPort = await startSocksProxyServer(sandboxAskCallback);
210
+ }
211
+ // Initialize platform-specific infrastructure
212
+ let linuxBridge;
213
+ if (getPlatform() === 'linux') {
214
+ linuxBridge = await initializeLinuxNetworkBridge(httpProxyPort, socksProxyPort);
215
+ }
216
+ const context = {
217
+ httpProxyPort,
218
+ socksProxyPort,
219
+ linuxBridge,
220
+ };
221
+ managerContext = context;
222
+ logForDebugging('Network infrastructure initialized');
223
+ return context;
224
+ }
225
+ catch (error) {
226
+ // Clear state on error so initialization can be retried
227
+ initializationPromise = undefined;
228
+ managerContext = undefined;
229
+ reset().catch(e => {
230
+ logForDebugging(`Cleanup failed in initializationPromise ${e}`, {
231
+ level: 'error',
232
+ });
233
+ });
234
+ throw error;
235
+ }
236
+ })();
237
+ await initializationPromise;
238
+ }
239
+ function isSupportedPlatform() {
240
+ const platform = getPlatform();
241
+ if (platform === 'linux') {
242
+ // WSL1 doesn't support bubblewrap
243
+ return getWslVersion() !== '1';
244
+ }
245
+ return platform === 'macos';
246
+ }
247
+ function isSandboxingEnabled() {
248
+ // Sandboxing is enabled if config has been set (via initialize())
249
+ return config !== undefined;
250
+ }
251
+ /**
252
+ * Check sandbox dependencies for the current platform
253
+ * @param ripgrepConfig - Ripgrep command to check. If not provided, uses config from initialization or defaults to 'rg'
254
+ * @returns { warnings, errors } - errors mean sandbox cannot run, warnings mean degraded functionality
255
+ */
256
+ function checkDependencies(ripgrepConfig) {
257
+ if (!isSupportedPlatform()) {
258
+ return { errors: ['Unsupported platform'], warnings: [] };
259
+ }
260
+ const errors = [];
261
+ const warnings = [];
262
+ // Check ripgrep - use provided config, then initialized config, then default 'rg'
263
+ const rgToCheck = ripgrepConfig ?? config?.ripgrep ?? { command: 'rg' };
264
+ if (whichSync(rgToCheck.command) === null) {
265
+ errors.push(`ripgrep (${rgToCheck.command}) not found`);
266
+ }
267
+ const platform = getPlatform();
268
+ if (platform === 'linux') {
269
+ const linuxDeps = checkLinuxDependencies(config?.seccomp);
270
+ errors.push(...linuxDeps.errors);
271
+ warnings.push(...linuxDeps.warnings);
272
+ }
273
+ return { errors, warnings };
274
+ }
275
+ function getFsReadConfig() {
276
+ if (!config) {
277
+ return { denyOnly: [], allowWithinDeny: [] };
278
+ }
279
+ const denyPaths = [];
280
+ for (const p of config.filesystem.denyRead) {
281
+ const stripped = removeTrailingGlobSuffix(p);
282
+ if (getPlatform() === 'linux' && containsGlobChars(stripped)) {
283
+ // Expand glob to concrete paths on Linux (bubblewrap doesn't support globs)
284
+ const expanded = expandGlobPattern(p);
285
+ logForDebugging(`[Sandbox] Expanded glob pattern "${p}" to ${expanded.length} paths on Linux`);
286
+ denyPaths.push(...expanded);
287
+ }
288
+ else {
289
+ denyPaths.push(stripped);
290
+ }
291
+ }
292
+ // Process allowRead paths (re-allow within denied regions)
293
+ const allowPaths = [];
294
+ for (const p of config.filesystem.allowRead ?? []) {
295
+ const stripped = removeTrailingGlobSuffix(p);
296
+ if (getPlatform() === 'linux' && containsGlobChars(stripped)) {
297
+ const expanded = expandGlobPattern(p);
298
+ logForDebugging(`[Sandbox] Expanded allowRead glob pattern "${p}" to ${expanded.length} paths on Linux`);
299
+ allowPaths.push(...expanded);
300
+ }
301
+ else {
302
+ allowPaths.push(stripped);
303
+ }
304
+ }
305
+ return {
306
+ denyOnly: denyPaths,
307
+ allowWithinDeny: allowPaths,
308
+ };
309
+ }
310
+ function getFsWriteConfig() {
311
+ if (!config) {
312
+ return { allowOnly: getDefaultWritePaths(), denyWithinAllow: [] };
313
+ }
314
+ // Filter out glob patterns on Linux/WSL for allowWrite (bubblewrap doesn't support globs)
315
+ const allowPaths = config.filesystem.allowWrite
316
+ .map(path => removeTrailingGlobSuffix(path))
317
+ .filter(path => {
318
+ if (getPlatform() === 'linux' && containsGlobChars(path)) {
319
+ logForDebugging(`Skipping glob pattern on Linux/WSL: ${path}`);
320
+ return false;
321
+ }
322
+ return true;
323
+ });
324
+ // Filter out glob patterns on Linux/WSL for denyWrite (bubblewrap doesn't support globs)
325
+ const denyPaths = config.filesystem.denyWrite
326
+ .map(path => removeTrailingGlobSuffix(path))
327
+ .filter(path => {
328
+ if (getPlatform() === 'linux' && containsGlobChars(path)) {
329
+ logForDebugging(`Skipping glob pattern on Linux/WSL: ${path}`);
330
+ return false;
331
+ }
332
+ return true;
333
+ });
334
+ // Build allowOnly list: default paths + configured allow paths
335
+ const allowOnly = [...getDefaultWritePaths(), ...allowPaths];
336
+ return {
337
+ allowOnly,
338
+ denyWithinAllow: denyPaths,
339
+ };
340
+ }
341
+ function getNetworkRestrictionConfig() {
342
+ if (!config) {
343
+ return {};
344
+ }
345
+ const allowedHosts = config.network.allowedDomains;
346
+ const deniedHosts = config.network.deniedDomains;
347
+ return {
348
+ ...(allowedHosts.length > 0 && { allowedHosts }),
349
+ ...(deniedHosts.length > 0 && { deniedHosts }),
350
+ };
351
+ }
352
+ function getAllowUnixSockets() {
353
+ return config?.network?.allowUnixSockets;
354
+ }
355
+ function getAllowAllUnixSockets() {
356
+ return config?.network?.allowAllUnixSockets;
357
+ }
358
+ function getAllowLocalBinding() {
359
+ return config?.network?.allowLocalBinding;
360
+ }
361
+ function getIgnoreViolations() {
362
+ return config?.ignoreViolations;
363
+ }
364
+ function getEnableWeakerNestedSandbox() {
365
+ return config?.enableWeakerNestedSandbox;
366
+ }
367
+ function getEnableWeakerNetworkIsolation() {
368
+ return config?.enableWeakerNetworkIsolation;
369
+ }
370
+ function getRipgrepConfig() {
371
+ return config?.ripgrep ?? { command: 'rg' };
372
+ }
373
+ function getMandatoryDenySearchDepth() {
374
+ return config?.mandatoryDenySearchDepth ?? 3;
375
+ }
376
+ function getAllowGitConfig() {
377
+ return config?.filesystem?.allowGitConfig ?? false;
378
+ }
379
+ function getSeccompConfig() {
380
+ return config?.seccomp;
381
+ }
382
+ function getProxyPort() {
383
+ return managerContext?.httpProxyPort;
384
+ }
385
+ function getSocksProxyPort() {
386
+ return managerContext?.socksProxyPort;
387
+ }
388
+ function getLinuxHttpSocketPath() {
389
+ return managerContext?.linuxBridge?.httpSocketPath;
390
+ }
391
+ function getLinuxSocksSocketPath() {
392
+ return managerContext?.linuxBridge?.socksSocketPath;
393
+ }
394
+ /**
395
+ * Wait for network initialization to complete if already in progress
396
+ * Returns true if initialized successfully, false otherwise
397
+ */
398
+ async function waitForNetworkInitialization() {
399
+ if (!config) {
400
+ return false;
401
+ }
402
+ if (initializationPromise) {
403
+ try {
404
+ await initializationPromise;
405
+ return true;
406
+ }
407
+ catch {
408
+ return false;
409
+ }
410
+ }
411
+ return managerContext !== undefined;
412
+ }
413
+ async function wrapWithSandbox(command, binShell, customConfig, abortSignal) {
414
+ const platform = getPlatform();
415
+ // Get configs - use custom if provided, otherwise fall back to main config
416
+ // If neither exists, defaults to empty arrays (most restrictive)
417
+ // Always include default system write paths (like /dev/null, /tmp/claude)
418
+ //
419
+ // Strip trailing /** and filter remaining globs on Linux (bwrap needs
420
+ // real paths, not globs; macOS subpath matching is also recursive so
421
+ // stripping is harmless there).
422
+ const stripWriteGlobs = (paths) => paths
423
+ .map(p => removeTrailingGlobSuffix(p))
424
+ .filter(p => {
425
+ if (getPlatform() === 'linux' && containsGlobChars(p)) {
426
+ logForDebugging(`[Sandbox] Skipping glob write pattern on Linux: ${p}`);
427
+ return false;
428
+ }
429
+ return true;
430
+ });
431
+ const userAllowWrite = stripWriteGlobs(customConfig?.filesystem?.allowWrite ?? config?.filesystem.allowWrite ?? []);
432
+ const writeConfig = {
433
+ allowOnly: [...getDefaultWritePaths(), ...userAllowWrite],
434
+ denyWithinAllow: stripWriteGlobs(customConfig?.filesystem?.denyWrite ?? config?.filesystem.denyWrite ?? []),
435
+ };
436
+ const rawDenyRead = customConfig?.filesystem?.denyRead ?? config?.filesystem.denyRead ?? [];
437
+ const expandedDenyRead = [];
438
+ for (const p of rawDenyRead) {
439
+ const stripped = removeTrailingGlobSuffix(p);
440
+ if (getPlatform() === 'linux' && containsGlobChars(stripped)) {
441
+ expandedDenyRead.push(...expandGlobPattern(p));
442
+ }
443
+ else {
444
+ expandedDenyRead.push(stripped);
445
+ }
446
+ }
447
+ const rawAllowRead = customConfig?.filesystem?.allowRead ?? config?.filesystem.allowRead ?? [];
448
+ const expandedAllowRead = [];
449
+ for (const p of rawAllowRead) {
450
+ const stripped = removeTrailingGlobSuffix(p);
451
+ if (getPlatform() === 'linux' && containsGlobChars(stripped)) {
452
+ expandedAllowRead.push(...expandGlobPattern(p));
453
+ }
454
+ else {
455
+ expandedAllowRead.push(stripped);
456
+ }
457
+ }
458
+ const readConfig = {
459
+ denyOnly: expandedDenyRead,
460
+ allowWithinDeny: expandedAllowRead,
461
+ };
462
+ // Check if network config is specified - this determines if we need network restrictions
463
+ // Network restriction is needed when:
464
+ // 1. customConfig has network.allowedDomains defined (even if empty array = block all)
465
+ // 2. OR config has network.allowedDomains defined (even if empty array = block all)
466
+ // An empty allowedDomains array means "no domains allowed" = block all network access
467
+ const hasNetworkConfig = customConfig?.network?.allowedDomains !== undefined ||
468
+ config?.network?.allowedDomains !== undefined;
469
+ // Network RESTRICTION is needed whenever network config is specified
470
+ // This includes empty allowedDomains which means "block all network"
471
+ const needsNetworkRestriction = hasNetworkConfig;
472
+ // Network PROXY is needed whenever network config is specified
473
+ // Even with empty allowedDomains, we route through proxy so that:
474
+ // 1. updateConfig() can enable network access for already-running processes
475
+ // 2. The proxy blocks all requests when allowlist is empty
476
+ const needsNetworkProxy = hasNetworkConfig;
477
+ // Wait for network initialization only if proxy is actually needed
478
+ if (needsNetworkProxy) {
479
+ await waitForNetworkInitialization();
480
+ }
481
+ // Check custom config to allow pseudo-terminal (can be applied dynamically)
482
+ const allowPty = customConfig?.allowPty ?? config?.allowPty;
483
+ // Check custom config to allow browser process operations (Chrome/Chromium)
484
+ const allowBrowserProcess = customConfig?.allowBrowserProcess ?? config?.allowBrowserProcess;
485
+ switch (platform) {
486
+ case 'macos':
487
+ // macOS sandbox profile supports glob patterns directly, no ripgrep needed
488
+ return wrapCommandWithSandboxMacOS({
489
+ command,
490
+ needsNetworkRestriction,
491
+ // Only pass proxy ports if proxy is running (when there are domains to filter)
492
+ httpProxyPort: needsNetworkProxy ? getProxyPort() : undefined,
493
+ socksProxyPort: needsNetworkProxy ? getSocksProxyPort() : undefined,
494
+ readConfig,
495
+ writeConfig,
496
+ allowUnixSockets: getAllowUnixSockets(),
497
+ allowAllUnixSockets: getAllowAllUnixSockets(),
498
+ allowLocalBinding: getAllowLocalBinding(),
499
+ ignoreViolations: getIgnoreViolations(),
500
+ allowPty,
501
+ allowBrowserProcess,
502
+ allowGitConfig: getAllowGitConfig(),
503
+ enableWeakerNetworkIsolation: getEnableWeakerNetworkIsolation(),
504
+ binShell,
505
+ });
506
+ case 'linux':
507
+ return wrapCommandWithSandboxLinux({
508
+ command,
509
+ needsNetworkRestriction,
510
+ // Only pass socket paths if proxy is running (when there are domains to filter)
511
+ httpSocketPath: needsNetworkProxy
512
+ ? getLinuxHttpSocketPath()
513
+ : undefined,
514
+ socksSocketPath: needsNetworkProxy
515
+ ? getLinuxSocksSocketPath()
516
+ : undefined,
517
+ httpProxyPort: needsNetworkProxy
518
+ ? managerContext?.httpProxyPort
519
+ : undefined,
520
+ socksProxyPort: needsNetworkProxy
521
+ ? managerContext?.socksProxyPort
522
+ : undefined,
523
+ readConfig,
524
+ writeConfig,
525
+ enableWeakerNestedSandbox: getEnableWeakerNestedSandbox(),
526
+ allowAllUnixSockets: getAllowAllUnixSockets(),
527
+ binShell,
528
+ ripgrepConfig: getRipgrepConfig(),
529
+ mandatoryDenySearchDepth: getMandatoryDenySearchDepth(),
530
+ allowGitConfig: getAllowGitConfig(),
531
+ seccompConfig: getSeccompConfig(),
532
+ abortSignal,
533
+ });
534
+ default:
535
+ // Unsupported platform - this should not happen since isSandboxingEnabled() checks platform support
536
+ throw new Error(`Sandbox configuration is not supported on platform: ${platform}`);
537
+ }
538
+ }
539
+ /**
540
+ * Get the current sandbox configuration
541
+ * @returns The current configuration, or undefined if not initialized
542
+ */
543
+ function getConfig() {
544
+ return config;
545
+ }
546
+ /**
547
+ * Update the sandbox configuration
548
+ * @param newConfig - The new configuration to use
549
+ */
550
+ function updateConfig(newConfig) {
551
+ // Deep clone the config to avoid mutations
552
+ config = cloneDeep(newConfig);
553
+ logForDebugging('Sandbox configuration updated');
554
+ }
555
+ /**
556
+ * Lightweight cleanup to call after each sandboxed command completes.
557
+ *
558
+ * On Linux, bwrap creates empty files on the host filesystem as mount points
559
+ * when protecting non-existent deny paths (e.g. ~/.bashrc, ~/.gitconfig).
560
+ * These persist after bwrap exits. This function removes them.
561
+ *
562
+ * Safe to call on any platform — it's a no-op on macOS.
563
+ * Also called automatically by reset() and on process exit as safety nets.
564
+ */
565
+ function cleanupAfterCommand() {
566
+ cleanupBwrapMountPoints();
567
+ }
568
+ async function reset() {
569
+ // Clean up any leftover bwrap mount points
570
+ cleanupAfterCommand();
571
+ // Stop log monitor
572
+ if (logMonitorShutdown) {
573
+ logMonitorShutdown();
574
+ logMonitorShutdown = undefined;
575
+ }
576
+ if (managerContext?.linuxBridge) {
577
+ const { httpSocketPath, socksSocketPath, httpBridgeProcess, socksBridgeProcess, } = managerContext.linuxBridge;
578
+ // Create array to wait for process exits
579
+ const exitPromises = [];
580
+ // Kill HTTP bridge and wait for it to exit
581
+ if (httpBridgeProcess.pid && !httpBridgeProcess.killed) {
582
+ try {
583
+ process.kill(httpBridgeProcess.pid, 'SIGTERM');
584
+ logForDebugging('Sent SIGTERM to HTTP bridge process');
585
+ // Wait for process to exit
586
+ exitPromises.push(new Promise(resolve => {
587
+ httpBridgeProcess.once('exit', () => {
588
+ logForDebugging('HTTP bridge process exited');
589
+ resolve();
590
+ });
591
+ // Timeout after 5 seconds
592
+ setTimeout(() => {
593
+ if (!httpBridgeProcess.killed) {
594
+ logForDebugging('HTTP bridge did not exit, forcing SIGKILL', {
595
+ level: 'warn',
596
+ });
597
+ try {
598
+ if (httpBridgeProcess.pid) {
599
+ process.kill(httpBridgeProcess.pid, 'SIGKILL');
600
+ }
601
+ }
602
+ catch {
603
+ // Process may have already exited
604
+ }
605
+ }
606
+ resolve();
607
+ }, 5000);
608
+ }));
609
+ }
610
+ catch (err) {
611
+ if (err.code !== 'ESRCH') {
612
+ logForDebugging(`Error killing HTTP bridge: ${err}`, {
613
+ level: 'error',
614
+ });
615
+ }
616
+ }
617
+ }
618
+ // Kill SOCKS bridge and wait for it to exit
619
+ if (socksBridgeProcess.pid && !socksBridgeProcess.killed) {
620
+ try {
621
+ process.kill(socksBridgeProcess.pid, 'SIGTERM');
622
+ logForDebugging('Sent SIGTERM to SOCKS bridge process');
623
+ // Wait for process to exit
624
+ exitPromises.push(new Promise(resolve => {
625
+ socksBridgeProcess.once('exit', () => {
626
+ logForDebugging('SOCKS bridge process exited');
627
+ resolve();
628
+ });
629
+ // Timeout after 5 seconds
630
+ setTimeout(() => {
631
+ if (!socksBridgeProcess.killed) {
632
+ logForDebugging('SOCKS bridge did not exit, forcing SIGKILL', {
633
+ level: 'warn',
634
+ });
635
+ try {
636
+ if (socksBridgeProcess.pid) {
637
+ process.kill(socksBridgeProcess.pid, 'SIGKILL');
638
+ }
639
+ }
640
+ catch {
641
+ // Process may have already exited
642
+ }
643
+ }
644
+ resolve();
645
+ }, 5000);
646
+ }));
647
+ }
648
+ catch (err) {
649
+ if (err.code !== 'ESRCH') {
650
+ logForDebugging(`Error killing SOCKS bridge: ${err}`, {
651
+ level: 'error',
652
+ });
653
+ }
654
+ }
655
+ }
656
+ // Wait for both processes to exit
657
+ await Promise.all(exitPromises);
658
+ // Clean up sockets
659
+ if (httpSocketPath) {
660
+ try {
661
+ fs.rmSync(httpSocketPath, { force: true });
662
+ logForDebugging('Cleaned up HTTP socket');
663
+ }
664
+ catch (err) {
665
+ logForDebugging(`HTTP socket cleanup error: ${err}`, {
666
+ level: 'error',
667
+ });
668
+ }
669
+ }
670
+ if (socksSocketPath) {
671
+ try {
672
+ fs.rmSync(socksSocketPath, { force: true });
673
+ logForDebugging('Cleaned up SOCKS socket');
674
+ }
675
+ catch (err) {
676
+ logForDebugging(`SOCKS socket cleanup error: ${err}`, {
677
+ level: 'error',
678
+ });
679
+ }
680
+ }
681
+ }
682
+ // Close servers in parallel (only if they exist, i.e., were started by us)
683
+ const closePromises = [];
684
+ if (httpProxyServer) {
685
+ const server = httpProxyServer; // Capture reference to avoid TypeScript error
686
+ const httpClose = new Promise(resolve => {
687
+ server.close(error => {
688
+ if (error && error.message !== 'Server is not running.') {
689
+ logForDebugging(`Error closing HTTP proxy server: ${error.message}`, {
690
+ level: 'error',
691
+ });
692
+ }
693
+ resolve();
694
+ });
695
+ });
696
+ closePromises.push(httpClose);
697
+ }
698
+ if (socksProxyServer) {
699
+ const socksClose = socksProxyServer.close().catch((error) => {
700
+ logForDebugging(`Error closing SOCKS proxy server: ${error.message}`, {
701
+ level: 'error',
702
+ });
703
+ });
704
+ closePromises.push(socksClose);
705
+ }
706
+ // Wait for all servers to close
707
+ await Promise.all(closePromises);
708
+ // Clear references
709
+ httpProxyServer = undefined;
710
+ socksProxyServer = undefined;
711
+ managerContext = undefined;
712
+ initializationPromise = undefined;
713
+ }
714
+ function getSandboxViolationStore() {
715
+ return sandboxViolationStore;
716
+ }
717
+ function annotateStderrWithSandboxFailures(command, stderr) {
718
+ if (!config) {
719
+ return stderr;
720
+ }
721
+ const violations = sandboxViolationStore.getViolationsForCommand(command);
722
+ if (violations.length === 0) {
723
+ return stderr;
724
+ }
725
+ let annotated = stderr;
726
+ annotated += EOL + '<sandbox_violations>' + EOL;
727
+ for (const violation of violations) {
728
+ annotated += violation.line + EOL;
729
+ }
730
+ annotated += '</sandbox_violations>';
731
+ return annotated;
732
+ }
733
+ /**
734
+ * Returns glob patterns from Edit/Read permission rules that are not
735
+ * fully supported on Linux. Returns empty array on macOS or when
736
+ * sandboxing is disabled.
737
+ *
738
+ * Patterns ending with /** are excluded since they work as subpaths.
739
+ */
740
+ function getLinuxGlobPatternWarnings() {
741
+ // Only warn on Linux/WSL (bubblewrap doesn't support globs)
742
+ // macOS supports glob patterns via regex conversion
743
+ if (getPlatform() !== 'linux' || !config) {
744
+ return [];
745
+ }
746
+ const globPatterns = [];
747
+ // Check filesystem paths for glob patterns
748
+ // Note: denyRead is excluded because globs are now expanded to concrete paths on Linux
749
+ const allPaths = [
750
+ ...config.filesystem.allowWrite,
751
+ ...config.filesystem.denyWrite,
752
+ ];
753
+ for (const path of allPaths) {
754
+ // Strip trailing /** since that's just a subpath (directory and everything under it)
755
+ const pathWithoutTrailingStar = removeTrailingGlobSuffix(path);
756
+ // Only warn if there are still glob characters after removing trailing /**
757
+ if (containsGlobChars(pathWithoutTrailingStar)) {
758
+ globPatterns.push(path);
759
+ }
760
+ }
761
+ return globPatterns;
762
+ }
763
+ // ============================================================================
764
+ // Export as Namespace with Interface
765
+ // ============================================================================
766
+ /**
767
+ * Global sandbox manager that handles both network and filesystem restrictions
768
+ * for this session. This runs outside of the sandbox, on the host machine.
769
+ */
770
+ export const SandboxManager = {
771
+ initialize,
772
+ isSupportedPlatform,
773
+ isSandboxingEnabled,
774
+ checkDependencies,
775
+ getFsReadConfig,
776
+ getFsWriteConfig,
777
+ getNetworkRestrictionConfig,
778
+ getAllowUnixSockets,
779
+ getAllowLocalBinding,
780
+ getIgnoreViolations,
781
+ getEnableWeakerNestedSandbox,
782
+ getProxyPort,
783
+ getSocksProxyPort,
784
+ getLinuxHttpSocketPath,
785
+ getLinuxSocksSocketPath,
786
+ waitForNetworkInitialization,
787
+ wrapWithSandbox,
788
+ cleanupAfterCommand,
789
+ reset,
790
+ getSandboxViolationStore,
791
+ annotateStderrWithSandboxFailures,
792
+ getLinuxGlobPatternWarnings,
793
+ getConfig,
794
+ updateConfig,
795
+ };
796
+ //# sourceMappingURL=sandbox-manager.js.map