fix(codex): copy from remote-runtime Codex sessions reaches the viewer's clipboard (#27605)

Orca-launched Codex on a headless runtime gets SSH_CONNECTION so Codex sends
OSC 52 to the viewing terminal instead of only the host's clipboard.

Fixes #23530

Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
OrcaWin
2026-10-11 15:31:04 -07:00
committed by GitHub
co-authored by m4air
parent aa1eca1d11
commit 5e9fb931a1
4 changed files with 133 additions and 0 deletions
@@ -11,6 +11,7 @@ import { registerPtyHandlers, buildPtyHostEnv } from './pty'
import { buildJcodeRuntimeDir, shouldInjectJcodeRuntimeDir } from '../../shared/jcode-runtime-dir'
import { makePaneKey } from '../../shared/stable-pane-id'
import { selectShellStartupFeatures } from '../shell-startup-features'
import { CODEX_REMOTE_VIEWER_SSH_CONNECTION } from './pty/host-env/codex-terminal-clipboard'
vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock()))
vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock()))
@@ -229,6 +230,40 @@ describe('registerPtyHandlers', () => {
}
})
describe('Codex terminal clipboard on headless runtimes (#23530)', () => {
const buildEnv = (launchAgent: 'codex' | 'claude', routeBrowserOpensToClient: boolean) =>
buildPtyHostEnv(
'pty-codex-clipboard',
{},
{
isPackaged: true,
userDataPath: '/tmp/orca-user-data',
selectedCodexHomePath: null,
agentStatusHooksEnabled: false,
launchCommand: launchAgent,
launchAgent,
routeBrowserOpensToClient
}
)
it.each([
['codex', true, CODEX_REMOTE_VIEWER_SSH_CONNECTION],
['codex', false, undefined],
['claude', true, undefined]
] as const)(
'launching %s with a remote viewer=%s sets SSH_CONNECTION=%s',
(launchAgent, remoteViewer, expected) => {
vi.stubEnv('SSH_CONNECTION', undefined)
vi.stubEnv('SSH_TTY', undefined)
try {
expect(buildEnv(launchAgent, remoteViewer).SSH_CONNECTION).toBe(expected)
} finally {
vi.unstubAllEnvs()
}
}
)
})
it('passes the PTY-resolved Codex home to the WSL relay lane', () => {
const runtimeHome =
'\\\\wsl.localhost\\Ubuntu\\home\\jin\\.local\\share\\orca\\codex-runtime-home\\home'
+6
View File
@@ -31,6 +31,7 @@ import {
import { AGENT_HOOK_RUNTIME_ENV_KEYS } from './spawn-env-keys'
import { applyManagedDataAccountEnvironment } from '../../../managed-data-accounts/launch-environment'
import { applyOpenCodeStatusPluginEnv, captureOpenCodeSourceConfig } from './opencode-config'
import { applyCodexRemoteViewerClipboardEnv } from './codex-terminal-clipboard'
/**
* Mutates `baseEnv` in place with all host-local PTY env vars and returns it.
@@ -292,6 +293,11 @@ export function buildPtyHostEnv(
const cliCommand = opts.isWsl ? getWslCliCommandName(opts.isPackaged) : 'orca'
baseEnv.BROWSER = `${cliCommand} open-url --url %s`
}
// Why the browser-relay flag: both mean a headless runtime whose viewer is another machine.
applyCodexRemoteViewerClipboardEnv(baseEnv, {
launchAgent: opts.launchAgent,
viewerIsRemote: opts.routeBrowserOpensToClient === true
})
// Why: must run after the prepends above — they re-read PATH from the unscrubbed
// process.env when baseEnv carries none, which is the daemon path's normal shape.
@@ -0,0 +1,56 @@
import { describe, expect, it } from 'vitest'
import {
applyCodexRemoteViewerClipboardEnv,
CODEX_REMOTE_VIEWER_SSH_CONNECTION
} from './codex-terminal-clipboard'
describe('applyCodexRemoteViewerClipboardEnv', () => {
it('marks a Codex launch on a host the viewer is not sitting at', () => {
const env: Record<string, string> = {}
applyCodexRemoteViewerClipboardEnv(env, {
launchAgent: 'codex',
viewerIsRemote: true,
inheritedEnv: {}
})
expect(env.SSH_CONNECTION).toBe(CODEX_REMOTE_VIEWER_SSH_CONNECTION)
})
it('leaves a local Codex launch alone', () => {
const env: Record<string, string> = {}
applyCodexRemoteViewerClipboardEnv(env, {
launchAgent: 'codex',
viewerIsRemote: false,
inheritedEnv: {}
})
expect(env.SSH_CONNECTION).toBeUndefined()
})
it.each([undefined, 'claude'] as const)('leaves a %s launch alone', (launchAgent) => {
const env: Record<string, string> = {}
applyCodexRemoteViewerClipboardEnv(env, { launchAgent, viewerIsRemote: true, inheritedEnv: {} })
expect(env.SSH_CONNECTION).toBeUndefined()
})
it('never overrides an SSH session the launch already carries', () => {
const env: Record<string, string> = { SSH_CONNECTION: '10.0.0.1 5000 10.0.0.2 22' }
applyCodexRemoteViewerClipboardEnv(env, {
launchAgent: 'codex',
viewerIsRemote: true,
inheritedEnv: {}
})
expect(env.SSH_CONNECTION).toBe('10.0.0.1 5000 10.0.0.2 22')
})
it.each([
['SSH_CONNECTION', '10.0.0.1 5000 10.0.0.2 22'],
['SSH_TTY', '/dev/ttys004']
])('skips when the host process already inherits %s', (key, value) => {
const env: Record<string, string> = {}
applyCodexRemoteViewerClipboardEnv(env, {
launchAgent: 'codex',
viewerIsRemote: true,
inheritedEnv: { [key]: value }
})
expect(env.SSH_CONNECTION).toBeUndefined()
})
})
@@ -0,0 +1,36 @@
import type { TuiAgent } from '../../../../shared/tui-agent'
/**
* Codex (rust-v0.162.1 `clipboard_copy.rs`) copies to the clipboard of the machine it
* runs on and sends OSC 52 to the terminal only when SSH_TTY, SSH_CONNECTION or TMUX
* is set. SSH_CONNECTION is the least side-effectful trigger: TMUX makes Codex shell
* out to tmux and wrap OSC 52 in tmux passthrough, and SSH_TTY names a tty device.
* Syntactically valid (client ip/port, server ip/port) for tools that parse it.
*/
export const CODEX_REMOTE_VIEWER_SSH_CONNECTION = '127.0.0.1 0 127.0.0.1 0'
/**
* Makes an Orca-launched Codex copy through the viewer's terminal (OSC 52) when the
* viewer is on another machine, so selection copy and Ctrl+C reach the user's clipboard.
* Scoped to Codex launches: bare shells keep their real SSH posture. (#23530)
*/
export function applyCodexRemoteViewerClipboardEnv(
env: Record<string, string>,
options: {
launchAgent: TuiAgent | undefined
viewerIsRemote: boolean
inheritedEnv?: NodeJS.ProcessEnv
}
): void {
if (options.launchAgent !== 'codex' || !options.viewerIsRemote) {
return
}
const inherited = options.inheritedEnv ?? process.env
// Why process.env too: daemon spawns merge the host's inherited env after this build.
const alreadySsh = ['SSH_CONNECTION', 'SSH_TTY'].some(
(key) => env[key] !== undefined || inherited[key] !== undefined
)
if (!alreadySsh) {
env.SSH_CONNECTION = CODEX_REMOTE_VIEWER_SSH_CONNECTION
}
}