Fix inherited PTY setup launch state

This commit is contained in:
Brennan Benson
2026-08-28 02:46:57 -07:00
parent 65dd06a870
commit dea567e43c
10 changed files with 146 additions and 71 deletions
@@ -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([
@@ -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<string, string>,
explicitEnv: Record<string, string> | 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<string, string>,
@@ -146,7 +130,7 @@ export function createDaemonPtyEnvironment(opts: PtySubprocessOptions): Record<s
if (opts.env?.TERM) {
env.TERM = opts.env.TERM
}
removeUnspecifiedPaneIdentityEnv(env, opts.env)
removeUnspecifiedPtyChildScopedEnv(env, opts.env)
if (opts.env?.fish_history === undefined) {
dropInheritedOrcaFishHistory(env)
}
@@ -461,6 +461,8 @@ describe('registerPtyHandlers', () => {
})
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',
@@ -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<string, string>,
explicitEnv: Record<string, string> | undefined
): void {
for (const key of PANE_IDENTITY_ENV_KEYS) {
if (!explicitEnv || !Object.hasOwn(explicitEnv, key)) {
delete env[key]
}
}
}
export function promoteAgentTeamsShimPath(
env: Record<string, string>,
requestedPath: string | undefined
@@ -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')
})
})
})
+3 -3
View File
@@ -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<string, string>
// 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 ?? []) {
+24 -22
View File
@@ -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<string, string> }
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', {
@@ -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<string, string> }
const explicit = mockPtySpawn.mock.calls[1][2] as { env: Record<string, string> }
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',
+2
View File
@@ -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<string, string>
removeUnspecifiedPtyChildScopedEnv(baseEnv, rendererEnv)
const augmented: Record<string, string> = {}
for (const augmenter of this.envAugmenters) {
try {
+24
View File
@@ -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<string, string>,
explicitEnv: Record<string, string> | undefined
): void {
for (const key of PTY_CHILD_SCOPED_ENV_KEYS) {
if (!explicitEnv || !Object.hasOwn(explicitEnv, key)) {
delete env[key]
}
}
}