@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.
- package/package.json +13 -13
- package/src/commands/mcp/add.ts +8 -4
- package/src/config/cliArgParser.ts +2 -0
- package/src/config/config.ts +2 -0
- package/src/config/configBuilder.ts +3 -0
- package/src/config/configError.ts +23 -0
- package/src/config/extensions/extensionEnablement.ts +62 -1
- package/src/config/interactiveContext.ts +2 -1
- package/src/config/intermediateConfig.ts +4 -0
- package/src/config/keyBindings.ts +29 -3
- package/src/config/policy.ts +7 -1
- package/src/config/postConfigRuntime.ts +20 -14
- package/src/config/settings-schema/schema-ui.ts +9 -0
- package/src/config/settingsLoader.ts +21 -17
- package/src/config/settingsSchema.ts +2 -2
- package/src/config/trustedFolders.ts +275 -58
- package/src/config/yargsOptions.ts +7 -0
- package/src/generated/git-commit.json +1 -1
- package/src/nonInteractiveCli.ts +1 -0
- package/src/nonInteractiveCliSupport.ts +171 -51
- package/src/runtime/interactiveToolScheduler.ts +6 -6
- package/src/services/BuiltinCommandLoader.ts +33 -25
- package/src/services/FileCommandLoader.ts +34 -7
- package/src/test-utils/async.ts +20 -0
- package/src/test-utils/render.tsx +10 -1
- package/src/ui/App.tsx +17 -10
- package/src/ui/AppContainerRuntime.tsx +7 -9
- package/src/ui/cliUiRuntime.ts +28 -0
- package/src/ui/commands/authCommand.ts +240 -3
- package/src/ui/commands/compressCommand.ts +9 -1
- package/src/ui/commands/directoryCommand.tsx +1 -1
- package/src/ui/commands/logoutCommand.ts +2 -2
- package/src/ui/commands/permissionsCommand.ts +77 -20
- package/src/ui/commands/policiesCommand.ts +6 -1
- package/src/ui/commands/quotaCommand.ts +501 -0
- package/src/ui/commands/statsCommand.ts +2 -2
- package/src/ui/commands/statsQuota.ts +1 -1
- package/src/ui/commands/subagentCommand.ts +59 -27
- package/src/ui/components/AuthDialog.tsx +3 -3
- package/src/ui/components/Composer.tsx +1 -0
- package/src/ui/components/DialogManager.tsx +9 -2
- package/src/ui/components/FolderTrustDialog.tsx +71 -60
- package/src/ui/components/PermissionsModifyTrustDialog.tsx +211 -146
- package/src/ui/components/ProfileCreateWizard/constants.ts +13 -1
- package/src/ui/components/QueuedMessagesPanel.tsx +388 -0
- package/src/ui/components/inputPromptHooks.ts +2 -0
- package/src/ui/components/inputPromptKeyHandlers.ts +14 -0
- package/src/ui/components/inputPromptTypes.ts +6 -0
- package/src/ui/components/messages/AiMessage.tsx +24 -12
- package/src/ui/components/messages/ToolMessage.tsx +26 -40
- package/src/ui/containers/AppContainer/builders/buildUIActions.ts +3 -1
- package/src/ui/containers/AppContainer/builders/buildUIState.ts +5 -4
- package/src/ui/containers/AppContainer/hooks/useAppBootstrap.ts +3 -33
- package/src/ui/containers/AppContainer/hooks/useAppDialogs.ts +2 -14
- package/src/ui/containers/AppContainer/hooks/useAppInput.ts +31 -25
- package/src/ui/containers/AppContainer/hooks/useAppLayout.ts +90 -60
- package/src/ui/containers/AppContainer/hooks/useDisplayPreferences.ts +10 -0
- package/src/ui/containers/AppContainer/hooks/useInputHandling.ts +3 -17
- package/src/ui/containers/AppContainer/hooks/useKeybindings.ts +51 -38
- package/src/ui/containers/AppContainer/hooks/useSteer.ts +51 -0
- package/src/ui/contexts/KeypressContext.tsx +13 -2
- package/src/ui/contexts/ShellCommandDisplayContext.tsx +103 -0
- package/src/ui/contexts/TodoContext.tsx +0 -13
- package/src/ui/contexts/TodoProvider.tsx +1 -34
- package/src/ui/contexts/UIActionsContext.tsx +2 -1
- package/src/ui/contexts/UIStateContext.tsx +3 -2
- package/src/ui/hooks/agentStream/streamUtils.ts +0 -24
- package/src/ui/hooks/agentStream/thoughtState.ts +110 -30
- package/src/ui/hooks/agentStream/types.ts +1 -0
- package/src/ui/hooks/agentStream/useAgentEventStream.ts +8 -17
- package/src/ui/hooks/agentStream/useAgentStream.ts +2 -2
- package/src/ui/hooks/agentStream/useAgentStreamLifecycle.ts +4 -5
- package/src/ui/hooks/agentStream/useAgentStreamOrchestration.ts +12 -5
- package/src/ui/hooks/agentStream/useQueuedSubmissions.ts +99 -0
- package/src/ui/hooks/agentStream/useStreamEventHandlers.ts +4 -13
- package/src/ui/hooks/agentStream/useStreamState.ts +60 -41
- package/src/ui/hooks/agentStream/useSubmitQuery.ts +284 -59
- package/src/ui/hooks/slashCommandProcessorSupport.ts +4 -0
- package/src/ui/hooks/useAgentStream-test-helpers.ts +1 -0
- package/src/ui/hooks/useAutoAcceptIndicator.ts +13 -0
- package/src/ui/hooks/useFolderTrust.ts +118 -81
- package/src/ui/hooks/useGitBranchName.ts +79 -33
- package/src/ui/hooks/useIdeTrustListener.ts +5 -19
- package/src/ui/hooks/usePermissionsModifyTrust.ts +223 -85
- package/src/ui/hooks/useReactToolScheduler.ts +13 -17
- package/src/ui/hooks/useTodoPausePreserver.ts +0 -55
- package/src/ui/layouts/DefaultAppLayout.tsx +2 -0
- package/src/ui/layouts/DefaultAppLayoutHelpers.tsx +37 -162
- package/src/ui/layouts/InlineContent.tsx +198 -0
- package/src/ui/trustDialogHelpers.ts +190 -0
- package/src/utils/cleanup-state.ts +115 -0
- package/src/utils/cleanup.ts +32 -34
- package/src/utils/sandbox-containers.ts +183 -53
- package/src/utils/sandbox-exec.ts +9 -3
- package/src/zed-integration/acp-terminal-shell-host.ts +112 -0
- package/src/zed-integration/acp-types.ts +35 -0
- package/src/zed-integration/fileSystemService.ts +1 -1
- package/src/zed-integration/runZedIntegration.ts +142 -0
- package/src/zed-integration/zed-agent-event-handler.ts +179 -0
- package/src/zed-integration/zed-agent-setup.ts +43 -0
- package/src/zed-integration/zed-command-registry.ts +167 -0
- package/src/zed-integration/zed-config-options.ts +280 -0
- package/src/zed-integration/zed-content-utils.ts +2 -2
- package/src/zed-integration/zed-helpers.ts +132 -0
- package/src/zed-integration/zed-initialize.ts +64 -0
- package/src/zed-integration/zed-plan-update.ts +20 -0
- package/src/zed-integration/zed-prompt-command.ts +77 -0
- package/src/zed-integration/zed-session-config.ts +119 -0
- package/src/zed-integration/zed-session-errors.ts +337 -0
- package/src/zed-integration/zed-session-events.ts +216 -0
- package/src/zed-integration/zed-session-info.ts +255 -0
- package/src/zed-integration/zed-session-lifecycle.ts +186 -0
- package/src/zed-integration/zed-session-listing.ts +153 -0
- package/src/zed-integration/zed-session-loader.ts +193 -0
- package/src/zed-integration/zed-session-pagination.ts +240 -0
- package/src/zed-integration/zed-session-replay.ts +564 -0
- package/src/zed-integration/zed-stream-batcher.ts +236 -0
- package/src/zed-integration/zed-terminal-manager.ts +416 -0
- package/src/zed-integration/zed-terminal-setup.ts +72 -0
- package/src/zed-integration/zed-test-helpers.ts +349 -4
- package/src/zed-integration/zed-tool-handler.ts +47 -5
- package/src/zed-integration/zedIntegration.ts +538 -587
- package/src/services/todo-continuation/todoContinuationService.ts +0 -610
- package/src/ui/containers/AppContainer/hooks/useIdeRestartHotkey.ts +0 -33
- package/src/ui/containers/AppContainer/hooks/useTodoContinuationFlow.ts +0 -160
- 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
|
+
}
|
package/src/utils/cleanup.ts
CHANGED
|
@@ -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
|
-
|
|
26
|
+
registerCleanupFn(fn);
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export function registerSyncCleanup(fn: () => void) {
|
|
26
|
-
|
|
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 (
|
|
32
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
} catch {
|
|
46
|
-
// Ignore errors during cleanup.
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
syncCleanupFunctions.length = 0;
|
|
57
|
+
drainSyncCleanups(() => {
|
|
58
|
+
// Ignore errors during cleanup.
|
|
59
|
+
});
|
|
50
60
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
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
|
-
|
|
489
|
-
|
|
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
|
-
|
|
497
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
612
|
-
|
|
613
|
-
process.
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
-
|
|
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
|
-
|
|
624
|
-
|
|
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
|
-
|
|
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
|
+
}
|