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.
- package/extensions/answer.ts +11 -5
- package/extensions/btw.ts +73 -2
- package/extensions/insights.ts +38 -1
- package/extensions/loop.ts +14 -3
- package/extensions/memory.ts +37 -14
- package/extensions/review/models.ts +1 -1
- package/extensions/review/prompts.ts +4 -4
- package/extensions/review/submit-review-tool.ts +1 -5
- package/extensions/review/submit-triage-tool.ts +1 -0
- package/extensions/sandbox/index.ts +595 -101
- package/extensions/sandbox/macos-sandbox-shell.mjs +202 -0
- package/extensions/usage/anthropic.ts +5 -6
- package/extensions/usage/github-copilot.ts +30 -9
- package/extensions/usage/index.ts +1 -1
- package/extensions/usage/openai-codex.ts +2 -2
- package/extensions/websearch/providers/pi-model.shared.ts +19 -1
- package/package.json +2 -2
- package/skills/oracle/SKILL.md +3 -3
- package/skills/oracle/scripts/oracle +54 -50
|
@@ -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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
//
|
|
1144
|
-
//
|
|
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
|
|
1438
|
-
const
|
|
1439
|
-
const
|
|
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
|
|
1442
|
-
_violation: FilesystemViolation,
|
|
1443
|
-
autoRetryAvailable: boolean,
|
|
1444
|
-
): string[] {
|
|
1623
|
+
function getViolationPromptOptions(autoRetryAvailable: boolean): string[] {
|
|
1445
1624
|
if (!autoRetryAvailable) {
|
|
1446
|
-
return [
|
|
1625
|
+
return [VIOLATION_ALLOW_ADAPT_OPTION, VIOLATION_DENY_OPTION];
|
|
1447
1626
|
}
|
|
1448
1627
|
|
|
1449
|
-
return [
|
|
1628
|
+
return [VIOLATION_ALLOW_RETRY_OPTION, VIOLATION_ALLOW_ADAPT_OPTION, VIOLATION_DENY_OPTION];
|
|
1450
1629
|
}
|
|
1451
1630
|
|
|
1452
|
-
function
|
|
1631
|
+
function parseViolationPromptSelection(
|
|
1453
1632
|
selection: string | undefined,
|
|
1454
1633
|
autoRetryAvailable: boolean,
|
|
1455
|
-
):
|
|
1456
|
-
if (selection ===
|
|
1457
|
-
if (selection ===
|
|
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
|
|
1462
|
-
return `\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
|
|
1466
|
-
return `\n${
|
|
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
|
|
1470
|
-
return `\n${
|
|
1648
|
+
function formatViolationDeniedMessage(blockedTarget: string): string {
|
|
1649
|
+
return `\n${blockedTarget}\n\nAccess remains denied for this session.`;
|
|
1471
1650
|
}
|
|
1472
1651
|
|
|
1473
|
-
function
|
|
1474
|
-
return `\n${
|
|
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
|
|
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
|
|
1482
|
-
return `\n${
|
|
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
|
|
1486
|
-
return `\n${
|
|
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<
|
|
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
|
-
|
|
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(
|
|
1541
|
-
|
|
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
|
-
|
|
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
|
-
}) ??
|
|
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:
|
|
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<
|
|
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
|
-
|
|
1803
|
+
getViolationPromptOptions(autoRetryAvailable),
|
|
1603
1804
|
),
|
|
1604
1805
|
);
|
|
1605
|
-
const decision =
|
|
1806
|
+
const decision = parseViolationPromptSelection(selection, autoRetryAvailable);
|
|
1606
1807
|
if (decision === "deny") {
|
|
1607
1808
|
recordFilesystemEvent("blocked");
|
|
1608
|
-
return { kind: "deny", message:
|
|
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:
|
|
1623
|
-
retrySuccessMessage:
|
|
1624
|
-
retryFailureMessage:
|
|
1625
|
-
retrySkippedMessage:
|
|
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
|
-
?
|
|
1633
|
-
:
|
|
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:
|
|
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<
|
|
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
|
-
//
|
|
1799
|
-
//
|
|
1800
|
-
//
|
|
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
|
|
1815
|
-
const shouldStop = newViolations.some((violation) => {
|
|
2164
|
+
for (const violation of newViolations) {
|
|
1816
2165
|
const filesystemViolation = detectFilesystemViolationFromLine(violation.line);
|
|
1817
|
-
if (
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
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
|
|
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 } =
|
|
1961
|
-
|
|
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
|
-
|
|
1971
|
-
const
|
|
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
|
-
|
|
2344
|
+
[...attempt.runtimeProtectedWriteViolations, ...storedFilesystemViolations],
|
|
1974
2345
|
cwd,
|
|
1975
|
-
|
|
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 (
|
|
1986
|
-
const notice =
|
|
2354
|
+
if (runtimeProtectedWriteViolations.length > 0) {
|
|
2355
|
+
const notice = formatRuntimeProtectedWriteNotice(
|
|
2356
|
+
runtimeProtectedWriteViolations,
|
|
2357
|
+
!attempt.interruptedByFilesystemViolation,
|
|
2358
|
+
);
|
|
1987
2359
|
postamble = appendOutputPostamble(postamble, notice, attempt.combinedOutput);
|
|
1988
|
-
|
|
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 {
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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;
|