tau-all-agent 0.2.0 → 0.3.1
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/extensions/answer.ts +115 -85
- package/extensions/branch-term/index.ts +9 -5
- package/extensions/btw.ts +70 -36
- package/extensions/insights.ts +70 -56
- package/extensions/loop.ts +14 -52
- package/extensions/memory.ts +65 -33
- package/extensions/notify.ts +5 -3
- package/extensions/review/fix.ts +10 -16
- package/extensions/review/git.ts +7 -1
- package/extensions/review/index.ts +2 -2
- package/extensions/review/models.ts +12 -2
- package/extensions/review/review.ts +3 -5
- package/extensions/review/runtime.ts +9 -8
- package/extensions/review/triage.ts +2 -4
- package/extensions/sandbox/README.md +7 -0
- package/extensions/sandbox/bash.ts +83 -85
- package/extensions/sandbox/command.ts +15 -1
- package/extensions/sandbox/config.ts +26 -15
- package/extensions/sandbox/index.ts +21 -11
- package/extensions/sandbox/permissions/network.ts +8 -3
- package/extensions/sandbox/permissions/unsandboxed.ts +1 -240
- package/extensions/sandbox/runtime.ts +54 -20
- package/extensions/subagent/README.md +58 -1
- package/extensions/subagent/config.ts +118 -0
- package/extensions/subagent/index.ts +54 -22
- package/extensions/telegram/README.md +2 -0
- package/extensions/telegram/assistant-result.d.mts +4 -1
- package/extensions/telegram/assistant-result.mjs +31 -16
- package/extensions/telegram/daemon.mjs +146 -194
- package/extensions/telegram/index.ts +11 -7
- package/extensions/tool-display-mode.ts +29 -50
- package/extensions/usage/README.md +5 -0
- package/extensions/usage/index.ts +341 -349
- package/extensions/websearch/README.md +8 -1
- package/extensions/websearch/config.ts +2 -1
- package/extensions/websearch/index.ts +18 -9
- package/extensions/websearch/providers/{openai-codex.pi.ts → openai.pi.ts} +51 -16
- package/extensions/websearch/providers/shared.ts +10 -3
- package/extensions/websearch/types.ts +2 -1
- package/extensions/worktree.ts +33 -89
- package/package.json +1 -1
- package/skills/oracle/SKILL.md +14 -8
- package/skills/oracle/scripts/oracle +43 -19
- package/skills/update-changelog/SKILL.md +8 -4
package/extensions/review/fix.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
REVIEW_CANCELLED_ERROR,
|
|
19
19
|
type AgentEndMessage,
|
|
20
20
|
type AgentEndMessages,
|
|
21
|
+
type AgentSettledState,
|
|
21
22
|
type FixPassAgentTracker,
|
|
22
23
|
type ReviewRuntime,
|
|
23
24
|
} from "./runtime.js";
|
|
@@ -154,8 +155,8 @@ function getLastAssistantMessage(messages: AgentEndMessages): AgentEndMessage |
|
|
|
154
155
|
return messages.findLast((message) => message?.role === "assistant");
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
function
|
|
158
|
-
return getLastAssistantMessage(messages)?.stopReason === "aborted";
|
|
158
|
+
function wasFixPassAborted(result: AgentSettledState): boolean {
|
|
159
|
+
return result.aborted || getLastAssistantMessage(result.messages)?.stopReason === "aborted";
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
async function waitForPromptStartIfImmediate(
|
|
@@ -167,13 +168,6 @@ async function waitForPromptStartIfImmediate(
|
|
|
167
168
|
await agentTracker.waitForStartAfter(startCountBeforePrompt, FIX_PASS_START_GRACE_MS);
|
|
168
169
|
}
|
|
169
170
|
|
|
170
|
-
async function waitForFixPassCompletion(
|
|
171
|
-
agentTracker: FixPassAgentTracker,
|
|
172
|
-
): Promise<AgentEndMessages> {
|
|
173
|
-
await agentTracker.waitForNextSettled();
|
|
174
|
-
return agentTracker.getLastEnd()?.messages ?? [];
|
|
175
|
-
}
|
|
176
|
-
|
|
177
171
|
export async function runFixPassFromReview(
|
|
178
172
|
pi: ExtensionAPI,
|
|
179
173
|
ctx: ExtensionCommandContext,
|
|
@@ -181,9 +175,9 @@ export async function runFixPassFromReview(
|
|
|
181
175
|
additionalContext: string | undefined,
|
|
182
176
|
agentTracker: FixPassAgentTracker,
|
|
183
177
|
reviewMessageQueue: ReviewMessageQueue,
|
|
184
|
-
): Promise<
|
|
178
|
+
): Promise<AgentSettledState> {
|
|
185
179
|
const failedFocusCount = countFailedFocusRuns(reviewDetails);
|
|
186
|
-
const fixPassFinished =
|
|
180
|
+
const fixPassFinished = agentTracker.waitForNextSettled();
|
|
187
181
|
|
|
188
182
|
const startCountBeforeSteering = agentTracker.getStartCount();
|
|
189
183
|
const steeringStartsImmediately = ctx.isIdle();
|
|
@@ -200,11 +194,11 @@ export async function runFixPassFromReview(
|
|
|
200
194
|
});
|
|
201
195
|
await waitForPromptStartIfImmediate(agentTracker, startCountBeforeFix, fixPass.startsImmediately);
|
|
202
196
|
|
|
203
|
-
const
|
|
204
|
-
if (!
|
|
197
|
+
const result = await fixPassFinished;
|
|
198
|
+
if (!wasFixPassAborted(result)) {
|
|
205
199
|
notifyFixUsedPartialReview(ctx, failedFocusCount);
|
|
206
200
|
}
|
|
207
|
-
return
|
|
201
|
+
return result;
|
|
208
202
|
}
|
|
209
203
|
|
|
210
204
|
export async function runFixLoop(
|
|
@@ -220,7 +214,7 @@ export async function runFixLoop(
|
|
|
220
214
|
if (!reviewDetails) return;
|
|
221
215
|
|
|
222
216
|
const beforeFixFingerprint = reviewDetails.fingerprint;
|
|
223
|
-
const
|
|
217
|
+
const result = await runFixPassFromReview(
|
|
224
218
|
pi,
|
|
225
219
|
ctx,
|
|
226
220
|
reviewDetails,
|
|
@@ -229,7 +223,7 @@ export async function runFixLoop(
|
|
|
229
223
|
reviewMessageQueue,
|
|
230
224
|
);
|
|
231
225
|
if (runtime.closed) return;
|
|
232
|
-
if (
|
|
226
|
+
if (wasFixPassAborted(result)) {
|
|
233
227
|
notify(ctx, "Fix loop stopped: fix pass was aborted.", "warning");
|
|
234
228
|
return;
|
|
235
229
|
}
|
package/extensions/review/git.ts
CHANGED
|
@@ -47,7 +47,13 @@ function hashString(value: string): string {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
function runGit(context: ReviewCommandContext, args: string[]) {
|
|
50
|
-
|
|
50
|
+
// Diff can refresh index metadata even when optional locks are disabled.
|
|
51
|
+
return runReviewCommand(context, "git", [
|
|
52
|
+
"--no-optional-locks",
|
|
53
|
+
"-c",
|
|
54
|
+
"diff.autoRefreshIndex=false",
|
|
55
|
+
...args,
|
|
56
|
+
]);
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
export async function isGitRepo(context: ReviewCommandContext): Promise<boolean> {
|
|
@@ -103,8 +103,8 @@ export default function reviewExtension(pi: ExtensionAPI) {
|
|
|
103
103
|
});
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
pi.on("agent_settled", async () => {
|
|
107
|
-
agentTracker.handleSettled();
|
|
106
|
+
pi.on("agent_settled", async (event) => {
|
|
107
|
+
agentTracker.handleSettled(event.aborted);
|
|
108
108
|
});
|
|
109
109
|
|
|
110
110
|
pi.registerCommand("review", {
|
|
@@ -3,8 +3,8 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
3
3
|
|
|
4
4
|
import { REVIEW_TASK_TIMEOUT_MS } from "./runner.js";
|
|
5
5
|
|
|
6
|
-
const OPENAI_FAST_MODEL_ID = "gpt-
|
|
7
|
-
const ANTHROPIC_FAST_MODEL_ID = "claude-haiku-
|
|
6
|
+
const OPENAI_FAST_MODEL_ID = "gpt-6-luna";
|
|
7
|
+
const ANTHROPIC_FAST_MODEL_ID = "claude-haiku-5-5";
|
|
8
8
|
|
|
9
9
|
type ModelFamily = "openai" | "anthropic";
|
|
10
10
|
|
|
@@ -310,6 +310,16 @@ function createResolvedReviewModel(options: {
|
|
|
310
310
|
};
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
// --no-extensions also disables the built-in llama.cpp provider.
|
|
314
|
+
export function buildReviewModelArgs(modelArg: string | undefined): string[] {
|
|
315
|
+
if (!modelArg) return [];
|
|
316
|
+
return [
|
|
317
|
+
"--model",
|
|
318
|
+
modelArg,
|
|
319
|
+
...(/^llama\.cpp\//i.test(modelArg) ? ["--extension", "builtin:llama.cpp"] : []),
|
|
320
|
+
];
|
|
321
|
+
}
|
|
322
|
+
|
|
313
323
|
export function selectReviewDedupModel(ctx: ExtensionContext): { modelArg: string } | null {
|
|
314
324
|
if (!ctx.model) return null;
|
|
315
325
|
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
buildResolvedReviewModelLabel,
|
|
55
55
|
buildResolvedReviewStatusModelLabel,
|
|
56
56
|
buildReviewProgressModelLabel,
|
|
57
|
+
buildReviewModelArgs,
|
|
57
58
|
clearProviderCandidateProbe,
|
|
58
59
|
getFallbackThinkingLevels,
|
|
59
60
|
getProviderCandidateAvailability,
|
|
@@ -572,10 +573,8 @@ async function runFocusTaskOnce(
|
|
|
572
573
|
"--no-skills",
|
|
573
574
|
"--no-prompt-templates",
|
|
574
575
|
"--no-themes",
|
|
576
|
+
...buildReviewModelArgs(modelArg),
|
|
575
577
|
];
|
|
576
|
-
if (modelArg) {
|
|
577
|
-
args.push("--model", modelArg);
|
|
578
|
-
}
|
|
579
578
|
|
|
580
579
|
const taskResult = await runPiSubmitToolTask({
|
|
581
580
|
args,
|
|
@@ -862,8 +861,7 @@ async function runReviewDedupTask(options: {
|
|
|
862
861
|
"--no-skills",
|
|
863
862
|
"--no-prompt-templates",
|
|
864
863
|
"--no-themes",
|
|
865
|
-
|
|
866
|
-
model.modelArg,
|
|
864
|
+
...buildReviewModelArgs(model.modelArg),
|
|
867
865
|
],
|
|
868
866
|
prompt: buildReviewDedupPrompt(findings),
|
|
869
867
|
cwd,
|
|
@@ -30,17 +30,18 @@ export type AgentEndState = {
|
|
|
30
30
|
messages: AgentEndMessages;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
export type AgentSettledState = AgentEndState & { aborted: boolean };
|
|
34
|
+
|
|
33
35
|
export type FixPassAgentTracker = {
|
|
34
|
-
waitForNextSettled: () => Promise<
|
|
36
|
+
waitForNextSettled: () => Promise<AgentSettledState>;
|
|
35
37
|
waitForStartAfter: (lastSeenStartCount: number, timeoutMs: number) => Promise<boolean>;
|
|
36
38
|
getStartCount: () => number;
|
|
37
|
-
getLastEnd: () => AgentEndState | undefined;
|
|
38
39
|
};
|
|
39
40
|
|
|
40
41
|
export type AgentRunTracker = FixPassAgentTracker & {
|
|
41
42
|
handleStart: () => void;
|
|
42
43
|
handleEnd: (state: AgentEndState) => void;
|
|
43
|
-
handleSettled: () => void;
|
|
44
|
+
handleSettled: (aborted: boolean) => void;
|
|
44
45
|
reset: () => void;
|
|
45
46
|
};
|
|
46
47
|
|
|
@@ -264,12 +265,12 @@ export function renderReviewProgressHeader(
|
|
|
264
265
|
}
|
|
265
266
|
|
|
266
267
|
export function createAgentRunTracker(): AgentRunTracker {
|
|
267
|
-
let resolveNextAgentSettled: (() => void) | undefined;
|
|
268
|
+
let resolveNextAgentSettled: ((state: AgentSettledState) => void) | undefined;
|
|
268
269
|
let resolveNextAgentStart: (() => void) | undefined;
|
|
269
270
|
let lastAgentEnd: AgentEndState | undefined;
|
|
270
271
|
let agentStartCount = 0;
|
|
271
272
|
|
|
272
|
-
function waitForNextSettled(): Promise<
|
|
273
|
+
function waitForNextSettled(): Promise<AgentSettledState> {
|
|
273
274
|
return new Promise((resolve) => {
|
|
274
275
|
resolveNextAgentSettled = resolve;
|
|
275
276
|
});
|
|
@@ -301,8 +302,8 @@ export function createAgentRunTracker(): AgentRunTracker {
|
|
|
301
302
|
waitForNextSettled,
|
|
302
303
|
waitForStartAfter,
|
|
303
304
|
getStartCount: () => agentStartCount,
|
|
304
|
-
getLastEnd: () => lastAgentEnd,
|
|
305
305
|
handleStart: () => {
|
|
306
|
+
lastAgentEnd = undefined;
|
|
306
307
|
agentStartCount += 1;
|
|
307
308
|
const resolve = resolveNextAgentStart;
|
|
308
309
|
if (!resolve) return;
|
|
@@ -312,11 +313,11 @@ export function createAgentRunTracker(): AgentRunTracker {
|
|
|
312
313
|
handleEnd: (state) => {
|
|
313
314
|
lastAgentEnd = state;
|
|
314
315
|
},
|
|
315
|
-
handleSettled: () => {
|
|
316
|
+
handleSettled: (aborted) => {
|
|
316
317
|
const resolve = resolveNextAgentSettled;
|
|
317
318
|
if (!resolve) return;
|
|
318
319
|
resolveNextAgentSettled = undefined;
|
|
319
|
-
resolve();
|
|
320
|
+
resolve({ messages: lastAgentEnd?.messages ?? [], aborted });
|
|
320
321
|
},
|
|
321
322
|
reset: () => {
|
|
322
323
|
resolveNextAgentSettled = undefined;
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
} from "./git.js";
|
|
36
36
|
import {
|
|
37
37
|
buildResolvedReviewStatusModelLabel,
|
|
38
|
+
buildReviewModelArgs,
|
|
38
39
|
getResolvedReviewStatusModelArg,
|
|
39
40
|
resolveModels,
|
|
40
41
|
type ResolvedReviewModel,
|
|
@@ -451,11 +452,8 @@ async function runTriageTask(options: {
|
|
|
451
452
|
"--no-skills",
|
|
452
453
|
"--no-prompt-templates",
|
|
453
454
|
"--no-themes",
|
|
455
|
+
...buildReviewModelArgs(getResolvedReviewStatusModelArg(model)),
|
|
454
456
|
];
|
|
455
|
-
const modelArg = getResolvedReviewStatusModelArg(model);
|
|
456
|
-
if (modelArg) {
|
|
457
|
-
args.push("--model", modelArg);
|
|
458
|
-
}
|
|
459
457
|
|
|
460
458
|
for (let attempt = 0; ; attempt += 1) {
|
|
461
459
|
signal.throwIfAborted();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Sandbox
|
|
2
|
+
|
|
3
|
+
Sandboxes Bash commands and their descendants on macOS and Linux, including calls through codemode. Pi itself, other tools, and MCP servers retain host permissions. Run `/sandbox` for help.
|
|
4
|
+
|
|
5
|
+
Relative filesystem rules and socket paths resolve from the owning session's working directory, including session-only `/sandbox filesystem` changes. Configuration files are not rewritten.
|
|
6
|
+
|
|
7
|
+
Terminal-status reports may expose approval titles containing commands, paths, or hosts. Disable these reports with `PI_PROGRAM_STATUS=0`.
|
|
@@ -209,7 +209,7 @@ function maybeAllowGitMetadataWriteForSession(options: {
|
|
|
209
209
|
applyRuntimeConfigForSession(ctx, nextConfig);
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
/** Create a single-invocation native backend
|
|
212
|
+
/** Create a single-invocation native backend that requires fresh human approval. */
|
|
213
213
|
export function createUnsandboxedBashOps(
|
|
214
214
|
runtime: SandboxRuntime,
|
|
215
215
|
sandboxedOps: SandboxedBashOperations,
|
|
@@ -243,7 +243,7 @@ export function createUnsandboxedBashOps(
|
|
|
243
243
|
return sandboxedOps.runSerially(async () => {
|
|
244
244
|
assertAvailable();
|
|
245
245
|
if (options.signal?.aborted) throw new Error("aborted");
|
|
246
|
-
const title = formatUnsandboxedApproval(command
|
|
246
|
+
const title = formatUnsandboxedApproval(command);
|
|
247
247
|
let choice: string | undefined;
|
|
248
248
|
try {
|
|
249
249
|
choice = await ctx.ui.select(title, [...UNSANDBOXED_APPROVAL_CHOICES], {
|
|
@@ -672,104 +672,102 @@ export function createSandboxedBashOps(options: SandboxedBashOpsOptions): Sandbo
|
|
|
672
672
|
runSerially,
|
|
673
673
|
async exec(command, cwd, { onData, signal, timeout, env }) {
|
|
674
674
|
validateTimeout(timeout);
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
675
|
+
if (!existsSync(cwd)) {
|
|
676
|
+
throw new Error(`Working directory does not exist: ${cwd}`);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const attemptStartedAt = Date.now();
|
|
680
|
+
const initialRun = await prepareAndRunSandboxAttempt({
|
|
681
|
+
command,
|
|
682
|
+
cwd,
|
|
683
|
+
onData,
|
|
684
|
+
signal,
|
|
685
|
+
timeout,
|
|
686
|
+
env,
|
|
687
|
+
});
|
|
688
|
+
const retryTimeout = getRemainingTimeout(timeout, attemptStartedAt);
|
|
679
689
|
|
|
680
|
-
|
|
681
|
-
|
|
690
|
+
let processedAttempt: ProcessedSandboxAttempt;
|
|
691
|
+
try {
|
|
692
|
+
processedAttempt = await processSandboxAttempt({
|
|
693
|
+
attempt: initialRun.attempt,
|
|
682
694
|
command,
|
|
695
|
+
commandId: initialRun.commandId,
|
|
683
696
|
cwd,
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
timeout,
|
|
687
|
-
env,
|
|
697
|
+
runtimeConfig: initialRun.runtimeConfig,
|
|
698
|
+
autoRetryAvailable: true,
|
|
688
699
|
});
|
|
689
|
-
|
|
700
|
+
} catch (postProcessError) {
|
|
701
|
+
reportPostProcessingError(postProcessError);
|
|
702
|
+
safeCleanupAfterCommand();
|
|
703
|
+
return { exitCode: initialRun.attempt.exitCode };
|
|
704
|
+
}
|
|
690
705
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
cwd,
|
|
698
|
-
runtimeConfig: initialRun.runtimeConfig,
|
|
699
|
-
autoRetryAvailable: true,
|
|
700
|
-
});
|
|
701
|
-
} catch (postProcessError) {
|
|
702
|
-
reportPostProcessingError(postProcessError);
|
|
703
|
-
safeCleanupAfterCommand();
|
|
704
|
-
return { exitCode: initialRun.attempt.exitCode };
|
|
705
|
-
}
|
|
706
|
+
const retryResolution = processedAttempt.resolution;
|
|
707
|
+
if (retryResolution?.kind !== "allow-retry") {
|
|
708
|
+
if (processedAttempt.postamble) onData(Buffer.from(processedAttempt.postamble));
|
|
709
|
+
safeCleanupAfterCommand();
|
|
710
|
+
return { exitCode: processedAttempt.exitCode };
|
|
711
|
+
}
|
|
706
712
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
safeCleanupAfterCommand();
|
|
711
|
-
return { exitCode: processedAttempt.exitCode };
|
|
712
|
-
}
|
|
713
|
+
if (processedAttempt.postamble) {
|
|
714
|
+
onData(Buffer.from(ensureTrailingNewline(processedAttempt.postamble)));
|
|
715
|
+
}
|
|
713
716
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
}
|
|
717
|
+
initialRun.attempt.combinedOutput = "";
|
|
718
|
+
safeCleanupAfterCommand();
|
|
717
719
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
+
if (retryTimeout !== undefined && retryTimeout <= 0) {
|
|
721
|
+
onData(Buffer.from(retryResolution.retrySkippedMessage));
|
|
722
|
+
return { exitCode: processedAttempt.exitCode };
|
|
723
|
+
}
|
|
720
724
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
+
const retryRun = await prepareAndRunSandboxAttempt({
|
|
726
|
+
command,
|
|
727
|
+
cwd,
|
|
728
|
+
onData,
|
|
729
|
+
signal,
|
|
730
|
+
timeout: retryTimeout,
|
|
731
|
+
env,
|
|
732
|
+
});
|
|
725
733
|
|
|
726
|
-
|
|
734
|
+
let processedRetry: ProcessedSandboxAttempt;
|
|
735
|
+
try {
|
|
736
|
+
processedRetry = await processSandboxAttempt({
|
|
737
|
+
attempt: retryRun.attempt,
|
|
727
738
|
command,
|
|
739
|
+
commandId: retryRun.commandId,
|
|
728
740
|
cwd,
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
timeout: retryTimeout,
|
|
732
|
-
env,
|
|
741
|
+
runtimeConfig: retryRun.runtimeConfig,
|
|
742
|
+
autoRetryAvailable: false,
|
|
733
743
|
});
|
|
744
|
+
} catch (postProcessError) {
|
|
745
|
+
reportPostProcessingError(postProcessError);
|
|
746
|
+
safeCleanupAfterCommand();
|
|
747
|
+
return { exitCode: retryRun.attempt.exitCode };
|
|
748
|
+
}
|
|
734
749
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
if (processedRetry.exitCode === 0) {
|
|
753
|
-
retryPostamble = appendOutputPostamble(
|
|
754
|
-
retryPostamble,
|
|
755
|
-
retryResolution.retrySuccessMessage,
|
|
756
|
-
retryRun.attempt.combinedOutput,
|
|
757
|
-
);
|
|
758
|
-
} else if (
|
|
759
|
-
!processedRetry.resolution &&
|
|
760
|
-
processedRetry.runtimeProtectedWriteViolations.length === 0
|
|
761
|
-
) {
|
|
762
|
-
retryPostamble = appendOutputPostamble(
|
|
763
|
-
retryPostamble,
|
|
764
|
-
retryResolution.retryFailureMessage,
|
|
765
|
-
retryRun.attempt.combinedOutput,
|
|
766
|
-
);
|
|
767
|
-
}
|
|
750
|
+
let retryPostamble = processedRetry.postamble;
|
|
751
|
+
if (processedRetry.exitCode === 0) {
|
|
752
|
+
retryPostamble = appendOutputPostamble(
|
|
753
|
+
retryPostamble,
|
|
754
|
+
retryResolution.retrySuccessMessage,
|
|
755
|
+
retryRun.attempt.combinedOutput,
|
|
756
|
+
);
|
|
757
|
+
} else if (
|
|
758
|
+
!processedRetry.resolution &&
|
|
759
|
+
processedRetry.runtimeProtectedWriteViolations.length === 0
|
|
760
|
+
) {
|
|
761
|
+
retryPostamble = appendOutputPostamble(
|
|
762
|
+
retryPostamble,
|
|
763
|
+
retryResolution.retryFailureMessage,
|
|
764
|
+
retryRun.attempt.combinedOutput,
|
|
765
|
+
);
|
|
766
|
+
}
|
|
768
767
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
}, signal);
|
|
768
|
+
if (retryPostamble) onData(Buffer.from(retryPostamble));
|
|
769
|
+
safeCleanupAfterCommand();
|
|
770
|
+
return { exitCode: processedRetry.exitCode };
|
|
773
771
|
},
|
|
774
772
|
};
|
|
775
773
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
cloneRuntimeConfig,
|
|
6
6
|
escapeSlashCommandArg,
|
|
7
7
|
mutateStringList,
|
|
8
|
+
resolveSandboxPath,
|
|
8
9
|
type ListOp,
|
|
9
10
|
type PromptMode,
|
|
10
11
|
type SandboxConfigPath,
|
|
@@ -585,7 +586,20 @@ export function registerSandboxCommand(pi: ExtensionAPI, runtime: SandboxRuntime
|
|
|
585
586
|
: list === "allow-write"
|
|
586
587
|
? nextConfig.filesystem.allowWrite
|
|
587
588
|
: nextConfig.filesystem.denyWrite;
|
|
588
|
-
|
|
589
|
+
// Exact runtime entries are already expanded and may contain literal dollars.
|
|
590
|
+
const resolvedPath = values.includes(targetPath)
|
|
591
|
+
? targetPath
|
|
592
|
+
: resolveSandboxPath(targetPath, `filesystem ${list}`, ctx.cwd);
|
|
593
|
+
if (resolvedPath === null) {
|
|
594
|
+
notify(
|
|
595
|
+
ctx,
|
|
596
|
+
"Invalid filesystem path: unset or empty environment variable, or control characters",
|
|
597
|
+
"warning",
|
|
598
|
+
);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const changed = mutateStringList(values, op, resolvedPath);
|
|
589
603
|
if (!changed) {
|
|
590
604
|
notify(
|
|
591
605
|
ctx,
|
|
@@ -291,7 +291,7 @@ export function normalizePromptMode(value: unknown): PromptMode {
|
|
|
291
291
|
|
|
292
292
|
function expandPath(value: string, cwd?: string): string {
|
|
293
293
|
const expanded =
|
|
294
|
-
value === "~" ? homedir() : value.startsWith("~/") ?
|
|
294
|
+
value === "~" ? homedir() : value.startsWith("~/") ? homedir() + value.slice(1) : value;
|
|
295
295
|
return cwd && !expanded.startsWith("/") ? resolve(cwd, expanded) : expanded;
|
|
296
296
|
}
|
|
297
297
|
|
|
@@ -570,7 +570,7 @@ function containsControlCharacter(value: string): boolean {
|
|
|
570
570
|
return false;
|
|
571
571
|
}
|
|
572
572
|
|
|
573
|
-
function
|
|
573
|
+
export function resolveSandboxPath(value: string, field: string, cwd: string): string | null {
|
|
574
574
|
const source = value.trim();
|
|
575
575
|
if (!source) return null;
|
|
576
576
|
|
|
@@ -605,24 +605,29 @@ function expandPathConfigEntry(value: string, field: string): string | null {
|
|
|
605
605
|
return null;
|
|
606
606
|
}
|
|
607
607
|
|
|
608
|
-
return expanded;
|
|
608
|
+
return expandPath(expanded, cwd);
|
|
609
609
|
}
|
|
610
610
|
|
|
611
|
-
function expandPathConfigList(
|
|
611
|
+
function expandPathConfigList(
|
|
612
|
+
values: string[] | undefined,
|
|
613
|
+
field: string,
|
|
614
|
+
cwd: string,
|
|
615
|
+
): string[] | undefined {
|
|
612
616
|
if (!values) return undefined;
|
|
613
617
|
|
|
614
618
|
return values
|
|
615
|
-
.map((value) =>
|
|
619
|
+
.map((value) => resolveSandboxPath(value, field, cwd))
|
|
616
620
|
.filter((value): value is string => value !== null);
|
|
617
621
|
}
|
|
618
622
|
|
|
619
623
|
function expandMitmProxyPathConfig(
|
|
620
624
|
mitmProxy: SandboxRuntimeConfig["network"]["mitmProxy"],
|
|
625
|
+
cwd: string,
|
|
621
626
|
): SandboxRuntimeConfig["network"]["mitmProxy"] {
|
|
622
627
|
if (!mitmProxy) return undefined;
|
|
623
628
|
if (!isPlainObject(mitmProxy) || typeof mitmProxy.socketPath !== "string") return mitmProxy;
|
|
624
629
|
|
|
625
|
-
const socketPath =
|
|
630
|
+
const socketPath = resolveSandboxPath(mitmProxy.socketPath, "network.mitmProxy.socketPath", cwd);
|
|
626
631
|
if (!socketPath) {
|
|
627
632
|
console.error("Warning: Disabling network.mitmProxy because its socketPath did not expand.");
|
|
628
633
|
return undefined;
|
|
@@ -633,30 +638,36 @@ function expandMitmProxyPathConfig(
|
|
|
633
638
|
|
|
634
639
|
function expandNetworkPathConfig(
|
|
635
640
|
network: SandboxRuntimeConfig["network"],
|
|
641
|
+
cwd: string,
|
|
636
642
|
): SandboxRuntimeConfig["network"] {
|
|
637
643
|
return {
|
|
638
644
|
...network,
|
|
639
|
-
allowUnixSockets: expandPathConfigList(
|
|
640
|
-
|
|
645
|
+
allowUnixSockets: expandPathConfigList(
|
|
646
|
+
network.allowUnixSockets,
|
|
647
|
+
"network.allowUnixSockets",
|
|
648
|
+
cwd,
|
|
649
|
+
),
|
|
650
|
+
mitmProxy: expandMitmProxyPathConfig(network.mitmProxy, cwd),
|
|
641
651
|
};
|
|
642
652
|
}
|
|
643
653
|
|
|
644
654
|
function expandFilesystemPathConfig(
|
|
645
655
|
filesystem: SandboxRuntimeConfig["filesystem"],
|
|
656
|
+
cwd: string,
|
|
646
657
|
): SandboxRuntimeConfig["filesystem"] {
|
|
647
658
|
return {
|
|
648
659
|
...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") ?? [],
|
|
660
|
+
denyRead: expandPathConfigList(filesystem.denyRead, "filesystem.denyRead", cwd) ?? [],
|
|
661
|
+
allowRead: expandPathConfigList(filesystem.allowRead, "filesystem.allowRead", cwd),
|
|
662
|
+
allowWrite: expandPathConfigList(filesystem.allowWrite, "filesystem.allowWrite", cwd) ?? [],
|
|
663
|
+
denyWrite: expandPathConfigList(filesystem.denyWrite, "filesystem.denyWrite", cwd) ?? [],
|
|
653
664
|
};
|
|
654
665
|
}
|
|
655
666
|
|
|
656
|
-
export function toRuntimeConfig(config: SandboxConfig): SandboxRuntimeConfig {
|
|
667
|
+
export function toRuntimeConfig(config: SandboxConfig, cwd: string): SandboxRuntimeConfig {
|
|
657
668
|
const { allowGitCommonDir: _allowGitCommonDir, allowTempDirs, ...filesystem } = config.filesystem;
|
|
658
|
-
const expandedNetwork = expandNetworkPathConfig(config.network);
|
|
659
|
-
const expandedFilesystem = expandFilesystemPathConfig(filesystem);
|
|
669
|
+
const expandedNetwork = expandNetworkPathConfig(config.network, cwd);
|
|
670
|
+
const expandedFilesystem = expandFilesystemPathConfig(filesystem, cwd);
|
|
660
671
|
const allowWrite = allowTempDirs
|
|
661
672
|
? [...expandedFilesystem.allowWrite, ...getTemporaryWritePaths()]
|
|
662
673
|
: expandedFilesystem.allowWrite;
|
|
@@ -63,7 +63,7 @@ import { Type } from "typebox";
|
|
|
63
63
|
|
|
64
64
|
import { createSandboxedBashOps, createUnsandboxedBashOps } from "./bash.js";
|
|
65
65
|
import { registerSandboxCommand } from "./command.js";
|
|
66
|
-
import { isUnsandboxedApproval
|
|
66
|
+
import { isUnsandboxedApproval } from "./permissions/unsandboxed.js";
|
|
67
67
|
import { createSandboxRuntime, getSandboxRunMode } from "./runtime.js";
|
|
68
68
|
|
|
69
69
|
export default function sandboxExtension(pi: ExtensionAPI): void {
|
|
@@ -102,6 +102,7 @@ export default function sandboxExtension(pi: ExtensionAPI): void {
|
|
|
102
102
|
.withPermissionContext(ctx, request.signal, async (_ctx, signal) => {
|
|
103
103
|
if (
|
|
104
104
|
!ctx.hasUI ||
|
|
105
|
+
!["tui", "rpc"].includes(ctx.mode) ||
|
|
105
106
|
runtime.state.status !== "active" ||
|
|
106
107
|
runtime.promptMode !== "interactive"
|
|
107
108
|
) {
|
|
@@ -109,12 +110,7 @@ export default function sandboxExtension(pi: ExtensionAPI): void {
|
|
|
109
110
|
}
|
|
110
111
|
// The subagent already owns the parent's permission queue. Use the original UI,
|
|
111
112
|
// not the invocation wrapper, to avoid recursively entering that queue.
|
|
112
|
-
const choice =
|
|
113
|
-
ctx.mode === "tui"
|
|
114
|
-
? await showUnsandboxedApproval(ctx, title, signal)
|
|
115
|
-
: ctx.mode === "rpc"
|
|
116
|
-
? await ctx.ui.select(title, choices, { signal })
|
|
117
|
-
: undefined;
|
|
113
|
+
const choice = await ctx.ui.select(title, choices, { signal });
|
|
118
114
|
return signal.aborted ||
|
|
119
115
|
runtime.state.status !== "active" ||
|
|
120
116
|
runtime.promptMode !== "interactive"
|
|
@@ -155,8 +151,6 @@ export default function sandboxExtension(pi: ExtensionAPI): void {
|
|
|
155
151
|
});
|
|
156
152
|
|
|
157
153
|
const localBashTool = createBashToolDefinition(process.cwd());
|
|
158
|
-
const sandboxedBashTool = createBashToolDefinition(process.cwd(), { operations: sandboxedOps });
|
|
159
|
-
|
|
160
154
|
pi.registerTool({
|
|
161
155
|
...localBashTool,
|
|
162
156
|
label: "bash (sandbox-aware)",
|
|
@@ -222,8 +216,24 @@ export default function sandboxExtension(pi: ExtensionAPI): void {
|
|
|
222
216
|
return localBashTool.execute(id, params, signal, onUpdate, ctx);
|
|
223
217
|
}
|
|
224
218
|
|
|
225
|
-
runtime.
|
|
226
|
-
|
|
219
|
+
return runtime.withPermissionContext(ctx, signal, (invocationContext, invocationSignal) => {
|
|
220
|
+
const tool = createBashToolDefinition(ctx.cwd, {
|
|
221
|
+
operations: {
|
|
222
|
+
exec(command, cwd, options) {
|
|
223
|
+
return sandboxedOps.runSerially(
|
|
224
|
+
() =>
|
|
225
|
+
runtime.withBashContext(ctx, invocationSignal, async () => {
|
|
226
|
+
const result = await sandboxedOps.exec(command, cwd, options);
|
|
227
|
+
if (invocationSignal.aborted) throw new Error("aborted");
|
|
228
|
+
return result;
|
|
229
|
+
}),
|
|
230
|
+
invocationSignal,
|
|
231
|
+
);
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
return tool.execute(id, params, invocationSignal, onUpdate, invocationContext);
|
|
236
|
+
});
|
|
227
237
|
},
|
|
228
238
|
});
|
|
229
239
|
|