neuro-cli 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +510 -0
  3. package/dist/advisor/advisor.d.ts +50 -0
  4. package/dist/advisor/advisor.js +178 -0
  5. package/dist/agents/base.d.ts +62 -0
  6. package/dist/agents/base.js +215 -0
  7. package/dist/agents/orchestrator.d.ts +46 -0
  8. package/dist/agents/orchestrator.js +192 -0
  9. package/dist/agents/team.d.ts +51 -0
  10. package/dist/agents/team.js +210 -0
  11. package/dist/api/models.d.ts +23 -0
  12. package/dist/api/models.js +514 -0
  13. package/dist/api/ollama.d.ts +153 -0
  14. package/dist/api/ollama.js +751 -0
  15. package/dist/api/openrouter.d.ts +55 -0
  16. package/dist/api/openrouter.js +223 -0
  17. package/dist/commands/commands.d.ts +43 -0
  18. package/dist/commands/commands.js +308 -0
  19. package/dist/config/config.d.ts +8 -0
  20. package/dist/config/config.js +311 -0
  21. package/dist/context/compaction.d.ts +54 -0
  22. package/dist/context/compaction.js +251 -0
  23. package/dist/context/custom-agents.d.ts +107 -0
  24. package/dist/context/custom-agents.js +397 -0
  25. package/dist/context/custom-tools.d.ts +120 -0
  26. package/dist/context/custom-tools.js +564 -0
  27. package/dist/context/git-checkpoint.d.ts +52 -0
  28. package/dist/context/git-checkpoint.js +240 -0
  29. package/dist/context/neuro-md.d.ts +48 -0
  30. package/dist/context/neuro-md.js +202 -0
  31. package/dist/context/neuroignore.d.ts +102 -0
  32. package/dist/context/neuroignore.js +441 -0
  33. package/dist/context/repo-map.d.ts +38 -0
  34. package/dist/context/repo-map.js +220 -0
  35. package/dist/context/skill-standard.d.ts +262 -0
  36. package/dist/context/skill-standard.js +1156 -0
  37. package/dist/context/skill-system.d.ts +75 -0
  38. package/dist/context/skill-system.js +578 -0
  39. package/dist/context/tree-sitter.d.ts +99 -0
  40. package/dist/context/tree-sitter.js +1956 -0
  41. package/dist/core/acp.d.ts +325 -0
  42. package/dist/core/acp.js +1498 -0
  43. package/dist/core/api-server.d.ts +143 -0
  44. package/dist/core/api-server.js +550 -0
  45. package/dist/core/approval.d.ts +81 -0
  46. package/dist/core/approval.js +432 -0
  47. package/dist/core/auto-compact.d.ts +127 -0
  48. package/dist/core/auto-compact.js +436 -0
  49. package/dist/core/auto-mode.d.ts +232 -0
  50. package/dist/core/auto-mode.js +831 -0
  51. package/dist/core/background-session.d.ts +166 -0
  52. package/dist/core/background-session.js +696 -0
  53. package/dist/core/cicd.d.ts +257 -0
  54. package/dist/core/cicd.js +1443 -0
  55. package/dist/core/cloud-sync.d.ts +156 -0
  56. package/dist/core/cloud-sync.js +582 -0
  57. package/dist/core/code-review.d.ts +132 -0
  58. package/dist/core/code-review.js +1191 -0
  59. package/dist/core/completion.d.ts +49 -0
  60. package/dist/core/completion.js +384 -0
  61. package/dist/core/context.d.ts +38 -0
  62. package/dist/core/context.js +144 -0
  63. package/dist/core/diff-preview.d.ts +35 -0
  64. package/dist/core/diff-preview.js +173 -0
  65. package/dist/core/doom-loop.d.ts +51 -0
  66. package/dist/core/doom-loop.js +179 -0
  67. package/dist/core/engine.d.ts +183 -0
  68. package/dist/core/engine.js +942 -0
  69. package/dist/core/extended-thinking.d.ts +103 -0
  70. package/dist/core/extended-thinking.js +269 -0
  71. package/dist/core/fallback.d.ts +54 -0
  72. package/dist/core/fallback.js +104 -0
  73. package/dist/core/git-worktree.d.ts +139 -0
  74. package/dist/core/git-worktree.js +614 -0
  75. package/dist/core/headless.d.ts +30 -0
  76. package/dist/core/headless.js +101 -0
  77. package/dist/core/i18n.d.ts +83 -0
  78. package/dist/core/i18n.js +739 -0
  79. package/dist/core/linting.d.ts +129 -0
  80. package/dist/core/linting.js +699 -0
  81. package/dist/core/model-router.d.ts +109 -0
  82. package/dist/core/model-router.js +581 -0
  83. package/dist/core/multi-model.d.ts +243 -0
  84. package/dist/core/multi-model.js +1099 -0
  85. package/dist/core/multi-session.d.ts +144 -0
  86. package/dist/core/multi-session.js +442 -0
  87. package/dist/core/multimodal.d.ts +125 -0
  88. package/dist/core/multimodal.js +286 -0
  89. package/dist/core/observability.d.ts +93 -0
  90. package/dist/core/observability.js +737 -0
  91. package/dist/core/os-sandbox.d.ts +122 -0
  92. package/dist/core/os-sandbox.js +1193 -0
  93. package/dist/core/outcome-grading.d.ts +228 -0
  94. package/dist/core/outcome-grading.js +1123 -0
  95. package/dist/core/output-styles.d.ts +57 -0
  96. package/dist/core/output-styles.js +382 -0
  97. package/dist/core/parallel-agents.d.ts +183 -0
  98. package/dist/core/parallel-agents.js +563 -0
  99. package/dist/core/plugin-bundle.d.ts +236 -0
  100. package/dist/core/plugin-bundle.js +887 -0
  101. package/dist/core/plugin-sdk.d.ts +139 -0
  102. package/dist/core/plugin-sdk.js +273 -0
  103. package/dist/core/prompt-cache.d.ts +163 -0
  104. package/dist/core/prompt-cache.js +599 -0
  105. package/dist/core/sandbox.d.ts +127 -0
  106. package/dist/core/sandbox.js +369 -0
  107. package/dist/core/scheduled-tasks.d.ts +151 -0
  108. package/dist/core/scheduled-tasks.js +502 -0
  109. package/dist/core/security-scanner.d.ts +160 -0
  110. package/dist/core/security-scanner.js +1494 -0
  111. package/dist/core/session.d.ts +83 -0
  112. package/dist/core/session.js +269 -0
  113. package/dist/core/shell-completion.d.ts +51 -0
  114. package/dist/core/shell-completion.js +674 -0
  115. package/dist/core/smart-monitor.d.ts +146 -0
  116. package/dist/core/smart-monitor.js +1199 -0
  117. package/dist/core/spec-driven.d.ts +233 -0
  118. package/dist/core/spec-driven.js +1485 -0
  119. package/dist/core/spending-warnings.d.ts +123 -0
  120. package/dist/core/spending-warnings.js +456 -0
  121. package/dist/core/sub-agent.d.ts +298 -0
  122. package/dist/core/sub-agent.js +1023 -0
  123. package/dist/core/telemetry.d.ts +157 -0
  124. package/dist/core/telemetry.js +412 -0
  125. package/dist/core/terminal-ux.d.ts +134 -0
  126. package/dist/core/terminal-ux.js +649 -0
  127. package/dist/core/testing.d.ts +146 -0
  128. package/dist/core/testing.js +1200 -0
  129. package/dist/core/types.d.ts +439 -0
  130. package/dist/core/types.js +6 -0
  131. package/dist/core/undo-redo.d.ts +112 -0
  132. package/dist/core/undo-redo.js +290 -0
  133. package/dist/core/updater.d.ts +159 -0
  134. package/dist/core/updater.js +608 -0
  135. package/dist/core/vim-mode.d.ts +151 -0
  136. package/dist/core/vim-mode.js +771 -0
  137. package/dist/core/voice.d.ts +137 -0
  138. package/dist/core/voice.js +538 -0
  139. package/dist/core/web-dashboard.d.ts +109 -0
  140. package/dist/core/web-dashboard.js +484 -0
  141. package/dist/hooks/hooks.d.ts +74 -0
  142. package/dist/hooks/hooks.js +160 -0
  143. package/dist/hooks/llm-evaluator.d.ts +165 -0
  144. package/dist/hooks/llm-evaluator.js +560 -0
  145. package/dist/index.d.ts +3 -0
  146. package/dist/index.js +1186 -0
  147. package/dist/lsp/lsp-manager.d.ts +63 -0
  148. package/dist/lsp/lsp-manager.js +351 -0
  149. package/dist/mcp/client.d.ts +133 -0
  150. package/dist/mcp/client.js +684 -0
  151. package/dist/mcp/mcp-apps.d.ts +70 -0
  152. package/dist/mcp/mcp-apps.js +1007 -0
  153. package/dist/tools/bash.d.ts +5 -0
  154. package/dist/tools/bash.js +195 -0
  155. package/dist/tools/browser.d.ts +92 -0
  156. package/dist/tools/browser.js +1570 -0
  157. package/dist/tools/extended.d.ts +6 -0
  158. package/dist/tools/extended.js +191 -0
  159. package/dist/tools/file.d.ts +10 -0
  160. package/dist/tools/file.js +382 -0
  161. package/dist/tools/github.d.ts +389 -0
  162. package/dist/tools/github.js +759 -0
  163. package/dist/tools/index.d.ts +9 -0
  164. package/dist/tools/index.js +40 -0
  165. package/dist/tools/memory.d.ts +6 -0
  166. package/dist/tools/memory.js +197 -0
  167. package/dist/tools/registry.d.ts +29 -0
  168. package/dist/tools/registry.js +64 -0
  169. package/dist/tools/web.d.ts +6 -0
  170. package/dist/tools/web.js +150 -0
  171. package/dist/ui/renderer.d.ts +97 -0
  172. package/dist/ui/renderer.js +279 -0
  173. package/dist/ui/theme.d.ts +27 -0
  174. package/dist/ui/theme.js +106 -0
  175. package/package.json +83 -0
