@vybestack/llxprt-code 0.10.0 → 0.11.0-nightly.260728.ffaa8d5d7

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 (126) hide show
  1. package/package.json +13 -13
  2. package/src/commands/mcp/add.ts +8 -4
  3. package/src/config/cliArgParser.ts +2 -0
  4. package/src/config/config.ts +2 -0
  5. package/src/config/configBuilder.ts +3 -0
  6. package/src/config/configError.ts +23 -0
  7. package/src/config/extensions/extensionEnablement.ts +62 -1
  8. package/src/config/interactiveContext.ts +2 -1
  9. package/src/config/intermediateConfig.ts +4 -0
  10. package/src/config/keyBindings.ts +29 -3
  11. package/src/config/policy.ts +7 -1
  12. package/src/config/postConfigRuntime.ts +20 -14
  13. package/src/config/settings-schema/schema-ui.ts +9 -0
  14. package/src/config/settingsLoader.ts +21 -17
  15. package/src/config/settingsSchema.ts +2 -2
  16. package/src/config/trustedFolders.ts +275 -58
  17. package/src/config/yargsOptions.ts +7 -0
  18. package/src/generated/git-commit.json +1 -1
  19. package/src/nonInteractiveCli.ts +1 -0
  20. package/src/nonInteractiveCliSupport.ts +171 -51
  21. package/src/runtime/interactiveToolScheduler.ts +6 -6
  22. package/src/services/BuiltinCommandLoader.ts +33 -25
  23. package/src/services/FileCommandLoader.ts +34 -7
  24. package/src/test-utils/async.ts +20 -0
  25. package/src/test-utils/render.tsx +10 -1
  26. package/src/ui/App.tsx +17 -10
  27. package/src/ui/AppContainerRuntime.tsx +7 -9
  28. package/src/ui/cliUiRuntime.ts +28 -0
  29. package/src/ui/commands/authCommand.ts +240 -3
  30. package/src/ui/commands/compressCommand.ts +9 -1
  31. package/src/ui/commands/directoryCommand.tsx +1 -1
  32. package/src/ui/commands/logoutCommand.ts +2 -2
  33. package/src/ui/commands/permissionsCommand.ts +77 -20
  34. package/src/ui/commands/policiesCommand.ts +6 -1
  35. package/src/ui/commands/quotaCommand.ts +501 -0
  36. package/src/ui/commands/statsCommand.ts +2 -2
  37. package/src/ui/commands/statsQuota.ts +1 -1
  38. package/src/ui/commands/subagentCommand.ts +59 -27
  39. package/src/ui/components/AuthDialog.tsx +3 -3
  40. package/src/ui/components/Composer.tsx +1 -0
  41. package/src/ui/components/DialogManager.tsx +9 -2
  42. package/src/ui/components/FolderTrustDialog.tsx +71 -60
  43. package/src/ui/components/PermissionsModifyTrustDialog.tsx +211 -146
  44. package/src/ui/components/ProfileCreateWizard/constants.ts +13 -1
  45. package/src/ui/components/QueuedMessagesPanel.tsx +388 -0
  46. package/src/ui/components/inputPromptHooks.ts +2 -0
  47. package/src/ui/components/inputPromptKeyHandlers.ts +14 -0
  48. package/src/ui/components/inputPromptTypes.ts +6 -0
  49. package/src/ui/components/messages/AiMessage.tsx +24 -12
  50. package/src/ui/components/messages/ToolMessage.tsx +26 -40
  51. package/src/ui/containers/AppContainer/builders/buildUIActions.ts +3 -1
  52. package/src/ui/containers/AppContainer/builders/buildUIState.ts +5 -4
  53. package/src/ui/containers/AppContainer/hooks/useAppBootstrap.ts +3 -33
  54. package/src/ui/containers/AppContainer/hooks/useAppDialogs.ts +2 -14
  55. package/src/ui/containers/AppContainer/hooks/useAppInput.ts +31 -25
  56. package/src/ui/containers/AppContainer/hooks/useAppLayout.ts +90 -60
  57. package/src/ui/containers/AppContainer/hooks/useDisplayPreferences.ts +10 -0
  58. package/src/ui/containers/AppContainer/hooks/useInputHandling.ts +3 -17
  59. package/src/ui/containers/AppContainer/hooks/useKeybindings.ts +51 -38
  60. package/src/ui/containers/AppContainer/hooks/useSteer.ts +51 -0
  61. package/src/ui/contexts/KeypressContext.tsx +13 -2
  62. package/src/ui/contexts/ShellCommandDisplayContext.tsx +103 -0
  63. package/src/ui/contexts/TodoContext.tsx +0 -13
  64. package/src/ui/contexts/TodoProvider.tsx +1 -34
  65. package/src/ui/contexts/UIActionsContext.tsx +2 -1
  66. package/src/ui/contexts/UIStateContext.tsx +3 -2
  67. package/src/ui/hooks/agentStream/streamUtils.ts +0 -24
  68. package/src/ui/hooks/agentStream/thoughtState.ts +110 -30
  69. package/src/ui/hooks/agentStream/types.ts +1 -0
  70. package/src/ui/hooks/agentStream/useAgentEventStream.ts +8 -17
  71. package/src/ui/hooks/agentStream/useAgentStream.ts +2 -2
  72. package/src/ui/hooks/agentStream/useAgentStreamLifecycle.ts +4 -5
  73. package/src/ui/hooks/agentStream/useAgentStreamOrchestration.ts +12 -5
  74. package/src/ui/hooks/agentStream/useQueuedSubmissions.ts +99 -0
  75. package/src/ui/hooks/agentStream/useStreamEventHandlers.ts +4 -13
  76. package/src/ui/hooks/agentStream/useStreamState.ts +60 -41
  77. package/src/ui/hooks/agentStream/useSubmitQuery.ts +284 -59
  78. package/src/ui/hooks/slashCommandProcessorSupport.ts +4 -0
  79. package/src/ui/hooks/useAgentStream-test-helpers.ts +1 -0
  80. package/src/ui/hooks/useAutoAcceptIndicator.ts +13 -0
  81. package/src/ui/hooks/useFolderTrust.ts +118 -81
  82. package/src/ui/hooks/useGitBranchName.ts +79 -33
  83. package/src/ui/hooks/useIdeTrustListener.ts +5 -19
  84. package/src/ui/hooks/usePermissionsModifyTrust.ts +223 -85
  85. package/src/ui/hooks/useReactToolScheduler.ts +13 -17
  86. package/src/ui/hooks/useTodoPausePreserver.ts +0 -55
  87. package/src/ui/layouts/DefaultAppLayout.tsx +2 -0
  88. package/src/ui/layouts/DefaultAppLayoutHelpers.tsx +37 -162
  89. package/src/ui/layouts/InlineContent.tsx +198 -0
  90. package/src/ui/trustDialogHelpers.ts +190 -0
  91. package/src/utils/cleanup-state.ts +115 -0
  92. package/src/utils/cleanup.ts +32 -34
  93. package/src/utils/sandbox-containers.ts +183 -53
  94. package/src/utils/sandbox-exec.ts +9 -3
  95. package/src/zed-integration/acp-terminal-shell-host.ts +112 -0
  96. package/src/zed-integration/acp-types.ts +35 -0
  97. package/src/zed-integration/fileSystemService.ts +1 -1
  98. package/src/zed-integration/runZedIntegration.ts +142 -0
  99. package/src/zed-integration/zed-agent-event-handler.ts +179 -0
  100. package/src/zed-integration/zed-agent-setup.ts +43 -0
  101. package/src/zed-integration/zed-command-registry.ts +167 -0
  102. package/src/zed-integration/zed-config-options.ts +280 -0
  103. package/src/zed-integration/zed-content-utils.ts +2 -2
  104. package/src/zed-integration/zed-helpers.ts +132 -0
  105. package/src/zed-integration/zed-initialize.ts +64 -0
  106. package/src/zed-integration/zed-plan-update.ts +20 -0
  107. package/src/zed-integration/zed-prompt-command.ts +77 -0
  108. package/src/zed-integration/zed-session-config.ts +119 -0
  109. package/src/zed-integration/zed-session-errors.ts +337 -0
  110. package/src/zed-integration/zed-session-events.ts +216 -0
  111. package/src/zed-integration/zed-session-info.ts +255 -0
  112. package/src/zed-integration/zed-session-lifecycle.ts +186 -0
  113. package/src/zed-integration/zed-session-listing.ts +153 -0
  114. package/src/zed-integration/zed-session-loader.ts +193 -0
  115. package/src/zed-integration/zed-session-pagination.ts +240 -0
  116. package/src/zed-integration/zed-session-replay.ts +564 -0
  117. package/src/zed-integration/zed-stream-batcher.ts +236 -0
  118. package/src/zed-integration/zed-terminal-manager.ts +416 -0
  119. package/src/zed-integration/zed-terminal-setup.ts +72 -0
  120. package/src/zed-integration/zed-test-helpers.ts +349 -4
  121. package/src/zed-integration/zed-tool-handler.ts +47 -5
  122. package/src/zed-integration/zedIntegration.ts +538 -587
  123. package/src/services/todo-continuation/todoContinuationService.ts +0 -610
  124. package/src/ui/containers/AppContainer/hooks/useIdeRestartHotkey.ts +0 -33
  125. package/src/ui/containers/AppContainer/hooks/useTodoContinuationFlow.ts +0 -160
  126. package/src/ui/hooks/useTodoContinuation.ts +0 -299
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Vybestack LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ /**
8
+ * Private, lightweight cleanup-state owner.
9
+ *
10
+ * Owns the cleanup registration queues and the reentrancy/concurrency guard.
11
+ * This module has NO `@vybestack/llxprt-code-core`, React, ink, or
12
+ * `cleanup.ts` imports, so importing it never transitively evaluates the
13
+ * core barrel. The pure-node test base setup imports only the reset
14
+ * operation from here, eliminating the core-barrel cost.
15
+ *
16
+ * State is managed via immutable reassignment: registration appends to a
17
+ * new array assigned back to the module-level binding; draining clears by
18
+ * reassigning to an empty array. No mutable arrays are exposed to callers.
19
+ *
20
+ * Draining semantics: callbacks appended during an active drain are picked
21
+ * up by continuing to drain until the queue is empty. This preserves the
22
+ * observable behavior of the original live-array `for...of` iteration.
23
+ */
24
+
25
+ type CleanupFn = (() => void) | (() => Promise<void>);
26
+ type SyncCleanupFn = () => void;
27
+
28
+ interface CleanupState {
29
+ readonly asyncFns: readonly CleanupFn[];
30
+ readonly syncFns: readonly SyncCleanupFn[];
31
+ readonly inProgress: boolean;
32
+ }
33
+
34
+ const EMPTY_ASYNC_FNS: readonly CleanupFn[] = [];
35
+ const EMPTY_SYNC_FNS: readonly SyncCleanupFn[] = [];
36
+
37
+ const EMPTY_STATE: CleanupState = {
38
+ asyncFns: EMPTY_ASYNC_FNS,
39
+ syncFns: EMPTY_SYNC_FNS,
40
+ inProgress: false,
41
+ };
42
+
43
+ let state: CleanupState = EMPTY_STATE;
44
+
45
+ export function registerCleanupFn(fn: CleanupFn): void {
46
+ state = { ...state, asyncFns: [...state.asyncFns, fn] };
47
+ }
48
+
49
+ export function registerSyncCleanupFn(fn: SyncCleanupFn): void {
50
+ state = { ...state, syncFns: [...state.syncFns, fn] };
51
+ }
52
+
53
+ export function isCleanupInProgress(): boolean {
54
+ return state.inProgress;
55
+ }
56
+
57
+ /** Mark cleanup as started, preventing reentrant/concurrent runs. */
58
+ export function beginCleanup(): void {
59
+ state = { ...state, inProgress: true };
60
+ }
61
+
62
+ /**
63
+ * Drain all pending synchronous callbacks in registration order until the
64
+ * queue is empty (callbacks appended during draining are included).
65
+ * Returns the drained callbacks. Each callback's errors are tolerated
66
+ * exactly as before — the caller passes an error handler.
67
+ */
68
+ export function drainSyncCleanups(
69
+ onError: (fn: SyncCleanupFn, error: unknown) => void,
70
+ ): void {
71
+ while (state.syncFns.length > 0) {
72
+ // Snapshot the current batch, then clear the queue so callbacks appended
73
+ // during this batch form a new batch for the next loop iteration.
74
+ const batch = state.syncFns;
75
+ state = { ...state, syncFns: [] };
76
+ for (const fn of batch) {
77
+ try {
78
+ fn();
79
+ } catch (error) {
80
+ onError(fn, error);
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Drain all pending asynchronous callbacks in registration order until the
88
+ * queue is empty (callbacks appended during draining are included).
89
+ */
90
+ export async function drainAsyncCleanups(
91
+ onError: (fn: CleanupFn, error: unknown) => void,
92
+ ): Promise<void> {
93
+ while (state.asyncFns.length > 0) {
94
+ const batch = state.asyncFns;
95
+ state = { ...state, asyncFns: [] };
96
+ for (const fn of batch) {
97
+ try {
98
+ await fn();
99
+ } catch (error) {
100
+ onError(fn, error);
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Reset cleanup state for testing purposes only.
108
+ * Clears pending sync/async callbacks and returns the in-progress guard
109
+ * to idle.
110
+ * DO NOT use this in production code.
111
+ * @internal
112
+ */
113
+ export function __resetCleanupStateForTesting(): void {
114
+ state = EMPTY_STATE;
115
+ }
@@ -9,27 +9,42 @@ import { join } from 'path';
9
9
  import { ShellExecutionService } from '@vybestack/llxprt-code-core';
10
10
  import { FileOutput } from '@vybestack/llxprt-code-telemetry';
11
11
  import { Storage } from '@vybestack/llxprt-code-settings';
12
+ import {
13
+ beginCleanup,
14
+ drainAsyncCleanups,
15
+ drainSyncCleanups,
16
+ isCleanupInProgress,
17
+ registerCleanupFn,
18
+ registerSyncCleanupFn,
19
+ } from './cleanup-state.js';
12
20
 
13
21
  type FileOutputWithOptionalDisposeInstance = typeof FileOutput & {
14
22
  disposeInstance?: unknown;
15
23
  };
16
24
 
17
- const cleanupFunctions: Array<(() => void) | (() => Promise<void>)> = [];
18
- const syncCleanupFunctions: Array<() => void> = [];
19
- let cleanupInProgress = false;
20
-
21
25
  export function registerCleanup(fn: (() => void) | (() => Promise<void>)) {
22
- cleanupFunctions.push(fn);
26
+ registerCleanupFn(fn);
23
27
  }
24
28
 
25
29
  export function registerSyncCleanup(fn: () => void) {
26
- syncCleanupFunctions.push(fn);
30
+ registerSyncCleanupFn(fn);
31
+ }
32
+
33
+ export function runBestEffortSyncCleanup(
34
+ cleanup: (() => void) | undefined,
35
+ onError?: (error: unknown) => void,
36
+ ) {
37
+ try {
38
+ cleanup?.();
39
+ } catch (error) {
40
+ onError?.(error);
41
+ }
27
42
  }
28
43
 
29
44
  export async function runExitCleanup() {
30
45
  // Guard against concurrent cleanup if signal handlers fire multiple times
31
- if (cleanupInProgress) return;
32
- cleanupInProgress = true;
46
+ if (isCleanupInProgress()) return;
47
+ beginCleanup();
33
48
 
34
49
  // Tear down any active PTYs first to release FDs/sockets promptly
35
50
  try {
@@ -39,23 +54,13 @@ export async function runExitCleanup() {
39
54
  }
40
55
 
41
56
  // Run sync cleanups first (e.g., stdio restoration)
42
- for (const fn of syncCleanupFunctions) {
43
- try {
44
- fn();
45
- } catch {
46
- // Ignore errors during cleanup.
47
- }
48
- }
49
- syncCleanupFunctions.length = 0;
57
+ drainSyncCleanups(() => {
58
+ // Ignore errors during cleanup.
59
+ });
50
60
 
51
- for (const fn of cleanupFunctions) {
52
- try {
53
- await fn();
54
- } catch {
55
- // Ignore errors during cleanup.
56
- }
57
- }
58
- cleanupFunctions.length = 0; // Clear the array
61
+ await drainAsyncCleanups(() => {
62
+ // Ignore errors during cleanup.
63
+ });
59
64
 
60
65
  try {
61
66
  const disposeInstance = (
@@ -72,16 +77,9 @@ export async function runExitCleanup() {
72
77
  }
73
78
  }
74
79
 
75
- /**
76
- * Reset cleanup state for testing purposes only.
77
- * DO NOT use this in production code.
78
- * @internal
79
- */
80
- export function __resetCleanupStateForTesting() {
81
- cleanupFunctions.length = 0;
82
- syncCleanupFunctions.length = 0;
83
- cleanupInProgress = false;
84
- }
80
+ // Re-export the test reset from the state module that owns the state,
81
+ // preserving backward compatibility for existing consumers.
82
+ export { __resetCleanupStateForTesting } from './cleanup-state.js';
85
83
 
86
84
  export async function cleanupCheckpoints() {
87
85
  const storage = new Storage(process.cwd());
@@ -8,6 +8,7 @@ import { execSync, spawn, exec, type ChildProcess } from 'node:child_process';
8
8
  import os from 'node:os';
9
9
  import path from 'node:path';
10
10
  import fs from 'node:fs';
11
+ import crypto from 'node:crypto';
11
12
  import { promisify } from 'node:util';
12
13
  import { parse } from 'shell-quote';
13
14
  import type { Config, SandboxConfig } from '@vybestack/llxprt-code-core';
@@ -18,6 +19,7 @@ import type {
18
19
  CredentialProxyBridgeResult,
19
20
  SshAgentResult,
20
21
  } from './sandbox-ssh.js';
22
+ import { runBestEffortSyncCleanup } from './cleanup.js';
21
23
  import {
22
24
  USER_SETTINGS_DIR,
23
25
  SETTINGS_DIRECTORY_NAME,
@@ -42,6 +44,7 @@ import {
42
44
  createAndStartProxy,
43
45
  stopProxy,
44
46
  getProxySocketPath,
47
+ getProxyCapabilityToken,
45
48
  } from '@vybestack/llxprt-code-providers/auth.js';
46
49
 
47
50
  const execAsync = promisify(exec);
@@ -72,6 +75,21 @@ const SANDBOX_PROXY_NAME = 'llxprt-code-sandbox-proxy';
72
75
 
73
76
  export { LOCAL_DEV_SANDBOX_IMAGE_NAME };
74
77
 
78
+ /** Composes two optional cleanup callbacks into one, calling both safely. */
79
+ function composeCleanups(
80
+ a: (() => void) | undefined,
81
+ b: (() => void) | undefined,
82
+ ): (() => void) | undefined {
83
+ if (a === undefined && b === undefined) return undefined;
84
+ const reportCleanupError = (error: unknown): void => {
85
+ debugLogger.error('cleanup step failed:', error);
86
+ };
87
+ return () => {
88
+ runBestEffortSyncCleanup(a, reportCleanupError);
89
+ runBestEffortSyncCleanup(b, reportCleanupError);
90
+ };
91
+ }
92
+
75
93
  /** Rewrites only the loopback hostname of a proxy URL to the sandbox proxy name,
76
94
  * preserving credentials, port, and path. Falls back to the original URL on parse failure. */
77
95
  function rewriteProxyHostname(proxyUrl: string): string {
@@ -422,6 +440,97 @@ export async function setupContainerUser(
422
440
  return userFlag;
423
441
  }
424
442
 
443
+ /**
444
+ * Writes the capability token to a temp env file and pushes --env-file to args.
445
+ * Uses --env-file instead of --env to avoid exposing the token in the process
446
+ * argument list (visible via ps aux / docker inspect).
447
+ *
448
+ * Returns a cleanup function that removes the temp file. The caller should
449
+ * register it on process exit / sandbox close to avoid leaving the token
450
+ * file on disk.
451
+ */
452
+ function pushCapabilityEnvFile(
453
+ args: string[],
454
+ resolvedTmpdir: string,
455
+ ): (() => void) | undefined {
456
+ const capabilityToken = getProxyCapabilityToken();
457
+ if (capabilityToken === undefined) return undefined;
458
+ if (/[\r\n=]/.test(capabilityToken)) {
459
+ throw new Error(
460
+ 'Capability token contains invalid characters for env file',
461
+ );
462
+ }
463
+ const envFile = path.join(
464
+ resolvedTmpdir,
465
+ `.capability-env-${process.pid}-${crypto.randomUUID()}`,
466
+ );
467
+ // Use openSync with explicit mode for defense-in-depth: writeFileSync applies
468
+ // the mode option but is subject to umask masking. openSync + fchmodSync
469
+ // guarantees the restrictive permissions regardless of umask.
470
+ const fd = fs.openSync(envFile, 'w', 0o600);
471
+ try {
472
+ fs.writeSync(fd, `LLXPRT_CAPABILITY_TOKEN=${capabilityToken}\n`);
473
+ fs.fchmodSync(fd, 0o600);
474
+ } catch (writeErr) {
475
+ try {
476
+ fs.closeSync(fd);
477
+ } catch {
478
+ // fd may already be closed
479
+ }
480
+ try {
481
+ fs.unlinkSync(envFile);
482
+ } catch {
483
+ // file may not exist or already removed
484
+ }
485
+ throw writeErr;
486
+ }
487
+ // closeSync may fail if the fd is already closed or invalid; the file was
488
+ // written successfully, so swallow the error and still return the cleanup
489
+ // closure to ensure the temp env file (containing the capability token)
490
+ // is eventually removed.
491
+ try {
492
+ fs.closeSync(fd);
493
+ } catch {
494
+ // fd may already be closed; file content is still valid
495
+ }
496
+ args.push('--env-file', envFile);
497
+ return () => {
498
+ try {
499
+ fs.unlinkSync(envFile);
500
+ } catch {
501
+ // file may already be removed
502
+ }
503
+ };
504
+ }
505
+
506
+ /** Sets up the macOS credential proxy bridge based on container command. */
507
+ async function setupMacOSCredProxyBridge(
508
+ args: string[],
509
+ config: SandboxConfig,
510
+ socketPath: string,
511
+ reservedTunnelPorts: Set<number>,
512
+ ): Promise<CredentialProxyBridgeResult | undefined> {
513
+ switch (config.command) {
514
+ case 'podman':
515
+ return setupCredentialProxyPodmanMacOS(
516
+ args,
517
+ socketPath,
518
+ SSH_TUNNEL_POLL_TIMEOUT_MS,
519
+ {
520
+ reserveTunnelPort: (port: number) => {
521
+ reservedTunnelPorts.add(port);
522
+ },
523
+ excludedTunnelPorts: reservedTunnelPorts,
524
+ },
525
+ );
526
+ case 'docker':
527
+ return setupCredentialProxyDockerMacOS(args, socketPath);
528
+ case 'sandbox-exec':
529
+ default:
530
+ return undefined;
531
+ }
532
+ }
533
+
425
534
  /** Starts credential proxy and sets up bridge for Podman/Docker macOS. */
426
535
  export async function setupCredentialProxy(
427
536
  args: string[],
@@ -435,69 +544,70 @@ export async function setupCredentialProxy(
435
544
  }> {
436
545
  let credentialProxyBridgeResult: CredentialProxyBridgeResult | undefined;
437
546
  let credentialProxyBridgeCleanup: (() => void) | undefined;
547
+ let envFileCleanup: (() => void) | undefined;
438
548
 
439
549
  // @plan:PLAN-20250214-CREDPROXY.P34 R25.1: Start credential proxy BEFORE spawning container
440
- // The proxy must be listening before the container starts so it can connect immediately
441
- let credentialProxyHandle: { stop: () => Promise<void> } | undefined;
442
550
  try {
443
- credentialProxyHandle = await createAndStartProxy({
444
- socketPath: resolvedTmpdir,
445
- });
446
- void credentialProxyHandle;
551
+ await createAndStartProxy({ socketPath: resolvedTmpdir });
447
552
  } catch (err) {
448
- // @plan:PLAN-20250214-CREDPROXY.P34 R25.1a: Proxy creation failure aborts before spawning container
449
553
  throw new FatalSandboxError(
450
554
  `Failed to start credential proxy: ${err instanceof Error ? err.message : String(err)}`,
451
555
  );
452
556
  }
453
557
  const socketPath = getProxySocketPath();
454
558
  if (socketPath === undefined) {
559
+ try {
560
+ await stopProxy();
561
+ } catch {
562
+ // best-effort cleanup
563
+ }
455
564
  return { credentialProxyBridgeResult, credentialProxyBridgeCleanup };
456
565
  }
457
566
 
458
567
  // @plan:PLAN-20250214-CREDPROXY.P34 R3.6: Pass socket path to container via env var
459
- if (os.platform() === 'darwin') {
460
- const sandboxCommand: string = config.command;
461
- switch (sandboxCommand) {
462
- case 'podman':
463
- credentialProxyBridgeResult = await setupCredentialProxyPodmanMacOS(
464
- args,
465
- socketPath,
466
- SSH_TUNNEL_POLL_TIMEOUT_MS,
467
- {
468
- reserveTunnelPort: (port: number) => {
469
- reservedTunnelPorts.add(port);
470
- },
471
- excludedTunnelPorts: reservedTunnelPorts,
472
- },
473
- );
474
- break;
475
- case 'docker':
476
- credentialProxyBridgeResult = await setupCredentialProxyDockerMacOS(
477
- args,
478
- socketPath,
479
- );
480
- break;
481
- default:
482
- args.push('--env', `LLXPRT_CREDENTIAL_SOCKET=${socketPath}`);
483
- break;
568
+ const isDarwin = os.platform() === 'darwin';
569
+ try {
570
+ if (isDarwin) {
571
+ credentialProxyBridgeResult = await setupMacOSCredProxyBridge(
572
+ args,
573
+ config,
574
+ socketPath,
575
+ reservedTunnelPorts,
576
+ );
484
577
  }
485
578
 
486
- if (credentialProxyBridgeResult !== undefined) {
579
+ const effectiveSocketPath =
580
+ credentialProxyBridgeResult?.containerSocketPath ?? socketPath;
581
+ args.push('--env', `LLXPRT_CREDENTIAL_SOCKET=${effectiveSocketPath}`);
582
+
583
+ if (credentialProxyBridgeResult !== undefined && isDarwin) {
487
584
  credentialProxyBridgeCleanup = credentialProxyBridgeResult.cleanup;
488
- if (credentialProxyBridgeResult.entrypointPrefix !== undefined) {
489
- entrypointPrefixes.push(credentialProxyBridgeResult.entrypointPrefix);
585
+ const prefix = credentialProxyBridgeResult.entrypointPrefix;
586
+ if (prefix !== undefined) {
587
+ entrypointPrefixes.push(prefix);
490
588
  }
491
- args.push(
492
- '--env',
493
- `LLXPRT_CREDENTIAL_SOCKET=${credentialProxyBridgeResult.containerSocketPath}`,
494
- );
495
589
  }
496
- } else {
497
- args.push('--env', `LLXPRT_CREDENTIAL_SOCKET=${socketPath}`);
590
+ envFileCleanup = pushCapabilityEnvFile(args, resolvedTmpdir);
591
+ } catch (err) {
592
+ envFileCleanup?.();
593
+ credentialProxyBridgeCleanup?.();
594
+ try {
595
+ await stopProxy();
596
+ } catch {
597
+ // best-effort cleanup; the original error is the one we want to throw
598
+ }
599
+ throw err;
498
600
  }
499
601
 
500
- return { credentialProxyBridgeResult, credentialProxyBridgeCleanup };
602
+ // Compose env file cleanup with any existing bridge cleanup so the temp
603
+ // file is removed when the sandbox container closes (not just on process exit).
604
+ return {
605
+ credentialProxyBridgeResult,
606
+ credentialProxyBridgeCleanup: composeCleanups(
607
+ credentialProxyBridgeCleanup,
608
+ envFileCleanup,
609
+ ),
610
+ };
501
611
  }
502
612
 
503
613
  /** Spawns proxy container and waits for it to be ready. */
@@ -589,7 +699,7 @@ export function wireCleanupHandlers(
589
699
  _cliConfig: Config | undefined,
590
700
  sshResult: SshAgentResult,
591
701
  portForwardingResult: PortForwardingResult | undefined,
592
- credentialProxyBridgeResult: CredentialProxyBridgeResult | undefined,
702
+ credentialProxyBridgeCleanup: (() => void) | undefined,
593
703
  setCredentialProxyBridgeCleanup: (c: (() => void) | undefined) => void,
594
704
  ): void {
595
705
  sandboxProcess.on('error', (err) => {
@@ -608,20 +718,40 @@ export function wireCleanupHandlers(
608
718
  sandboxProcess.on('close', portForwardingResult.cleanup);
609
719
  }
610
720
 
611
- if (credentialProxyBridgeResult?.cleanup !== undefined) {
612
- const stopCredentialBridgeTunnel = credentialProxyBridgeResult.cleanup;
613
- process.on('exit', stopCredentialBridgeTunnel);
614
- process.on('SIGINT', stopCredentialBridgeTunnel);
615
- process.on('SIGTERM', stopCredentialBridgeTunnel);
616
- sandboxProcess.on('close', () => {
721
+ if (credentialProxyBridgeCleanup !== undefined) {
722
+ // Register the composed cleanup (bridge tunnel + env file) on all
723
+ // process-level events as well as the sandbox close event. This ensures
724
+ // tunnels are torn down even on abnormal exit where the sandbox close
725
+ // event may not fire.
726
+ let bridgeCleanedUp = false;
727
+ const runBridgeCleanup = (): void => {
728
+ if (bridgeCleanedUp) return;
729
+ bridgeCleanedUp = true;
730
+ credentialProxyBridgeCleanup();
731
+ process.off('exit', runBridgeCleanup);
732
+ process.off('SIGINT', runBridgeCleanup);
733
+ process.off('SIGTERM', runBridgeCleanup);
734
+ sandboxProcess.off('close', runBridgeCleanup);
617
735
  setCredentialProxyBridgeCleanup(undefined);
618
- stopCredentialBridgeTunnel();
619
- });
736
+ };
737
+ process.on('exit', runBridgeCleanup);
738
+ process.on('SIGINT', runBridgeCleanup);
739
+ process.on('SIGTERM', runBridgeCleanup);
740
+ sandboxProcess.on('close', runBridgeCleanup);
620
741
  }
621
742
 
622
743
  // @plan:PLAN-20250214-CREDPROXY.P34 R25.2, R25.3: Clean up credential proxy on sandbox exit
623
- const stopCredentialProxy = () => {
624
- void stopProxy();
744
+ let proxyStopped = false;
745
+ const stopCredentialProxy = (): void => {
746
+ if (proxyStopped) return;
747
+ proxyStopped = true;
748
+ process.off('exit', stopCredentialProxy);
749
+ process.off('SIGINT', stopCredentialProxy);
750
+ process.off('SIGTERM', stopCredentialProxy);
751
+ sandboxProcess.off('close', stopCredentialProxy);
752
+ void stopProxy().catch(() => {
753
+ // best-effort cleanup during shutdown
754
+ });
625
755
  };
626
756
  process.on('exit', stopCredentialProxy);
627
757
  process.on('SIGINT', stopCredentialProxy);
@@ -21,6 +21,7 @@ import type {
21
21
  SshAgentResult,
22
22
  } from './sandbox-ssh.js';
23
23
  import type { ContainerSandboxPrepared } from './sandbox-containers.js';
24
+ import { runBestEffortSyncCleanup } from './cleanup.js';
24
25
  import {
25
26
  buildContainerRunArgs,
26
27
  addContainerVolumeMounts,
@@ -36,6 +37,7 @@ import {
36
37
  restoreStdinAfterSandbox,
37
38
  LOCAL_DEV_SANDBOX_IMAGE_NAME,
38
39
  } from './sandbox-containers.js';
40
+ import { stopProxy } from '@vybestack/llxprt-code-providers/auth.js';
39
41
  import { entrypoint } from './sandbox-entrypoint.js';
40
42
  import { ensureSandboxImageIsPresent } from './sandbox-image.js';
41
43
  import {
@@ -197,7 +199,12 @@ async function prepareContainerSandbox(
197
199
  credentialProxyBridgeResult = cpResult.credentialProxyBridgeResult;
198
200
  credentialProxyBridgeCleanup = cpResult.credentialProxyBridgeCleanup;
199
201
  } catch (err) {
200
- credentialProxyBridgeResult?.cleanup?.();
202
+ runBestEffortSyncCleanup(credentialProxyBridgeResult?.cleanup);
203
+ try {
204
+ await stopProxy();
205
+ } catch {
206
+ // best-effort cleanup; the original error is the one we want to throw
207
+ }
201
208
  if (err instanceof FatalSandboxError) {
202
209
  throw err;
203
210
  }
@@ -242,7 +249,6 @@ async function executeContainerSandbox(
242
249
  image,
243
250
  workdir,
244
251
  portForwardingResult,
245
- credentialProxyBridgeResult,
246
252
  sshResult,
247
253
  } = prepared;
248
254
  let credentialProxyBridgeCleanup = prepared.credentialProxyBridgeCleanup;
@@ -279,7 +285,7 @@ async function executeContainerSandbox(
279
285
  cliConfig,
280
286
  sshResult,
281
287
  portForwardingResult,
282
- credentialProxyBridgeResult,
288
+ credentialProxyBridgeCleanup,
283
289
  (c) => {
284
290
  credentialProxyBridgeCleanup = c;
285
291
  },
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Vybestack LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import type {
8
+ IShellToolHost,
9
+ ShellExecutionResult,
10
+ ShellOutputEvent,
11
+ } from '@vybestack/llxprt-code-tools';
12
+ import type { TerminalManager } from './zed-terminal-manager.js';
13
+
14
+ export class AcpTerminalShellHost implements IShellToolHost {
15
+ constructor(
16
+ private readonly delegate: IShellToolHost,
17
+ private readonly terminals: TerminalManager,
18
+ ) {}
19
+
20
+ getTargetDir(): string {
21
+ return this.delegate.getTargetDir();
22
+ }
23
+
24
+ getWorkspaceContext() {
25
+ return this.delegate.getWorkspaceContext();
26
+ }
27
+
28
+ isCommandAllowed(command: string) {
29
+ return this.delegate.isCommandAllowed(command);
30
+ }
31
+
32
+ isShellInvocationAllowlisted(command: string, toolName: string): boolean {
33
+ return this.delegate.isShellInvocationAllowlisted(command, toolName);
34
+ }
35
+
36
+ isInteractive(): boolean {
37
+ return this.delegate.isInteractive();
38
+ }
39
+
40
+ isYoloMode(): boolean {
41
+ return this.delegate.isYoloMode();
42
+ }
43
+
44
+ getDebugMode(): boolean {
45
+ return this.delegate.getDebugMode();
46
+ }
47
+
48
+ getShellExecutionConfig() {
49
+ return this.delegate.getShellExecutionConfig();
50
+ }
51
+
52
+ getTimeoutConfig() {
53
+ return this.delegate.getTimeoutConfig();
54
+ }
55
+
56
+ getOutputLimits() {
57
+ return this.delegate.getOutputLimits();
58
+ }
59
+
60
+ executeShellCommand(
61
+ command: string,
62
+ cwd: string,
63
+ onOutput: (event: ShellOutputEvent) => void,
64
+ signal: AbortSignal,
65
+ ): Promise<ShellExecutionResult> {
66
+ return this.terminals.executeShellCommand(command, cwd, onOutput, signal);
67
+ }
68
+
69
+ getCommandRoots(command: string): string[] {
70
+ return this.delegate.getCommandRoots(command);
71
+ }
72
+
73
+ stripShellWrapper(command: string): string {
74
+ return this.delegate.stripShellWrapper(command);
75
+ }
76
+
77
+ validatePathWithinWorkspace(
78
+ workspaceContext: ReturnType<IShellToolHost['getWorkspaceContext']>,
79
+ dirPath: string,
80
+ label: string,
81
+ ): string | null {
82
+ return this.delegate.validatePathWithinWorkspace(
83
+ workspaceContext,
84
+ dirPath,
85
+ label,
86
+ );
87
+ }
88
+
89
+ isPtyActive(pid: number): boolean {
90
+ return this.delegate.isPtyActive(pid);
91
+ }
92
+
93
+ formatMemoryUsage(bytes: number): string {
94
+ return this.delegate.formatMemoryUsage(bytes);
95
+ }
96
+
97
+ trySummarizeOutput(
98
+ content: string,
99
+ signal: AbortSignal,
100
+ tokenBudget?: number,
101
+ ): Promise<string> {
102
+ return this.delegate.trySummarizeOutput(content, signal, tokenBudget);
103
+ }
104
+
105
+ getSummarizeConfig() {
106
+ return this.delegate.getSummarizeConfig();
107
+ }
108
+
109
+ limitOutputTokens(content: string) {
110
+ return this.delegate.limitOutputTokens(content);
111
+ }
112
+ }