tau-coding-agent 0.1.4 → 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/README.md +1 -1
- package/extensions/answer.ts +59 -47
- package/extensions/branch-term.ts +8 -13
- package/extensions/btw.ts +77 -6
- package/extensions/ghostty.ts +64 -11
- package/extensions/insights.ts +42 -4
- package/extensions/loop.ts +51 -12
- package/extensions/memory.ts +35 -17
- package/extensions/notify.ts +24 -5
- package/extensions/openai-fast.ts +6 -4
- package/extensions/openai-verbosity.ts +6 -4
- package/extensions/review/fix.ts +239 -0
- package/extensions/review/git.ts +517 -0
- package/extensions/review/index.ts +232 -0
- package/extensions/review/message-queue.ts +249 -0
- package/extensions/review/models.ts +589 -0
- package/extensions/review/prompts.ts +337 -0
- package/extensions/review/renderers/inline.ts +126 -0
- package/extensions/review/request.ts +504 -0
- package/extensions/review/review.ts +1305 -0
- package/extensions/review/runner.ts +522 -0
- package/extensions/review/runtime.ts +275 -0
- package/extensions/review/schema.ts +160 -0
- package/extensions/review/submit-review-tool.ts +62 -0
- package/extensions/review/submit-triage-tool.ts +60 -0
- package/extensions/review/triage.ts +746 -0
- package/extensions/sandbox/index.ts +976 -185
- package/extensions/sandbox/macos-sandbox-shell.mjs +202 -0
- package/extensions/{interlude.ts → stash.ts} +13 -11
- package/extensions/tool-display-mode.ts +2 -3
- package/extensions/usage/anthropic.ts +5 -6
- package/extensions/usage/github-copilot.ts +30 -9
- package/extensions/usage/index.ts +9 -5
- package/extensions/usage/openai-codex.ts +2 -2
- package/extensions/websearch/README.md +1 -1
- package/extensions/websearch/browser/chromium.ts +42 -31
- package/extensions/websearch/browser/discovery.ts +11 -9
- package/extensions/websearch/browser/firefox.ts +10 -7
- package/extensions/websearch/browser/fs.ts +10 -0
- package/extensions/websearch/browser/sqlite.ts +22 -15
- package/extensions/websearch/config.ts +2 -2
- package/extensions/websearch/index.ts +6 -6
- package/extensions/websearch/providers/pi-model.shared.ts +19 -1
- package/extensions/worktree.ts +33 -33
- package/package.json +4 -4
- package/skills/git-clean-history/SKILL.md +1 -0
- package/skills/git-commit/SKILL.md +2 -2
- package/skills/oracle/SKILL.md +3 -3
- package/skills/oracle/scripts/oracle +82 -61
- package/themes/tau-dark.json +2 -1
- package/extensions/review.ts +0 -4420
|
@@ -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,
|
|
@@ -63,12 +69,22 @@ import {
|
|
|
63
69
|
globToRegex,
|
|
64
70
|
normalizePathForSandbox,
|
|
65
71
|
} from "@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-utils.js";
|
|
66
|
-
import
|
|
67
|
-
|
|
72
|
+
import {
|
|
73
|
+
CONFIG_DIR_NAME,
|
|
74
|
+
createBashTool,
|
|
75
|
+
getAgentDir,
|
|
76
|
+
type BashOperations,
|
|
77
|
+
type ExtensionAPI,
|
|
78
|
+
type ExtensionContext,
|
|
79
|
+
} from "@earendil-works/pi-coding-agent";
|
|
68
80
|
|
|
69
81
|
// --- Constants ---
|
|
70
82
|
|
|
71
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));
|
|
86
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
87
|
+
const MAX_TIMEOUT_SECONDS = MAX_TIMEOUT_MS / 1000;
|
|
72
88
|
|
|
73
89
|
const DEFAULT_CONFIG: SandboxConfig = {
|
|
74
90
|
enabled: true,
|
|
@@ -79,30 +95,43 @@ const DEFAULT_CONFIG: SandboxConfig = {
|
|
|
79
95
|
"127.0.0.1",
|
|
80
96
|
"npmjs.org",
|
|
81
97
|
"*.npmjs.org",
|
|
82
|
-
"
|
|
98
|
+
"npmjs.com",
|
|
99
|
+
"*.npmjs.com",
|
|
83
100
|
"registry.yarnpkg.com",
|
|
101
|
+
"nodejs.org",
|
|
102
|
+
"*.nodejs.org",
|
|
84
103
|
"pypi.org",
|
|
85
104
|
"*.pypi.org",
|
|
86
105
|
"pythonhosted.org",
|
|
87
106
|
"*.pythonhosted.org",
|
|
88
107
|
"crates.io",
|
|
89
108
|
"*.crates.io",
|
|
109
|
+
"rustup.rs",
|
|
110
|
+
"*.rust-lang.org",
|
|
111
|
+
"repo.maven.apache.org",
|
|
112
|
+
"gradle.org",
|
|
113
|
+
"*.gradle.org",
|
|
90
114
|
"proxy.golang.org",
|
|
91
115
|
"sum.golang.org",
|
|
92
116
|
"go.dev",
|
|
93
117
|
"golang.org",
|
|
94
118
|
"rubygems.org",
|
|
95
119
|
"*.rubygems.org",
|
|
120
|
+
"nuget.org",
|
|
121
|
+
"*.nuget.org",
|
|
96
122
|
"github.com",
|
|
97
123
|
"*.github.com",
|
|
98
124
|
"githubusercontent.com",
|
|
99
125
|
"*.githubusercontent.com",
|
|
100
|
-
"
|
|
101
|
-
"
|
|
126
|
+
"gitlab.com",
|
|
127
|
+
"*.gitlab.com",
|
|
128
|
+
"bitbucket.org",
|
|
129
|
+
"*.bitbucket.org",
|
|
102
130
|
"ghcr.io",
|
|
103
131
|
"docker.io",
|
|
104
132
|
"*.docker.io",
|
|
105
|
-
"
|
|
133
|
+
"docker.com",
|
|
134
|
+
"*.docker.com",
|
|
106
135
|
"sentry.io",
|
|
107
136
|
"*.sentry.io",
|
|
108
137
|
"datadoghq.com",
|
|
@@ -117,6 +146,8 @@ const DEFAULT_CONFIG: SandboxConfig = {
|
|
|
117
146
|
"*.openai.com",
|
|
118
147
|
"chatgpt.com",
|
|
119
148
|
"*.chatgpt.com",
|
|
149
|
+
"openrouter.ai",
|
|
150
|
+
"*.openrouter.ai",
|
|
120
151
|
"google.com",
|
|
121
152
|
"*.google.com",
|
|
122
153
|
"googleapis.com",
|
|
@@ -129,26 +160,87 @@ const DEFAULT_CONFIG: SandboxConfig = {
|
|
|
129
160
|
"*.doist.com",
|
|
130
161
|
],
|
|
131
162
|
deniedDomains: [],
|
|
163
|
+
allowUnixSockets: ["$SSH_AUTH_SOCK"],
|
|
132
164
|
allowLocalBinding: true,
|
|
165
|
+
allowMachLookup: [
|
|
166
|
+
"com.apple.dnssd.service",
|
|
167
|
+
"com.apple.SystemConfiguration.configd",
|
|
168
|
+
"com.apple.SystemConfiguration.DNSConfiguration",
|
|
169
|
+
],
|
|
133
170
|
},
|
|
134
171
|
filesystem: {
|
|
135
172
|
denyRead: ["~/.ssh", "~/.aws", "~/.gnupg"],
|
|
136
|
-
|
|
137
|
-
|
|
173
|
+
allowRead: ["~/.ssh/config", "~/.ssh/known_hosts", "~/.ssh/*.pub"],
|
|
174
|
+
allowWrite: [
|
|
175
|
+
".",
|
|
176
|
+
"~/.cache",
|
|
177
|
+
"~/Library/Caches",
|
|
178
|
+
join(getAgentDir(), "*.lock"),
|
|
179
|
+
"~/**/__pycache__",
|
|
180
|
+
"~/**/__pycache__/*",
|
|
181
|
+
"~/.npm",
|
|
182
|
+
"~/.rustup",
|
|
183
|
+
"~/.cargo/registry",
|
|
184
|
+
"~/.cargo/git",
|
|
185
|
+
"~/.cargo/.package-cache",
|
|
186
|
+
"~/.cargo/.package-cache-mutate",
|
|
187
|
+
"~/.cargo/.global-cache",
|
|
188
|
+
"~/go/pkg",
|
|
189
|
+
"~/.m2/repository",
|
|
190
|
+
"~/.m2/wrapper/dists",
|
|
191
|
+
"~/.gradle",
|
|
192
|
+
"~/Library/Application Support/kotlin",
|
|
193
|
+
"~/.android",
|
|
194
|
+
"~/.bundle/cache",
|
|
195
|
+
"~/.gem/cache",
|
|
196
|
+
"~/.gem/specs",
|
|
197
|
+
"~/.nuget/packages",
|
|
198
|
+
"~/.local/share/NuGet/*-cache",
|
|
199
|
+
"~/.local/share/NuGet/*-cache/**",
|
|
200
|
+
],
|
|
201
|
+
denyWrite: [
|
|
202
|
+
".env",
|
|
203
|
+
".env.*",
|
|
204
|
+
"*.pem",
|
|
205
|
+
"*.key",
|
|
206
|
+
"~/.gradle/gradle.properties",
|
|
207
|
+
"~/.gradle/init.gradle",
|
|
208
|
+
"~/.gradle/init.gradle.kts",
|
|
209
|
+
"~/.gradle/init.d",
|
|
210
|
+
"~/.android/adbkey",
|
|
211
|
+
],
|
|
138
212
|
allowTempDirs: true,
|
|
139
213
|
allowGitConfig: true,
|
|
140
214
|
allowGitCommonDir: true,
|
|
141
215
|
},
|
|
216
|
+
ignoreViolations: {
|
|
217
|
+
"*": ["/__pycache__", "mach-lookup com.apple.usymptomsd"],
|
|
218
|
+
},
|
|
142
219
|
};
|
|
143
220
|
|
|
144
221
|
const STATUS_KEY = "sandbox";
|
|
145
222
|
const SANDBOX_EVENT_LIMIT = 50;
|
|
146
|
-
const
|
|
223
|
+
const READ_TRAVERSAL_PROCESSES = new Set(["find", "ls", "fd", "fdfind"]);
|
|
147
224
|
const GIT_FILESYSTEM_PATHS_CACHE = new Map<string, GitFilesystemPaths | null>();
|
|
225
|
+
const ENV_PATH_REFERENCE_PATTERN = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g;
|
|
148
226
|
|
|
149
227
|
// --- Types ---
|
|
150
228
|
|
|
151
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 };
|
|
152
244
|
|
|
153
245
|
type SandboxBypassReason = "no-sandbox-flag" | "config-disabled" | "missing-dependencies";
|
|
154
246
|
type SandboxBlockedReason = "unsupported-platform" | "init-failed";
|
|
@@ -158,7 +250,7 @@ type SandboxRunMode = "sandbox" | "user-disabled" | SandboxBypassReason | Sandbo
|
|
|
158
250
|
type SandboxState =
|
|
159
251
|
| { status: "pending" }
|
|
160
252
|
| { status: "active"; runtimeConfig: SandboxRuntimeConfig }
|
|
161
|
-
| { status: "suspended"
|
|
253
|
+
| { status: "suspended" }
|
|
162
254
|
| { status: "bypassed"; reason: SandboxBypassReason }
|
|
163
255
|
| { status: "blocked"; reason: SandboxBlockedReason };
|
|
164
256
|
|
|
@@ -171,33 +263,32 @@ type SandboxConfig = Omit<SandboxRuntimeConfig, "filesystem"> & {
|
|
|
171
263
|
};
|
|
172
264
|
};
|
|
173
265
|
|
|
174
|
-
type SandboxEventKind = "filesystem" | "network" | "init" | "runtime";
|
|
266
|
+
type SandboxEventKind = "filesystem" | "network" | "mach-lookup" | "init" | "runtime";
|
|
175
267
|
type SandboxEventReason =
|
|
176
268
|
| "explicit-deny-read"
|
|
177
269
|
| "explicit-deny-write"
|
|
178
270
|
| "explicit-deny-domain"
|
|
179
271
|
| "missing-allow-write"
|
|
180
272
|
| "missing-allowed-domain"
|
|
273
|
+
| "missing-mach-lookup"
|
|
181
274
|
| "missing-dependencies"
|
|
182
275
|
| "unsupported-platform"
|
|
183
276
|
| "init-failed"
|
|
277
|
+
| "runtime-protected-write"
|
|
184
278
|
| "already-approved-still-failed"
|
|
185
279
|
| "unknown";
|
|
186
|
-
type
|
|
187
|
-
type SandboxConfigPathStatus = "loaded" | "parse-error";
|
|
280
|
+
type SandboxConfigPathStatus = "loaded" | "parse-error" | "skipped-untrusted";
|
|
188
281
|
type SandboxConfigPathLabel = "Global" | "Project" | "Override";
|
|
189
282
|
|
|
190
283
|
type PromptStatus = "completed" | "error";
|
|
191
284
|
type UiLevel = "info" | "warning" | "error";
|
|
192
285
|
|
|
193
|
-
type ListOp = "add" | "remove";
|
|
194
286
|
type NetworkList = "allow" | "deny";
|
|
195
287
|
type FilesystemList = "deny-read" | "allow-write" | "deny-write";
|
|
196
288
|
|
|
197
289
|
type FilesystemViolationKind = "read" | "write" | "unknown";
|
|
198
290
|
type FilesystemReadAccess = "metadata" | "data" | "unknown";
|
|
199
|
-
type
|
|
200
|
-
|
|
291
|
+
type FilesystemWriteAccess = "unlink" | "unknown";
|
|
201
292
|
interface SandboxEvent {
|
|
202
293
|
timestamp: number;
|
|
203
294
|
kind: SandboxEventKind;
|
|
@@ -245,18 +336,12 @@ interface FilesystemViolation {
|
|
|
245
336
|
path?: string;
|
|
246
337
|
processName?: string;
|
|
247
338
|
readAccess?: FilesystemReadAccess;
|
|
339
|
+
writeAccess?: FilesystemWriteAccess;
|
|
248
340
|
}
|
|
249
341
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
message: string;
|
|
254
|
-
retrySuccessMessage: string;
|
|
255
|
-
retryFailureMessage: string;
|
|
256
|
-
retrySkippedMessage: string;
|
|
257
|
-
}
|
|
258
|
-
| { kind: "allow-adapt"; message: string }
|
|
259
|
-
| { kind: "deny"; message: string };
|
|
342
|
+
interface MachLookupViolation {
|
|
343
|
+
service: string;
|
|
344
|
+
}
|
|
260
345
|
|
|
261
346
|
// --- Helpers ---
|
|
262
347
|
|
|
@@ -307,6 +392,19 @@ function notify(ctx: ExtensionContext, text: string, level: UiLevel = "info"): v
|
|
|
307
392
|
else console.log(text);
|
|
308
393
|
}
|
|
309
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
|
+
|
|
310
408
|
function showHelp(ctx: ExtensionContext): void {
|
|
311
409
|
const lines = [
|
|
312
410
|
"Usage:",
|
|
@@ -316,6 +414,12 @@ function showHelp(ctx: ExtensionContext): void {
|
|
|
316
414
|
" /sandbox doctor",
|
|
317
415
|
" /sandbox mode <interactive|non-interactive>",
|
|
318
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
|
+
: []),
|
|
319
423
|
" /sandbox filesystem <deny-read|allow-write|deny-write> <add|remove> <path>",
|
|
320
424
|
"",
|
|
321
425
|
"Startup flags:",
|
|
@@ -371,6 +475,7 @@ const SANDBOX_TOP_LEVEL_COMPLETIONS: CommandCompletionOption[] = [
|
|
|
371
475
|
{ value: "doctor", label: "doctor" },
|
|
372
476
|
{ value: "mode ", label: "mode" },
|
|
373
477
|
{ value: "network ", label: "network" },
|
|
478
|
+
...(IS_MACOS ? [{ value: "mach-lookup ", label: "mach-lookup" }] : []),
|
|
374
479
|
{ value: "filesystem ", label: "filesystem" },
|
|
375
480
|
{ value: "help", label: "help" },
|
|
376
481
|
];
|
|
@@ -436,6 +541,37 @@ function getStringValueCompletions(
|
|
|
436
541
|
}));
|
|
437
542
|
}
|
|
438
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
|
+
|
|
439
575
|
function getSandboxArgumentCompletions(
|
|
440
576
|
prefix: string,
|
|
441
577
|
runtimeConfig: SandboxRuntimeConfig | null,
|
|
@@ -499,6 +635,10 @@ function getSandboxArgumentCompletions(
|
|
|
499
635
|
return null;
|
|
500
636
|
}
|
|
501
637
|
|
|
638
|
+
if (subcommand === "mach-lookup" && IS_MACOS) {
|
|
639
|
+
return getMachLookupArgumentCompletions({ tokens, endsWithSpace, runtimeConfig });
|
|
640
|
+
}
|
|
641
|
+
|
|
502
642
|
if (subcommand === "filesystem") {
|
|
503
643
|
if (tokens.length === 1 && endsWithSpace) {
|
|
504
644
|
return getCommandCompletions("filesystem ", "", SANDBOX_FILESYSTEM_LIST_COMPLETIONS);
|
|
@@ -575,12 +715,7 @@ function formatSandboxConfigLoadErrorMessage(
|
|
|
575
715
|
return `Could not read sandbox override config ${path}: ${detail ?? "unknown error"}`;
|
|
576
716
|
}
|
|
577
717
|
|
|
578
|
-
function
|
|
579
|
-
if (!Array.isArray(value)) {
|
|
580
|
-
console.error(`Warning: Expected ${field} to be a string[]; using defaults.`);
|
|
581
|
-
return [...fallback];
|
|
582
|
-
}
|
|
583
|
-
|
|
718
|
+
function cleanStringArray(value: unknown[], field: string): string[] {
|
|
584
719
|
const cleaned = value
|
|
585
720
|
.filter((item): item is string => typeof item === "string")
|
|
586
721
|
.map((item) => item.trim())
|
|
@@ -594,6 +729,30 @@ function coerceStringArray(value: unknown, fallback: string[], field: string): s
|
|
|
594
729
|
return cleaned;
|
|
595
730
|
}
|
|
596
731
|
|
|
732
|
+
function coerceStringArray(value: unknown, fallback: string[], field: string): string[] {
|
|
733
|
+
if (!Array.isArray(value)) {
|
|
734
|
+
console.error(`Warning: Expected ${field} to be a string[]; using defaults.`);
|
|
735
|
+
return [...fallback];
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
return cleanStringArray(value, field);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function coerceOptionalStringArray(
|
|
742
|
+
value: unknown,
|
|
743
|
+
fallback: string[] | undefined,
|
|
744
|
+
field: string,
|
|
745
|
+
): string[] | undefined {
|
|
746
|
+
if (value === undefined) return fallback ? [...fallback] : undefined;
|
|
747
|
+
|
|
748
|
+
if (!Array.isArray(value)) {
|
|
749
|
+
console.error(`Warning: Expected ${field} to be a string[]; using defaults.`);
|
|
750
|
+
return fallback ? [...fallback] : undefined;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return cleanStringArray(value, field);
|
|
754
|
+
}
|
|
755
|
+
|
|
597
756
|
function finalizeConfig(config: SandboxConfig): SandboxConfig {
|
|
598
757
|
return {
|
|
599
758
|
...config,
|
|
@@ -611,6 +770,16 @@ function finalizeConfig(config: SandboxConfig): SandboxConfig {
|
|
|
611
770
|
DEFAULT_CONFIG.network.deniedDomains,
|
|
612
771
|
"network.deniedDomains",
|
|
613
772
|
),
|
|
773
|
+
allowMachLookup: coerceStringArray(
|
|
774
|
+
config.network?.allowMachLookup,
|
|
775
|
+
DEFAULT_CONFIG.network.allowMachLookup ?? [],
|
|
776
|
+
"network.allowMachLookup",
|
|
777
|
+
),
|
|
778
|
+
allowUnixSockets: coerceOptionalStringArray(
|
|
779
|
+
config.network?.allowUnixSockets,
|
|
780
|
+
DEFAULT_CONFIG.network.allowUnixSockets,
|
|
781
|
+
"network.allowUnixSockets",
|
|
782
|
+
),
|
|
614
783
|
},
|
|
615
784
|
filesystem: {
|
|
616
785
|
...config.filesystem,
|
|
@@ -619,6 +788,11 @@ function finalizeConfig(config: SandboxConfig): SandboxConfig {
|
|
|
619
788
|
DEFAULT_CONFIG.filesystem.denyRead,
|
|
620
789
|
"filesystem.denyRead",
|
|
621
790
|
),
|
|
791
|
+
allowRead: coerceOptionalStringArray(
|
|
792
|
+
config.filesystem?.allowRead,
|
|
793
|
+
DEFAULT_CONFIG.filesystem.allowRead,
|
|
794
|
+
"filesystem.allowRead",
|
|
795
|
+
),
|
|
622
796
|
allowWrite: coerceStringArray(
|
|
623
797
|
config.filesystem?.allowWrite,
|
|
624
798
|
DEFAULT_CONFIG.filesystem.allowWrite,
|
|
@@ -680,13 +854,17 @@ function loadOverrideConfig(cwd: string, overrideConfigPath: string): LoadedSand
|
|
|
680
854
|
};
|
|
681
855
|
}
|
|
682
856
|
|
|
683
|
-
function loadConfig(
|
|
857
|
+
function loadConfig(
|
|
858
|
+
cwd: string,
|
|
859
|
+
overrideConfigPath?: string,
|
|
860
|
+
options: { projectTrusted?: boolean } = {},
|
|
861
|
+
): LoadedSandboxConfig {
|
|
684
862
|
if (overrideConfigPath) {
|
|
685
863
|
return loadOverrideConfig(cwd, overrideConfigPath);
|
|
686
864
|
}
|
|
687
865
|
|
|
688
|
-
const projectConfigPath = join(cwd,
|
|
689
|
-
const globalConfigPath = join(
|
|
866
|
+
const projectConfigPath = join(cwd, CONFIG_DIR_NAME, "sandbox.json");
|
|
867
|
+
const globalConfigPath = join(getAgentDir(), "sandbox.json");
|
|
690
868
|
|
|
691
869
|
let globalConfig: Partial<SandboxConfig> = {};
|
|
692
870
|
let projectConfig: Partial<SandboxConfig> = {};
|
|
@@ -703,12 +881,16 @@ function loadConfig(cwd: string, overrideConfigPath?: string): LoadedSandboxConf
|
|
|
703
881
|
}
|
|
704
882
|
|
|
705
883
|
if (existsSync(projectConfigPath)) {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
884
|
+
if (options.projectTrusted !== true) {
|
|
885
|
+
paths.push({ label: "Project", path: projectConfigPath, status: "skipped-untrusted" });
|
|
886
|
+
} else {
|
|
887
|
+
try {
|
|
888
|
+
projectConfig = JSON.parse(readFileSync(projectConfigPath, "utf-8"));
|
|
889
|
+
paths.push({ label: "Project", path: projectConfigPath, status: "loaded" });
|
|
890
|
+
} catch (e) {
|
|
891
|
+
paths.push({ label: "Project", path: projectConfigPath, status: "parse-error" });
|
|
892
|
+
console.error(`Warning: Could not parse ${projectConfigPath}: ${e}`);
|
|
893
|
+
}
|
|
712
894
|
}
|
|
713
895
|
}
|
|
714
896
|
|
|
@@ -773,15 +955,128 @@ function getTemporaryWritePaths(): string[] {
|
|
|
773
955
|
return cachedTemporaryWritePaths;
|
|
774
956
|
}
|
|
775
957
|
|
|
958
|
+
function deduplicateStrings(values: string[]): string[] {
|
|
959
|
+
return Array.from(new Set(values));
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function deduplicateOptionalStrings(values: string[] | undefined): string[] | undefined {
|
|
963
|
+
return values ? deduplicateStrings(values) : undefined;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function containsControlCharacter(value: string): boolean {
|
|
967
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
968
|
+
const code = value.charCodeAt(index);
|
|
969
|
+
if (code <= 0x1f || (code >= 0x7f && code <= 0x9f) || code === 0x2028 || code === 0x2029) {
|
|
970
|
+
return true;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function expandPathConfigEntry(value: string, field: string): string | null {
|
|
978
|
+
const source = value.trim();
|
|
979
|
+
if (!source) return null;
|
|
980
|
+
|
|
981
|
+
const missingEnvNames = new Set<string>();
|
|
982
|
+
const expanded = source.replace(
|
|
983
|
+
ENV_PATH_REFERENCE_PATTERN,
|
|
984
|
+
(_match, bracedName: string | undefined, bareName: string | undefined) => {
|
|
985
|
+
const envName = bracedName ?? bareName;
|
|
986
|
+
const envValue = envName ? (process.env[envName]?.trim() ?? "") : "";
|
|
987
|
+
if (!envValue) {
|
|
988
|
+
if (envName) missingEnvNames.add(envName);
|
|
989
|
+
return "";
|
|
990
|
+
}
|
|
991
|
+
return envValue;
|
|
992
|
+
},
|
|
993
|
+
);
|
|
994
|
+
|
|
995
|
+
if (missingEnvNames.size > 0) {
|
|
996
|
+
const names = Array.from(missingEnvNames).join(", ");
|
|
997
|
+
const label = missingEnvNames.size === 1 ? "environment variable" : "environment variables";
|
|
998
|
+
const verb = missingEnvNames.size === 1 ? "is" : "are";
|
|
999
|
+
console.error(
|
|
1000
|
+
`Warning: Ignoring ${field} entry because ${label} ${names} ${verb} unset or empty.`,
|
|
1001
|
+
);
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (containsControlCharacter(expanded)) {
|
|
1006
|
+
console.error(
|
|
1007
|
+
`Warning: Ignoring ${field} entry because the expanded path contains control characters.`,
|
|
1008
|
+
);
|
|
1009
|
+
return null;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
return expanded;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function expandPathConfigList(values: string[] | undefined, field: string): string[] | undefined {
|
|
1016
|
+
if (!values) return undefined;
|
|
1017
|
+
|
|
1018
|
+
return values
|
|
1019
|
+
.map((value) => expandPathConfigEntry(value, field))
|
|
1020
|
+
.filter((value): value is string => value !== null);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function expandMitmProxyPathConfig(
|
|
1024
|
+
mitmProxy: SandboxRuntimeConfig["network"]["mitmProxy"],
|
|
1025
|
+
): SandboxRuntimeConfig["network"]["mitmProxy"] {
|
|
1026
|
+
if (!mitmProxy) return undefined;
|
|
1027
|
+
if (!isPlainObject(mitmProxy) || typeof mitmProxy.socketPath !== "string") return mitmProxy;
|
|
1028
|
+
|
|
1029
|
+
const socketPath = expandPathConfigEntry(mitmProxy.socketPath, "network.mitmProxy.socketPath");
|
|
1030
|
+
if (!socketPath) {
|
|
1031
|
+
console.error("Warning: Disabling network.mitmProxy because its socketPath did not expand.");
|
|
1032
|
+
return undefined;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
return { ...mitmProxy, socketPath } as SandboxRuntimeConfig["network"]["mitmProxy"];
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function expandNetworkPathConfig(
|
|
1039
|
+
network: SandboxRuntimeConfig["network"],
|
|
1040
|
+
): SandboxRuntimeConfig["network"] {
|
|
1041
|
+
return {
|
|
1042
|
+
...network,
|
|
1043
|
+
allowUnixSockets: expandPathConfigList(network.allowUnixSockets, "network.allowUnixSockets"),
|
|
1044
|
+
mitmProxy: expandMitmProxyPathConfig(network.mitmProxy),
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function expandFilesystemPathConfig(
|
|
1049
|
+
filesystem: SandboxRuntimeConfig["filesystem"],
|
|
1050
|
+
): SandboxRuntimeConfig["filesystem"] {
|
|
1051
|
+
return {
|
|
1052
|
+
...filesystem,
|
|
1053
|
+
denyRead: expandPathConfigList(filesystem.denyRead, "filesystem.denyRead") ?? [],
|
|
1054
|
+
allowRead: expandPathConfigList(filesystem.allowRead, "filesystem.allowRead"),
|
|
1055
|
+
allowWrite: expandPathConfigList(filesystem.allowWrite, "filesystem.allowWrite") ?? [],
|
|
1056
|
+
denyWrite: expandPathConfigList(filesystem.denyWrite, "filesystem.denyWrite") ?? [],
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
|
|
776
1060
|
function toRuntimeConfig(config: SandboxConfig): SandboxRuntimeConfig {
|
|
777
1061
|
const { allowGitCommonDir: _allowGitCommonDir, allowTempDirs, ...filesystem } = config.filesystem;
|
|
1062
|
+
const expandedNetwork = expandNetworkPathConfig(config.network);
|
|
1063
|
+
const expandedFilesystem = expandFilesystemPathConfig(filesystem);
|
|
778
1064
|
const allowWrite = allowTempDirs
|
|
779
|
-
?
|
|
780
|
-
:
|
|
1065
|
+
? [...expandedFilesystem.allowWrite, ...getTemporaryWritePaths()]
|
|
1066
|
+
: expandedFilesystem.allowWrite;
|
|
781
1067
|
|
|
782
1068
|
return {
|
|
783
|
-
network:
|
|
784
|
-
|
|
1069
|
+
network: {
|
|
1070
|
+
...expandedNetwork,
|
|
1071
|
+
allowUnixSockets: deduplicateOptionalStrings(expandedNetwork.allowUnixSockets),
|
|
1072
|
+
},
|
|
1073
|
+
filesystem: {
|
|
1074
|
+
...expandedFilesystem,
|
|
1075
|
+
denyRead: deduplicateStrings(expandedFilesystem.denyRead),
|
|
1076
|
+
allowRead: deduplicateOptionalStrings(expandedFilesystem.allowRead),
|
|
1077
|
+
allowWrite: deduplicateStrings(allowWrite),
|
|
1078
|
+
denyWrite: deduplicateStrings(expandedFilesystem.denyWrite),
|
|
1079
|
+
},
|
|
785
1080
|
ignoreViolations: config.ignoreViolations,
|
|
786
1081
|
enableWeakerNestedSandbox: config.enableWeakerNestedSandbox,
|
|
787
1082
|
enableWeakerNetworkIsolation: config.enableWeakerNetworkIsolation,
|
|
@@ -818,6 +1113,19 @@ function inferSandboxRuleMatch(path: string, rules: string[], cwd?: string): str
|
|
|
818
1113
|
return null;
|
|
819
1114
|
}
|
|
820
1115
|
|
|
1116
|
+
function matchesSandboxRuleExactly(path: string, rule: string, cwd?: string): boolean {
|
|
1117
|
+
if (containsGlobChars(rule)) return false;
|
|
1118
|
+
return normalizeSandboxPath(path) === normalizeSandboxPath(rule, cwd);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function inferExactSandboxRuleMatch(path: string, rules: string[], cwd?: string): string | null {
|
|
1122
|
+
for (const rule of rules) {
|
|
1123
|
+
if (matchesSandboxRuleExactly(path, rule, cwd)) return rule;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
821
1129
|
function isSandboxWritablePath(
|
|
822
1130
|
runtimeConfig: SandboxRuntimeConfig,
|
|
823
1131
|
path: string,
|
|
@@ -827,6 +1135,42 @@ function isSandboxWritablePath(
|
|
|
827
1135
|
return inferSandboxRuleMatch(path, runtimeConfig.filesystem.denyWrite, cwd) === null;
|
|
828
1136
|
}
|
|
829
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
|
+
|
|
830
1174
|
function resolveGitFilesystemPaths(cwd: string): GitFilesystemPaths | null {
|
|
831
1175
|
if (GIT_FILESYSTEM_PATHS_CACHE.has(cwd)) {
|
|
832
1176
|
return GIT_FILESYSTEM_PATHS_CACHE.get(cwd) ?? null;
|
|
@@ -910,8 +1254,8 @@ function extractAppendedSandboxAnnotation(
|
|
|
910
1254
|
: violationLines;
|
|
911
1255
|
if (newViolationLines.length === 0) return "";
|
|
912
1256
|
|
|
913
|
-
//
|
|
914
|
-
//
|
|
1257
|
+
// Sandbox violations are summarized elsewhere via compact extension messages,
|
|
1258
|
+
// so suppress the verbose synthetic annotation block.
|
|
915
1259
|
return "";
|
|
916
1260
|
}
|
|
917
1261
|
|
|
@@ -957,7 +1301,60 @@ function extractPathLikeValue(text: string): string | undefined {
|
|
|
957
1301
|
|
|
958
1302
|
function extractViolationProcessName(line: string): string | undefined {
|
|
959
1303
|
const match = line.match(/^([^\s(]+)\(/);
|
|
960
|
-
|
|
1304
|
+
const processName = match?.[1]?.trim();
|
|
1305
|
+
if (!processName) return undefined;
|
|
1306
|
+
return processName.split("/").pop() || processName;
|
|
1307
|
+
}
|
|
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
|
+
);
|
|
961
1358
|
}
|
|
962
1359
|
|
|
963
1360
|
function detectFilesystemViolationFromLine(line: string): FilesystemViolation | null {
|
|
@@ -966,8 +1363,12 @@ function detectFilesystemViolationFromLine(line: string): FilesystemViolation |
|
|
|
966
1363
|
const path = extractPathLikeValue(line);
|
|
967
1364
|
const processName = extractViolationProcessName(line);
|
|
968
1365
|
|
|
1366
|
+
if (lower.includes("file-write-unlink")) {
|
|
1367
|
+
return { kind: "write", path, processName, writeAccess: "unlink" };
|
|
1368
|
+
}
|
|
1369
|
+
|
|
969
1370
|
if (lower.includes("file-write")) {
|
|
970
|
-
return { kind: "write", path, processName };
|
|
1371
|
+
return { kind: "write", path, processName, writeAccess: "unknown" };
|
|
971
1372
|
}
|
|
972
1373
|
|
|
973
1374
|
if (lower.includes("file-read-metadata")) {
|
|
@@ -989,6 +1390,7 @@ function detectFilesystemViolations(
|
|
|
989
1390
|
output: string,
|
|
990
1391
|
fallbackOutput: string = output,
|
|
991
1392
|
skipViolationLines = 0,
|
|
1393
|
+
allowOutputFallback = true,
|
|
992
1394
|
): FilesystemViolation[] {
|
|
993
1395
|
const violations: FilesystemViolation[] = [];
|
|
994
1396
|
const allViolationLines = extractSandboxViolationLines(output);
|
|
@@ -1002,7 +1404,7 @@ function detectFilesystemViolations(
|
|
|
1002
1404
|
if (violation) violations.push(violation);
|
|
1003
1405
|
}
|
|
1004
1406
|
|
|
1005
|
-
if (violations.length > 0) return violations;
|
|
1407
|
+
if (violations.length > 0 || !allowOutputFallback) return violations;
|
|
1006
1408
|
|
|
1007
1409
|
const hasEperm = /\bEPERM\b/i.test(fallbackOutput);
|
|
1008
1410
|
const hasOperationNotPermitted = /(?:^|\n)[^\n]*Operation not permitted(?:$|\n)/i.test(
|
|
@@ -1016,19 +1418,30 @@ function detectFilesystemViolations(
|
|
|
1016
1418
|
return violations;
|
|
1017
1419
|
}
|
|
1018
1420
|
|
|
1019
|
-
function
|
|
1421
|
+
function isTraversalViolation(
|
|
1020
1422
|
runtimeConfig: SandboxRuntimeConfig | null,
|
|
1021
1423
|
violation: FilesystemViolation,
|
|
1022
1424
|
cwd?: string,
|
|
1023
1425
|
): boolean {
|
|
1024
|
-
if (!runtimeConfig || violation.
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1426
|
+
if (!runtimeConfig || !violation.path) return false;
|
|
1427
|
+
if (!READ_TRAVERSAL_PROCESSES.has(violation.processName ?? "")) return false;
|
|
1428
|
+
|
|
1429
|
+
if (violation.kind === "read") {
|
|
1430
|
+
return inferSandboxRuleMatch(violation.path, runtimeConfig.filesystem.denyRead, cwd) !== null;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
if (violation.kind !== "write" || violation.writeAccess !== "unlink") return false;
|
|
1434
|
+
|
|
1435
|
+
// Seatbelt can emit file-write-unlink checks for protected directory roots
|
|
1436
|
+
// while traversal commands enumerate them. Only exact protected roots are
|
|
1437
|
+
// treated as skipped traversal so writes under protected trees still prompt.
|
|
1438
|
+
return (
|
|
1439
|
+
inferExactSandboxRuleMatch(violation.path, runtimeConfig.filesystem.denyRead, cwd) !== null ||
|
|
1440
|
+
inferExactSandboxRuleMatch(violation.path, runtimeConfig.filesystem.denyWrite, cwd) !== null
|
|
1441
|
+
);
|
|
1029
1442
|
}
|
|
1030
1443
|
|
|
1031
|
-
function
|
|
1444
|
+
function getTraversalPaths(options: {
|
|
1032
1445
|
runtimeConfig: SandboxRuntimeConfig | null;
|
|
1033
1446
|
output: string;
|
|
1034
1447
|
cwd?: string;
|
|
@@ -1047,7 +1460,7 @@ function getMetadataTraversalPaths(options: {
|
|
|
1047
1460
|
const skippedPaths: string[] = [];
|
|
1048
1461
|
for (const line of violationLines) {
|
|
1049
1462
|
const violation = detectFilesystemViolationFromLine(line);
|
|
1050
|
-
if (!violation || !
|
|
1463
|
+
if (!violation || !isTraversalViolation(runtimeConfig, violation, cwd)) {
|
|
1051
1464
|
return null;
|
|
1052
1465
|
}
|
|
1053
1466
|
if (violation.path && !skippedPaths.includes(violation.path)) {
|
|
@@ -1058,7 +1471,7 @@ function getMetadataTraversalPaths(options: {
|
|
|
1058
1471
|
return skippedPaths.length > 0 ? skippedPaths : null;
|
|
1059
1472
|
}
|
|
1060
1473
|
|
|
1061
|
-
function
|
|
1474
|
+
function formatTraversalNotice(paths: string[]): string {
|
|
1062
1475
|
if (paths.length === 0) return "";
|
|
1063
1476
|
|
|
1064
1477
|
const visiblePaths = paths.slice(0, 3).join(", ");
|
|
@@ -1067,6 +1480,22 @@ function formatMetadataTraversalNotice(paths: string[]): string {
|
|
|
1067
1480
|
return `[sandbox] Continued after skipping protected ${label}: ${visiblePaths}${suffix}`;
|
|
1068
1481
|
}
|
|
1069
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
|
+
|
|
1070
1499
|
function escapeSlashCommandArg(value: string): string {
|
|
1071
1500
|
if (/^[a-zA-Z0-9_./:@%+\-~]+$/.test(value)) return value;
|
|
1072
1501
|
return JSON.stringify(value);
|
|
@@ -1187,56 +1616,61 @@ function formatFilesystemViolationSummary(violation: FilesystemViolation): strin
|
|
|
1187
1616
|
return "[sandbox] Blocked filesystem access (EPERM).";
|
|
1188
1617
|
}
|
|
1189
1618
|
|
|
1190
|
-
const
|
|
1191
|
-
const
|
|
1192
|
-
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";
|
|
1193
1622
|
|
|
1194
|
-
function
|
|
1195
|
-
_violation: FilesystemViolation,
|
|
1196
|
-
autoRetryAvailable: boolean,
|
|
1197
|
-
): string[] {
|
|
1623
|
+
function getViolationPromptOptions(autoRetryAvailable: boolean): string[] {
|
|
1198
1624
|
if (!autoRetryAvailable) {
|
|
1199
|
-
return [
|
|
1625
|
+
return [VIOLATION_ALLOW_ADAPT_OPTION, VIOLATION_DENY_OPTION];
|
|
1200
1626
|
}
|
|
1201
1627
|
|
|
1202
|
-
return [
|
|
1628
|
+
return [VIOLATION_ALLOW_RETRY_OPTION, VIOLATION_ALLOW_ADAPT_OPTION, VIOLATION_DENY_OPTION];
|
|
1203
1629
|
}
|
|
1204
1630
|
|
|
1205
|
-
function
|
|
1631
|
+
function parseViolationPromptSelection(
|
|
1206
1632
|
selection: string | undefined,
|
|
1207
1633
|
autoRetryAvailable: boolean,
|
|
1208
|
-
):
|
|
1209
|
-
if (selection ===
|
|
1210
|
-
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";
|
|
1211
1637
|
return "deny";
|
|
1212
1638
|
}
|
|
1213
1639
|
|
|
1214
|
-
function
|
|
1215
|
-
return
|
|
1640
|
+
function formatViolationAllowRetryMessage(blockedTarget: string): string {
|
|
1641
|
+
return `\n${blockedTarget}\n\nGranting access and retrying the command per user request...\n\n`;
|
|
1216
1642
|
}
|
|
1217
1643
|
|
|
1218
|
-
function
|
|
1219
|
-
return
|
|
1644
|
+
function formatViolationAllowAdaptMessage(blockedTarget: string): string {
|
|
1645
|
+
return `\n${blockedTarget}\n\nAccess granted for this session. Retry the command manually if appropriate.`;
|
|
1220
1646
|
}
|
|
1221
1647
|
|
|
1222
|
-
function
|
|
1223
|
-
return
|
|
1648
|
+
function formatViolationDeniedMessage(blockedTarget: string): string {
|
|
1649
|
+
return `\n${blockedTarget}\n\nAccess remains denied for this session.`;
|
|
1224
1650
|
}
|
|
1225
1651
|
|
|
1226
|
-
function
|
|
1227
|
-
return
|
|
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.`;
|
|
1228
1654
|
}
|
|
1229
1655
|
|
|
1230
|
-
function
|
|
1231
|
-
return
|
|
1656
|
+
function formatViolationRetrySucceededMessage(blockedTarget: string): string {
|
|
1657
|
+
return `\n${blockedTarget}\n\nAccess granted for this session and the command was retried successfully.`;
|
|
1232
1658
|
}
|
|
1233
1659
|
|
|
1234
|
-
function
|
|
1235
|
-
return
|
|
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.`;
|
|
1236
1662
|
}
|
|
1237
1663
|
|
|
1238
|
-
function
|
|
1239
|
-
return
|
|
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.`;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
function formatFilesystemBlockedTarget(target: string): string {
|
|
1669
|
+
return `Sandbox blocked filesystem ${target}.`;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
function formatMachLookupBlockedTarget(service: string): string {
|
|
1673
|
+
return `Sandbox blocked access to macOS service ${service}.`;
|
|
1240
1674
|
}
|
|
1241
1675
|
|
|
1242
1676
|
function appendOutputPostamble(postamble: string, addition: string, output: string): string {
|
|
@@ -1262,7 +1696,7 @@ async function handleFilesystemViolation(options: {
|
|
|
1262
1696
|
rawOutput: string;
|
|
1263
1697
|
command: string;
|
|
1264
1698
|
cwd?: string;
|
|
1265
|
-
pendingPrompts?: Map<string, Promise<
|
|
1699
|
+
pendingPrompts?: Map<string, Promise<ViolationResolution | null>>;
|
|
1266
1700
|
applyRuntimeConfigForSession?: (
|
|
1267
1701
|
ctx: ExtensionContext,
|
|
1268
1702
|
runtimeConfig: SandboxRuntimeConfig,
|
|
@@ -1270,7 +1704,9 @@ async function handleFilesystemViolation(options: {
|
|
|
1270
1704
|
existingViolationCount?: number;
|
|
1271
1705
|
recordEvent?: (event: SandboxEvent) => void;
|
|
1272
1706
|
autoRetryAvailable?: boolean;
|
|
1273
|
-
|
|
1707
|
+
runtimeProtectedWriteViolations?: FilesystemViolation[];
|
|
1708
|
+
allowOutputFallback?: boolean;
|
|
1709
|
+
}): Promise<ViolationResolution | null> {
|
|
1274
1710
|
const {
|
|
1275
1711
|
pi,
|
|
1276
1712
|
ctx,
|
|
@@ -1285,19 +1721,35 @@ async function handleFilesystemViolation(options: {
|
|
|
1285
1721
|
existingViolationCount,
|
|
1286
1722
|
recordEvent,
|
|
1287
1723
|
autoRetryAvailable = true,
|
|
1724
|
+
runtimeProtectedWriteViolations = [],
|
|
1725
|
+
allowOutputFallback = true,
|
|
1288
1726
|
} = options;
|
|
1289
|
-
const violations = detectFilesystemViolations(
|
|
1290
|
-
|
|
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;
|
|
1291
1742
|
|
|
1292
1743
|
const violation =
|
|
1293
|
-
|
|
1744
|
+
actionableViolations.find((candidate) => {
|
|
1294
1745
|
const candidateAction = buildFilesystemAllowAction(runtimeConfig, candidate, cwd);
|
|
1295
1746
|
if (!candidateAction) return false;
|
|
1296
1747
|
return !isFilesystemAllowActionAlreadyApplied(runtimeConfig, candidateAction);
|
|
1297
|
-
}) ??
|
|
1748
|
+
}) ?? actionableViolations[0];
|
|
1298
1749
|
|
|
1299
1750
|
const summary = formatFilesystemViolationSummary(violation);
|
|
1300
1751
|
const target = describeFilesystemViolationTarget(violation);
|
|
1752
|
+
const blockedTarget = formatFilesystemBlockedTarget(target);
|
|
1301
1753
|
const allowAction = buildFilesystemAllowAction(runtimeConfig, violation, cwd);
|
|
1302
1754
|
const allowCommand = allowAction ? buildFilesystemAllowCommand(allowAction) : null;
|
|
1303
1755
|
const alreadyApproved = allowAction
|
|
@@ -1336,25 +1788,25 @@ async function handleFilesystemViolation(options: {
|
|
|
1336
1788
|
|
|
1337
1789
|
if (alreadyApproved) {
|
|
1338
1790
|
recordFilesystemEvent("blocked");
|
|
1339
|
-
return { kind: "allow-adapt", message:
|
|
1791
|
+
return { kind: "allow-adapt", message: formatViolationAlreadyAllowedMessage(blockedTarget) };
|
|
1340
1792
|
}
|
|
1341
1793
|
|
|
1342
1794
|
const promptKey = `${allowCommand}:${autoRetryAvailable ? "retry" : "adapt"}`;
|
|
1343
1795
|
const existingPrompt = pendingPrompts?.get(promptKey);
|
|
1344
1796
|
if (existingPrompt) return existingPrompt;
|
|
1345
1797
|
|
|
1346
|
-
const promptTask: Promise<
|
|
1798
|
+
const promptTask: Promise<ViolationResolution | null> = (async () => {
|
|
1347
1799
|
try {
|
|
1348
1800
|
const selection = await withPromptSignal(pi, () =>
|
|
1349
1801
|
ctx.ui.select(
|
|
1350
1802
|
`Sandbox blocked filesystem ${target}`,
|
|
1351
|
-
|
|
1803
|
+
getViolationPromptOptions(autoRetryAvailable),
|
|
1352
1804
|
),
|
|
1353
1805
|
);
|
|
1354
|
-
const decision =
|
|
1806
|
+
const decision = parseViolationPromptSelection(selection, autoRetryAvailable);
|
|
1355
1807
|
if (decision === "deny") {
|
|
1356
1808
|
recordFilesystemEvent("blocked");
|
|
1357
|
-
return { kind: "deny", message:
|
|
1809
|
+
return { kind: "deny", message: formatViolationDeniedMessage(blockedTarget) };
|
|
1358
1810
|
}
|
|
1359
1811
|
|
|
1360
1812
|
const nextConfig = cloneRuntimeConfig(runtimeConfig);
|
|
@@ -1368,18 +1820,163 @@ async function handleFilesystemViolation(options: {
|
|
|
1368
1820
|
if (decision === "allow-retry") {
|
|
1369
1821
|
return {
|
|
1370
1822
|
kind: "allow-retry",
|
|
1371
|
-
message:
|
|
1372
|
-
retrySuccessMessage:
|
|
1373
|
-
retryFailureMessage:
|
|
1374
|
-
retrySkippedMessage:
|
|
1823
|
+
message: formatViolationAllowRetryMessage(blockedTarget),
|
|
1824
|
+
retrySuccessMessage: formatViolationRetrySucceededMessage(blockedTarget),
|
|
1825
|
+
retryFailureMessage: formatViolationRetryFailedMessage(blockedTarget),
|
|
1826
|
+
retrySkippedMessage: formatViolationRetrySkippedMessage(blockedTarget),
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
return {
|
|
1831
|
+
kind: "allow-adapt",
|
|
1832
|
+
message: changed
|
|
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),
|
|
1375
1972
|
};
|
|
1376
1973
|
}
|
|
1377
1974
|
|
|
1378
1975
|
return {
|
|
1379
1976
|
kind: "allow-adapt",
|
|
1380
1977
|
message: changed
|
|
1381
|
-
?
|
|
1382
|
-
:
|
|
1978
|
+
? formatViolationAllowAdaptMessage(blockedTarget)
|
|
1979
|
+
: formatViolationAlreadyAllowedMessage(blockedTarget),
|
|
1383
1980
|
};
|
|
1384
1981
|
} catch {
|
|
1385
1982
|
return null;
|
|
@@ -1413,17 +2010,20 @@ interface BashAttemptResult {
|
|
|
1413
2010
|
exitCode: number | null;
|
|
1414
2011
|
combinedOutput: string;
|
|
1415
2012
|
interruptedByFilesystemViolation: boolean;
|
|
2013
|
+
runtimeProtectedWriteViolations: FilesystemViolation[];
|
|
1416
2014
|
}
|
|
1417
2015
|
|
|
1418
2016
|
interface ProcessedSandboxAttempt {
|
|
1419
2017
|
exitCode: number | null;
|
|
1420
2018
|
postamble: string;
|
|
1421
|
-
resolution:
|
|
2019
|
+
resolution: ViolationResolution | null;
|
|
2020
|
+
runtimeProtectedWriteViolations: FilesystemViolation[];
|
|
1422
2021
|
}
|
|
1423
2022
|
|
|
1424
2023
|
interface PreparedSandboxAttempt {
|
|
1425
2024
|
attempt: BashAttemptResult;
|
|
1426
2025
|
existingViolationCount: number;
|
|
2026
|
+
runtimeConfig: SandboxRuntimeConfig | null;
|
|
1427
2027
|
}
|
|
1428
2028
|
|
|
1429
2029
|
function killProcessGroup(
|
|
@@ -1489,7 +2089,8 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1489
2089
|
applyRuntimeConfigForSession,
|
|
1490
2090
|
recordEvent,
|
|
1491
2091
|
} = options;
|
|
1492
|
-
const pendingFilesystemPrompts = new Map<string, Promise<
|
|
2092
|
+
const pendingFilesystemPrompts = new Map<string, Promise<ViolationResolution | null>>();
|
|
2093
|
+
const pendingMachLookupPrompts = new Map<string, Promise<ViolationResolution | null>>();
|
|
1493
2094
|
|
|
1494
2095
|
let executionQueue: Promise<void> = Promise.resolve();
|
|
1495
2096
|
|
|
@@ -1502,10 +2103,17 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1502
2103
|
return run;
|
|
1503
2104
|
}
|
|
1504
2105
|
|
|
2106
|
+
function withSandboxDefaultEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
2107
|
+
const baseEnv = env ?? process.env;
|
|
2108
|
+
if (baseEnv.GIT_OPTIONAL_LOCKS !== undefined) return baseEnv;
|
|
2109
|
+
return { ...baseEnv, GIT_OPTIONAL_LOCKS: "0" };
|
|
2110
|
+
}
|
|
2111
|
+
|
|
1505
2112
|
async function runSandboxAttempt(
|
|
1506
2113
|
command: string,
|
|
1507
2114
|
wrappedCommand: string,
|
|
1508
2115
|
cwd: string,
|
|
2116
|
+
runtimeConfig: SandboxRuntimeConfig | null,
|
|
1509
2117
|
onData: (data: Buffer) => void,
|
|
1510
2118
|
existingViolationCount: number,
|
|
1511
2119
|
signal?: AbortSignal,
|
|
@@ -1515,7 +2123,7 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1515
2123
|
return new Promise((resolve, reject) => {
|
|
1516
2124
|
const child = spawn("bash", ["-c", wrappedCommand], {
|
|
1517
2125
|
cwd,
|
|
1518
|
-
env,
|
|
2126
|
+
env: withSandboxDefaultEnv(env),
|
|
1519
2127
|
detached: true,
|
|
1520
2128
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1521
2129
|
});
|
|
@@ -1524,6 +2132,7 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1524
2132
|
let timedOut = false;
|
|
1525
2133
|
let interruptedByFilesystemViolation = false;
|
|
1526
2134
|
let seenViolationCount = existingViolationCount;
|
|
2135
|
+
const runtimeProtectedWriteViolations = new Map<string, FilesystemViolation>();
|
|
1527
2136
|
let timeoutHandle: NodeJS.Timeout | undefined;
|
|
1528
2137
|
let timeoutEscalationHandle: NodeJS.Timeout | undefined;
|
|
1529
2138
|
let filesystemStopEscalationHandle: NodeJS.Timeout | undefined;
|
|
@@ -1538,11 +2147,9 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1538
2147
|
}, 500);
|
|
1539
2148
|
};
|
|
1540
2149
|
|
|
1541
|
-
//
|
|
1542
|
-
//
|
|
1543
|
-
//
|
|
1544
|
-
// but there is no Linux implementation yet:
|
|
1545
|
-
// 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.
|
|
1546
2153
|
const unsubscribeViolations =
|
|
1547
2154
|
process.platform !== "darwin"
|
|
1548
2155
|
? () => undefined
|
|
@@ -1554,14 +2161,23 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1554
2161
|
const newViolations = violations.slice(seenViolationCount);
|
|
1555
2162
|
seenViolationCount = violations.length;
|
|
1556
2163
|
|
|
1557
|
-
const
|
|
1558
|
-
const shouldStop = newViolations.some((violation) => {
|
|
2164
|
+
for (const violation of newViolations) {
|
|
1559
2165
|
const filesystemViolation = detectFilesystemViolationFromLine(violation.line);
|
|
1560
|
-
if (
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
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
|
+
}
|
|
1565
2181
|
}
|
|
1566
2182
|
});
|
|
1567
2183
|
|
|
@@ -1624,11 +2240,22 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1624
2240
|
exitCode: interruptedByFilesystemViolation && code === null ? 1 : code,
|
|
1625
2241
|
combinedOutput: Buffer.concat(chunks).toString("utf-8"),
|
|
1626
2242
|
interruptedByFilesystemViolation,
|
|
2243
|
+
runtimeProtectedWriteViolations: Array.from(runtimeProtectedWriteViolations.values()),
|
|
1627
2244
|
});
|
|
1628
2245
|
});
|
|
1629
2246
|
});
|
|
1630
2247
|
}
|
|
1631
2248
|
|
|
2249
|
+
function validateTimeout(timeout: number | undefined): void {
|
|
2250
|
+
if (timeout === undefined) return;
|
|
2251
|
+
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
2252
|
+
throw new Error("Invalid timeout: must be a finite number of seconds");
|
|
2253
|
+
}
|
|
2254
|
+
if (timeout * 1000 > MAX_TIMEOUT_MS) {
|
|
2255
|
+
throw new Error(`Invalid timeout: maximum is ${MAX_TIMEOUT_SECONDS} seconds`);
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
|
|
1632
2259
|
function getRemainingTimeout(timeout: number | undefined, startedAt: number): number | undefined {
|
|
1633
2260
|
if (timeout === undefined) return undefined;
|
|
1634
2261
|
|
|
@@ -1661,7 +2288,13 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1661
2288
|
applyRuntimeConfigForSession,
|
|
1662
2289
|
});
|
|
1663
2290
|
|
|
1664
|
-
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
|
+
);
|
|
1665
2298
|
const existingViolationCount =
|
|
1666
2299
|
SandboxManager.getSandboxViolationStore().getViolationsForCommand(command).length;
|
|
1667
2300
|
|
|
@@ -1670,13 +2303,14 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1670
2303
|
command,
|
|
1671
2304
|
wrappedCommand,
|
|
1672
2305
|
cwd,
|
|
2306
|
+
attemptRuntimeConfig,
|
|
1673
2307
|
onData,
|
|
1674
2308
|
existingViolationCount,
|
|
1675
2309
|
signal,
|
|
1676
2310
|
timeout,
|
|
1677
2311
|
env,
|
|
1678
2312
|
);
|
|
1679
|
-
return { attempt, existingViolationCount };
|
|
2313
|
+
return { attempt, existingViolationCount, runtimeConfig: attemptRuntimeConfig };
|
|
1680
2314
|
} catch (err) {
|
|
1681
2315
|
safeCleanupAfterCommand();
|
|
1682
2316
|
throw err;
|
|
@@ -1688,42 +2322,95 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1688
2322
|
command: string;
|
|
1689
2323
|
cwd: string;
|
|
1690
2324
|
existingViolationCount: number;
|
|
2325
|
+
runtimeConfig: SandboxRuntimeConfig | null;
|
|
1691
2326
|
autoRetryAvailable: boolean;
|
|
1692
2327
|
}): Promise<ProcessedSandboxAttempt> {
|
|
1693
|
-
const { attempt, command, cwd, existingViolationCount, autoRetryAvailable } =
|
|
1694
|
-
|
|
1695
|
-
if (commandSucceeded) {
|
|
1696
|
-
return { exitCode: attempt.exitCode, postamble: "", resolution: null };
|
|
1697
|
-
}
|
|
1698
|
-
|
|
2328
|
+
const { attempt, command, cwd, existingViolationCount, runtimeConfig, autoRetryAvailable } =
|
|
2329
|
+
options;
|
|
1699
2330
|
const annotatedOutput = SandboxManager.annotateStderrWithSandboxFailures(
|
|
1700
2331
|
command,
|
|
1701
2332
|
attempt.combinedOutput,
|
|
1702
2333
|
);
|
|
1703
|
-
|
|
1704
|
-
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(
|
|
1705
2343
|
runtimeConfig,
|
|
1706
|
-
|
|
2344
|
+
[...attempt.runtimeProtectedWriteViolations, ...storedFilesystemViolations],
|
|
1707
2345
|
cwd,
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
const effectiveExitCode = metadataTraversalPaths ? 0 : attempt.exitCode;
|
|
2346
|
+
);
|
|
2347
|
+
const machLookupViolations = detectMachLookupViolations(storedViolationLines);
|
|
1711
2348
|
let postamble = extractAppendedSandboxAnnotation(
|
|
1712
2349
|
attempt.combinedOutput,
|
|
1713
2350
|
annotatedOutput,
|
|
1714
2351
|
existingViolationCount,
|
|
1715
2352
|
);
|
|
1716
|
-
let resolution: FilesystemViolationResolution | null = null;
|
|
1717
2353
|
|
|
1718
|
-
if (
|
|
1719
|
-
const notice =
|
|
2354
|
+
if (runtimeProtectedWriteViolations.length > 0) {
|
|
2355
|
+
const notice = formatRuntimeProtectedWriteNotice(
|
|
2356
|
+
runtimeProtectedWriteViolations,
|
|
2357
|
+
!attempt.interruptedByFilesystemViolation,
|
|
2358
|
+
);
|
|
1720
2359
|
postamble = appendOutputPostamble(postamble, notice, attempt.combinedOutput);
|
|
1721
|
-
|
|
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
|
+
|
|
1722
2409
|
resolution = await handleFilesystemViolation({
|
|
1723
2410
|
pi,
|
|
1724
2411
|
ctx: getContext(),
|
|
1725
2412
|
promptMode: getPromptMode(),
|
|
1726
|
-
runtimeConfig,
|
|
2413
|
+
runtimeConfig: currentRuntimeConfig,
|
|
1727
2414
|
output: annotatedOutput,
|
|
1728
2415
|
rawOutput: attempt.combinedOutput,
|
|
1729
2416
|
command,
|
|
@@ -1733,18 +2420,42 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1733
2420
|
existingViolationCount,
|
|
1734
2421
|
recordEvent,
|
|
1735
2422
|
autoRetryAvailable,
|
|
2423
|
+
runtimeProtectedWriteViolations,
|
|
2424
|
+
allowOutputFallback: machLookupViolations.length === 0,
|
|
1736
2425
|
});
|
|
1737
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
|
+
|
|
1738
2443
|
if (resolution) {
|
|
1739
2444
|
postamble = appendOutputPostamble(postamble, resolution.message, attempt.combinedOutput);
|
|
1740
2445
|
}
|
|
1741
2446
|
}
|
|
1742
2447
|
|
|
1743
|
-
return {
|
|
2448
|
+
return {
|
|
2449
|
+
exitCode: effectiveExitCode,
|
|
2450
|
+
postamble,
|
|
2451
|
+
resolution,
|
|
2452
|
+
runtimeProtectedWriteViolations,
|
|
2453
|
+
};
|
|
1744
2454
|
}
|
|
1745
2455
|
|
|
1746
2456
|
return {
|
|
1747
2457
|
async exec(command, cwd, { onData, signal, timeout, env }) {
|
|
2458
|
+
validateTimeout(timeout);
|
|
1748
2459
|
return runSerially(async () => {
|
|
1749
2460
|
if (!existsSync(cwd)) {
|
|
1750
2461
|
throw new Error(`Working directory does not exist: ${cwd}`);
|
|
@@ -1767,6 +2478,7 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1767
2478
|
command,
|
|
1768
2479
|
cwd,
|
|
1769
2480
|
existingViolationCount: initialRun.existingViolationCount,
|
|
2481
|
+
runtimeConfig: initialRun.runtimeConfig,
|
|
1770
2482
|
autoRetryAvailable: true,
|
|
1771
2483
|
});
|
|
1772
2484
|
} catch (postProcessError) {
|
|
@@ -1811,6 +2523,7 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1811
2523
|
command,
|
|
1812
2524
|
cwd,
|
|
1813
2525
|
existingViolationCount: retryRun.existingViolationCount,
|
|
2526
|
+
runtimeConfig: retryRun.runtimeConfig,
|
|
1814
2527
|
autoRetryAvailable: false,
|
|
1815
2528
|
});
|
|
1816
2529
|
} catch (postProcessError) {
|
|
@@ -1826,7 +2539,10 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1826
2539
|
retryResolution.retrySuccessMessage,
|
|
1827
2540
|
retryRun.attempt.combinedOutput,
|
|
1828
2541
|
);
|
|
1829
|
-
} else if (
|
|
2542
|
+
} else if (
|
|
2543
|
+
!processedRetry.resolution &&
|
|
2544
|
+
processedRetry.runtimeProtectedWriteViolations.length === 0
|
|
2545
|
+
) {
|
|
1830
2546
|
retryPostamble = appendOutputPostamble(
|
|
1831
2547
|
retryPostamble,
|
|
1832
2548
|
retryResolution.retryFailureMessage,
|
|
@@ -1849,7 +2565,7 @@ function getSandboxRunMode(state: SandboxState): SandboxRunMode {
|
|
|
1849
2565
|
}
|
|
1850
2566
|
|
|
1851
2567
|
function getStateRuntimeConfig(state: SandboxState): SandboxRuntimeConfig | null {
|
|
1852
|
-
if (state.status === "active"
|
|
2568
|
+
if (state.status === "active") return state.runtimeConfig;
|
|
1853
2569
|
return null;
|
|
1854
2570
|
}
|
|
1855
2571
|
|
|
@@ -1917,6 +2633,9 @@ function describeFilesystemEventSummary(
|
|
|
1917
2633
|
|
|
1918
2634
|
if (reason === "explicit-deny-read") return "filesystem read matched a deny-read rule";
|
|
1919
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
|
+
}
|
|
1920
2639
|
if (reason === "already-approved-still-failed") {
|
|
1921
2640
|
return "filesystem access was previously allowed for this session but is still failing";
|
|
1922
2641
|
}
|
|
@@ -1985,7 +2704,12 @@ function renderSandboxDoctorReport(options: {
|
|
|
1985
2704
|
} else {
|
|
1986
2705
|
lines.push("- Config paths:");
|
|
1987
2706
|
for (const configPath of configPaths) {
|
|
1988
|
-
const status =
|
|
2707
|
+
const status =
|
|
2708
|
+
configPath.status === "loaded"
|
|
2709
|
+
? "loaded"
|
|
2710
|
+
: configPath.status === "skipped-untrusted"
|
|
2711
|
+
? "skipped (project not trusted)"
|
|
2712
|
+
: "parse error";
|
|
1989
2713
|
lines.push(` - ${configPath.label}: ${configPath.path} (${status})`);
|
|
1990
2714
|
}
|
|
1991
2715
|
}
|
|
@@ -2017,6 +2741,10 @@ function getSandboxConfigParseErrors(paths: SandboxConfigPath[]): SandboxConfigP
|
|
|
2017
2741
|
return paths.filter((configPath) => configPath.status === "parse-error");
|
|
2018
2742
|
}
|
|
2019
2743
|
|
|
2744
|
+
function getSkippedUntrustedProjectConfigPaths(paths: SandboxConfigPath[]): SandboxConfigPath[] {
|
|
2745
|
+
return paths.filter((configPath) => configPath.status === "skipped-untrusted");
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2020
2748
|
function notifySandboxConfigParseErrors(ctx: ExtensionContext, paths: SandboxConfigPath[]): void {
|
|
2021
2749
|
const details = paths
|
|
2022
2750
|
.map((configPath) => `${configPath.label.toLowerCase()} (${configPath.path})`)
|
|
@@ -2042,7 +2770,7 @@ function loadSandboxConfigForContext(
|
|
|
2042
2770
|
const { exitOnError = false } = options;
|
|
2043
2771
|
|
|
2044
2772
|
try {
|
|
2045
|
-
return loadConfig(cwd, overrideConfigPath);
|
|
2773
|
+
return loadConfig(cwd, overrideConfigPath, { projectTrusted: ctx.isProjectTrusted() });
|
|
2046
2774
|
} catch (error) {
|
|
2047
2775
|
if (!(error instanceof SandboxConfigLoadError)) throw error;
|
|
2048
2776
|
|
|
@@ -2083,6 +2811,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2083
2811
|
let sessionContext: ExtensionContext | null = null;
|
|
2084
2812
|
let sandboxConfigPaths: SandboxConfigPath[] = [];
|
|
2085
2813
|
let sandboxEvents: SandboxEvent[] = [];
|
|
2814
|
+
const warnedSkippedProjectConfigPaths = new Set<string>();
|
|
2086
2815
|
|
|
2087
2816
|
const pendingNetworkApprovals = new Map<string, Promise<boolean>>();
|
|
2088
2817
|
|
|
@@ -2127,21 +2856,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
2127
2856
|
});
|
|
2128
2857
|
}
|
|
2129
2858
|
|
|
2859
|
+
function notifySkippedUntrustedProjectConfigs(ctx: ExtensionContext): void {
|
|
2860
|
+
const skippedConfigs = getSkippedUntrustedProjectConfigPaths(sandboxConfigPaths).filter(
|
|
2861
|
+
(configPath) => !warnedSkippedProjectConfigPaths.has(configPath.path),
|
|
2862
|
+
);
|
|
2863
|
+
if (skippedConfigs.length === 0) return;
|
|
2864
|
+
|
|
2865
|
+
for (const configPath of skippedConfigs) {
|
|
2866
|
+
warnedSkippedProjectConfigPaths.add(configPath.path);
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
const details = skippedConfigs.map((configPath) => configPath.path).join(", ");
|
|
2870
|
+
notify(
|
|
2871
|
+
ctx,
|
|
2872
|
+
`Ignoring project sandbox config because this project is not trusted: ${details}`,
|
|
2873
|
+
"warning",
|
|
2874
|
+
);
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2130
2877
|
function applyRuntimeConfigForSession(
|
|
2131
2878
|
ctx: ExtensionContext,
|
|
2132
2879
|
runtimeConfig: SandboxRuntimeConfig,
|
|
2133
|
-
targetStatus: "active" | "suspended" = sandboxState.status === "suspended"
|
|
2134
|
-
? "suspended"
|
|
2135
|
-
: "active",
|
|
2136
2880
|
): void {
|
|
2137
2881
|
const nextConfig = cloneRuntimeConfig(runtimeConfig);
|
|
2138
|
-
sandboxState = { status:
|
|
2882
|
+
sandboxState = { status: "active", runtimeConfig: nextConfig };
|
|
2139
2883
|
SandboxManager.updateConfig(nextConfig);
|
|
2140
|
-
setSandboxStatus(ctx,
|
|
2884
|
+
setSandboxStatus(ctx, true, nextConfig, promptMode);
|
|
2141
2885
|
}
|
|
2142
2886
|
|
|
2143
2887
|
const createNetworkAskCallback = (): SandboxAskCallback => {
|
|
2144
2888
|
return async ({ host, port }) => {
|
|
2889
|
+
if (sandboxState.status === "suspended") return true;
|
|
2890
|
+
|
|
2145
2891
|
const normalizedHost = host.toLowerCase();
|
|
2146
2892
|
const key = normalizedHost;
|
|
2147
2893
|
|
|
@@ -2355,6 +3101,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2355
3101
|
if (!loadedConfig) return;
|
|
2356
3102
|
|
|
2357
3103
|
sandboxConfigPaths = loadedConfig.paths;
|
|
3104
|
+
notifySkippedUntrustedProjectConfigs(ctx);
|
|
2358
3105
|
const parseErrors = getSandboxConfigParseErrors(sandboxConfigPaths);
|
|
2359
3106
|
if (parseErrors.length > 0) {
|
|
2360
3107
|
notifySandboxConfigParseErrors(ctx, parseErrors);
|
|
@@ -2444,47 +3191,34 @@ export default function (pi: ExtensionAPI) {
|
|
|
2444
3191
|
return;
|
|
2445
3192
|
}
|
|
2446
3193
|
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
"unsupported-platform",
|
|
2458
|
-
`sandbox not supported on ${process.platform}`,
|
|
2459
|
-
);
|
|
2460
|
-
notify(ctx, `Sandbox not supported on ${process.platform}`, "warning");
|
|
2461
|
-
return;
|
|
2462
|
-
}
|
|
3194
|
+
if (!isSupportedPlatform()) {
|
|
3195
|
+
sandboxState = { status: "blocked", reason: "unsupported-platform" };
|
|
3196
|
+
recordRuntimeEvent(
|
|
3197
|
+
"init",
|
|
3198
|
+
"unsupported-platform",
|
|
3199
|
+
`sandbox not supported on ${process.platform}`,
|
|
3200
|
+
);
|
|
3201
|
+
notify(ctx, `Sandbox not supported on ${process.platform}`, "warning");
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
2463
3204
|
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
3205
|
+
const sandboxConfigOverride = getStringFlag(pi, "sandbox-config");
|
|
3206
|
+
const loadedConfig = loadSandboxConfigForContext(ctx, ctx.cwd, sandboxConfigOverride);
|
|
3207
|
+
if (!loadedConfig) return;
|
|
2467
3208
|
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
sandboxConfig = loadedConfig.config;
|
|
2474
|
-
runtimeConfig = await initializeSandboxRuntime(ctx, loadedConfig.config);
|
|
2475
|
-
if (!runtimeConfig) return;
|
|
2476
|
-
initializedNow = true;
|
|
3209
|
+
sandboxConfigPaths = loadedConfig.paths;
|
|
3210
|
+
notifySkippedUntrustedProjectConfigs(ctx);
|
|
3211
|
+
const parseErrors = getSandboxConfigParseErrors(sandboxConfigPaths);
|
|
3212
|
+
if (parseErrors.length > 0) {
|
|
3213
|
+
notifySandboxConfigParseErrors(ctx, parseErrors);
|
|
2477
3214
|
}
|
|
2478
|
-
|
|
3215
|
+
sandboxConfig = loadedConfig.config;
|
|
3216
|
+
const runtimeConfig = await initializeSandboxRuntime(ctx, loadedConfig.config);
|
|
2479
3217
|
if (!runtimeConfig) return;
|
|
2480
3218
|
|
|
2481
|
-
|
|
2482
|
-
setSandboxStatus(ctx, true, runtimeConfig, promptMode);
|
|
2483
|
-
} else {
|
|
2484
|
-
applyRuntimeConfigForSession(ctx, runtimeConfig, "active");
|
|
2485
|
-
}
|
|
3219
|
+
setSandboxStatus(ctx, true, runtimeConfig, promptMode);
|
|
2486
3220
|
|
|
2487
|
-
|
|
3221
|
+
announceSandboxState(pi, ctx, true);
|
|
2488
3222
|
return;
|
|
2489
3223
|
}
|
|
2490
3224
|
|
|
@@ -2499,9 +3233,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
2499
3233
|
return;
|
|
2500
3234
|
}
|
|
2501
3235
|
|
|
2502
|
-
sandboxState = { status: "suspended"
|
|
3236
|
+
sandboxState = { status: "suspended" };
|
|
3237
|
+
pendingNetworkApprovals.clear();
|
|
2503
3238
|
setSandboxStatus(ctx, false);
|
|
2504
|
-
|
|
3239
|
+
|
|
3240
|
+
try {
|
|
3241
|
+
await SandboxManager.reset();
|
|
3242
|
+
} catch (error) {
|
|
3243
|
+
notify(
|
|
3244
|
+
ctx,
|
|
3245
|
+
`Sandbox disabled, but cleanup failed: ${error instanceof Error ? error.message : error}`,
|
|
3246
|
+
"warning",
|
|
3247
|
+
);
|
|
3248
|
+
return;
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
announceSandboxState(pi, ctx, false);
|
|
2505
3252
|
return;
|
|
2506
3253
|
}
|
|
2507
3254
|
|
|
@@ -2529,9 +3276,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
2529
3276
|
` allowLocalBinding: ${runtimeConfig.network.allowLocalBinding ? "true" : "false"}`,
|
|
2530
3277
|
` allowAllUnixSockets: ${runtimeConfig.network.allowAllUnixSockets ? "true" : "false"}`,
|
|
2531
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
|
+
: []),
|
|
2532
3286
|
"",
|
|
2533
3287
|
" Filesystem:",
|
|
2534
3288
|
` Deny Read: ${runtimeConfig.filesystem.denyRead.join(", ") || "(none)"}`,
|
|
3289
|
+
` Allow Read: ${runtimeConfig.filesystem.allowRead?.join(", ") || "(none)"}`,
|
|
2535
3290
|
` Allow Write: ${runtimeConfig.filesystem.allowWrite.join(", ") || "(none)"}`,
|
|
2536
3291
|
` Deny Write: ${runtimeConfig.filesystem.denyWrite.join(", ") || "(none)"}`,
|
|
2537
3292
|
` allowTempDirs: ${sandboxConfig?.filesystem.allowTempDirs ? "true" : "false"}`,
|
|
@@ -2606,6 +3361,42 @@ export default function (pi: ExtensionAPI) {
|
|
|
2606
3361
|
return;
|
|
2607
3362
|
}
|
|
2608
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
|
+
|
|
2609
3400
|
if (subcommand === "filesystem") {
|
|
2610
3401
|
const runtimeConfig = requireRuntimeConfig(ctx, sandboxState);
|
|
2611
3402
|
if (!runtimeConfig) return;
|