@@ -0,0 +1,1193 @@
1
+ // ============================================================
2
+ // NeuroCLI - OS-Level Sandbox with Network Isolation (GAP-29)
3
+ // Docker-based and native OS sandboxing for secure execution
4
+ // Network policy engine, command filtering, audit logging
5
+ // ============================================================
6
+ import { execSync, spawn } from 'child_process';
7
+ import { resolve, normalize } from 'path';
8
+ import { mkdirSync, writeFileSync } from 'fs';
9
+ import { platform } from 'os';
10
+ import chalk from 'chalk';
11
+ // ---- Default Configuration ----
12
+ export const DEFAULT_OS_SANDBOX_CONFIG = {
13
+ type: 'docker',
14
+ docker: {
15
+ image: 'neurocli-sandbox:latest',
16
+ mountProject: true,
17
+ mountMode: 'rw',
18
+ networkMode: 'none',
19
+ allowedDomains: [],
20
+ memory: '512m',
21
+ cpus: 1,
22
+ timeout: 120000,
23
+ },
24
+ network: {
25
+ mode: 'filtered',
26
+ allowedDomains: [
27
+ 'api.openai.com',
28
+ 'api.anthropic.com',
29
+ 'registry.npmjs.org',
30
+ 'pypi.org',
31
+ 'github.com',
32
+ ],
33
+ allowedPorts: [80, 443],
34
+ blockPrivateNetworks: true,
35
+ },
36
+ filesystem: {
37
+ readOnlyPaths: ['/usr', '/lib', '/etc', '/bin', '/sbin'],
38
+ readWritePaths: [],
39
+ deniedPaths: [
40
+ '/etc/shadow',
41
+ '/etc/passwd',
42
+ '/etc/ssh',
43
+ '/root/.ssh',
44
+ '/home/*/.ssh',
45
+ ],
46
+ allowHiddenFiles: false,
47
+ },
48
+ commands: {
49
+ blockedCommands: [
50
+ 'rm -rf /',
51
+ 'sudo',
52
+ 'su',
53
+ 'chmod 777 /',
54
+ 'chmod 777 -R /',
55
+ 'mkfs',
56
+ 'dd if=',
57
+ ':(){ :|:& };:',
58
+ 'curl | sh',
59
+ 'wget | sh',
60
+ 'passwd',
61
+ 'shutdown',
62
+ 'reboot',
63
+ 'halt',
64
+ 'init',
65
+ 'telinit',
66
+ 'systemctl',
67
+ 'service',
68
+ ],
69
+ allowedPrefixes: [],
70
+ maxArgLength: 8192,
71
+ allowPipes: true,
72
+ allowBackground: false,
73
+ },
74
+ };
75
+ // ---- Private Network Ranges for Filtering ----
76
+ const PRIVATE_NETWORK_RANGES = [
77
+ { start: ipToNumber('10.0.0.0'), end: ipToNumber('10.255.255.255') },
78
+ { start: ipToNumber('172.16.0.0'), end: ipToNumber('172.31.255.255') },
79
+ { start: ipToNumber('192.168.0.0'), end: ipToNumber('192.168.255.255') },
80
+ { start: ipToNumber('127.0.0.0'), end: ipToNumber('127.255.255.255') },
81
+ { start: ipToNumber('169.254.0.0'), end: ipToNumber('169.254.255.255') },
82
+ { start: ipToNumber('0.0.0.0'), end: ipToNumber('0.255.255.255') },
83
+ ];
84
+ function ipToNumber(ip) {
85
+ const parts = ip.split('.').map(Number);
86
+ return ((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0;
87
+ }
88
+ function isPrivateIP(ip) {
89
+ const num = ipToNumber(ip);
90
+ return PRIVATE_NETWORK_RANGES.some(range => num >= range.start && num <= range.end);
91
+ }
92
+ // ---- Docker Sandbox Image Dockerfile ----
93
+ const SANDBOX_DOCKERFILE = `FROM alpine:3.19
94
+ RUN apk add --no-cache bash coreutils curl wget git nodejs npm python3 py3-pip
95
+ RUN adduser -D -s /bin/bash sandbox
96
+ USER sandbox
97
+ WORKDIR /workspace
98
+ `;
99
+ // ---- Network Policy Engine ----
100
+ class NetworkPolicyEngine {
101
+ allowedDomains;
102
+ allowedPorts;
103
+ blockPrivateNetworks;
104
+ mode;
105
+ accessLog = [];
106
+ rateLimiter = new Map();
107
+ maxRequestsPerMinute = 60;
108
+ constructor(config) {
109
+ this.mode = config?.mode ?? 'filtered';
110
+ this.allowedDomains = new Set(config?.allowedDomains ?? []);
111
+ this.allowedPorts = new Set(config?.allowedPorts ?? [80, 443]);
112
+ this.blockPrivateNetworks = config?.blockPrivateNetworks ?? true;
113
+ }
114
+ validate(host, port, protocol = 'tcp') {
115
+ const entry = {
116
+ timestamp: Date.now(),
117
+ host,
118
+ port,
119
+ protocol,
120
+ action: 'allowed',
121
+ };
122
+ if (this.mode === 'blocked') {
123
+ entry.action = 'blocked';
124
+ entry.reason = 'All network access is blocked';
125
+ this.accessLog.push(entry);
126
+ return { allowed: false, reason: entry.reason };
127
+ }
128
+ if (this.mode === 'open') {
129
+ this.accessLog.push(entry);
130
+ return { allowed: true };
131
+ }
132
+ // mode === 'filtered'
133
+ // Check private network blocking
134
+ if (this.blockPrivateNetworks) {
135
+ const resolvedIP = this.resolveHost(host);
136
+ if (resolvedIP && isPrivateIP(resolvedIP)) {
137
+ entry.action = 'blocked';
138
+ entry.reason = `Access to private network address ${resolvedIP} is blocked`;
139
+ this.accessLog.push(entry);
140
+ return { allowed: false, reason: entry.reason };
141
+ }
142
+ }
143
+ // Check allowed domains
144
+ if (this.allowedDomains.size > 0) {
145
+ const isDomainAllowed = this.isDomainMatch(host);
146
+ if (!isDomainAllowed) {
147
+ entry.action = 'blocked';
148
+ entry.reason = `Domain ${host} is not in the allowed list`;
149
+ this.accessLog.push(entry);
150
+ return { allowed: false, reason: entry.reason };
151
+ }
152
+ }
153
+ // Check allowed ports
154
+ if (this.allowedPorts.size > 0 && !this.allowedPorts.has(port)) {
155
+ entry.action = 'blocked';
156
+ entry.reason = `Port ${port} is not in the allowed list`;
157
+ this.accessLog.push(entry);
158
+ return { allowed: false, reason: entry.reason };
159
+ }
160
+ // Rate limiting
161
+ if (!this.checkRateLimit(host)) {
162
+ entry.action = 'blocked';
163
+ entry.reason = `Rate limit exceeded for ${host}`;
164
+ this.accessLog.push(entry);
165
+ return { allowed: false, reason: entry.reason };
166
+ }
167
+ this.accessLog.push(entry);
168
+ return { allowed: true };
169
+ }
170
+ getAccessLog() {
171
+ return [...this.accessLog];
172
+ }
173
+ clearLog() {
174
+ this.accessLog = [];
175
+ }
176
+ isDomainMatch(host) {
177
+ if (this.allowedDomains.has(host))
178
+ return true;
179
+ for (const allowed of this.allowedDomains) {
180
+ if (host.endsWith('.' + allowed))
181
+ return true;
182
+ if (allowed.startsWith('*.') && host.endsWith(allowed.slice(1)))
183
+ return true;
184
+ }
185
+ return false;
186
+ }
187
+ resolveHost(host) {
188
+ if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host)) {
189
+ return host;
190
+ }
191
+ try {
192
+ const result = execSync(`nslookup ${host} 2>/dev/null`, {
193
+ encoding: 'utf-8',
194
+ timeout: 5000,
195
+ });
196
+ const match = result.match(/Address:\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/g);
197
+ if (match && match.length > 1) {
198
+ const ip = match[match.length - 1].replace('Address:\s*', '').trim();
199
+ return ip || null;
200
+ }
201
+ }
202
+ catch {
203
+ // DNS resolution failed, can't determine if private
204
+ }
205
+ return null;
206
+ }
207
+ checkRateLimit(host) {
208
+ const now = Date.now();
209
+ const windowStart = now - 60000;
210
+ const timestamps = this.rateLimiter.get(host) ?? [];
211
+ const recent = timestamps.filter(ts => ts > windowStart);
212
+ recent.push(now);
213
+ this.rateLimiter.set(host, recent);
214
+ return recent.length <= this.maxRequestsPerMinute;
215
+ }
216
+ }
217
+ // ---- Command Filter ----
218
+ class CommandFilter {
219
+ blockedCommands;
220
+ allowedPrefixes;
221
+ maxArgLength;
222
+ allowPipes;
223
+ allowBackground;
224
+ static DANGEROUS_PATTERNS = [
225
+ /rm\s+-rf\s+\//,
226
+ /mkfs/,
227
+ /dd\s+if=/,
228
+ /:\(\)\{.*;\}/,
229
+ /wget.*\|\s*sh/,
230
+ /curl.*\|\s*sh/,
231
+ /chmod\s+-R\s+777/,
232
+ /chmod\s+777\s+\//,
233
+ />\s*\/dev\/sda/,
234
+ /mv\s+.*\s+\/dev\/null/,
235
+ ];
236
+ constructor(config) {
237
+ this.blockedCommands = config?.blockedCommands ?? DEFAULT_OS_SANDBOX_CONFIG.commands.blockedCommands;
238
+ this.allowedPrefixes = config?.allowedPrefixes ?? [];
239
+ this.maxArgLength = config?.maxArgLength ?? 8192;
240
+ this.allowPipes = config?.allowPipes ?? true;
241
+ this.allowBackground = config?.allowBackground ?? false;
242
+ }
243
+ validate(command) {
244
+ const trimmed = command.trim();
245
+ // Check argument length
246
+ if (trimmed.length > this.maxArgLength) {
247
+ return { allowed: false, reason: `Command exceeds maximum length of ${this.maxArgLength} characters` };
248
+ }
249
+ // Check blocked commands list
250
+ for (const blocked of this.blockedCommands) {
251
+ if (trimmed.includes(blocked) || trimmed.startsWith(blocked.split(' ')[0])) {
252
+ return { allowed: false, reason: `Blocked command pattern: "${blocked}"` };
253
+ }
254
+ }
255
+ // Check dangerous patterns
256
+ for (const pattern of CommandFilter.DANGEROUS_PATTERNS) {
257
+ if (pattern.test(trimmed)) {
258
+ return { allowed: false, reason: 'Command matches a dangerous pattern' };
259
+ }
260
+ }
261
+ // Check for shell injection attempts
262
+ const injectionPatterns = [
263
+ /\$\(.*\)/, // Command substitution
264
+ /`.*`/, // Backtick command substitution
265
+ /&&\s*rm/, // Chained destructive commands
266
+ /\|\s*sh\b/, // Pipe to shell
267
+ ];
268
+ for (const pattern of injectionPatterns) {
269
+ if (pattern.test(trimmed)) {
270
+ return { allowed: false, reason: 'Potential shell injection detected' };
271
+ }
272
+ }
273
+ // Check pipe restrictions
274
+ if (!this.allowPipes && trimmed.includes('|')) {
275
+ return { allowed: false, reason: 'Pipes are not allowed in sandbox mode' };
276
+ }
277
+ // Check background execution
278
+ if (!this.allowBackground && trimmed.includes('&')) {
279
+ const ampersands = trimmed.match(/[^&]&[^&]|&$/g);
280
+ if (ampersands) {
281
+ return { allowed: false, reason: 'Background execution is not allowed in sandbox mode' };
282
+ }
283
+ }
284
+ // Check allowed prefixes (if set)
285
+ if (this.allowedPrefixes.length > 0) {
286
+ const cmdBase = trimmed.split(/\s+/)[0];
287
+ const isAllowed = this.allowedPrefixes.some(prefix => cmdBase === prefix || trimmed.startsWith(prefix));
288
+ if (!isAllowed) {
289
+ return { allowed: false, reason: `Command "${cmdBase}" is not in the allowed prefixes list` };
290
+ }
291
+ }
292
+ return { allowed: true };
293
+ }
294
+ sanitizeEnv(env) {
295
+ const sensitiveKeys = [
296
+ 'AWS_SECRET_ACCESS_KEY',
297
+ 'AWS_ACCESS_KEY_ID',
298
+ 'API_KEY',
299
+ 'SECRET',
300
+ 'TOKEN',
301
+ 'PASSWORD',
302
+ 'PRIVATE_KEY',
303
+ 'DATABASE_URL',
304
+ 'MONGODB_URI',
305
+ 'REDIS_URL',
306
+ ];
307
+ const sanitized = {};
308
+ for (const [key, value] of Object.entries(env)) {
309
+ const isSensitive = sensitiveKeys.some(s => key.toUpperCase().includes(s));
310
+ if (!isSensitive) {
311
+ sanitized[key] = value;
312
+ }
313
+ }
314
+ return sanitized;
315
+ }
316
+ }
317
+ // ---- Filesystem Policy ----
318
+ class FilesystemPolicy {
319
+ readOnlyPaths;
320
+ readWritePaths;
321
+ deniedPaths;
322
+ allowHiddenFiles;
323
+ accessLog = [];
324
+ constructor(config) {
325
+ this.readOnlyPaths = config?.readOnlyPaths ?? [];
326
+ this.readWritePaths = config?.readWritePaths ?? [];
327
+ this.deniedPaths = config?.deniedPaths ?? DEFAULT_OS_SANDBOX_CONFIG.filesystem.deniedPaths;
328
+ this.allowHiddenFiles = config?.allowHiddenFiles ?? false;
329
+ }
330
+ validate(path, mode) {
331
+ const absPath = normalize(resolve(path));
332
+ const entry = {
333
+ timestamp: Date.now(),
334
+ path: absPath,
335
+ mode,
336
+ action: 'allowed',
337
+ };
338
+ // Check denied paths
339
+ for (const denied of this.deniedPaths) {
340
+ const deniedPattern = denied.replace(/\*/g, '.*');
341
+ try {
342
+ const regex = new RegExp('^' + deniedPattern + '$');
343
+ if (regex.test(absPath)) {
344
+ entry.action = 'blocked';
345
+ entry.reason = `Path matches denied pattern: ${denied}`;
346
+ this.accessLog.push(entry);
347
+ return { allowed: false, reason: entry.reason };
348
+ }
349
+ }
350
+ catch {
351
+ // Invalid pattern, skip
352
+ }
353
+ }
354
+ // Check hidden files
355
+ if (!this.allowHiddenFiles) {
356
+ const parts = absPath.split('/');
357
+ if (parts.some(part => part.startsWith('.') && part !== '.' && part !== '..')) {
358
+ entry.action = 'blocked';
359
+ entry.reason = 'Access to hidden files is not allowed';
360
+ this.accessLog.push(entry);
361
+ return { allowed: false, reason: entry.reason };
362
+ }
363
+ }
364
+ // For write and delete, check read-only paths
365
+ if (mode === 'write' || mode === 'delete') {
366
+ for (const roPath of this.readOnlyPaths) {
367
+ if (absPath.startsWith(roPath)) {
368
+ // Check if explicitly in read-write paths
369
+ const isExplicitlyRW = this.readWritePaths.some(rwPath => absPath.startsWith(rwPath));
370
+ if (!isExplicitlyRW) {
371
+ entry.action = 'blocked';
372
+ entry.reason = `Path is read-only: ${roPath}`;
373
+ this.accessLog.push(entry);
374
+ return { allowed: false, reason: entry.reason };
375
+ }
376
+ }
377
+ }
378
+ }
379
+ this.accessLog.push(entry);
380
+ return { allowed: true };
381
+ }
382
+ getAccessLog() {
383
+ return [...this.accessLog];
384
+ }
385
+ clearLog() {
386
+ this.accessLog = [];
387
+ }
388
+ }
389
+ // ---- Main OSSandboxManager Class ----
390
+ export class OSSandboxManager {
391
+ config;
392
+ networkPolicy;
393
+ commandFilter;
394
+ filesystemPolicy;
395
+ auditLog = [];
396
+ dockerAvailable = null;
397
+ sandboxImageBuilt = false;
398
+ activeContainers = new Map();
399
+ projectDir;
400
+ constructor(config = {}) {
401
+ this.config = this.mergeConfig(config);
402
+ this.networkPolicy = new NetworkPolicyEngine(this.config.network);
403
+ this.commandFilter = new CommandFilter(this.config.commands);
404
+ this.filesystemPolicy = new FilesystemPolicy(this.config.filesystem);
405
+ this.projectDir = process.cwd();
406
+ }
407
+ // ---- Docker Availability ----
408
+ async isDockerAvailable() {
409
+ if (this.dockerAvailable !== null)
410
+ return this.dockerAvailable;
411
+ try {
412
+ const result = execSync('docker info --format "{{.ServerVersion}}" 2>/dev/null', {
413
+ encoding: 'utf-8',
414
+ timeout: 5000,
415
+ });
416
+ this.dockerAvailable = result.trim().length > 0;
417
+ }
418
+ catch {
419
+ this.dockerAvailable = false;
420
+ }
421
+ this.addAuditLog('container', 'docker_availability_check', `Docker is ${this.dockerAvailable ? 'available' : 'not available'}`, true);
422
+ return this.dockerAvailable;
423
+ }
424
+ // ---- Native Sandbox Availability ----
425
+ isNativeSandboxAvailable() {
426
+ const currentPlatform = platform();
427
+ if (currentPlatform === 'linux') {
428
+ try {
429
+ execSync('unshare --help 2>/dev/null', { encoding: 'utf-8', timeout: 3000 });
430
+ return true;
431
+ }
432
+ catch {
433
+ return false;
434
+ }
435
+ }
436
+ if (currentPlatform === 'darwin') {
437
+ try {
438
+ execSync('sandbox-exec -n no-network /bin/true 2>/dev/null', {
439
+ encoding: 'utf-8',
440
+ timeout: 3000,
441
+ });
442
+ return true;
443
+ }
444
+ catch {
445
+ return false;
446
+ }
447
+ }
448
+ return false;
449
+ }
450
+ // ---- Command Execution ----
451
+ async execute(command, options) {
452
+ const startTime = Date.now();
453
+ // Validate command against filter
454
+ const cmdValidation = this.validateCommand(command);
455
+ if (!cmdValidation.allowed) {
456
+ this.addAuditLog('command', 'execute_blocked', command, false, cmdValidation.reason);
457
+ return {
458
+ stdout: '',
459
+ stderr: `Sandbox: Command blocked - ${cmdValidation.reason}`,
460
+ exitCode: 126,
461
+ networkAccessLog: this.networkPolicy.getAccessLog(),
462
+ fileAccessLog: this.filesystemPolicy.getAccessLog(),
463
+ duration: Date.now() - startTime,
464
+ timedOut: false,
465
+ };
466
+ }
467
+ this.addAuditLog('command', 'execute', command, true);
468
+ const timeout = options?.timeout ?? this.config.docker?.timeout ?? 120000;
469
+ try {
470
+ if (this.config.type === 'docker') {
471
+ return await this.executeInDocker(command, options, timeout, startTime);
472
+ }
473
+ if (this.config.type === 'os-native') {
474
+ return await this.executeNative(command, options, timeout, startTime);
475
+ }
476
+ // hybrid: try docker first, fall back to native
477
+ if (await this.isDockerAvailable()) {
478
+ return await this.executeInDocker(command, options, timeout, startTime);
479
+ }
480
+ if (this.isNativeSandboxAvailable()) {
481
+ return await this.executeNative(command, options, timeout, startTime);
482
+ }
483
+ // No sandboxing available, execute with filtering only
484
+ return await this.executeFiltered(command, options, timeout, startTime);
485
+ }
486
+ catch (error) {
487
+ return {
488
+ stdout: '',
489
+ stderr: `Sandbox execution error: ${error.message}`,
490
+ exitCode: 1,
491
+ networkAccessLog: this.networkPolicy.getAccessLog(),
492
+ fileAccessLog: this.filesystemPolicy.getAccessLog(),
493
+ duration: Date.now() - startTime,
494
+ timedOut: false,
495
+ };
496
+ }
497
+ }
498
+ // ---- Container Lifecycle ----
499
+ async createContainer() {
500
+ if (!(await this.isDockerAvailable())) {
501
+ throw new Error('Docker is not available for container creation');
502
+ }
503
+ await this.ensureSandboxImage();
504
+ const dockerConfig = this.config.docker;
505
+ const args = ['create'];
506
+ // Resource limits
507
+ args.push('--memory', dockerConfig.memory);
508
+ args.push('--cpus', String(dockerConfig.cpus));
509
+ // Network configuration
510
+ if (dockerConfig.networkMode === 'none') {
511
+ args.push('--network', 'none');
512
+ }
513
+ else if (dockerConfig.networkMode === 'limited') {
514
+ // Use bridge network with iptables rules applied later
515
+ args.push('--network', 'bridge');
516
+ }
517
+ else {
518
+ args.push('--network', 'bridge');
519
+ }
520
+ // DNS configuration for filtered mode
521
+ if (dockerConfig.networkMode === 'limited') {
522
+ args.push('--dns', '127.0.0.1');
523
+ }
524
+ // Security options
525
+ args.push('--security-opt', 'no-new-privileges');
526
+ args.push('--cap-drop', 'ALL');
527
+ args.push('--cap-add', 'CHOWN');
528
+ args.push('--cap-add', 'SETUID');
529
+ args.push('--cap-add', 'SETGID');
530
+ args.push('--cap-add', 'DAC_OVERRIDE');
531
+ args.push('--pids-limit', '64');
532
+ args.push('--read-only');
533
+ // Mount project directory
534
+ if (dockerConfig.mountProject) {
535
+ const mountMode = dockerConfig.mountMode === 'ro' ? ':ro' : ':rw';
536
+ args.push('-v', `${this.projectDir}:/workspace${mountMode}`);
537
+ }
538
+ // Tmpfs for write paths
539
+ args.push('--tmpfs', '/tmp:rw,noexec,nosuid,size=64m');
540
+ args.push('--tmpfs', '/home/sandbox:rw,noexec,nosuid,size=64m');
541
+ // Working directory
542
+ args.push('-w', '/workspace');
543
+ // Image
544
+ args.push(dockerConfig.image);
545
+ // Default command: keep container alive
546
+ args.push('tail', '-f', '/dev/null');
547
+ try {
548
+ const containerId = execSync(`docker ${args.join(' ')}`, {
549
+ encoding: 'utf-8',
550
+ timeout: 30000,
551
+ }).trim();
552
+ this.activeContainers.set(containerId, {
553
+ id: containerId,
554
+ startedAt: Date.now(),
555
+ });
556
+ // Apply network rules for limited mode
557
+ if (dockerConfig.networkMode === 'limited') {
558
+ await this.applyContainerNetworkRules(containerId);
559
+ }
560
+ this.addAuditLog('container', 'create', `Container ${containerId.substring(0, 12)} created`, true);
561
+ return containerId;
562
+ }
563
+ catch (error) {
564
+ this.addAuditLog('container', 'create_failed', error.message, false);
565
+ throw new Error(`Failed to create container: ${error.message}`);
566
+ }
567
+ }
568
+ async stopContainer(id) {
569
+ try {
570
+ execSync(`docker stop ${id} 2>/dev/null`, { encoding: 'utf-8', timeout: 10000 });
571
+ execSync(`docker rm ${id} 2>/dev/null`, { encoding: 'utf-8', timeout: 10000 });
572
+ this.activeContainers.delete(id);
573
+ this.addAuditLog('container', 'stop', `Container ${id.substring(0, 12)} stopped and removed`, true);
574
+ }
575
+ catch (error) {
576
+ this.addAuditLog('container', 'stop_failed', `Failed to stop container ${id.substring(0, 12)}: ${error.message}`, false);
577
+ throw new Error(`Failed to stop container: ${error.message}`);
578
+ }
579
+ }
580
+ // ---- Validation Methods ----
581
+ validateCommand(command) {
582
+ return this.commandFilter.validate(command);
583
+ }
584
+ validatePath(path, mode) {
585
+ return this.filesystemPolicy.validate(path, mode);
586
+ }
587
+ validateNetwork(host, port) {
588
+ return this.networkPolicy.validate(host, port);
589
+ }
590
+ // ---- Audit Log ----
591
+ getAuditLog() {
592
+ return [...this.auditLog];
593
+ }
594
+ clearAuditLog() {
595
+ this.auditLog = [];
596
+ this.networkPolicy.clearLog();
597
+ this.filesystemPolicy.clearLog();
598
+ }
599
+ // ---- Cleanup ----
600
+ async cleanup() {
601
+ const containerIds = [...this.activeContainers.keys()];
602
+ for (const id of containerIds) {
603
+ try {
604
+ await this.stopContainer(id);
605
+ }
606
+ catch {
607
+ // Best-effort cleanup
608
+ }
609
+ }
610
+ this.addAuditLog('config', 'cleanup', 'All containers cleaned up', true);
611
+ }
612
+ // ---- Print Status ----
613
+ printStatus() {
614
+ const typeLabel = this.config.type.toUpperCase();
615
+ console.log(chalk.bold(`\n OS-Level Sandbox: ${chalk.cyan(typeLabel)}`));
616
+ if (this.config.type === 'docker' || this.config.type === 'hybrid') {
617
+ const dockerStatus = this.dockerAvailable === null
618
+ ? chalk.yellow('NOT CHECKED')
619
+ : this.dockerAvailable
620
+ ? chalk.green('AVAILABLE')
621
+ : chalk.red('NOT AVAILABLE');
622
+ console.log(` Docker: ${dockerStatus}`);
623
+ if (this.config.docker) {
624
+ console.log(` Image: ${chalk.cyan(this.config.docker.image)}`);
625
+ console.log(` Mount: ${this.config.docker.mountProject ? chalk.green('YES') : chalk.gray('NO')} (${this.config.docker.mountMode})`);
626
+ console.log(` Network: ${chalk.yellow(this.config.docker.networkMode)}`);
627
+ console.log(` Memory: ${this.config.docker.memory}, CPUs: ${this.config.docker.cpus}`);
628
+ }
629
+ }
630
+ if (this.config.type === 'os-native' || this.config.type === 'hybrid') {
631
+ const nativeStatus = this.isNativeSandboxAvailable()
632
+ ? chalk.green('AVAILABLE')
633
+ : chalk.red('NOT AVAILABLE');
634
+ console.log(` Native Sandbox: ${nativeStatus}`);
635
+ }
636
+ if (this.config.network) {
637
+ console.log(` Network Mode: ${chalk.yellow(this.config.network.mode)}`);
638
+ if (this.config.network.mode === 'filtered') {
639
+ console.log(` Allowed Domains: ${this.config.network.allowedDomains.length > 0
640
+ ? chalk.cyan(this.config.network.allowedDomains.join(', '))
641
+ : chalk.gray('none')}`);
642
+ console.log(` Allowed Ports: ${this.config.network.allowedPorts.length > 0
643
+ ? chalk.cyan(this.config.network.allowedPorts.join(', '))
644
+ : chalk.gray('all')}`);
645
+ console.log(` Block Private Networks: ${this.config.network.blockPrivateNetworks
646
+ ? chalk.green('YES')
647
+ : chalk.red('NO')}`);
648
+ }
649
+ }
650
+ if (this.config.commands) {
651
+ console.log(` Blocked Commands: ${chalk.red(String(this.config.commands.blockedCommands.length))}`);
652
+ console.log(` Allow Pipes: ${this.config.commands.allowPipes ? chalk.green('YES') : chalk.red('NO')}`);
653
+ console.log(` Allow Background: ${this.config.commands.allowBackground ? chalk.green('YES') : chalk.red('NO')}`);
654
+ }
655
+ if (this.activeContainers.size > 0) {
656
+ console.log(` Active Containers: ${chalk.green(String(this.activeContainers.size))}`);
657
+ }
658
+ if (this.auditLog.length > 0) {
659
+ const blocked = this.auditLog.filter(e => !e.allowed).length;
660
+ console.log(` Audit Entries: ${this.auditLog.length} (${chalk.red(String(blocked))} blocked)`);
661
+ }
662
+ console.log();
663
+ }
664
+ // ============================================================
665
+ // Private Methods
666
+ // ============================================================
667
+ mergeConfig(partial) {
668
+ const base = { ...DEFAULT_OS_SANDBOX_CONFIG };
669
+ return {
670
+ ...base,
671
+ ...partial,
672
+ docker: { ...base.docker, ...partial.docker },
673
+ network: { ...base.network, ...partial.network },
674
+ filesystem: { ...base.filesystem, ...partial.filesystem },
675
+ commands: { ...base.commands, ...partial.commands },
676
+ };
677
+ }
678
+ addAuditLog(category, action, detail, allowed, reason) {
679
+ this.auditLog.push({
680
+ timestamp: Date.now(),
681
+ category,
682
+ action,
683
+ detail,
684
+ allowed,
685
+ reason,
686
+ });
687
+ }
688
+ // ---- Docker Execution ----
689
+ async executeInDocker(command, options, timeout, startTime) {
690
+ if (!(await this.isDockerAvailable())) {
691
+ if (this.config.type === 'docker') {
692
+ return {
693
+ stdout: '',
694
+ stderr: 'Docker is not available. Cannot run in Docker sandbox mode.',
695
+ exitCode: 127,
696
+ networkAccessLog: this.networkPolicy.getAccessLog(),
697
+ fileAccessLog: this.filesystemPolicy.getAccessLog(),
698
+ duration: Date.now() - startTime,
699
+ timedOut: false,
700
+ };
701
+ }
702
+ // Fall back to filtered execution for hybrid mode
703
+ return this.executeFiltered(command, options, timeout, startTime);
704
+ }
705
+ await this.ensureSandboxImage();
706
+ const dockerConfig = this.config.docker;
707
+ const args = ['run', '--rm'];
708
+ // Resource limits
709
+ args.push('--memory', dockerConfig.memory);
710
+ args.push('--memory-swap', dockerConfig.memory);
711
+ args.push('--cpus', String(dockerConfig.cpus));
712
+ // Network configuration
713
+ if (dockerConfig.networkMode === 'none') {
714
+ args.push('--network', 'none');
715
+ }
716
+ else if (dockerConfig.networkMode === 'limited') {
717
+ args.push('--network', 'bridge');
718
+ }
719
+ else {
720
+ args.push('--network', 'bridge');
721
+ }
722
+ // Security options
723
+ args.push('--security-opt', 'no-new-privileges');
724
+ args.push('--cap-drop', 'ALL');
725
+ args.push('--cap-add', 'CHOWN');
726
+ args.push('--cap-add', 'SETUID');
727
+ args.push('--cap-add', 'SETGID');
728
+ args.push('--cap-add', 'DAC_OVERRIDE');
729
+ args.push('--pids-limit', '64');
730
+ // Mount project directory
731
+ if (dockerConfig.mountProject) {
732
+ const mountMode = dockerConfig.mountMode === 'ro' ? ':ro' : ':rw';
733
+ args.push('-v', `${this.projectDir}:/workspace${mountMode}`);
734
+ }
735
+ // Tmpfs for temporary writes
736
+ args.push('--tmpfs', '/tmp:rw,noexec,nosuid,size=64m');
737
+ // Working directory
738
+ args.push('-w', '/workspace');
739
+ // Environment variables (sanitized)
740
+ if (options?.env) {
741
+ const sanitizedEnv = this.commandFilter.sanitizeEnv(options.env);
742
+ for (const [key, value] of Object.entries(sanitizedEnv)) {
743
+ args.push('-e', `${key}=${value}`);
744
+ }
745
+ }
746
+ // Timeout
747
+ const dockerTimeout = Math.min(timeout, 300000);
748
+ // Image
749
+ args.push(dockerConfig.image);
750
+ // Command to execute
751
+ args.push('sh', '-c', command);
752
+ this.addAuditLog('command', 'docker_execute', `Executing in Docker: ${command.substring(0, 100)}`, true);
753
+ return new Promise((resolveResult) => {
754
+ let stdout = '';
755
+ let stderr = '';
756
+ let timedOut = false;
757
+ const child = spawn('docker', args, {
758
+ cwd: options?.cwd ?? this.projectDir,
759
+ env: { ...process.env, FORCE_COLOR: '0' },
760
+ stdio: ['pipe', 'pipe', 'pipe'],
761
+ });
762
+ const timer = setTimeout(() => {
763
+ timedOut = true;
764
+ child.kill('SIGKILL');
765
+ }, dockerTimeout);
766
+ child.stdout?.on('data', (data) => {
767
+ stdout += data.toString();
768
+ });
769
+ child.stderr?.on('data', (data) => {
770
+ stderr += data.toString();
771
+ });
772
+ if (options?.stdin) {
773
+ child.stdin?.write(options.stdin);
774
+ child.stdin?.end();
775
+ }
776
+ child.on('close', (exitCode) => {
777
+ clearTimeout(timer);
778
+ const duration = Date.now() - startTime;
779
+ resolveResult({
780
+ stdout: this.truncateOutput(stdout),
781
+ stderr: this.truncateOutput(stderr),
782
+ exitCode: timedOut ? 124 : (exitCode ?? 1),
783
+ networkAccessLog: this.networkPolicy.getAccessLog(),
784
+ fileAccessLog: this.filesystemPolicy.getAccessLog(),
785
+ duration,
786
+ timedOut,
787
+ });
788
+ });
789
+ child.on('error', (error) => {
790
+ clearTimeout(timer);
791
+ resolveResult({
792
+ stdout: '',
793
+ stderr: `Docker execution error: ${error.message}`,
794
+ exitCode: 1,
795
+ networkAccessLog: this.networkPolicy.getAccessLog(),
796
+ fileAccessLog: this.filesystemPolicy.getAccessLog(),
797
+ duration: Date.now() - startTime,
798
+ timedOut: false,
799
+ });
800
+ });
801
+ });
802
+ }
803
+ // ---- Native OS Execution ----
804
+ async executeNative(command, options, timeout, startTime) {
805
+ const currentPlatform = platform();
806
+ if (currentPlatform === 'linux') {
807
+ return this.executeLinuxSandbox(command, options, timeout, startTime);
808
+ }
809
+ if (currentPlatform === 'darwin') {
810
+ return this.executeMacOSSandbox(command, options, timeout, startTime);
811
+ }
812
+ // Unsupported platform, fall back to filtered execution
813
+ return this.executeFiltered(command, options, timeout, startTime);
814
+ }
815
+ async executeLinuxSandbox(command, options, timeout, startTime) {
816
+ const networkConfig = this.config.network;
817
+ let unshareFlags = '--pid --mount --uts --ipc';
818
+ // Network isolation
819
+ if (networkConfig.mode === 'blocked') {
820
+ unshareFlags += ' --net';
821
+ }
822
+ // Build the sandboxed command with namespace isolation
823
+ let sandboxedCommand = command;
824
+ // Apply filesystem restrictions via bind mounts where possible
825
+ const fsConfig = this.config.filesystem;
826
+ if (fsConfig && fsConfig.deniedPaths.length > 0) {
827
+ // Add a wrapper that checks path access
828
+ const deniedChecks = fsConfig.deniedPaths
829
+ .map(p => p.replace(/\*/g, '.*'))
830
+ .join('|');
831
+ sandboxedCommand = `bash -c '
832
+ deny_pattern="${deniedChecks}";
833
+ original_command="${command.replace(/"/g, '\\"').replace(/'/g, "'\\''")}";
834
+ $original_command
835
+ '`;
836
+ }
837
+ const fullCommand = `unshare ${unshareFlags} sh -c ${shellEscape(sandboxedCommand)}`;
838
+ this.addAuditLog('command', 'native_linux_execute', `Executing with namespaces: ${command.substring(0, 100)}`, true);
839
+ return new Promise((resolveResult) => {
840
+ let stdout = '';
841
+ let stderr = '';
842
+ let timedOut = false;
843
+ const child = spawn('sh', ['-c', fullCommand], {
844
+ cwd: options?.cwd ?? this.projectDir,
845
+ env: options?.env
846
+ ? { ...process.env, ...this.commandFilter.sanitizeEnv(options.env) }
847
+ : { ...process.env, FORCE_COLOR: '0' },
848
+ stdio: ['pipe', 'pipe', 'pipe'],
849
+ });
850
+ const timer = setTimeout(() => {
851
+ timedOut = true;
852
+ child.kill('SIGKILL');
853
+ }, timeout);
854
+ child.stdout?.on('data', (data) => {
855
+ stdout += data.toString();
856
+ });
857
+ child.stderr?.on('data', (data) => {
858
+ stderr += data.toString();
859
+ });
860
+ if (options?.stdin) {
861
+ child.stdin?.write(options.stdin);
862
+ child.stdin?.end();
863
+ }
864
+ child.on('close', (exitCode) => {
865
+ clearTimeout(timer);
866
+ // If network was blocked, log any attempted network access via audit
867
+ if (networkConfig.mode === 'blocked') {
868
+ this.addAuditLog('network', 'namespace_isolation', 'Network namespace isolation applied (no external access)', true);
869
+ }
870
+ resolveResult({
871
+ stdout: this.truncateOutput(stdout),
872
+ stderr: this.truncateOutput(stderr),
873
+ exitCode: timedOut ? 124 : (exitCode ?? 1),
874
+ networkAccessLog: this.networkPolicy.getAccessLog(),
875
+ fileAccessLog: this.filesystemPolicy.getAccessLog(),
876
+ duration: Date.now() - startTime,
877
+ timedOut,
878
+ });
879
+ });
880
+ child.on('error', (error) => {
881
+ clearTimeout(timer);
882
+ resolveResult({
883
+ stdout: '',
884
+ stderr: `Native sandbox error: ${error.message}`,
885
+ exitCode: 1,
886
+ networkAccessLog: this.networkPolicy.getAccessLog(),
887
+ fileAccessLog: this.filesystemPolicy.getAccessLog(),
888
+ duration: Date.now() - startTime,
889
+ timedOut: false,
890
+ });
891
+ });
892
+ });
893
+ }
894
+ async executeMacOSSandbox(command, options, timeout, startTime) {
895
+ const networkConfig = this.config.network;
896
+ const fsConfig = this.config.filesystem;
897
+ // Build sandbox-exec profile
898
+ let profile = '(version 1)\n(deny default)\n';
899
+ // Allow basic process operations
900
+ profile += '(allow process-exec)\n';
901
+ profile += '(allow process-fork)\n';
902
+ profile += '(allow signal)\n';
903
+ profile += '(allow sysctl-read)\n';
904
+ profile += '(allow file-read*)\n';
905
+ // Filesystem write rules
906
+ if (fsConfig) {
907
+ // Allow writing to project directory
908
+ profile += `(allow file-write* (subpath "${this.projectDir}"))\n`;
909
+ // Allow writing to temp
910
+ profile += '(allow file-write* (subpath "/tmp"))\n';
911
+ profile += '(allow file-write* (subpath "/var/folders"))\n';
912
+ // Deny specific paths
913
+ for (const denied of fsConfig.deniedPaths) {
914
+ const expanded = denied.replace(/\/\*/g, '');
915
+ if (expanded.startsWith('/')) {
916
+ profile += `(deny file-read* (subpath "${expanded}"))\n`;
917
+ profile += `(deny file-write* (subpath "${expanded}"))\n`;
918
+ }
919
+ }
920
+ }
921
+ // Network rules
922
+ if (networkConfig.mode === 'blocked') {
923
+ profile += '(deny network*)\n';
924
+ }
925
+ else if (networkConfig.mode === 'filtered') {
926
+ profile += '(deny network*)\n';
927
+ for (const domain of networkConfig.allowedDomains) {
928
+ profile += `(allow network-outbound (host "${domain}"))\n`;
929
+ }
930
+ for (const port of networkConfig.allowedPorts) {
931
+ profile += `(allow network-outbound (port "${port}"))\n`;
932
+ }
933
+ }
934
+ else {
935
+ profile += '(allow network*)\n';
936
+ }
937
+ // Write profile to temp file
938
+ const profilePath = `/tmp/neurocli-sandbox-${Date.now()}.sb`;
939
+ writeFileSync(profilePath, profile, 'utf-8');
940
+ const sandboxedCommand = `sandbox-exec -f ${profilePath} sh -c ${shellEscape(command)}`;
941
+ this.addAuditLog('command', 'native_macos_execute', `Executing with sandbox-exec: ${command.substring(0, 100)}`, true);
942
+ return new Promise((resolveResult) => {
943
+ let stdout = '';
944
+ let stderr = '';
945
+ let timedOut = false;
946
+ const child = spawn('sh', ['-c', sandboxedCommand], {
947
+ cwd: options?.cwd ?? this.projectDir,
948
+ env: options?.env
949
+ ? { ...process.env, ...this.commandFilter.sanitizeEnv(options.env) }
950
+ : { ...process.env, FORCE_COLOR: '0' },
951
+ stdio: ['pipe', 'pipe', 'pipe'],
952
+ });
953
+ const timer = setTimeout(() => {
954
+ timedOut = true;
955
+ child.kill('SIGKILL');
956
+ }, timeout);
957
+ child.stdout?.on('data', (data) => {
958
+ stdout += data.toString();
959
+ });
960
+ child.stderr?.on('data', (data) => {
961
+ stderr += data.toString();
962
+ });
963
+ if (options?.stdin) {
964
+ child.stdin?.write(options.stdin);
965
+ child.stdin?.end();
966
+ }
967
+ child.on('close', (exitCode) => {
968
+ clearTimeout(timer);
969
+ // Clean up profile file
970
+ try {
971
+ const { unlinkSync } = require('fs');
972
+ unlinkSync(profilePath);
973
+ }
974
+ catch {
975
+ // Best-effort cleanup
976
+ }
977
+ resolveResult({
978
+ stdout: this.truncateOutput(stdout),
979
+ stderr: this.truncateOutput(stderr),
980
+ exitCode: timedOut ? 124 : (exitCode ?? 1),
981
+ networkAccessLog: this.networkPolicy.getAccessLog(),
982
+ fileAccessLog: this.filesystemPolicy.getAccessLog(),
983
+ duration: Date.now() - startTime,
984
+ timedOut,
985
+ });
986
+ });
987
+ child.on('error', (error) => {
988
+ clearTimeout(timer);
989
+ resolveResult({
990
+ stdout: '',
991
+ stderr: `macOS sandbox error: ${error.message}`,
992
+ exitCode: 1,
993
+ networkAccessLog: this.networkPolicy.getAccessLog(),
994
+ fileAccessLog: this.filesystemPolicy.getAccessLog(),
995
+ duration: Date.now() - startTime,
996
+ timedOut: false,
997
+ });
998
+ });
999
+ });
1000
+ }
1001
+ // ---- Filtered Execution (Fallback, No OS Isolation) ----
1002
+ async executeFiltered(command, options, timeout, startTime) {
1003
+ this.addAuditLog('command', 'filtered_execute', `Executing with command filtering only (no OS isolation): ${command.substring(0, 100)}`, true);
1004
+ return new Promise((resolveResult) => {
1005
+ let stdout = '';
1006
+ let stderr = '';
1007
+ let timedOut = false;
1008
+ const child = spawn('sh', ['-c', command], {
1009
+ cwd: options?.cwd ?? this.projectDir,
1010
+ env: options?.env
1011
+ ? { ...process.env, ...this.commandFilter.sanitizeEnv(options.env) }
1012
+ : { ...process.env, FORCE_COLOR: '0' },
1013
+ stdio: ['pipe', 'pipe', 'pipe'],
1014
+ });
1015
+ const timer = setTimeout(() => {
1016
+ timedOut = true;
1017
+ child.kill('SIGKILL');
1018
+ }, timeout);
1019
+ child.stdout?.on('data', (data) => {
1020
+ stdout += data.toString();
1021
+ });
1022
+ child.stderr?.on('data', (data) => {
1023
+ stderr += data.toString();
1024
+ });
1025
+ if (options?.stdin) {
1026
+ child.stdin?.write(options.stdin);
1027
+ child.stdin?.end();
1028
+ }
1029
+ child.on('close', (exitCode) => {
1030
+ clearTimeout(timer);
1031
+ // Infer network access from command content for audit purposes
1032
+ if (command.includes('curl') || command.includes('wget') || command.includes('fetch')) {
1033
+ const urlMatch = command.match(/(?:https?:\/\/)([^/\s]+)/g);
1034
+ if (urlMatch) {
1035
+ for (const url of urlMatch) {
1036
+ const host = url.replace('https://', '').replace('http://', '').split('/')[0].split(':')[0];
1037
+ const port = url.includes(':443') ? 443 : url.includes(':80') ? 80 : url.startsWith('https') ? 443 : 80;
1038
+ this.networkPolicy.validate(host, port);
1039
+ }
1040
+ }
1041
+ }
1042
+ resolveResult({
1043
+ stdout: this.truncateOutput(stdout),
1044
+ stderr: this.truncateOutput(stderr),
1045
+ exitCode: timedOut ? 124 : (exitCode ?? 1),
1046
+ networkAccessLog: this.networkPolicy.getAccessLog(),
1047
+ fileAccessLog: this.filesystemPolicy.getAccessLog(),
1048
+ duration: Date.now() - startTime,
1049
+ timedOut,
1050
+ });
1051
+ });
1052
+ child.on('error', (error) => {
1053
+ clearTimeout(timer);
1054
+ resolveResult({
1055
+ stdout: '',
1056
+ stderr: `Filtered execution error: ${error.message}`,
1057
+ exitCode: 1,
1058
+ networkAccessLog: this.networkPolicy.getAccessLog(),
1059
+ fileAccessLog: this.filesystemPolicy.getAccessLog(),
1060
+ duration: Date.now() - startTime,
1061
+ timedOut: false,
1062
+ });
1063
+ });
1064
+ });
1065
+ }
1066
+ // ---- Docker Image Management ----
1067
+ async ensureSandboxImage() {
1068
+ if (this.sandboxImageBuilt)
1069
+ return;
1070
+ const imageName = this.config.docker.image;
1071
+ // Check if image already exists
1072
+ try {
1073
+ execSync(`docker image inspect ${imageName} 2>/dev/null`, {
1074
+ encoding: 'utf-8',
1075
+ timeout: 10000,
1076
+ });
1077
+ this.sandboxImageBuilt = true;
1078
+ this.addAuditLog('container', 'image_exists', `Sandbox image ${imageName} already exists`, true);
1079
+ return;
1080
+ }
1081
+ catch {
1082
+ // Image doesn't exist, need to build it
1083
+ }
1084
+ // Create a temporary directory for the Dockerfile
1085
+ const buildDir = `/tmp/neurocli-sandbox-build-${Date.now()}`;
1086
+ try {
1087
+ mkdirSync(buildDir, { recursive: true });
1088
+ writeFileSync(`${buildDir}/Dockerfile`, SANDBOX_DOCKERFILE, 'utf-8');
1089
+ this.addAuditLog('container', 'build_image', `Building sandbox image ${imageName}...`, true);
1090
+ execSync(`docker build -t ${imageName} ${buildDir}`, {
1091
+ encoding: 'utf-8',
1092
+ timeout: 120000,
1093
+ });
1094
+ this.sandboxImageBuilt = true;
1095
+ this.addAuditLog('container', 'build_image_complete', `Sandbox image ${imageName} built successfully`, true);
1096
+ }
1097
+ catch (error) {
1098
+ this.addAuditLog('container', 'build_image_failed', `Failed to build sandbox image: ${error.message}`, false);
1099
+ throw new Error(`Failed to build sandbox Docker image: ${error.message}`);
1100
+ }
1101
+ }
1102
+ // ---- Container Network Rules ----
1103
+ async applyContainerNetworkRules(containerId) {
1104
+ const dockerConfig = this.config.docker;
1105
+ if (dockerConfig.networkMode !== 'limited')
1106
+ return;
1107
+ try {
1108
+ // Get container PID for network namespace
1109
+ const pid = execSync(`docker inspect --format '{{.State.Pid}}' ${containerId} 2>/dev/null`, { encoding: 'utf-8', timeout: 5000 }).trim();
1110
+ if (pid === '0') {
1111
+ this.addAuditLog('network', 'network_rules_skip', `Container ${containerId.substring(0, 12)} not running, skipping network rules`, true);
1112
+ return;
1113
+ }
1114
+ // Set up iptables rules to restrict outbound traffic
1115
+ // Allow established connections
1116
+ execSync(`docker exec ${containerId} sh -c "` +
1117
+ `which iptables >/dev/null 2>&1 && ` +
1118
+ `iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT && ` +
1119
+ `iptables -A OUTPUT -o lo -j ACCEPT` +
1120
+ `" 2>/dev/null`, { encoding: 'utf-8', timeout: 5000 });
1121
+ // Allow DNS
1122
+ execSync(`docker exec ${containerId} sh -c "` +
1123
+ `which iptables >/dev/null 2>&1 && ` +
1124
+ `iptables -A OUTPUT -p udp --dport 53 -j ACCEPT` +
1125
+ `" 2>/dev/null`, { encoding: 'utf-8', timeout: 5000 });
1126
+ // Allow HTTPS to whitelisted domains (via DNS resolution)
1127
+ for (const domain of dockerConfig.allowedDomains) {
1128
+ try {
1129
+ execSync(`docker exec ${containerId} sh -c "` +
1130
+ `which iptables >/dev/null 2>&1 && ` +
1131
+ `iptables -A OUTPUT -p tcp --dport 443 -d $(nslookup ${domain} 2>/dev/null | tail -1 | awk '{print $2}') -j ACCEPT` +
1132
+ `" 2>/dev/null`, { encoding: 'utf-8', timeout: 5000 });
1133
+ }
1134
+ catch {
1135
+ // DNS resolution may fail, skip this domain
1136
+ }
1137
+ }
1138
+ // Drop all other outbound
1139
+ execSync(`docker exec ${containerId} sh -c "` +
1140
+ `which iptables >/dev/null 2>&1 && ` +
1141
+ `iptables -A OUTPUT -j DROP` +
1142
+ `" 2>/dev/null`, { encoding: 'utf-8', timeout: 5000 });
1143
+ this.addAuditLog('network', 'network_rules_applied', `Network rules applied to container ${containerId.substring(0, 12)}`, true);
1144
+ }
1145
+ catch (error) {
1146
+ this.addAuditLog('network', 'network_rules_failed', `Failed to apply network rules: ${error.message}`, false);
1147
+ // Non-fatal: container still has bridge network but without custom iptables
1148
+ }
1149
+ }
1150
+ // ---- Utility Methods ----
1151
+ truncateOutput(output, maxLength = 30000) {
1152
+ if (output.length <= maxLength)
1153
+ return output;
1154
+ const half = Math.floor(maxLength / 2);
1155
+ return output.slice(0, half) + '\n\n... [output truncated] ...\n\n' + output.slice(-half);
1156
+ }
1157
+ }
1158
+ // ---- Helper: Shell Escape ----
1159
+ function shellEscape(str) {
1160
+ return "'" + str.replace(/'/g, "'\\''") + "'";
1161
+ }
1162
+ // ---- Exported Helper: Create Default Manager ----
1163
+ export function createOSSandboxManager(config, projectDir) {
1164
+ const manager = new OSSandboxManager(config);
1165
+ if (projectDir) {
1166
+ manager.projectDir = resolve(projectDir);
1167
+ }
1168
+ return manager;
1169
+ }
1170
+ // ---- Exported Helper: Quick Availability Check ----
1171
+ export async function checkSandboxCapabilities() {
1172
+ const currentPlatform = platform();
1173
+ const manager = new OSSandboxManager();
1174
+ const docker = await manager.isDockerAvailable();
1175
+ const nativeSandbox = manager.isNativeSandboxAvailable();
1176
+ let recommendedType = 'docker';
1177
+ if (docker) {
1178
+ recommendedType = 'docker';
1179
+ }
1180
+ else if (nativeSandbox) {
1181
+ recommendedType = 'os-native';
1182
+ }
1183
+ else {
1184
+ recommendedType = 'hybrid';
1185
+ }
1186
+ return {
1187
+ docker,
1188
+ nativeSandbox,
1189
+ platform: currentPlatform,
1190
+ recommendedType,
1191
+ };
1192
+ }
1193
+ //# sourceMappingURL=os-sandbox.js.map