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,616 @@
|
|
|
1
|
+
import { SandboxManager, type SandboxRuntimeConfig } from "@anthropic-ai/sandbox-runtime";
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_PROMPT_MODE,
|
|
6
|
+
SandboxConfigLoadError,
|
|
7
|
+
cloneRuntimeConfig,
|
|
8
|
+
getSandboxConfigParseErrors,
|
|
9
|
+
getSkippedUntrustedProjectConfigPaths,
|
|
10
|
+
loadConfig,
|
|
11
|
+
normalizePromptMode,
|
|
12
|
+
toRuntimeConfig,
|
|
13
|
+
type LoadedSandboxConfig,
|
|
14
|
+
type PromptMode,
|
|
15
|
+
type SandboxConfig,
|
|
16
|
+
type SandboxConfigPath,
|
|
17
|
+
} from "./config.js";
|
|
18
|
+
import { createNetworkPermissions } from "./permissions/network.js";
|
|
19
|
+
import { isUnsandboxedApproval, showUnsandboxedApproval } from "./permissions/unsandboxed.js";
|
|
20
|
+
|
|
21
|
+
const STATUS_KEY = "sandbox";
|
|
22
|
+
const SANDBOX_EVENT_LIMIT = 50;
|
|
23
|
+
|
|
24
|
+
type UiLevel = "info" | "warning" | "error";
|
|
25
|
+
|
|
26
|
+
export type SandboxEventOutcome = "blocked" | "allowed";
|
|
27
|
+
type SandboxBypassReason = "no-sandbox-flag" | "config-disabled";
|
|
28
|
+
type SandboxBlockedReason = "unsupported-platform" | "init-failed" | "missing-dependencies";
|
|
29
|
+
type SandboxRunMode = "sandbox" | "user-disabled" | SandboxBypassReason | SandboxBlockedReason;
|
|
30
|
+
|
|
31
|
+
export type SandboxState =
|
|
32
|
+
| { status: "pending" }
|
|
33
|
+
| { status: "active"; runtimeConfig: SandboxRuntimeConfig }
|
|
34
|
+
| { status: "suspended" }
|
|
35
|
+
| { status: "bypassed"; reason: SandboxBypassReason }
|
|
36
|
+
| { status: "blocked"; reason: SandboxBlockedReason };
|
|
37
|
+
|
|
38
|
+
type SandboxEventKind = "filesystem" | "network" | "mach-lookup" | "init" | "runtime";
|
|
39
|
+
export type SandboxEventReason =
|
|
40
|
+
| "explicit-deny-read"
|
|
41
|
+
| "explicit-deny-write"
|
|
42
|
+
| "explicit-deny-domain"
|
|
43
|
+
| "missing-allow-write"
|
|
44
|
+
| "missing-allowed-domain"
|
|
45
|
+
| "missing-mach-lookup"
|
|
46
|
+
| "missing-dependencies"
|
|
47
|
+
| "unsupported-platform"
|
|
48
|
+
| "init-failed"
|
|
49
|
+
| "runtime-protected-write"
|
|
50
|
+
| "already-approved-still-failed"
|
|
51
|
+
| "unknown";
|
|
52
|
+
|
|
53
|
+
export interface SandboxEvent {
|
|
54
|
+
timestamp: number;
|
|
55
|
+
kind: SandboxEventKind;
|
|
56
|
+
outcome: SandboxEventOutcome;
|
|
57
|
+
reason: SandboxEventReason;
|
|
58
|
+
target?: string;
|
|
59
|
+
command?: string;
|
|
60
|
+
cwd?: string;
|
|
61
|
+
summary: string;
|
|
62
|
+
suggestedCommand?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SandboxRuntime {
|
|
66
|
+
readonly state: SandboxState;
|
|
67
|
+
readonly config: SandboxConfig | null;
|
|
68
|
+
readonly promptMode: PromptMode;
|
|
69
|
+
readonly context: ExtensionContext | null;
|
|
70
|
+
readonly configPaths: SandboxConfigPath[];
|
|
71
|
+
readonly events: SandboxEvent[];
|
|
72
|
+
getRuntimeConfig(): SandboxRuntimeConfig | null;
|
|
73
|
+
captureContext(ctx: ExtensionContext): void;
|
|
74
|
+
withPermissionContext<T>(
|
|
75
|
+
ctx: ExtensionContext,
|
|
76
|
+
signal: AbortSignal | undefined,
|
|
77
|
+
run: (ctx: ExtensionContext, signal: AbortSignal) => Promise<T>,
|
|
78
|
+
): Promise<T>;
|
|
79
|
+
start(ctx: ExtensionContext): Promise<void>;
|
|
80
|
+
shutdown(ctx: ExtensionContext): Promise<void>;
|
|
81
|
+
enable(ctx: ExtensionContext): Promise<void>;
|
|
82
|
+
disable(ctx: ExtensionContext): Promise<void>;
|
|
83
|
+
setPromptMode(ctx: ExtensionContext, mode: PromptMode): void;
|
|
84
|
+
applyRuntimeConfigForSession(ctx: ExtensionContext, runtimeConfig: SandboxRuntimeConfig): void;
|
|
85
|
+
recordEvent(event: SandboxEvent): void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function setSandboxStatus(
|
|
89
|
+
ctx: ExtensionContext,
|
|
90
|
+
enabled: boolean,
|
|
91
|
+
runtimeConfig?: SandboxRuntimeConfig,
|
|
92
|
+
promptMode: PromptMode = DEFAULT_PROMPT_MODE,
|
|
93
|
+
): void {
|
|
94
|
+
if (!ctx.hasUI) return;
|
|
95
|
+
|
|
96
|
+
if (!enabled || !runtimeConfig) {
|
|
97
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const networkCount = runtimeConfig.network.allowedDomains.length;
|
|
102
|
+
const writeCount = runtimeConfig.filesystem.allowWrite.length;
|
|
103
|
+
const text = `sandbox (${promptMode}, ${networkCount} domains, ${writeCount} write paths)`;
|
|
104
|
+
ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("dim", text));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function notify(ctx: ExtensionContext, text: string, level: UiLevel = "info"): void {
|
|
108
|
+
if (ctx.hasUI) {
|
|
109
|
+
ctx.ui.notify(text, level);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (level === "error" || level === "warning") console.error(text);
|
|
114
|
+
else console.log(text);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function announceSandboxState(
|
|
118
|
+
pi: ExtensionAPI,
|
|
119
|
+
ctx: ExtensionContext,
|
|
120
|
+
enabled: boolean,
|
|
121
|
+
notification?: string,
|
|
122
|
+
level: UiLevel = "info",
|
|
123
|
+
): void {
|
|
124
|
+
const text = `Sandbox ${enabled ? "enabled" : "disabled"}`;
|
|
125
|
+
notify(ctx, notification ?? text, level);
|
|
126
|
+
pi.sendMessage(
|
|
127
|
+
{
|
|
128
|
+
customType: "sandbox-state",
|
|
129
|
+
content: text,
|
|
130
|
+
display: false,
|
|
131
|
+
},
|
|
132
|
+
{ triggerTurn: false },
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getStringFlag(pi: ExtensionAPI, name: string): string | undefined {
|
|
137
|
+
const value = pi.getFlag(name);
|
|
138
|
+
if (typeof value !== "string") return undefined;
|
|
139
|
+
|
|
140
|
+
const trimmed = value.trim();
|
|
141
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function getSandboxRunMode(state: SandboxState): SandboxRunMode {
|
|
145
|
+
if (state.status === "active" || state.status === "pending") return "sandbox";
|
|
146
|
+
if (state.status === "suspended") return "user-disabled";
|
|
147
|
+
return state.reason;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getStateRuntimeConfig(state: SandboxState): SandboxRuntimeConfig | null {
|
|
151
|
+
return state.status === "active" ? state.runtimeConfig : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function formatSandboxEventTimestamp(timestamp: number): string {
|
|
155
|
+
const date = new Date(timestamp);
|
|
156
|
+
const hours = `${date.getHours()}`.padStart(2, "0");
|
|
157
|
+
const minutes = `${date.getMinutes()}`.padStart(2, "0");
|
|
158
|
+
const seconds = `${date.getSeconds()}`.padStart(2, "0");
|
|
159
|
+
return `${hours}:${minutes}:${seconds}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function describeSandboxRuntimeState(state: SandboxState, promptMode: PromptMode): string {
|
|
163
|
+
if (state.status === "active") return `active (${promptMode})`;
|
|
164
|
+
if (state.status === "pending") return "pending";
|
|
165
|
+
if (state.status === "suspended") return `suspended (${promptMode})`;
|
|
166
|
+
if (state.status === "bypassed") {
|
|
167
|
+
if (state.reason === "no-sandbox-flag") return "bypassed (--no-sandbox)";
|
|
168
|
+
return "bypassed (config disabled)";
|
|
169
|
+
}
|
|
170
|
+
if (state.reason === "missing-dependencies") return "blocked (missing dependencies)";
|
|
171
|
+
return state.reason === "unsupported-platform"
|
|
172
|
+
? "blocked (unsupported platform)"
|
|
173
|
+
: "blocked (init failed)";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function notifySandboxConfigParseErrors(ctx: ExtensionContext, paths: SandboxConfigPath[]): void {
|
|
177
|
+
const details = paths
|
|
178
|
+
.map((configPath) => `${configPath.label.toLowerCase()} (${configPath.path})`)
|
|
179
|
+
.join(", ");
|
|
180
|
+
notify(ctx, `Could not parse sandbox config: ${details}`, "warning");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function requestShutdownWithError(ctx: ExtensionContext, message: string): void {
|
|
184
|
+
// Validate during session_start so we can use Pi's registered flag values and shutdown path.
|
|
185
|
+
// This briefly renders startup UI, but Pi exits through its normal cleanup path instead of
|
|
186
|
+
// leaving the terminal in a broken state.
|
|
187
|
+
notify(ctx, message, "error");
|
|
188
|
+
process.exitCode = 1;
|
|
189
|
+
ctx.shutdown();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function loadSandboxConfigForContext(
|
|
193
|
+
ctx: ExtensionContext,
|
|
194
|
+
cwd: string,
|
|
195
|
+
overrideConfigPath: string | undefined,
|
|
196
|
+
options: { exitOnError?: boolean } = {},
|
|
197
|
+
): LoadedSandboxConfig | null {
|
|
198
|
+
const { exitOnError = false } = options;
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
return loadConfig(cwd, overrideConfigPath, { projectTrusted: ctx.isProjectTrusted() });
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (!(error instanceof SandboxConfigLoadError)) throw error;
|
|
204
|
+
|
|
205
|
+
if (exitOnError) {
|
|
206
|
+
requestShutdownWithError(ctx, error.message);
|
|
207
|
+
} else {
|
|
208
|
+
notify(ctx, error.message, "error");
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function getSandboxDependencyErrors(config: SandboxConfig): string[] {
|
|
215
|
+
return SandboxManager.checkDependencies(config.ripgrep).errors;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isSupportedPlatform(): boolean {
|
|
219
|
+
return process.platform === "darwin" || process.platform === "linux";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function createSandboxRuntime(pi: ExtensionAPI): SandboxRuntime {
|
|
223
|
+
let sessionCwd = process.cwd();
|
|
224
|
+
let sandboxState: SandboxState = { status: "pending" };
|
|
225
|
+
let sandboxConfig: SandboxConfig | null = null;
|
|
226
|
+
let promptMode: PromptMode = DEFAULT_PROMPT_MODE;
|
|
227
|
+
let sessionContext: ExtensionContext | null = null;
|
|
228
|
+
let sandboxConfigPaths: SandboxConfigPath[] = [];
|
|
229
|
+
let sandboxEvents: SandboxEvent[] = [];
|
|
230
|
+
const warnedSkippedProjectConfigPaths = new Set<string>();
|
|
231
|
+
let permissionLifetime = new AbortController();
|
|
232
|
+
const permissionInvocations = new Set<Promise<unknown>>();
|
|
233
|
+
|
|
234
|
+
function recordSandboxEvent(event: SandboxEvent): void {
|
|
235
|
+
sandboxEvents.push(event);
|
|
236
|
+
if (sandboxEvents.length > SANDBOX_EVENT_LIMIT) {
|
|
237
|
+
sandboxEvents.splice(0, sandboxEvents.length - SANDBOX_EVENT_LIMIT);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function recordRuntimeEvent(
|
|
242
|
+
kind: SandboxEventKind,
|
|
243
|
+
reason: SandboxEventReason,
|
|
244
|
+
summary: string,
|
|
245
|
+
): void {
|
|
246
|
+
recordSandboxEvent({
|
|
247
|
+
timestamp: Date.now(),
|
|
248
|
+
kind,
|
|
249
|
+
outcome: "blocked",
|
|
250
|
+
reason,
|
|
251
|
+
cwd: sessionCwd,
|
|
252
|
+
summary,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function notifySkippedUntrustedProjectConfigs(ctx: ExtensionContext): void {
|
|
257
|
+
const skippedConfigs = getSkippedUntrustedProjectConfigPaths(sandboxConfigPaths).filter(
|
|
258
|
+
(configPath) => !warnedSkippedProjectConfigPaths.has(configPath.path),
|
|
259
|
+
);
|
|
260
|
+
if (skippedConfigs.length === 0) return;
|
|
261
|
+
|
|
262
|
+
for (const configPath of skippedConfigs) {
|
|
263
|
+
warnedSkippedProjectConfigPaths.add(configPath.path);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const details = skippedConfigs.map((configPath) => configPath.path).join(", ");
|
|
267
|
+
notify(
|
|
268
|
+
ctx,
|
|
269
|
+
`Ignoring project sandbox config because this project is not trusted: ${details}`,
|
|
270
|
+
"warning",
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function applyRuntimeConfigForSession(
|
|
275
|
+
ctx: ExtensionContext,
|
|
276
|
+
runtimeConfig: SandboxRuntimeConfig,
|
|
277
|
+
): void {
|
|
278
|
+
const nextConfig = cloneRuntimeConfig(runtimeConfig);
|
|
279
|
+
sandboxState = { status: "active", runtimeConfig: nextConfig };
|
|
280
|
+
SandboxManager.updateConfig(nextConfig);
|
|
281
|
+
setSandboxStatus(ctx, true, nextConfig, promptMode);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const networkPermissions = createNetworkPermissions({
|
|
285
|
+
getContext: () => sessionContext,
|
|
286
|
+
getPromptMode: () => promptMode,
|
|
287
|
+
getRuntimeConfig: () => getStateRuntimeConfig(sandboxState),
|
|
288
|
+
isSuspended: () => sandboxState.status === "suspended",
|
|
289
|
+
applyRuntimeConfigForSession,
|
|
290
|
+
recordEvent: (event) => {
|
|
291
|
+
recordSandboxEvent({
|
|
292
|
+
timestamp: Date.now(),
|
|
293
|
+
kind: "network",
|
|
294
|
+
cwd: sessionCwd,
|
|
295
|
+
...event,
|
|
296
|
+
});
|
|
297
|
+
},
|
|
298
|
+
notify,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
async function initializeSandboxRuntime(
|
|
302
|
+
ctx: ExtensionContext,
|
|
303
|
+
config: SandboxConfig,
|
|
304
|
+
): Promise<SandboxRuntimeConfig | null> {
|
|
305
|
+
promptMode = normalizePromptMode(config.mode);
|
|
306
|
+
|
|
307
|
+
const dependencyErrors = getSandboxDependencyErrors(config);
|
|
308
|
+
if (dependencyErrors.length > 0) {
|
|
309
|
+
promptMode = DEFAULT_PROMPT_MODE;
|
|
310
|
+
networkPermissions.clear();
|
|
311
|
+
sandboxState = { status: "blocked", reason: "missing-dependencies" };
|
|
312
|
+
recordRuntimeEvent(
|
|
313
|
+
"init",
|
|
314
|
+
"missing-dependencies",
|
|
315
|
+
`sandbox dependencies missing: ${dependencyErrors.join("; ")}`,
|
|
316
|
+
);
|
|
317
|
+
setSandboxStatus(ctx, false);
|
|
318
|
+
notify(
|
|
319
|
+
ctx,
|
|
320
|
+
`Sandbox dependencies missing: ${dependencyErrors.join("; ")}. Shell execution is blocked until setup is fixed or sandboxing is explicitly disabled.`,
|
|
321
|
+
"error",
|
|
322
|
+
);
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const runtimeConfig = toRuntimeConfig(config);
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
await SandboxManager.initialize(runtimeConfig, networkPermissions.ask, true);
|
|
330
|
+
const activeConfig = cloneRuntimeConfig(runtimeConfig);
|
|
331
|
+
sandboxState = { status: "active", runtimeConfig: activeConfig };
|
|
332
|
+
return activeConfig;
|
|
333
|
+
} catch (error) {
|
|
334
|
+
const errorMessage = error instanceof Error ? error.message : `${error}`;
|
|
335
|
+
promptMode = DEFAULT_PROMPT_MODE;
|
|
336
|
+
networkPermissions.clear();
|
|
337
|
+
sandboxState = { status: "blocked", reason: "init-failed" };
|
|
338
|
+
recordRuntimeEvent("init", "init-failed", `sandbox initialization failed: ${errorMessage}`);
|
|
339
|
+
setSandboxStatus(ctx, false);
|
|
340
|
+
notify(ctx, `Sandbox initialization failed: ${errorMessage}`, "error");
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function resetRuntimeState(): void {
|
|
346
|
+
sandboxState = { status: "pending" };
|
|
347
|
+
sandboxConfig = null;
|
|
348
|
+
promptMode = DEFAULT_PROMPT_MODE;
|
|
349
|
+
sandboxConfigPaths = [];
|
|
350
|
+
sandboxEvents = [];
|
|
351
|
+
networkPermissions.clear();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function loadCurrentConfig(
|
|
355
|
+
ctx: ExtensionContext,
|
|
356
|
+
options: { exitOnError?: boolean } = {},
|
|
357
|
+
): SandboxConfig | null {
|
|
358
|
+
const sandboxConfigOverride = getStringFlag(pi, "sandbox-config");
|
|
359
|
+
const loadedConfig = loadSandboxConfigForContext(ctx, ctx.cwd, sandboxConfigOverride, options);
|
|
360
|
+
if (!loadedConfig) return null;
|
|
361
|
+
|
|
362
|
+
sandboxConfigPaths = loadedConfig.paths;
|
|
363
|
+
notifySkippedUntrustedProjectConfigs(ctx);
|
|
364
|
+
const parseErrors = getSandboxConfigParseErrors(sandboxConfigPaths);
|
|
365
|
+
if (parseErrors.length > 0) {
|
|
366
|
+
notifySandboxConfigParseErrors(ctx, parseErrors);
|
|
367
|
+
}
|
|
368
|
+
sandboxConfig = loadedConfig.config;
|
|
369
|
+
return loadedConfig.config;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function start(ctx: ExtensionContext): Promise<void> {
|
|
373
|
+
await cancelPermissionInvocations();
|
|
374
|
+
permissionLifetime = new AbortController();
|
|
375
|
+
setSandboxStatus(ctx, false);
|
|
376
|
+
sessionContext = permissionContext(ctx);
|
|
377
|
+
resetRuntimeState();
|
|
378
|
+
sessionCwd = ctx.cwd;
|
|
379
|
+
|
|
380
|
+
const noSandbox = pi.getFlag("no-sandbox") as boolean;
|
|
381
|
+
if (noSandbox) {
|
|
382
|
+
sandboxState = { status: "bypassed", reason: "no-sandbox-flag" };
|
|
383
|
+
announceSandboxState(pi, ctx, false, "Sandbox disabled via --no-sandbox", "warning");
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const config = loadCurrentConfig(ctx, { exitOnError: true });
|
|
388
|
+
if (!config) return;
|
|
389
|
+
|
|
390
|
+
if (!config.enabled) {
|
|
391
|
+
sandboxState = { status: "bypassed", reason: "config-disabled" };
|
|
392
|
+
announceSandboxState(pi, ctx, false, "Sandbox disabled via config");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (!isSupportedPlatform()) {
|
|
397
|
+
sandboxState = { status: "blocked", reason: "unsupported-platform" };
|
|
398
|
+
recordRuntimeEvent(
|
|
399
|
+
"init",
|
|
400
|
+
"unsupported-platform",
|
|
401
|
+
`sandbox not supported on ${process.platform}`,
|
|
402
|
+
);
|
|
403
|
+
notify(ctx, `Sandbox not supported on ${process.platform}`, "warning");
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const runtimeConfig = await initializeSandboxRuntime(ctx, config);
|
|
408
|
+
if (!runtimeConfig) return;
|
|
409
|
+
|
|
410
|
+
setSandboxStatus(ctx, true, runtimeConfig, promptMode);
|
|
411
|
+
announceSandboxState(pi, ctx, true, "Sandbox initialized");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function shutdown(ctx: ExtensionContext): Promise<void> {
|
|
415
|
+
await cancelPermissionInvocations();
|
|
416
|
+
setSandboxStatus(ctx, false);
|
|
417
|
+
if (getStateRuntimeConfig(sandboxState)) {
|
|
418
|
+
try {
|
|
419
|
+
await SandboxManager.reset();
|
|
420
|
+
} catch {
|
|
421
|
+
// Ignore cleanup errors.
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
resetRuntimeState();
|
|
426
|
+
sessionContext = null;
|
|
427
|
+
sessionCwd = process.cwd();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function enable(ctx: ExtensionContext): Promise<void> {
|
|
431
|
+
if (sandboxState.status === "active") {
|
|
432
|
+
notify(ctx, "Sandbox is already enabled", "info");
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (!isSupportedPlatform()) {
|
|
437
|
+
sandboxState = { status: "blocked", reason: "unsupported-platform" };
|
|
438
|
+
recordRuntimeEvent(
|
|
439
|
+
"init",
|
|
440
|
+
"unsupported-platform",
|
|
441
|
+
`sandbox not supported on ${process.platform}`,
|
|
442
|
+
);
|
|
443
|
+
notify(ctx, `Sandbox not supported on ${process.platform}`, "warning");
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const config = loadCurrentConfig(ctx);
|
|
448
|
+
if (!config) return;
|
|
449
|
+
|
|
450
|
+
const runtimeConfig = await initializeSandboxRuntime(ctx, config);
|
|
451
|
+
if (!runtimeConfig) return;
|
|
452
|
+
|
|
453
|
+
setSandboxStatus(ctx, true, runtimeConfig, promptMode);
|
|
454
|
+
announceSandboxState(pi, ctx, true);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function disable(ctx: ExtensionContext): Promise<void> {
|
|
458
|
+
if (sandboxState.status !== "active") {
|
|
459
|
+
notify(ctx, `Sandbox is not active (mode: ${getSandboxRunMode(sandboxState)})`, "info");
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
sandboxState = { status: "suspended" };
|
|
464
|
+
await cancelPermissionInvocations();
|
|
465
|
+
permissionLifetime = new AbortController();
|
|
466
|
+
networkPermissions.clear();
|
|
467
|
+
setSandboxStatus(ctx, false);
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
await SandboxManager.reset();
|
|
471
|
+
} catch (error) {
|
|
472
|
+
notify(
|
|
473
|
+
ctx,
|
|
474
|
+
`Sandbox disabled, but cleanup failed: ${error instanceof Error ? error.message : error}`,
|
|
475
|
+
"warning",
|
|
476
|
+
);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
announceSandboxState(pi, ctx, false);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function setPromptMode(ctx: ExtensionContext, mode: PromptMode): void {
|
|
484
|
+
if (promptMode !== mode) {
|
|
485
|
+
permissionLifetime.abort();
|
|
486
|
+
permissionLifetime = new AbortController();
|
|
487
|
+
}
|
|
488
|
+
promptMode = mode;
|
|
489
|
+
if (sandboxState.status === "active") {
|
|
490
|
+
setSandboxStatus(ctx, true, sandboxState.runtimeConfig, promptMode);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Own invocation-local permission work until its dialog or command has settled. */
|
|
495
|
+
function withPermissionContext<T>(
|
|
496
|
+
ctx: ExtensionContext,
|
|
497
|
+
signal: AbortSignal | undefined,
|
|
498
|
+
run: (ctx: ExtensionContext, signal: AbortSignal) => Promise<T>,
|
|
499
|
+
): Promise<T> {
|
|
500
|
+
const signals = [permissionLifetime.signal, ctx.signal, signal].filter(
|
|
501
|
+
(value): value is AbortSignal => value !== undefined,
|
|
502
|
+
);
|
|
503
|
+
const combined = AbortSignal.any(signals);
|
|
504
|
+
const invocationContext = permissionContext(ctx, combined);
|
|
505
|
+
const invocation = (async () => {
|
|
506
|
+
if (combined.aborted) throw new Error("aborted");
|
|
507
|
+
return run(invocationContext, combined);
|
|
508
|
+
})();
|
|
509
|
+
permissionInvocations.add(invocation);
|
|
510
|
+
return invocation.finally(() => permissionInvocations.delete(invocation));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function cancelPermissionInvocations(): Promise<void> {
|
|
514
|
+
permissionLifetime.abort();
|
|
515
|
+
await Promise.allSettled(permissionInvocations);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function permissionContext(ctx: ExtensionContext, contextSignal = ctx.signal): ExtensionContext {
|
|
519
|
+
function queued<T>(
|
|
520
|
+
run: (signal?: AbortSignal) => Promise<T>,
|
|
521
|
+
cancelled: T,
|
|
522
|
+
signal?: AbortSignal,
|
|
523
|
+
): Promise<T> {
|
|
524
|
+
const signals = [contextSignal, signal].filter(
|
|
525
|
+
(value): value is AbortSignal => value !== undefined,
|
|
526
|
+
);
|
|
527
|
+
const combined = signals.length ? AbortSignal.any(signals) : undefined;
|
|
528
|
+
const guardedRun = async (queuedSignal?: AbortSignal): Promise<T> => {
|
|
529
|
+
if (queuedSignal?.aborted) return cancelled;
|
|
530
|
+
const result = await run(queuedSignal);
|
|
531
|
+
return queuedSignal?.aborted ? cancelled : result;
|
|
532
|
+
};
|
|
533
|
+
const request = {
|
|
534
|
+
run: guardedRun,
|
|
535
|
+
signal: combined,
|
|
536
|
+
result: undefined as Promise<T> | undefined,
|
|
537
|
+
};
|
|
538
|
+
pi.events.emit("subagent:permission", request);
|
|
539
|
+
return (request.result ?? guardedRun(combined)).catch((error: unknown) => {
|
|
540
|
+
if (combined?.aborted || (error instanceof Error && error.name === "AbortError"))
|
|
541
|
+
return cancelled;
|
|
542
|
+
throw error;
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
const overrides: Pick<ExtensionContext["ui"], "select" | "confirm"> = {
|
|
546
|
+
select: (title, options, opts) =>
|
|
547
|
+
queued(
|
|
548
|
+
(signal) =>
|
|
549
|
+
isUnsandboxedApproval(options) && ctx.mode === "tui"
|
|
550
|
+
? showUnsandboxedApproval(ctx, title, signal)
|
|
551
|
+
: ctx.ui.select(title, options, { ...opts, signal }),
|
|
552
|
+
undefined,
|
|
553
|
+
opts?.signal,
|
|
554
|
+
),
|
|
555
|
+
confirm: (title, message, opts) =>
|
|
556
|
+
queued(
|
|
557
|
+
(signal) => ctx.ui.confirm(title, message, { ...opts, signal }),
|
|
558
|
+
false,
|
|
559
|
+
opts?.signal,
|
|
560
|
+
),
|
|
561
|
+
};
|
|
562
|
+
const ui = new Proxy(ctx.ui, {
|
|
563
|
+
get(_target, key) {
|
|
564
|
+
const current = ctx.ui;
|
|
565
|
+
return Reflect.get(overrides, key) ?? Reflect.get(current, key);
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
return Object.defineProperties(
|
|
569
|
+
{},
|
|
570
|
+
{
|
|
571
|
+
...Object.getOwnPropertyDescriptors(ctx),
|
|
572
|
+
ui: {
|
|
573
|
+
enumerable: true,
|
|
574
|
+
configurable: true,
|
|
575
|
+
get() {
|
|
576
|
+
void ctx.ui;
|
|
577
|
+
return ui;
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
) as ExtensionContext;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return {
|
|
585
|
+
get state() {
|
|
586
|
+
return sandboxState;
|
|
587
|
+
},
|
|
588
|
+
get config() {
|
|
589
|
+
return sandboxConfig;
|
|
590
|
+
},
|
|
591
|
+
get promptMode() {
|
|
592
|
+
return promptMode;
|
|
593
|
+
},
|
|
594
|
+
get context() {
|
|
595
|
+
return sessionContext;
|
|
596
|
+
},
|
|
597
|
+
get configPaths() {
|
|
598
|
+
return sandboxConfigPaths;
|
|
599
|
+
},
|
|
600
|
+
get events() {
|
|
601
|
+
return sandboxEvents;
|
|
602
|
+
},
|
|
603
|
+
getRuntimeConfig: () => getStateRuntimeConfig(sandboxState),
|
|
604
|
+
captureContext(ctx) {
|
|
605
|
+
sessionContext = permissionContext(ctx);
|
|
606
|
+
},
|
|
607
|
+
withPermissionContext,
|
|
608
|
+
start,
|
|
609
|
+
shutdown,
|
|
610
|
+
enable,
|
|
611
|
+
disable,
|
|
612
|
+
setPromptMode,
|
|
613
|
+
applyRuntimeConfigForSession,
|
|
614
|
+
recordEvent: recordSandboxEvent,
|
|
615
|
+
};
|
|
616
|
+
}
|