tau-coding-agent 0.1.6 → 0.2.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 (80) hide show
  1. package/README.md +15 -12
  2. package/extensions/answer.ts +129 -79
  3. package/extensions/branch-term/README.md +7 -0
  4. package/extensions/{branch-term.ts → branch-term/index.ts} +113 -104
  5. package/extensions/btw.ts +17 -2
  6. package/extensions/caffeinate/README.md +5 -0
  7. package/extensions/caffeinate/index.ts +144 -0
  8. package/extensions/fast.ts +292 -0
  9. package/extensions/ghostty.ts +214 -211
  10. package/extensions/git-diff-stats.ts +124 -84
  11. package/extensions/git-pr-status.ts +274 -208
  12. package/extensions/insights.ts +109 -191
  13. package/extensions/loop.ts +149 -145
  14. package/extensions/memory.ts +138 -86
  15. package/extensions/notify.ts +14 -26
  16. package/extensions/openai-verbosity.ts +108 -42
  17. package/extensions/review/fix.ts +15 -6
  18. package/extensions/review/git.ts +93 -103
  19. package/extensions/review/index.ts +60 -62
  20. package/extensions/review/interrupt.ts +117 -0
  21. package/extensions/review/message-queue.ts +45 -12
  22. package/extensions/review/models.ts +79 -3
  23. package/extensions/review/prompts.ts +41 -38
  24. package/extensions/review/review.ts +161 -166
  25. package/extensions/review/runner.ts +139 -147
  26. package/extensions/review/runtime.ts +173 -120
  27. package/extensions/review/triage.ts +41 -44
  28. package/extensions/sandbox/bash.ts +775 -0
  29. package/extensions/sandbox/command.ts +605 -0
  30. package/extensions/sandbox/config.ts +764 -0
  31. package/extensions/sandbox/index.ts +132 -3338
  32. package/extensions/sandbox/permissions/dialog.ts +118 -0
  33. package/extensions/sandbox/permissions/filesystem.ts +559 -0
  34. package/extensions/sandbox/permissions/mach-lookup.ts +186 -0
  35. package/extensions/sandbox/permissions/network.ts +164 -0
  36. package/extensions/sandbox/permissions/unsandboxed.ts +270 -0
  37. package/extensions/sandbox/runtime.ts +616 -0
  38. package/extensions/stash.ts +28 -15
  39. package/extensions/subagent/README.md +73 -0
  40. package/extensions/subagent/index.ts +822 -0
  41. package/extensions/subagent/interrupt.ts +117 -0
  42. package/extensions/subagent/permissions.ts +101 -0
  43. package/extensions/subagent/rpc.ts +177 -0
  44. package/extensions/tool-display-mode.ts +267 -64
  45. package/extensions/usage/index.ts +249 -356
  46. package/extensions/usage/openrouter.ts +10 -2
  47. package/extensions/websearch/README.md +12 -47
  48. package/extensions/websearch/config.ts +5 -2
  49. package/extensions/websearch/index.ts +94 -109
  50. package/extensions/websearch/output.ts +51 -0
  51. package/extensions/websearch/providers/anthropic.pi.ts +27 -44
  52. package/extensions/websearch/providers/gemini.browser.ts +68 -77
  53. package/extensions/websearch/providers/gemini.pi.ts +17 -17
  54. package/extensions/websearch/providers/openai-codex.pi.ts +145 -7
  55. package/extensions/websearch/providers/pi-model.shared.ts +33 -39
  56. package/extensions/websearch/providers/shared.ts +87 -2
  57. package/extensions/websearch/types.ts +0 -3
  58. package/extensions/worktree.ts +132 -172
  59. package/package.json +10 -5
  60. package/skills/browser-tools/SKILL.md +29 -234
  61. package/skills/browser-tools/references/cookies.md +36 -0
  62. package/skills/browser-tools/references/interaction.md +90 -0
  63. package/skills/browser-tools/references/logging.md +34 -0
  64. package/skills/git-clean-history/SKILL.md +6 -6
  65. package/skills/git-commit/SKILL.md +5 -3
  66. package/skills/github-pull-request/SKILL.md +60 -0
  67. package/skills/github-pull-request/references/create.md +40 -0
  68. package/skills/github-pull-request/references/stewardship.md +60 -0
  69. package/skills/oracle/SKILL.md +3 -3
  70. package/skills/oracle/scripts/oracle +10 -1
  71. package/skills/sentry/SKILL.md +15 -185
  72. package/skills/sentry/references/events.md +79 -0
  73. package/skills/sentry/references/issues.md +62 -0
  74. package/skills/sentry/references/logs.md +46 -0
  75. package/skills/update-changelog/SKILL.md +27 -121
  76. package/skills/web-design/SKILL.md +16 -105
  77. package/themes/tau-dark.json +4 -0
  78. package/extensions/openai-fast.ts +0 -229
  79. package/extensions/websearch/providers/openai-codex.browser.ts +0 -77
  80. package/extensions/websearch/providers/openai-codex.shared.ts +0 -123
