tau-coding-agent 0.1.5 → 0.2.0

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 (86) hide show
  1. package/README.md +15 -12
  2. package/extensions/answer.ts +129 -73
  3. package/extensions/branch-term/README.md +7 -0
  4. package/extensions/{branch-term.ts → branch-term/index.ts} +113 -104
  5. package/extensions/btw.ts +90 -4
  6. package/extensions/caffeinate/README.md +5 -0
  7. package/extensions/caffeinate/index.ts +144 -0
  8. package/extensions/fast.ts +292 -0
  9. package/extensions/ghostty.ts +214 -211
  10. package/extensions/git-diff-stats.ts +124 -84
  11. package/extensions/git-pr-status.ts +274 -208
  12. package/extensions/insights.ts +138 -183
  13. package/extensions/loop.ts +150 -135
  14. package/extensions/memory.ts +172 -97
  15. package/extensions/notify.ts +14 -26
  16. package/extensions/openai-verbosity.ts +108 -42
  17. package/extensions/review/fix.ts +15 -6
  18. package/extensions/review/git.ts +93 -103
  19. package/extensions/review/index.ts +60 -62
  20. package/extensions/review/interrupt.ts +117 -0
  21. package/extensions/review/message-queue.ts +45 -12
  22. package/extensions/review/models.ts +79 -3
  23. package/extensions/review/prompts.ts +43 -40
  24. package/extensions/review/review.ts +161 -166
  25. package/extensions/review/runner.ts +139 -147
  26. package/extensions/review/runtime.ts +173 -120
  27. package/extensions/review/submit-review-tool.ts +1 -5
  28. package/extensions/review/submit-triage-tool.ts +1 -0
  29. package/extensions/review/triage.ts +41 -44
  30. package/extensions/sandbox/bash.ts +775 -0
  31. package/extensions/sandbox/command.ts +605 -0
  32. package/extensions/sandbox/config.ts +764 -0
  33. package/extensions/sandbox/index.ts +138 -2850
  34. package/extensions/sandbox/macos-sandbox-shell.mjs +202 -0
  35. package/extensions/sandbox/permissions/dialog.ts +118 -0
  36. package/extensions/sandbox/permissions/filesystem.ts +559 -0
  37. package/extensions/sandbox/permissions/mach-lookup.ts +186 -0
  38. package/extensions/sandbox/permissions/network.ts +164 -0
  39. package/extensions/sandbox/permissions/unsandboxed.ts +270 -0
  40. package/extensions/sandbox/runtime.ts +616 -0
  41. package/extensions/stash.ts +28 -15
  42. package/extensions/subagent/README.md +73 -0
  43. package/extensions/subagent/index.ts +822 -0
  44. package/extensions/subagent/interrupt.ts +117 -0
  45. package/extensions/subagent/permissions.ts +101 -0
  46. package/extensions/subagent/rpc.ts +177 -0
  47. package/extensions/tool-display-mode.ts +267 -64
  48. package/extensions/usage/anthropic.ts +5 -6
  49. package/extensions/usage/github-copilot.ts +30 -9
  50. package/extensions/usage/index.ts +250 -357
  51. package/extensions/usage/openai-codex.ts +2 -2
  52. package/extensions/usage/openrouter.ts +10 -2
  53. package/extensions/websearch/README.md +12 -47
  54. package/extensions/websearch/config.ts +5 -2
  55. package/extensions/websearch/index.ts +94 -109
  56. package/extensions/websearch/output.ts +51 -0
  57. package/extensions/websearch/providers/anthropic.pi.ts +27 -44
  58. package/extensions/websearch/providers/gemini.browser.ts +68 -77
  59. package/extensions/websearch/providers/gemini.pi.ts +17 -17
  60. package/extensions/websearch/providers/openai-codex.pi.ts +145 -7
  61. package/extensions/websearch/providers/pi-model.shared.ts +44 -32
  62. package/extensions/websearch/providers/shared.ts +87 -2
  63. package/extensions/websearch/types.ts +0 -3
  64. package/extensions/worktree.ts +132 -172
  65. package/package.json +10 -5
  66. package/skills/browser-tools/SKILL.md +29 -234
  67. package/skills/browser-tools/references/cookies.md +36 -0
  68. package/skills/browser-tools/references/interaction.md +90 -0
  69. package/skills/browser-tools/references/logging.md +34 -0
  70. package/skills/git-clean-history/SKILL.md +6 -6
  71. package/skills/git-commit/SKILL.md +5 -3
  72. package/skills/github-pull-request/SKILL.md +60 -0
  73. package/skills/github-pull-request/references/create.md +40 -0
  74. package/skills/github-pull-request/references/stewardship.md +60 -0
  75. package/skills/oracle/SKILL.md +4 -4
  76. package/skills/oracle/scripts/oracle +64 -51
  77. package/skills/sentry/SKILL.md +15 -185
  78. package/skills/sentry/references/events.md +79 -0
  79. package/skills/sentry/references/issues.md +62 -0
  80. package/skills/sentry/references/logs.md +46 -0
  81. package/skills/update-changelog/SKILL.md +27 -121
  82. package/skills/web-design/SKILL.md +16 -105
  83. package/themes/tau-dark.json +4 -0
  84. package/extensions/openai-fast.ts +0 -229
  85. package/extensions/websearch/providers/openai-codex.browser.ts +0 -77
  86. package/extensions/websearch/providers/openai-codex.shared.ts +0 -123
