fix(omp): inherit login-shell directory settings for task launches

This commit is contained in:
Neil
2026-09-19 00:33:12 -07:00
parent f960bc767e
commit bb2f1accac
12 changed files with 444 additions and 5 deletions
+4 -1
View File
@@ -160,7 +160,10 @@ export function buildPtyHostEnv(
if (shouldPrepareOmpShadow) {
const ompEnv = piTitlebarExtensionService.buildPtyEnv(id, preexistingOmpAgentDir, 'omp', {
materializeDefaultHome: explicitPiAgentKind === 'omp'
materializeDefaultHome: explicitPiAgentKind === 'omp',
...(baseEnv.PI_CONFIG_DIR !== undefined && !opts.isWsl
? { configDirName: baseEnv.PI_CONFIG_DIR }
: {})
})
Object.assign(baseEnv, ompEnv)
exposePiManagedExtensionEnv(baseEnv, 'omp', ompEnv)
@@ -0,0 +1,77 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { inheritOmpLaunchEnvironment } from './omp-launch-environment'
import { resolveLoginShellEnvironment } from '../../../startup/login-shell-environment'
vi.mock('../../../startup/login-shell-environment', () => ({
resolveLoginShellEnvironment: vi.fn()
}))
beforeEach(() => {
vi.stubGlobal('process', { ...process, platform: 'darwin', env: {} })
vi.mocked(resolveLoginShellEnvironment).mockReset().mockResolvedValue({
XDG_DATA_HOME: '/login/data',
XDG_STATE_HOME: '/login/state',
XDG_CACHE_HOME: '/login/cache',
PI_CONFIG_DIR: '.config/omp',
PI_CODING_AGENT_DIR: '/pi/override',
UNRELATED: 'ignore'
})
})
afterEach(() => vi.unstubAllGlobals())
describe('OMP launch directory environment', () => {
it.each([{ launchAgent: 'omp' }, { launchCommand: 'omp' }, {}])(
'inherits login-shell category roots for %j',
async (options) => {
const env = {}
await inheritOmpLaunchEnvironment(env, options)
expect(env).toEqual({
XDG_DATA_HOME: '/login/data',
XDG_STATE_HOME: '/login/state',
XDG_CACHE_HOME: '/login/cache',
PI_CONFIG_DIR: '.config/omp'
})
}
)
it('uses the same login roots for merged local env and daemon pane deltas', async () => {
const local = { XDG_DATA_HOME: '/old/data', PI_CONFIG_DIR: '.old-omp' }
const daemon = {}
await inheritOmpLaunchEnvironment(local, { launchAgent: 'omp', explicitEnv: {} })
await inheritOmpLaunchEnvironment(daemon, { launchAgent: 'omp' })
expect(local).toEqual(daemon)
expect(local.XDG_DATA_HOME).toBe('/login/data')
const overridden = { XDG_DATA_HOME: '/old/data' }
await inheritOmpLaunchEnvironment(overridden, {
launchAgent: 'omp',
explicitEnv: { XDG_DATA_HOME: '/pane/data' }
})
expect(overridden.XDG_DATA_HOME).toBe('/pane/data')
})
it('preserves explicit pane roots and intentionally empty values', async () => {
const env = { XDG_DATA_HOME: '/pane/data', XDG_STATE_HOME: '' }
await inheritOmpLaunchEnvironment(env, { launchAgent: 'omp' })
expect(env.XDG_DATA_HOME).toBe('/pane/data')
expect(env.XDG_STATE_HOME).toBe('')
})
it.each([
{ isWsl: true, launchAgent: 'omp' },
{ launchAgent: 'pi' },
{ launchAgent: 'claude' },
{ launchCommand: 'pi' },
{ launchCommand: 'npm test' }
])('does not probe a different execution environment for %j', async (options) => {
const env = {}
await inheritOmpLaunchEnvironment(env, options)
expect(env).toEqual({})
expect(resolveLoginShellEnvironment).not.toHaveBeenCalled()
})
it('does not import POSIX roots into native Windows', async () => {
vi.stubGlobal('process', { ...process, platform: 'win32' })
await inheritOmpLaunchEnvironment({}, { launchAgent: 'omp' })
expect(resolveLoginShellEnvironment).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,38 @@
import { detectExplicitPiAgentKindFromCommand } from '../../../../shared/pi-agent-kind'
import { resolveSetupAgentSequenceLaunchCommand } from '../../../../shared/setup-agent-sequencing'
import { resolveLoginShellEnvironment } from '../../../startup/login-shell-environment'
const OMP_DIRECTORY_ENV_KEYS = [
'XDG_CONFIG_HOME',
'XDG_DATA_HOME',
'XDG_STATE_HOME',
'XDG_CACHE_HOME',
'PI_CONFIG_DIR'
] as const
export async function inheritOmpLaunchEnvironment(
env: Record<string, string>,
options: {
isWsl?: boolean
launchAgent?: string
launchCommand?: string
explicitEnv?: Record<string, string>
}
): Promise<void> {
if (options.isWsl || process.platform === 'win32') {
return
}
const command = resolveSetupAgentSequenceLaunchCommand(env, options.launchCommand)
const agent = options.launchAgent ?? detectExplicitPiAgentKindFromCommand(command)
if (agent !== 'omp' && (options.launchAgent !== undefined || command?.trim())) {
return
}
const shellEnv = await resolveLoginShellEnvironment()
for (const key of OMP_DIRECTORY_ENV_KEYS) {
// Explicit pane values, including empty values, take precedence over the login shell.
const value = (options.explicitEnv ?? env)[key] ?? shellEnv[key] ?? process.env[key]
if (value !== undefined) {
env[key] = value
}
}
}
+6
View File
@@ -1,3 +1,4 @@
import { inheritOmpLaunchEnvironment } from '../host-env/omp-launch-environment'
import { getAppEnvironment } from '../../../../shared/app-environment'
import { isTuiAgent } from '../../../../shared/tui-agent-config'
import { isAgentStatusHooksEnabled } from '../../../agent-hooks/managed-agent-hook-controls'
@@ -133,6 +134,11 @@ export async function assemblePtyIpcSpawnCodexEnv(ctx: PtyIpcSpawnState): Promis
// Why: clone before mutating so injections don't leak back into args.env (renderer may reuse it).
ctx.env = { ...ctx.baseEnv }
try {
await inheritOmpLaunchEnvironment(ctx.env, {
isWsl: shouldSkipCodexHomeEnvForWindowsShell(ctx.effectiveShellOverride, ctx.cwd),
launchAgent: args.launchAgent,
launchCommand: ctx.launchCommand
})
buildPtyHostEnv(sessionIdForEnv, ctx.env, {
isPackaged: getAppEnvironment().isPackaged(),
resourcesPath: process.resourcesPath,
@@ -1,3 +1,4 @@
import { inheritOmpLaunchEnvironment } from '../host-env/omp-launch-environment'
import { getAppEnvironment } from '../../../../shared/app-environment'
import type { OrcaRuntimeService } from '../../../runtime/orca-runtime'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
@@ -55,6 +56,12 @@ export function configureLocalPtyProvider(args: {
)
const skipCodexHomeEnv = ctx?.isWsl === true && !selectedCodexHomePath
const ptySettings = getSettings?.()
await inheritOmpLaunchEnvironment(baseEnv, {
explicitEnv: ctx?.explicitEnv,
isWsl: ctx?.isWsl,
launchAgent: ctx?.launchAgent,
launchCommand: ctx?.command
})
const env = buildPtyHostEnv(id, baseEnv, {
isPackaged: getAppEnvironment().isPackaged(),
resourcesPath: process.resourcesPath,
@@ -1,3 +1,4 @@
import { inheritOmpLaunchEnvironment } from '../host-env/omp-launch-environment'
import { getAppEnvironment } from '../../../../shared/app-environment'
import type { PtySpawnResult } from '../../../providers/types'
import { LocalPtyProvider } from '../../../providers/local-pty-provider'
@@ -258,6 +259,12 @@ export async function prepareRuntimePtySpawn(
throw new Error('Invalid PTY session id')
}
try {
ctx.env ??= {}
await inheritOmpLaunchEnvironment(ctx.env, {
isWsl: shouldSkipCodexHomeEnvForWindowsShell(ctx.daemonShellOverride, ctx.cwd),
launchAgent: args.launchAgent,
launchCommand: ctx.launchCommand
})
ctx.env = buildPtyHostEnv(ctx.sessionId, ctx.env ?? {}, {
isPackaged: getAppEnvironment().isPackaged(),
resourcesPath: process.resourcesPath,
@@ -98,6 +98,59 @@ describe('PiTitlebarExtensionService', () => {
rmSync(join(userDataDir, 'omp-managed-status-extension'), { recursive: true, force: true })
})
it.each([
['pi', '.pi', 'ORCA_PI_SOURCE_AGENT_DIR'],
['omp', '.omp', 'ORCA_OMP_SOURCE_AGENT_DIR'],
['prime-agent', '.prime', 'ORCA_PRIME_AGENT_SOURCE_AGENT_DIR']
] as const)('does not reinterpret XDG data as %s configuration', (kind, root, sourceKey) => {
const fakeHome = mkdtempSync(join(tmpdir(), 'orca-agent-xdg-'))
const dataHome = join(fakeHome, 'data')
const dataAgentDir = join(dataHome, root.slice(1), 'agent')
mkdirSync(dataAgentDir, { recursive: true })
homedirOverride.current = fakeHome
vi.stubEnv('XDG_DATA_HOME', dataHome)
try {
const env = new PiTitlebarExtensionService().buildPtyEnv('pty-xdg', undefined, kind)
expect(env[sourceKey]).toBe(join(fakeHome, root, 'agent'))
expect(readdirSync(dataAgentDir)).toEqual([])
expect(env.PI_CODING_AGENT_DIR).toBeUndefined()
} finally {
vi.unstubAllEnvs()
homedirOverride.current = ''
rmSync(fakeHome, { recursive: true, force: true })
}
})
it('materializes OMP extensions under the launch config root without overriding data routing', () => {
const fakeHome = mkdtempSync(join(tmpdir(), 'orca-omp-config-'))
homedirOverride.current = fakeHome
try {
const service = new PiTitlebarExtensionService()
const environment = { PI_CONFIG_DIR: '.config/omp', XDG_DATA_HOME: join(fakeHome, 'data') }
const env = service.buildPtyEnv('pty-config', undefined, 'omp', {
configDirName: environment.PI_CONFIG_DIR
})
const agentDir = join(fakeHome, '.config', 'omp', 'agent')
expect(env.ORCA_OMP_SOURCE_AGENT_DIR).toBe(agentDir)
expect(existsSync(join(agentDir, 'extensions', 'orca-agent-status.ts'))).toBe(true)
expect(env.PI_CODING_AGENT_DIR).toBeUndefined()
expect(existsSync(join(fakeHome, '.omp'))).toBe(false)
expect(existsSync(environment.XDG_DATA_HOME)).toBe(false)
expect(
service.buildPtyEnv('pty-explicit', piHome, 'omp', {
configDirName: environment.PI_CONFIG_DIR
}).ORCA_OMP_SOURCE_AGENT_DIR
).toBe(piHome)
expect(
service.buildPtyEnv('pty-pi', undefined, 'pi', { configDirName: environment.PI_CONFIG_DIR })
.ORCA_PI_SOURCE_AGENT_DIR
).toBe(join(fakeHome, '.pi', 'agent'))
} finally {
homedirOverride.current = ''
rmSync(fakeHome, { recursive: true, force: true })
}
})
function expectPiHomeIntact(): void {
expect(readFileSync(join(piHome, 'auth.json'), 'utf-8')).toBe('secret token')
expect(readFileSync(join(piHome, 'skills', 'my-skill', 'SKILL.md'), 'utf-8')).toBe(
+7 -4
View File
@@ -58,8 +58,9 @@ const AGENT_HOME_DIR_NAME: Record<PiAgentKind, string> = {
'prime-agent': '.prime'
}
function getDefaultPiAgentDir(kind: PiAgentKind): string {
return join(homedir(), AGENT_HOME_DIR_NAME[kind], PI_AGENT_SUBDIR)
function getDefaultPiAgentDir(kind: PiAgentKind, configDirName: string | undefined): string {
const root = kind === 'omp' ? configDirName || AGENT_HOME_DIR_NAME.omp : AGENT_HOME_DIR_NAME[kind]
return join(homedir(), root, PI_AGENT_SUBDIR)
}
function toSafeOverlayDirName(ptyId: string): string {
@@ -177,9 +178,11 @@ export class PiTitlebarExtensionService {
ptyId: string,
existingAgentDir: string | undefined,
kind: PiAgentKind,
options?: { materializeDefaultHome?: boolean }
options?: { materializeDefaultHome?: boolean; configDirName?: string }
): Record<string, string> {
const sourceAgentDir = existingAgentDir || getDefaultPiAgentDir(kind)
const sourceAgentDir =
existingAgentDir ||
getDefaultPiAgentDir(kind, options?.configDirName ?? process.env.PI_CONFIG_DIR)
if (kind !== 'prime-agent') {
try {
this.safeRemoveOverlay(this.getPtyOverlayDir(ptyId, kind), kind)
@@ -179,6 +179,18 @@ describe('LocalPtyProvider', () => {
)
})
it('passes explicit pane environment separately from inherited process values', async () => {
const buildSpawnEnv = vi.fn((_id: string, env: Record<string, string>) => env)
provider.configure({ buildSpawnEnv })
const env = { XDG_DATA_HOME: '/pane/data' }
await provider.spawn({ cols: 80, rows: 24, env })
expect(buildSpawnEnv).toHaveBeenCalledWith(
expect.any(String),
expect.any(Object),
expect.objectContaining({ explicitEnv: env })
)
})
it('invokes buildSpawnEnv callback to customize environment', async () => {
const buildSpawnEnv = vi.fn((_id: string, env: Record<string, string>) => {
env.CUSTOM_VAR = 'custom-value'
@@ -7,6 +7,7 @@ export type LocalPtyProviderOptions = {
id: string,
baseEnv: Record<string, string>,
ctx?: {
explicitEnv?: PtySpawnOptions['env']
command?: string
launchAgent?: PtySpawnOptions['launchAgent']
codexHomePathOverride?: PtySpawnOptions['codexHomePathOverride']
@@ -61,6 +61,7 @@ export function buildLocalPtySpawnEnvironment(args: {
return awaitCancelableLocalPtySpawn(
id,
getOptions().buildSpawnEnv!(id, spawnEnv, {
explicitEnv: spawn.env ?? {},
command: spawn.command,
launchAgent: spawn.launchAgent,
codexHomePathOverride: spawn.codexHomePathOverride,
+231
View File
@@ -0,0 +1,231 @@
import { chmod, mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
import { dirname, delimiter, join } from 'node:path'
import { test as base, expect } from './helpers/orca-app'
import { buildShellCommandFromArgv } from '../../src/shared/tui-agent-startup-shell'
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import {
execInTerminal,
waitForActivePanePtyId,
waitForActiveTerminalManager
} from './helpers/terminal'
const test = base.extend({
launchEnv: async ({ seedTestRepo }, run, testInfo) => {
void seedTestRepo
const shell = testInfo.outputPath('login-shell')
const profileDir = testInfo.outputPath('profile')
await mkdir(profileDir, { recursive: true })
await writeFile(
join(profileDir, '.zprofile'),
`
export PI_CONFIG_DIR="\${PI_CONFIG_DIR:-.omp-profile-proof}"
export XDG_DATA_HOME="\${XDG_DATA_HOME:-$HOME/xdg-data}"
export XDG_STATE_HOME="\${XDG_STATE_HOME:-$HOME/xdg-state}"
export XDG_CACHE_HOME="\${XDG_CACHE_HOME:-$HOME/xdg-cache}"
mkdir -p "$HOME/$PI_CONFIG_DIR/agent" "$XDG_DATA_HOME/omp" "$XDG_STATE_HOME/omp" "$XDG_CACHE_HOME/omp"
`
)
await writeFile(
shell,
`#!/bin/sh
case "$ORCA_E2E_HOME_DIR" in
*/orca-e2e-userdata-*/home) ;;
*) echo 'Refusing profile proof outside isolated E2E home' >&2; exit 73 ;;
esac
export HOME="$ORCA_E2E_HOME_DIR"
export ZDOTDIR=${buildShellCommandFromArgv([profileDir], 'posix')}
exec /bin/zsh "$@"
`
)
await chmod(shell, 0o700)
await run({
PATH: [dirname(process.env.ORCA_OMP_PROOF_BINARY ?? '/usr/bin/omp'), process.env.PATH]
.filter(Boolean)
.join(delimiter),
SHELL: shell,
XDG_DATA_HOME: undefined,
XDG_STATE_HOME: undefined,
XDG_CACHE_HOME: undefined,
XDG_CONFIG_HOME: undefined,
PI_CONFIG_DIR: undefined
})
}
})
test('OMP launched by Orca uses login-profile data and config roots', async ({
orcaPage,
electronApp
}, testInfo) => {
test.skip(
!process.env.ORCA_OMP_PROOF_BINARY || process.platform !== 'darwin',
'Opt-in macOS OMP runtime proof'
)
await waitForSessionReady(orcaPage)
const worktreeId = await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
await waitForActiveTerminalManager(orcaPage)
const ptyId = await waitForActivePanePtyId(orcaPage)
const home = await electronApp.evaluate(({ app }) => app.getPath('home'))
expect(await electronApp.evaluate(() => process.env.XDG_DATA_HOME)).toBeUndefined()
const result = testInfo.outputPath('omp-paths.json')
const probe = testInfo.outputPath('path-probe.ts')
await writeFile(
probe,
`import { writeFileSync } from 'node:fs'
export default function (api) {
api.on('session_start', (_event, ctx) => {
writeFileSync(process.env.ORCA_PATH_PROBE_OUTPUT || ${JSON.stringify(result)}, JSON.stringify({
data: process.env.XDG_DATA_HOME, config: process.env.PI_CONFIG_DIR,
source: process.env.ORCA_OMP_SOURCE_AGENT_DIR,
status: process.env.ORCA_OMP_STATUS_EXTENSION,
session: ctx.sessionManager.getSessionFile(), pid: process.pid
}))
})
}`
)
await execInTerminal(
orcaPage,
ptyId,
buildShellCommandFromArgv(['omp', '--no-extensions', '--extension', probe], 'posix')
)
await expect
.poll(
async () => {
try {
return JSON.parse(await readFile(result, 'utf8'))
} catch {
return null
}
},
{ timeout: 45_000 }
)
.toEqual(
expect.objectContaining({
data: join(home, 'xdg-data'),
config: '.omp-profile-proof',
source: join(home, '.omp-profile-proof', 'agent'),
status: expect.stringContaining(join('.omp-profile-proof', 'agent', 'extensions')),
session: expect.stringContaining(join('xdg-data', 'omp', 'sessions'))
})
)
await expect(orcaPage.locator('.xterm-screen').first()).toBeVisible()
await orcaPage.screenshot({ path: testInfo.outputPath('omp-profile-root.png') })
expect(await readdir(join(home, 'xdg-data', 'omp'))).toContain('agent.db')
const overrideData = join(home, 'pane-data')
await mkdir(join(overrideData, 'omp'), { recursive: true })
const overrideResult = testInfo.outputPath('omp-pane-paths.json')
const overridePty = await orcaPage.evaluate(
async ({ command, worktreeId, home, overrideData, overrideResult }) => {
const pane = await window.api.pty.spawn({
cols: 100,
rows: 30,
cwd: home,
worktreeId,
initiallyHidden: true,
launchAgent: 'omp',
command,
env: {
XDG_DATA_HOME: overrideData,
PI_CONFIG_DIR: '.omp-pane-config',
ORCA_PATH_PROBE_OUTPUT: overrideResult
}
})
return pane.id
},
{
command: buildShellCommandFromArgv(['omp', '--no-extensions', '--extension', probe], 'posix'),
worktreeId,
home,
overrideData,
overrideResult
}
)
try {
await expect
.poll(
async () => {
try {
return JSON.parse(await readFile(overrideResult, 'utf8'))
} catch {
return null
}
},
{ timeout: 45_000 }
)
.toEqual(
expect.objectContaining({
data: overrideData,
config: '.omp-pane-config',
source: join(home, '.omp-pane-config', 'agent'),
session: expect.stringContaining(join('pane-data', 'omp', 'sessions'))
})
)
expect(await readdir(join(overrideData, 'omp'))).toContain('agent.db')
} finally {
await orcaPage.evaluate((id) => window.api.pty.kill(id), overridePty)
}
const baselineResult = testInfo.outputPath('omp-login-baseline.json')
const baselineCommand = buildShellCommandFromArgv(
[
'env',
...[
'XDG_DATA_HOME',
'XDG_STATE_HOME',
'XDG_CACHE_HOME',
'PI_CONFIG_DIR',
'ORCA_OMP_SOURCE_AGENT_DIR',
'ORCA_OMP_STATUS_EXTENSION',
'ORCA_PI_STATUS_OWNED',
'ZDOTDIR',
'ORCA_ORIG_ZDOTDIR'
].flatMap((key) => ['-u', key]),
`ORCA_PATH_PROBE_OUTPUT=${baselineResult}`,
`HOME=${home}`,
`ZDOTDIR=${testInfo.outputPath('profile')}`,
'/bin/zsh',
'-ilc',
buildShellCommandFromArgv(
[process.env.ORCA_OMP_PROOF_BINARY ?? '', '--no-extensions', '--extension', probe],
'posix'
)
],
'posix'
)
const baselinePty = await orcaPage.evaluate(
async ({ command, worktreeId, home }) => {
return (
await window.api.pty.spawn({
cols: 100,
rows: 30,
cwd: home,
worktreeId,
initiallyHidden: true,
command
})
).id
},
{ command: baselineCommand, worktreeId, home }
)
try {
await expect
.poll(
async () => {
try {
return JSON.parse(await readFile(baselineResult, 'utf8'))
} catch {
return null
}
},
{ timeout: 45_000 }
)
.toEqual(
expect.objectContaining({
data: join(home, 'xdg-data'),
config: '.omp-profile-proof',
session: expect.stringContaining(join('xdg-data', 'omp', 'sessions'))
})
)
} finally {
await orcaPage.evaluate((id) => window.api.pty.kill(id), baselinePty)
}
})