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.
- package/README.md +15 -12
- package/extensions/answer.ts +129 -73
- package/extensions/branch-term/README.md +7 -0
- package/extensions/{branch-term.ts → branch-term/index.ts} +113 -104
- package/extensions/btw.ts +90 -4
- package/extensions/caffeinate/README.md +5 -0
- package/extensions/caffeinate/index.ts +144 -0
- package/extensions/fast.ts +292 -0
- package/extensions/ghostty.ts +214 -211
- package/extensions/git-diff-stats.ts +124 -84
- package/extensions/git-pr-status.ts +274 -208
- package/extensions/insights.ts +138 -183
- package/extensions/loop.ts +150 -135
- package/extensions/memory.ts +172 -97
- package/extensions/notify.ts +14 -26
- package/extensions/openai-verbosity.ts +108 -42
- package/extensions/review/fix.ts +15 -6
- package/extensions/review/git.ts +93 -103
- package/extensions/review/index.ts +60 -62
- package/extensions/review/interrupt.ts +117 -0
- package/extensions/review/message-queue.ts +45 -12
- package/extensions/review/models.ts +79 -3
- package/extensions/review/prompts.ts +43 -40
- package/extensions/review/review.ts +161 -166
- package/extensions/review/runner.ts +139 -147
- package/extensions/review/runtime.ts +173 -120
- package/extensions/review/submit-review-tool.ts +1 -5
- package/extensions/review/submit-triage-tool.ts +1 -0
- package/extensions/review/triage.ts +41 -44
- package/extensions/sandbox/bash.ts +775 -0
- package/extensions/sandbox/command.ts +605 -0
- package/extensions/sandbox/config.ts +764 -0
- package/extensions/sandbox/index.ts +138 -2850
- package/extensions/sandbox/macos-sandbox-shell.mjs +202 -0
- package/extensions/sandbox/permissions/dialog.ts +118 -0
- package/extensions/sandbox/permissions/filesystem.ts +559 -0
- package/extensions/sandbox/permissions/mach-lookup.ts +186 -0
- package/extensions/sandbox/permissions/network.ts +164 -0
- package/extensions/sandbox/permissions/unsandboxed.ts +270 -0
- package/extensions/sandbox/runtime.ts +616 -0
- package/extensions/stash.ts +28 -15
- package/extensions/subagent/README.md +73 -0
- package/extensions/subagent/index.ts +822 -0
- package/extensions/subagent/interrupt.ts +117 -0
- package/extensions/subagent/permissions.ts +101 -0
- package/extensions/subagent/rpc.ts +177 -0
- package/extensions/tool-display-mode.ts +267 -64
- package/extensions/usage/anthropic.ts +5 -6
- package/extensions/usage/github-copilot.ts +30 -9
- package/extensions/usage/index.ts +250 -357
- package/extensions/usage/openai-codex.ts +2 -2
- package/extensions/usage/openrouter.ts +10 -2
- package/extensions/websearch/README.md +12 -47
- package/extensions/websearch/config.ts +5 -2
- package/extensions/websearch/index.ts +94 -109
- package/extensions/websearch/output.ts +51 -0
- package/extensions/websearch/providers/anthropic.pi.ts +27 -44
- package/extensions/websearch/providers/gemini.browser.ts +68 -77
- package/extensions/websearch/providers/gemini.pi.ts +17 -17
- package/extensions/websearch/providers/openai-codex.pi.ts +145 -7
- package/extensions/websearch/providers/pi-model.shared.ts +44 -32
- package/extensions/websearch/providers/shared.ts +87 -2
- package/extensions/websearch/types.ts +0 -3
- package/extensions/worktree.ts +132 -172
- package/package.json +10 -5
- package/skills/browser-tools/SKILL.md +29 -234
- package/skills/browser-tools/references/cookies.md +36 -0
- package/skills/browser-tools/references/interaction.md +90 -0
- package/skills/browser-tools/references/logging.md +34 -0
- package/skills/git-clean-history/SKILL.md +6 -6
- package/skills/git-commit/SKILL.md +5 -3
- package/skills/github-pull-request/SKILL.md +60 -0
- package/skills/github-pull-request/references/create.md +40 -0
- package/skills/github-pull-request/references/stewardship.md +60 -0
- package/skills/oracle/SKILL.md +4 -4
- package/skills/oracle/scripts/oracle +64 -51
- package/skills/sentry/SKILL.md +15 -185
- package/skills/sentry/references/events.md +79 -0
- package/skills/sentry/references/issues.md +62 -0
- package/skills/sentry/references/logs.md +46 -0
- package/skills/update-changelog/SKILL.md +27 -121
- package/skills/web-design/SKILL.md +16 -105
- package/themes/tau-dark.json +4 -0
- package/extensions/openai-fast.ts +0 -229
- package/extensions/websearch/providers/openai-codex.browser.ts +0 -77
- 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
|
+
}
|