@@ -0,0 +1,764 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir, tmpdir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import type { SandboxRuntimeConfig } from "@anthropic-ai/sandbox-runtime";
5
+ import {
6
+ containsGlobChars,
7
+ globToRegex,
8
+ normalizePathForSandbox,
9
+ } from "@anthropic-ai/sandbox-runtime/dist/sandbox/sandbox-utils.js";
10
+ import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
11
+
12
+ export type PromptMode = "interactive" | "non-interactive";
13
+ export type ListOp = "add" | "remove";
14
+
15
+ type DefaultConfigExtension = {
16
+ allowMachLookup?: string[];
17
+ allowWrite?: string[];
18
+ ignoreViolations?: string[];
19
+ };
20
+
21
+ export type SandboxConfig = Omit<SandboxRuntimeConfig, "filesystem"> & {
22
+ enabled?: boolean;
23
+ mode?: PromptMode;
24
+ filesystem: SandboxRuntimeConfig["filesystem"] & {
25
+ allowTempDirs?: boolean;
26
+ allowGitCommonDir?: boolean;
27
+ };
28
+ };
29
+
30
+ type SandboxConfigPathStatus = "loaded" | "parse-error" | "skipped-untrusted";
31
+ type SandboxConfigPathLabel = "Global" | "Project" | "Override";
32
+
33
+ export interface SandboxConfigPath {
34
+ label: SandboxConfigPathLabel;
35
+ path: string;
36
+ status: SandboxConfigPathStatus;
37
+ }
38
+
39
+ export interface LoadedSandboxConfig {
40
+ config: SandboxConfig;
41
+ paths: SandboxConfigPath[];
42
+ }
43
+
44
+ type SandboxConfigLoadErrorKind = "not-found" | "read-failed" | "parse-error";
45
+
46
+ export const DEFAULT_PROMPT_MODE: PromptMode = "interactive";
47
+
48
+ const ENV_PATH_REFERENCE_PATTERN = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g;
49
+
50
+ const GLOBAL_DEFAULT_CONFIG = {
51
+ enabled: true,
52
+ mode: DEFAULT_PROMPT_MODE,
53
+ network: {
54
+ allowedDomains: [
55
+ // System
56
+ "localhost",
57
+ "127.0.0.1",
58
+
59
+ // .NET
60
+ "nuget.org",
61
+ "*.nuget.org",
62
+
63
+ // Go
64
+ "proxy.golang.org",
65
+ "sum.golang.org",
66
+ "go.dev",
67
+ "golang.org",
68
+
69
+ // Java / Kotlin
70
+ "repo.maven.apache.org",
71
+ "gradle.org",
72
+ "*.gradle.org",
73
+
74
+ // JS / TS
75
+ "npmjs.org",
76
+ "*.npmjs.org",
77
+ "npmjs.com",
78
+ "*.npmjs.com",
79
+ "registry.yarnpkg.com",
80
+ "nodejs.org",
81
+ "*.nodejs.org",
82
+
83
+ // Python
84
+ "pypi.org",
85
+ "*.pypi.org",
86
+ "pythonhosted.org",
87
+ "*.pythonhosted.org",
88
+
89
+ // Ruby
90
+ "rubygems.org",
91
+ "*.rubygems.org",
92
+
93
+ // Rust
94
+ "crates.io",
95
+ "*.crates.io",
96
+ "rustup.rs",
97
+ "*.rust-lang.org",
98
+
99
+ // Source control
100
+ "github.com",
101
+ "*.github.com",
102
+ "githubusercontent.com",
103
+ "*.githubusercontent.com",
104
+ "gitlab.com",
105
+ "*.gitlab.com",
106
+ "bitbucket.org",
107
+ "*.bitbucket.org",
108
+
109
+ // Containers
110
+ "ghcr.io",
111
+ "docker.io",
112
+ "*.docker.io",
113
+ "docker.com",
114
+ "*.docker.com",
115
+
116
+ // AI providers
117
+ "anthropic.com",
118
+ "*.anthropic.com",
119
+ "claude.ai",
120
+ "*.claude.ai",
121
+ "openai.com",
122
+ "*.openai.com",
123
+ "chatgpt.com",
124
+ "*.chatgpt.com",
125
+ "openrouter.ai",
126
+ "*.openrouter.ai",
127
+ "google.com",
128
+ "*.google.com",
129
+ "googleapis.com",
130
+ "*.googleapis.com",
131
+
132
+ // Observability
133
+ "sentry.io",
134
+ "*.sentry.io",
135
+ "datadoghq.com",
136
+ "*.datadoghq.com",
137
+ "datadoghq.eu",
138
+ "*.datadoghq.eu",
139
+
140
+ // Productivity
141
+ "todoist.com",
142
+ "*.todoist.com",
143
+ "twist.com",
144
+ "*.twist.com",
145
+ "doist.com",
146
+ "*.doist.com",
147
+ ],
148
+ deniedDomains: [],
149
+ allowUnixSockets: ["$SSH_AUTH_SOCK"],
150
+ allowLocalBinding: true,
151
+ },
152
+ filesystem: {
153
+ denyRead: ["~/.ssh", "~/.aws", "~/.gnupg"],
154
+ allowRead: ["~/.ssh/config", "~/.ssh/known_hosts", "~/.ssh/*.pub"],
155
+ allowWrite: [
156
+ // System
157
+ ".",
158
+ "~/.cache",
159
+
160
+ // Pi
161
+ join(getAgentDir(), "*.lock"),
162
+
163
+ // .NET
164
+ "~/.nuget/packages",
165
+ "~/.local/share/NuGet",
166
+
167
+ // Go
168
+ "~/go/pkg",
169
+
170
+ // Java / Kotlin
171
+ "~/.m2/repository",
172
+ "~/.m2/wrapper/dists",
173
+ "~/.gradle",
174
+ "~/.konan",
175
+ "~/.android",
176
+
177
+ // JS / TS
178
+ "~/.npm",
179
+
180
+ // Python
181
+ "~/**/__pycache__",
182
+ "~/**/__pycache__/*",
183
+
184
+ // Ruby
185
+ "~/.bundle/cache",
186
+ "~/.gem/cache",
187
+ "~/.gem/specs",
188
+
189
+ // Rust
190
+ "~/.rustup",
191
+ "~/.cargo/registry",
192
+ "~/.cargo/git",
193
+ "~/.cargo/.package-cache",
194
+ "~/.cargo/.package-cache-mutate",
195
+ "~/.cargo/.global-cache",
196
+ ],
197
+ denyWrite: [
198
+ // Project secrets
199
+ ".env",
200
+ ".env.*",
201
+ "*.pem",
202
+ "*.key",
203
+
204
+ // Java / Kotlin
205
+ "~/.gradle/gradle.properties",
206
+ "~/.gradle/init.gradle",
207
+ "~/.gradle/init.gradle.kts",
208
+ "~/.gradle/init.d",
209
+ "~/.android/adbkey",
210
+ ],
211
+ allowTempDirs: true,
212
+ allowGitConfig: true,
213
+ allowGitCommonDir: true,
214
+ },
215
+ ignoreViolations: {
216
+ "*": ["/__pycache__"],
217
+ },
218
+ } satisfies SandboxConfig;
219
+
220
+ const MACOS_DEFAULT_CONFIG_EXTENSION: DefaultConfigExtension = {
221
+ allowMachLookup: [
222
+ // System
223
+ "com.apple.dnssd.service",
224
+ "com.apple.SystemConfiguration.configd",
225
+ "com.apple.SystemConfiguration.DNSConfiguration",
226
+
227
+ // Filesystem watching
228
+ "com.apple.FSEvents",
229
+ ],
230
+ allowWrite: [
231
+ // System
232
+ "~/Library/Caches",
233
+
234
+ // Java / Kotlin
235
+ "~/Library/Preferences/com.apple.java.util.prefs.plist*",
236
+ "~/Library/Application Support/kotlin",
237
+ "~/**/kotlin-native/klib/cache/**/*",
238
+ ],
239
+ ignoreViolations: ["mach-lookup com.apple.usymptomsd"],
240
+ };
241
+
242
+ const LINUX_DEFAULT_CONFIG_EXTENSION: DefaultConfigExtension = {
243
+ allowWrite: [
244
+ // Java / Kotlin
245
+ "~/.java/.userPrefs",
246
+ ],
247
+ };
248
+
249
+ const PLATFORM_DEFAULT_CONFIG_EXTENSIONS: Partial<Record<NodeJS.Platform, DefaultConfigExtension>> =
250
+ {
251
+ darwin: MACOS_DEFAULT_CONFIG_EXTENSION,
252
+ linux: LINUX_DEFAULT_CONFIG_EXTENSION,
253
+ };
254
+
255
+ const {
256
+ allowMachLookup: PLATFORM_ALLOW_MACH_LOOKUP = [],
257
+ allowWrite: PLATFORM_ALLOW_WRITE = [],
258
+ ignoreViolations: PLATFORM_IGNORE_VIOLATIONS = [],
259
+ } = PLATFORM_DEFAULT_CONFIG_EXTENSIONS[process.platform] ?? {};
260
+
261
+ const DEFAULT_CONFIG: SandboxConfig = {
262
+ ...GLOBAL_DEFAULT_CONFIG,
263
+ network: {
264
+ ...GLOBAL_DEFAULT_CONFIG.network,
265
+ allowMachLookup: PLATFORM_ALLOW_MACH_LOOKUP,
266
+ },
267
+ filesystem: {
268
+ ...GLOBAL_DEFAULT_CONFIG.filesystem,
269
+ allowWrite: [...GLOBAL_DEFAULT_CONFIG.filesystem.allowWrite, ...PLATFORM_ALLOW_WRITE],
270
+ },
271
+ ignoreViolations: {
272
+ "*": [...GLOBAL_DEFAULT_CONFIG.ignoreViolations["*"], ...PLATFORM_IGNORE_VIOLATIONS],
273
+ },
274
+ };
275
+
276
+ export class SandboxConfigLoadError extends Error {
277
+ readonly kind: SandboxConfigLoadErrorKind;
278
+ readonly path: string;
279
+
280
+ constructor(kind: SandboxConfigLoadErrorKind, path: string, detail?: string) {
281
+ super(formatSandboxConfigLoadErrorMessage(kind, path, detail));
282
+ this.name = "SandboxConfigLoadError";
283
+ this.kind = kind;
284
+ this.path = path;
285
+ }
286
+ }
287
+
288
+ export function normalizePromptMode(value: unknown): PromptMode {
289
+ return value === "non-interactive" ? "non-interactive" : "interactive";
290
+ }
291
+
292
+ function expandPath(value: string, cwd?: string): string {
293
+ const expanded =
294
+ value === "~" ? homedir() : value.startsWith("~/") ? join(homedir(), value.slice(2)) : value;
295
+ return cwd && !expanded.startsWith("/") ? resolve(cwd, expanded) : expanded;
296
+ }
297
+
298
+ function formatSandboxConfigLoadErrorMessage(
299
+ kind: SandboxConfigLoadErrorKind,
300
+ path: string,
301
+ detail?: string,
302
+ ): string {
303
+ if (kind === "not-found") {
304
+ return `Sandbox override config not found: ${path}`;
305
+ }
306
+
307
+ if (kind === "parse-error") {
308
+ return `Could not parse sandbox override config ${path}: ${detail ?? "invalid JSON"}`;
309
+ }
310
+
311
+ return `Could not read sandbox override config ${path}: ${detail ?? "unknown error"}`;
312
+ }
313
+
314
+ function cleanStringArray(value: unknown[], field: string): string[] {
315
+ const cleaned = value
316
+ .filter((item): item is string => typeof item === "string")
317
+ .map((item) => item.trim())
318
+ .filter((item) => item.length > 0);
319
+
320
+ const droppedCount = value.length - cleaned.length;
321
+ if (droppedCount > 0) {
322
+ console.error(`Warning: Ignoring ${droppedCount} invalid values in ${field}.`);
323
+ }
324
+
325
+ return cleaned;
326
+ }
327
+
328
+ function coerceStringArray(value: unknown, fallback: string[], field: string): string[] {
329
+ if (!Array.isArray(value)) {
330
+ console.error(`Warning: Expected ${field} to be a string[]; using defaults.`);
331
+ return [...fallback];
332
+ }
333
+
334
+ return cleanStringArray(value, field);
335
+ }
336
+
337
+ function coerceOptionalStringArray(
338
+ value: unknown,
339
+ fallback: string[] | undefined,
340
+ field: string,
341
+ ): string[] | undefined {
342
+ if (value === undefined) return fallback ? [...fallback] : undefined;
343
+
344
+ if (!Array.isArray(value)) {
345
+ console.error(`Warning: Expected ${field} to be a string[]; using defaults.`);
346
+ return fallback ? [...fallback] : undefined;
347
+ }
348
+
349
+ return cleanStringArray(value, field);
350
+ }
351
+
352
+ function finalizeConfig(config: SandboxConfig): SandboxConfig {
353
+ return {
354
+ ...config,
355
+ enabled: typeof config.enabled === "boolean" ? config.enabled : DEFAULT_CONFIG.enabled,
356
+ mode: normalizePromptMode(config.mode),
357
+ network: {
358
+ ...config.network,
359
+ allowedDomains: coerceStringArray(
360
+ config.network?.allowedDomains,
361
+ DEFAULT_CONFIG.network.allowedDomains,
362
+ "network.allowedDomains",
363
+ ),
364
+ deniedDomains: coerceStringArray(
365
+ config.network?.deniedDomains,
366
+ DEFAULT_CONFIG.network.deniedDomains,
367
+ "network.deniedDomains",
368
+ ),
369
+ allowMachLookup: coerceStringArray(
370
+ config.network?.allowMachLookup,
371
+ DEFAULT_CONFIG.network.allowMachLookup ?? [],
372
+ "network.allowMachLookup",
373
+ ),
374
+ allowUnixSockets: coerceOptionalStringArray(
375
+ config.network?.allowUnixSockets,
376
+ DEFAULT_CONFIG.network.allowUnixSockets,
377
+ "network.allowUnixSockets",
378
+ ),
379
+ },
380
+ filesystem: {
381
+ ...config.filesystem,
382
+ denyRead: coerceStringArray(
383
+ config.filesystem?.denyRead,
384
+ DEFAULT_CONFIG.filesystem.denyRead,
385
+ "filesystem.denyRead",
386
+ ),
387
+ allowRead: coerceOptionalStringArray(
388
+ config.filesystem?.allowRead,
389
+ DEFAULT_CONFIG.filesystem.allowRead,
390
+ "filesystem.allowRead",
391
+ ),
392
+ allowWrite: coerceStringArray(
393
+ config.filesystem?.allowWrite,
394
+ DEFAULT_CONFIG.filesystem.allowWrite,
395
+ "filesystem.allowWrite",
396
+ ),
397
+ denyWrite: coerceStringArray(
398
+ config.filesystem?.denyWrite,
399
+ DEFAULT_CONFIG.filesystem.denyWrite,
400
+ "filesystem.denyWrite",
401
+ ),
402
+ allowTempDirs:
403
+ typeof config.filesystem?.allowTempDirs === "boolean"
404
+ ? config.filesystem.allowTempDirs
405
+ : DEFAULT_CONFIG.filesystem.allowTempDirs,
406
+ allowGitCommonDir:
407
+ typeof config.filesystem?.allowGitCommonDir === "boolean"
408
+ ? config.filesystem.allowGitCommonDir
409
+ : DEFAULT_CONFIG.filesystem.allowGitCommonDir,
410
+ },
411
+ };
412
+ }
413
+
414
+ function loadOverrideConfig(cwd: string, overrideConfigPath: string): LoadedSandboxConfig {
415
+ const resolvedPath = expandPath(overrideConfigPath, cwd);
416
+
417
+ let source: string;
418
+ try {
419
+ source = readFileSync(resolvedPath, "utf-8");
420
+ } catch (error) {
421
+ const code =
422
+ error instanceof Error && "code" in error && typeof error.code === "string"
423
+ ? error.code
424
+ : undefined;
425
+ if (code === "ENOENT") {
426
+ throw new SandboxConfigLoadError("not-found", resolvedPath);
427
+ }
428
+
429
+ throw new SandboxConfigLoadError(
430
+ "read-failed",
431
+ resolvedPath,
432
+ error instanceof Error ? error.message : `${error}`,
433
+ );
434
+ }
435
+
436
+ let overrideConfig: Partial<SandboxConfig>;
437
+ try {
438
+ overrideConfig = JSON.parse(source);
439
+ } catch (error) {
440
+ throw new SandboxConfigLoadError(
441
+ "parse-error",
442
+ resolvedPath,
443
+ error instanceof Error ? error.message : `${error}`,
444
+ );
445
+ }
446
+
447
+ return {
448
+ config: finalizeConfig(deepMerge(DEFAULT_CONFIG, overrideConfig)),
449
+ paths: [{ label: "Override", path: resolvedPath, status: "loaded" }],
450
+ };
451
+ }
452
+
453
+ export function loadConfig(
454
+ cwd: string,
455
+ overrideConfigPath?: string,
456
+ options: { projectTrusted?: boolean } = {},
457
+ ): LoadedSandboxConfig {
458
+ if (overrideConfigPath) {
459
+ return loadOverrideConfig(cwd, overrideConfigPath);
460
+ }
461
+
462
+ const projectConfigPath = join(cwd, CONFIG_DIR_NAME, "sandbox.json");
463
+ const globalConfigPath = join(getAgentDir(), "sandbox.json");
464
+
465
+ let globalConfig: Partial<SandboxConfig> = {};
466
+ let projectConfig: Partial<SandboxConfig> = {};
467
+ const paths: SandboxConfigPath[] = [];
468
+
469
+ if (existsSync(globalConfigPath)) {
470
+ try {
471
+ globalConfig = JSON.parse(readFileSync(globalConfigPath, "utf-8"));
472
+ paths.push({ label: "Global", path: globalConfigPath, status: "loaded" });
473
+ } catch (e) {
474
+ paths.push({ label: "Global", path: globalConfigPath, status: "parse-error" });
475
+ console.error(`Warning: Could not parse ${globalConfigPath}: ${e}`);
476
+ }
477
+ }
478
+
479
+ if (existsSync(projectConfigPath)) {
480
+ if (options.projectTrusted !== true) {
481
+ paths.push({ label: "Project", path: projectConfigPath, status: "skipped-untrusted" });
482
+ } else {
483
+ try {
484
+ projectConfig = JSON.parse(readFileSync(projectConfigPath, "utf-8"));
485
+ paths.push({ label: "Project", path: projectConfigPath, status: "loaded" });
486
+ } catch (e) {
487
+ paths.push({ label: "Project", path: projectConfigPath, status: "parse-error" });
488
+ console.error(`Warning: Could not parse ${projectConfigPath}: ${e}`);
489
+ }
490
+ }
491
+ }
492
+
493
+ const merged = deepMerge(deepMerge(DEFAULT_CONFIG, globalConfig), projectConfig);
494
+
495
+ return {
496
+ config: finalizeConfig(merged),
497
+ paths,
498
+ };
499
+ }
500
+
501
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
502
+ return typeof value === "object" && value !== null && !Array.isArray(value);
503
+ }
504
+
505
+ function deepMerge(base: SandboxConfig, overrides: Partial<SandboxConfig>): SandboxConfig {
506
+ const result: SandboxConfig = { ...base };
507
+
508
+ if (overrides.enabled !== undefined) result.enabled = overrides.enabled;
509
+ if (overrides.mode !== undefined) {
510
+ result.mode = normalizePromptMode(overrides.mode);
511
+ }
512
+ if (isPlainObject(overrides.network)) {
513
+ result.network = {
514
+ ...base.network,
515
+ ...(overrides.network as Partial<SandboxRuntimeConfig["network"]>),
516
+ };
517
+ }
518
+ if (isPlainObject(overrides.filesystem)) {
519
+ result.filesystem = {
520
+ ...base.filesystem,
521
+ ...(overrides.filesystem as Partial<SandboxConfig["filesystem"]>),
522
+ };
523
+ }
524
+ if (overrides.ignoreViolations !== undefined) {
525
+ result.ignoreViolations = overrides.ignoreViolations;
526
+ }
527
+ if (overrides.enableWeakerNestedSandbox !== undefined) {
528
+ result.enableWeakerNestedSandbox = overrides.enableWeakerNestedSandbox;
529
+ }
530
+ if (overrides.enableWeakerNetworkIsolation !== undefined) {
531
+ result.enableWeakerNetworkIsolation = overrides.enableWeakerNetworkIsolation;
532
+ }
533
+
534
+ return result;
535
+ }
536
+
537
+ let cachedTemporaryWritePaths: string[] | undefined;
538
+
539
+ function getTemporaryWritePaths(): string[] {
540
+ if (cachedTemporaryWritePaths) return cachedTemporaryWritePaths;
541
+
542
+ // allowTempDirs always includes the conventional shared temp path, even when
543
+ // os.tmpdir() points at a platform-specific per-user directory.
544
+ const currentTmpDir = tmpdir();
545
+ const paths = ["/tmp", currentTmpDir, normalizePathForSandbox(currentTmpDir)];
546
+ if (process.platform === "darwin") paths.push("/private/tmp");
547
+
548
+ cachedTemporaryWritePaths = Array.from(
549
+ new Set(paths.map((path) => path.replace(/\/+$/, "") || "/")),
550
+ );
551
+ return cachedTemporaryWritePaths;
552
+ }
553
+
554
+ function deduplicateStrings(values: string[]): string[] {
555
+ return Array.from(new Set(values));
556
+ }
557
+
558
+ function deduplicateOptionalStrings(values: string[] | undefined): string[] | undefined {
559
+ return values ? deduplicateStrings(values) : undefined;
560
+ }
561
+
562
+ function containsControlCharacter(value: string): boolean {
563
+ for (let index = 0; index < value.length; index += 1) {
564
+ const code = value.charCodeAt(index);
565
+ if (code <= 0x1f || (code >= 0x7f && code <= 0x9f) || code === 0x2028 || code === 0x2029) {
566
+ return true;
567
+ }
568
+ }
569
+
570
+ return false;
571
+ }
572
+
573
+ function expandPathConfigEntry(value: string, field: string): string | null {
574
+ const source = value.trim();
575
+ if (!source) return null;
576
+
577
+ const missingEnvNames = new Set<string>();
578
+ const expanded = source.replace(
579
+ ENV_PATH_REFERENCE_PATTERN,
580
+ (_match, bracedName: string | undefined, bareName: string | undefined) => {
581
+ const envName = bracedName ?? bareName;
582
+ const envValue = envName ? (process.env[envName]?.trim() ?? "") : "";
583
+ if (!envValue) {
584
+ if (envName) missingEnvNames.add(envName);
585
+ return "";
586
+ }
587
+ return envValue;
588
+ },
589
+ );
590
+
591
+ if (missingEnvNames.size > 0) {
592
+ const names = Array.from(missingEnvNames).join(", ");
593
+ const label = missingEnvNames.size === 1 ? "environment variable" : "environment variables";
594
+ const verb = missingEnvNames.size === 1 ? "is" : "are";
595
+ console.error(
596
+ `Warning: Ignoring ${field} entry because ${label} ${names} ${verb} unset or empty.`,
597
+ );
598
+ return null;
599
+ }
600
+
601
+ if (containsControlCharacter(expanded)) {
602
+ console.error(
603
+ `Warning: Ignoring ${field} entry because the expanded path contains control characters.`,
604
+ );
605
+ return null;
606
+ }
607
+
608
+ return expanded;
609
+ }
610
+
611
+ function expandPathConfigList(values: string[] | undefined, field: string): string[] | undefined {
612
+ if (!values) return undefined;
613
+
614
+ return values
615
+ .map((value) => expandPathConfigEntry(value, field))
616
+ .filter((value): value is string => value !== null);
617
+ }
618
+
619
+ function expandMitmProxyPathConfig(
620
+ mitmProxy: SandboxRuntimeConfig["network"]["mitmProxy"],
621
+ ): SandboxRuntimeConfig["network"]["mitmProxy"] {
622
+ if (!mitmProxy) return undefined;
623
+ if (!isPlainObject(mitmProxy) || typeof mitmProxy.socketPath !== "string") return mitmProxy;
624
+
625
+ const socketPath = expandPathConfigEntry(mitmProxy.socketPath, "network.mitmProxy.socketPath");
626
+ if (!socketPath) {
627
+ console.error("Warning: Disabling network.mitmProxy because its socketPath did not expand.");
628
+ return undefined;
629
+ }
630
+
631
+ return { ...mitmProxy, socketPath } as SandboxRuntimeConfig["network"]["mitmProxy"];
632
+ }
633
+
634
+ function expandNetworkPathConfig(
635
+ network: SandboxRuntimeConfig["network"],
636
+ ): SandboxRuntimeConfig["network"] {
637
+ return {
638
+ ...network,
639
+ allowUnixSockets: expandPathConfigList(network.allowUnixSockets, "network.allowUnixSockets"),
640
+ mitmProxy: expandMitmProxyPathConfig(network.mitmProxy),
641
+ };
642
+ }
643
+
644
+ function expandFilesystemPathConfig(
645
+ filesystem: SandboxRuntimeConfig["filesystem"],
646
+ ): SandboxRuntimeConfig["filesystem"] {
647
+ return {
648
+ ...filesystem,
649
+ denyRead: expandPathConfigList(filesystem.denyRead, "filesystem.denyRead") ?? [],
650
+ allowRead: expandPathConfigList(filesystem.allowRead, "filesystem.allowRead"),
651
+ allowWrite: expandPathConfigList(filesystem.allowWrite, "filesystem.allowWrite") ?? [],
652
+ denyWrite: expandPathConfigList(filesystem.denyWrite, "filesystem.denyWrite") ?? [],
653
+ };
654
+ }
655
+
656
+ export function toRuntimeConfig(config: SandboxConfig): SandboxRuntimeConfig {
657
+ const { allowGitCommonDir: _allowGitCommonDir, allowTempDirs, ...filesystem } = config.filesystem;
658
+ const expandedNetwork = expandNetworkPathConfig(config.network);
659
+ const expandedFilesystem = expandFilesystemPathConfig(filesystem);
660
+ const allowWrite = allowTempDirs
661
+ ? [...expandedFilesystem.allowWrite, ...getTemporaryWritePaths()]
662
+ : expandedFilesystem.allowWrite;
663
+
664
+ return {
665
+ network: {
666
+ ...expandedNetwork,
667
+ allowUnixSockets: deduplicateOptionalStrings(expandedNetwork.allowUnixSockets),
668
+ },
669
+ filesystem: {
670
+ ...expandedFilesystem,
671
+ denyRead: deduplicateStrings(expandedFilesystem.denyRead),
672
+ allowRead: deduplicateOptionalStrings(expandedFilesystem.allowRead),
673
+ allowWrite: deduplicateStrings(allowWrite),
674
+ denyWrite: deduplicateStrings(expandedFilesystem.denyWrite),
675
+ },
676
+ ignoreViolations: config.ignoreViolations,
677
+ enableWeakerNestedSandbox: config.enableWeakerNestedSandbox,
678
+ enableWeakerNetworkIsolation: config.enableWeakerNetworkIsolation,
679
+ };
680
+ }
681
+
682
+ export function cloneRuntimeConfig(config: SandboxRuntimeConfig): SandboxRuntimeConfig {
683
+ return structuredClone(config);
684
+ }
685
+
686
+ function normalizeSandboxPath(value: string, cwd?: string): string {
687
+ return normalizePathForSandbox(expandPath(value, cwd));
688
+ }
689
+
690
+ function matchesSandboxRule(path: string, rule: string, cwd?: string): boolean {
691
+ const normalizedPath = normalizeSandboxPath(path);
692
+ const normalizedRule = normalizeSandboxPath(rule, cwd);
693
+
694
+ if (containsGlobChars(rule)) {
695
+ return new RegExp(globToRegex(normalizedRule)).test(normalizedPath);
696
+ }
697
+
698
+ if (normalizedPath === normalizedRule) return true;
699
+
700
+ const prefix = normalizedRule.endsWith("/") ? normalizedRule : `${normalizedRule}/`;
701
+ return normalizedPath.startsWith(prefix);
702
+ }
703
+
704
+ export function inferSandboxRuleMatch(path: string, rules: string[], cwd?: string): string | null {
705
+ for (const rule of rules) {
706
+ if (matchesSandboxRule(path, rule, cwd)) return rule;
707
+ }
708
+
709
+ return null;
710
+ }
711
+
712
+ function matchesSandboxRuleExactly(path: string, rule: string, cwd?: string): boolean {
713
+ if (containsGlobChars(rule)) return false;
714
+ return normalizeSandboxPath(path) === normalizeSandboxPath(rule, cwd);
715
+ }
716
+
717
+ export function inferExactSandboxRuleMatch(
718
+ path: string,
719
+ rules: string[],
720
+ cwd?: string,
721
+ ): string | null {
722
+ for (const rule of rules) {
723
+ if (matchesSandboxRuleExactly(path, rule, cwd)) return rule;
724
+ }
725
+
726
+ return null;
727
+ }
728
+
729
+ export function isSandboxWritablePath(
730
+ runtimeConfig: SandboxRuntimeConfig,
731
+ path: string,
732
+ cwd?: string,
733
+ ): boolean {
734
+ if (!inferSandboxRuleMatch(path, runtimeConfig.filesystem.allowWrite, cwd)) return false;
735
+ return inferSandboxRuleMatch(path, runtimeConfig.filesystem.denyWrite, cwd) === null;
736
+ }
737
+
738
+ export function escapeSlashCommandArg(value: string): string {
739
+ if (/^[a-zA-Z0-9_./:@%+\-~]+$/.test(value)) return value;
740
+ return JSON.stringify(value);
741
+ }
742
+
743
+ export function mutateStringList(values: string[], op: ListOp, value: string): boolean {
744
+ if (op === "add") {
745
+ if (values.includes(value)) return false;
746
+ values.push(value);
747
+ return true;
748
+ }
749
+
750
+ const index = values.indexOf(value);
751
+ if (index === -1) return false;
752
+ values.splice(index, 1);
753
+ return true;
754
+ }
755
+
756
+ export function getSandboxConfigParseErrors(paths: SandboxConfigPath[]): SandboxConfigPath[] {
757
+ return paths.filter((configPath) => configPath.status === "parse-error");
758
+ }
759
+
760
+ export function getSkippedUntrustedProjectConfigPaths(
761
+ paths: SandboxConfigPath[],
762
+ ): SandboxConfigPath[] {
763
+ return paths.filter((configPath) => configPath.status === "skipped-untrusted");
764
+ }