tau-coding-agent 0.1.3 → 0.1.5
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 +49 -43
- package/extensions/branch-term.ts +11 -16
- package/extensions/btw.ts +8 -8
- package/extensions/ghostty.ts +65 -12
- package/extensions/git-diff-stats.ts +1 -1
- package/extensions/git-pr-status.ts +1 -1
- package/extensions/insights.ts +6 -5
- package/extensions/loop.ts +39 -11
- package/extensions/memory.ts +6 -5
- package/extensions/notify.ts +25 -6
- 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 +66 -0
- package/extensions/review/submit-triage-tool.ts +59 -0
- package/extensions/review/triage.ts +746 -0
- package/extensions/sandbox/index.ts +428 -103
- package/extensions/{interlude.ts → stash.ts} +13 -11
- package/extensions/tool-display-mode.ts +173 -17
- package/extensions/usage/anthropic.ts +1 -1
- package/extensions/usage/github-copilot.ts +1 -1
- package/extensions/usage/index.ts +9 -5
- package/extensions/usage/minimax.ts +1 -1
- package/extensions/usage/openai-codex.ts +1 -1
- package/extensions/usage/openrouter.ts +1 -1
- package/extensions/usage/providers.ts +0 -2
- package/extensions/usage/types.ts +1 -2
- package/extensions/usage/zai.ts +1 -1
- 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 +9 -9
- package/extensions/websearch/providers/anthropic.pi.ts +1 -1
- package/extensions/websearch/providers/gemini.pi.ts +1 -1
- package/extensions/websearch/providers/openai-codex.pi.ts +1 -1
- package/extensions/websearch/providers/pi-model.shared.ts +2 -2
- package/extensions/worktree.ts +36 -36
- package/package.json +7 -7
- package/skills/git-clean-history/SKILL.md +1 -0
- package/skills/git-commit/SKILL.md +2 -2
- package/skills/oracle/scripts/oracle +28 -12
- package/themes/tau-dark.json +2 -1
- package/extensions/review.ts +0 -4347
- package/extensions/usage/google-gemini-cli.ts +0 -232
|
@@ -25,8 +25,9 @@
|
|
|
25
25
|
* },
|
|
26
26
|
* "filesystem": {
|
|
27
27
|
* "denyRead": ["~/.ssh", "~/.aws"],
|
|
28
|
-
* "allowWrite": ["."
|
|
28
|
+
* "allowWrite": ["."],
|
|
29
29
|
* "denyWrite": [".env"],
|
|
30
|
+
* "allowTempDirs": true,
|
|
30
31
|
* "allowGitCommonDir": true
|
|
31
32
|
* }
|
|
32
33
|
* }
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
|
|
48
49
|
import { spawn, spawnSync } from "node:child_process";
|
|
49
50
|
import { existsSync, readFileSync } from "node:fs";
|
|
50
|
-
import { homedir } from "node:os";
|
|
51
|
+
import { homedir, tmpdir } from "node:os";
|
|
51
52
|
import { join, resolve } from "node:path";
|
|
52
53
|
import {
|
|
53
54
|
SandboxManager,
|
|
@@ -62,12 +63,20 @@ import {
|
|
|
62
63
|
globToRegex,
|
|
63
64
|
normalizePathForSandbox,
|
|
64
65
|
} from "@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-utils.js";
|
|
65
|
-
import
|
|
66
|
-
|
|
66
|
+
import {
|
|
67
|
+
CONFIG_DIR_NAME,
|
|
68
|
+
createBashTool,
|
|
69
|
+
getAgentDir,
|
|
70
|
+
type BashOperations,
|
|
71
|
+
type ExtensionAPI,
|
|
72
|
+
type ExtensionContext,
|
|
73
|
+
} from "@earendil-works/pi-coding-agent";
|
|
67
74
|
|
|
68
75
|
// --- Constants ---
|
|
69
76
|
|
|
70
77
|
const DEFAULT_PROMPT_MODE: PromptMode = "interactive";
|
|
78
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
79
|
+
const MAX_TIMEOUT_SECONDS = MAX_TIMEOUT_MS / 1000;
|
|
71
80
|
|
|
72
81
|
const DEFAULT_CONFIG: SandboxConfig = {
|
|
73
82
|
enabled: true,
|
|
@@ -78,30 +87,43 @@ const DEFAULT_CONFIG: SandboxConfig = {
|
|
|
78
87
|
"127.0.0.1",
|
|
79
88
|
"npmjs.org",
|
|
80
89
|
"*.npmjs.org",
|
|
81
|
-
"
|
|
90
|
+
"npmjs.com",
|
|
91
|
+
"*.npmjs.com",
|
|
82
92
|
"registry.yarnpkg.com",
|
|
93
|
+
"nodejs.org",
|
|
94
|
+
"*.nodejs.org",
|
|
83
95
|
"pypi.org",
|
|
84
96
|
"*.pypi.org",
|
|
85
97
|
"pythonhosted.org",
|
|
86
98
|
"*.pythonhosted.org",
|
|
87
99
|
"crates.io",
|
|
88
100
|
"*.crates.io",
|
|
101
|
+
"rustup.rs",
|
|
102
|
+
"*.rust-lang.org",
|
|
103
|
+
"repo.maven.apache.org",
|
|
104
|
+
"gradle.org",
|
|
105
|
+
"*.gradle.org",
|
|
89
106
|
"proxy.golang.org",
|
|
90
107
|
"sum.golang.org",
|
|
91
108
|
"go.dev",
|
|
92
109
|
"golang.org",
|
|
93
110
|
"rubygems.org",
|
|
94
111
|
"*.rubygems.org",
|
|
112
|
+
"nuget.org",
|
|
113
|
+
"*.nuget.org",
|
|
95
114
|
"github.com",
|
|
96
115
|
"*.github.com",
|
|
97
116
|
"githubusercontent.com",
|
|
98
117
|
"*.githubusercontent.com",
|
|
99
|
-
"
|
|
100
|
-
"
|
|
118
|
+
"gitlab.com",
|
|
119
|
+
"*.gitlab.com",
|
|
120
|
+
"bitbucket.org",
|
|
121
|
+
"*.bitbucket.org",
|
|
101
122
|
"ghcr.io",
|
|
102
123
|
"docker.io",
|
|
103
124
|
"*.docker.io",
|
|
104
|
-
"
|
|
125
|
+
"docker.com",
|
|
126
|
+
"*.docker.com",
|
|
105
127
|
"sentry.io",
|
|
106
128
|
"*.sentry.io",
|
|
107
129
|
"datadoghq.com",
|
|
@@ -116,6 +138,8 @@ const DEFAULT_CONFIG: SandboxConfig = {
|
|
|
116
138
|
"*.openai.com",
|
|
117
139
|
"chatgpt.com",
|
|
118
140
|
"*.chatgpt.com",
|
|
141
|
+
"openrouter.ai",
|
|
142
|
+
"*.openrouter.ai",
|
|
119
143
|
"google.com",
|
|
120
144
|
"*.google.com",
|
|
121
145
|
"googleapis.com",
|
|
@@ -128,21 +152,64 @@ const DEFAULT_CONFIG: SandboxConfig = {
|
|
|
128
152
|
"*.doist.com",
|
|
129
153
|
],
|
|
130
154
|
deniedDomains: [],
|
|
155
|
+
allowUnixSockets: ["$SSH_AUTH_SOCK"],
|
|
131
156
|
allowLocalBinding: true,
|
|
132
157
|
},
|
|
133
158
|
filesystem: {
|
|
134
159
|
denyRead: ["~/.ssh", "~/.aws", "~/.gnupg"],
|
|
135
|
-
|
|
136
|
-
|
|
160
|
+
allowRead: ["~/.ssh/config", "~/.ssh/known_hosts", "~/.ssh/*.pub"],
|
|
161
|
+
allowWrite: [
|
|
162
|
+
".",
|
|
163
|
+
"~/.cache",
|
|
164
|
+
"~/Library/Caches",
|
|
165
|
+
join(getAgentDir(), "*.lock"),
|
|
166
|
+
"~/**/__pycache__",
|
|
167
|
+
"~/**/__pycache__/*",
|
|
168
|
+
"~/.npm",
|
|
169
|
+
"~/.rustup",
|
|
170
|
+
"~/.cargo/registry",
|
|
171
|
+
"~/.cargo/git",
|
|
172
|
+
"~/.cargo/.package-cache",
|
|
173
|
+
"~/.cargo/.package-cache-mutate",
|
|
174
|
+
"~/.cargo/.global-cache",
|
|
175
|
+
"~/go/pkg",
|
|
176
|
+
"~/.m2/repository",
|
|
177
|
+
"~/.m2/wrapper/dists",
|
|
178
|
+
"~/.gradle",
|
|
179
|
+
"~/Library/Application Support/kotlin",
|
|
180
|
+
"~/.android",
|
|
181
|
+
"~/.bundle/cache",
|
|
182
|
+
"~/.gem/cache",
|
|
183
|
+
"~/.gem/specs",
|
|
184
|
+
"~/.nuget/packages",
|
|
185
|
+
"~/.local/share/NuGet/*-cache",
|
|
186
|
+
"~/.local/share/NuGet/*-cache/**",
|
|
187
|
+
],
|
|
188
|
+
denyWrite: [
|
|
189
|
+
".env",
|
|
190
|
+
".env.*",
|
|
191
|
+
"*.pem",
|
|
192
|
+
"*.key",
|
|
193
|
+
"~/.gradle/gradle.properties",
|
|
194
|
+
"~/.gradle/init.gradle",
|
|
195
|
+
"~/.gradle/init.gradle.kts",
|
|
196
|
+
"~/.gradle/init.d",
|
|
197
|
+
"~/.android/adbkey",
|
|
198
|
+
],
|
|
199
|
+
allowTempDirs: true,
|
|
137
200
|
allowGitConfig: true,
|
|
138
201
|
allowGitCommonDir: true,
|
|
139
202
|
},
|
|
203
|
+
ignoreViolations: {
|
|
204
|
+
"*": ["/__pycache__"],
|
|
205
|
+
},
|
|
140
206
|
};
|
|
141
207
|
|
|
142
208
|
const STATUS_KEY = "sandbox";
|
|
143
209
|
const SANDBOX_EVENT_LIMIT = 50;
|
|
144
|
-
const
|
|
210
|
+
const READ_TRAVERSAL_PROCESSES = new Set(["find", "ls", "fd", "fdfind"]);
|
|
145
211
|
const GIT_FILESYSTEM_PATHS_CACHE = new Map<string, GitFilesystemPaths | null>();
|
|
212
|
+
const ENV_PATH_REFERENCE_PATTERN = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g;
|
|
146
213
|
|
|
147
214
|
// --- Types ---
|
|
148
215
|
|
|
@@ -156,7 +223,7 @@ type SandboxRunMode = "sandbox" | "user-disabled" | SandboxBypassReason | Sandbo
|
|
|
156
223
|
type SandboxState =
|
|
157
224
|
| { status: "pending" }
|
|
158
225
|
| { status: "active"; runtimeConfig: SandboxRuntimeConfig }
|
|
159
|
-
| { status: "suspended"
|
|
226
|
+
| { status: "suspended" }
|
|
160
227
|
| { status: "bypassed"; reason: SandboxBypassReason }
|
|
161
228
|
| { status: "blocked"; reason: SandboxBlockedReason };
|
|
162
229
|
|
|
@@ -164,6 +231,7 @@ type SandboxConfig = Omit<SandboxRuntimeConfig, "filesystem"> & {
|
|
|
164
231
|
enabled?: boolean;
|
|
165
232
|
mode?: PromptMode;
|
|
166
233
|
filesystem: SandboxRuntimeConfig["filesystem"] & {
|
|
234
|
+
allowTempDirs?: boolean;
|
|
167
235
|
allowGitCommonDir?: boolean;
|
|
168
236
|
};
|
|
169
237
|
};
|
|
@@ -181,7 +249,7 @@ type SandboxEventReason =
|
|
|
181
249
|
| "already-approved-still-failed"
|
|
182
250
|
| "unknown";
|
|
183
251
|
type SandboxEventOutcome = "blocked" | "allowed";
|
|
184
|
-
type SandboxConfigPathStatus = "loaded" | "parse-error";
|
|
252
|
+
type SandboxConfigPathStatus = "loaded" | "parse-error" | "skipped-untrusted";
|
|
185
253
|
type SandboxConfigPathLabel = "Global" | "Project" | "Override";
|
|
186
254
|
|
|
187
255
|
type PromptStatus = "completed" | "error";
|
|
@@ -193,6 +261,7 @@ type FilesystemList = "deny-read" | "allow-write" | "deny-write";
|
|
|
193
261
|
|
|
194
262
|
type FilesystemViolationKind = "read" | "write" | "unknown";
|
|
195
263
|
type FilesystemReadAccess = "metadata" | "data" | "unknown";
|
|
264
|
+
type FilesystemWriteAccess = "unlink" | "unknown";
|
|
196
265
|
type FilesystemViolationResolutionKind = "allow-retry" | "allow-adapt" | "deny";
|
|
197
266
|
|
|
198
267
|
interface SandboxEvent {
|
|
@@ -242,6 +311,7 @@ interface FilesystemViolation {
|
|
|
242
311
|
path?: string;
|
|
243
312
|
processName?: string;
|
|
244
313
|
readAccess?: FilesystemReadAccess;
|
|
314
|
+
writeAccess?: FilesystemWriteAccess;
|
|
245
315
|
}
|
|
246
316
|
|
|
247
317
|
type FilesystemViolationResolution =
|
|
@@ -572,12 +642,7 @@ function formatSandboxConfigLoadErrorMessage(
|
|
|
572
642
|
return `Could not read sandbox override config ${path}: ${detail ?? "unknown error"}`;
|
|
573
643
|
}
|
|
574
644
|
|
|
575
|
-
function
|
|
576
|
-
if (!Array.isArray(value)) {
|
|
577
|
-
console.error(`Warning: Expected ${field} to be a string[]; using defaults.`);
|
|
578
|
-
return [...fallback];
|
|
579
|
-
}
|
|
580
|
-
|
|
645
|
+
function cleanStringArray(value: unknown[], field: string): string[] {
|
|
581
646
|
const cleaned = value
|
|
582
647
|
.filter((item): item is string => typeof item === "string")
|
|
583
648
|
.map((item) => item.trim())
|
|
@@ -591,6 +656,30 @@ function coerceStringArray(value: unknown, fallback: string[], field: string): s
|
|
|
591
656
|
return cleaned;
|
|
592
657
|
}
|
|
593
658
|
|
|
659
|
+
function coerceStringArray(value: unknown, fallback: string[], field: string): string[] {
|
|
660
|
+
if (!Array.isArray(value)) {
|
|
661
|
+
console.error(`Warning: Expected ${field} to be a string[]; using defaults.`);
|
|
662
|
+
return [...fallback];
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return cleanStringArray(value, field);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function coerceOptionalStringArray(
|
|
669
|
+
value: unknown,
|
|
670
|
+
fallback: string[] | undefined,
|
|
671
|
+
field: string,
|
|
672
|
+
): string[] | undefined {
|
|
673
|
+
if (value === undefined) return fallback ? [...fallback] : undefined;
|
|
674
|
+
|
|
675
|
+
if (!Array.isArray(value)) {
|
|
676
|
+
console.error(`Warning: Expected ${field} to be a string[]; using defaults.`);
|
|
677
|
+
return fallback ? [...fallback] : undefined;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return cleanStringArray(value, field);
|
|
681
|
+
}
|
|
682
|
+
|
|
594
683
|
function finalizeConfig(config: SandboxConfig): SandboxConfig {
|
|
595
684
|
return {
|
|
596
685
|
...config,
|
|
@@ -608,6 +697,11 @@ function finalizeConfig(config: SandboxConfig): SandboxConfig {
|
|
|
608
697
|
DEFAULT_CONFIG.network.deniedDomains,
|
|
609
698
|
"network.deniedDomains",
|
|
610
699
|
),
|
|
700
|
+
allowUnixSockets: coerceOptionalStringArray(
|
|
701
|
+
config.network?.allowUnixSockets,
|
|
702
|
+
DEFAULT_CONFIG.network.allowUnixSockets,
|
|
703
|
+
"network.allowUnixSockets",
|
|
704
|
+
),
|
|
611
705
|
},
|
|
612
706
|
filesystem: {
|
|
613
707
|
...config.filesystem,
|
|
@@ -616,6 +710,11 @@ function finalizeConfig(config: SandboxConfig): SandboxConfig {
|
|
|
616
710
|
DEFAULT_CONFIG.filesystem.denyRead,
|
|
617
711
|
"filesystem.denyRead",
|
|
618
712
|
),
|
|
713
|
+
allowRead: coerceOptionalStringArray(
|
|
714
|
+
config.filesystem?.allowRead,
|
|
715
|
+
DEFAULT_CONFIG.filesystem.allowRead,
|
|
716
|
+
"filesystem.allowRead",
|
|
717
|
+
),
|
|
619
718
|
allowWrite: coerceStringArray(
|
|
620
719
|
config.filesystem?.allowWrite,
|
|
621
720
|
DEFAULT_CONFIG.filesystem.allowWrite,
|
|
@@ -626,6 +725,10 @@ function finalizeConfig(config: SandboxConfig): SandboxConfig {
|
|
|
626
725
|
DEFAULT_CONFIG.filesystem.denyWrite,
|
|
627
726
|
"filesystem.denyWrite",
|
|
628
727
|
),
|
|
728
|
+
allowTempDirs:
|
|
729
|
+
typeof config.filesystem?.allowTempDirs === "boolean"
|
|
730
|
+
? config.filesystem.allowTempDirs
|
|
731
|
+
: DEFAULT_CONFIG.filesystem.allowTempDirs,
|
|
629
732
|
allowGitCommonDir:
|
|
630
733
|
typeof config.filesystem?.allowGitCommonDir === "boolean"
|
|
631
734
|
? config.filesystem.allowGitCommonDir
|
|
@@ -673,13 +776,17 @@ function loadOverrideConfig(cwd: string, overrideConfigPath: string): LoadedSand
|
|
|
673
776
|
};
|
|
674
777
|
}
|
|
675
778
|
|
|
676
|
-
function loadConfig(
|
|
779
|
+
function loadConfig(
|
|
780
|
+
cwd: string,
|
|
781
|
+
overrideConfigPath?: string,
|
|
782
|
+
options: { projectTrusted?: boolean } = {},
|
|
783
|
+
): LoadedSandboxConfig {
|
|
677
784
|
if (overrideConfigPath) {
|
|
678
785
|
return loadOverrideConfig(cwd, overrideConfigPath);
|
|
679
786
|
}
|
|
680
787
|
|
|
681
|
-
const projectConfigPath = join(cwd,
|
|
682
|
-
const globalConfigPath = join(
|
|
788
|
+
const projectConfigPath = join(cwd, CONFIG_DIR_NAME, "sandbox.json");
|
|
789
|
+
const globalConfigPath = join(getAgentDir(), "sandbox.json");
|
|
683
790
|
|
|
684
791
|
let globalConfig: Partial<SandboxConfig> = {};
|
|
685
792
|
let projectConfig: Partial<SandboxConfig> = {};
|
|
@@ -696,12 +803,16 @@ function loadConfig(cwd: string, overrideConfigPath?: string): LoadedSandboxConf
|
|
|
696
803
|
}
|
|
697
804
|
|
|
698
805
|
if (existsSync(projectConfigPath)) {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
806
|
+
if (options.projectTrusted !== true) {
|
|
807
|
+
paths.push({ label: "Project", path: projectConfigPath, status: "skipped-untrusted" });
|
|
808
|
+
} else {
|
|
809
|
+
try {
|
|
810
|
+
projectConfig = JSON.parse(readFileSync(projectConfigPath, "utf-8"));
|
|
811
|
+
paths.push({ label: "Project", path: projectConfigPath, status: "loaded" });
|
|
812
|
+
} catch (e) {
|
|
813
|
+
paths.push({ label: "Project", path: projectConfigPath, status: "parse-error" });
|
|
814
|
+
console.error(`Warning: Could not parse ${projectConfigPath}: ${e}`);
|
|
815
|
+
}
|
|
705
816
|
}
|
|
706
817
|
}
|
|
707
818
|
|
|
@@ -749,12 +860,145 @@ function deepMerge(base: SandboxConfig, overrides: Partial<SandboxConfig>): Sand
|
|
|
749
860
|
return result;
|
|
750
861
|
}
|
|
751
862
|
|
|
863
|
+
let cachedTemporaryWritePaths: string[] | undefined;
|
|
864
|
+
|
|
865
|
+
function getTemporaryWritePaths(): string[] {
|
|
866
|
+
if (cachedTemporaryWritePaths) return cachedTemporaryWritePaths;
|
|
867
|
+
|
|
868
|
+
// allowTempDirs always includes the conventional shared temp path, even when
|
|
869
|
+
// os.tmpdir() points at a platform-specific per-user directory.
|
|
870
|
+
const currentTmpDir = tmpdir();
|
|
871
|
+
const paths = ["/tmp", currentTmpDir, normalizePathForSandbox(currentTmpDir)];
|
|
872
|
+
if (process.platform === "darwin") paths.push("/private/tmp");
|
|
873
|
+
|
|
874
|
+
cachedTemporaryWritePaths = Array.from(
|
|
875
|
+
new Set(paths.map((path) => path.replace(/\/+$/, "") || "/")),
|
|
876
|
+
);
|
|
877
|
+
return cachedTemporaryWritePaths;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function deduplicateStrings(values: string[]): string[] {
|
|
881
|
+
return Array.from(new Set(values));
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function deduplicateOptionalStrings(values: string[] | undefined): string[] | undefined {
|
|
885
|
+
return values ? deduplicateStrings(values) : undefined;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function containsControlCharacter(value: string): boolean {
|
|
889
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
890
|
+
const code = value.charCodeAt(index);
|
|
891
|
+
if (code <= 0x1f || (code >= 0x7f && code <= 0x9f) || code === 0x2028 || code === 0x2029) {
|
|
892
|
+
return true;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function expandPathConfigEntry(value: string, field: string): string | null {
|
|
900
|
+
const source = value.trim();
|
|
901
|
+
if (!source) return null;
|
|
902
|
+
|
|
903
|
+
const missingEnvNames = new Set<string>();
|
|
904
|
+
const expanded = source.replace(
|
|
905
|
+
ENV_PATH_REFERENCE_PATTERN,
|
|
906
|
+
(_match, bracedName: string | undefined, bareName: string | undefined) => {
|
|
907
|
+
const envName = bracedName ?? bareName;
|
|
908
|
+
const envValue = envName ? (process.env[envName]?.trim() ?? "") : "";
|
|
909
|
+
if (!envValue) {
|
|
910
|
+
if (envName) missingEnvNames.add(envName);
|
|
911
|
+
return "";
|
|
912
|
+
}
|
|
913
|
+
return envValue;
|
|
914
|
+
},
|
|
915
|
+
);
|
|
916
|
+
|
|
917
|
+
if (missingEnvNames.size > 0) {
|
|
918
|
+
const names = Array.from(missingEnvNames).join(", ");
|
|
919
|
+
const label = missingEnvNames.size === 1 ? "environment variable" : "environment variables";
|
|
920
|
+
const verb = missingEnvNames.size === 1 ? "is" : "are";
|
|
921
|
+
console.error(
|
|
922
|
+
`Warning: Ignoring ${field} entry because ${label} ${names} ${verb} unset or empty.`,
|
|
923
|
+
);
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (containsControlCharacter(expanded)) {
|
|
928
|
+
console.error(
|
|
929
|
+
`Warning: Ignoring ${field} entry because the expanded path contains control characters.`,
|
|
930
|
+
);
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return expanded;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function expandPathConfigList(values: string[] | undefined, field: string): string[] | undefined {
|
|
938
|
+
if (!values) return undefined;
|
|
939
|
+
|
|
940
|
+
return values
|
|
941
|
+
.map((value) => expandPathConfigEntry(value, field))
|
|
942
|
+
.filter((value): value is string => value !== null);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function expandMitmProxyPathConfig(
|
|
946
|
+
mitmProxy: SandboxRuntimeConfig["network"]["mitmProxy"],
|
|
947
|
+
): SandboxRuntimeConfig["network"]["mitmProxy"] {
|
|
948
|
+
if (!mitmProxy) return undefined;
|
|
949
|
+
if (!isPlainObject(mitmProxy) || typeof mitmProxy.socketPath !== "string") return mitmProxy;
|
|
950
|
+
|
|
951
|
+
const socketPath = expandPathConfigEntry(mitmProxy.socketPath, "network.mitmProxy.socketPath");
|
|
952
|
+
if (!socketPath) {
|
|
953
|
+
console.error("Warning: Disabling network.mitmProxy because its socketPath did not expand.");
|
|
954
|
+
return undefined;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
return { ...mitmProxy, socketPath } as SandboxRuntimeConfig["network"]["mitmProxy"];
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function expandNetworkPathConfig(
|
|
961
|
+
network: SandboxRuntimeConfig["network"],
|
|
962
|
+
): SandboxRuntimeConfig["network"] {
|
|
963
|
+
return {
|
|
964
|
+
...network,
|
|
965
|
+
allowUnixSockets: expandPathConfigList(network.allowUnixSockets, "network.allowUnixSockets"),
|
|
966
|
+
mitmProxy: expandMitmProxyPathConfig(network.mitmProxy),
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function expandFilesystemPathConfig(
|
|
971
|
+
filesystem: SandboxRuntimeConfig["filesystem"],
|
|
972
|
+
): SandboxRuntimeConfig["filesystem"] {
|
|
973
|
+
return {
|
|
974
|
+
...filesystem,
|
|
975
|
+
denyRead: expandPathConfigList(filesystem.denyRead, "filesystem.denyRead") ?? [],
|
|
976
|
+
allowRead: expandPathConfigList(filesystem.allowRead, "filesystem.allowRead"),
|
|
977
|
+
allowWrite: expandPathConfigList(filesystem.allowWrite, "filesystem.allowWrite") ?? [],
|
|
978
|
+
denyWrite: expandPathConfigList(filesystem.denyWrite, "filesystem.denyWrite") ?? [],
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
|
|
752
982
|
function toRuntimeConfig(config: SandboxConfig): SandboxRuntimeConfig {
|
|
753
|
-
const { allowGitCommonDir: _allowGitCommonDir, ...filesystem } = config.filesystem;
|
|
983
|
+
const { allowGitCommonDir: _allowGitCommonDir, allowTempDirs, ...filesystem } = config.filesystem;
|
|
984
|
+
const expandedNetwork = expandNetworkPathConfig(config.network);
|
|
985
|
+
const expandedFilesystem = expandFilesystemPathConfig(filesystem);
|
|
986
|
+
const allowWrite = allowTempDirs
|
|
987
|
+
? [...expandedFilesystem.allowWrite, ...getTemporaryWritePaths()]
|
|
988
|
+
: expandedFilesystem.allowWrite;
|
|
754
989
|
|
|
755
990
|
return {
|
|
756
|
-
network:
|
|
757
|
-
|
|
991
|
+
network: {
|
|
992
|
+
...expandedNetwork,
|
|
993
|
+
allowUnixSockets: deduplicateOptionalStrings(expandedNetwork.allowUnixSockets),
|
|
994
|
+
},
|
|
995
|
+
filesystem: {
|
|
996
|
+
...expandedFilesystem,
|
|
997
|
+
denyRead: deduplicateStrings(expandedFilesystem.denyRead),
|
|
998
|
+
allowRead: deduplicateOptionalStrings(expandedFilesystem.allowRead),
|
|
999
|
+
allowWrite: deduplicateStrings(allowWrite),
|
|
1000
|
+
denyWrite: deduplicateStrings(expandedFilesystem.denyWrite),
|
|
1001
|
+
},
|
|
758
1002
|
ignoreViolations: config.ignoreViolations,
|
|
759
1003
|
enableWeakerNestedSandbox: config.enableWeakerNestedSandbox,
|
|
760
1004
|
enableWeakerNetworkIsolation: config.enableWeakerNetworkIsolation,
|
|
@@ -791,6 +1035,19 @@ function inferSandboxRuleMatch(path: string, rules: string[], cwd?: string): str
|
|
|
791
1035
|
return null;
|
|
792
1036
|
}
|
|
793
1037
|
|
|
1038
|
+
function matchesSandboxRuleExactly(path: string, rule: string, cwd?: string): boolean {
|
|
1039
|
+
if (containsGlobChars(rule)) return false;
|
|
1040
|
+
return normalizeSandboxPath(path) === normalizeSandboxPath(rule, cwd);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function inferExactSandboxRuleMatch(path: string, rules: string[], cwd?: string): string | null {
|
|
1044
|
+
for (const rule of rules) {
|
|
1045
|
+
if (matchesSandboxRuleExactly(path, rule, cwd)) return rule;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
794
1051
|
function isSandboxWritablePath(
|
|
795
1052
|
runtimeConfig: SandboxRuntimeConfig,
|
|
796
1053
|
path: string,
|
|
@@ -930,7 +1187,9 @@ function extractPathLikeValue(text: string): string | undefined {
|
|
|
930
1187
|
|
|
931
1188
|
function extractViolationProcessName(line: string): string | undefined {
|
|
932
1189
|
const match = line.match(/^([^\s(]+)\(/);
|
|
933
|
-
|
|
1190
|
+
const processName = match?.[1]?.trim();
|
|
1191
|
+
if (!processName) return undefined;
|
|
1192
|
+
return processName.split("/").pop() || processName;
|
|
934
1193
|
}
|
|
935
1194
|
|
|
936
1195
|
function detectFilesystemViolationFromLine(line: string): FilesystemViolation | null {
|
|
@@ -939,8 +1198,12 @@ function detectFilesystemViolationFromLine(line: string): FilesystemViolation |
|
|
|
939
1198
|
const path = extractPathLikeValue(line);
|
|
940
1199
|
const processName = extractViolationProcessName(line);
|
|
941
1200
|
|
|
1201
|
+
if (lower.includes("file-write-unlink")) {
|
|
1202
|
+
return { kind: "write", path, processName, writeAccess: "unlink" };
|
|
1203
|
+
}
|
|
1204
|
+
|
|
942
1205
|
if (lower.includes("file-write")) {
|
|
943
|
-
return { kind: "write", path, processName };
|
|
1206
|
+
return { kind: "write", path, processName, writeAccess: "unknown" };
|
|
944
1207
|
}
|
|
945
1208
|
|
|
946
1209
|
if (lower.includes("file-read-metadata")) {
|
|
@@ -989,19 +1252,30 @@ function detectFilesystemViolations(
|
|
|
989
1252
|
return violations;
|
|
990
1253
|
}
|
|
991
1254
|
|
|
992
|
-
function
|
|
1255
|
+
function isTraversalViolation(
|
|
993
1256
|
runtimeConfig: SandboxRuntimeConfig | null,
|
|
994
1257
|
violation: FilesystemViolation,
|
|
995
1258
|
cwd?: string,
|
|
996
1259
|
): boolean {
|
|
997
|
-
if (!runtimeConfig || violation.
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1260
|
+
if (!runtimeConfig || !violation.path) return false;
|
|
1261
|
+
if (!READ_TRAVERSAL_PROCESSES.has(violation.processName ?? "")) return false;
|
|
1262
|
+
|
|
1263
|
+
if (violation.kind === "read") {
|
|
1264
|
+
return inferSandboxRuleMatch(violation.path, runtimeConfig.filesystem.denyRead, cwd) !== null;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
if (violation.kind !== "write" || violation.writeAccess !== "unlink") return false;
|
|
1268
|
+
|
|
1269
|
+
// Seatbelt can emit file-write-unlink checks for protected directory roots
|
|
1270
|
+
// while traversal commands enumerate them. Only exact protected roots are
|
|
1271
|
+
// treated as skipped traversal so writes under protected trees still prompt.
|
|
1272
|
+
return (
|
|
1273
|
+
inferExactSandboxRuleMatch(violation.path, runtimeConfig.filesystem.denyRead, cwd) !== null ||
|
|
1274
|
+
inferExactSandboxRuleMatch(violation.path, runtimeConfig.filesystem.denyWrite, cwd) !== null
|
|
1275
|
+
);
|
|
1002
1276
|
}
|
|
1003
1277
|
|
|
1004
|
-
function
|
|
1278
|
+
function getTraversalPaths(options: {
|
|
1005
1279
|
runtimeConfig: SandboxRuntimeConfig | null;
|
|
1006
1280
|
output: string;
|
|
1007
1281
|
cwd?: string;
|
|
@@ -1020,7 +1294,7 @@ function getMetadataTraversalPaths(options: {
|
|
|
1020
1294
|
const skippedPaths: string[] = [];
|
|
1021
1295
|
for (const line of violationLines) {
|
|
1022
1296
|
const violation = detectFilesystemViolationFromLine(line);
|
|
1023
|
-
if (!violation || !
|
|
1297
|
+
if (!violation || !isTraversalViolation(runtimeConfig, violation, cwd)) {
|
|
1024
1298
|
return null;
|
|
1025
1299
|
}
|
|
1026
1300
|
if (violation.path && !skippedPaths.includes(violation.path)) {
|
|
@@ -1031,7 +1305,7 @@ function getMetadataTraversalPaths(options: {
|
|
|
1031
1305
|
return skippedPaths.length > 0 ? skippedPaths : null;
|
|
1032
1306
|
}
|
|
1033
1307
|
|
|
1034
|
-
function
|
|
1308
|
+
function formatTraversalNotice(paths: string[]): string {
|
|
1035
1309
|
if (paths.length === 0) return "";
|
|
1036
1310
|
|
|
1037
1311
|
const visiblePaths = paths.slice(0, 3).join(", ");
|
|
@@ -1184,32 +1458,36 @@ function parseFilesystemPromptSelection(
|
|
|
1184
1458
|
return "deny";
|
|
1185
1459
|
}
|
|
1186
1460
|
|
|
1187
|
-
function formatFilesystemAllowRetryMessage(
|
|
1188
|
-
return
|
|
1461
|
+
function formatFilesystemAllowRetryMessage(target: string): string {
|
|
1462
|
+
return `\n${formatFilesystemBlockedTarget(target)}\n\nGranting access and retrying the command per user request...\n\n`;
|
|
1189
1463
|
}
|
|
1190
1464
|
|
|
1191
|
-
function formatFilesystemAllowAdaptMessage(
|
|
1192
|
-
return
|
|
1465
|
+
function formatFilesystemAllowAdaptMessage(target: string): string {
|
|
1466
|
+
return `\n${formatFilesystemBlockedTarget(target)}\n\nAccess granted for this session. Retry the command manually if appropriate.`;
|
|
1193
1467
|
}
|
|
1194
1468
|
|
|
1195
|
-
function formatFilesystemDeniedMessage(
|
|
1196
|
-
return
|
|
1469
|
+
function formatFilesystemDeniedMessage(target: string): string {
|
|
1470
|
+
return `\n${formatFilesystemBlockedTarget(target)}\n\nAccess remains denied for this session.`;
|
|
1197
1471
|
}
|
|
1198
1472
|
|
|
1199
|
-
function formatFilesystemAlreadyAllowedMessage(
|
|
1200
|
-
return
|
|
1473
|
+
function formatFilesystemAlreadyAllowedMessage(target: string): string {
|
|
1474
|
+
return `\n${formatFilesystemBlockedTarget(target)}\n\nAccess had already been granted for this session. The remaining failure may be unrelated to sandbox policy.`;
|
|
1201
1475
|
}
|
|
1202
1476
|
|
|
1203
1477
|
function formatFilesystemRetrySucceededMessage(_target: string): string {
|
|
1204
1478
|
return "";
|
|
1205
1479
|
}
|
|
1206
1480
|
|
|
1207
|
-
function formatFilesystemRetryFailedMessage(
|
|
1208
|
-
return
|
|
1481
|
+
function formatFilesystemRetryFailedMessage(target: string): string {
|
|
1482
|
+
return `\n${formatFilesystemBlockedTarget(target)}\n\nAccess granted for this session and the command was retried per user request, but the command still exited non-zero. The sandbox block was resolved; the remaining failure may be unrelated.`;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
function formatFilesystemRetrySkippedMessage(target: string): string {
|
|
1486
|
+
return `\n${formatFilesystemBlockedTarget(target)}\n\nAccess granted for this session, but automatic retry was skipped because the timeout was exhausted. Retry the command manually if needed.`;
|
|
1209
1487
|
}
|
|
1210
1488
|
|
|
1211
|
-
function
|
|
1212
|
-
return
|
|
1489
|
+
function formatFilesystemBlockedTarget(target: string): string {
|
|
1490
|
+
return `Sandbox blocked filesystem ${target}.`;
|
|
1213
1491
|
}
|
|
1214
1492
|
|
|
1215
1493
|
function appendOutputPostamble(postamble: string, addition: string, output: string): string {
|
|
@@ -1475,6 +1753,12 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1475
1753
|
return run;
|
|
1476
1754
|
}
|
|
1477
1755
|
|
|
1756
|
+
function withSandboxDefaultEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
1757
|
+
const baseEnv = env ?? process.env;
|
|
1758
|
+
if (baseEnv.GIT_OPTIONAL_LOCKS !== undefined) return baseEnv;
|
|
1759
|
+
return { ...baseEnv, GIT_OPTIONAL_LOCKS: "0" };
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1478
1762
|
async function runSandboxAttempt(
|
|
1479
1763
|
command: string,
|
|
1480
1764
|
wrappedCommand: string,
|
|
@@ -1488,7 +1772,7 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1488
1772
|
return new Promise((resolve, reject) => {
|
|
1489
1773
|
const child = spawn("bash", ["-c", wrappedCommand], {
|
|
1490
1774
|
cwd,
|
|
1491
|
-
env,
|
|
1775
|
+
env: withSandboxDefaultEnv(env),
|
|
1492
1776
|
detached: true,
|
|
1493
1777
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1494
1778
|
});
|
|
@@ -1531,7 +1815,7 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1531
1815
|
const shouldStop = newViolations.some((violation) => {
|
|
1532
1816
|
const filesystemViolation = detectFilesystemViolationFromLine(violation.line);
|
|
1533
1817
|
if (!filesystemViolation) return false;
|
|
1534
|
-
return !
|
|
1818
|
+
return !isTraversalViolation(runtimeConfig, filesystemViolation, cwd);
|
|
1535
1819
|
});
|
|
1536
1820
|
if (shouldStop) {
|
|
1537
1821
|
stopForFilesystemViolation();
|
|
@@ -1602,6 +1886,16 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1602
1886
|
});
|
|
1603
1887
|
}
|
|
1604
1888
|
|
|
1889
|
+
function validateTimeout(timeout: number | undefined): void {
|
|
1890
|
+
if (timeout === undefined) return;
|
|
1891
|
+
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
1892
|
+
throw new Error("Invalid timeout: must be a finite number of seconds");
|
|
1893
|
+
}
|
|
1894
|
+
if (timeout * 1000 > MAX_TIMEOUT_MS) {
|
|
1895
|
+
throw new Error(`Invalid timeout: maximum is ${MAX_TIMEOUT_SECONDS} seconds`);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1605
1899
|
function getRemainingTimeout(timeout: number | undefined, startedAt: number): number | undefined {
|
|
1606
1900
|
if (timeout === undefined) return undefined;
|
|
1607
1901
|
|
|
@@ -1674,13 +1968,13 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1674
1968
|
attempt.combinedOutput,
|
|
1675
1969
|
);
|
|
1676
1970
|
const runtimeConfig = getRuntimeConfig();
|
|
1677
|
-
const
|
|
1971
|
+
const traversalPaths = getTraversalPaths({
|
|
1678
1972
|
runtimeConfig,
|
|
1679
1973
|
output: annotatedOutput,
|
|
1680
1974
|
cwd,
|
|
1681
1975
|
skipViolationLines: existingViolationCount,
|
|
1682
1976
|
});
|
|
1683
|
-
const effectiveExitCode =
|
|
1977
|
+
const effectiveExitCode = traversalPaths ? 0 : attempt.exitCode;
|
|
1684
1978
|
let postamble = extractAppendedSandboxAnnotation(
|
|
1685
1979
|
attempt.combinedOutput,
|
|
1686
1980
|
annotatedOutput,
|
|
@@ -1688,8 +1982,8 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1688
1982
|
);
|
|
1689
1983
|
let resolution: FilesystemViolationResolution | null = null;
|
|
1690
1984
|
|
|
1691
|
-
if (
|
|
1692
|
-
const notice =
|
|
1985
|
+
if (traversalPaths) {
|
|
1986
|
+
const notice = formatTraversalNotice(traversalPaths);
|
|
1693
1987
|
postamble = appendOutputPostamble(postamble, notice, attempt.combinedOutput);
|
|
1694
1988
|
} else if (runtimeConfig) {
|
|
1695
1989
|
resolution = await handleFilesystemViolation({
|
|
@@ -1718,6 +2012,7 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1718
2012
|
|
|
1719
2013
|
return {
|
|
1720
2014
|
async exec(command, cwd, { onData, signal, timeout, env }) {
|
|
2015
|
+
validateTimeout(timeout);
|
|
1721
2016
|
return runSerially(async () => {
|
|
1722
2017
|
if (!existsSync(cwd)) {
|
|
1723
2018
|
throw new Error(`Working directory does not exist: ${cwd}`);
|
|
@@ -1822,7 +2117,7 @@ function getSandboxRunMode(state: SandboxState): SandboxRunMode {
|
|
|
1822
2117
|
}
|
|
1823
2118
|
|
|
1824
2119
|
function getStateRuntimeConfig(state: SandboxState): SandboxRuntimeConfig | null {
|
|
1825
|
-
if (state.status === "active"
|
|
2120
|
+
if (state.status === "active") return state.runtimeConfig;
|
|
1826
2121
|
return null;
|
|
1827
2122
|
}
|
|
1828
2123
|
|
|
@@ -1958,7 +2253,12 @@ function renderSandboxDoctorReport(options: {
|
|
|
1958
2253
|
} else {
|
|
1959
2254
|
lines.push("- Config paths:");
|
|
1960
2255
|
for (const configPath of configPaths) {
|
|
1961
|
-
const status =
|
|
2256
|
+
const status =
|
|
2257
|
+
configPath.status === "loaded"
|
|
2258
|
+
? "loaded"
|
|
2259
|
+
: configPath.status === "skipped-untrusted"
|
|
2260
|
+
? "skipped (project not trusted)"
|
|
2261
|
+
: "parse error";
|
|
1962
2262
|
lines.push(` - ${configPath.label}: ${configPath.path} (${status})`);
|
|
1963
2263
|
}
|
|
1964
2264
|
}
|
|
@@ -1990,6 +2290,10 @@ function getSandboxConfigParseErrors(paths: SandboxConfigPath[]): SandboxConfigP
|
|
|
1990
2290
|
return paths.filter((configPath) => configPath.status === "parse-error");
|
|
1991
2291
|
}
|
|
1992
2292
|
|
|
2293
|
+
function getSkippedUntrustedProjectConfigPaths(paths: SandboxConfigPath[]): SandboxConfigPath[] {
|
|
2294
|
+
return paths.filter((configPath) => configPath.status === "skipped-untrusted");
|
|
2295
|
+
}
|
|
2296
|
+
|
|
1993
2297
|
function notifySandboxConfigParseErrors(ctx: ExtensionContext, paths: SandboxConfigPath[]): void {
|
|
1994
2298
|
const details = paths
|
|
1995
2299
|
.map((configPath) => `${configPath.label.toLowerCase()} (${configPath.path})`)
|
|
@@ -2015,7 +2319,7 @@ function loadSandboxConfigForContext(
|
|
|
2015
2319
|
const { exitOnError = false } = options;
|
|
2016
2320
|
|
|
2017
2321
|
try {
|
|
2018
|
-
return loadConfig(cwd, overrideConfigPath);
|
|
2322
|
+
return loadConfig(cwd, overrideConfigPath, { projectTrusted: ctx.isProjectTrusted() });
|
|
2019
2323
|
} catch (error) {
|
|
2020
2324
|
if (!(error instanceof SandboxConfigLoadError)) throw error;
|
|
2021
2325
|
|
|
@@ -2056,6 +2360,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2056
2360
|
let sessionContext: ExtensionContext | null = null;
|
|
2057
2361
|
let sandboxConfigPaths: SandboxConfigPath[] = [];
|
|
2058
2362
|
let sandboxEvents: SandboxEvent[] = [];
|
|
2363
|
+
const warnedSkippedProjectConfigPaths = new Set<string>();
|
|
2059
2364
|
|
|
2060
2365
|
const pendingNetworkApprovals = new Map<string, Promise<boolean>>();
|
|
2061
2366
|
|
|
@@ -2100,21 +2405,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
2100
2405
|
});
|
|
2101
2406
|
}
|
|
2102
2407
|
|
|
2408
|
+
function notifySkippedUntrustedProjectConfigs(ctx: ExtensionContext): void {
|
|
2409
|
+
const skippedConfigs = getSkippedUntrustedProjectConfigPaths(sandboxConfigPaths).filter(
|
|
2410
|
+
(configPath) => !warnedSkippedProjectConfigPaths.has(configPath.path),
|
|
2411
|
+
);
|
|
2412
|
+
if (skippedConfigs.length === 0) return;
|
|
2413
|
+
|
|
2414
|
+
for (const configPath of skippedConfigs) {
|
|
2415
|
+
warnedSkippedProjectConfigPaths.add(configPath.path);
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
const details = skippedConfigs.map((configPath) => configPath.path).join(", ");
|
|
2419
|
+
notify(
|
|
2420
|
+
ctx,
|
|
2421
|
+
`Ignoring project sandbox config because this project is not trusted: ${details}`,
|
|
2422
|
+
"warning",
|
|
2423
|
+
);
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2103
2426
|
function applyRuntimeConfigForSession(
|
|
2104
2427
|
ctx: ExtensionContext,
|
|
2105
2428
|
runtimeConfig: SandboxRuntimeConfig,
|
|
2106
|
-
targetStatus: "active" | "suspended" = sandboxState.status === "suspended"
|
|
2107
|
-
? "suspended"
|
|
2108
|
-
: "active",
|
|
2109
2429
|
): void {
|
|
2110
2430
|
const nextConfig = cloneRuntimeConfig(runtimeConfig);
|
|
2111
|
-
sandboxState = { status:
|
|
2431
|
+
sandboxState = { status: "active", runtimeConfig: nextConfig };
|
|
2112
2432
|
SandboxManager.updateConfig(nextConfig);
|
|
2113
|
-
setSandboxStatus(ctx,
|
|
2433
|
+
setSandboxStatus(ctx, true, nextConfig, promptMode);
|
|
2114
2434
|
}
|
|
2115
2435
|
|
|
2116
2436
|
const createNetworkAskCallback = (): SandboxAskCallback => {
|
|
2117
2437
|
return async ({ host, port }) => {
|
|
2438
|
+
if (sandboxState.status === "suspended") return true;
|
|
2439
|
+
|
|
2118
2440
|
const normalizedHost = host.toLowerCase();
|
|
2119
2441
|
const key = normalizedHost;
|
|
2120
2442
|
|
|
@@ -2328,6 +2650,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2328
2650
|
if (!loadedConfig) return;
|
|
2329
2651
|
|
|
2330
2652
|
sandboxConfigPaths = loadedConfig.paths;
|
|
2653
|
+
notifySkippedUntrustedProjectConfigs(ctx);
|
|
2331
2654
|
const parseErrors = getSandboxConfigParseErrors(sandboxConfigPaths);
|
|
2332
2655
|
if (parseErrors.length > 0) {
|
|
2333
2656
|
notifySandboxConfigParseErrors(ctx, parseErrors);
|
|
@@ -2417,45 +2740,32 @@ export default function (pi: ExtensionAPI) {
|
|
|
2417
2740
|
return;
|
|
2418
2741
|
}
|
|
2419
2742
|
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
"unsupported-platform",
|
|
2431
|
-
`sandbox not supported on ${process.platform}`,
|
|
2432
|
-
);
|
|
2433
|
-
notify(ctx, `Sandbox not supported on ${process.platform}`, "warning");
|
|
2434
|
-
return;
|
|
2435
|
-
}
|
|
2743
|
+
if (!isSupportedPlatform()) {
|
|
2744
|
+
sandboxState = { status: "blocked", reason: "unsupported-platform" };
|
|
2745
|
+
recordRuntimeEvent(
|
|
2746
|
+
"init",
|
|
2747
|
+
"unsupported-platform",
|
|
2748
|
+
`sandbox not supported on ${process.platform}`,
|
|
2749
|
+
);
|
|
2750
|
+
notify(ctx, `Sandbox not supported on ${process.platform}`, "warning");
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2436
2753
|
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2754
|
+
const sandboxConfigOverride = getStringFlag(pi, "sandbox-config");
|
|
2755
|
+
const loadedConfig = loadSandboxConfigForContext(ctx, ctx.cwd, sandboxConfigOverride);
|
|
2756
|
+
if (!loadedConfig) return;
|
|
2440
2757
|
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
sandboxConfig = loadedConfig.config;
|
|
2447
|
-
runtimeConfig = await initializeSandboxRuntime(ctx, loadedConfig.config);
|
|
2448
|
-
if (!runtimeConfig) return;
|
|
2449
|
-
initializedNow = true;
|
|
2758
|
+
sandboxConfigPaths = loadedConfig.paths;
|
|
2759
|
+
notifySkippedUntrustedProjectConfigs(ctx);
|
|
2760
|
+
const parseErrors = getSandboxConfigParseErrors(sandboxConfigPaths);
|
|
2761
|
+
if (parseErrors.length > 0) {
|
|
2762
|
+
notifySandboxConfigParseErrors(ctx, parseErrors);
|
|
2450
2763
|
}
|
|
2451
|
-
|
|
2764
|
+
sandboxConfig = loadedConfig.config;
|
|
2765
|
+
const runtimeConfig = await initializeSandboxRuntime(ctx, loadedConfig.config);
|
|
2452
2766
|
if (!runtimeConfig) return;
|
|
2453
2767
|
|
|
2454
|
-
|
|
2455
|
-
setSandboxStatus(ctx, true, runtimeConfig, promptMode);
|
|
2456
|
-
} else {
|
|
2457
|
-
applyRuntimeConfigForSession(ctx, runtimeConfig, "active");
|
|
2458
|
-
}
|
|
2768
|
+
setSandboxStatus(ctx, true, runtimeConfig, promptMode);
|
|
2459
2769
|
|
|
2460
2770
|
notify(ctx, "Sandbox enabled", "info");
|
|
2461
2771
|
return;
|
|
@@ -2472,8 +2782,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
2472
2782
|
return;
|
|
2473
2783
|
}
|
|
2474
2784
|
|
|
2475
|
-
sandboxState = { status: "suspended"
|
|
2785
|
+
sandboxState = { status: "suspended" };
|
|
2786
|
+
pendingNetworkApprovals.clear();
|
|
2476
2787
|
setSandboxStatus(ctx, false);
|
|
2788
|
+
|
|
2789
|
+
try {
|
|
2790
|
+
await SandboxManager.reset();
|
|
2791
|
+
} catch (error) {
|
|
2792
|
+
notify(
|
|
2793
|
+
ctx,
|
|
2794
|
+
`Sandbox disabled, but cleanup failed: ${error instanceof Error ? error.message : error}`,
|
|
2795
|
+
"warning",
|
|
2796
|
+
);
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2477
2800
|
notify(ctx, "Sandbox disabled", "info");
|
|
2478
2801
|
return;
|
|
2479
2802
|
}
|
|
@@ -2505,8 +2828,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
2505
2828
|
"",
|
|
2506
2829
|
" Filesystem:",
|
|
2507
2830
|
` Deny Read: ${runtimeConfig.filesystem.denyRead.join(", ") || "(none)"}`,
|
|
2831
|
+
` Allow Read: ${runtimeConfig.filesystem.allowRead?.join(", ") || "(none)"}`,
|
|
2508
2832
|
` Allow Write: ${runtimeConfig.filesystem.allowWrite.join(", ") || "(none)"}`,
|
|
2509
2833
|
` Deny Write: ${runtimeConfig.filesystem.denyWrite.join(", ") || "(none)"}`,
|
|
2834
|
+
` allowTempDirs: ${sandboxConfig?.filesystem.allowTempDirs ? "true" : "false"}`,
|
|
2510
2835
|
` allowGitConfig: ${runtimeConfig.filesystem.allowGitConfig ? "true" : "false"}`,
|
|
2511
2836
|
` allowGitCommonDir: ${sandboxConfig?.filesystem.allowGitCommonDir ? "true" : "false"}`,
|
|
2512
2837
|
"",
|