tau-coding-agent 0.1.5 → 0.1.6

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.
@@ -21,7 +21,12 @@
21
21
  * "mode": "interactive",
22
22
  * "network": {
23
23
  * "allowedDomains": ["github.com", "*.github.com"],
24
- * "deniedDomains": []
24
+ * "deniedDomains": [],
25
+ * "allowMachLookup": [
26
+ * "com.apple.dnssd.service",
27
+ * "com.apple.SystemConfiguration.configd",
28
+ * "com.apple.SystemConfiguration.DNSConfiguration"
29
+ * ]
25
30
  * },
26
31
  * "filesystem": {
27
32
  * "denyRead": ["~/.ssh", "~/.aws"],
@@ -50,6 +55,7 @@ import { spawn, spawnSync } from "node:child_process";
50
55
  import { existsSync, readFileSync } from "node:fs";
51
56
  import { homedir, tmpdir } from "node:os";
52
57
  import { join, resolve } from "node:path";
58
+ import { fileURLToPath } from "node:url";
53
59
  import {
54
60
  SandboxManager,
55
61
  type SandboxAskCallback,
@@ -75,6 +81,8 @@ import {
75
81
  // --- Constants ---
76
82
 
77
83
  const DEFAULT_PROMPT_MODE: PromptMode = "interactive";
84
+ const IS_MACOS = process.platform === "darwin";
85
+ const MACOS_SANDBOX_SHELL = fileURLToPath(new URL("./macos-sandbox-shell.mjs", import.meta.url));
78
86
  const MAX_TIMEOUT_MS = 2_147_483_647;
79
87
  const MAX_TIMEOUT_SECONDS = MAX_TIMEOUT_MS / 1000;
80
88
 
@@ -154,6 +162,11 @@ const DEFAULT_CONFIG: SandboxConfig = {
154
162
  deniedDomains: [],
155
163
  allowUnixSockets: ["$SSH_AUTH_SOCK"],
156
164
  allowLocalBinding: true,
165
+ allowMachLookup: [
166
+ "com.apple.dnssd.service",
167
+ "com.apple.SystemConfiguration.configd",
168
+ "com.apple.SystemConfiguration.DNSConfiguration",
169
+ ],
157
170
  },
158
171
  filesystem: {
159
172
  denyRead: ["~/.ssh", "~/.aws", "~/.gnupg"],
@@ -201,7 +214,7 @@ const DEFAULT_CONFIG: SandboxConfig = {
201
214
  allowGitCommonDir: true,
202
215
  },
203
216
  ignoreViolations: {
204
- "*": ["/__pycache__"],
217
+ "*": ["/__pycache__", "mach-lookup com.apple.usymptomsd"],
205
218
  },
206
219
  };
207
220
 
@@ -214,6 +227,20 @@ const ENV_PATH_REFERENCE_PATTERN = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_]
214
227
  // --- Types ---
215
228
 
216
229
  type PromptMode = "interactive" | "non-interactive";
230
+ type ListOp = "add" | "remove";
231
+ type SandboxEventOutcome = "blocked" | "allowed";
232
+ type ViolationResolutionKind = "allow-retry" | "allow-adapt" | "deny";
233
+
234
+ type ViolationResolution =
235
+ | {
236
+ kind: "allow-retry";
237
+ message: string;
238
+ retrySuccessMessage: string;
239
+ retryFailureMessage: string;
240
+ retrySkippedMessage: string;
241
+ }
242
+ | { kind: "allow-adapt"; message: string }
243
+ | { kind: "deny"; message: string };
217
244
 
218
245
  type SandboxBypassReason = "no-sandbox-flag" | "config-disabled" | "missing-dependencies";
219
246
  type SandboxBlockedReason = "unsupported-platform" | "init-failed";
@@ -236,34 +263,32 @@ type SandboxConfig = Omit<SandboxRuntimeConfig, "filesystem"> & {
236
263
  };
237
264
  };
238
265
 
239
- type SandboxEventKind = "filesystem" | "network" | "init" | "runtime";
266
+ type SandboxEventKind = "filesystem" | "network" | "mach-lookup" | "init" | "runtime";
240
267
  type SandboxEventReason =
241
268
  | "explicit-deny-read"
242
269
  | "explicit-deny-write"
243
270
  | "explicit-deny-domain"
244
271
  | "missing-allow-write"
245
272
  | "missing-allowed-domain"
273
+ | "missing-mach-lookup"
246
274
  | "missing-dependencies"
247
275
  | "unsupported-platform"
248
276
  | "init-failed"
277
+ | "runtime-protected-write"
249
278
  | "already-approved-still-failed"
250
279
  | "unknown";
251
- type SandboxEventOutcome = "blocked" | "allowed";
252
280
  type SandboxConfigPathStatus = "loaded" | "parse-error" | "skipped-untrusted";
253
281
  type SandboxConfigPathLabel = "Global" | "Project" | "Override";
254
282
 
255
283
  type PromptStatus = "completed" | "error";
256
284
  type UiLevel = "info" | "warning" | "error";
257
285
 
258
- type ListOp = "add" | "remove";
259
286
  type NetworkList = "allow" | "deny";
260
287
  type FilesystemList = "deny-read" | "allow-write" | "deny-write";
261
288
 
262
289
  type FilesystemViolationKind = "read" | "write" | "unknown";
263
290
  type FilesystemReadAccess = "metadata" | "data" | "unknown";
264
291
  type FilesystemWriteAccess = "unlink" | "unknown";
265
- type FilesystemViolationResolutionKind = "allow-retry" | "allow-adapt" | "deny";
266
-
267
292
  interface SandboxEvent {
268
293
  timestamp: number;
269
294
  kind: SandboxEventKind;
@@ -314,16 +339,9 @@ interface FilesystemViolation {
314
339
  writeAccess?: FilesystemWriteAccess;
315
340
  }
316
341
 
317
- type FilesystemViolationResolution =
318
- | {
319
- kind: "allow-retry";
320
- message: string;
321
- retrySuccessMessage: string;
322
- retryFailureMessage: string;
323
- retrySkippedMessage: string;
324
- }
325
- | { kind: "allow-adapt"; message: string }
326
- | { kind: "deny"; message: string };
342
+ interface MachLookupViolation {
343
+ service: string;
344
+ }
327
345
 
328
346
  // --- Helpers ---
329
347
 
@@ -374,6 +392,19 @@ function notify(ctx: ExtensionContext, text: string, level: UiLevel = "info"): v
374
392
  else console.log(text);
375
393
  }
376
394
 
395
+ function announceSandboxState(pi: ExtensionAPI, ctx: ExtensionContext, enabled: boolean): void {
396
+ const text = `Sandbox ${enabled ? "enabled" : "disabled"}`;
397
+ notify(ctx, text, "info");
398
+ pi.sendMessage(
399
+ {
400
+ customType: "sandbox-state",
401
+ content: text,
402
+ display: false,
403
+ },
404
+ { triggerTurn: false },
405
+ );
406
+ }
407
+
377
408
  function showHelp(ctx: ExtensionContext): void {
378
409
  const lines = [
379
410
  "Usage:",
@@ -383,6 +414,12 @@ function showHelp(ctx: ExtensionContext): void {
383
414
  " /sandbox doctor",
384
415
  " /sandbox mode <interactive|non-interactive>",
385
416
  " /sandbox network <allow|deny> <add|remove> <domain>",
417
+ ...(IS_MACOS
418
+ ? [
419
+ " /sandbox mach-lookup <add|remove> <service>",
420
+ " Service rules support one trailing *; use * for all services.",
421
+ ]
422
+ : []),
386
423
  " /sandbox filesystem <deny-read|allow-write|deny-write> <add|remove> <path>",
387
424
  "",
388
425
  "Startup flags:",
@@ -438,6 +475,7 @@ const SANDBOX_TOP_LEVEL_COMPLETIONS: CommandCompletionOption[] = [
438
475
  { value: "doctor", label: "doctor" },
439
476
  { value: "mode ", label: "mode" },
440
477
  { value: "network ", label: "network" },
478
+ ...(IS_MACOS ? [{ value: "mach-lookup ", label: "mach-lookup" }] : []),
441
479
  { value: "filesystem ", label: "filesystem" },
442
480
  { value: "help", label: "help" },
443
481
  ];
@@ -503,6 +541,37 @@ function getStringValueCompletions(
503
541
  }));
504
542
  }
505
543
 
544
+ function getMachLookupArgumentCompletions(options: {
545
+ tokens: string[];
546
+ endsWithSpace: boolean;
547
+ runtimeConfig: SandboxRuntimeConfig | null;
548
+ }): Array<{ value: string; label: string; description?: string }> | null {
549
+ const { tokens, endsWithSpace, runtimeConfig } = options;
550
+
551
+ if (tokens.length === 1 && endsWithSpace) {
552
+ return getCommandCompletions("mach-lookup ", "", SANDBOX_LIST_OPERATION_COMPLETIONS);
553
+ }
554
+ if (tokens.length === 2 && !endsWithSpace) {
555
+ return getCommandCompletions(
556
+ "mach-lookup ",
557
+ tokens[1] ?? "",
558
+ SANDBOX_LIST_OPERATION_COMPLETIONS,
559
+ );
560
+ }
561
+
562
+ if (tokens[1]?.toLowerCase() !== "remove") return null;
563
+
564
+ const values = runtimeConfig?.network.allowMachLookup ?? [];
565
+ const valueBase = "mach-lookup remove ";
566
+ if (tokens.length === 2 && endsWithSpace) {
567
+ return getStringValueCompletions(valueBase, "", values);
568
+ }
569
+ if (tokens.length === 3 && !endsWithSpace) {
570
+ return getStringValueCompletions(valueBase, tokens[2] ?? "", values);
571
+ }
572
+ return null;
573
+ }
574
+
506
575
  function getSandboxArgumentCompletions(
507
576
  prefix: string,
508
577
  runtimeConfig: SandboxRuntimeConfig | null,
@@ -566,6 +635,10 @@ function getSandboxArgumentCompletions(
566
635
  return null;
567
636
  }
568
637
 
638
+ if (subcommand === "mach-lookup" && IS_MACOS) {
639
+ return getMachLookupArgumentCompletions({ tokens, endsWithSpace, runtimeConfig });
640
+ }
641
+
569
642
  if (subcommand === "filesystem") {
570
643
  if (tokens.length === 1 && endsWithSpace) {
571
644
  return getCommandCompletions("filesystem ", "", SANDBOX_FILESYSTEM_LIST_COMPLETIONS);
@@ -697,6 +770,11 @@ function finalizeConfig(config: SandboxConfig): SandboxConfig {
697
770
  DEFAULT_CONFIG.network.deniedDomains,
698
771
  "network.deniedDomains",
699
772
  ),
773
+ allowMachLookup: coerceStringArray(
774
+ config.network?.allowMachLookup,
775
+ DEFAULT_CONFIG.network.allowMachLookup ?? [],
776
+ "network.allowMachLookup",
777
+ ),
700
778
  allowUnixSockets: coerceOptionalStringArray(
701
779
  config.network?.allowUnixSockets,
702
780
  DEFAULT_CONFIG.network.allowUnixSockets,
@@ -1057,6 +1135,42 @@ function isSandboxWritablePath(
1057
1135
  return inferSandboxRuleMatch(path, runtimeConfig.filesystem.denyWrite, cwd) === null;
1058
1136
  }
1059
1137
 
1138
+ function isRuntimeProtectedWriteViolation(
1139
+ runtimeConfig: SandboxRuntimeConfig | null,
1140
+ violation: FilesystemViolation,
1141
+ cwd?: string,
1142
+ ): boolean {
1143
+ if (
1144
+ !runtimeConfig ||
1145
+ runtimeConfig.filesystem.disabled ||
1146
+ !violation.path ||
1147
+ violation.kind !== "write"
1148
+ ) {
1149
+ return false;
1150
+ }
1151
+
1152
+ return isSandboxWritablePath(runtimeConfig, violation.path, cwd);
1153
+ }
1154
+
1155
+ function getRuntimeProtectedWriteViolations(
1156
+ runtimeConfig: SandboxRuntimeConfig | null,
1157
+ violations: FilesystemViolation[],
1158
+ cwd?: string,
1159
+ ): FilesystemViolation[] {
1160
+ const violationsByPath = new Map<string, FilesystemViolation>();
1161
+
1162
+ for (const violation of violations) {
1163
+ if (!isRuntimeProtectedWriteViolation(runtimeConfig, violation, cwd) || !violation.path) {
1164
+ continue;
1165
+ }
1166
+ if (!violationsByPath.has(violation.path)) {
1167
+ violationsByPath.set(violation.path, violation);
1168
+ }
1169
+ }
1170
+
1171
+ return Array.from(violationsByPath.values());
1172
+ }
1173
+
1060
1174
  function resolveGitFilesystemPaths(cwd: string): GitFilesystemPaths | null {
1061
1175
  if (GIT_FILESYSTEM_PATHS_CACHE.has(cwd)) {
1062
1176
  return GIT_FILESYSTEM_PATHS_CACHE.get(cwd) ?? null;
@@ -1140,8 +1254,8 @@ function extractAppendedSandboxAnnotation(
1140
1254
  : violationLines;
1141
1255
  if (newViolationLines.length === 0) return "";
1142
1256
 
1143
- // Filesystem and network sandbox violations are summarized elsewhere via compact
1144
- // extension messages, so suppress the verbose synthetic annotation block.
1257
+ // Sandbox violations are summarized elsewhere via compact extension messages,
1258
+ // so suppress the verbose synthetic annotation block.
1145
1259
  return "";
1146
1260
  }
1147
1261
 
@@ -1192,6 +1306,57 @@ function extractViolationProcessName(line: string): string | undefined {
1192
1306
  return processName.split("/").pop() || processName;
1193
1307
  }
1194
1308
 
1309
+ function detectMachLookupViolationFromLine(line: string): MachLookupViolation | null {
1310
+ const match = line.match(/\bdeny\(\d+\)\s+mach-lookup\s+([^\s()"'*]+)/i);
1311
+ const service = match?.[1];
1312
+ return service ? { service } : null;
1313
+ }
1314
+
1315
+ function detectMachLookupViolations(lines: string[]): MachLookupViolation[] {
1316
+ const violationsByService = new Map<string, MachLookupViolation>();
1317
+
1318
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
1319
+ const violation = detectMachLookupViolationFromLine(lines[index]);
1320
+ if (violation && !violationsByService.has(violation.service)) {
1321
+ violationsByService.set(violation.service, violation);
1322
+ }
1323
+ }
1324
+
1325
+ return Array.from(violationsByService.values());
1326
+ }
1327
+
1328
+ function isValidMachLookupRule(rule: string): boolean {
1329
+ const trimmed = rule.trim();
1330
+ if (!trimmed || /\s/.test(trimmed)) return false;
1331
+
1332
+ const prefix = trimmed.endsWith("*") ? trimmed.slice(0, -1) : trimmed;
1333
+ return !prefix.includes("*");
1334
+ }
1335
+
1336
+ function matchesMachLookupRule(service: string, rule: string): boolean {
1337
+ if (rule === "*") return true;
1338
+ if (rule.endsWith("*")) return service.startsWith(rule.slice(0, -1));
1339
+ return service === rule;
1340
+ }
1341
+
1342
+ function mutateMachLookupAllowList(
1343
+ runtimeConfig: SandboxRuntimeConfig,
1344
+ op: ListOp,
1345
+ service: string,
1346
+ ): boolean {
1347
+ runtimeConfig.network.allowMachLookup ??= [];
1348
+ return mutateStringList(runtimeConfig.network.allowMachLookup, op, service);
1349
+ }
1350
+
1351
+ function isMachLookupAlreadyAllowed(
1352
+ runtimeConfig: SandboxRuntimeConfig | null,
1353
+ service: string,
1354
+ ): boolean {
1355
+ return (runtimeConfig?.network.allowMachLookup ?? []).some((rule) =>
1356
+ matchesMachLookupRule(service, rule),
1357
+ );
1358
+ }
1359
+
1195
1360
  function detectFilesystemViolationFromLine(line: string): FilesystemViolation | null {
1196
1361
  // Runtime emits concrete op variants (e.g. file-write-create/unlink, file-read-data).
1197
1362
  const lower = line.toLowerCase();
@@ -1225,6 +1390,7 @@ function detectFilesystemViolations(
1225
1390
  output: string,
1226
1391
  fallbackOutput: string = output,
1227
1392
  skipViolationLines = 0,
1393
+ allowOutputFallback = true,
1228
1394
  ): FilesystemViolation[] {
1229
1395
  const violations: FilesystemViolation[] = [];
1230
1396
  const allViolationLines = extractSandboxViolationLines(output);
@@ -1238,7 +1404,7 @@ function detectFilesystemViolations(
1238
1404
  if (violation) violations.push(violation);
1239
1405
  }
1240
1406
 
1241
- if (violations.length > 0) return violations;
1407
+ if (violations.length > 0 || !allowOutputFallback) return violations;
1242
1408
 
1243
1409
  const hasEperm = /\bEPERM\b/i.test(fallbackOutput);
1244
1410
  const hasOperationNotPermitted = /(?:^|\n)[^\n]*Operation not permitted(?:$|\n)/i.test(
@@ -1314,6 +1480,22 @@ function formatTraversalNotice(paths: string[]): string {
1314
1480
  return `[sandbox] Continued after skipping protected ${label}: ${visiblePaths}${suffix}`;
1315
1481
  }
1316
1482
 
1483
+ function formatRuntimeProtectedWriteNotice(
1484
+ violations: FilesystemViolation[],
1485
+ continued: boolean,
1486
+ ): string {
1487
+ const paths = violations
1488
+ .map((violation) => violation.path)
1489
+ .filter((path): path is string => path !== undefined);
1490
+ if (paths.length === 0) return "";
1491
+
1492
+ const visiblePaths = paths.slice(0, 3).join(", ");
1493
+ const suffix = paths.length > 3 ? ", ..." : "";
1494
+ const label = paths.length === 1 ? "write" : "writes";
1495
+ const prefix = continued ? "Continued after blocking" : "Blocked";
1496
+ return `[sandbox] ${prefix} runtime-protected ${label} that sandbox config cannot override: ${visiblePaths}${suffix}`;
1497
+ }
1498
+
1317
1499
  function escapeSlashCommandArg(value: string): string {
1318
1500
  if (/^[a-zA-Z0-9_./:@%+\-~]+$/.test(value)) return value;
1319
1501
  return JSON.stringify(value);
@@ -1434,62 +1616,63 @@ function formatFilesystemViolationSummary(violation: FilesystemViolation): strin
1434
1616
  return "[sandbox] Blocked filesystem access (EPERM).";
1435
1617
  }
1436
1618
 
1437
- const FILESYSTEM_ALLOW_RETRY_OPTION = "Allow and retry now";
1438
- const FILESYSTEM_ALLOW_ADAPT_OPTION = "Allow but adapt for side-effects";
1439
- const FILESYSTEM_DENY_OPTION = "Deny";
1619
+ const VIOLATION_ALLOW_RETRY_OPTION = "Allow and retry now";
1620
+ const VIOLATION_ALLOW_ADAPT_OPTION = "Allow but adapt for side-effects";
1621
+ const VIOLATION_DENY_OPTION = "Deny";
1440
1622
 
1441
- function getFilesystemPromptOptions(
1442
- _violation: FilesystemViolation,
1443
- autoRetryAvailable: boolean,
1444
- ): string[] {
1623
+ function getViolationPromptOptions(autoRetryAvailable: boolean): string[] {
1445
1624
  if (!autoRetryAvailable) {
1446
- return [FILESYSTEM_ALLOW_ADAPT_OPTION, FILESYSTEM_DENY_OPTION];
1625
+ return [VIOLATION_ALLOW_ADAPT_OPTION, VIOLATION_DENY_OPTION];
1447
1626
  }
1448
1627
 
1449
- return [FILESYSTEM_ALLOW_RETRY_OPTION, FILESYSTEM_ALLOW_ADAPT_OPTION, FILESYSTEM_DENY_OPTION];
1628
+ return [VIOLATION_ALLOW_RETRY_OPTION, VIOLATION_ALLOW_ADAPT_OPTION, VIOLATION_DENY_OPTION];
1450
1629
  }
1451
1630
 
1452
- function parseFilesystemPromptSelection(
1631
+ function parseViolationPromptSelection(
1453
1632
  selection: string | undefined,
1454
1633
  autoRetryAvailable: boolean,
1455
- ): FilesystemViolationResolutionKind {
1456
- if (selection === FILESYSTEM_ALLOW_ADAPT_OPTION) return "allow-adapt";
1457
- if (selection === FILESYSTEM_ALLOW_RETRY_OPTION && autoRetryAvailable) return "allow-retry";
1634
+ ): ViolationResolutionKind {
1635
+ if (selection === VIOLATION_ALLOW_ADAPT_OPTION) return "allow-adapt";
1636
+ if (selection === VIOLATION_ALLOW_RETRY_OPTION && autoRetryAvailable) return "allow-retry";
1458
1637
  return "deny";
1459
1638
  }
1460
1639
 
1461
- function formatFilesystemAllowRetryMessage(target: string): string {
1462
- return `\n${formatFilesystemBlockedTarget(target)}\n\nGranting access and retrying the command per user request...\n\n`;
1640
+ function formatViolationAllowRetryMessage(blockedTarget: string): string {
1641
+ return `\n${blockedTarget}\n\nGranting access and retrying the command per user request...\n\n`;
1463
1642
  }
1464
1643
 
1465
- function formatFilesystemAllowAdaptMessage(target: string): string {
1466
- return `\n${formatFilesystemBlockedTarget(target)}\n\nAccess granted for this session. Retry the command manually if appropriate.`;
1644
+ function formatViolationAllowAdaptMessage(blockedTarget: string): string {
1645
+ return `\n${blockedTarget}\n\nAccess granted for this session. Retry the command manually if appropriate.`;
1467
1646
  }
1468
1647
 
1469
- function formatFilesystemDeniedMessage(target: string): string {
1470
- return `\n${formatFilesystemBlockedTarget(target)}\n\nAccess remains denied for this session.`;
1648
+ function formatViolationDeniedMessage(blockedTarget: string): string {
1649
+ return `\n${blockedTarget}\n\nAccess remains denied for this session.`;
1471
1650
  }
1472
1651
 
1473
- function formatFilesystemAlreadyAllowedMessage(target: string): string {
1474
- return `\n${formatFilesystemBlockedTarget(target)}\n\nAccess had already been granted for this session. The remaining failure may be unrelated to sandbox policy.`;
1652
+ function formatViolationAlreadyAllowedMessage(blockedTarget: string): string {
1653
+ return `\n${blockedTarget}\n\nAccess had already been granted for this session. The remaining failure may be unrelated to sandbox policy.`;
1475
1654
  }
1476
1655
 
1477
- function formatFilesystemRetrySucceededMessage(_target: string): string {
1478
- return "";
1656
+ function formatViolationRetrySucceededMessage(blockedTarget: string): string {
1657
+ return `\n${blockedTarget}\n\nAccess granted for this session and the command was retried successfully.`;
1479
1658
  }
1480
1659
 
1481
- function formatFilesystemRetryFailedMessage(target: string): string {
1482
- return `\n${formatFilesystemBlockedTarget(target)}\n\nAccess granted for this session and the command was retried per user request, but the command still exited non-zero. The sandbox block was resolved; the remaining failure may be unrelated.`;
1660
+ function formatViolationRetryFailedMessage(blockedTarget: string): string {
1661
+ return `\n${blockedTarget}\n\nAccess granted for this session and the command was retried per user request, but the command still exited non-zero. The sandbox block was resolved; the remaining failure may be unrelated.`;
1483
1662
  }
1484
1663
 
1485
- function formatFilesystemRetrySkippedMessage(target: string): string {
1486
- return `\n${formatFilesystemBlockedTarget(target)}\n\nAccess granted for this session, but automatic retry was skipped because the timeout was exhausted. Retry the command manually if needed.`;
1664
+ function formatViolationRetrySkippedMessage(blockedTarget: string): string {
1665
+ return `\n${blockedTarget}\n\nAccess granted for this session, but automatic retry was skipped because the timeout was exhausted. Retry the command manually if needed.`;
1487
1666
  }
1488
1667
 
1489
1668
  function formatFilesystemBlockedTarget(target: string): string {
1490
1669
  return `Sandbox blocked filesystem ${target}.`;
1491
1670
  }
1492
1671
 
1672
+ function formatMachLookupBlockedTarget(service: string): string {
1673
+ return `Sandbox blocked access to macOS service ${service}.`;
1674
+ }
1675
+
1493
1676
  function appendOutputPostamble(postamble: string, addition: string, output: string): string {
1494
1677
  if (!addition) return postamble;
1495
1678
 
@@ -1513,7 +1696,7 @@ async function handleFilesystemViolation(options: {
1513
1696
  rawOutput: string;
1514
1697
  command: string;
1515
1698
  cwd?: string;
1516
- pendingPrompts?: Map<string, Promise<FilesystemViolationResolution | null>>;
1699
+ pendingPrompts?: Map<string, Promise<ViolationResolution | null>>;
1517
1700
  applyRuntimeConfigForSession?: (
1518
1701
  ctx: ExtensionContext,
1519
1702
  runtimeConfig: SandboxRuntimeConfig,
@@ -1521,7 +1704,9 @@ async function handleFilesystemViolation(options: {
1521
1704
  existingViolationCount?: number;
1522
1705
  recordEvent?: (event: SandboxEvent) => void;
1523
1706
  autoRetryAvailable?: boolean;
1524
- }): Promise<FilesystemViolationResolution | null> {
1707
+ runtimeProtectedWriteViolations?: FilesystemViolation[];
1708
+ allowOutputFallback?: boolean;
1709
+ }): Promise<ViolationResolution | null> {
1525
1710
  const {
1526
1711
  pi,
1527
1712
  ctx,
@@ -1536,19 +1721,35 @@ async function handleFilesystemViolation(options: {
1536
1721
  existingViolationCount,
1537
1722
  recordEvent,
1538
1723
  autoRetryAvailable = true,
1724
+ runtimeProtectedWriteViolations = [],
1725
+ allowOutputFallback = true,
1539
1726
  } = options;
1540
- const violations = detectFilesystemViolations(output, rawOutput, existingViolationCount ?? 0);
1541
- if (violations.length === 0) return null;
1727
+ const violations = detectFilesystemViolations(
1728
+ output,
1729
+ rawOutput,
1730
+ existingViolationCount ?? 0,
1731
+ allowOutputFallback,
1732
+ );
1733
+ const runtimeProtectedWritePaths = new Set(
1734
+ runtimeProtectedWriteViolations.map((violation) => violation.path).filter(Boolean),
1735
+ );
1736
+ const actionableViolations = violations.filter((violation) => {
1737
+ const isRuntimeProtectedWrite =
1738
+ violation.kind !== "read" && runtimeProtectedWritePaths.has(violation.path);
1739
+ return !isRuntimeProtectedWrite && !isTraversalViolation(runtimeConfig, violation, cwd);
1740
+ });
1741
+ if (actionableViolations.length === 0) return null;
1542
1742
 
1543
1743
  const violation =
1544
- violations.find((candidate) => {
1744
+ actionableViolations.find((candidate) => {
1545
1745
  const candidateAction = buildFilesystemAllowAction(runtimeConfig, candidate, cwd);
1546
1746
  if (!candidateAction) return false;
1547
1747
  return !isFilesystemAllowActionAlreadyApplied(runtimeConfig, candidateAction);
1548
- }) ?? violations[0];
1748
+ }) ?? actionableViolations[0];
1549
1749
 
1550
1750
  const summary = formatFilesystemViolationSummary(violation);
1551
1751
  const target = describeFilesystemViolationTarget(violation);
1752
+ const blockedTarget = formatFilesystemBlockedTarget(target);
1552
1753
  const allowAction = buildFilesystemAllowAction(runtimeConfig, violation, cwd);
1553
1754
  const allowCommand = allowAction ? buildFilesystemAllowCommand(allowAction) : null;
1554
1755
  const alreadyApproved = allowAction
@@ -1587,25 +1788,25 @@ async function handleFilesystemViolation(options: {
1587
1788
 
1588
1789
  if (alreadyApproved) {
1589
1790
  recordFilesystemEvent("blocked");
1590
- return { kind: "allow-adapt", message: formatFilesystemAlreadyAllowedMessage(target) };
1791
+ return { kind: "allow-adapt", message: formatViolationAlreadyAllowedMessage(blockedTarget) };
1591
1792
  }
1592
1793
 
1593
1794
  const promptKey = `${allowCommand}:${autoRetryAvailable ? "retry" : "adapt"}`;
1594
1795
  const existingPrompt = pendingPrompts?.get(promptKey);
1595
1796
  if (existingPrompt) return existingPrompt;
1596
1797
 
1597
- const promptTask: Promise<FilesystemViolationResolution | null> = (async () => {
1798
+ const promptTask: Promise<ViolationResolution | null> = (async () => {
1598
1799
  try {
1599
1800
  const selection = await withPromptSignal(pi, () =>
1600
1801
  ctx.ui.select(
1601
1802
  `Sandbox blocked filesystem ${target}`,
1602
- getFilesystemPromptOptions(violation, autoRetryAvailable),
1803
+ getViolationPromptOptions(autoRetryAvailable),
1603
1804
  ),
1604
1805
  );
1605
- const decision = parseFilesystemPromptSelection(selection, autoRetryAvailable);
1806
+ const decision = parseViolationPromptSelection(selection, autoRetryAvailable);
1606
1807
  if (decision === "deny") {
1607
1808
  recordFilesystemEvent("blocked");
1608
- return { kind: "deny", message: formatFilesystemDeniedMessage(target) };
1809
+ return { kind: "deny", message: formatViolationDeniedMessage(blockedTarget) };
1609
1810
  }
1610
1811
 
1611
1812
  const nextConfig = cloneRuntimeConfig(runtimeConfig);
@@ -1619,18 +1820,163 @@ async function handleFilesystemViolation(options: {
1619
1820
  if (decision === "allow-retry") {
1620
1821
  return {
1621
1822
  kind: "allow-retry",
1622
- message: formatFilesystemAllowRetryMessage(target),
1623
- retrySuccessMessage: formatFilesystemRetrySucceededMessage(target),
1624
- retryFailureMessage: formatFilesystemRetryFailedMessage(target),
1625
- retrySkippedMessage: formatFilesystemRetrySkippedMessage(target),
1823
+ message: formatViolationAllowRetryMessage(blockedTarget),
1824
+ retrySuccessMessage: formatViolationRetrySucceededMessage(blockedTarget),
1825
+ retryFailureMessage: formatViolationRetryFailedMessage(blockedTarget),
1826
+ retrySkippedMessage: formatViolationRetrySkippedMessage(blockedTarget),
1626
1827
  };
1627
1828
  }
1628
1829
 
1629
1830
  return {
1630
1831
  kind: "allow-adapt",
1631
1832
  message: changed
1632
- ? formatFilesystemAllowAdaptMessage(target)
1633
- : formatFilesystemAlreadyAllowedMessage(target),
1833
+ ? formatViolationAllowAdaptMessage(blockedTarget)
1834
+ : formatViolationAlreadyAllowedMessage(blockedTarget),
1835
+ };
1836
+ } catch {
1837
+ return null;
1838
+ }
1839
+ })();
1840
+
1841
+ if (!pendingPrompts) return promptTask;
1842
+
1843
+ pendingPrompts.set(promptKey, promptTask);
1844
+ try {
1845
+ return await promptTask;
1846
+ } finally {
1847
+ pendingPrompts.delete(promptKey);
1848
+ }
1849
+ }
1850
+
1851
+ function buildMachLookupAllowCommand(service: string): string {
1852
+ return `/sandbox mach-lookup add ${escapeSlashCommandArg(service)}`;
1853
+ }
1854
+
1855
+ function formatMachLookupViolationSummary(service: string): string {
1856
+ return `[sandbox] Blocked macOS service lookup: ${service}`;
1857
+ }
1858
+
1859
+ function describeMachLookupEventSummary(
1860
+ reason: SandboxEventReason,
1861
+ outcome: SandboxEventOutcome,
1862
+ ): string {
1863
+ if (outcome === "allowed") return "user allowed macOS service lookup for this session";
1864
+ if (reason === "already-approved-still-failed") {
1865
+ return "macOS service lookup was previously allowed for this session but is still failing";
1866
+ }
1867
+ return "macOS service lookup is not in the allowed service list";
1868
+ }
1869
+
1870
+ async function handleMachLookupViolation(options: {
1871
+ pi: ExtensionAPI;
1872
+ ctx: ExtensionContext | null;
1873
+ promptMode: PromptMode;
1874
+ runtimeConfig: SandboxRuntimeConfig;
1875
+ violations: MachLookupViolation[];
1876
+ command: string;
1877
+ cwd?: string;
1878
+ pendingPrompts?: Map<string, Promise<ViolationResolution | null>>;
1879
+ applyRuntimeConfigForSession?: (
1880
+ ctx: ExtensionContext,
1881
+ runtimeConfig: SandboxRuntimeConfig,
1882
+ ) => void;
1883
+ recordEvent?: (event: SandboxEvent) => void;
1884
+ autoRetryAvailable?: boolean;
1885
+ }): Promise<ViolationResolution | null> {
1886
+ const {
1887
+ pi,
1888
+ ctx,
1889
+ promptMode,
1890
+ runtimeConfig,
1891
+ violations,
1892
+ command,
1893
+ cwd,
1894
+ pendingPrompts,
1895
+ applyRuntimeConfigForSession,
1896
+ recordEvent,
1897
+ autoRetryAvailable = true,
1898
+ } = options;
1899
+ if (violations.length === 0) return null;
1900
+
1901
+ const violation =
1902
+ violations.find((candidate) => !isMachLookupAlreadyAllowed(runtimeConfig, candidate.service)) ??
1903
+ violations[0];
1904
+ const { service } = violation;
1905
+ const blockedTarget = formatMachLookupBlockedTarget(service);
1906
+ const allowCommand = buildMachLookupAllowCommand(service);
1907
+ const alreadyApproved = isMachLookupAlreadyAllowed(runtimeConfig, service);
1908
+ const eventReason: SandboxEventReason = alreadyApproved
1909
+ ? "already-approved-still-failed"
1910
+ : "missing-mach-lookup";
1911
+
1912
+ const recordMachLookupEvent = (outcome: SandboxEventOutcome): void => {
1913
+ recordEvent?.({
1914
+ timestamp: Date.now(),
1915
+ kind: "mach-lookup",
1916
+ outcome,
1917
+ reason: eventReason,
1918
+ target: service,
1919
+ command,
1920
+ cwd,
1921
+ summary: describeMachLookupEventSummary(eventReason, outcome),
1922
+ suggestedCommand: outcome === "blocked" && !alreadyApproved ? allowCommand : undefined,
1923
+ });
1924
+ };
1925
+
1926
+ if (promptMode === "non-interactive" || !ctx?.hasUI) {
1927
+ recordMachLookupEvent("blocked");
1928
+ return {
1929
+ kind: "deny",
1930
+ message: `${formatMachLookupViolationSummary(service)}\n[sandbox] To temporarily allow for this session, run: ${allowCommand}`,
1931
+ };
1932
+ }
1933
+
1934
+ if (alreadyApproved) {
1935
+ recordMachLookupEvent("blocked");
1936
+ return { kind: "allow-adapt", message: formatViolationAlreadyAllowedMessage(blockedTarget) };
1937
+ }
1938
+
1939
+ const promptKey = `${allowCommand}:${autoRetryAvailable ? "retry" : "adapt"}`;
1940
+ const existingPrompt = pendingPrompts?.get(promptKey);
1941
+ if (existingPrompt) return existingPrompt;
1942
+
1943
+ const promptTask: Promise<ViolationResolution | null> = (async () => {
1944
+ try {
1945
+ const selection = await withPromptSignal(pi, () =>
1946
+ ctx.ui.select(
1947
+ `Sandbox blocked access to macOS service ${service}`,
1948
+ getViolationPromptOptions(autoRetryAvailable),
1949
+ ),
1950
+ );
1951
+ const decision = parseViolationPromptSelection(selection, autoRetryAvailable);
1952
+ if (decision === "deny") {
1953
+ recordMachLookupEvent("blocked");
1954
+ return { kind: "deny", message: formatViolationDeniedMessage(blockedTarget) };
1955
+ }
1956
+
1957
+ const nextConfig = cloneRuntimeConfig(runtimeConfig);
1958
+ const changed = mutateMachLookupAllowList(nextConfig, "add", service);
1959
+ if (changed) {
1960
+ applyRuntimeConfigForSession?.(ctx, nextConfig);
1961
+ }
1962
+
1963
+ recordMachLookupEvent("allowed");
1964
+
1965
+ if (decision === "allow-retry") {
1966
+ return {
1967
+ kind: "allow-retry",
1968
+ message: formatViolationAllowRetryMessage(blockedTarget),
1969
+ retrySuccessMessage: formatViolationRetrySucceededMessage(blockedTarget),
1970
+ retryFailureMessage: formatViolationRetryFailedMessage(blockedTarget),
1971
+ retrySkippedMessage: formatViolationRetrySkippedMessage(blockedTarget),
1972
+ };
1973
+ }
1974
+
1975
+ return {
1976
+ kind: "allow-adapt",
1977
+ message: changed
1978
+ ? formatViolationAllowAdaptMessage(blockedTarget)
1979
+ : formatViolationAlreadyAllowedMessage(blockedTarget),
1634
1980
  };
1635
1981
  } catch {
1636
1982
  return null;
@@ -1664,17 +2010,20 @@ interface BashAttemptResult {
1664
2010
  exitCode: number | null;
1665
2011
  combinedOutput: string;
1666
2012
  interruptedByFilesystemViolation: boolean;
2013
+ runtimeProtectedWriteViolations: FilesystemViolation[];
1667
2014
  }
1668
2015
 
1669
2016
  interface ProcessedSandboxAttempt {
1670
2017
  exitCode: number | null;
1671
2018
  postamble: string;
1672
- resolution: FilesystemViolationResolution | null;
2019
+ resolution: ViolationResolution | null;
2020
+ runtimeProtectedWriteViolations: FilesystemViolation[];
1673
2021
  }
1674
2022
 
1675
2023
  interface PreparedSandboxAttempt {
1676
2024
  attempt: BashAttemptResult;
1677
2025
  existingViolationCount: number;
2026
+ runtimeConfig: SandboxRuntimeConfig | null;
1678
2027
  }
1679
2028
 
1680
2029
  function killProcessGroup(
@@ -1740,7 +2089,8 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
1740
2089
  applyRuntimeConfigForSession,
1741
2090
  recordEvent,
1742
2091
  } = options;
1743
- const pendingFilesystemPrompts = new Map<string, Promise<FilesystemViolationResolution | null>>();
2092
+ const pendingFilesystemPrompts = new Map<string, Promise<ViolationResolution | null>>();
2093
+ const pendingMachLookupPrompts = new Map<string, Promise<ViolationResolution | null>>();
1744
2094
 
1745
2095
  let executionQueue: Promise<void> = Promise.resolve();
1746
2096
 
@@ -1763,6 +2113,7 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
1763
2113
  command: string,
1764
2114
  wrappedCommand: string,
1765
2115
  cwd: string,
2116
+ runtimeConfig: SandboxRuntimeConfig | null,
1766
2117
  onData: (data: Buffer) => void,
1767
2118
  existingViolationCount: number,
1768
2119
  signal?: AbortSignal,
@@ -1781,6 +2132,7 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
1781
2132
  let timedOut = false;
1782
2133
  let interruptedByFilesystemViolation = false;
1783
2134
  let seenViolationCount = existingViolationCount;
2135
+ const runtimeProtectedWriteViolations = new Map<string, FilesystemViolation>();
1784
2136
  let timeoutHandle: NodeJS.Timeout | undefined;
1785
2137
  let timeoutEscalationHandle: NodeJS.Timeout | undefined;
1786
2138
  let filesystemStopEscalationHandle: NodeJS.Timeout | undefined;
@@ -1795,11 +2147,9 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
1795
2147
  }, 500);
1796
2148
  };
1797
2149
 
1798
- // sandbox-runtime only provides live filesystem violation events on macOS.
1799
- // Upstream documents Linux violation monitoring as future work via
1800
- // automatic strace-based detection integrated with the violation store,
1801
- // but there is no Linux implementation yet:
1802
- // https://github.com/anthropic-experimental/sandbox-runtime#known-limitations-and-future-work
2150
+ // The Linux monitor filters attempts against configured write rules and does not
2151
+ // report mandatory runtime denies within already-allowed paths, so runtime-protected
2152
+ // continuation remains macOS-only.
1803
2153
  const unsubscribeViolations =
1804
2154
  process.platform !== "darwin"
1805
2155
  ? () => undefined
@@ -1811,14 +2161,23 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
1811
2161
  const newViolations = violations.slice(seenViolationCount);
1812
2162
  seenViolationCount = violations.length;
1813
2163
 
1814
- const runtimeConfig = getRuntimeConfig();
1815
- const shouldStop = newViolations.some((violation) => {
2164
+ for (const violation of newViolations) {
1816
2165
  const filesystemViolation = detectFilesystemViolationFromLine(violation.line);
1817
- if (!filesystemViolation) return false;
1818
- return !isTraversalViolation(runtimeConfig, filesystemViolation, cwd);
1819
- });
1820
- if (shouldStop) {
1821
- stopForFilesystemViolation();
2166
+ if (filesystemViolation) {
2167
+ if (isTraversalViolation(runtimeConfig, filesystemViolation, cwd)) continue;
2168
+
2169
+ if (isRuntimeProtectedWriteViolation(runtimeConfig, filesystemViolation, cwd)) {
2170
+ if (filesystemViolation.path) {
2171
+ runtimeProtectedWriteViolations.set(
2172
+ filesystemViolation.path,
2173
+ filesystemViolation,
2174
+ );
2175
+ }
2176
+ continue;
2177
+ }
2178
+
2179
+ stopForFilesystemViolation();
2180
+ }
1822
2181
  }
1823
2182
  });
1824
2183
 
@@ -1881,6 +2240,7 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
1881
2240
  exitCode: interruptedByFilesystemViolation && code === null ? 1 : code,
1882
2241
  combinedOutput: Buffer.concat(chunks).toString("utf-8"),
1883
2242
  interruptedByFilesystemViolation,
2243
+ runtimeProtectedWriteViolations: Array.from(runtimeProtectedWriteViolations.values()),
1884
2244
  });
1885
2245
  });
1886
2246
  });
@@ -1928,7 +2288,13 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
1928
2288
  applyRuntimeConfigForSession,
1929
2289
  });
1930
2290
 
1931
- const wrappedCommand = await SandboxManager.wrapWithSandbox(command);
2291
+ const runtimeConfig = getRuntimeConfig();
2292
+ const attemptRuntimeConfig = runtimeConfig ? cloneRuntimeConfig(runtimeConfig) : null;
2293
+ const wrappedCommand = await SandboxManager.wrapWithSandbox(
2294
+ command,
2295
+ IS_MACOS ? MACOS_SANDBOX_SHELL : undefined,
2296
+ attemptRuntimeConfig ?? undefined,
2297
+ );
1932
2298
  const existingViolationCount =
1933
2299
  SandboxManager.getSandboxViolationStore().getViolationsForCommand(command).length;
1934
2300
 
@@ -1937,13 +2303,14 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
1937
2303
  command,
1938
2304
  wrappedCommand,
1939
2305
  cwd,
2306
+ attemptRuntimeConfig,
1940
2307
  onData,
1941
2308
  existingViolationCount,
1942
2309
  signal,
1943
2310
  timeout,
1944
2311
  env,
1945
2312
  );
1946
- return { attempt, existingViolationCount };
2313
+ return { attempt, existingViolationCount, runtimeConfig: attemptRuntimeConfig };
1947
2314
  } catch (err) {
1948
2315
  safeCleanupAfterCommand();
1949
2316
  throw err;
@@ -1955,42 +2322,95 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
1955
2322
  command: string;
1956
2323
  cwd: string;
1957
2324
  existingViolationCount: number;
2325
+ runtimeConfig: SandboxRuntimeConfig | null;
1958
2326
  autoRetryAvailable: boolean;
1959
2327
  }): Promise<ProcessedSandboxAttempt> {
1960
- const { attempt, command, cwd, existingViolationCount, autoRetryAvailable } = options;
1961
- const commandSucceeded = attempt.exitCode === 0 && !attempt.interruptedByFilesystemViolation;
1962
- if (commandSucceeded) {
1963
- return { exitCode: attempt.exitCode, postamble: "", resolution: null };
1964
- }
1965
-
2328
+ const { attempt, command, cwd, existingViolationCount, runtimeConfig, autoRetryAvailable } =
2329
+ options;
1966
2330
  const annotatedOutput = SandboxManager.annotateStderrWithSandboxFailures(
1967
2331
  command,
1968
2332
  attempt.combinedOutput,
1969
2333
  );
1970
- const runtimeConfig = getRuntimeConfig();
1971
- const traversalPaths = getTraversalPaths({
2334
+ // Capture violations delivered after the child closed but before post-processing.
2335
+ const storedViolationLines = SandboxManager.getSandboxViolationStore()
2336
+ .getViolationsForCommand(command)
2337
+ .slice(existingViolationCount)
2338
+ .map((violation) => violation.line);
2339
+ const storedFilesystemViolations = storedViolationLines
2340
+ .map((line) => detectFilesystemViolationFromLine(line))
2341
+ .filter((violation): violation is FilesystemViolation => violation !== null);
2342
+ const runtimeProtectedWriteViolations = getRuntimeProtectedWriteViolations(
1972
2343
  runtimeConfig,
1973
- output: annotatedOutput,
2344
+ [...attempt.runtimeProtectedWriteViolations, ...storedFilesystemViolations],
1974
2345
  cwd,
1975
- skipViolationLines: existingViolationCount,
1976
- });
1977
- const effectiveExitCode = traversalPaths ? 0 : attempt.exitCode;
2346
+ );
2347
+ const machLookupViolations = detectMachLookupViolations(storedViolationLines);
1978
2348
  let postamble = extractAppendedSandboxAnnotation(
1979
2349
  attempt.combinedOutput,
1980
2350
  annotatedOutput,
1981
2351
  existingViolationCount,
1982
2352
  );
1983
- let resolution: FilesystemViolationResolution | null = null;
1984
2353
 
1985
- if (traversalPaths) {
1986
- const notice = formatTraversalNotice(traversalPaths);
2354
+ if (runtimeProtectedWriteViolations.length > 0) {
2355
+ const notice = formatRuntimeProtectedWriteNotice(
2356
+ runtimeProtectedWriteViolations,
2357
+ !attempt.interruptedByFilesystemViolation,
2358
+ );
1987
2359
  postamble = appendOutputPostamble(postamble, notice, attempt.combinedOutput);
1988
- } else if (runtimeConfig) {
2360
+
2361
+ for (const violation of runtimeProtectedWriteViolations) {
2362
+ recordEvent?.({
2363
+ timestamp: Date.now(),
2364
+ kind: "filesystem",
2365
+ outcome: "blocked",
2366
+ reason: "runtime-protected-write",
2367
+ target: violation.path,
2368
+ command,
2369
+ cwd,
2370
+ summary: "filesystem write is protected by the sandbox runtime",
2371
+ });
2372
+ }
2373
+ }
2374
+
2375
+ const commandSucceeded = attempt.exitCode === 0 && !attempt.interruptedByFilesystemViolation;
2376
+ if (commandSucceeded) {
2377
+ return {
2378
+ exitCode: attempt.exitCode,
2379
+ postamble,
2380
+ resolution: null,
2381
+ runtimeProtectedWriteViolations,
2382
+ };
2383
+ }
2384
+
2385
+ const traversalPaths = getTraversalPaths({
2386
+ runtimeConfig,
2387
+ output: annotatedOutput,
2388
+ cwd,
2389
+ skipViolationLines: existingViolationCount,
2390
+ });
2391
+ const continuedTraversal = machLookupViolations.length === 0 ? traversalPaths : null;
2392
+ const effectiveExitCode = continuedTraversal ? 0 : attempt.exitCode;
2393
+ let resolution: ViolationResolution | null = null;
2394
+
2395
+ if (continuedTraversal) {
2396
+ const notice = formatTraversalNotice(continuedTraversal);
2397
+ postamble = appendOutputPostamble(postamble, notice, attempt.combinedOutput);
2398
+ } else {
2399
+ const currentRuntimeConfig = getRuntimeConfig();
2400
+ if (!currentRuntimeConfig) {
2401
+ return {
2402
+ exitCode: effectiveExitCode,
2403
+ postamble,
2404
+ resolution,
2405
+ runtimeProtectedWriteViolations,
2406
+ };
2407
+ }
2408
+
1989
2409
  resolution = await handleFilesystemViolation({
1990
2410
  pi,
1991
2411
  ctx: getContext(),
1992
2412
  promptMode: getPromptMode(),
1993
- runtimeConfig,
2413
+ runtimeConfig: currentRuntimeConfig,
1994
2414
  output: annotatedOutput,
1995
2415
  rawOutput: attempt.combinedOutput,
1996
2416
  command,
@@ -2000,14 +2420,37 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
2000
2420
  existingViolationCount,
2001
2421
  recordEvent,
2002
2422
  autoRetryAvailable,
2423
+ runtimeProtectedWriteViolations,
2424
+ allowOutputFallback: machLookupViolations.length === 0,
2003
2425
  });
2004
2426
 
2427
+ if (!resolution) {
2428
+ resolution = await handleMachLookupViolation({
2429
+ pi,
2430
+ ctx: getContext(),
2431
+ promptMode: getPromptMode(),
2432
+ runtimeConfig: currentRuntimeConfig,
2433
+ violations: machLookupViolations,
2434
+ command,
2435
+ cwd,
2436
+ pendingPrompts: pendingMachLookupPrompts,
2437
+ applyRuntimeConfigForSession,
2438
+ recordEvent,
2439
+ autoRetryAvailable,
2440
+ });
2441
+ }
2442
+
2005
2443
  if (resolution) {
2006
2444
  postamble = appendOutputPostamble(postamble, resolution.message, attempt.combinedOutput);
2007
2445
  }
2008
2446
  }
2009
2447
 
2010
- return { exitCode: effectiveExitCode, postamble, resolution };
2448
+ return {
2449
+ exitCode: effectiveExitCode,
2450
+ postamble,
2451
+ resolution,
2452
+ runtimeProtectedWriteViolations,
2453
+ };
2011
2454
  }
2012
2455
 
2013
2456
  return {
@@ -2035,6 +2478,7 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
2035
2478
  command,
2036
2479
  cwd,
2037
2480
  existingViolationCount: initialRun.existingViolationCount,
2481
+ runtimeConfig: initialRun.runtimeConfig,
2038
2482
  autoRetryAvailable: true,
2039
2483
  });
2040
2484
  } catch (postProcessError) {
@@ -2079,6 +2523,7 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
2079
2523
  command,
2080
2524
  cwd,
2081
2525
  existingViolationCount: retryRun.existingViolationCount,
2526
+ runtimeConfig: retryRun.runtimeConfig,
2082
2527
  autoRetryAvailable: false,
2083
2528
  });
2084
2529
  } catch (postProcessError) {
@@ -2094,7 +2539,10 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
2094
2539
  retryResolution.retrySuccessMessage,
2095
2540
  retryRun.attempt.combinedOutput,
2096
2541
  );
2097
- } else if (!processedRetry.resolution) {
2542
+ } else if (
2543
+ !processedRetry.resolution &&
2544
+ processedRetry.runtimeProtectedWriteViolations.length === 0
2545
+ ) {
2098
2546
  retryPostamble = appendOutputPostamble(
2099
2547
  retryPostamble,
2100
2548
  retryResolution.retryFailureMessage,
@@ -2185,6 +2633,9 @@ function describeFilesystemEventSummary(
2185
2633
 
2186
2634
  if (reason === "explicit-deny-read") return "filesystem read matched a deny-read rule";
2187
2635
  if (reason === "explicit-deny-write") return "filesystem write matched a deny-write rule";
2636
+ if (reason === "runtime-protected-write") {
2637
+ return "filesystem write is protected by the sandbox runtime";
2638
+ }
2188
2639
  if (reason === "already-approved-still-failed") {
2189
2640
  return "filesystem access was previously allowed for this session but is still failing";
2190
2641
  }
@@ -2767,7 +3218,7 @@ export default function (pi: ExtensionAPI) {
2767
3218
 
2768
3219
  setSandboxStatus(ctx, true, runtimeConfig, promptMode);
2769
3220
 
2770
- notify(ctx, "Sandbox enabled", "info");
3221
+ announceSandboxState(pi, ctx, true);
2771
3222
  return;
2772
3223
  }
2773
3224
 
@@ -2797,7 +3248,7 @@ export default function (pi: ExtensionAPI) {
2797
3248
  return;
2798
3249
  }
2799
3250
 
2800
- notify(ctx, "Sandbox disabled", "info");
3251
+ announceSandboxState(pi, ctx, false);
2801
3252
  return;
2802
3253
  }
2803
3254
 
@@ -2825,6 +3276,13 @@ export default function (pi: ExtensionAPI) {
2825
3276
  ` allowLocalBinding: ${runtimeConfig.network.allowLocalBinding ? "true" : "false"}`,
2826
3277
  ` allowAllUnixSockets: ${runtimeConfig.network.allowAllUnixSockets ? "true" : "false"}`,
2827
3278
  ` allowUnixSockets: ${runtimeConfig.network.allowUnixSockets?.join(", ") || "(none)"}`,
3279
+ ...(IS_MACOS
3280
+ ? [
3281
+ "",
3282
+ " macOS service lookup (mach-lookup):",
3283
+ ` Allowed: ${runtimeConfig.network.allowMachLookup?.join(", ") || "(none)"}`,
3284
+ ]
3285
+ : []),
2828
3286
  "",
2829
3287
  " Filesystem:",
2830
3288
  ` Deny Read: ${runtimeConfig.filesystem.denyRead.join(", ") || "(none)"}`,
@@ -2903,6 +3361,42 @@ export default function (pi: ExtensionAPI) {
2903
3361
  return;
2904
3362
  }
2905
3363
 
3364
+ if (subcommand === "mach-lookup") {
3365
+ if (!IS_MACOS) {
3366
+ notify(ctx, "Mach service lookup controls are only available on macOS.", "warning");
3367
+ return;
3368
+ }
3369
+
3370
+ const runtimeConfig = requireRuntimeConfig(ctx, sandboxState);
3371
+ if (!runtimeConfig) return;
3372
+
3373
+ const op = tokens[1]?.toLowerCase() as ListOp | undefined;
3374
+ const service = tokens[2]?.trim() ?? "";
3375
+
3376
+ if (
3377
+ (op !== "add" && op !== "remove") ||
3378
+ tokens.length !== 3 ||
3379
+ !isValidMachLookupRule(service)
3380
+ ) {
3381
+ notify(ctx, "Usage: /sandbox mach-lookup <add|remove> <service>", "warning");
3382
+ return;
3383
+ }
3384
+
3385
+ const nextConfig = cloneRuntimeConfig(runtimeConfig);
3386
+ const changed = mutateMachLookupAllowList(nextConfig, op, service);
3387
+ if (!changed) {
3388
+ notify(
3389
+ ctx,
3390
+ `No change: mach-lookup allow list already ${op === "add" ? "contains" : "omits"} ${service}`,
3391
+ );
3392
+ return;
3393
+ }
3394
+
3395
+ applyRuntimeConfigForSession(ctx, nextConfig);
3396
+ notify(ctx, `Updated mach-lookup allow list (${op}: ${service})`, "info");
3397
+ return;
3398
+ }
3399
+
2906
3400
  if (subcommand === "filesystem") {
2907
3401
  const runtimeConfig = requireRuntimeConfig(ctx, sandboxState);
2908
3402
  if (!runtimeConfig) return;