diff --git a/config/scripts/vitest-node-runtime-files.mjs b/config/scripts/vitest-node-runtime-files.mjs index 96eeefde245..b819a19094c 100644 --- a/config/scripts/vitest-node-runtime-files.mjs +++ b/config/scripts/vitest-node-runtime-files.mjs @@ -94,6 +94,10 @@ const NODE_RUNTIME_INCLUDE = [ 'src/**/*.node-pty.test.ts', 'src/**/node-pty-*.test.ts', 'src/**/*.live-shell.test.ts', + // These drive live zsh through node-pty and rely on Node's native PTY stream delivery. + 'src/main/shell-startup-feature-channel.test.ts', + 'src/main/daemon/shell-ready.test.ts', + 'src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts', 'src/**/*real-pty*.test.ts', 'src/**/*-native.test.ts', 'src/**/*retention*.test.ts', diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/daemon-bash-rcfile.txt b/src/main/__fixtures__/shell-wrapper-snapshots/daemon-bash-rcfile.txt index 39db3318927..3b61d36b65d 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/daemon-bash-rcfile.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/daemon-bash-rcfile.txt @@ -119,6 +119,9 @@ if [[ -n "${__orca_codex_binary:-}" && -x "${__orca_codex_binary}" ]]; then function codex { # Why local: zsh's warn_create_global warns for each global a function creates. local __orca_codex_arg __orca_codex_isolate="${ORCA_CODEX_ISOLATE:-1}" __orca_codex_identity="${ORCA_CODEX_ISOLATE:-1}" + if [[ "${ORCA_REMOTE_VIEWER_CLIPBOARD:-}" == 1 && -z "${SSH_CONNECTION+x}" && -z "${SSH_TTY+x}" ]]; then + local -x SSH_CONNECTION='127.0.0.1 0 127.0.0.1 0' + fi if [[ -n "${ORCA_CODEX_LAUNCH_PREFLIGHT:-}" && -x "${ORCA_CODEX_LAUNCH_PREFLIGHT}" ]]; then "${ORCA_CODEX_LAUNCH_PREFLIGHT}" agent hooks prepare-codex >/dev/null 2>&1 || : fi diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/daemon-fish-init.txt b/src/main/__fixtures__/shell-wrapper-snapshots/daemon-fish-init.txt index 404ffd4ccea..7c53db56ee8 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/daemon-fish-init.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/daemon-fish-init.txt @@ -21,7 +21,12 @@ if test "$__orca_codex_type" = file if test "$ORCA_CODEX_ISOLATE" != 0; and not string match -qr -- '^(agents|queue|--remote|--no-daemon|--remote=.*)$' $argv; and command codex --help 2>/dev/null /dev/null 2>&1 || : fi diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/local-bash-rcfile.txt b/src/main/__fixtures__/shell-wrapper-snapshots/local-bash-rcfile.txt index 7bc19fd874d..0fee1d58a0b 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/local-bash-rcfile.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/local-bash-rcfile.txt @@ -127,6 +127,9 @@ if [[ -n "${__orca_codex_binary:-}" && -x "${__orca_codex_binary}" ]]; then function codex { # Why local: zsh's warn_create_global warns for each global a function creates. local __orca_codex_arg __orca_codex_isolate="${ORCA_CODEX_ISOLATE:-1}" __orca_codex_identity="${ORCA_CODEX_ISOLATE:-1}" + if [[ "${ORCA_REMOTE_VIEWER_CLIPBOARD:-}" == 1 && -z "${SSH_CONNECTION+x}" && -z "${SSH_TTY+x}" ]]; then + local -x SSH_CONNECTION='127.0.0.1 0 127.0.0.1 0' + fi if [[ -n "${ORCA_CODEX_LAUNCH_PREFLIGHT:-}" && -x "${ORCA_CODEX_LAUNCH_PREFLIGHT}" ]]; then "${ORCA_CODEX_LAUNCH_PREFLIGHT}" agent hooks prepare-codex >/dev/null 2>&1 || : fi diff --git a/src/main/__fixtures__/shell-wrapper-snapshots/local-fish-init.txt b/src/main/__fixtures__/shell-wrapper-snapshots/local-fish-init.txt index 404ffd4ccea..7c53db56ee8 100644 --- a/src/main/__fixtures__/shell-wrapper-snapshots/local-fish-init.txt +++ b/src/main/__fixtures__/shell-wrapper-snapshots/local-fish-init.txt @@ -21,7 +21,12 @@ if test "$__orca_codex_type" = file if test "$ORCA_CODEX_ISOLATE" != 0; and not string match -qr -- '^(agents|queue|--remote|--no-daemon|--remote=.*)$' $argv; and command codex --help 2>/dev/null /dev/null 2>&1 || : fi diff --git a/src/main/daemon/pty-subprocess-env-inheritance.test.ts b/src/main/daemon/pty-subprocess-env-inheritance.test.ts index 8c6c096f628..4d80d1ca595 100644 --- a/src/main/daemon/pty-subprocess-env-inheritance.test.ts +++ b/src/main/daemon/pty-subprocess-env-inheritance.test.ts @@ -128,9 +128,11 @@ describe('createPtySubprocess', () => { ORCA_TAB_ID: process.env.ORCA_TAB_ID, ORCA_WORKTREE_ID: process.env.ORCA_WORKTREE_ID, ORCA_WSL_CLI_DIR: process.env.ORCA_WSL_CLI_DIR, - ORCA_ORCAD_MANAGED_ACTIVATION_ROOT: process.env.ORCA_ORCAD_MANAGED_ACTIVATION_ROOT + ORCA_ORCAD_MANAGED_ACTIVATION_ROOT: process.env.ORCA_ORCAD_MANAGED_ACTIVATION_ROOT, + ORCA_REMOTE_VIEWER_CLIPBOARD: process.env.ORCA_REMOTE_VIEWER_CLIPBOARD } process.env.ORCA_ORCAD_MANAGED_ACTIVATION_ROOT = '/home/u/.orca-remote/.activation' + process.env.ORCA_REMOTE_VIEWER_CLIPBOARD = '1' process.env.ORCA_PANE_KEY = 'parent-tab:parent-leaf' process.env.ORCA_TAB_ID = 'parent-tab' process.env.ORCA_WORKTREE_ID = 'parent-worktree' @@ -154,6 +156,7 @@ describe('createPtySubprocess', () => { expect(env.ORCA_WORKTREE_ID).toBeUndefined() expect(env.ORCA_WSL_CLI_DIR).toBeUndefined() expect(env.ORCA_ORCAD_MANAGED_ACTIVATION_ROOT).toBeUndefined() + expect(env.ORCA_REMOTE_VIEWER_CLIPBOARD).toBeUndefined() }) it('preserves explicit child Orca pane identity over parent env', async () => { diff --git a/src/main/daemon/pty-subprocess/spawn-environment.ts b/src/main/daemon/pty-subprocess/spawn-environment.ts index 6c3675b73ad..8b6051b71e1 100644 --- a/src/main/daemon/pty-subprocess/spawn-environment.ts +++ b/src/main/daemon/pty-subprocess/spawn-environment.ts @@ -29,6 +29,7 @@ import { applyScrubSafeAgentEnvAliases } from '../../../shared/agent-hook-scrub- import type { TuiAgent } from '../../../shared/tui-agent' import type { PtySubprocessOptions } from '../pty-subprocess' import { ORCAD_MANAGED_ACTIVATION_ROOT_ENV } from '../../../shared/orcad-managed-launch-env' +import { REMOTE_VIEWER_CLIPBOARD_ENV } from '../../../shared/remote-viewer-clipboard-env' const PANE_IDENTITY_ENV_KEYS = [ 'ORCA_PANE_KEY', @@ -38,6 +39,8 @@ const PANE_IDENTITY_ENV_KEYS = [ // Not identity but equally per-spawn: an inherited copy names another launch's CLI. 'ORCA_WSL_CLI_DIR', 'JCODE_RUNTIME_DIR', + // Per-viewer: an Orca started from a remote viewer's pane must not mark its own local panes. + REMOTE_VIEWER_CLIPBOARD_ENV, // A server's own launch marker; a user's `orcad` in this shell is not client-managed. ORCAD_MANAGED_ACTIVATION_ROOT_ENV ] as const diff --git a/src/main/ipc/pty/host-env/remote-viewer-clipboard-env.ts b/src/main/ipc/pty/host-env/remote-viewer-clipboard-env.ts index 072b0cfc308..71a96380f39 100644 --- a/src/main/ipc/pty/host-env/remote-viewer-clipboard-env.ts +++ b/src/main/ipc/pty/host-env/remote-viewer-clipboard-env.ts @@ -1,7 +1,9 @@ import type { TuiAgent } from '../../../../shared/tui-agent' - -/** Codex v0.162.1 emits OSC 52 under SSH/TMUX; a valid SSH_CONNECTION avoids tmux subprocess/passthrough and SSH_TTY device semantics. */ -export const CODEX_REMOTE_VIEWER_SSH_CONNECTION = '127.0.0.1 0 127.0.0.1 0' +import { + CODEX_REMOTE_VIEWER_SSH_CONNECTION, + REMOTE_VIEWER_CLIPBOARD_ENV +} from '../../../../shared/remote-viewer-clipboard-env' +export { CODEX_REMOTE_VIEWER_SSH_CONNECTION } /** Pi's own opt-in to emit OSC 52 alongside its host clipboard write; inert for every other program. */ export const PI_CLIPBOARD_OSC52_ENV = 'PI_CLIPBOARD_OSC52' @@ -18,6 +20,7 @@ export function applyRemoteViewerClipboardEnv( if (!options.viewerIsRemote) { return } + env[REMOTE_VIEWER_CLIPBOARD_ENV] = '1' const inherited = options.inheritedEnv ?? process.env // Daemons merge their inherited environment after assembly; respect existing values there too. const isSet = (key: string): boolean => env[key] !== undefined || inherited[key] !== undefined diff --git a/src/main/ipc/pty/pty-spawn-env-terminal-basics.test.ts b/src/main/ipc/pty/pty-spawn-env-terminal-basics.test.ts index 0b8ee99d744..47ddffb99fd 100644 --- a/src/main/ipc/pty/pty-spawn-env-terminal-basics.test.ts +++ b/src/main/ipc/pty/pty-spawn-env-terminal-basics.test.ts @@ -19,6 +19,7 @@ import { } from '../../../shared/jcode-runtime-dir' import { makePaneKey } from '../../../shared/stable-pane-id' import { selectShellStartupFeatures } from '../../shell-startup-features' +import { REMOTE_VIEWER_CLIPBOARD_ENV } from '../../../shared/remote-viewer-clipboard-env' import { CODEX_REMOTE_VIEWER_SSH_CONNECTION, PI_CLIPBOARD_OSC52_ENV @@ -299,8 +300,7 @@ describe('registerPtyHandlers', () => { } ) - // #33937: pi started by hand in a headless host's shell copied only to the host clipboard. - it('asks a hand-started pi for OSC 52 copies in a plain shell', () => { + it('installs clipboard forwarding for agents started by hand in a remote shell', () => { vi.stubEnv(PI_CLIPBOARD_OSC52_ENV, undefined) try { const env = buildPtyHostEnv( @@ -315,7 +315,17 @@ describe('registerPtyHandlers', () => { } ) expect(env[PI_CLIPBOARD_OSC52_ENV]).toBe('1') + expect(env[REMOTE_VIEWER_CLIPBOARD_ENV]).toBe('1') expect(env.SSH_CONNECTION).toBeUndefined() + expect( + selectShellStartupFeatures({ + shellPath: '/bin/bash', + env, + hasStartupCommand: false, + waitsForShellReady: false, + emitsStartupIdentity: false + }) + ).toContain('overlay') } finally { vi.unstubAllEnvs() } diff --git a/src/main/providers/local-pty-default-shell.test.ts b/src/main/providers/local-pty-default-shell.test.ts index 8071813b183..0e46de28a41 100644 --- a/src/main/providers/local-pty-default-shell.test.ts +++ b/src/main/providers/local-pty-default-shell.test.ts @@ -1,4 +1,10 @@ import { afterEach, describe, expect, it, vi } from 'vitest' +import { spawnSync } from 'node:child_process' +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { REMOTE_VIEWER_CLIPBOARD_ENV } from '../../shared/remote-viewer-clipboard-env' +import { finalizeLocalPtySpawnEnvironment } from './local-pty-finalize-environment' import { createLocalPtyLaunchPlan } from './local-pty-launch-plan' vi.mock('./local-pty-utils', () => ({ @@ -65,17 +71,72 @@ describe.skipIf(process.platform === 'win32')('default terminal shell', () => { it.each([{ command: 'codex' }, { launchAgent: 'codex' as const }])( 'keeps controlled login args for non-profile launches (%o)', (overrides) => { - const plan = createLocalPtyLaunchPlan( - { - cwd: '/tmp', - cols: 80, - rows: 24, - terminalShellArgs: ['--rcfile', '/tmp/orca'], - ...overrides - }, - () => ({ getDefaultShell: () => '/bin/zsh' }) - ) - expect('shellArgs' in plan && plan.shellArgs).toEqual(['-l']) + const spawn = { + cwd: '/tmp', + cols: 80, + rows: 24, + terminalShellArgs: ['--rcfile', '/tmp/orca'], + ...overrides + } + const getOptions = () => ({ getDefaultShell: () => '/bin/zsh' }) + const plan = createLocalPtyLaunchPlan(spawn, getOptions) + if (!('shellArgs' in plan)) { + throw new Error('Expected a POSIX shell launch plan') + } + finalizeLocalPtySpawnEnvironment({ + spawn, + getOptions, + plan, + env: { [REMOTE_VIEWER_CLIPBOARD_ENV]: '1' } + }) + expect(plan.shellArgs).toEqual(['-l']) + } + ) + + it.for([ + { shell: '/bin/bash', shellArgs: ['--rcfile', 'custom rc'], expected: 'custom:off' }, + { shell: '/bin/zsh', shellArgs: ['-f'], expected: 'unset:off' }, + { shell: '/bin/zsh', shellArgs: [], expected: 'zshrc:off' } + ])( + 'honors remote profile startup behavior for $shell $shellArgs', + ({ shell, shellArgs, expected }, { skip }) => { + if (spawnSync(shell, ['--version']).status !== 0) { + skip('Shell is not installed') + } + const home = mkdtempSync(join(tmpdir(), 'orca-profile-argv-')) + vi.stubEnv('ORCA_USER_DATA_PATH', home) + vi.stubEnv('ZDOTDIR', home) + try { + writeFileSync(join(home, '.bash_profile'), 'export ORCA_TEST_STARTUP=profile\n') + writeFileSync(join(home, 'custom rc'), 'export ORCA_TEST_STARTUP=custom\n') + writeFileSync(join(home, '.zshrc'), 'export ORCA_TEST_STARTUP=zshrc\n') + const terminalShellArgs = shellArgs.map((arg) => + arg === 'custom rc' ? join(home, arg) : arg + ) + const spawn = { cwd: home, cols: 80, rows: 24, terminalShellArgs } + const getOptions = () => ({ getDefaultShell: () => shell }) + const plan = createLocalPtyLaunchPlan(spawn, getOptions) + if (!('shellArgs' in plan)) { + throw new Error('Expected a POSIX shell launch plan') + } + const env = { HOME: home, PATH: '/usr/bin:/bin', [REMOTE_VIEWER_CLIPBOARD_ENV]: '1' } + finalizeLocalPtySpawnEnvironment({ spawn, getOptions, plan, env }) + + const result = spawnSync( + plan.shellPath, + [ + ...plan.shellArgs, + '-i', + '-c', + 'login=off; if [ -n "${ZSH_VERSION:-}" ]; then [[ -o login ]] && login=on; else shopt -q login_shell && login=on; fi; printf "\\nORCA_STARTUP_RESULT=%s:%s\\n" "${ORCA_TEST_STARTUP:-unset}" "$login"' + ], + { env, encoding: 'utf8', timeout: 10_000 } + ) + expect(result.status, result.stderr).toBe(0) + expect(result.stdout.match(/^ORCA_STARTUP_RESULT=(.*)$/m)?.[1]).toBe(expected) + } finally { + rmSync(home, { recursive: true, force: true }) + } } ) }) diff --git a/src/main/providers/local-pty-finalize-environment.ts b/src/main/providers/local-pty-finalize-environment.ts index e0986c84794..d8669868761 100644 --- a/src/main/providers/local-pty-finalize-environment.ts +++ b/src/main/providers/local-pty-finalize-environment.ts @@ -98,6 +98,7 @@ export function finalizeLocalPtySpawnEnvironment(args: { // Only this shell launch config may choose Orca-owned features. delete env.ORCA_SHELL_FEATURES delete env[POSIX_SHELL_STARTUP_COMMAND_ENV] + const userShellArgs = !spawn.command && !spawn.launchAgent ? spawn.terminalShellArgs : undefined // Capture before launch config prefixes XDG_DATA_DIRS. const fishLaunch = { inheritedXdgDataDirs: env.XDG_DATA_DIRS, @@ -130,7 +131,7 @@ export function finalizeLocalPtySpawnEnvironment(args: { Object.keys(shellLaunch.env).map((key) => [key, env[key]]) ) Object.assign(env, shellLaunch.env) - plan.shellArgs = shellLaunch.args ?? plan.shellArgs + plan.shellArgs = userShellArgs ?? shellLaunch.args ?? plan.shellArgs plan.shellReadyLaunch = spawn.command ? shellLaunch : null } return historyResult diff --git a/src/main/providers/local-pty-launch-helpers.ts b/src/main/providers/local-pty-launch-helpers.ts index 0d02bd7062f..3b4883f0f54 100644 --- a/src/main/providers/local-pty-launch-helpers.ts +++ b/src/main/providers/local-pty-launch-helpers.ts @@ -5,6 +5,7 @@ import { resolvePathEnvKey } from '../pty/windows-environment-path' import { expandWindowsEnvironmentVariables } from '../../shared/windows-environment-expansion' import { resolveSafePtyDefaultCwd } from './pty-default-cwd' import { ORCAD_MANAGED_ACTIVATION_ROOT_ENV } from '../../shared/orcad-managed-launch-env' +import { REMOTE_VIEWER_CLIPBOARD_ENV } from '../../shared/remote-viewer-clipboard-env' const PANE_IDENTITY_ENV_KEYS = [ 'ORCA_PANE_KEY', @@ -12,6 +13,8 @@ const PANE_IDENTITY_ENV_KEYS = [ 'ORCA_WORKTREE_ID', 'ORCA_AGENT_LAUNCH_TOKEN', 'JCODE_RUNTIME_DIR', + // Per-viewer: an Orca started from a remote viewer's pane must not mark its own local panes. + REMOTE_VIEWER_CLIPBOARD_ENV, // A server's own launch marker; a user's `orcad` in this shell is not client-managed. ORCAD_MANAGED_ACTIVATION_ROOT_ENV ] as const 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 999c37fa2e6..457d9fd83d7 100644 --- a/src/main/providers/local-pty-provider-spawn-env.test.ts +++ b/src/main/providers/local-pty-provider-spawn-env.test.ts @@ -566,9 +566,11 @@ describe('LocalPtyProvider', () => { 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_ORCAD_MANAGED_ACTIVATION_ROOT: process.env.ORCA_ORCAD_MANAGED_ACTIVATION_ROOT + ORCA_ORCAD_MANAGED_ACTIVATION_ROOT: process.env.ORCA_ORCAD_MANAGED_ACTIVATION_ROOT, + ORCA_REMOTE_VIEWER_CLIPBOARD: process.env.ORCA_REMOTE_VIEWER_CLIPBOARD } process.env.ORCA_ORCAD_MANAGED_ACTIVATION_ROOT = '/home/u/.orca-remote/.activation' + process.env.ORCA_REMOTE_VIEWER_CLIPBOARD = '1' process.env.ORCA_PANE_KEY = 'parent-tab:parent-leaf' process.env.ORCA_TAB_ID = 'parent-tab' process.env.ORCA_WORKTREE_ID = 'parent-worktree' @@ -590,6 +592,7 @@ describe('LocalPtyProvider', () => { expect(spawnCall[2].env.ORCA_TAB_ID).toBeUndefined() expect(spawnCall[2].env.ORCA_WORKTREE_ID).toBeUndefined() expect(spawnCall[2].env.ORCA_ORCAD_MANAGED_ACTIVATION_ROOT).toBeUndefined() + expect(spawnCall[2].env.ORCA_REMOTE_VIEWER_CLIPBOARD).toBeUndefined() }) it('preserves explicit child Orca pane identity over parent env', async () => { diff --git a/src/main/pty/codex-shell-no-daemon.test.ts b/src/main/pty/codex-shell-no-daemon.test.ts index c01f576b260..9eb96e129ec 100644 --- a/src/main/pty/codex-shell-no-daemon.test.ts +++ b/src/main/pty/codex-shell-no-daemon.test.ts @@ -18,6 +18,10 @@ import { } from '../../shared/codex-shell-function' import { resolveFishBinary } from '../../shared/fish-binary-requirement' import { getPowerShellAgentWrappers } from '../../shared/powershell-agent-wrappers' +import { + CODEX_REMOTE_VIEWER_SSH_CONNECTION, + REMOTE_VIEWER_CLIPBOARD_ENV +} from '../../shared/remote-viewer-clipboard-env' const isWindows = process.platform === 'win32' const fishLookup = resolveFishBinary() @@ -109,6 +113,7 @@ if (a.length === 1 && a[0] === '--help') { process.exit(0) } let out = ['ARGV', ...a].join('|') +if (process.env.FAKE_CODEX_READ_CLIPBOARD_ENV === '1') out += '|ssh=' + (process.env.SSH_CONNECTION ?? '') if (process.env.FAKE_CODEX_READ_STDIN === '1') out += '|stdin=' + fs.readFileSync(0, 'utf8').trim() console.log(out) process.exit(Number(process.env.FAKE_CODEX_EXIT || 0)) @@ -125,6 +130,7 @@ process.exit(Number(process.env.FAKE_CODEX_EXIT || 0)) if [ "$#" -eq 1 ] && [ "$1" = --help ]; then echo help >> ${JSON.stringify(helpLog)}; cat ${JSON.stringify(helpFile)}; exit 0; fi out=ARGV for a in "$@"; do out="$out|$a"; done +[ "\${FAKE_CODEX_READ_CLIPBOARD_ENV:-}" = 1 ] && out="$out|ssh=\${SSH_CONNECTION-}" [ "\${FAKE_CODEX_READ_STDIN:-}" = 1 ] && out="$out|stdin=$(cat)" printf '%s\\n' "$out" exit "\${FAKE_CODEX_EXIT:-0}" @@ -190,6 +196,9 @@ function run( CODEX_HOME: join(sandbox.bin, '..', 'codex-home'), // Nested Orca tests must not forward the host pane's identity. ...Object.fromEntries(CODEX_COMMAND_IDENTITY_ENV_KEYS.map((key) => [key, ''])), + SSH_CONNECTION: undefined, + SSH_TTY: undefined, + [REMOTE_VIEWER_CLIPBOARD_ENV]: undefined, ...env } }) @@ -220,6 +229,37 @@ describe('codex wrapper --no-daemon rule', () => { for (const [shell, available] of shells) { describe.skipIf(!available)(shell, () => { + it.each( + [ + ['1', undefined, undefined, CODEX_REMOTE_VIEWER_SSH_CONNECTION, '0'], + ['1', undefined, undefined, CODEX_REMOTE_VIEWER_SSH_CONNECTION, '3'], + ['0', undefined, undefined, '', '0'], + ['1', '', undefined, '', '0'], + ['1', '10.0.0.1 5000 10.0.0.2 22', undefined, '10.0.0.1 5000 10.0.0.2 22', '0'], + ['1', undefined, '/dev/ttys004', '', '0'] + ].filter(([, inherited]) => !isPowerShell(shell) || inherited !== '') + )( + 'routes manual copies for remote=%s without marking the parent shell', + (remote, inherited, inheritedTty, expected, exitCode) => { + const sandbox = makeSandbox(HELP_WITH_FLAG) + const bareCall = isPowerShell(shell) + ? '$orcaClipboardExit = $LASTEXITCODE\n& (Get-Command codex -CommandType Application | Select-Object -First 1).Source shell\nexit $orcaClipboardExit' + : 'command codex shell' + const result = run(shell, `codex clipboard\n${bareCall}`, sandbox, { + FAKE_CODEX_READ_CLIPBOARD_ENV: '1', + FAKE_CODEX_EXIT: exitCode ?? '0', + [REMOTE_VIEWER_CLIPBOARD_ENV]: remote ?? '0', + ...(inherited !== undefined ? { SSH_CONNECTION: inherited } : {}), + ...(inheritedTty ? { SSH_TTY: inheritedTty } : {}) + }) + expect(result.status, result.stderr).toBe(Number(exitCode)) + expect(lines(result.stdout)).toEqual([ + `ARGV|--no-daemon|clipboard|ssh=${expected}`, + `ARGV|shell|ssh=${inherited ?? ''}` + ]) + } + ) + it('adds --no-daemon first unless an argument is denylisted', () => { const sandbox = makeSandbox(HELP_WITH_FLAG) const result = run(shell, ALL.map((argv) => codexCall(shell, argv)).join('\n'), sandbox) diff --git a/src/main/pty/wsl-orca-env.test.ts b/src/main/pty/wsl-orca-env.test.ts index 7f2e8ceadf4..c1f3f25c8d0 100644 --- a/src/main/pty/wsl-orca-env.test.ts +++ b/src/main/pty/wsl-orca-env.test.ts @@ -245,9 +245,13 @@ describe('addOrcaWslInteropEnv', () => { // #10861: wsl.exe drops SSH_CONNECTION, so WSL Codex copied only to the Windows clipboard. it('crosses the Codex remote-viewer clipboard marker into the guest untranslated (/u)', () => { - const env: Record = { SSH_CONNECTION: CODEX_REMOTE_VIEWER_SSH_CONNECTION } + const env: Record = { + SSH_CONNECTION: CODEX_REMOTE_VIEWER_SSH_CONNECTION, + ORCA_REMOTE_VIEWER_CLIPBOARD: '1' + } addOrcaWslInteropEnv(env) expect(env.WSLENV?.split(':')).toContain('SSH_CONNECTION/u') + expect(env.WSLENV?.split(':')).toContain('ORCA_REMOTE_VIEWER_CLIPBOARD/u') }) it("leaves the Windows host's own SSH session out of the guest", () => { diff --git a/src/main/pty/wsl-orca-env.ts b/src/main/pty/wsl-orca-env.ts index 715e4371ce6..c3812a767da 100644 --- a/src/main/pty/wsl-orca-env.ts +++ b/src/main/pty/wsl-orca-env.ts @@ -15,6 +15,7 @@ import { } from '../../shared/setup-agent-sequencing' import { getShellReadyWrapperRoot } from '../providers/local-pty-shell-ready-wrapper-root' import { ORCA_IMAGE_PROTOCOL_ENV } from '../../shared/terminal-image-protocol' +import { REMOTE_VIEWER_CLIPBOARD_ENV } from '../../shared/remote-viewer-clipboard-env' import { RUNTIME_SOURCE_ID_ENV, RUNTIME_SOURCE_INCARNATION_ENV, @@ -127,6 +128,7 @@ export function addOrcaWslInteropEnv(env: Record): void { // Forward only Orca's clipboard marker; a host SSH session must not leak into bare guest shells. ...(env.SSH_CONNECTION === CODEX_REMOTE_VIEWER_SSH_CONNECTION ? ['SSH_CONNECTION/u'] : []), `${PI_CLIPBOARD_OSC52_ENV}/u`, + `${REMOTE_VIEWER_CLIPBOARD_ENV}/u`, 'ORCA_OMP_FRESH_CONFIG/p', ...worktreeSetupWslenvEntries(env) ] diff --git a/src/main/shell-startup-features.ts b/src/main/shell-startup-features.ts index 64040f1d662..e42dfad615d 100644 --- a/src/main/shell-startup-features.ts +++ b/src/main/shell-startup-features.ts @@ -1,3 +1,5 @@ +import { REMOTE_VIEWER_CLIPBOARD_ENV } from '../shared/remote-viewer-clipboard-env' + // Wrappers consume the feature env before user config so children cannot inherit the selection. export const SHELL_STARTUP_FEATURE_ENV = 'ORCA_SHELL_FEATURES' @@ -14,6 +16,7 @@ export const SHELL_STARTUP_FEATURES = [ export type ShellStartupFeature = (typeof SHELL_STARTUP_FEATURES)[number] const OVERLAY_ENV_KEYS = [ + REMOTE_VIEWER_CLIPBOARD_ENV, 'ORCA_DATA_ACCOUNT_DATA_HOME', 'ORCA_OPENCODE_CONFIG_DIR', 'ORCA_MIMOCODE_CONFIG_DIR', diff --git a/src/shared/codex-shell-function.ts b/src/shared/codex-shell-function.ts index 00b56d36ea5..dc344821405 100644 --- a/src/shared/codex-shell-function.ts +++ b/src/shared/codex-shell-function.ts @@ -1,3 +1,8 @@ +import { + CODEX_REMOTE_VIEWER_SSH_CONNECTION, + REMOTE_VIEWER_CLIPBOARD_ENV +} from './remote-viewer-clipboard-env' + // Embed launch-prep/--no-daemon wrappers on all hosts. Codex 0.156+ otherwise shares CODEX_HOME servers with the first tab’s Orca hooks/env and lifetime (#22873). agents/queue/--remote require sharing; any -c opts out. const CODEX_SHARED_SERVER_ARGS = ['agents', 'queue', '--remote'] as const export const CODEX_NO_DAEMON_SKIP_ARGS = [...CODEX_SHARED_SERVER_ARGS, '--no-daemon'] as const @@ -31,6 +36,9 @@ if [[ -n "\${__orca_codex_binary:-}" && -x "\${__orca_codex_binary}" ]]; then function codex { # Why local: zsh's warn_create_global warns for each global a function creates. local __orca_codex_arg __orca_codex_isolate="\${ORCA_CODEX_ISOLATE:-1}" __orca_codex_identity="\${ORCA_CODEX_ISOLATE:-1}" + if [[ "\${${REMOTE_VIEWER_CLIPBOARD_ENV}:-}" == 1 && -z "\${SSH_CONNECTION+x}" && -z "\${SSH_TTY+x}" ]]; then + local -x SSH_CONNECTION='${CODEX_REMOTE_VIEWER_SSH_CONNECTION}' + fi if [[ -n "\${ORCA_CODEX_LAUNCH_PREFLIGHT:-}" && -x "\${ORCA_CODEX_LAUNCH_PREFLIGHT}" ]]; then "\${ORCA_CODEX_LAUNCH_PREFLIGHT}" agent hooks prepare-codex >/dev/null 2>&1 || : fi @@ -70,7 +78,12 @@ ${CODEX_COMMAND_IDENTITY_ENV_KEYS.map((key) => ` test -n "$${key}"; and not if test "$ORCA_CODEX_ISOLATE" != 0; and not string match -qr -- '${CODEX_NO_DAEMON_SKIP_ARG_PATTERN}' $argv; and command codex --help 2>/dev/null `'${key}'`).join(',')}) { $v = [Environment]::GetEnvironmentVariable($k); if ($v -and -not $v.Contains("'")) { $orcaCodexIdentity += '-c', "${identitySetArg('$k', '$v')}" } } } $orcaCodexArgs = @(__OrcaEscapeNativeArgs $orcaCodexExecutable.Source (@($orcaCodexIdentity) + @($args))) + $orcaCodexRemoteClipboard = $env:${REMOTE_VIEWER_CLIPBOARD_ENV} -eq '1' -and -not (Test-Path Env:SSH_CONNECTION) -and -not (Test-Path Env:SSH_TTY) + if ($orcaCodexRemoteClipboard) { $env:SSH_CONNECTION = '${CODEX_REMOTE_VIEWER_SSH_CONNECTION}' } # Why: a native command inside a function never sees the function's pipeline input on its own. - if ($MyInvocation.ExpectingInput) { - $input | & $orcaCodexExecutable.Source @orcaCodexFlags @orcaCodexArgs - } else { - & $orcaCodexExecutable.Source @orcaCodexFlags @orcaCodexArgs + try { + if ($MyInvocation.ExpectingInput) { + $input | & $orcaCodexExecutable.Source @orcaCodexFlags @orcaCodexArgs + } else { + & $orcaCodexExecutable.Source @orcaCodexFlags @orcaCodexArgs + } + } finally { + if ($orcaCodexRemoteClipboard) { Remove-Item Env:SSH_CONNECTION } } $global:LASTEXITCODE = $LASTEXITCODE } diff --git a/src/shared/remote-viewer-clipboard-env.ts b/src/shared/remote-viewer-clipboard-env.ts new file mode 100644 index 00000000000..92d744d04fb --- /dev/null +++ b/src/shared/remote-viewer-clipboard-env.ts @@ -0,0 +1,4 @@ +export const REMOTE_VIEWER_CLIPBOARD_ENV = 'ORCA_REMOTE_VIEWER_CLIPBOARD' + +/** Codex forwards copies through OSC 52 under SSH; keep this marker out of the parent shell. */ +export const CODEX_REMOTE_VIEWER_SSH_CONNECTION = '127.0.0.1 0 127.0.0.1 0'