From dea567e43c71d17a5a68a4f1bc92b0ccf5c7d8d1 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:31:55 -0700 Subject: [PATCH] Fix inherited PTY setup launch state --- .../pty-subprocess-env-inheritance.test.ts | 24 ++++++++-- .../pty-subprocess/spawn-environment.ts | 20 +------- .../pty-serializer-settlement-mapping.test.ts | 16 +++++++ .../providers/local-pty-launch-helpers.ts | 18 -------- .../local-pty-provider-spawn-env.test.ts | 24 ++++++++-- src/main/providers/local-pty-provider.ts | 6 +-- src/relay/pty-handler-revive.test.ts | 46 ++++++++++--------- .../pty-handler-spawn-environment.test.ts | 37 +++++++++++++++ src/relay/pty-handler.ts | 2 + src/shared/pty-child-scoped-env.ts | 24 ++++++++++ 10 files changed, 146 insertions(+), 71 deletions(-) create mode 100644 src/shared/pty-child-scoped-env.ts diff --git a/src/main/daemon/pty-subprocess-env-inheritance.test.ts b/src/main/daemon/pty-subprocess-env-inheritance.test.ts index f6f6cb3a056..dba475e8672 100644 --- a/src/main/daemon/pty-subprocess-env-inheritance.test.ts +++ b/src/main/daemon/pty-subprocess-env-inheritance.test.ts @@ -119,17 +119,21 @@ describe('createPtySubprocess', () => { ) }) - it('does not inherit parent Orca pane identity when caller omits pane env', async () => { + it('does not inherit parent-scoped Orca env when caller omits it', async () => { const proc = mockPtyProcess() spawnMock.mockReturnValue(proc) const saved = { ORCA_PANE_KEY: process.env.ORCA_PANE_KEY, ORCA_TAB_ID: process.env.ORCA_TAB_ID, - ORCA_WORKTREE_ID: process.env.ORCA_WORKTREE_ID + ORCA_WORKTREE_ID: process.env.ORCA_WORKTREE_ID, + ORCA_SEQUENCED_STARTUP_COMMAND: process.env.ORCA_SEQUENCED_STARTUP_COMMAND, + ORCA_SEQUENCED_STARTUP_SCRIPT: process.env.ORCA_SEQUENCED_STARTUP_SCRIPT } process.env.ORCA_PANE_KEY = 'parent-tab:parent-leaf' process.env.ORCA_TAB_ID = 'parent-tab' process.env.ORCA_WORKTREE_ID = 'parent-worktree' + process.env.ORCA_SEQUENCED_STARTUP_COMMAND = 'parent-agent' + process.env.ORCA_SEQUENCED_STARTUP_SCRIPT = 'parent-gate' try { await createPtySubprocess({ sessionId: 'test', cols: 80, rows: 24 }) @@ -147,19 +151,25 @@ describe('createPtySubprocess', () => { expect(env.ORCA_PANE_KEY).toBeUndefined() expect(env.ORCA_TAB_ID).toBeUndefined() expect(env.ORCA_WORKTREE_ID).toBeUndefined() + expect(env.ORCA_SEQUENCED_STARTUP_COMMAND).toBeUndefined() + expect(env.ORCA_SEQUENCED_STARTUP_SCRIPT).toBeUndefined() }) - it('preserves explicit child Orca pane identity over parent env', async () => { + it('preserves explicit child-scoped Orca env over parent env', async () => { const proc = mockPtyProcess() spawnMock.mockReturnValue(proc) const saved = { ORCA_PANE_KEY: process.env.ORCA_PANE_KEY, ORCA_TAB_ID: process.env.ORCA_TAB_ID, - ORCA_WORKTREE_ID: process.env.ORCA_WORKTREE_ID + ORCA_WORKTREE_ID: process.env.ORCA_WORKTREE_ID, + ORCA_SEQUENCED_STARTUP_COMMAND: process.env.ORCA_SEQUENCED_STARTUP_COMMAND, + ORCA_SEQUENCED_STARTUP_SCRIPT: process.env.ORCA_SEQUENCED_STARTUP_SCRIPT } process.env.ORCA_PANE_KEY = 'parent-tab:parent-leaf' process.env.ORCA_TAB_ID = 'parent-tab' process.env.ORCA_WORKTREE_ID = 'parent-worktree' + process.env.ORCA_SEQUENCED_STARTUP_COMMAND = 'parent-agent' + process.env.ORCA_SEQUENCED_STARTUP_SCRIPT = 'parent-gate' try { await createPtySubprocess({ @@ -169,7 +179,9 @@ describe('createPtySubprocess', () => { env: { ORCA_PANE_KEY: 'child-tab:child-leaf', ORCA_TAB_ID: 'child-tab', - ORCA_WORKTREE_ID: 'child-worktree' + ORCA_WORKTREE_ID: 'child-worktree', + ORCA_SEQUENCED_STARTUP_COMMAND: 'child-agent', + ORCA_SEQUENCED_STARTUP_SCRIPT: 'child-gate' } }) } finally { @@ -186,6 +198,8 @@ describe('createPtySubprocess', () => { expect(env.ORCA_PANE_KEY).toBe('child-tab:child-leaf') expect(env.ORCA_TAB_ID).toBe('child-tab') expect(env.ORCA_WORKTREE_ID).toBe('child-worktree') + expect(env.ORCA_SEQUENCED_STARTUP_COMMAND).toBe('child-agent') + expect(env.ORCA_SEQUENCED_STARTUP_SCRIPT).toBe('child-gate') }) it.each([ diff --git a/src/main/daemon/pty-subprocess/spawn-environment.ts b/src/main/daemon/pty-subprocess/spawn-environment.ts index cbace940f46..91d5493a805 100644 --- a/src/main/daemon/pty-subprocess/spawn-environment.ts +++ b/src/main/daemon/pty-subprocess/spawn-environment.ts @@ -7,6 +7,7 @@ import { stripLegacyTerminalShimEnv } from '../../pty/legacy-terminal-shim-dir' import { removeInheritedNoColor } from '../../pty/terminal-color-env' import { resolvePathEnvKey } from '../../pty/windows-environment-path' import { dropInheritedOrcaHistFile } from '../../worktree-history-file-path' +import { removeUnspecifiedPtyChildScopedEnv } from '../../../shared/pty-child-scoped-env' import { gitCredentialPromptGuardEnv, mergeGitConfigEnvProtocol @@ -19,12 +20,6 @@ import { import type { TuiAgent } from '../../../shared/tui-agent' import type { PtySubprocessOptions } from '../pty-subprocess' -const PANE_IDENTITY_ENV_KEYS = [ - 'ORCA_PANE_KEY', - 'ORCA_TAB_ID', - 'ORCA_WORKTREE_ID', - 'ORCA_AGENT_LAUNCH_TOKEN' -] as const const WINDOWS_PATH_ENV_KEY_RE = /^path$/i function composeGuardedDaemonGitConfigEnv( @@ -58,17 +53,6 @@ function deleteRequestedDaemonEnvKeys( } } -function removeUnspecifiedPaneIdentityEnv( - env: Record, - explicitEnv: Record | undefined -): void { - for (const key of PANE_IDENTITY_ENV_KEYS) { - if (!explicitEnv || !Object.hasOwn(explicitEnv, key)) { - delete env[key] - } - } -} - /** Removes the second PATH key only when the daemon's env merge created it. */ function collapseWindowsPathEnvKeys( env: Record, @@ -146,7 +130,7 @@ export function createDaemonPtyEnvironment(opts: PtySubprocessOptions): Record { }) it('forwards the trusted Orca terminal handle into managed WSL terminals', async () => { const platform = Object.getOwnPropertyDescriptor(process, 'platform') + const savedSequencedStartupCommand = process.env.ORCA_SEQUENCED_STARTUP_COMMAND + const savedSequencedStartupScript = process.env.ORCA_SEQUENCED_STARTUP_SCRIPT Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' @@ -473,6 +475,8 @@ describe('registerPtyHandlers', () => { onPtyExit: vi.fn(), onPtyData: vi.fn() } + process.env.ORCA_SEQUENCED_STARTUP_COMMAND = 'parent-agent' + process.env.ORCA_SEQUENCED_STARTUP_SCRIPT = 'parent-gate' try { registerPtyHandlers(mainWindow as never, runtime as never) @@ -485,6 +489,16 @@ describe('registerPtyHandlers', () => { if (platform) { Object.defineProperty(process, 'platform', platform) } + if (savedSequencedStartupCommand === undefined) { + delete process.env.ORCA_SEQUENCED_STARTUP_COMMAND + } else { + process.env.ORCA_SEQUENCED_STARTUP_COMMAND = savedSequencedStartupCommand + } + if (savedSequencedStartupScript === undefined) { + delete process.env.ORCA_SEQUENCED_STARTUP_SCRIPT + } else { + process.env.ORCA_SEQUENCED_STARTUP_SCRIPT = savedSequencedStartupScript + } } const spawnCall = spawnMock.mock.calls.at(-1)! @@ -493,6 +507,8 @@ describe('registerPtyHandlers', () => { expect(env.ORCA_TERMINAL_HANDLE).toBe('term_wsl') expect(env.ORCA_USER_DATA_PATH).toBe('/tmp/orca-user-data') expect(env.ORCA_CLI_COMMAND).toBe('orca-ide') + expect(env.ORCA_SEQUENCED_STARTUP_COMMAND).toBeUndefined() + expect(env.ORCA_SEQUENCED_STARTUP_SCRIPT).toBeUndefined() expect(env.WSLENV?.split(':')).toEqual( expect.arrayContaining([ 'ORCA_TERMINAL_HANDLE/u', diff --git a/src/main/providers/local-pty-launch-helpers.ts b/src/main/providers/local-pty-launch-helpers.ts index 220669d7cb7..89202bfc8a9 100644 --- a/src/main/providers/local-pty-launch-helpers.ts +++ b/src/main/providers/local-pty-launch-helpers.ts @@ -5,28 +5,10 @@ import { resolvePathEnvKey } from '../pty/windows-environment-path' import { expandWindowsEnvironmentVariables } from '../../shared/windows-environment-expansion' import { resolveSafePtyDefaultCwd } from './pty-default-cwd' -const PANE_IDENTITY_ENV_KEYS = [ - 'ORCA_PANE_KEY', - 'ORCA_TAB_ID', - 'ORCA_WORKTREE_ID', - 'ORCA_AGENT_LAUNCH_TOKEN' -] as const - export function getDefaultCwd(): string { return resolveSafePtyDefaultCwd() } -export function removeUnspecifiedPaneIdentityEnv( - env: Record, - explicitEnv: Record | undefined -): void { - for (const key of PANE_IDENTITY_ENV_KEYS) { - if (!explicitEnv || !Object.hasOwn(explicitEnv, key)) { - delete env[key] - } - } -} - export function promoteAgentTeamsShimPath( env: Record, requestedPath: string | undefined diff --git a/src/main/providers/local-pty-provider-spawn-env.test.ts b/src/main/providers/local-pty-provider-spawn-env.test.ts index 97a85fcce10..120c8c876ae 100644 --- a/src/main/providers/local-pty-provider-spawn-env.test.ts +++ b/src/main/providers/local-pty-provider-spawn-env.test.ts @@ -543,15 +543,19 @@ describe('LocalPtyProvider', () => { expect(spawnEnv.Path.split(':')[0]).toBe('/tmp/orca-agent-teams-bin') }) - it('does not inherit parent Orca pane identity when caller omits pane env', async () => { + it('does not inherit parent-scoped Orca env when caller omits it', async () => { const saved = { ORCA_PANE_KEY: process.env.ORCA_PANE_KEY, ORCA_TAB_ID: process.env.ORCA_TAB_ID, - ORCA_WORKTREE_ID: process.env.ORCA_WORKTREE_ID + ORCA_WORKTREE_ID: process.env.ORCA_WORKTREE_ID, + ORCA_SEQUENCED_STARTUP_COMMAND: process.env.ORCA_SEQUENCED_STARTUP_COMMAND, + ORCA_SEQUENCED_STARTUP_SCRIPT: process.env.ORCA_SEQUENCED_STARTUP_SCRIPT } process.env.ORCA_PANE_KEY = 'parent-tab:parent-leaf' process.env.ORCA_TAB_ID = 'parent-tab' process.env.ORCA_WORKTREE_ID = 'parent-worktree' + process.env.ORCA_SEQUENCED_STARTUP_COMMAND = 'parent-agent' + process.env.ORCA_SEQUENCED_STARTUP_SCRIPT = 'parent-gate' try { await provider.spawn({ cols: 80, rows: 24 }) @@ -569,17 +573,23 @@ describe('LocalPtyProvider', () => { expect(spawnCall[2].env.ORCA_PANE_KEY).toBeUndefined() expect(spawnCall[2].env.ORCA_TAB_ID).toBeUndefined() expect(spawnCall[2].env.ORCA_WORKTREE_ID).toBeUndefined() + expect(spawnCall[2].env.ORCA_SEQUENCED_STARTUP_COMMAND).toBeUndefined() + expect(spawnCall[2].env.ORCA_SEQUENCED_STARTUP_SCRIPT).toBeUndefined() }) - it('preserves explicit child Orca pane identity over parent env', async () => { + it('preserves explicit child-scoped Orca env over parent env', async () => { const saved = { ORCA_PANE_KEY: process.env.ORCA_PANE_KEY, ORCA_TAB_ID: process.env.ORCA_TAB_ID, - ORCA_WORKTREE_ID: process.env.ORCA_WORKTREE_ID + ORCA_WORKTREE_ID: process.env.ORCA_WORKTREE_ID, + ORCA_SEQUENCED_STARTUP_COMMAND: process.env.ORCA_SEQUENCED_STARTUP_COMMAND, + ORCA_SEQUENCED_STARTUP_SCRIPT: process.env.ORCA_SEQUENCED_STARTUP_SCRIPT } process.env.ORCA_PANE_KEY = 'parent-tab:parent-leaf' process.env.ORCA_TAB_ID = 'parent-tab' process.env.ORCA_WORKTREE_ID = 'parent-worktree' + process.env.ORCA_SEQUENCED_STARTUP_COMMAND = 'parent-agent' + process.env.ORCA_SEQUENCED_STARTUP_SCRIPT = 'parent-gate' try { await provider.spawn({ @@ -588,7 +598,9 @@ describe('LocalPtyProvider', () => { env: { ORCA_PANE_KEY: 'child-tab:child-leaf', ORCA_TAB_ID: 'child-tab', - ORCA_WORKTREE_ID: 'child-worktree' + ORCA_WORKTREE_ID: 'child-worktree', + ORCA_SEQUENCED_STARTUP_COMMAND: 'child-agent', + ORCA_SEQUENCED_STARTUP_SCRIPT: 'child-gate' } }) } finally { @@ -605,6 +617,8 @@ describe('LocalPtyProvider', () => { expect(spawnCall[2].env.ORCA_PANE_KEY).toBe('child-tab:child-leaf') expect(spawnCall[2].env.ORCA_TAB_ID).toBe('child-tab') expect(spawnCall[2].env.ORCA_WORKTREE_ID).toBe('child-worktree') + expect(spawnCall[2].env.ORCA_SEQUENCED_STARTUP_COMMAND).toBe('child-agent') + expect(spawnCall[2].env.ORCA_SEQUENCED_STARTUP_SCRIPT).toBe('child-gate') }) }) }) diff --git a/src/main/providers/local-pty-provider.ts b/src/main/providers/local-pty-provider.ts index 1ad88aaa2ba..1565e28cd9e 100644 --- a/src/main/providers/local-pty-provider.ts +++ b/src/main/providers/local-pty-provider.ts @@ -42,6 +42,7 @@ import type { ShellReadySignal } from './local-pty-shell-ready-startup-command' import { removeInheritedNoColor } from '../pty/terminal-color-env' import { removeAppImageRuntimeEnv } from '../pty/appimage-terminal-env' import { stripInheritedBuildModeEnv } from '../pty/build-mode-env' +import { removeUnspecifiedPtyChildScopedEnv } from '../../shared/pty-child-scoped-env' import { stripLegacyTerminalShimEnv } from '../pty/legacy-terminal-shim-dir' import { dropIncoherentCondaActivationEnv } from '../pty/conda-activation-env' import { SessionNotFoundError } from '../daemon/daemon-errors' @@ -103,7 +104,6 @@ import { getWslContextFromWorktreeId, normalizeLocalCallerSessionId, promoteAgentTeamsShimPath, - removeUnspecifiedPaneIdentityEnv, resolveForegroundFallbackProcess } from './local-pty-launch-helpers' @@ -613,8 +613,8 @@ export class LocalPtyProvider implements IPtyProvider { // Why: supports-hyperlinks rejects TERM_PROGRAM=Orca, so tools drop OSC 8 links; force it since xterm.js parses them. FORCE_HYPERLINK: '1' } as Record - // Why: Orca can be launched from an Orca terminal; pane identity belongs to the child PTY, not the parent shell. - removeUnspecifiedPaneIdentityEnv(spawnEnv, args.env) + // Why: Orca can be launched from an Orca terminal; pane identity and setup launch state belong to the child PTY. + removeUnspecifiedPtyChildScopedEnv(spawnEnv, args.env) removeAppImageRuntimeEnv(spawnEnv) removeInheritedNoColor(spawnEnv) for (const key of args.envToDelete ?? []) { diff --git a/src/relay/pty-handler-revive.test.ts b/src/relay/pty-handler-revive.test.ts index 9350067da3b..2774943f732 100644 --- a/src/relay/pty-handler-revive.test.ts +++ b/src/relay/pty-handler-revive.test.ts @@ -422,8 +422,13 @@ describe('PtyHandler', () => { }) it('revive preserves attach identity metadata without exporting hook identity env', async () => { - const oldPaneKey = process.env.ORCA_PANE_KEY - const oldTabId = process.env.ORCA_TAB_ID + const keys = [ + 'ORCA_PANE_KEY', + 'ORCA_TAB_ID', + 'ORCA_SEQUENCED_STARTUP_COMMAND', + 'ORCA_SEQUENCED_STARTUP_SCRIPT' + ] as const + const saved = Object.fromEntries(keys.map((key) => [key, process.env[key]])) delete process.env.ORCA_PANE_KEY delete process.env.ORCA_TAB_ID try { @@ -436,15 +441,12 @@ describe('PtyHandler', () => { tabId: 'tab-5' }) } finally { - if (oldPaneKey === undefined) { - delete process.env.ORCA_PANE_KEY - } else { - process.env.ORCA_PANE_KEY = oldPaneKey - } - if (oldTabId === undefined) { - delete process.env.ORCA_TAB_ID - } else { - process.env.ORCA_TAB_ID = oldTabId + for (const key of keys) { + if (saved[key] === undefined) { + delete process.env[key] + } else { + process.env[key] = saved[key] + } } } const state = (await dispatcher.callRequest('pty.serialize', { ids: ['pty-1'] })) as string @@ -454,27 +456,27 @@ describe('PtyHandler', () => { dispatcher = createMockDispatcher() handler = new PtyHandler(dispatcher as unknown as RelayDispatcher) const killSpy = vi.spyOn(process, 'kill').mockImplementation(() => true) - delete process.env.ORCA_PANE_KEY - delete process.env.ORCA_TAB_ID + for (const key of keys) { + process.env[key] = `parent-${key}` + } try { await dispatcher.callRequest('pty.revive', { state }) } finally { killSpy.mockRestore() - if (oldPaneKey === undefined) { - delete process.env.ORCA_PANE_KEY - } else { - process.env.ORCA_PANE_KEY = oldPaneKey - } - if (oldTabId === undefined) { - delete process.env.ORCA_TAB_ID - } else { - process.env.ORCA_TAB_ID = oldTabId + for (const key of keys) { + if (saved[key] === undefined) { + delete process.env[key] + } else { + process.env[key] = saved[key] + } } } const callArgs = mockPtySpawn.mock.calls[0][2] as { env: Record } expect(callArgs.env.ORCA_PANE_KEY).toBeUndefined() expect(callArgs.env.ORCA_TAB_ID).toBeUndefined() + expect(callArgs.env.ORCA_SEQUENCED_STARTUP_COMMAND).toBeUndefined() + expect(callArgs.env.ORCA_SEQUENCED_STARTUP_SCRIPT).toBeUndefined() await expect( dispatcher.callRequest('pty.attach', { diff --git a/src/relay/pty-handler-spawn-environment.test.ts b/src/relay/pty-handler-spawn-environment.test.ts index 39c5a207a48..5df0f5a945b 100644 --- a/src/relay/pty-handler-spawn-environment.test.ts +++ b/src/relay/pty-handler-spawn-environment.test.ts @@ -89,6 +89,43 @@ describe('PtyHandler', () => { expect(spawnOptions.env.PATH).toBe(expectedEnv.PATH) }) + it('does not inherit parent-scoped Orca env and preserves explicit child values', async () => { + const keys = [ + 'ORCA_PANE_KEY', + 'ORCA_TAB_ID', + 'ORCA_WORKTREE_ID', + 'ORCA_AGENT_LAUNCH_TOKEN', + 'ORCA_SEQUENCED_STARTUP_COMMAND', + 'ORCA_SEQUENCED_STARTUP_SCRIPT' + ] as const + const saved = Object.fromEntries(keys.map((key) => [key, process.env[key]])) + for (const key of keys) { + process.env[key] = `parent-${key}` + } + + try { + await dispatcher.callRequest('pty.spawn', {}) + await dispatcher.callRequest('pty.spawn', { + env: Object.fromEntries(keys.map((key) => [key, `child-${key}`])) + }) + } finally { + for (const key of keys) { + if (saved[key] === undefined) { + delete process.env[key] + } else { + process.env[key] = saved[key] + } + } + } + + const inherited = mockPtySpawn.mock.calls[0][2] as { env: Record } + const explicit = mockPtySpawn.mock.calls[1][2] as { env: Record } + for (const key of keys) { + expect(inherited.env[key]).toBeUndefined() + expect(explicit.env[key]).toBe(`child-${key}`) + } + }) + describe('half-activated conda env (#14195)', () => { const CONDA_KEYS = [ 'CONDA_SHLVL', diff --git a/src/relay/pty-handler.ts b/src/relay/pty-handler.ts index a9fd21a4fe2..bfc267573ca 100644 --- a/src/relay/pty-handler.ts +++ b/src/relay/pty-handler.ts @@ -23,6 +23,7 @@ import { DEFAULT_SSH_RELAY_GRACE_PERIOD_SECONDS } from '../shared/ssh-types' import { shouldUseShellReadyStartupDelivery } from '../shared/codex-startup-delivery' import { buildStartupCommandSubmission } from '../shared/startup-command-submission' import { resolveSetupAgentSequenceLaunchCommand } from '../shared/setup-agent-sequencing' +import { removeUnspecifiedPtyChildScopedEnv } from '../shared/pty-child-scoped-env' import { isPathInsideOrEqual, normalizeRuntimePathForComparison @@ -654,6 +655,7 @@ export class PtyHandler { }, rendererEnv ) as Record + removeUnspecifiedPtyChildScopedEnv(baseEnv, rendererEnv) const augmented: Record = {} for (const augmenter of this.envAugmenters) { try { diff --git a/src/shared/pty-child-scoped-env.ts b/src/shared/pty-child-scoped-env.ts new file mode 100644 index 00000000000..8e540efb364 --- /dev/null +++ b/src/shared/pty-child-scoped-env.ts @@ -0,0 +1,24 @@ +import { + SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV, + SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV +} from './setup-agent-sequencing' + +const PTY_CHILD_SCOPED_ENV_KEYS = [ + 'ORCA_PANE_KEY', + 'ORCA_TAB_ID', + 'ORCA_WORKTREE_ID', + 'ORCA_AGENT_LAUNCH_TOKEN', + SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV, + SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV +] as const + +export function removeUnspecifiedPtyChildScopedEnv( + env: Record, + explicitEnv: Record | undefined +): void { + for (const key of PTY_CHILD_SCOPED_ENV_KEYS) { + if (!explicitEnv || !Object.hasOwn(explicitEnv, key)) { + delete env[key] + } + } +}