From c698f9bdedd45f54023015d5a404e92dc751d05c Mon Sep 17 00:00:00 2001 From: Eotel <49769982+Eotel@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:41:15 +0900 Subject: [PATCH] fix: prevent setup command truncation during shell startup (#25820) * fix: keep setup command short so it survives shell startup The POSIX setup wrapper inlined the runner path six times. On long worktree paths it exceeded macOS's 1024-byte canonical PTY input limit when typed while the shell was still sourcing rc files, so the tail and submit byte were dropped, setup never ran, and a gated agent waited forever. Carry the setup script in ORCA_SEQUENCED_SETUP_SCRIPT and submit only `bash -lc 'eval "$ORCA_SEQUENCED_SETUP_SCRIPT"'`, mirroring the existing startup gate. The env is merged into setup.envVars wherever the gate is built so provisioning, fallback retries and renderer tabs/splits all receive it, and WSL imports it untranslated. Closes #20935 * fix(wsl): forward the setup script WSLENV entry before the daemon boundary Same-protocol daemons from older builds rebuild WSLENV without ORCA_SEQUENCED_SETUP_SCRIPT, so WSL setup would eval an empty script and the gated agent would wait for its timeout. Also swap new spec casts for the spawnedPtyEnv fixture. * fix: fail loudly when the sequenced setup script env is missing A dropped ORCA_SEQUENCED_SETUP_SCRIPT made eval run an empty string and exit 0, silently skipping setup. ${VAR:?missing} reports it in the Setup pane. --------- Co-authored-by: Neil --- .../pty/pty-daemon-spawn-wsl-runtime.test.ts | 7 +++ src/main/ipc/pty/spawn-host-env.ts | 6 +++ src/main/ipc/worktree-remote.ts | 4 +- .../ipc/worktrees-local-create-flow.test.ts | 10 ++-- src/main/pty/wsl-orca-env.test.ts | 9 ++-- src/main/pty/wsl-orca-env.ts | 2 + ...worktree-setup-and-startup-part-02.spec.ts | 13 +++-- ...worktree-setup-and-startup-part-04.spec.ts | 26 ++++++---- ...runtime-local-worktree-terminal-startup.ts | 18 +++++-- .../worktree-activation-setup-script.test.ts | 52 +++++++------------ .../worktree-activation-web-runtime.test.ts | 20 +++---- .../lib/worktree-initial-terminal-seeding.ts | 1 + src/shared/setup-agent-sequencing.test.ts | 38 ++++++++++---- src/shared/setup-agent-sequencing.ts | 20 ++++--- 14 files changed, 144 insertions(+), 82 deletions(-) diff --git a/src/main/ipc/pty/pty-daemon-spawn-wsl-runtime.test.ts b/src/main/ipc/pty/pty-daemon-spawn-wsl-runtime.test.ts index 2de9bee161e..a1496e06aca 100644 --- a/src/main/ipc/pty/pty-daemon-spawn-wsl-runtime.test.ts +++ b/src/main/ipc/pty/pty-daemon-spawn-wsl-runtime.test.ts @@ -593,6 +593,13 @@ describe('registerPtyHandlers', () => { expect(env.GIT_CONFIG_KEY_0).toBe('http.proxy') expect(env.GIT_CONFIG_KEY_1).toBeUndefined() }) + it('forwards the sequenced setup script into WSLENV before the daemon boundary', async () => { + const env = await withWin32Platform(() => + daemonSpawnAndGetEnv({ ORCA_SEQUENCED_SETUP_SCRIPT: 'true', WSLENV: 'EXISTING/p' }) + ) + + expect(env.WSLENV).toBe('EXISTING/p:ORCA_SEQUENCED_SETUP_SCRIPT') + }) }) }) }) diff --git a/src/main/ipc/pty/spawn-host-env.ts b/src/main/ipc/pty/spawn-host-env.ts index 655ac5daf2c..2d231d9a329 100644 --- a/src/main/ipc/pty/spawn-host-env.ts +++ b/src/main/ipc/pty/spawn-host-env.ts @@ -3,8 +3,10 @@ import type { AgentProviderSessionMetadata } from '../../../shared/agent-session import { getAppEnvironment } from '../../../shared/app-environment' import type { GlobalSettings } from '../../../shared/global-settings-types' import { stampRuntimeSourceEnv } from '../../../shared/runtime-source-env' +import { SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV } from '../../../shared/setup-agent-sequencing' import type { TuiAgent } from '../../../shared/tui-agent' import { isTuiAgent } from '../../../shared/tui-agent-config' +import { addWslEnvKeys } from '../../../shared/wsl-env' import { isAgentStatusHooksEnabled } from '../../agent-hooks/managed-agent-hook-controls' import { awaitExplicitAgentPluginGuestReadiness } from '../../agent-hooks/wsl-agent-plugin-readiness' import { ensureCodexStateDbBackfillRecoveryStarted } from '../../codex/codex-state-db-backfill-recovery' @@ -245,6 +247,10 @@ export async function resolvePtySpawnHostEnv(args: PtySpawnHostEnvArgs): Promise getRuntimeSourceStamp(deps.runtime, getAppEnvironment().getPath('userData')) ) promoteAgentTeamsShimPath(env, args.requestedAgentTeamsPath) + if (process.platform === 'win32' && env[SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV]) { + // Why: same-protocol daemons from older builds don't add this WSLENV entry themselves. + addWslEnvKeys(env, [SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV]) + } } catch (err) { // Host prep creates hook/extension state; only this request's minted ID is safe to sweep. if (args.mintedSessionId) { diff --git a/src/main/ipc/worktree-remote.ts b/src/main/ipc/worktree-remote.ts index 8a299d9a544..75b3d6967d2 100644 --- a/src/main/ipc/worktree-remote.ts +++ b/src/main/ipc/worktree-remote.ts @@ -317,7 +317,8 @@ async function spawnLocalStartupAndSetupTerminals(args: { defaultTabs: CreateWorktreeResult['defaultTabs'] createdWithAgent: CreateWorktreeArgs['createdWithAgent'] }): Promise { - const { runtime, worktree, startup, setup, defaultTabs, createdWithAgent } = args + const { runtime, worktree, startup, defaultTabs, createdWithAgent } = args + let { setup } = args if (!startup || defaultTabs?.tabs.length) { return { didSpawnSetup: false } } @@ -345,6 +346,7 @@ async function spawnLocalStartupAndSetupTerminals(args: { ...(sequenced.startupEnv ? { env: { ...startup.env, ...sequenced.startupEnv } } : {}) } wrappedSetupCommandStr = sequenced.setupCommand + setup = { ...setup, envVars: { ...setup.envVars, ...sequenced.setupEnv } } } try { diff --git a/src/main/ipc/worktrees-local-create-flow.test.ts b/src/main/ipc/worktrees-local-create-flow.test.ts index 15cb18ab13d..0e1053c9b0f 100644 --- a/src/main/ipc/worktrees-local-create-flow.test.ts +++ b/src/main/ipc/worktrees-local-create-flow.test.ts @@ -1,5 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { resolve } from 'node:path' +import { SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV } from '../../shared/setup-agent-sequencing' +import type { CreateWorktreeResult } from '../../shared/worktree/create-types' import { resolveRegisteredWorktreePath } from './registered-worktree-roots-cache' import { computeWorkspaceRootAsync } from './worktree-logic' import type * as WorktreeLogic from './worktree-logic' @@ -779,15 +781,17 @@ describe('registerWorktreeHandlers', () => { request_kind: 'new' } } - })) as { setup?: { command?: string; runnerScriptPath: string } } + })) as { setup?: CreateWorktreeResult['setup'] } expect(result.setup).toEqual( expect.objectContaining({ runnerScriptPath: 'C:\\workspace\\repo\\.git\\orca\\setup-runner.sh', - command: expect.stringContaining('bash /mnt/c/workspace/repo/.git/orca/setup-runner.sh') + command: `bash -lc 'eval "\${${SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV}:?missing}"'` }) ) - expect(result.setup?.command).toContain('printf') + expect(result.setup?.envVars[SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV]).toContain( + 'bash /mnt/c/workspace/repo/.git/orca/setup-runner.sh' + ) }) it('opens Setup beside the startup terminal without surfacing the workspace', async () => { diff --git a/src/main/pty/wsl-orca-env.test.ts b/src/main/pty/wsl-orca-env.test.ts index bea84798d03..7f2e8ceadf4 100644 --- a/src/main/pty/wsl-orca-env.test.ts +++ b/src/main/pty/wsl-orca-env.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import { isAbsolute } from 'node:path' import { getShellReadyWrapperRoot } from '../providers/local-pty-shell-ready-wrapper-root' import { + SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV, SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV, SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV } from '../../shared/setup-agent-sequencing' @@ -46,10 +47,11 @@ describe('addOrcaWslInteropEnv', () => { expect(env.WSLENV?.split(':')).toContain('ORCA_SHELL_READY_ROOT/p') }) - it('imports setup-gated startup env into WSL without path translation', () => { + it('imports setup-gated startup and setup env into WSL without path translation', () => { const env: Record = { [SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]: 'codex', - [SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]: 'while :; do sleep 1; done' + [SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]: 'while :; do sleep 1; done', + [SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV]: '( bash /repo/setup-runner.sh )' } addOrcaWslInteropEnv(env) @@ -57,7 +59,8 @@ describe('addOrcaWslInteropEnv', () => { expect(env.WSLENV?.split(':')).toEqual([ 'ORCA_SHELL_READY_ROOT/p', `${SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV}/u`, - `${SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV}/u` + `${SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV}/u`, + `${SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV}/u` ]) }) diff --git a/src/main/pty/wsl-orca-env.ts b/src/main/pty/wsl-orca-env.ts index 63d1fb1f55c..715e4371ce6 100644 --- a/src/main/pty/wsl-orca-env.ts +++ b/src/main/pty/wsl-orca-env.ts @@ -9,6 +9,7 @@ import { ORCHESTRATION_COMPATIBILITY_HOST_KIND_ENV } from '../../shared/orchestration-compatibility-evidence' import { + SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV, SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV, SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV } from '../../shared/setup-agent-sequencing' @@ -98,6 +99,7 @@ export function addOrcaWslInteropEnv(env: Record): void { 'ORCA_OPENCODE_AGENT/u', `${SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV}/u`, `${SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV}/u`, + `${SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV}/u`, 'ORCA_ORCHESTRATION_COMPATIBILITY_HOST_KIND/u', 'ORCA_ORCHESTRATION_COMPATIBILITY_HOST_ID/u', 'ORCA_ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION/u', diff --git a/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-02.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-02.spec.ts index 0888554f5e7..aa3978d99a9 100644 --- a/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-02.spec.ts +++ b/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-02.spec.ts @@ -1,5 +1,6 @@ import { getDefaultWorkspaceSession } from '../../../shared/constants' import { describe, expect, it, vi } from 'vitest' +import { SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV } from '../../../shared/setup-agent-sequencing' import { OrcaRuntimeService, SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV, @@ -115,15 +116,19 @@ describe('OrcaRuntimeService', () => { } const startupCommand = startup.command const startupScript = startup.env[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]! - const setupCommand = (spawn.mock.calls[1]![0] as { command: string }).command + const setupScript = spawnedPtyEnv(spawn.mock.calls[1]![0])[ + SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV + ] const nonceMatch = startupScript.match(/if \[ "\$seen" = ([0-9a-f-]+) \]/) expect(nonceMatch?.[1]).toBeTruthy() expect(startupCommand.length).toBeLessThan(256) expect(startupScript).toContain('exec claude') expect(startupScript).toContain('/mnt/c/tmp/repo/.git/orca/setup-runner.sh') - expect(setupCommand).toContain('bash /mnt/c/tmp/repo/.git/orca/setup-runner.sh') - expect(setupCommand).toContain('printf') - expect(setupCommand).toContain(`${nonceMatch![1]} "$status"`) + expect(spawn.mock.calls[1]![0]).toMatchObject({ + command: `bash -lc 'eval "\${${SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV}:?missing}"'` + }) + expect(setupScript).toContain('bash /mnt/c/tmp/repo/.git/orca/setup-runner.sh') + expect(setupScript).toContain(`${nonceMatch![1]} "$status"`) expect(result.setup).toBeUndefined() }) diff --git a/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-04.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-04.spec.ts index 3b32998b653..a9bdec4fa88 100644 --- a/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-04.spec.ts +++ b/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-04.spec.ts @@ -1,6 +1,7 @@ import type { RuntimeClientEvent } from '../../../shared/runtime-client-events' import { getDefaultWorkspaceSession } from '../../../shared/constants' import { describe, expect, it, vi } from 'vitest' +import { SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV } from '../../../shared/setup-agent-sequencing' import { OrcaRuntimeService, SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV, @@ -334,10 +335,13 @@ describe('OrcaRuntimeService', () => { 2, expect.objectContaining({ cwd: '/tmp/workspaces/runtime-startup-setup-split', - command: expect.stringContaining('bash /tmp/repo/.git/orca/setup-runner.sh'), + command: `bash -lc 'eval "\${${SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV}:?missing}"'`, env: expect.objectContaining({ ORCA_ROOT_PATH: '/tmp/repo', - ORCA_WORKTREE_PATH: '/tmp/workspaces/runtime-startup-setup-split' + ORCA_WORKTREE_PATH: '/tmp/workspaces/runtime-startup-setup-split', + [SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV]: expect.stringContaining( + 'bash /tmp/repo/.git/orca/setup-runner.sh' + ) }), worktreeId: result.worktree.id }) @@ -348,16 +352,17 @@ describe('OrcaRuntimeService', () => { } const startupCommand = startup.command const startupScript = startup.env[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]! - const setupCommand = (spawn.mock.calls[1]![0] as { command: string }).command + const setupScript = spawnedPtyEnv(spawn.mock.calls[1]![0])[ + SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV + ] const nonceMatch = startupScript.match(/if \[ "\$seen" = ([0-9a-f-]+) \]/) expect(nonceMatch?.[1]).toBeTruthy() const markerPath = `/tmp/repo/.git/orca/setup-runner.sh.${nonceMatch![1]}.done` expect(startupCommand.length).toBeLessThan(256) expect(startupScript).toContain('--dangerously-bypass-approvals-and-sandbox') - expect(setupCommand).toContain('printf') - expect(setupCommand).toContain(`${nonceMatch![1]} "$status"`) + expect(setupScript).toContain(`${nonceMatch![1]} "$status"`) expect(startupScript).toContain(markerPath) - expect(setupCommand).toContain(markerPath) + expect(setupScript).toContain(markerPath) const mainEnv = spawnedPtyEnv(spawn.mock.calls[0]![0]) const setupEnv = spawnedPtyEnv(spawn.mock.calls[1]![0]) expect(result.setup).toBeUndefined() @@ -473,7 +478,12 @@ describe('OrcaRuntimeService', () => { expect.any(String), expect.objectContaining({ runnerScriptPath: 'C:\\tmp\\repo\\.git\\orca\\setup-runner.sh', - command: expect.stringContaining('bash /mnt/c/tmp/repo/.git/orca/setup-runner.sh') + command: `bash -lc 'eval "\${${SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV}:?missing}"'`, + envVars: expect.objectContaining({ + [SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV]: expect.stringContaining( + 'bash /mnt/c/tmp/repo/.git/orca/setup-runner.sh' + ) + }) }), undefined, undefined @@ -483,8 +493,6 @@ describe('OrcaRuntimeService', () => { expect( events.filter((event) => event.type === 'activateWorktree' && event.setup !== undefined) ).toEqual([]) - const activationSetup = activateWorktree.mock.calls[0]?.[2] as { command?: string } | undefined - expect(activationSetup?.command).toContain('printf') }) it('lets explicit startup draft agents override the desktop default', async () => { diff --git a/src/main/runtime/runtime-local-worktree-terminal-startup.ts b/src/main/runtime/runtime-local-worktree-terminal-startup.ts index 7337765955d..5a7cfe7bbc9 100644 --- a/src/main/runtime/runtime-local-worktree-terminal-startup.ts +++ b/src/main/runtime/runtime-local-worktree-terminal-startup.ts @@ -72,7 +72,8 @@ export async function startRuntimeLocalWorktreeTerminals(args: { warning?: string ports: Ports }): Promise { - const { request, repo, worktree, setup, defaultTabs, startup, ports } = args + const { request, repo, worktree, defaultTabs, startup, ports } = args + let { setup } = args const shouldActivate = request.activate === true || request.runHooks === true let warning = args.warning let didSpawnStartup = false @@ -98,6 +99,7 @@ export async function startRuntimeLocalWorktreeTerminals(args: { ...(sequenced.startupEnv ? { env: { ...startup.env, ...sequenced.startupEnv } } : {}) } wrappedSetupCommand = sequenced.setupCommand + setup = { ...setup, envVars: { ...setup.envVars, ...sequenced.setupEnv } } } // Main owns failed agent startup too, keeping its reason visible without a terminal service. @@ -162,7 +164,12 @@ export async function startRuntimeLocalWorktreeTerminals(args: { Boolean(setup || defaultTabs)) if (runtimeWillProvision) { const provisioned = await ports.provision({ - ...provisionArgs(args, startupTerminalHandle, startupPaneHeld, wrappedSetupCommand), + ...provisionArgs( + { ...args, setup }, + startupTerminalHandle, + startupPaneHeld, + wrappedSetupCommand + ), ...(provisionInBackground ? { surfaceOwner: false as const } : {}) }) didSpawnSetup = provisioned.setupSpawned @@ -186,7 +193,12 @@ export async function startRuntimeLocalWorktreeTerminals(args: { } } else if (ports.canSpawn && (setup || defaultTabs || didSpawnStartup)) { const provisioning = ports.provision({ - ...provisionArgs(args, startupTerminalHandle, startupPaneHeld, wrappedSetupCommand), + ...provisionArgs( + { ...args, setup }, + startupTerminalHandle, + startupPaneHeld, + wrappedSetupCommand + ), surfaceOwner: false }) if (request.awaitTerminalProvisioning) { diff --git a/src/renderer/src/lib/worktree-activation-setup-script.test.ts b/src/renderer/src/lib/worktree-activation-setup-script.test.ts index 7ed04bccb61..7db3bad2124 100644 --- a/src/renderer/src/lib/worktree-activation-setup-script.test.ts +++ b/src/renderer/src/lib/worktree-activation-setup-script.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it, vi } from 'vitest' -import { SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV } from '../../../shared/setup-agent-sequencing' +import { + SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV, + SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV +} from '../../../shared/setup-agent-sequencing' import { ensureWorktreeHasInitialTerminal } from './worktree-initial-terminal-seeding' import { createMockStore, @@ -10,6 +13,13 @@ import { registerWorktreeActivationReset() +function sequencedSetup(env: Record, runnerCommand: string): unknown { + return { + command: `bash -lc 'eval "\${${SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV}:?missing}"'`, + env: { ...env, [SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV]: expect.stringContaining(runnerCommand) } + } +} + describe('ensureWorktreeHasInitialTerminal', () => { it('creates a background Setup tab for newly created worktrees by default', () => { let createdIndex = 0 @@ -112,14 +122,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(result).toBe('tab-1') expect(createTab).toHaveBeenCalledTimes(1) expect(createdTabStartups(store)['tab-2']).toEqual( - expect.objectContaining({ - command: expect.stringContaining('bash /tmp/repo/.git/orca/setup-runner.sh') - }) - ) - expect(createdTabStartups(store)['tab-2']).toEqual( - expect.objectContaining({ - command: expect.stringContaining('printf') - }) + sequencedSetup({ ORCA_ROOT_PATH: '/tmp/repo' }, 'bash /tmp/repo/.git/orca/setup-runner.sh') ) }) @@ -176,9 +179,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { }) }) ) - expect(startups['tab-3']).toEqual( - expect.objectContaining({ command: expect.stringContaining('bash /tmp/setup.sh') }) - ) + expect(startups['tab-3']).toEqual(sequencedSetup({}, 'bash /tmp/setup.sh')) if (!activateCreatedTabs) { expect(store.setActiveTab).not.toHaveBeenCalled() } @@ -219,14 +220,7 @@ describe('ensureWorktreeHasInitialTerminal', () => { }) ) expect(createdTabStartups(store)['tab-2']).toEqual( - expect.objectContaining({ - command: expect.stringContaining('printf') - }) - ) - expect(createdTabStartups(store)['tab-2']).toEqual( - expect.objectContaining({ - command: expect.stringContaining('bash /tmp/repo/.git/orca/setup-runner.sh') - }) + sequencedSetup({ ORCA_ROOT_PATH: '/tmp/repo' }, 'bash /tmp/repo/.git/orca/setup-runner.sh') ) }) @@ -277,14 +271,9 @@ describe('ensureWorktreeHasInitialTerminal', () => { }) }) ) - expect(createdTabStartups(store)['tab-2']).toEqual({ - command: expect.stringContaining('bash /tmp/repo/.git/orca/setup-runner.sh'), - env: { ORCA_ROOT_PATH: '/tmp/repo' } - }) - expect(createdTabStartups(store)['tab-2']).toEqual({ - command: expect.stringContaining('printf'), - env: { ORCA_ROOT_PATH: '/tmp/repo' } - }) + expect(createdTabStartups(store)['tab-2']).toEqual( + sequencedSetup({ ORCA_ROOT_PATH: '/tmp/repo' }, 'bash /tmp/repo/.git/orca/setup-runner.sh') + ) }) it('keeps WSL setup shell metadata when gating startup behind setup completion', () => { @@ -312,10 +301,9 @@ describe('ensureWorktreeHasInitialTerminal', () => { }) }) ) - expect(createdTabStartups(store)['tab-2']).toEqual({ - command: expect.stringContaining('bash /mnt/c/repo/.git/orca/setup-runner.sh'), - env: { ORCA_ROOT_PATH: 'C:\\repo' } - }) + expect(createdTabStartups(store)['tab-2']).toEqual( + sequencedSetup({ ORCA_ROOT_PATH: 'C:\\repo' }, 'bash /mnt/c/repo/.git/orca/setup-runner.sh') + ) }) it.each(['split-vertical', 'split-horizontal', 'new-tab', null] as const)( diff --git a/src/renderer/src/lib/worktree-activation-web-runtime.test.ts b/src/renderer/src/lib/worktree-activation-web-runtime.test.ts index 1d695fff573..da486256092 100644 --- a/src/renderer/src/lib/worktree-activation-web-runtime.test.ts +++ b/src/renderer/src/lib/worktree-activation-web-runtime.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest' +import { SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV } from '../../../shared/setup-agent-sequencing' import { activateAndRevealFolderWorkspace, activateAndRevealWorktree } from './worktree-activation' import { ensureWorktreeHasInitialTerminal } from './worktree-initial-terminal-seeding' import type { AppStoreState } from './worktree-activation.test-fixture' @@ -300,16 +301,15 @@ describe('ensureWorktreeHasInitialTerminal', () => { expect(store.setTabCustomTitle).toHaveBeenCalledWith('tab-2', 'Setup', { recordInteraction: false }) - expect(createdTabStartups(store)['tab-2']).toEqual( - expect.objectContaining({ - command: expect.stringContaining('bash /tmp/repo/.git/orca/setup-runner.sh') - }) - ) - expect(createdTabStartups(store)['tab-2']).toEqual( - expect.objectContaining({ - command: expect.stringContaining('printf') - }) - ) + expect(createdTabStartups(store)['tab-2']).toEqual({ + command: `bash -lc 'eval "\${${SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV}:?missing}"'`, + env: { + ORCA_ROOT_PATH: '/tmp/repo', + [SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV]: expect.stringContaining( + 'bash /tmp/repo/.git/orca/setup-runner.sh' + ) + } + }) }) it('holds the issue command for the first mirrored web runtime tab when none exists yet', () => { diff --git a/src/renderer/src/lib/worktree-initial-terminal-seeding.ts b/src/renderer/src/lib/worktree-initial-terminal-seeding.ts index 1946c1b45c6..f2eb9607881 100644 --- a/src/renderer/src/lib/worktree-initial-terminal-seeding.ts +++ b/src/renderer/src/lib/worktree-initial-terminal-seeding.ts @@ -129,6 +129,7 @@ export function ensureWorktreeHasInitialTerminal( ...(sequenced.startupEnv ? { env: { ...startup.env, ...sequenced.startupEnv } } : {}) } wrappedSetupCommandStr = sequenced.setupCommand + setup = { ...setup, envVars: { ...setup.envVars, ...sequenced.setupEnv } } } const backendStartupTerminalSpawned = opts?.backendStartupTerminalSpawned === true diff --git a/src/shared/setup-agent-sequencing.test.ts b/src/shared/setup-agent-sequencing.test.ts index 42b864fcc68..4f741e19759 100644 --- a/src/shared/setup-agent-sequencing.test.ts +++ b/src/shared/setup-agent-sequencing.test.ts @@ -8,7 +8,9 @@ import { afterEach, describe, expect, it } from 'vitest' import { createSequencedSetupAgentCommands, SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV, - SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV + SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV, + SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV, + type SequencedSetupAgentCommands } from './setup-agent-sequencing' const TEMP_DIRS: string[] = [] @@ -29,6 +31,9 @@ describe('createSequencedSetupAgentCommands', () => { nonce: 'long-path' }) + expect(Buffer.byteLength(result.setupCommand)).toBeLessThan(256) + expect(result.setupCommand).not.toContain('nested-worktree') + expect(result.setupEnv?.[SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV]).toContain('nested-worktree') expect(result.startupCommand.length).toBeLessThan(256) expect(result.startupCommand).not.toContain('nested-worktree') expect(result.startupEnv?.[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]).toContain( @@ -44,7 +49,7 @@ describe('createSequencedSetupAgentCommands', () => { nonce: 'nonce-remote' }) - expect(result.setupCommand).toContain( + expect(setupScript(result)).toContain( 'bash /remote/repo/.git/worktrees/feature/orca/setup-runner.sh' ) expect(result.startupEnv?.[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]).toContain( @@ -61,8 +66,8 @@ describe('createSequencedSetupAgentCommands', () => { nonce: 'nonce-wsl-shell' }) - expect(result.setupCommand).toContain('bash /mnt/c/repo/.git/orca/setup-runner.sh') - expect(result.setupCommand).toContain( + expect(setupScript(result)).toContain('bash /mnt/c/repo/.git/orca/setup-runner.sh') + expect(setupScript(result)).toContain( '/mnt/c/repo/.git/orca/setup-runner.sh.nonce-wsl-shell.done' ) expect(result.startupEnv?.[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]).toContain( @@ -80,11 +85,11 @@ describe('createSequencedSetupAgentCommands', () => { nonce: 'nonce-gitbash-cmd' }) - expect(result.setupCommand).toContain( + expect(setupScript(result)).toContain( 'powershell.exe -NoProfile -NonInteractive -EncodedCommand' ) expect(result.setupCommand).not.toMatch(/bash\s+\S*setup-runner/) - expect(decodePowerShellScript(result.setupCommand)).toContain( + expect(decodePowerShellScript(setupScript(result))).toContain( "$runner = 'C:\\repo\\.git\\orca\\setup-runner.cmd'" ) // PowerShell Invoke-Expression cannot parse POSIX apostrophe escaping; bash must evaluate startup. @@ -95,7 +100,7 @@ describe('createSequencedSetupAgentCommands', () => { 'eval "$ORCA_SEQUENCED_STARTUP_COMMAND"' ) // Why: bash writes and reads the marker here, so it needs the /c/... form of the path. - expect(result.setupCommand).toContain( + expect(setupScript(result)).toContain( '/c/repo/.git/orca/setup-runner.cmd.nonce-gitbash-cmd.done' ) expect(result.startupEnv?.[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]).toContain( @@ -198,7 +203,10 @@ describe('createSequencedSetupAgentCommands', () => { expect(readFileSync(markerPath, 'utf8')).toBe('stale:0\n') const setupExit = await waitForExit( - spawn('bash', ['-lc', commands.setupCommand], { stdio: 'pipe' }) + spawn('bash', ['-lc', commands.setupCommand], { + stdio: 'pipe', + env: { ...process.env, ...commands.setupEnv } + }) ) expect(setupExit.code).toBe(0) @@ -232,7 +240,10 @@ describe('createSequencedSetupAgentCommands', () => { }) const setupExitPromise = waitForExit( - spawn('bash', ['-lc', commands.setupCommand], { stdio: 'pipe' }) + spawn('bash', ['-lc', commands.setupCommand], { + stdio: 'pipe', + env: { ...process.env, ...commands.setupEnv } + }) ) const startupExit = await waitForExit( spawn('bash', ['-lc', commands.startupCommand], { @@ -282,7 +293,10 @@ describe('createSequencedSetupAgentCommands', () => { }) const setupExitPromise = waitForExit( - spawn('bash', ['-lc', commands.setupCommand], { stdio: 'pipe' }) + spawn('bash', ['-lc', commands.setupCommand], { + stdio: 'pipe', + env: { ...process.env, ...commands.setupEnv } + }) ) const startupExit = await waitForExit( spawn('bash', ['-lc', commands.startupCommand], { @@ -331,6 +345,10 @@ describe('createSequencedSetupAgentCommands', () => { ) }) +function setupScript(commands: SequencedSetupAgentCommands): string { + return commands.setupEnv?.[SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV] ?? commands.setupCommand +} + function makeTempDir(): string { const dir = mkdtempSync(join(tmpdir(), 'orca-setup-sequencing-')) TEMP_DIRS.push(dir) diff --git a/src/shared/setup-agent-sequencing.ts b/src/shared/setup-agent-sequencing.ts index cc48ba6b18c..0225e4d10db 100644 --- a/src/shared/setup-agent-sequencing.ts +++ b/src/shared/setup-agent-sequencing.ts @@ -15,9 +15,11 @@ const DEFAULT_WAIT_TIMEOUT_SECONDS = 2 * 60 * 60 export const SETUP_COMPLETE_MESSAGE = 'Setup finished; starting agent.' export const SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV = 'ORCA_SEQUENCED_STARTUP_COMMAND' export const SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV = 'ORCA_SEQUENCED_STARTUP_SCRIPT' +export const SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV = 'ORCA_SEQUENCED_SETUP_SCRIPT' export type SequencedSetupAgentCommands = { setupCommand: string + setupEnv?: Record startupCommand: string startupEnv?: Record } @@ -75,8 +77,15 @@ export function createSequencedSetupAgentCommands(args: { waitTimeoutSeconds ) return { - setupCommand: buildPosixSetupCommand(resolution.command, markerPath, nonce), - // Why: long worktree paths can push the gate past a PTY's canonical input cap and drop its submit byte. + // Why: long worktree paths can push typed wrappers past a PTY's canonical input cap and drop their submit byte. + setupCommand: `bash -lc 'eval "\${${SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV}:?missing}"'`, + setupEnv: { + [SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV]: buildPosixSetupScript( + resolution.command, + markerPath, + nonce + ) + }, startupCommand: `bash -lc 'eval "$${SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV}"'`, startupEnv: { [SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]: args.startupCommand, @@ -85,13 +94,12 @@ export function createSequencedSetupAgentCommands(args: { } } -function buildPosixSetupCommand(setupCommand: string, markerPath: string, nonce: string): string { +function buildPosixSetupScript(setupCommand: string, markerPath: string, nonce: string): string { const marker = quotePosixShellUnlessPlainNoAt(markerPath) const tmp = quotePosixShellUnlessPlainNoAt(`${markerPath}.tmp`) const nonceValue = quotePosixShellUnlessPlainNoAt(nonce) - // Why no `( … )`: this is typed into the user's line editor, where autopair widgets corrupt it (#18059). - const script = [ + return [ `rm -f ${marker} ${tmp} 2>/dev/null`, setupCommand, 'status=$?', @@ -99,8 +107,6 @@ function buildPosixSetupCommand(setupCommand: string, markerPath: string, nonce: `mv -f ${tmp} ${marker}`, 'exit "$status"' ].join('; ') - - return `bash -lc ${quotePosixShellUnlessPlainNoAt(script)}` } function buildPosixStartupScript(