tau-coding-agent 0.1.4 → 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 +48 -42
- package/extensions/branch-term.ts +8 -13
- package/extensions/btw.ts +4 -4
- package/extensions/ghostty.ts +64 -11
- package/extensions/insights.ts +4 -3
- package/extensions/loop.ts +37 -9
- package/extensions/memory.ts +5 -10
- 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 +66 -0
- package/extensions/review/submit-triage-tool.ts +59 -0
- package/extensions/review/triage.ts +746 -0
- package/extensions/sandbox/index.ts +399 -102
- package/extensions/{interlude.ts → stash.ts} +13 -11
- package/extensions/tool-display-mode.ts +2 -3
- package/extensions/usage/index.ts +8 -4
- 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/worktree.ts +33 -33
- package/package.json +3 -3
- package/skills/git-clean-history/SKILL.md +1 -0
- package/skills/git-commit/SKILL.md +2 -2
- package/skills/oracle/scripts/oracle +28 -11
- package/themes/tau-dark.json +2 -1
- package/extensions/review.ts +0 -4420
|
@@ -63,12 +63,20 @@ import {
|
|
|
63
63
|
globToRegex,
|
|
64
64
|
normalizePathForSandbox,
|
|
65
65
|
} from "@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-utils.js";
|
|
66
|
-
import
|
|
67
|
-
|
|
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";
|
|
68
74
|
|
|
69
75
|
// --- Constants ---
|
|
70
76
|
|
|
71
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;
|
|
72
80
|
|
|
73
81
|
const DEFAULT_CONFIG: SandboxConfig = {
|
|
74
82
|
enabled: true,
|
|
@@ -79,30 +87,43 @@ const DEFAULT_CONFIG: SandboxConfig = {
|
|
|
79
87
|
"127.0.0.1",
|
|
80
88
|
"npmjs.org",
|
|
81
89
|
"*.npmjs.org",
|
|
82
|
-
"
|
|
90
|
+
"npmjs.com",
|
|
91
|
+
"*.npmjs.com",
|
|
83
92
|
"registry.yarnpkg.com",
|
|
93
|
+
"nodejs.org",
|
|
94
|
+
"*.nodejs.org",
|
|
84
95
|
"pypi.org",
|
|
85
96
|
"*.pypi.org",
|
|
86
97
|
"pythonhosted.org",
|
|
87
98
|
"*.pythonhosted.org",
|
|
88
99
|
"crates.io",
|
|
89
100
|
"*.crates.io",
|
|
101
|
+
"rustup.rs",
|
|
102
|
+
"*.rust-lang.org",
|
|
103
|
+
"repo.maven.apache.org",
|
|
104
|
+
"gradle.org",
|
|
105
|
+
"*.gradle.org",
|
|
90
106
|
"proxy.golang.org",
|
|
91
107
|
"sum.golang.org",
|
|
92
108
|
"go.dev",
|
|
93
109
|
"golang.org",
|
|
94
110
|
"rubygems.org",
|
|
95
111
|
"*.rubygems.org",
|
|
112
|
+
"nuget.org",
|
|
113
|
+
"*.nuget.org",
|
|
96
114
|
"github.com",
|
|
97
115
|
"*.github.com",
|
|
98
116
|
"githubusercontent.com",
|
|
99
117
|
"*.githubusercontent.com",
|
|
100
|
-
"
|
|
101
|
-
"
|
|
118
|
+
"gitlab.com",
|
|
119
|
+
"*.gitlab.com",
|
|
120
|
+
"bitbucket.org",
|
|
121
|
+
"*.bitbucket.org",
|
|
102
122
|
"ghcr.io",
|
|
103
123
|
"docker.io",
|
|
104
124
|
"*.docker.io",
|
|
105
|
-
"
|
|
125
|
+
"docker.com",
|
|
126
|
+
"*.docker.com",
|
|
106
127
|
"sentry.io",
|
|
107
128
|
"*.sentry.io",
|
|
108
129
|
"datadoghq.com",
|
|
@@ -117,6 +138,8 @@ const DEFAULT_CONFIG: SandboxConfig = {
|
|
|
117
138
|
"*.openai.com",
|
|
118
139
|
"chatgpt.com",
|
|
119
140
|
"*.chatgpt.com",
|
|
141
|
+
"openrouter.ai",
|
|
142
|
+
"*.openrouter.ai",
|
|
120
143
|
"google.com",
|
|
121
144
|
"*.google.com",
|
|
122
145
|
"googleapis.com",
|
|
@@ -129,22 +152,64 @@ const DEFAULT_CONFIG: SandboxConfig = {
|
|
|
129
152
|
"*.doist.com",
|
|
130
153
|
],
|
|
131
154
|
deniedDomains: [],
|
|
155
|
+
allowUnixSockets: ["$SSH_AUTH_SOCK"],
|
|
132
156
|
allowLocalBinding: true,
|
|
133
157
|
},
|
|
134
158
|
filesystem: {
|
|
135
159
|
denyRead: ["~/.ssh", "~/.aws", "~/.gnupg"],
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
],
|
|
138
199
|
allowTempDirs: true,
|
|
139
200
|
allowGitConfig: true,
|
|
140
201
|
allowGitCommonDir: true,
|
|
141
202
|
},
|
|
203
|
+
ignoreViolations: {
|
|
204
|
+
"*": ["/__pycache__"],
|
|
205
|
+
},
|
|
142
206
|
};
|
|
143
207
|
|
|
144
208
|
const STATUS_KEY = "sandbox";
|
|
145
209
|
const SANDBOX_EVENT_LIMIT = 50;
|
|
146
|
-
const
|
|
210
|
+
const READ_TRAVERSAL_PROCESSES = new Set(["find", "ls", "fd", "fdfind"]);
|
|
147
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;
|
|
148
213
|
|
|
149
214
|
// --- Types ---
|
|
150
215
|
|
|
@@ -158,7 +223,7 @@ type SandboxRunMode = "sandbox" | "user-disabled" | SandboxBypassReason | Sandbo
|
|
|
158
223
|
type SandboxState =
|
|
159
224
|
| { status: "pending" }
|
|
160
225
|
| { status: "active"; runtimeConfig: SandboxRuntimeConfig }
|
|
161
|
-
| { status: "suspended"
|
|
226
|
+
| { status: "suspended" }
|
|
162
227
|
| { status: "bypassed"; reason: SandboxBypassReason }
|
|
163
228
|
| { status: "blocked"; reason: SandboxBlockedReason };
|
|
164
229
|
|
|
@@ -184,7 +249,7 @@ type SandboxEventReason =
|
|
|
184
249
|
| "already-approved-still-failed"
|
|
185
250
|
| "unknown";
|
|
186
251
|
type SandboxEventOutcome = "blocked" | "allowed";
|
|
187
|
-
type SandboxConfigPathStatus = "loaded" | "parse-error";
|
|
252
|
+
type SandboxConfigPathStatus = "loaded" | "parse-error" | "skipped-untrusted";
|
|
188
253
|
type SandboxConfigPathLabel = "Global" | "Project" | "Override";
|
|
189
254
|
|
|
190
255
|
type PromptStatus = "completed" | "error";
|
|
@@ -196,6 +261,7 @@ type FilesystemList = "deny-read" | "allow-write" | "deny-write";
|
|
|
196
261
|
|
|
197
262
|
type FilesystemViolationKind = "read" | "write" | "unknown";
|
|
198
263
|
type FilesystemReadAccess = "metadata" | "data" | "unknown";
|
|
264
|
+
type FilesystemWriteAccess = "unlink" | "unknown";
|
|
199
265
|
type FilesystemViolationResolutionKind = "allow-retry" | "allow-adapt" | "deny";
|
|
200
266
|
|
|
201
267
|
interface SandboxEvent {
|
|
@@ -245,6 +311,7 @@ interface FilesystemViolation {
|
|
|
245
311
|
path?: string;
|
|
246
312
|
processName?: string;
|
|
247
313
|
readAccess?: FilesystemReadAccess;
|
|
314
|
+
writeAccess?: FilesystemWriteAccess;
|
|
248
315
|
}
|
|
249
316
|
|
|
250
317
|
type FilesystemViolationResolution =
|
|
@@ -575,12 +642,7 @@ function formatSandboxConfigLoadErrorMessage(
|
|
|
575
642
|
return `Could not read sandbox override config ${path}: ${detail ?? "unknown error"}`;
|
|
576
643
|
}
|
|
577
644
|
|
|
578
|
-
function
|
|
579
|
-
if (!Array.isArray(value)) {
|
|
580
|
-
console.error(`Warning: Expected ${field} to be a string[]; using defaults.`);
|
|
581
|
-
return [...fallback];
|
|
582
|
-
}
|
|
583
|
-
|
|
645
|
+
function cleanStringArray(value: unknown[], field: string): string[] {
|
|
584
646
|
const cleaned = value
|
|
585
647
|
.filter((item): item is string => typeof item === "string")
|
|
586
648
|
.map((item) => item.trim())
|
|
@@ -594,6 +656,30 @@ function coerceStringArray(value: unknown, fallback: string[], field: string): s
|
|
|
594
656
|
return cleaned;
|
|
595
657
|
}
|
|
596
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
|
+
|
|
597
683
|
function finalizeConfig(config: SandboxConfig): SandboxConfig {
|
|
598
684
|
return {
|
|
599
685
|
...config,
|
|
@@ -611,6 +697,11 @@ function finalizeConfig(config: SandboxConfig): SandboxConfig {
|
|
|
611
697
|
DEFAULT_CONFIG.network.deniedDomains,
|
|
612
698
|
"network.deniedDomains",
|
|
613
699
|
),
|
|
700
|
+
allowUnixSockets: coerceOptionalStringArray(
|
|
701
|
+
config.network?.allowUnixSockets,
|
|
702
|
+
DEFAULT_CONFIG.network.allowUnixSockets,
|
|
703
|
+
"network.allowUnixSockets",
|
|
704
|
+
),
|
|
614
705
|
},
|
|
615
706
|
filesystem: {
|
|
616
707
|
...config.filesystem,
|
|
@@ -619,6 +710,11 @@ function finalizeConfig(config: SandboxConfig): SandboxConfig {
|
|
|
619
710
|
DEFAULT_CONFIG.filesystem.denyRead,
|
|
620
711
|
"filesystem.denyRead",
|
|
621
712
|
),
|
|
713
|
+
allowRead: coerceOptionalStringArray(
|
|
714
|
+
config.filesystem?.allowRead,
|
|
715
|
+
DEFAULT_CONFIG.filesystem.allowRead,
|
|
716
|
+
"filesystem.allowRead",
|
|
717
|
+
),
|
|
622
718
|
allowWrite: coerceStringArray(
|
|
623
719
|
config.filesystem?.allowWrite,
|
|
624
720
|
DEFAULT_CONFIG.filesystem.allowWrite,
|
|
@@ -680,13 +776,17 @@ function loadOverrideConfig(cwd: string, overrideConfigPath: string): LoadedSand
|
|
|
680
776
|
};
|
|
681
777
|
}
|
|
682
778
|
|
|
683
|
-
function loadConfig(
|
|
779
|
+
function loadConfig(
|
|
780
|
+
cwd: string,
|
|
781
|
+
overrideConfigPath?: string,
|
|
782
|
+
options: { projectTrusted?: boolean } = {},
|
|
783
|
+
): LoadedSandboxConfig {
|
|
684
784
|
if (overrideConfigPath) {
|
|
685
785
|
return loadOverrideConfig(cwd, overrideConfigPath);
|
|
686
786
|
}
|
|
687
787
|
|
|
688
|
-
const projectConfigPath = join(cwd,
|
|
689
|
-
const globalConfigPath = join(
|
|
788
|
+
const projectConfigPath = join(cwd, CONFIG_DIR_NAME, "sandbox.json");
|
|
789
|
+
const globalConfigPath = join(getAgentDir(), "sandbox.json");
|
|
690
790
|
|
|
691
791
|
let globalConfig: Partial<SandboxConfig> = {};
|
|
692
792
|
let projectConfig: Partial<SandboxConfig> = {};
|
|
@@ -703,12 +803,16 @@ function loadConfig(cwd: string, overrideConfigPath?: string): LoadedSandboxConf
|
|
|
703
803
|
}
|
|
704
804
|
|
|
705
805
|
if (existsSync(projectConfigPath)) {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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
|
+
}
|
|
712
816
|
}
|
|
713
817
|
}
|
|
714
818
|
|
|
@@ -773,15 +877,128 @@ function getTemporaryWritePaths(): string[] {
|
|
|
773
877
|
return cachedTemporaryWritePaths;
|
|
774
878
|
}
|
|
775
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
|
+
|
|
776
982
|
function toRuntimeConfig(config: SandboxConfig): SandboxRuntimeConfig {
|
|
777
983
|
const { allowGitCommonDir: _allowGitCommonDir, allowTempDirs, ...filesystem } = config.filesystem;
|
|
984
|
+
const expandedNetwork = expandNetworkPathConfig(config.network);
|
|
985
|
+
const expandedFilesystem = expandFilesystemPathConfig(filesystem);
|
|
778
986
|
const allowWrite = allowTempDirs
|
|
779
|
-
?
|
|
780
|
-
:
|
|
987
|
+
? [...expandedFilesystem.allowWrite, ...getTemporaryWritePaths()]
|
|
988
|
+
: expandedFilesystem.allowWrite;
|
|
781
989
|
|
|
782
990
|
return {
|
|
783
|
-
network:
|
|
784
|
-
|
|
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
|
+
},
|
|
785
1002
|
ignoreViolations: config.ignoreViolations,
|
|
786
1003
|
enableWeakerNestedSandbox: config.enableWeakerNestedSandbox,
|
|
787
1004
|
enableWeakerNetworkIsolation: config.enableWeakerNetworkIsolation,
|
|
@@ -818,6 +1035,19 @@ function inferSandboxRuleMatch(path: string, rules: string[], cwd?: string): str
|
|
|
818
1035
|
return null;
|
|
819
1036
|
}
|
|
820
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
|
+
|
|
821
1051
|
function isSandboxWritablePath(
|
|
822
1052
|
runtimeConfig: SandboxRuntimeConfig,
|
|
823
1053
|
path: string,
|
|
@@ -957,7 +1187,9 @@ function extractPathLikeValue(text: string): string | undefined {
|
|
|
957
1187
|
|
|
958
1188
|
function extractViolationProcessName(line: string): string | undefined {
|
|
959
1189
|
const match = line.match(/^([^\s(]+)\(/);
|
|
960
|
-
|
|
1190
|
+
const processName = match?.[1]?.trim();
|
|
1191
|
+
if (!processName) return undefined;
|
|
1192
|
+
return processName.split("/").pop() || processName;
|
|
961
1193
|
}
|
|
962
1194
|
|
|
963
1195
|
function detectFilesystemViolationFromLine(line: string): FilesystemViolation | null {
|
|
@@ -966,8 +1198,12 @@ function detectFilesystemViolationFromLine(line: string): FilesystemViolation |
|
|
|
966
1198
|
const path = extractPathLikeValue(line);
|
|
967
1199
|
const processName = extractViolationProcessName(line);
|
|
968
1200
|
|
|
1201
|
+
if (lower.includes("file-write-unlink")) {
|
|
1202
|
+
return { kind: "write", path, processName, writeAccess: "unlink" };
|
|
1203
|
+
}
|
|
1204
|
+
|
|
969
1205
|
if (lower.includes("file-write")) {
|
|
970
|
-
return { kind: "write", path, processName };
|
|
1206
|
+
return { kind: "write", path, processName, writeAccess: "unknown" };
|
|
971
1207
|
}
|
|
972
1208
|
|
|
973
1209
|
if (lower.includes("file-read-metadata")) {
|
|
@@ -1016,19 +1252,30 @@ function detectFilesystemViolations(
|
|
|
1016
1252
|
return violations;
|
|
1017
1253
|
}
|
|
1018
1254
|
|
|
1019
|
-
function
|
|
1255
|
+
function isTraversalViolation(
|
|
1020
1256
|
runtimeConfig: SandboxRuntimeConfig | null,
|
|
1021
1257
|
violation: FilesystemViolation,
|
|
1022
1258
|
cwd?: string,
|
|
1023
1259
|
): boolean {
|
|
1024
|
-
if (!runtimeConfig || violation.
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
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
|
+
);
|
|
1029
1276
|
}
|
|
1030
1277
|
|
|
1031
|
-
function
|
|
1278
|
+
function getTraversalPaths(options: {
|
|
1032
1279
|
runtimeConfig: SandboxRuntimeConfig | null;
|
|
1033
1280
|
output: string;
|
|
1034
1281
|
cwd?: string;
|
|
@@ -1047,7 +1294,7 @@ function getMetadataTraversalPaths(options: {
|
|
|
1047
1294
|
const skippedPaths: string[] = [];
|
|
1048
1295
|
for (const line of violationLines) {
|
|
1049
1296
|
const violation = detectFilesystemViolationFromLine(line);
|
|
1050
|
-
if (!violation || !
|
|
1297
|
+
if (!violation || !isTraversalViolation(runtimeConfig, violation, cwd)) {
|
|
1051
1298
|
return null;
|
|
1052
1299
|
}
|
|
1053
1300
|
if (violation.path && !skippedPaths.includes(violation.path)) {
|
|
@@ -1058,7 +1305,7 @@ function getMetadataTraversalPaths(options: {
|
|
|
1058
1305
|
return skippedPaths.length > 0 ? skippedPaths : null;
|
|
1059
1306
|
}
|
|
1060
1307
|
|
|
1061
|
-
function
|
|
1308
|
+
function formatTraversalNotice(paths: string[]): string {
|
|
1062
1309
|
if (paths.length === 0) return "";
|
|
1063
1310
|
|
|
1064
1311
|
const visiblePaths = paths.slice(0, 3).join(", ");
|
|
@@ -1211,32 +1458,36 @@ function parseFilesystemPromptSelection(
|
|
|
1211
1458
|
return "deny";
|
|
1212
1459
|
}
|
|
1213
1460
|
|
|
1214
|
-
function formatFilesystemAllowRetryMessage(
|
|
1215
|
-
return
|
|
1461
|
+
function formatFilesystemAllowRetryMessage(target: string): string {
|
|
1462
|
+
return `\n${formatFilesystemBlockedTarget(target)}\n\nGranting access and retrying the command per user request...\n\n`;
|
|
1216
1463
|
}
|
|
1217
1464
|
|
|
1218
|
-
function formatFilesystemAllowAdaptMessage(
|
|
1219
|
-
return
|
|
1465
|
+
function formatFilesystemAllowAdaptMessage(target: string): string {
|
|
1466
|
+
return `\n${formatFilesystemBlockedTarget(target)}\n\nAccess granted for this session. Retry the command manually if appropriate.`;
|
|
1220
1467
|
}
|
|
1221
1468
|
|
|
1222
|
-
function formatFilesystemDeniedMessage(
|
|
1223
|
-
return
|
|
1469
|
+
function formatFilesystemDeniedMessage(target: string): string {
|
|
1470
|
+
return `\n${formatFilesystemBlockedTarget(target)}\n\nAccess remains denied for this session.`;
|
|
1224
1471
|
}
|
|
1225
1472
|
|
|
1226
|
-
function formatFilesystemAlreadyAllowedMessage(
|
|
1227
|
-
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.`;
|
|
1228
1475
|
}
|
|
1229
1476
|
|
|
1230
1477
|
function formatFilesystemRetrySucceededMessage(_target: string): string {
|
|
1231
1478
|
return "";
|
|
1232
1479
|
}
|
|
1233
1480
|
|
|
1234
|
-
function formatFilesystemRetryFailedMessage(
|
|
1235
|
-
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.`;
|
|
1236
1487
|
}
|
|
1237
1488
|
|
|
1238
|
-
function
|
|
1239
|
-
return
|
|
1489
|
+
function formatFilesystemBlockedTarget(target: string): string {
|
|
1490
|
+
return `Sandbox blocked filesystem ${target}.`;
|
|
1240
1491
|
}
|
|
1241
1492
|
|
|
1242
1493
|
function appendOutputPostamble(postamble: string, addition: string, output: string): string {
|
|
@@ -1502,6 +1753,12 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1502
1753
|
return run;
|
|
1503
1754
|
}
|
|
1504
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
|
+
|
|
1505
1762
|
async function runSandboxAttempt(
|
|
1506
1763
|
command: string,
|
|
1507
1764
|
wrappedCommand: string,
|
|
@@ -1515,7 +1772,7 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1515
1772
|
return new Promise((resolve, reject) => {
|
|
1516
1773
|
const child = spawn("bash", ["-c", wrappedCommand], {
|
|
1517
1774
|
cwd,
|
|
1518
|
-
env,
|
|
1775
|
+
env: withSandboxDefaultEnv(env),
|
|
1519
1776
|
detached: true,
|
|
1520
1777
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1521
1778
|
});
|
|
@@ -1558,7 +1815,7 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1558
1815
|
const shouldStop = newViolations.some((violation) => {
|
|
1559
1816
|
const filesystemViolation = detectFilesystemViolationFromLine(violation.line);
|
|
1560
1817
|
if (!filesystemViolation) return false;
|
|
1561
|
-
return !
|
|
1818
|
+
return !isTraversalViolation(runtimeConfig, filesystemViolation, cwd);
|
|
1562
1819
|
});
|
|
1563
1820
|
if (shouldStop) {
|
|
1564
1821
|
stopForFilesystemViolation();
|
|
@@ -1629,6 +1886,16 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1629
1886
|
});
|
|
1630
1887
|
}
|
|
1631
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
|
+
|
|
1632
1899
|
function getRemainingTimeout(timeout: number | undefined, startedAt: number): number | undefined {
|
|
1633
1900
|
if (timeout === undefined) return undefined;
|
|
1634
1901
|
|
|
@@ -1701,13 +1968,13 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1701
1968
|
attempt.combinedOutput,
|
|
1702
1969
|
);
|
|
1703
1970
|
const runtimeConfig = getRuntimeConfig();
|
|
1704
|
-
const
|
|
1971
|
+
const traversalPaths = getTraversalPaths({
|
|
1705
1972
|
runtimeConfig,
|
|
1706
1973
|
output: annotatedOutput,
|
|
1707
1974
|
cwd,
|
|
1708
1975
|
skipViolationLines: existingViolationCount,
|
|
1709
1976
|
});
|
|
1710
|
-
const effectiveExitCode =
|
|
1977
|
+
const effectiveExitCode = traversalPaths ? 0 : attempt.exitCode;
|
|
1711
1978
|
let postamble = extractAppendedSandboxAnnotation(
|
|
1712
1979
|
attempt.combinedOutput,
|
|
1713
1980
|
annotatedOutput,
|
|
@@ -1715,8 +1982,8 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1715
1982
|
);
|
|
1716
1983
|
let resolution: FilesystemViolationResolution | null = null;
|
|
1717
1984
|
|
|
1718
|
-
if (
|
|
1719
|
-
const notice =
|
|
1985
|
+
if (traversalPaths) {
|
|
1986
|
+
const notice = formatTraversalNotice(traversalPaths);
|
|
1720
1987
|
postamble = appendOutputPostamble(postamble, notice, attempt.combinedOutput);
|
|
1721
1988
|
} else if (runtimeConfig) {
|
|
1722
1989
|
resolution = await handleFilesystemViolation({
|
|
@@ -1745,6 +2012,7 @@ function createSandboxedBashOps(options: SandboxedBashOpsOptions): BashOperation
|
|
|
1745
2012
|
|
|
1746
2013
|
return {
|
|
1747
2014
|
async exec(command, cwd, { onData, signal, timeout, env }) {
|
|
2015
|
+
validateTimeout(timeout);
|
|
1748
2016
|
return runSerially(async () => {
|
|
1749
2017
|
if (!existsSync(cwd)) {
|
|
1750
2018
|
throw new Error(`Working directory does not exist: ${cwd}`);
|
|
@@ -1849,7 +2117,7 @@ function getSandboxRunMode(state: SandboxState): SandboxRunMode {
|
|
|
1849
2117
|
}
|
|
1850
2118
|
|
|
1851
2119
|
function getStateRuntimeConfig(state: SandboxState): SandboxRuntimeConfig | null {
|
|
1852
|
-
if (state.status === "active"
|
|
2120
|
+
if (state.status === "active") return state.runtimeConfig;
|
|
1853
2121
|
return null;
|
|
1854
2122
|
}
|
|
1855
2123
|
|
|
@@ -1985,7 +2253,12 @@ function renderSandboxDoctorReport(options: {
|
|
|
1985
2253
|
} else {
|
|
1986
2254
|
lines.push("- Config paths:");
|
|
1987
2255
|
for (const configPath of configPaths) {
|
|
1988
|
-
const status =
|
|
2256
|
+
const status =
|
|
2257
|
+
configPath.status === "loaded"
|
|
2258
|
+
? "loaded"
|
|
2259
|
+
: configPath.status === "skipped-untrusted"
|
|
2260
|
+
? "skipped (project not trusted)"
|
|
2261
|
+
: "parse error";
|
|
1989
2262
|
lines.push(` - ${configPath.label}: ${configPath.path} (${status})`);
|
|
1990
2263
|
}
|
|
1991
2264
|
}
|
|
@@ -2017,6 +2290,10 @@ function getSandboxConfigParseErrors(paths: SandboxConfigPath[]): SandboxConfigP
|
|
|
2017
2290
|
return paths.filter((configPath) => configPath.status === "parse-error");
|
|
2018
2291
|
}
|
|
2019
2292
|
|
|
2293
|
+
function getSkippedUntrustedProjectConfigPaths(paths: SandboxConfigPath[]): SandboxConfigPath[] {
|
|
2294
|
+
return paths.filter((configPath) => configPath.status === "skipped-untrusted");
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2020
2297
|
function notifySandboxConfigParseErrors(ctx: ExtensionContext, paths: SandboxConfigPath[]): void {
|
|
2021
2298
|
const details = paths
|
|
2022
2299
|
.map((configPath) => `${configPath.label.toLowerCase()} (${configPath.path})`)
|
|
@@ -2042,7 +2319,7 @@ function loadSandboxConfigForContext(
|
|
|
2042
2319
|
const { exitOnError = false } = options;
|
|
2043
2320
|
|
|
2044
2321
|
try {
|
|
2045
|
-
return loadConfig(cwd, overrideConfigPath);
|
|
2322
|
+
return loadConfig(cwd, overrideConfigPath, { projectTrusted: ctx.isProjectTrusted() });
|
|
2046
2323
|
} catch (error) {
|
|
2047
2324
|
if (!(error instanceof SandboxConfigLoadError)) throw error;
|
|
2048
2325
|
|
|
@@ -2083,6 +2360,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2083
2360
|
let sessionContext: ExtensionContext | null = null;
|
|
2084
2361
|
let sandboxConfigPaths: SandboxConfigPath[] = [];
|
|
2085
2362
|
let sandboxEvents: SandboxEvent[] = [];
|
|
2363
|
+
const warnedSkippedProjectConfigPaths = new Set<string>();
|
|
2086
2364
|
|
|
2087
2365
|
const pendingNetworkApprovals = new Map<string, Promise<boolean>>();
|
|
2088
2366
|
|
|
@@ -2127,21 +2405,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
2127
2405
|
});
|
|
2128
2406
|
}
|
|
2129
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
|
+
|
|
2130
2426
|
function applyRuntimeConfigForSession(
|
|
2131
2427
|
ctx: ExtensionContext,
|
|
2132
2428
|
runtimeConfig: SandboxRuntimeConfig,
|
|
2133
|
-
targetStatus: "active" | "suspended" = sandboxState.status === "suspended"
|
|
2134
|
-
? "suspended"
|
|
2135
|
-
: "active",
|
|
2136
2429
|
): void {
|
|
2137
2430
|
const nextConfig = cloneRuntimeConfig(runtimeConfig);
|
|
2138
|
-
sandboxState = { status:
|
|
2431
|
+
sandboxState = { status: "active", runtimeConfig: nextConfig };
|
|
2139
2432
|
SandboxManager.updateConfig(nextConfig);
|
|
2140
|
-
setSandboxStatus(ctx,
|
|
2433
|
+
setSandboxStatus(ctx, true, nextConfig, promptMode);
|
|
2141
2434
|
}
|
|
2142
2435
|
|
|
2143
2436
|
const createNetworkAskCallback = (): SandboxAskCallback => {
|
|
2144
2437
|
return async ({ host, port }) => {
|
|
2438
|
+
if (sandboxState.status === "suspended") return true;
|
|
2439
|
+
|
|
2145
2440
|
const normalizedHost = host.toLowerCase();
|
|
2146
2441
|
const key = normalizedHost;
|
|
2147
2442
|
|
|
@@ -2355,6 +2650,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2355
2650
|
if (!loadedConfig) return;
|
|
2356
2651
|
|
|
2357
2652
|
sandboxConfigPaths = loadedConfig.paths;
|
|
2653
|
+
notifySkippedUntrustedProjectConfigs(ctx);
|
|
2358
2654
|
const parseErrors = getSandboxConfigParseErrors(sandboxConfigPaths);
|
|
2359
2655
|
if (parseErrors.length > 0) {
|
|
2360
2656
|
notifySandboxConfigParseErrors(ctx, parseErrors);
|
|
@@ -2444,45 +2740,32 @@ export default function (pi: ExtensionAPI) {
|
|
|
2444
2740
|
return;
|
|
2445
2741
|
}
|
|
2446
2742
|
|
|
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
|
-
}
|
|
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
|
+
}
|
|
2463
2753
|
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2754
|
+
const sandboxConfigOverride = getStringFlag(pi, "sandbox-config");
|
|
2755
|
+
const loadedConfig = loadSandboxConfigForContext(ctx, ctx.cwd, sandboxConfigOverride);
|
|
2756
|
+
if (!loadedConfig) return;
|
|
2467
2757
|
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
sandboxConfig = loadedConfig.config;
|
|
2474
|
-
runtimeConfig = await initializeSandboxRuntime(ctx, loadedConfig.config);
|
|
2475
|
-
if (!runtimeConfig) return;
|
|
2476
|
-
initializedNow = true;
|
|
2758
|
+
sandboxConfigPaths = loadedConfig.paths;
|
|
2759
|
+
notifySkippedUntrustedProjectConfigs(ctx);
|
|
2760
|
+
const parseErrors = getSandboxConfigParseErrors(sandboxConfigPaths);
|
|
2761
|
+
if (parseErrors.length > 0) {
|
|
2762
|
+
notifySandboxConfigParseErrors(ctx, parseErrors);
|
|
2477
2763
|
}
|
|
2478
|
-
|
|
2764
|
+
sandboxConfig = loadedConfig.config;
|
|
2765
|
+
const runtimeConfig = await initializeSandboxRuntime(ctx, loadedConfig.config);
|
|
2479
2766
|
if (!runtimeConfig) return;
|
|
2480
2767
|
|
|
2481
|
-
|
|
2482
|
-
setSandboxStatus(ctx, true, runtimeConfig, promptMode);
|
|
2483
|
-
} else {
|
|
2484
|
-
applyRuntimeConfigForSession(ctx, runtimeConfig, "active");
|
|
2485
|
-
}
|
|
2768
|
+
setSandboxStatus(ctx, true, runtimeConfig, promptMode);
|
|
2486
2769
|
|
|
2487
2770
|
notify(ctx, "Sandbox enabled", "info");
|
|
2488
2771
|
return;
|
|
@@ -2499,8 +2782,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
2499
2782
|
return;
|
|
2500
2783
|
}
|
|
2501
2784
|
|
|
2502
|
-
sandboxState = { status: "suspended"
|
|
2785
|
+
sandboxState = { status: "suspended" };
|
|
2786
|
+
pendingNetworkApprovals.clear();
|
|
2503
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
|
+
|
|
2504
2800
|
notify(ctx, "Sandbox disabled", "info");
|
|
2505
2801
|
return;
|
|
2506
2802
|
}
|
|
@@ -2532,6 +2828,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2532
2828
|
"",
|
|
2533
2829
|
" Filesystem:",
|
|
2534
2830
|
` Deny Read: ${runtimeConfig.filesystem.denyRead.join(", ") || "(none)"}`,
|
|
2831
|
+
` Allow Read: ${runtimeConfig.filesystem.allowRead?.join(", ") || "(none)"}`,
|
|
2535
2832
|
` Allow Write: ${runtimeConfig.filesystem.allowWrite.join(", ") || "(none)"}`,
|
|
2536
2833
|
` Deny Write: ${runtimeConfig.filesystem.denyWrite.join(", ") || "(none)"}`,
|
|
2537
2834
|
` allowTempDirs: ${sandboxConfig?.filesystem.allowTempDirs ? "true" : "false"}`,
|