mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
fix(omp): guard fresh launches with execution-host settings
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
# Fresh OMP launches
|
||||
|
||||
Orca's new-session and draft launch plans apply a one-time `--config` overlay
|
||||
containing `autoResume: false`. OMP's configured session directory, settings,
|
||||
authentication and extensions remain in their usual locations. Saved launch
|
||||
configuration omits the overlay so explicit resume keeps its normal semantics.
|
||||
Custom commands with session selectors, unknown flags, positional arguments or
|
||||
shell compounds are left unchanged.
|
||||
|
||||
The execution host creates the overlay. Local and WSL terminals use Orca userData
|
||||
(with WSLENV path translation); SSH relays use their own managed directory. Config
|
||||
creation is independent of status-hook preferences and does not require plugin
|
||||
source installation. An unavailable file produces a terminal diagnostic and skips
|
||||
OMP. Filesystem failures do not prevent unrelated agents or bare shells starting.
|
||||
|
||||
The guard invokes OMP in the current shell, preserving functions, aliases and the
|
||||
managed status wrapper. A nonzero agent exit never triggers a second launch.
|
||||
POSIX commands also support fish; environment presence is checked before expansion
|
||||
so an old host under `set -u` reports the same missing-settings diagnostic.
|
||||
|
||||
## Mixed versions
|
||||
|
||||
The existing command and environment transport carries the launch unchanged; no
|
||||
new RPC or stream opcode is introduced. A new host exports the path before shell
|
||||
startup, including bare shells that receive an OMP command later. An old client
|
||||
continues its existing launch behavior on a new host. A new fresh-launch command
|
||||
on an old host without the managed environment fails visibly and requests a host
|
||||
update and terminal restart. It must not silently fall back to OMP auto-resume.
|
||||
|
||||
## Verification
|
||||
|
||||
`src/shared/omp-fresh-launch-shell.test.ts` runs actual available bash, zsh and fish
|
||||
shells, checking exact argv, a single invocation, nonzero exit, deleted settings
|
||||
and an absent environment variable. `src/relay/omp-fresh-launch-environment.test.ts`
|
||||
checks the guarded command through relay environment assembly and the actual OMP
|
||||
shell wrapper, retaining both extension and config arguments and prefill.
|
||||
Local host assembly tests recognize guarded POSIX, cmd and PowerShell commands.
|
||||
|
||||
Run the actual OMP storage smoke against a read-only OMP checkout:
|
||||
|
||||
```sh
|
||||
ORCA_BACKGROUND_LAUNCH=1 bun tests/tools/omp-fresh-session-runtime-smoke.mjs /path/to/oh-my-pi
|
||||
```
|
||||
|
||||
For Windows, bundle `tests/tools/omp-fresh-launch-windows-smoke.ts` with
|
||||
`bun build --target=node --outfile=/tmp/omp-fresh-windows-smoke.mjs`, transfer the
|
||||
bundle to the host and run it using Node with `ORCA_BACKGROUND_LAUNCH=1`.
|
||||
The smoke uses temporary files and process-local environment only. Both cmd and
|
||||
PowerShell must pass existing/missing/unset/directory settings cases, preserving
|
||||
exit 17 for the single successful launch and returning exit 1 without launching
|
||||
when settings are unavailable. This passed on Windows host `awin` on 2026-09-14.
|
||||
@@ -129,6 +129,7 @@ export const agentHookServerModuleMock = () => ({
|
||||
export const piTitlebarExtensionModuleMock = () => ({
|
||||
piTitlebarExtensionService: {
|
||||
buildPtyEnv: piBuildPtyEnvMock,
|
||||
buildFreshOmpEnv: () => ({ ORCA_OMP_FRESH_CONFIG: '/tmp/orca-fresh-session.yml' }),
|
||||
clearPty: piClearPtyMock
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { withFreshOmpLaunch } from '../../shared/omp-fresh-launch'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
readFileSyncMock,
|
||||
@@ -293,14 +294,19 @@ describe('registerPtyHandlers', () => {
|
||||
})
|
||||
expect(env.ORCA_PI_SOURCE_AGENT_DIR).toBe('/tmp/default-pi-agent')
|
||||
})
|
||||
it('threads command: "omp" through to piBuildPtyEnv and emits OMP status metadata', async () => {
|
||||
it.each([
|
||||
'omp',
|
||||
withFreshOmpLaunch('omp', 'posix'),
|
||||
withFreshOmpLaunch('omp', 'powershell'),
|
||||
withFreshOmpLaunch('omp', 'cmd')
|
||||
])('threads OMP command %s through to the host integration', async (command) => {
|
||||
// Why: OMP launches emit ORCA_OMP_* shadow vars, not Pi-named ones; only PI_CODING_AGENT_DIR stays (OMP's own binary reads it).
|
||||
const env = await spawnAndGetEnv(
|
||||
undefined,
|
||||
{ PI_CODING_AGENT_DIR: '/tmp/user-omp-agent' },
|
||||
undefined,
|
||||
undefined,
|
||||
'omp'
|
||||
command
|
||||
)
|
||||
expect(piBuildPtyEnvMock).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { withFreshOmpLaunch } from '../../shared/omp-fresh-launch'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { spawnMock } from './pty-ipc-mock-registry'
|
||||
import { BUNDLED_CLI_PATH, TEST_CODEX_HOME, makeDisposable } from './pty-ipc-test-constants'
|
||||
@@ -58,6 +59,22 @@ describe('registerPtyHandlers', () => {
|
||||
const { handlers, mainWindow, spawnAndGetEnv, withBundledCli } = setupPtyIpcSuite()
|
||||
|
||||
describe('spawn environment', () => {
|
||||
it('prepares fresh OMP settings even when status hooks are disabled', () => {
|
||||
const env = buildPtyHostEnv(
|
||||
'fresh-without-hooks',
|
||||
{ ORCA_OMP_FRESH_CONFIG: '/other-host/stale.yml' },
|
||||
{
|
||||
isPackaged: true,
|
||||
userDataPath: '/tmp/orca-user-data',
|
||||
selectedCodexHomePath: null,
|
||||
agentStatusHooksEnabled: false,
|
||||
launchCommand: withFreshOmpLaunch('omp', 'posix')
|
||||
}
|
||||
)
|
||||
expect(env.ORCA_OMP_FRESH_CONFIG).toBe('/tmp/orca-fresh-session.yml')
|
||||
expect(env.ORCA_OMP_STATUS_EXTENSION).toBeUndefined()
|
||||
})
|
||||
|
||||
it('routes headless browser launches through the owning Orca workspace', () => {
|
||||
const inheritedBrowser = process.env.BROWSER
|
||||
delete process.env.BROWSER
|
||||
|
||||
@@ -194,6 +194,9 @@ export function buildPtyHostEnv(
|
||||
overlay: 'ORCA_OMP_CODING_AGENT_DIR',
|
||||
source: 'ORCA_OMP_SOURCE_AGENT_DIR'
|
||||
})
|
||||
if (shouldPrepareOmpShadow) {
|
||||
Object.assign(baseEnv, piTitlebarExtensionService.buildFreshOmpEnv())
|
||||
}
|
||||
delete baseEnv.ORCA_OMP_STATUS_EXTENSION
|
||||
delete baseEnv.ORCA_PRIME_AGENT_SOURCE_AGENT_DIR
|
||||
delete baseEnv.ORCA_PRIME_AGENT_STATUS_EXTENSION
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
OMP_FRESH_CONFIG_FILENAME,
|
||||
OMP_FRESH_CONFIG_SOURCE,
|
||||
ORCA_OMP_FRESH_CONFIG_ENV
|
||||
} from '../../shared/omp-fresh-launch'
|
||||
import { materializeOmpFreshConfig } from '../../shared/omp-fresh-config'
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
@@ -179,26 +175,22 @@ export class PiTitlebarExtensionService {
|
||||
}
|
||||
}
|
||||
|
||||
buildFreshOmpEnv(): Record<string, string> {
|
||||
return {
|
||||
ORCA_OMP_FRESH_CONFIG: materializeOmpFreshConfig(
|
||||
join(getAppEnvironment().getPath('userData'), OMP_MANAGED_STATUS_EXTENSION_DIR)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
buildPtyEnv(
|
||||
ptyId: string,
|
||||
existingAgentDir: string | undefined,
|
||||
kind: PiAgentKind,
|
||||
options?: { materializeDefaultHome?: boolean; configDirName?: string }
|
||||
): Record<string, string> {
|
||||
// The caller resolves the effective launch environment. Reading the
|
||||
// daemon's ambient PI_CONFIG_DIR here can select the host profile for a
|
||||
// guest/WSL launch whose environment has not been hydrated yet.
|
||||
const freshConfigEnv: Record<string, string> = {}
|
||||
if (kind === 'omp') {
|
||||
const configDir = join(
|
||||
getAppEnvironment().getPath('userData'),
|
||||
OMP_MANAGED_STATUS_EXTENSION_DIR
|
||||
)
|
||||
mkdirSync(configDir, { recursive: true })
|
||||
const configPath = join(configDir, OMP_FRESH_CONFIG_FILENAME)
|
||||
writeFileSync(configPath, OMP_FRESH_CONFIG_SOURCE)
|
||||
freshConfigEnv[ORCA_OMP_FRESH_CONFIG_ENV] = configPath
|
||||
}
|
||||
const freshConfigEnv = kind === 'omp' ? this.buildFreshOmpEnv() : {}
|
||||
// The caller resolves the effective launch environment before this point.
|
||||
const sourceAgentDir =
|
||||
existingAgentDir || getDefaultPiAgentDir(kind, options?.configDirName)
|
||||
if (kind !== 'prime-agent') {
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { runProcess } from '../shared/child-process/run-process'
|
||||
import { getPosixOmpShellWrapper } from '../main/pty/omp-shell-wrapper'
|
||||
import { mkdtempSync, readFileSync, rmSync, existsSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import type * as NodeOs from 'node:os'
|
||||
import { delimiter, join } from 'node:path'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { RelayDispatcher } from './dispatcher'
|
||||
import { PtyHandler } from './pty-handler'
|
||||
import { RelayAgentHookRuntime } from './relay-agent-hook-runtime'
|
||||
import { PluginOverlayManager } from './plugin-overlay'
|
||||
import { withFreshOmpLaunch } from '../shared/omp-fresh-launch'
|
||||
|
||||
const state = vi.hoisted(() => ({ home: '' }))
|
||||
vi.mock('node:os', async (original) => ({
|
||||
...(await original<typeof NodeOs>()),
|
||||
homedir: () => state.home
|
||||
}))
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
if (state.home) {
|
||||
rmSync(state.home, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('prepares the execution host OMP config and status extension for a guarded launch', async () => {
|
||||
state.home = mkdtempSync(join(tmpdir(), 'orca-relay-fresh-'))
|
||||
const dispatcher = new RelayDispatcher(() => {})
|
||||
const handler = new PtyHandler(dispatcher)
|
||||
const augment = vi.spyOn(handler, 'addEnvAugmenter')
|
||||
const source = vi.spyOn(PluginOverlayManager.prototype, 'hasPiSource').mockReturnValue(true)
|
||||
const original = PluginOverlayManager.prototype.materializePi
|
||||
vi.spyOn(PluginOverlayManager.prototype, 'materializePi').mockImplementation(function (
|
||||
this: PluginOverlayManager,
|
||||
...args
|
||||
) {
|
||||
this.setSources({ ompExtensionSource: '// Orca managed test status extension' })
|
||||
return original.apply(this, args)
|
||||
})
|
||||
const runtime = new RelayAgentHookRuntime(
|
||||
dispatcher,
|
||||
handler,
|
||||
join(state.home, 'relay.sock'),
|
||||
join(state.home, 'hooks')
|
||||
)
|
||||
await runtime.start()
|
||||
try {
|
||||
const environment = augment.mock.calls[1][0]({
|
||||
id: 'fresh-pane',
|
||||
shell: '/bin/bash',
|
||||
env: { HOME: state.home },
|
||||
command: withFreshOmpLaunch('omp', 'posix')
|
||||
})
|
||||
expect(readFileSync(environment.ORCA_OMP_FRESH_CONFIG, 'utf8')).toBe('autoResume: false\n')
|
||||
expect(existsSync(environment.ORCA_OMP_STATUS_EXTENSION)).toBe(true)
|
||||
expect(environment.ORCA_OMP_SOURCE_AGENT_DIR).toBe(join(state.home, '.omp', 'agent'))
|
||||
expect(environment.PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(environment.ORCA_PI_SOURCE_AGENT_DIR).toBeUndefined()
|
||||
if (process.platform !== 'win32' && existsSync('/bin/bash')) {
|
||||
writeFileSync(
|
||||
join(state.home, 'omp'),
|
||||
'#!/bin/sh\nprintf "%s\\n" "$@" "$ORCA_OMP_PREFILL"\nexit 17\n',
|
||||
{ mode: 0o755 }
|
||||
)
|
||||
const result = await runProcess({
|
||||
program: '/bin/bash',
|
||||
args: [
|
||||
'--noprofile',
|
||||
'--norc',
|
||||
'-c',
|
||||
`${getPosixOmpShellWrapper()}\n${withFreshOmpLaunch('omp', 'posix')}`
|
||||
],
|
||||
cwd: state.home,
|
||||
env: {
|
||||
...process.env,
|
||||
...environment,
|
||||
PATH: state.home + delimiter + process.env.PATH,
|
||||
ORCA_OMP_PREFILL: 'remote draft'
|
||||
}
|
||||
})
|
||||
expect(result.code).toBe(17)
|
||||
expect(result.stdout.trim().split('\n')).toEqual([
|
||||
'--extension',
|
||||
environment.ORCA_OMP_STATUS_EXTENSION,
|
||||
'--config',
|
||||
environment.ORCA_OMP_FRESH_CONFIG,
|
||||
'remote draft'
|
||||
])
|
||||
}
|
||||
|
||||
source.mockReturnValue(false)
|
||||
expect(
|
||||
augment.mock.calls[1][0]({ id: 'other', shell: '/bin/bash', env: {}, command: 'codex' })
|
||||
).toEqual({})
|
||||
} finally {
|
||||
runtime.stop()
|
||||
dispatcher.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('fails the config guard safely when the relay-owned directory cannot be created', () => {
|
||||
state.home = mkdtempSync(join(tmpdir(), 'orca-relay-fresh-fail-'))
|
||||
const blocked = join(state.home, 'file')
|
||||
writeFileSync(blocked, '')
|
||||
const manager = new PluginOverlayManager({ homeDir: blocked })
|
||||
expect(manager.materializeOmpFreshConfig()).toBe('')
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import { materializeOmpFreshConfig } from '../shared/omp-fresh-config'
|
||||
// Why: relay-side equivalent of Orca's local agent integration installers.
|
||||
// OpenCode still needs a config overlay, while Pi/OMP now get Orca-managed
|
||||
// extension files installed into the remote agent homes. Host paths from the
|
||||
@@ -275,6 +276,12 @@ export class PluginOverlayManager {
|
||||
}
|
||||
}
|
||||
|
||||
materializeOmpFreshConfig(): string {
|
||||
return materializeOmpFreshConfig(
|
||||
join(this.homeDir, RELAY_HOOKS_DIR, OMP_MANAGED_STATUS_EXTENSION_DIR)
|
||||
)
|
||||
}
|
||||
|
||||
/** Install the Pi/OMP status extension into the remote real agent dir.
|
||||
* `kind` selects which Pi-compatible agent's default dir to use when
|
||||
* `existingAgentDir` is not supplied.
|
||||
|
||||
@@ -94,9 +94,6 @@ export class RelayAgentHookRuntime {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!this.pluginOverlay.hasPiSource()) {
|
||||
return env
|
||||
}
|
||||
const launchCommandHint = resolveSetupAgentSequenceLaunchCommand(context.env, context.command)
|
||||
const explicitKind = isPiCompatibleAgentType(context.launchAgent)
|
||||
? context.launchAgent
|
||||
@@ -106,6 +103,12 @@ export class RelayAgentHookRuntime {
|
||||
const kind = explicitKind ?? 'pi'
|
||||
const hasLaunchCommand =
|
||||
typeof launchCommandHint === 'string' && launchCommandHint.trim().length > 0
|
||||
if (kind === 'omp' || !hasLaunchCommand) {
|
||||
env.ORCA_OMP_FRESH_CONFIG = this.pluginOverlay.materializeOmpFreshConfig()
|
||||
}
|
||||
if (!this.pluginOverlay.hasPiSource()) {
|
||||
return env
|
||||
}
|
||||
if (kind === 'pi') {
|
||||
const sourceDir = resolvePiSourceAgentDir(context.env, context.shell, 'pi')
|
||||
const result = this.pluginOverlay.materializePi(overlayId, sourceDir, 'pi', {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { OMP_FRESH_CONFIG_FILENAME, OMP_FRESH_CONFIG_SOURCE } from './omp-fresh-launch'
|
||||
|
||||
/** An empty path fails the launch guard without preventing unrelated terminal launches. */
|
||||
export function materializeOmpFreshConfig(directory: string): string {
|
||||
try {
|
||||
const configPath = join(directory, OMP_FRESH_CONFIG_FILENAME)
|
||||
if (existsSync(configPath) && readFileSync(configPath, 'utf8') === OMP_FRESH_CONFIG_SOURCE) {
|
||||
return configPath
|
||||
}
|
||||
mkdirSync(directory, { recursive: true })
|
||||
writeFileSync(configPath, OMP_FRESH_CONFIG_SOURCE)
|
||||
return configPath
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { delimiter } from 'node:path'
|
||||
import { mkdtemp, writeFile, readFile, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { runProcess } from './child-process/run-process'
|
||||
import { buildAgentStartupPlan } from './tui-agent-startup'
|
||||
|
||||
const roots: string[] = []
|
||||
afterEach(async () => {
|
||||
for (const root of roots.splice(0)) {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
const shells = ['bash', 'zsh', 'fish'].filter(
|
||||
(shell) =>
|
||||
process.platform !== 'win32' &&
|
||||
(process.env.PATH ?? '')
|
||||
.split(delimiter)
|
||||
.some((directory) => existsSync(join(directory, shell)))
|
||||
)
|
||||
it.each(shells)(
|
||||
'preserves same-shell function argv, one launch and exit status in %s',
|
||||
async (shell) => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'orca-fresh-shell-'))
|
||||
roots.push(root)
|
||||
const config = join(root, 'fresh settings.yml')
|
||||
const capture = join(root, 'argv')
|
||||
await writeFile(config, 'autoResume: false\n')
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'omp',
|
||||
prompt: 'task with spaces',
|
||||
cmdOverrides: {},
|
||||
platform: 'linux',
|
||||
isRemote: true
|
||||
})
|
||||
expect(plan).not.toBeNull()
|
||||
const define =
|
||||
shell === 'fish'
|
||||
? 'function omp; printf "%s\\n" $argv >> "$CAPTURE"; return 17; end; '
|
||||
: 'omp() { printf "%s\\n" "$@" >> "$CAPTURE"; return 17; }; '
|
||||
const result = await runProcess({
|
||||
program: shell,
|
||||
args: ['-c', define + plan!.launchCommand],
|
||||
cwd: root,
|
||||
env: { ...process.env, ORCA_OMP_FRESH_CONFIG: config, CAPTURE: capture }
|
||||
})
|
||||
expect(result.code).toBe(17)
|
||||
expect(await readFile(capture, 'utf8')).toBe(`--config\n${config}\ntask with spaces\n`)
|
||||
await rm(capture)
|
||||
await rm(config)
|
||||
const missing = await runProcess({
|
||||
program: shell,
|
||||
args: ['-c', define + plan!.launchCommand],
|
||||
cwd: root,
|
||||
env: { ...process.env, ORCA_OMP_FRESH_CONFIG: config, CAPTURE: capture }
|
||||
})
|
||||
expect(missing.code).not.toBe(0)
|
||||
expect(missing.stderr).toContain('fresh OMP settings are unavailable')
|
||||
await expect(readFile(capture)).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
const unsetEnv: NodeJS.ProcessEnv = { ...process.env, CAPTURE: capture }
|
||||
delete unsetEnv.ORCA_OMP_FRESH_CONFIG
|
||||
const unset = await runProcess({
|
||||
program: shell,
|
||||
args: ['-c', (shell === 'fish' ? '' : 'set -u; ') + define + plan!.launchCommand],
|
||||
cwd: root,
|
||||
env: unsetEnv
|
||||
})
|
||||
expect(unset.code).not.toBe(0)
|
||||
expect(unset.stderr).toContain('fresh OMP settings are unavailable')
|
||||
await expect(readFile(capture)).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
}
|
||||
)
|
||||
@@ -6,7 +6,7 @@ describe('OMP fresh launch intent', () => {
|
||||
it.each(['omp', 'omp launch', 'omp --model provider/model', 'omp --config user.yml'])(
|
||||
'%s adds the final overlay',
|
||||
(command) => {
|
||||
expect(withFreshOmpLaunch(command, 'posix')).toBe(
|
||||
expect(withFreshOmpLaunch(command, 'posix')).toContain(
|
||||
`${command} --config "$ORCA_OMP_FRESH_CONFIG"`
|
||||
)
|
||||
}
|
||||
@@ -29,13 +29,13 @@ describe('OMP fresh launch intent', () => {
|
||||
'echo omp',
|
||||
'omp && echo hi'
|
||||
])('preserves %s', (command) => {
|
||||
expect(withFreshOmpLaunch(command, 'posix')).toBe(command)
|
||||
expect(withFreshOmpLaunch(command, 'posix')).toContain(command)
|
||||
})
|
||||
it('quotes the host config path for each Windows shell', () => {
|
||||
expect(withFreshOmpLaunch('omp', 'powershell')).toBe(
|
||||
expect(withFreshOmpLaunch('omp', 'powershell')).toContain(
|
||||
'omp --config "$env:ORCA_OMP_FRESH_CONFIG"'
|
||||
)
|
||||
expect(withFreshOmpLaunch('omp', 'cmd')).toBe('omp --config "%ORCA_OMP_FRESH_CONFIG%"')
|
||||
expect(withFreshOmpLaunch('omp', 'cmd')).toContain('omp --config "%ORCA_OMP_FRESH_CONFIG%"')
|
||||
})
|
||||
it('keeps fresh intent out of saved resume command and environment', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
@@ -48,7 +48,7 @@ describe('OMP fresh launch intent', () => {
|
||||
expect(JSON.stringify(plan?.launchConfig)).not.toContain('ORCA_OMP_FRESH_CONFIG')
|
||||
expect(plan?.env).toBeUndefined()
|
||||
})
|
||||
it('does not require a new environment field from an older SSH relay', () => {
|
||||
it('requires host-owned configuration for fresh SSH launches', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'omp',
|
||||
prompt: 'new task',
|
||||
@@ -56,6 +56,6 @@ describe('OMP fresh launch intent', () => {
|
||||
platform: 'linux',
|
||||
isRemote: true
|
||||
})
|
||||
expect(plan?.launchCommand).not.toContain('ORCA_OMP_FRESH_CONFIG')
|
||||
expect(plan?.launchCommand).toContain('test -f "$ORCA_OMP_FRESH_CONFIG"')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,14 +23,14 @@ const VALUE_FLAGS = new Set([
|
||||
const SWITCH_FLAGS = new Set(['--no-extensions', '--no-skills', '--no-prompt-templates'])
|
||||
|
||||
/** Apply fresh intent to one launch command, never the saved resume configuration. */
|
||||
export function withFreshOmpLaunch(command: string, shell: AgentStartupShell): string {
|
||||
export function withFreshOmpLaunch(command: string, shell: AgentStartupShell, suffix = ''): string {
|
||||
const parsed = tokenizeStartupCommand(command, shell)
|
||||
if (!parsed.ok) {
|
||||
return command
|
||||
return command + suffix
|
||||
}
|
||||
const executable = parsed.tokens[0]?.split(/[\\/]/).at(-1)?.toLowerCase()
|
||||
if (!['omp', 'omp.exe', 'omp.cmd', 'omp.bat', 'omp.sh', 'omp.js'].includes(executable ?? '')) {
|
||||
return command
|
||||
return command + suffix
|
||||
}
|
||||
let index = parsed.tokens[1] === 'launch' ? 2 : 1
|
||||
for (; index < parsed.tokens.length; index++) {
|
||||
@@ -39,10 +39,10 @@ export function withFreshOmpLaunch(command: string, shell: AgentStartupShell): s
|
||||
const flag = equals === -1 ? token : token.slice(0, equals)
|
||||
if (VALUE_FLAGS.has(flag)) {
|
||||
if (equals === -1 && ++index >= parsed.tokens.length) {
|
||||
return command
|
||||
return command + suffix
|
||||
}
|
||||
} else if (!SWITCH_FLAGS.has(token)) {
|
||||
return command
|
||||
return command + suffix
|
||||
}
|
||||
}
|
||||
const path =
|
||||
@@ -51,5 +51,28 @@ export function withFreshOmpLaunch(command: string, shell: AgentStartupShell): s
|
||||
: shell === 'powershell'
|
||||
? `"$env:${ORCA_OMP_FRESH_CONFIG_ENV}"`
|
||||
: `"$${ORCA_OMP_FRESH_CONFIG_ENV}"`
|
||||
return `${command} --config ${path}`
|
||||
const launch = `${command} --config ${path}${suffix}`
|
||||
const error =
|
||||
'Orca: fresh OMP settings are unavailable on this host. Restart the terminal after updating Orca.'
|
||||
if (shell === 'powershell') {
|
||||
return `if (${path} -and (Test-Path -LiteralPath ${path} -PathType Leaf)) { ${launch} } else { $global:LASTEXITCODE = 1; Write-Error '${error}' }`
|
||||
}
|
||||
if (shell === 'cmd') {
|
||||
return `if exist ${path} (if not exist "${path.slice(1, -1)}\\*" (${launch}) else (echo ${error} 1>&2 & verify invalid 2>nul)) else (echo ${error} 1>&2 & verify invalid 2>nul)`
|
||||
}
|
||||
const available = `printenv ${ORCA_OMP_FRESH_CONFIG_ENV} >/dev/null && test -f ${path}`
|
||||
// && is supported by bash, zsh, and fish; invoking directly preserves shell aliases/functions.
|
||||
return `${available} || printf '%s\\n' '${error}' >&2; ${available} && ${launch}`
|
||||
}
|
||||
|
||||
/** Recognize only the host-settings guards generated by this module. */
|
||||
export function isFreshOmpLaunchCommand(command: string | undefined): boolean {
|
||||
return Boolean(
|
||||
command &&
|
||||
[
|
||||
`printenv ${ORCA_OMP_FRESH_CONFIG_ENV} >/dev/null && test -f "$${ORCA_OMP_FRESH_CONFIG_ENV}"`,
|
||||
`if ("$env:${ORCA_OMP_FRESH_CONFIG_ENV}" -and (Test-Path -LiteralPath "$env:${ORCA_OMP_FRESH_CONFIG_ENV}" -PathType Leaf))`,
|
||||
`if exist "%${ORCA_OMP_FRESH_CONFIG_ENV}%" (if not exist "%${ORCA_OMP_FRESH_CONFIG_ENV}%\\*"`
|
||||
].some((prefix) => command.startsWith(prefix))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isFreshOmpLaunchCommand } from './omp-fresh-launch'
|
||||
import { TUI_AGENT_CONFIG } from './tui-agent-config'
|
||||
import { getCommandTokenPathBasename, getFirstCommandToken } from './command-token-scanner'
|
||||
|
||||
@@ -49,6 +50,9 @@ const PRIME_AGENT_LAUNCH_BINARY = getLaunchBinary(TUI_AGENT_CONFIG['prime-agent'
|
||||
export function detectExplicitPiAgentKindFromCommand(
|
||||
command: string | undefined
|
||||
): PiAgentKind | null {
|
||||
if (isFreshOmpLaunchCommand(command)) {
|
||||
return 'omp'
|
||||
}
|
||||
const binary = getLaunchBinary(command ?? '')
|
||||
if (binary === OMP_LAUNCH_BINARY) {
|
||||
return 'omp'
|
||||
|
||||
@@ -666,7 +666,7 @@ describe('tui agent startup plans', () => {
|
||||
expect(plan).not.toBeNull()
|
||||
expect(plan?.env).toEqual({ ORCA_OMP_PREFILL: 'fix the omp regression' })
|
||||
expect(plan?.expectedProcess).toBe('omp')
|
||||
expect(plan?.launchCommand).toBe(
|
||||
expect(plan?.launchCommand).toContain(
|
||||
`omp --config "$ORCA_OMP_FRESH_CONFIG"; command test -n "$fish_pid" && set --erase -g ORCA_OMP_PREFILL; command test -z "$fish_pid" && unset ORCA_OMP_PREFILL; true`
|
||||
)
|
||||
})
|
||||
|
||||
@@ -72,9 +72,7 @@ export function buildAgentStartupPlan(args: {
|
||||
return null
|
||||
}
|
||||
const launchCommand =
|
||||
agent === 'omp' && !args.isRemote
|
||||
? withFreshOmpLaunch(baseCommand.command, shell)
|
||||
: baseCommand.command
|
||||
agent === 'omp' ? withFreshOmpLaunch(baseCommand.command, shell) : baseCommand.command
|
||||
const launchConfig = buildSleepingAgentLaunchConfig({
|
||||
...args,
|
||||
// Why: picker flags are a one-time launch choice; a resumed provider
|
||||
@@ -103,7 +101,10 @@ export function buildAgentStartupPlan(args: {
|
||||
const promptSeparator = config.argvPromptSeparator ? ` ${config.argvPromptSeparator}` : ''
|
||||
return {
|
||||
agent,
|
||||
launchCommand: `${launchCommand}${promptSeparator} ${quotedPrompt}`,
|
||||
launchCommand:
|
||||
agent === 'omp'
|
||||
? withFreshOmpLaunch(baseCommand.command, shell, `${promptSeparator} ${quotedPrompt}`)
|
||||
: `${launchCommand}${promptSeparator} ${quotedPrompt}`,
|
||||
expectedProcess: config.expectedProcess,
|
||||
followupPrompt: null,
|
||||
launchConfig,
|
||||
@@ -228,9 +229,7 @@ export function buildAgentDraftLaunchPlan(args: {
|
||||
return null
|
||||
}
|
||||
const launchCommand =
|
||||
agent === 'omp' && !args.isRemote
|
||||
? withFreshOmpLaunch(baseCommand.command, shell)
|
||||
: baseCommand.command
|
||||
agent === 'omp' ? withFreshOmpLaunch(baseCommand.command, shell) : baseCommand.command
|
||||
const launchConfig = buildSleepingAgentLaunchConfig({
|
||||
...args,
|
||||
// Why: see the new-session path above — resume must not replay picker flags.
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { mkdtempSync, writeFileSync, readFileSync, rmSync, mkdirSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import assert from 'node:assert/strict'
|
||||
import { runProcess } from '../../src/shared/child-process/run-process'
|
||||
import { withFreshOmpLaunch } from '../../src/shared/omp-fresh-launch'
|
||||
if (process.platform !== 'win32') {
|
||||
throw new Error('Run this smoke on a Windows host')
|
||||
}
|
||||
const root = mkdtempSync(join(tmpdir(), 'orca-fresh-windows-'))
|
||||
const config = join(root, 'fresh settings.yml')
|
||||
const capture = join(root, 'calls')
|
||||
const results = []
|
||||
try {
|
||||
writeFileSync(join(root, 'omp.cmd'), '@echo off\r\necho %*>>"%CAPTURE%"\r\nexit /b 17\r\n')
|
||||
for (const shell of ['cmd', 'powershell'] as const) {
|
||||
for (const state of ['present', 'missing', 'unset', 'directory']) {
|
||||
rmSync(config, { force: true, recursive: true })
|
||||
rmSync(capture, { force: true })
|
||||
if (state === 'present') {
|
||||
writeFileSync(config, 'autoResume: false\n')
|
||||
}
|
||||
if (state === 'directory') {
|
||||
mkdirSync(config)
|
||||
}
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
ORCA_BACKGROUND_LAUNCH: '1',
|
||||
ORCA_OMP_FRESH_CONFIG: config,
|
||||
CAPTURE: capture
|
||||
}
|
||||
if (state === 'unset') {
|
||||
delete env.ORCA_OMP_FRESH_CONFIG
|
||||
}
|
||||
const command = withFreshOmpLaunch('omp', shell, ' "task with spaces"')
|
||||
const runner = join(root, 'run.cmd')
|
||||
writeFileSync(runner, `@echo off\r\n${command}\r\n`)
|
||||
const result = await runProcess({
|
||||
program: shell === 'cmd' ? runner : 'powershell.exe',
|
||||
args:
|
||||
shell === 'cmd'
|
||||
? []
|
||||
: [
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-Command',
|
||||
`function omp { $args | ConvertTo-Json -Compress | Add-Content -LiteralPath $env:CAPTURE; $global:LASTEXITCODE = 17 }; ${
|
||||
command
|
||||
}; exit $global:LASTEXITCODE`
|
||||
],
|
||||
cwd: root,
|
||||
env
|
||||
})
|
||||
assert.equal(
|
||||
result.code,
|
||||
state === 'present' ? 17 : 1,
|
||||
JSON.stringify({ shell, state, ...result })
|
||||
)
|
||||
if (state === 'present') {
|
||||
const calls = readFileSync(capture, 'utf8').trim().split(/\r?\n/)
|
||||
assert.equal(calls.length, 1)
|
||||
assert.ok(calls[0].includes('--config'))
|
||||
assert.ok(calls[0].includes('task with spaces'))
|
||||
} else {
|
||||
assert.ok(
|
||||
result.stderr.includes('fresh OMP settings are unavailable'),
|
||||
JSON.stringify(result)
|
||||
)
|
||||
assert.throws(() => readFileSync(capture))
|
||||
}
|
||||
results.push({ shell, state, code: result.code, passed: true })
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify(results))
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
Reference in New Issue
Block a user