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.
Files changed (44) hide show
  1. package/extensions/answer.ts +115 -85
  2. package/extensions/branch-term/index.ts +9 -5
  3. package/extensions/btw.ts +70 -36
  4. package/extensions/insights.ts +70 -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 +7 -0
  16. package/extensions/sandbox/bash.ts +83 -85
  17. package/extensions/sandbox/command.ts +15 -1
  18. package/extensions/sandbox/config.ts +26 -15
  19. package/extensions/sandbox/index.ts +21 -11
  20. package/extensions/sandbox/permissions/network.ts +8 -3
  21. package/extensions/sandbox/permissions/unsandboxed.ts +1 -240
  22. package/extensions/sandbox/runtime.ts +54 -20
  23. package/extensions/subagent/README.md +58 -1
  24. package/extensions/subagent/config.ts +118 -0
  25. package/extensions/subagent/index.ts +54 -22
  26. package/extensions/telegram/README.md +2 -0
  27. package/extensions/telegram/assistant-result.d.mts +4 -1
  28. package/extensions/telegram/assistant-result.mjs +31 -16
  29. package/extensions/telegram/daemon.mjs +146 -194
  30. package/extensions/telegram/index.ts +11 -7
  31. package/extensions/tool-display-mode.ts +29 -50
  32. package/extensions/usage/README.md +5 -0
  33. package/extensions/usage/index.ts +341 -349
  34. package/extensions/websearch/README.md +8 -1
  35. package/extensions/websearch/config.ts +2 -1
  36. package/extensions/websearch/index.ts +18 -9
  37. package/extensions/websearch/providers/{openai-codex.pi.ts → openai.pi.ts} +51 -16
  38. package/extensions/websearch/providers/shared.ts +10 -3
  39. package/extensions/websearch/types.ts +2 -1
  40. package/extensions/worktree.ts +33 -89
  41. package/package.json +1 -1
  42. package/skills/oracle/SKILL.md +14 -8
  43. package/skills/oracle/scripts/oracle +43 -19
  44. 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,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 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
  }
@@ -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
- const changed = mutateStringList(values, op, targetPath);
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("~/") ? join(homedir(), value.slice(2)) : value;
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 expandPathConfigEntry(value: string, field: string): string | null {
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(values: string[] | undefined, field: string): string[] | undefined {
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) => expandPathConfigEntry(value, field))
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 = expandPathConfigEntry(mitmProxy.socketPath, "network.mitmProxy.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(network.allowUnixSockets, "network.allowUnixSockets"),
640
- mitmProxy: expandMitmProxyPathConfig(network.mitmProxy),
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, 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