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.
Files changed (42) hide show
  1. package/extensions/answer.ts +112 -85
  2. package/extensions/branch-term/index.ts +9 -5
  3. package/extensions/btw.ts +67 -36
  4. package/extensions/insights.ts +67 -56
  5. package/extensions/loop.ts +14 -52
  6. package/extensions/memory.ts +65 -33
  7. package/extensions/notify.ts +5 -3
  8. package/extensions/review/fix.ts +10 -16
  9. package/extensions/review/git.ts +7 -1
  10. package/extensions/review/index.ts +2 -2
  11. package/extensions/review/models.ts +12 -2
  12. package/extensions/review/review.ts +3 -5
  13. package/extensions/review/runtime.ts +9 -8
  14. package/extensions/review/triage.ts +2 -4
  15. package/extensions/sandbox/README.md +5 -0
  16. package/extensions/sandbox/bash.ts +83 -85
  17. package/extensions/sandbox/index.ts +21 -11
  18. package/extensions/sandbox/permissions/network.ts +8 -3
  19. package/extensions/sandbox/permissions/unsandboxed.ts +1 -240
  20. package/extensions/sandbox/runtime.ts +53 -19
  21. package/extensions/subagent/README.md +58 -1
  22. package/extensions/subagent/config.ts +118 -0
  23. package/extensions/subagent/index.ts +54 -22
  24. package/extensions/telegram/README.md +2 -0
  25. package/extensions/telegram/assistant-result.d.mts +4 -1
  26. package/extensions/telegram/assistant-result.mjs +31 -16
  27. package/extensions/telegram/daemon.mjs +146 -194
  28. package/extensions/telegram/index.ts +11 -7
  29. package/extensions/tool-display-mode.ts +29 -50
  30. package/extensions/usage/README.md +5 -0
  31. package/extensions/usage/index.ts +139 -80
  32. package/extensions/websearch/README.md +8 -1
  33. package/extensions/websearch/config.ts +2 -1
  34. package/extensions/websearch/index.ts +18 -9
  35. package/extensions/websearch/providers/{openai-codex.pi.ts → openai.pi.ts} +51 -16
  36. package/extensions/websearch/providers/shared.ts +10 -3
  37. package/extensions/websearch/types.ts +2 -1
  38. package/extensions/worktree.ts +33 -89
  39. package/package.json +1 -1
  40. package/skills/oracle/SKILL.md +14 -8
  41. package/skills/oracle/scripts/oracle +43 -19
  42. package/skills/update-changelog/SKILL.md +8 -4
