tau-all-agent 0.2.0 → 0.3.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/extensions/answer.ts +112 -85
- package/extensions/branch-term/index.ts +9 -5
- package/extensions/btw.ts +67 -36
- package/extensions/insights.ts +67 -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 +5 -0
- package/extensions/sandbox/bash.ts +83 -85
- 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 +53 -19
- 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 +139 -80
- 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,5 @@
|
|
|
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
|
+
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
|
}
|
|
@@ -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
|
|
|
@@ -19,7 +19,7 @@ interface NetworkPermissionEvent {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
interface NetworkPermissions {
|
|
22
|
-
ask: SandboxAskCallback
|
|
22
|
+
ask(request: Parameters<SandboxAskCallback>[0], signal: AbortSignal): Promise<boolean>;
|
|
23
23
|
clear(): void;
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -61,7 +61,11 @@ export function createNetworkPermissions(options: {
|
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
const ask
|
|
64
|
+
const ask = async (
|
|
65
|
+
{ host, port }: Parameters<SandboxAskCallback>[0],
|
|
66
|
+
signal: AbortSignal,
|
|
67
|
+
): Promise<boolean> => {
|
|
68
|
+
if (signal.aborted) return false;
|
|
65
69
|
if (isSuspended()) return true;
|
|
66
70
|
|
|
67
71
|
const normalizedHost = host.toLowerCase();
|
|
@@ -94,7 +98,7 @@ export function createNetworkPermissions(options: {
|
|
|
94
98
|
`Sandbox blocked network access to ${target}`,
|
|
95
99
|
"\nAllow for this session?",
|
|
96
100
|
);
|
|
97
|
-
if (!approved) {
|
|
101
|
+
if (!approved || signal.aborted) {
|
|
98
102
|
recordNetworkEvent("blocked", "missing-allowed-domain", normalizedHost, port);
|
|
99
103
|
return false;
|
|
100
104
|
}
|
|
@@ -125,6 +129,7 @@ export function createNetworkPermissions(options: {
|
|
|
125
129
|
notify(ctx, `Allowed network domain for this session: ${normalizedHost}`, "info");
|
|
126
130
|
return true;
|
|
127
131
|
} catch (error) {
|
|
132
|
+
if (signal.aborted) return false;
|
|
128
133
|
const ctx = getContext();
|
|
129
134
|
const message = `Sandbox permission prompt failed for ${normalizedHost}: ${error instanceof Error ? error.message : error}`;
|
|
130
135
|
if (ctx) notify(ctx, message, "warning");
|