mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
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:
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user