@@ -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 wasLastAssistantAborted(messages: AgentEndMessages): boolean {
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<AgentEndMessages> {
178
+ ): Promise<AgentSettledState> {
185
179
  const failedFocusCount = countFailedFocusRuns(reviewDetails);
186
- const fixPassFinished = waitForFixPassCompletion(agentTracker);
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 fixMessages = await fixPassFinished;
204
- if (!wasLastAssistantAborted(fixMessages)) {
197
+ const result = await fixPassFinished;
198
+ if (!wasFixPassAborted(result)) {
205
199
  notifyFixUsedPartialReview(ctx, failedFocusCount);
206
200
  }
207
- return fixMessages;
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 fixMessages = await runFixPassFromReview(
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 (wasLastAssistantAborted(fixMessages)) {
226
+ if (wasFixPassAborted(result)) {
233
227
  notify(ctx, "Fix loop stopped: fix pass was aborted.", "warning");
234
228
  return;
235
229
  }
@@ -47,7 +47,13 @@ function hashString(value: string): string {
47
47
  }
48
48
 
49
49
  function runGit(context: ReviewCommandContext, args: string[]) {
50
- return runReviewCommand(context, "git", args);
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-5.6-luna";
7
- const ANTHROPIC_FAST_MODEL_ID = "claude-haiku-4-5";
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
- "--model",
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<void>;
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<void> {
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 whose approval covers the resolved spawn context. */
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, absoluteCwd);
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
- return runSerially(async () => {
676
- if (!existsSync(cwd)) {
677
- throw new Error(`Working directory does not exist: ${cwd}`);
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
- const attemptStartedAt = Date.now();
681
- const initialRun = await prepareAndRunSandboxAttempt({
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
- onData,
685
- signal,
686
- timeout,
687
- env,
697
+ runtimeConfig: initialRun.runtimeConfig,
698
+ autoRetryAvailable: true,
688
699
  });
689
- const retryTimeout = getRemainingTimeout(timeout, attemptStartedAt);
700
+ } catch (postProcessError) {
701
+ reportPostProcessingError(postProcessError);
702
+ safeCleanupAfterCommand();
703
+ return { exitCode: initialRun.attempt.exitCode };
704
+ }
690
705
 
691
- let processedAttempt: ProcessedSandboxAttempt;
692
- try {
693
- processedAttempt = await processSandboxAttempt({
694
- attempt: initialRun.attempt,
695
- command,
696
- commandId: initialRun.commandId,
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
- const retryResolution = processedAttempt.resolution;
708
- if (retryResolution?.kind !== "allow-retry") {
709
- if (processedAttempt.postamble) onData(Buffer.from(processedAttempt.postamble));
710
- safeCleanupAfterCommand();
711
- return { exitCode: processedAttempt.exitCode };
712
- }
713
+ if (processedAttempt.postamble) {
714
+ onData(Buffer.from(ensureTrailingNewline(processedAttempt.postamble)));
715
+ }
713
716
 
714
- if (processedAttempt.postamble) {
715
- onData(Buffer.from(ensureTrailingNewline(processedAttempt.postamble)));
716
- }
717
+ initialRun.attempt.combinedOutput = "";
718
+ safeCleanupAfterCommand();
717
719
 
718
- initialRun.attempt.combinedOutput = "";
719
- safeCleanupAfterCommand();
720
+ if (retryTimeout !== undefined && retryTimeout <= 0) {
721
+ onData(Buffer.from(retryResolution.retrySkippedMessage));
722
+ return { exitCode: processedAttempt.exitCode };
723
+ }
720
724
 
721
- if (retryTimeout !== undefined && retryTimeout <= 0) {
722
- onData(Buffer.from(retryResolution.retrySkippedMessage));
723
- return { exitCode: processedAttempt.exitCode };
724
- }
725
+ const retryRun = await prepareAndRunSandboxAttempt({
726
+ command,
727
+ cwd,
728
+ onData,
729
+ signal,
730
+ timeout: retryTimeout,
731
+ env,
732
+ });
725
733
 
726
- const retryRun = await prepareAndRunSandboxAttempt({
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
- onData,
730
- signal,
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
- let processedRetry: ProcessedSandboxAttempt;
736
- try {
737
- processedRetry = await processSandboxAttempt({
738
- attempt: retryRun.attempt,
739
- command,
740
- commandId: retryRun.commandId,
741
- cwd,
742
- runtimeConfig: retryRun.runtimeConfig,
743
- autoRetryAvailable: false,
744
- });
745
- } catch (postProcessError) {
746
- reportPostProcessingError(postProcessError);
747
- safeCleanupAfterCommand();
748
- return { exitCode: retryRun.attempt.exitCode };
749
- }
750
-
751
- let retryPostamble = processedRetry.postamble;
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
- if (retryPostamble) onData(Buffer.from(retryPostamble));
770
- safeCleanupAfterCommand();
771
- return { exitCode: processedRetry.exitCode };
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, showUnsandboxedApproval } from "./permissions/unsandboxed.js";
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.captureContext(ctx);
226
- return sandboxedBashTool.execute(id, params, signal, onUpdate, ctx);
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: SandboxAskCallback = async ({ host, port }) => {
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");