@@ -0,0 +1,775 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { SandboxManager, type SandboxRuntimeConfig } from "@anthropic-ai/sandbox-runtime";
6
+ import {
7
+ createLocalBashOperations,
8
+ type BashOperations,
9
+ type ExtensionContext,
10
+ } from "@earendil-works/pi-coding-agent";
11
+
12
+ import {
13
+ cloneRuntimeConfig,
14
+ inferSandboxRuleMatch,
15
+ isSandboxWritablePath,
16
+ mutateStringList,
17
+ type PromptMode,
18
+ type SandboxConfig,
19
+ } from "./config.js";
20
+ import type { PermissionResolution } from "./permissions/dialog.js";
21
+ import {
22
+ detectFilesystemViolationFromLine,
23
+ extractAppendedSandboxAnnotation,
24
+ formatRuntimeProtectedWriteNotice,
25
+ formatTraversalNotice,
26
+ getRuntimeProtectedWriteViolations,
27
+ getTraversalPaths,
28
+ handleFilesystemViolation,
29
+ isRuntimeProtectedWriteViolation,
30
+ isTraversalViolation,
31
+ type FilesystemViolation,
32
+ } from "./permissions/filesystem.js";
33
+ import {
34
+ detectMachLookupViolations,
35
+ handleMachLookupViolation,
36
+ } from "./permissions/mach-lookup.js";
37
+ import {
38
+ formatUnsandboxedApproval,
39
+ UNSANDBOXED_APPROVAL_CHOICES,
40
+ } from "./permissions/unsandboxed.js";
41
+ import { notify, type SandboxEvent, type SandboxRuntime } from "./runtime.js";
42
+
43
+ const IS_MACOS = process.platform === "darwin";
44
+ const MACOS_SANDBOX_SHELL = fileURLToPath(new URL("./macos-sandbox-shell.mjs", import.meta.url));
45
+ const MAX_TIMEOUT_MS = 2_147_483_647;
46
+ const MAX_TIMEOUT_SECONDS = MAX_TIMEOUT_MS / 1000;
47
+ const GIT_FILESYSTEM_PATHS_CACHE = new Map<string, GitFilesystemPaths | null>();
48
+
49
+ interface GitFilesystemPaths {
50
+ gitDir: string;
51
+ gitCommonDir: string;
52
+ }
53
+
54
+ function resolveGitFilesystemPaths(cwd: string): GitFilesystemPaths | null {
55
+ if (GIT_FILESYSTEM_PATHS_CACHE.has(cwd)) {
56
+ return GIT_FILESYSTEM_PATHS_CACHE.get(cwd) ?? null;
57
+ }
58
+
59
+ const result = spawnSync(
60
+ "git",
61
+ ["rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir"],
62
+ {
63
+ cwd,
64
+ encoding: "utf-8",
65
+ stdio: ["ignore", "pipe", "ignore"],
66
+ },
67
+ );
68
+
69
+ if (result.status !== 0) {
70
+ GIT_FILESYSTEM_PATHS_CACHE.set(cwd, null);
71
+ return null;
72
+ }
73
+
74
+ const [gitDir, gitCommonDir] = result.stdout
75
+ .split("\n")
76
+ .map((line) => line.trim())
77
+ .filter(Boolean);
78
+
79
+ if (!gitDir || !gitCommonDir) {
80
+ GIT_FILESYSTEM_PATHS_CACHE.set(cwd, null);
81
+ return null;
82
+ }
83
+
84
+ const gitPaths = { gitDir, gitCommonDir };
85
+ GIT_FILESYSTEM_PATHS_CACHE.set(cwd, gitPaths);
86
+ return gitPaths;
87
+ }
88
+
89
+ function maybeAddGitMetadataWritePath(
90
+ runtimeConfig: SandboxRuntimeConfig,
91
+ path: string,
92
+ cwd?: string,
93
+ ): SandboxRuntimeConfig | null {
94
+ if (isSandboxWritablePath(runtimeConfig, path, cwd)) return null;
95
+ if (inferSandboxRuleMatch(path, runtimeConfig.filesystem.denyWrite, cwd)) return null;
96
+
97
+ const nextConfig = cloneRuntimeConfig(runtimeConfig);
98
+ if (!mutateStringList(nextConfig.filesystem.allowWrite, "add", path)) return null;
99
+ return nextConfig;
100
+ }
101
+
102
+ function appendOutputPostamble(postamble: string, addition: string, output: string): string {
103
+ if (!addition) return postamble;
104
+
105
+ const needsSeparator =
106
+ postamble.length > 0 ? !postamble.endsWith("\n") : output.length > 0 && !output.endsWith("\n");
107
+
108
+ return `${postamble}${needsSeparator ? "\n" : ""}${addition}`;
109
+ }
110
+
111
+ function ensureTrailingNewline(text: string): string {
112
+ if (!text || text.endsWith("\n")) return text;
113
+ return `${text}\n`;
114
+ }
115
+
116
+ interface SandboxedBashOpsOptions {
117
+ getContext: () => ExtensionContext | null;
118
+ getSandboxConfig: () => SandboxConfig | null;
119
+ getRuntimeConfig: () => SandboxRuntimeConfig | null;
120
+ getPromptMode: () => PromptMode;
121
+ applyRuntimeConfigForSession: (
122
+ ctx: ExtensionContext,
123
+ runtimeConfig: SandboxRuntimeConfig,
124
+ ) => void;
125
+ recordEvent?: (event: SandboxEvent) => void;
126
+ }
127
+
128
+ interface SandboxedBashOperations extends BashOperations {
129
+ runSerially<T>(task: () => Promise<T>, signal?: AbortSignal): Promise<T>;
130
+ }
131
+
132
+ interface BashAttemptResult {
133
+ exitCode: number | null;
134
+ combinedOutput: string;
135
+ interruptedByFilesystemViolation: boolean;
136
+ runtimeProtectedWriteViolations: FilesystemViolation[];
137
+ }
138
+
139
+ interface ProcessedSandboxAttempt {
140
+ exitCode: number | null;
141
+ postamble: string;
142
+ resolution: PermissionResolution | null;
143
+ runtimeProtectedWriteViolations: FilesystemViolation[];
144
+ }
145
+
146
+ interface PreparedSandboxAttempt {
147
+ attempt: BashAttemptResult;
148
+ commandId: string;
149
+ runtimeConfig: SandboxRuntimeConfig | null;
150
+ }
151
+
152
+ function killProcessGroup(
153
+ child: ReturnType<typeof spawn>,
154
+ signal: NodeJS.Signals = "SIGKILL",
155
+ ): void {
156
+ if (!child.pid) return;
157
+
158
+ try {
159
+ process.kill(-child.pid, signal);
160
+ } catch {
161
+ try {
162
+ child.kill(signal);
163
+ } catch {
164
+ // Process likely already exited.
165
+ }
166
+ }
167
+ }
168
+
169
+ let sandboxAttemptSequence = 0;
170
+
171
+ function createSandboxCommandId(): string {
172
+ sandboxAttemptSequence += 1;
173
+ return `tau-${process.pid}-${sandboxAttemptSequence}`;
174
+ }
175
+
176
+ function safeCleanupAfterCommand(): void {
177
+ try {
178
+ SandboxManager.cleanupAfterCommand();
179
+ } catch {
180
+ // Ignore cleanup errors.
181
+ }
182
+ }
183
+
184
+ function maybeAllowGitMetadataWriteForSession(options: {
185
+ ctx: ExtensionContext | null;
186
+ cwd: string;
187
+ runtimeConfig: SandboxRuntimeConfig | null;
188
+ allowGitCommonDir: boolean;
189
+ applyRuntimeConfigForSession: (
190
+ ctx: ExtensionContext,
191
+ runtimeConfig: SandboxRuntimeConfig,
192
+ ) => void;
193
+ }): void {
194
+ const { ctx, cwd, runtimeConfig, allowGitCommonDir, applyRuntimeConfigForSession } = options;
195
+ if (!ctx || !runtimeConfig) return;
196
+ if (!isSandboxWritablePath(runtimeConfig, cwd, cwd)) return;
197
+
198
+ const gitPaths = resolveGitFilesystemPaths(cwd);
199
+ if (!gitPaths) return;
200
+
201
+ let nextConfig = maybeAddGitMetadataWritePath(runtimeConfig, gitPaths.gitDir, cwd);
202
+ if (allowGitCommonDir && gitPaths.gitCommonDir !== gitPaths.gitDir) {
203
+ nextConfig =
204
+ maybeAddGitMetadataWritePath(nextConfig ?? runtimeConfig, gitPaths.gitCommonDir, cwd) ??
205
+ nextConfig;
206
+ }
207
+
208
+ if (!nextConfig) return;
209
+ applyRuntimeConfigForSession(ctx, nextConfig);
210
+ }
211
+
212
+ /** Create a single-invocation native backend whose approval covers the resolved spawn context. */
213
+ export function createUnsandboxedBashOps(
214
+ runtime: SandboxRuntime,
215
+ sandboxedOps: SandboxedBashOperations,
216
+ ctx: ExtensionContext,
217
+ invocationSignal: AbortSignal,
218
+ ): BashOperations {
219
+ function assertAvailable(): void {
220
+ if (invocationSignal.aborted) throw new Error("aborted");
221
+ if (runtime.state.status !== "active") {
222
+ throw new Error("Unsandboxed request denied: the sandbox must be active.");
223
+ }
224
+ if (runtime.promptMode !== "interactive" || !ctx.hasUI || !["tui", "rpc"].includes(ctx.mode)) {
225
+ throw new Error("Unsandboxed request denied: interactive human approval is required.");
226
+ }
227
+ if (
228
+ ctx.mode === "rpc" &&
229
+ process.env.TAU_SUBAGENT_CHILD === "1" &&
230
+ process.env.TAU_SUBAGENT_UNSANDBOXED_APPROVAL !== "1"
231
+ ) {
232
+ throw new Error(
233
+ "Unsandboxed request denied: the parent does not advertise a safe approval broker. Reload the parent and start a new subagent before requesting approval.",
234
+ );
235
+ }
236
+ }
237
+
238
+ assertAvailable();
239
+ const local = createLocalBashOperations();
240
+ return {
241
+ exec(command, cwd, options) {
242
+ const absoluteCwd = resolve(cwd);
243
+ return sandboxedOps.runSerially(async () => {
244
+ assertAvailable();
245
+ if (options.signal?.aborted) throw new Error("aborted");
246
+ const title = formatUnsandboxedApproval(command, absoluteCwd);
247
+ let choice: string | undefined;
248
+ try {
249
+ choice = await ctx.ui.select(title, [...UNSANDBOXED_APPROVAL_CHOICES], {
250
+ signal: invocationSignal,
251
+ });
252
+ } catch (error) {
253
+ throw new Error("Unsandboxed request denied: human approval was unavailable.", {
254
+ cause: error,
255
+ });
256
+ }
257
+ assertAvailable();
258
+ if (options.signal?.aborted) throw new Error("aborted");
259
+ if (choice !== "Run once outside sandbox") {
260
+ throw new Error("Unsandboxed request denied: this invocation was not approved.");
261
+ }
262
+ return local.exec(command, absoluteCwd, options);
263
+ }, invocationSignal);
264
+ },
265
+ };
266
+ }
267
+
268
+ export function createSandboxedBashOps(options: SandboxedBashOpsOptions): SandboxedBashOperations {
269
+ const {
270
+ getContext,
271
+ getSandboxConfig,
272
+ getRuntimeConfig,
273
+ getPromptMode,
274
+ applyRuntimeConfigForSession,
275
+ recordEvent,
276
+ } = options;
277
+ const pendingFilesystemDialogs = new Map<string, Promise<PermissionResolution | null>>();
278
+ const pendingMachLookupDialogs = new Map<string, Promise<PermissionResolution | null>>();
279
+
280
+ let executionQueue: Promise<void> = Promise.resolve();
281
+
282
+ function runSerially<T>(task: () => Promise<T>, signal?: AbortSignal): Promise<T> {
283
+ return new Promise<T>((resolve, reject) => {
284
+ let pending = true;
285
+ const cancel = () => {
286
+ if (!pending) return;
287
+ pending = false;
288
+ signal?.removeEventListener("abort", cancel);
289
+ reject(new Error("aborted"));
290
+ };
291
+ if (signal?.aborted) {
292
+ cancel();
293
+ return;
294
+ }
295
+ signal?.addEventListener("abort", cancel, { once: true });
296
+ const start = async () => {
297
+ if (!pending) return;
298
+ pending = false;
299
+ signal?.removeEventListener("abort", cancel);
300
+ try {
301
+ resolve(await task());
302
+ } catch (error) {
303
+ reject(error);
304
+ }
305
+ };
306
+ executionQueue = executionQueue.then(start, start);
307
+ });
308
+ }
309
+
310
+ function withSandboxDefaultEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
311
+ const baseEnv = env ?? process.env;
312
+ if (baseEnv.GIT_OPTIONAL_LOCKS !== undefined) return baseEnv;
313
+ return { ...baseEnv, GIT_OPTIONAL_LOCKS: "0" };
314
+ }
315
+
316
+ async function runSandboxAttempt(
317
+ commandId: string,
318
+ wrappedCommand: string,
319
+ cwd: string,
320
+ runtimeConfig: SandboxRuntimeConfig | null,
321
+ onData: (data: Buffer) => void,
322
+ signal?: AbortSignal,
323
+ timeout?: number,
324
+ env?: NodeJS.ProcessEnv,
325
+ ): Promise<BashAttemptResult> {
326
+ return new Promise((resolve, reject) => {
327
+ const child = spawn("bash", ["-c", wrappedCommand], {
328
+ cwd,
329
+ env: withSandboxDefaultEnv(env),
330
+ detached: true,
331
+ stdio: ["ignore", "pipe", "pipe"],
332
+ });
333
+
334
+ const chunks: Buffer[] = [];
335
+ let timedOut = false;
336
+ let interruptedByFilesystemViolation = false;
337
+ let seenViolationCount = 0;
338
+ const runtimeProtectedWriteViolations = new Map<string, FilesystemViolation>();
339
+ let timeoutHandle: NodeJS.Timeout | undefined;
340
+ let timeoutEscalationHandle: NodeJS.Timeout | undefined;
341
+ let filesystemStopEscalationHandle: NodeJS.Timeout | undefined;
342
+
343
+ const stopForFilesystemViolation = (): void => {
344
+ if (interruptedByFilesystemViolation) return;
345
+
346
+ interruptedByFilesystemViolation = true;
347
+ killProcessGroup(child, "SIGTERM");
348
+ filesystemStopEscalationHandle = setTimeout(() => {
349
+ killProcessGroup(child, "SIGKILL");
350
+ }, 500);
351
+ };
352
+
353
+ // The Linux monitor filters attempts against configured write rules and does not
354
+ // report mandatory runtime denies within already-allowed paths, so runtime-protected
355
+ // continuation remains macOS-only.
356
+ const unsubscribeViolations =
357
+ process.platform !== "darwin"
358
+ ? () => undefined
359
+ : SandboxManager.getSandboxViolationStore().subscribe(() => {
360
+ const violations =
361
+ SandboxManager.getSandboxViolationStore().getViolationsForCommand(commandId);
362
+ if (violations.length <= seenViolationCount) return;
363
+
364
+ const newViolations = violations.slice(seenViolationCount);
365
+ seenViolationCount = violations.length;
366
+
367
+ for (const violation of newViolations) {
368
+ const filesystemViolation = detectFilesystemViolationFromLine(violation.line);
369
+ if (filesystemViolation) {
370
+ if (isTraversalViolation(runtimeConfig, filesystemViolation, cwd)) continue;
371
+
372
+ if (isRuntimeProtectedWriteViolation(runtimeConfig, filesystemViolation, cwd)) {
373
+ if (filesystemViolation.path) {
374
+ runtimeProtectedWriteViolations.set(
375
+ filesystemViolation.path,
376
+ filesystemViolation,
377
+ );
378
+ }
379
+ continue;
380
+ }
381
+
382
+ stopForFilesystemViolation();
383
+ }
384
+ }
385
+ });
386
+
387
+ if (timeout !== undefined && timeout > 0) {
388
+ timeoutHandle = setTimeout(() => {
389
+ timedOut = true;
390
+ killProcessGroup(child, "SIGTERM");
391
+ timeoutEscalationHandle = setTimeout(() => {
392
+ killProcessGroup(child, "SIGKILL");
393
+ }, 2000);
394
+ }, timeout * 1000);
395
+ }
396
+
397
+ child.stdout?.on("data", (data) => {
398
+ chunks.push(data);
399
+ onData(data);
400
+ });
401
+ child.stderr?.on("data", (data) => {
402
+ chunks.push(data);
403
+ onData(data);
404
+ });
405
+
406
+ const onAbort = () => {
407
+ killProcessGroup(child, "SIGKILL");
408
+ };
409
+
410
+ child.on("error", (err) => {
411
+ if (timeoutHandle) clearTimeout(timeoutHandle);
412
+ if (timeoutEscalationHandle) clearTimeout(timeoutEscalationHandle);
413
+ if (filesystemStopEscalationHandle) clearTimeout(filesystemStopEscalationHandle);
414
+ unsubscribeViolations();
415
+ signal?.removeEventListener("abort", onAbort);
416
+ killProcessGroup(child, "SIGKILL");
417
+ reject(err);
418
+ });
419
+
420
+ if (signal) {
421
+ if (signal.aborted) onAbort();
422
+ else signal.addEventListener("abort", onAbort, { once: true });
423
+ }
424
+
425
+ child.on("close", (code) => {
426
+ if (timeoutHandle) clearTimeout(timeoutHandle);
427
+ if (timeoutEscalationHandle) clearTimeout(timeoutEscalationHandle);
428
+ if (filesystemStopEscalationHandle) clearTimeout(filesystemStopEscalationHandle);
429
+ unsubscribeViolations();
430
+ signal?.removeEventListener("abort", onAbort);
431
+
432
+ if (signal?.aborted) {
433
+ reject(new Error("aborted"));
434
+ return;
435
+ }
436
+
437
+ if (timedOut) {
438
+ reject(new Error(`timeout:${timeout}`));
439
+ return;
440
+ }
441
+
442
+ resolve({
443
+ exitCode: interruptedByFilesystemViolation && code === null ? 1 : code,
444
+ combinedOutput: Buffer.concat(chunks).toString("utf-8"),
445
+ interruptedByFilesystemViolation,
446
+ runtimeProtectedWriteViolations: Array.from(runtimeProtectedWriteViolations.values()),
447
+ });
448
+ });
449
+ });
450
+ }
451
+
452
+ function validateTimeout(timeout: number | undefined): void {
453
+ if (timeout === undefined) return;
454
+ if (!Number.isFinite(timeout) || timeout <= 0) {
455
+ throw new Error("Invalid timeout: must be a finite number of seconds");
456
+ }
457
+ if (timeout * 1000 > MAX_TIMEOUT_MS) {
458
+ throw new Error(`Invalid timeout: maximum is ${MAX_TIMEOUT_SECONDS} seconds`);
459
+ }
460
+ }
461
+
462
+ function getRemainingTimeout(timeout: number | undefined, startedAt: number): number | undefined {
463
+ if (timeout === undefined) return undefined;
464
+
465
+ return Math.max(0, timeout - (Date.now() - startedAt) / 1000);
466
+ }
467
+
468
+ function reportPostProcessingError(error: unknown): void {
469
+ const message = `[sandbox] Post-processing error: ${error instanceof Error ? error.message : error}`;
470
+ const ctx = getContext();
471
+ if (ctx) notify(ctx, message, "warning");
472
+ else console.warn(message);
473
+ }
474
+
475
+ async function prepareAndRunSandboxAttempt(options: {
476
+ command: string;
477
+ cwd: string;
478
+ onData: (data: Buffer) => void;
479
+ signal?: AbortSignal;
480
+ timeout?: number;
481
+ env?: NodeJS.ProcessEnv;
482
+ }): Promise<PreparedSandboxAttempt> {
483
+ const { command, cwd, onData, signal, timeout, env } = options;
484
+
485
+ maybeAllowGitMetadataWriteForSession({
486
+ ctx: getContext(),
487
+ cwd,
488
+ runtimeConfig: getRuntimeConfig(),
489
+ allowGitCommonDir: getSandboxConfig()?.filesystem.allowGitCommonDir === true,
490
+ applyRuntimeConfigForSession,
491
+ });
492
+
493
+ const runtimeConfig = getRuntimeConfig();
494
+ const attemptRuntimeConfig = runtimeConfig ? cloneRuntimeConfig(runtimeConfig) : null;
495
+ const commandId = createSandboxCommandId();
496
+
497
+ try {
498
+ const wrappedCommand = await SandboxManager.wrapWithSandbox(
499
+ command,
500
+ IS_MACOS ? MACOS_SANDBOX_SHELL : undefined,
501
+ attemptRuntimeConfig ?? undefined,
502
+ signal,
503
+ { commandId, commandText: command },
504
+ );
505
+ const attempt = await runSandboxAttempt(
506
+ commandId,
507
+ wrappedCommand,
508
+ cwd,
509
+ attemptRuntimeConfig,
510
+ onData,
511
+ signal,
512
+ timeout,
513
+ env,
514
+ );
515
+ return { attempt, commandId, runtimeConfig: attemptRuntimeConfig };
516
+ } catch (err) {
517
+ safeCleanupAfterCommand();
518
+ throw err;
519
+ }
520
+ }
521
+
522
+ async function processSandboxAttempt(options: {
523
+ attempt: BashAttemptResult;
524
+ command: string;
525
+ commandId: string;
526
+ cwd: string;
527
+ runtimeConfig: SandboxRuntimeConfig | null;
528
+ autoRetryAvailable: boolean;
529
+ }): Promise<ProcessedSandboxAttempt> {
530
+ const { attempt, command, commandId, cwd, runtimeConfig, autoRetryAvailable } = options;
531
+ const annotatedOutput = SandboxManager.annotateStderrWithSandboxFailures(
532
+ commandId,
533
+ attempt.combinedOutput,
534
+ );
535
+ // Capture violations delivered after the child closed but before post-processing.
536
+ const storedViolationLines = SandboxManager.getSandboxViolationStore()
537
+ .getViolationsForCommand(commandId)
538
+ .map((violation) => violation.line);
539
+ const storedFilesystemViolations = storedViolationLines
540
+ .map((line) => detectFilesystemViolationFromLine(line))
541
+ .filter((violation): violation is FilesystemViolation => violation !== null);
542
+ const runtimeProtectedWriteViolations = getRuntimeProtectedWriteViolations(
543
+ runtimeConfig,
544
+ [...attempt.runtimeProtectedWriteViolations, ...storedFilesystemViolations],
545
+ cwd,
546
+ );
547
+ const machLookupViolations = detectMachLookupViolations(storedViolationLines);
548
+ let postamble = extractAppendedSandboxAnnotation(attempt.combinedOutput, annotatedOutput);
549
+
550
+ if (runtimeProtectedWriteViolations.length > 0) {
551
+ const notice = formatRuntimeProtectedWriteNotice(
552
+ runtimeProtectedWriteViolations,
553
+ !attempt.interruptedByFilesystemViolation,
554
+ );
555
+ postamble = appendOutputPostamble(postamble, notice, attempt.combinedOutput);
556
+
557
+ for (const violation of runtimeProtectedWriteViolations) {
558
+ recordEvent?.({
559
+ timestamp: Date.now(),
560
+ kind: "filesystem",
561
+ outcome: "blocked",
562
+ reason: "runtime-protected-write",
563
+ target: violation.path,
564
+ command,
565
+ cwd,
566
+ summary: "filesystem write is protected by the sandbox runtime",
567
+ });
568
+ }
569
+ }
570
+
571
+ const commandSucceeded = attempt.exitCode === 0 && !attempt.interruptedByFilesystemViolation;
572
+ if (commandSucceeded) {
573
+ return {
574
+ exitCode: attempt.exitCode,
575
+ postamble,
576
+ resolution: null,
577
+ runtimeProtectedWriteViolations,
578
+ };
579
+ }
580
+
581
+ if (
582
+ /\bsandbox-exec:\s+sandbox_apply:\s+Operation not permitted\b/i.test(attempt.combinedOutput)
583
+ ) {
584
+ const notice =
585
+ "[sandbox] macOS sandbox initialization failed in a child process. An inherited sandbox can cause this.";
586
+ recordEvent?.({
587
+ timestamp: Date.now(),
588
+ kind: "runtime",
589
+ outcome: "blocked",
590
+ reason: "init-failed",
591
+ command,
592
+ cwd,
593
+ summary: "command reported a sandbox initialization failure",
594
+ });
595
+ return {
596
+ exitCode: attempt.exitCode,
597
+ postamble: appendOutputPostamble(postamble, notice, attempt.combinedOutput),
598
+ resolution: null,
599
+ runtimeProtectedWriteViolations,
600
+ };
601
+ }
602
+
603
+ const traversalPaths = getTraversalPaths({
604
+ runtimeConfig,
605
+ output: annotatedOutput,
606
+ cwd,
607
+ });
608
+ const continuedTraversal = machLookupViolations.length === 0 ? traversalPaths : null;
609
+ const effectiveExitCode = continuedTraversal ? 0 : attempt.exitCode;
610
+ let resolution: PermissionResolution | null = null;
611
+
612
+ if (continuedTraversal) {
613
+ const notice = formatTraversalNotice(continuedTraversal);
614
+ postamble = appendOutputPostamble(postamble, notice, attempt.combinedOutput);
615
+ } else {
616
+ const currentRuntimeConfig = getRuntimeConfig();
617
+ if (!currentRuntimeConfig) {
618
+ return {
619
+ exitCode: effectiveExitCode,
620
+ postamble,
621
+ resolution,
622
+ runtimeProtectedWriteViolations,
623
+ };
624
+ }
625
+
626
+ resolution = await handleFilesystemViolation({
627
+ ctx: getContext(),
628
+ promptMode: getPromptMode(),
629
+ runtimeConfig: currentRuntimeConfig,
630
+ getRuntimeConfig,
631
+ output: annotatedOutput,
632
+ rawOutput: attempt.combinedOutput,
633
+ command,
634
+ cwd,
635
+ pendingDialogs: pendingFilesystemDialogs,
636
+ applyRuntimeConfigForSession,
637
+ recordEvent,
638
+ autoRetryAvailable,
639
+ runtimeProtectedWriteViolations,
640
+ allowOutputFallback: machLookupViolations.length === 0,
641
+ });
642
+
643
+ if (!resolution) {
644
+ resolution = await handleMachLookupViolation({
645
+ ctx: getContext(),
646
+ promptMode: getPromptMode(),
647
+ runtimeConfig: currentRuntimeConfig,
648
+ violations: machLookupViolations,
649
+ command,
650
+ cwd,
651
+ pendingDialogs: pendingMachLookupDialogs,
652
+ applyRuntimeConfigForSession,
653
+ recordEvent,
654
+ autoRetryAvailable,
655
+ });
656
+ }
657
+
658
+ if (resolution) {
659
+ postamble = appendOutputPostamble(postamble, resolution.message, attempt.combinedOutput);
660
+ }
661
+ }
662
+
663
+ return {
664
+ exitCode: effectiveExitCode,
665
+ postamble,
666
+ resolution,
667
+ runtimeProtectedWriteViolations,
668
+ };
669
+ }
670
+
671
+ return {
672
+ runSerially,
673
+ async exec(command, cwd, { onData, signal, timeout, env }) {
674
+ validateTimeout(timeout);
675
+ return runSerially(async () => {
676
+ if (!existsSync(cwd)) {
677
+ throw new Error(`Working directory does not exist: ${cwd}`);
678
+ }
679
+
680
+ const attemptStartedAt = Date.now();
681
+ const initialRun = await prepareAndRunSandboxAttempt({
682
+ command,
683
+ cwd,
684
+ onData,
685
+ signal,
686
+ timeout,
687
+ env,
688
+ });
689
+ const retryTimeout = getRemainingTimeout(timeout, attemptStartedAt);
690
+
691
+ let processedAttempt: ProcessedSandboxAttempt;
692
+ try {
693
+ processedAttempt = await processSandboxAttempt({
694
+ attempt: initialRun.attempt,
695
+ command,
696
+ commandId: initialRun.commandId,
697
+ cwd,
698
+ runtimeConfig: initialRun.runtimeConfig,
699
+ autoRetryAvailable: true,
700
+ });
701
+ } catch (postProcessError) {
702
+ reportPostProcessingError(postProcessError);
703
+ safeCleanupAfterCommand();
704
+ return { exitCode: initialRun.attempt.exitCode };
705
+ }
706
+
707
+ const retryResolution = processedAttempt.resolution;
708
+ if (retryResolution?.kind !== "allow-retry") {
709
+ if (processedAttempt.postamble) onData(Buffer.from(processedAttempt.postamble));
710
+ safeCleanupAfterCommand();
711
+ return { exitCode: processedAttempt.exitCode };
712
+ }
713
+
714
+ if (processedAttempt.postamble) {
715
+ onData(Buffer.from(ensureTrailingNewline(processedAttempt.postamble)));
716
+ }
717
+
718
+ initialRun.attempt.combinedOutput = "";
719
+ safeCleanupAfterCommand();
720
+
721
+ if (retryTimeout !== undefined && retryTimeout <= 0) {
722
+ onData(Buffer.from(retryResolution.retrySkippedMessage));
723
+ return { exitCode: processedAttempt.exitCode };
724
+ }
725
+
726
+ const retryRun = await prepareAndRunSandboxAttempt({
727
+ command,
728
+ cwd,
729
+ onData,
730
+ signal,
731
+ timeout: retryTimeout,
732
+ env,
733
+ });
734
+
735
+ let processedRetry: ProcessedSandboxAttempt;
736
+ try {
737
+ processedRetry = await processSandboxAttempt({
738
+ attempt: retryRun.attempt,
739
+ command,
740
+ commandId: retryRun.commandId,
741
+ cwd,
742
+ runtimeConfig: retryRun.runtimeConfig,
743
+ autoRetryAvailable: false,
744
+ });
745
+ } catch (postProcessError) {
746
+ reportPostProcessingError(postProcessError);
747
+ safeCleanupAfterCommand();
748
+ return { exitCode: retryRun.attempt.exitCode };
749
+ }
750
+
751
+ let retryPostamble = processedRetry.postamble;
752
+ if (processedRetry.exitCode === 0) {
753
+ retryPostamble = appendOutputPostamble(
754
+ retryPostamble,
755
+ retryResolution.retrySuccessMessage,
756
+ retryRun.attempt.combinedOutput,
757
+ );
758
+ } else if (
759
+ !processedRetry.resolution &&
760
+ processedRetry.runtimeProtectedWriteViolations.length === 0
761
+ ) {
762
+ retryPostamble = appendOutputPostamble(
763
+ retryPostamble,
764
+ retryResolution.retryFailureMessage,
765
+ retryRun.attempt.combinedOutput,
766
+ );
767
+ }
768
+
769
+ if (retryPostamble) onData(Buffer.from(retryPostamble));
770
+ safeCleanupAfterCommand();
771
+ return { exitCode: processedRetry.exitCode };
772
+ }, signal);
773
+ },
774
+ };
775
+ }