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.
Files changed (60) hide show
  1. package/README.md +1 -1
  2. package/extensions/answer.ts +49 -43
  3. package/extensions/branch-term.ts +11 -16
  4. package/extensions/btw.ts +8 -8
  5. package/extensions/ghostty.ts +65 -12
  6. package/extensions/git-diff-stats.ts +1 -1
  7. package/extensions/git-pr-status.ts +1 -1
  8. package/extensions/insights.ts +6 -5
  9. package/extensions/loop.ts +39 -11
  10. package/extensions/memory.ts +6 -5
  11. package/extensions/notify.ts +25 -6
  12. package/extensions/openai-fast.ts +6 -4
  13. package/extensions/openai-verbosity.ts +6 -4
  14. package/extensions/review/fix.ts +239 -0
  15. package/extensions/review/git.ts +517 -0
  16. package/extensions/review/index.ts +232 -0
  17. package/extensions/review/message-queue.ts +249 -0
  18. package/extensions/review/models.ts +589 -0
  19. package/extensions/review/prompts.ts +337 -0
  20. package/extensions/review/renderers/inline.ts +126 -0
  21. package/extensions/review/request.ts +504 -0
  22. package/extensions/review/review.ts +1305 -0
  23. package/extensions/review/runner.ts +522 -0
  24. package/extensions/review/runtime.ts +275 -0
  25. package/extensions/review/schema.ts +160 -0
  26. package/extensions/review/submit-review-tool.ts +66 -0
  27. package/extensions/review/submit-triage-tool.ts +59 -0
  28. package/extensions/review/triage.ts +746 -0
  29. package/extensions/sandbox/index.ts +428 -103
  30. package/extensions/{interlude.ts → stash.ts} +13 -11
  31. package/extensions/tool-display-mode.ts +173 -17
  32. package/extensions/usage/anthropic.ts +1 -1
  33. package/extensions/usage/github-copilot.ts +1 -1
  34. package/extensions/usage/index.ts +9 -5
  35. package/extensions/usage/minimax.ts +1 -1
  36. package/extensions/usage/openai-codex.ts +1 -1
  37. package/extensions/usage/openrouter.ts +1 -1
  38. package/extensions/usage/providers.ts +0 -2
  39. package/extensions/usage/types.ts +1 -2
  40. package/extensions/usage/zai.ts +1 -1
  41. package/extensions/websearch/README.md +1 -1
  42. package/extensions/websearch/browser/chromium.ts +42 -31
  43. package/extensions/websearch/browser/discovery.ts +11 -9
  44. package/extensions/websearch/browser/firefox.ts +10 -7
  45. package/extensions/websearch/browser/fs.ts +10 -0
  46. package/extensions/websearch/browser/sqlite.ts +22 -15
  47. package/extensions/websearch/config.ts +2 -2
  48. package/extensions/websearch/index.ts +9 -9
  49. package/extensions/websearch/providers/anthropic.pi.ts +1 -1
  50. package/extensions/websearch/providers/gemini.pi.ts +1 -1
  51. package/extensions/websearch/providers/openai-codex.pi.ts +1 -1
  52. package/extensions/websearch/providers/pi-model.shared.ts +2 -2
  53. package/extensions/worktree.ts +36 -36
  54. package/package.json +7 -7
  55. package/skills/git-clean-history/SKILL.md +1 -0
  56. package/skills/git-commit/SKILL.md +2 -2
  57. package/skills/oracle/scripts/oracle +28 -12
  58. package/themes/tau-dark.json +2 -1
  59. package/extensions/review.ts +0 -4347
  60. 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": [".", "/tmp"],
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 type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
66
- import { type BashOperations, createBashTool } from "@mariozechner/pi-coding-agent";
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
- "registry.npmjs.org",
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
- "api.github.com",
100
- "raw.githubusercontent.com",
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
- "production.cloudflare.docker.com",
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
- allowWrite: [".", "/tmp", "/private/tmp", "~/.cache", "~/.npm"],
136
- denyWrite: [".env", ".env.*", "*.pem", "*.key"],
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 METADATA_TRAVERSAL_PROCESSES = new Set(["find", "ls", "fd", "fdfind"]);
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"; runtimeConfig: SandboxRuntimeConfig }
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 coerceStringArray(value: unknown, fallback: string[], field: string): string[] {
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(cwd: string, overrideConfigPath?: string): LoadedSandboxConfig {
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, ".pi", "sandbox.json");
682
- const globalConfigPath = join(homedir(), ".pi", "agent", "sandbox.json");
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
- try {
700
- projectConfig = JSON.parse(readFileSync(projectConfigPath, "utf-8"));
701
- paths.push({ label: "Project", path: projectConfigPath, status: "loaded" });
702
- } catch (e) {
703
- paths.push({ label: "Project", path: projectConfigPath, status: "parse-error" });
704
- console.error(`Warning: Could not parse ${projectConfigPath}: ${e}`);
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: config.network,
757
- filesystem,
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
- return match?.[1]?.trim() || undefined;
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 isMetadataTraversalViolation(
1255
+ function isTraversalViolation(
993
1256
  runtimeConfig: SandboxRuntimeConfig | null,
994
1257
  violation: FilesystemViolation,
995
1258
  cwd?: string,
996
1259
  ): boolean {
997
- if (!runtimeConfig || violation.kind !== "read" || violation.readAccess !== "metadata")
998
- return false;
999
- if (!violation.path || !METADATA_TRAVERSAL_PROCESSES.has(violation.processName ?? ""))
1000
- return false;
1001
- return inferSandboxRuleMatch(violation.path, runtimeConfig.filesystem.denyRead, cwd) !== null;
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 getMetadataTraversalPaths(options: {
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 || !isMetadataTraversalViolation(runtimeConfig, violation, cwd)) {
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 formatMetadataTraversalNotice(paths: string[]): string {
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(_target: string): string {
1188
- return "\nSandbox blocked access.\n\nGranting access and retrying the command per user request...\n\n";
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(_target: string): string {
1192
- return "\nSandbox blocked access.\n\nAccess granted for this session. Retry the command manually if appropriate.";
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(_target: string): string {
1196
- return "\nSandbox blocked access.\n\nAccess remains denied for this session.";
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(_target: string): string {
1200
- return "\nSandbox blocked access again after permission had already been granted. The remaining failure may be unrelated to sandbox policy.";
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(_target: string): string {
1208
- return "\nAccess granted and command retried per user request, but the command still exited non-zero. The sandbox block was resolved; the remaining failure may be unrelated.";
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 formatFilesystemRetrySkippedMessage(_target: string): string {
1212
- return "\nAccess granted for this session, but automatic retry was skipped because the timeout was exhausted. Retry the command manually if needed.";
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 !isMetadataTraversalViolation(runtimeConfig, filesystemViolation, cwd);
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 metadataTraversalPaths = getMetadataTraversalPaths({
1971
+ const traversalPaths = getTraversalPaths({
1678
1972
  runtimeConfig,
1679
1973
  output: annotatedOutput,
1680
1974
  cwd,
1681
1975
  skipViolationLines: existingViolationCount,
1682
1976
  });
1683
- const effectiveExitCode = metadataTraversalPaths ? 0 : attempt.exitCode;
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 (metadataTraversalPaths) {
1692
- const notice = formatMetadataTraversalNotice(metadataTraversalPaths);
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" || state.status === "suspended") return state.runtimeConfig;
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 = configPath.status === "loaded" ? "loaded" : "parse error";
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: targetStatus, runtimeConfig: nextConfig };
2431
+ sandboxState = { status: "active", runtimeConfig: nextConfig };
2112
2432
  SandboxManager.updateConfig(nextConfig);
2113
- setSandboxStatus(ctx, targetStatus === "active", nextConfig, promptMode);
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
- let runtimeConfig: SandboxRuntimeConfig | null = null;
2421
- let initializedNow = false;
2422
-
2423
- if (sandboxState.status === "suspended") {
2424
- runtimeConfig = sandboxState.runtimeConfig;
2425
- } else {
2426
- if (!isSupportedPlatform()) {
2427
- sandboxState = { status: "blocked", reason: "unsupported-platform" };
2428
- recordRuntimeEvent(
2429
- "init",
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
- const sandboxConfigOverride = getStringFlag(pi, "sandbox-config");
2438
- const loadedConfig = loadSandboxConfigForContext(ctx, ctx.cwd, sandboxConfigOverride);
2439
- if (!loadedConfig) return;
2754
+ const sandboxConfigOverride = getStringFlag(pi, "sandbox-config");
2755
+ const loadedConfig = loadSandboxConfigForContext(ctx, ctx.cwd, sandboxConfigOverride);
2756
+ if (!loadedConfig) return;
2440
2757
 
2441
- sandboxConfigPaths = loadedConfig.paths;
2442
- const parseErrors = getSandboxConfigParseErrors(sandboxConfigPaths);
2443
- if (parseErrors.length > 0) {
2444
- notifySandboxConfigParseErrors(ctx, parseErrors);
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
- if (initializedNow) {
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", runtimeConfig: sandboxState.runtimeConfig };
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
  "",