mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 00:03:46 +00:00
feat(agent-hooks): support Prime Agent status in WSL
This commit is contained in:
@@ -233,6 +233,13 @@ describe('WslHookRelayManager', () => {
|
||||
return { manager: new WslHookRelayManager(deps), deps }
|
||||
}
|
||||
|
||||
it('exposes the stable guest endpoint instance key with a port fallback', () => {
|
||||
expect(createManager({}).manager.getInstanceKey()).toBe('testinstance')
|
||||
expect(createManager({ instanceKey: () => 'unsafe/key' }).manager.getInstanceKey()).toBe(
|
||||
'port43117'
|
||||
)
|
||||
})
|
||||
|
||||
it('starts one relay per distro, installs hooks, exposes the guest endpoint path, and forwards envelopes', async () => {
|
||||
const { manager, deps } = createManager({})
|
||||
manager.ensureForDistro('Ubuntu')
|
||||
|
||||
@@ -109,6 +109,15 @@ export class WslHookRelayManager {
|
||||
return this.stateFor(distro)?.opencodeOverlayDir ?? null
|
||||
}
|
||||
|
||||
getInstanceKey(): string | null {
|
||||
const stableKey = sanitizeWslHookInstanceKey(this.deps.instanceKey() ?? undefined)
|
||||
if (stableKey) {
|
||||
return stableKey
|
||||
}
|
||||
const port = Number(this.deps.hookCoordsEnv().ORCA_AGENT_HOOK_PORT ?? '')
|
||||
return Number.isInteger(port) && port > 0 ? `port${port}` : null
|
||||
}
|
||||
|
||||
/** Kills every live relay. Non-permanent (hooks switched off mid-session) leaves the
|
||||
* manager reusable, so re-enabling hooks can start relays again without an app restart. */
|
||||
disposeAll({ permanent = true }: { permanent?: boolean } = {}): void {
|
||||
@@ -172,8 +181,7 @@ export class WslHookRelayManager {
|
||||
}
|
||||
// Why: restart-stable instance identity keeps the guest endpoint file at
|
||||
// ONE path across restarts so daemon-surviving agents re-coordinate.
|
||||
const instanceKey =
|
||||
sanitizeWslHookInstanceKey(this.deps.instanceKey() ?? undefined) ?? `port${port}`
|
||||
const instanceKey = this.getInstanceKey() ?? `port${port}`
|
||||
if (existing) {
|
||||
this.recovery.clearTimers(existing)
|
||||
}
|
||||
|
||||
@@ -604,6 +604,8 @@ describePosix('daemon shell-ready launch config', () => {
|
||||
'[[ -n "${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="${ORCA_CODEX_HOME}"'
|
||||
const agentTeamsPathRestoreLine = '[[ -n "${ORCA_AGENT_TEAMS_SHIM_DIR:-}" ]] || return 0'
|
||||
const ompWrapperLine = 'command omp --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"'
|
||||
const primeWrapperLine =
|
||||
'command prime-agent --extension "${ORCA_PRIME_AGENT_STATUS_EXTENSION}" "$@"'
|
||||
expect(zshrc).toContain(restoreLine)
|
||||
expect(zlogin).toContain(restoreLine)
|
||||
expect(bashRc).toContain(restoreLine)
|
||||
@@ -625,6 +627,9 @@ describePosix('daemon shell-ready launch config', () => {
|
||||
expect(zshrc).toContain(ompWrapperLine)
|
||||
expect(zlogin).toContain(ompWrapperLine)
|
||||
expect(bashRc).toContain(ompWrapperLine)
|
||||
expect(zshrc).toContain(primeWrapperLine)
|
||||
expect(zlogin).toContain(primeWrapperLine)
|
||||
expect(bashRc).toContain(primeWrapperLine)
|
||||
})
|
||||
|
||||
// Why: regression guard for issue #2422 — bash wrapper must emit OSC 133 C/D so SSH sessions clear stale 'working' agent rows.
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
isPowerShellExecutableName
|
||||
} from '../powershell-osc133-bootstrap'
|
||||
import { getPosixOmpShellWrapper } from '../pty/omp-shell-wrapper'
|
||||
import { getPosixPrimeAgentShellWrapper } from '../pty/prime-agent-shell-wrapper'
|
||||
import {
|
||||
getFishShellReadyInitCommand,
|
||||
getZshEnvTemplate,
|
||||
@@ -122,6 +123,7 @@ __orca_restore_agent_teams_path
|
||||
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
|
||||
[[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}"
|
||||
${getPosixOmpShellWrapper()}
|
||||
${getPosixPrimeAgentShellWrapper()}
|
||||
# Why: Codex must keep using Orca's runtime CODEX_HOME after profile scripts.
|
||||
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"
|
||||
# Why: emit OSC 133 C/D so terminal-command-lifecycle can drop stale agent
|
||||
@@ -243,6 +245,7 @@ if [[ ! -o login ]]; then
|
||||
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
|
||||
[[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}"
|
||||
${getPosixOmpShellWrapper()}
|
||||
${getPosixPrimeAgentShellWrapper()}
|
||||
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"
|
||||
fi
|
||||
__orca_osc133_precmd() {
|
||||
@@ -306,6 +309,7 @@ __orca_restore_agent_teams_path
|
||||
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
|
||||
[[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}"
|
||||
${getPosixOmpShellWrapper()}
|
||||
${getPosixPrimeAgentShellWrapper()}
|
||||
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"
|
||||
${getZshShellReadyMarkerRegistrationBlock(SHELL_READY_MARKER)}
|
||||
${getZshFinalZdotdirRestoreBlock()}
|
||||
|
||||
@@ -55,6 +55,7 @@ const {
|
||||
clearAgentHookPaneStateMock,
|
||||
registerPaneKeyAliasMock,
|
||||
piBuildPtyEnvMock,
|
||||
piBuildStatusOnlyPtyEnvMock,
|
||||
piClearPtyMock,
|
||||
isPwshAvailableMock,
|
||||
trackMock,
|
||||
@@ -91,6 +92,7 @@ const {
|
||||
clearAgentHookPaneStateMock: vi.fn(),
|
||||
registerPaneKeyAliasMock: vi.fn(),
|
||||
piBuildPtyEnvMock: vi.fn(),
|
||||
piBuildStatusOnlyPtyEnvMock: vi.fn(),
|
||||
piClearPtyMock: vi.fn(),
|
||||
trackMock: vi.fn(),
|
||||
classifyErrorMock: vi.fn(),
|
||||
@@ -175,6 +177,7 @@ vi.mock('../agent-hooks/server', () => ({
|
||||
vi.mock('../pi/titlebar-extension-service', () => ({
|
||||
piTitlebarExtensionService: {
|
||||
buildPtyEnv: piBuildPtyEnvMock,
|
||||
buildStatusOnlyPtyEnv: piBuildStatusOnlyPtyEnvMock,
|
||||
clearPty: piClearPtyMock
|
||||
}
|
||||
}))
|
||||
@@ -333,6 +336,7 @@ describe('registerPtyHandlers', () => {
|
||||
const savedOrcaOmpStatusExtension = process.env.ORCA_OMP_STATUS_EXTENSION
|
||||
const savedPrimeAgentDir = process.env.PRIME_AGENT_CODING_AGENT_DIR
|
||||
const savedOrcaPrimeAgentSourceDir = process.env.ORCA_PRIME_AGENT_SOURCE_AGENT_DIR
|
||||
const savedOrcaPrimeAgentStatusExtension = process.env.ORCA_PRIME_AGENT_STATUS_EXTENSION
|
||||
const savedOrcaClaudeAgentStatusSettings = process.env.ORCA_CLAUDE_AGENT_STATUS_SETTINGS
|
||||
const savedProcessPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
|
||||
const savedDisableMacosLoginShell = process.env.ORCA_DISABLE_MACOS_LOGIN_SHELL
|
||||
@@ -360,6 +364,7 @@ describe('registerPtyHandlers', () => {
|
||||
delete process.env.ORCA_OMP_STATUS_EXTENSION
|
||||
delete process.env.PRIME_AGENT_CODING_AGENT_DIR
|
||||
delete process.env.ORCA_PRIME_AGENT_SOURCE_AGENT_DIR
|
||||
delete process.env.ORCA_PRIME_AGENT_STATUS_EXTENSION
|
||||
handlers.clear()
|
||||
handleMock.mockReset()
|
||||
onMock.mockReset()
|
||||
@@ -382,6 +387,7 @@ describe('registerPtyHandlers', () => {
|
||||
clearAgentHookPaneStateMock.mockReset()
|
||||
registerPaneKeyAliasMock.mockReset()
|
||||
piBuildPtyEnvMock.mockReset()
|
||||
piBuildStatusOnlyPtyEnvMock.mockReset()
|
||||
piClearPtyMock.mockReset()
|
||||
isPwshAvailableMock.mockReset()
|
||||
trackMock.mockReset()
|
||||
@@ -481,6 +487,10 @@ describe('registerPtyHandlers', () => {
|
||||
}
|
||||
}
|
||||
)
|
||||
piBuildStatusOnlyPtyEnvMock.mockReturnValue({
|
||||
ORCA_PRIME_AGENT_STATUS_EXTENSION:
|
||||
'C:\\Users\\test\\AppData\\Roaming\\Orca\\prime-agent-managed-status-extension\\orca-agent-status.ts'
|
||||
})
|
||||
isPwshAvailableMock.mockReturnValue(false)
|
||||
spawnMock.mockReturnValue({
|
||||
onData: vi.fn(() => makeDisposable()),
|
||||
@@ -586,6 +596,11 @@ describe('registerPtyHandlers', () => {
|
||||
} else {
|
||||
delete process.env.ORCA_PRIME_AGENT_SOURCE_AGENT_DIR
|
||||
}
|
||||
if (savedOrcaPrimeAgentStatusExtension !== undefined) {
|
||||
process.env.ORCA_PRIME_AGENT_STATUS_EXTENSION = savedOrcaPrimeAgentStatusExtension
|
||||
} else {
|
||||
delete process.env.ORCA_PRIME_AGENT_STATUS_EXTENSION
|
||||
}
|
||||
if (savedOrcaClaudeAgentStatusSettings === undefined) {
|
||||
delete process.env.ORCA_CLAUDE_AGENT_STATUS_SETTINGS
|
||||
} else {
|
||||
@@ -4196,7 +4211,7 @@ describe('registerPtyHandlers', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('does not materialize Windows-host Prime extensions for a WSL launch', async () => {
|
||||
it('bridges a host-managed Prime status extension into WSL without redirecting Prime home', async () => {
|
||||
await withWin32Platform(async () => {
|
||||
const env = await daemonSpawnAndGetEnv(
|
||||
{ PRIME_AGENT_CODING_AGENT_DIR: 'C:\\Users\\test\\.prime\\agent' },
|
||||
@@ -4207,11 +4222,30 @@ describe('registerPtyHandlers', () => {
|
||||
)
|
||||
|
||||
expect(piBuildPtyEnvMock).not.toHaveBeenCalled()
|
||||
expect(piBuildStatusOnlyPtyEnvMock).toHaveBeenCalledWith('prime-agent')
|
||||
expect(env.ORCA_PRIME_AGENT_SOURCE_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_PRIME_AGENT_STATUS_EXTENSION).toBe(
|
||||
'C:\\Users\\test\\AppData\\Roaming\\Orca\\prime-agent-managed-status-extension\\orca-agent-status.ts'
|
||||
)
|
||||
expect(env.ORCA_WSL_HOOK_INSTANCE).toBe('port5678')
|
||||
expect(env.PRIME_AGENT_CODING_AGENT_DIR).toBe('C:\\Users\\test\\.prime\\agent')
|
||||
})
|
||||
})
|
||||
|
||||
it('prepares the Prime bridge for a typed launch in a bare WSL shell', async () => {
|
||||
await withWin32Platform(async () => {
|
||||
const env = await daemonSpawnAndGetEnv({}, undefined, undefined, undefined, {
|
||||
shellOverride: 'wsl.exe'
|
||||
})
|
||||
|
||||
expect(piBuildStatusOnlyPtyEnvMock).toHaveBeenCalledWith('prime-agent')
|
||||
expect(env.ORCA_PRIME_AGENT_STATUS_EXTENSION).toContain(
|
||||
'prime-agent-managed-status-extension'
|
||||
)
|
||||
expect(env.PRIME_AGENT_CODING_AGENT_DIR).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
it('points OPENCODE_CONFIG_DIR at the guest overlay when the WSL relay reports it', async () => {
|
||||
const guestDir = '/home/jin/.orca-relay/opencode-overlays/abc'
|
||||
const spy = vi.spyOn(wslHookRelayManager, 'getOpenCodeOverlayDir').mockReturnValue(guestDir)
|
||||
|
||||
+23
-7
@@ -1513,6 +1513,7 @@ function clearPiAgentShadowEnv(baseEnv: Record<string, string>, kind: PiAgentKin
|
||||
}
|
||||
if (kind === 'prime-agent') {
|
||||
delete baseEnv.ORCA_PRIME_AGENT_SOURCE_AGENT_DIR
|
||||
delete baseEnv.ORCA_PRIME_AGENT_STATUS_EXTENSION
|
||||
return
|
||||
}
|
||||
delete baseEnv.ORCA_PI_CODING_AGENT_DIR
|
||||
@@ -1544,6 +1545,11 @@ function exposePiManagedExtensionEnv(
|
||||
} else {
|
||||
delete baseEnv.ORCA_PRIME_AGENT_SOURCE_AGENT_DIR
|
||||
}
|
||||
if (managedEnv.ORCA_PRIME_AGENT_STATUS_EXTENSION) {
|
||||
baseEnv.ORCA_PRIME_AGENT_STATUS_EXTENSION = managedEnv.ORCA_PRIME_AGENT_STATUS_EXTENSION
|
||||
} else {
|
||||
delete baseEnv.ORCA_PRIME_AGENT_STATUS_EXTENSION
|
||||
}
|
||||
return
|
||||
}
|
||||
delete baseEnv.ORCA_PI_CODING_AGENT_DIR
|
||||
@@ -1686,6 +1692,8 @@ export function buildPtyHostEnv(
|
||||
})
|
||||
|
||||
const shouldPrepareOmpShadow = piAgentKind === 'omp' || !hasLaunchCommand
|
||||
const shouldPreparePrimeStatus =
|
||||
piAgentKind === 'prime-agent' || (opts.isWsl && !hasLaunchCommand)
|
||||
// Why: source shadows are agent-scoped; trusting the other kind's source reintroduces Pi/OMP extension-state shadowing.
|
||||
const preexistingPiAgentDir = resolvePiAgentSourceDir(baseEnv, 'pi')
|
||||
const preexistingOmpAgentDir =
|
||||
@@ -1744,6 +1752,12 @@ export function buildPtyHostEnv(
|
||||
if (opts.isWsl === true) {
|
||||
// Why: hook POSTs to 127.0.0.1 die inside WSL's NAT namespace; use the guest-resident relay's endpoint instead of the Windows one.
|
||||
const distro = opts.wslDistro ?? null
|
||||
const hookInstance = wslHookRelayManager.getInstanceKey()
|
||||
if (hookInstance) {
|
||||
baseEnv.ORCA_WSL_HOOK_INSTANCE = hookInstance
|
||||
} else {
|
||||
delete baseEnv.ORCA_WSL_HOOK_INSTANCE
|
||||
}
|
||||
wslHookRelayManager.ensureForDistro(distro)
|
||||
const guestEndpoint = wslHookRelayManager.getGuestEndpointFilePath(distro)
|
||||
if (guestEndpoint) {
|
||||
@@ -1790,13 +1804,14 @@ export function buildPtyHostEnv(
|
||||
exposePiManagedExtensionEnv(baseEnv, 'omp', ompEnv)
|
||||
}
|
||||
|
||||
if (piAgentKind === 'prime-agent' && !opts.isWsl) {
|
||||
const primeEnv = piTitlebarExtensionService.buildPtyEnv(
|
||||
id,
|
||||
preexistingPrimeAgentDir,
|
||||
'prime-agent',
|
||||
{ materializeDefaultHome: explicitPiAgentKind === 'prime-agent' }
|
||||
)
|
||||
if (shouldPreparePrimeStatus) {
|
||||
// Why: the Windows host cannot safely resolve or write Prime's guest
|
||||
// config root; WSL loads the host-managed status file explicitly.
|
||||
const primeEnv = opts.isWsl
|
||||
? piTitlebarExtensionService.buildStatusOnlyPtyEnv('prime-agent')
|
||||
: piTitlebarExtensionService.buildPtyEnv(id, preexistingPrimeAgentDir, 'prime-agent', {
|
||||
materializeDefaultHome: explicitPiAgentKind === 'prime-agent'
|
||||
})
|
||||
Object.assign(baseEnv, primeEnv)
|
||||
exposePiManagedExtensionEnv(baseEnv, 'prime-agent', primeEnv)
|
||||
}
|
||||
@@ -1814,6 +1829,7 @@ export function buildPtyHostEnv(
|
||||
})
|
||||
delete baseEnv.ORCA_OMP_STATUS_EXTENSION
|
||||
delete baseEnv.ORCA_PRIME_AGENT_SOURCE_AGENT_DIR
|
||||
delete baseEnv.ORCA_PRIME_AGENT_STATUS_EXTENSION
|
||||
}
|
||||
|
||||
// Why: keep the Codex home override PTY-scoped so dev/prod Orcas don't share hooks through ~/.codex.
|
||||
|
||||
@@ -30,6 +30,7 @@ type Harness = {
|
||||
fsMock: {
|
||||
existsSync: ReturnType<typeof vi.fn>
|
||||
readFileSync: ReturnType<typeof vi.fn>
|
||||
statSync: ReturnType<typeof vi.fn>
|
||||
}
|
||||
handlers: Record<string, HookHandler>
|
||||
processEnv: Record<string, string | undefined>
|
||||
@@ -62,6 +63,7 @@ function createHarness(args: {
|
||||
argv?: string[]
|
||||
existsSync?: (path: string) => boolean
|
||||
readFileSync?: (path: string, encoding: string) => string
|
||||
statSync?: (path: string) => { mtimeMs: number; size: number; ino: number }
|
||||
fetchImpl?: (...params: Parameters<typeof fetch>) => Promise<unknown>
|
||||
}): Harness {
|
||||
const fetchMock = vi.fn(
|
||||
@@ -86,6 +88,12 @@ function createHarness(args: {
|
||||
|
||||
const fsMock = {
|
||||
existsSync: vi.fn(args.existsSync ?? (() => false)),
|
||||
statSync: vi.fn(
|
||||
args.statSync ??
|
||||
((path: string) => {
|
||||
throw Object.assign(new Error(`ENOENT: ${path}`), { code: 'ENOENT' })
|
||||
})
|
||||
),
|
||||
readFileSync: vi.fn(
|
||||
args.readFileSync ??
|
||||
((path: string) => {
|
||||
@@ -562,6 +570,31 @@ describe('getPiAgentStatusExtensionSource', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('uses current Windows coordinates when a same-token guest endpoint is stale', async () => {
|
||||
const endpointPath = '/home/u/.orca-wsl/agent-hooks/instance-test/endpoint.env'
|
||||
const harness = createHarness({
|
||||
kind: 'prime-agent',
|
||||
env: { WSL_DISTRO_NAME: 'Ubuntu', ORCA_AGENT_HOOK_ENDPOINT: endpointPath },
|
||||
existsSync: (path) => path === '/mnt/c/Windows/System32/curl.exe',
|
||||
statSync: () => ({ mtimeMs: 1, size: 80, ino: 1 }),
|
||||
readFileSync: (path) => {
|
||||
if (path === endpointPath) {
|
||||
return 'ORCA_AGENT_HOOK_PORT=9999\nORCA_AGENT_HOOK_TOKEN=token-1\n'
|
||||
}
|
||||
throw Object.assign(new Error(`ENOENT: ${path}`), { code: 'ENOENT' })
|
||||
},
|
||||
fetchImpl: vi.fn(async () => {
|
||||
throw new Error('stale guest relay')
|
||||
})
|
||||
})
|
||||
|
||||
await harness.callHook('agent_start')
|
||||
|
||||
expect(harness.fetchMock.mock.calls[0]?.[0]).toBe('http://127.0.0.1:9999/hook/prime-agent')
|
||||
await vi.waitFor(() => expect(harness.spawnMock).toHaveBeenCalledTimes(1))
|
||||
expect(harness.spawnMock.mock.calls[0]?.[1]).toContain('http://127.0.0.1:4321/hook/prime-agent')
|
||||
})
|
||||
|
||||
it('probes WSL evidence and the curl path once per process', async () => {
|
||||
const harness = createHarness({
|
||||
kind: 'omp',
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import type { PiAgentKind } from '../../shared/pi-agent-kind'
|
||||
import { getPiAgentStatusHandlerSourceLines } from './agent-status-handler-source'
|
||||
import { getPiAgentStatusRuntimeDetectionSourceLines } from './agent-status-runtime-detection-source'
|
||||
import { getPiAgentStatusWslCurlSourceLines } from './agent-status-wsl-curl-source'
|
||||
|
||||
export const ORCA_PI_AGENT_STATUS_EXTENSION_FILE = 'orca-agent-status.ts'
|
||||
|
||||
@@ -228,92 +229,13 @@ export function getPiAgentStatusExtensionSource(kind: PiAgentKind = 'pi'): strin
|
||||
' // Why: status reporting must never fail the pi run just because Orca',
|
||||
' // is unavailable or the loopback request failed (e.g. Orca restart).',
|
||||
' if (!isWslRuntime()) return',
|
||||
' postViaWindowsCurl(url, coords, body)',
|
||||
' postViaWindowsCurl(body, ompRuntime)',
|
||||
' } finally {',
|
||||
' if (timeout) clearTimeout(timeout)',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
'// Why: WSL-ness and curl.exe presence cannot change within a process',
|
||||
'// lifetime; re-probing /proc and /mnt/c on every failed event would add',
|
||||
'// filesystem work to the per-event path.',
|
||||
'let cachedIsWslRuntime: boolean | null = null',
|
||||
'let cachedWindowsCurlPath: string | null | undefined',
|
||||
'',
|
||||
'function isWslRuntime(): boolean {',
|
||||
' if (cachedIsWslRuntime !== null) return cachedIsWslRuntime',
|
||||
' cachedIsWslRuntime = detectWslRuntime()',
|
||||
' return cachedIsWslRuntime',
|
||||
'}',
|
||||
'',
|
||||
'function detectWslRuntime(): boolean {',
|
||||
' if (process.env.WSL_DISTRO_NAME) return true',
|
||||
' try {',
|
||||
" const fs = require('fs')",
|
||||
" for (const path of ['/proc/sys/kernel/osrelease', '/proc/version']) {",
|
||||
' try {',
|
||||
" const contents = String(fs.readFileSync(path, 'utf8'))",
|
||||
' if (/microsoft|wsl/i.test(contents)) return true',
|
||||
' } catch {',
|
||||
' // Why: probe the next runtime hint when a proc file is absent or unreadable.',
|
||||
' }',
|
||||
' }',
|
||||
' } catch {',
|
||||
' return false',
|
||||
' }',
|
||||
' return false',
|
||||
'}',
|
||||
'',
|
||||
'function resolveWindowsCurlPath(): string | null {',
|
||||
' if (cachedWindowsCurlPath !== undefined) return cachedWindowsCurlPath',
|
||||
' try {',
|
||||
" const fs = require('fs')",
|
||||
" const curlPath = '/mnt/c/Windows/System32/curl.exe'",
|
||||
' cachedWindowsCurlPath = fs.existsSync(curlPath) ? curlPath : null',
|
||||
' } catch {',
|
||||
' cachedWindowsCurlPath = null',
|
||||
' }',
|
||||
' return cachedWindowsCurlPath',
|
||||
'}',
|
||||
'',
|
||||
'// Why: WSL loopback is not the Windows loopback, so a WSL-side POST cannot',
|
||||
'// reach Orca. curl.exe runs on the Windows side, where 127.0.0.1 IS the',
|
||||
'// listener Orca binds. Fire-and-forget: blocking on the spawn would stall',
|
||||
'// the pi event loop (and the TUI) on every hook event.',
|
||||
'function postViaWindowsCurl(url: string, coords: { token: string }, body: string): void {',
|
||||
' const curlPath = resolveWindowsCurlPath()',
|
||||
' if (!curlPath) return',
|
||||
' try {',
|
||||
" const { spawn } = require('child_process')",
|
||||
' const child = spawn(',
|
||||
' curlPath,',
|
||||
' [',
|
||||
" '-sS',",
|
||||
' // Why: the spawn is detached from the event loop, so these bounds',
|
||||
' // size a background process, not TUI latency. WSL->Win32 interop',
|
||||
' // connects can exceed 0.5s on loaded machines (observed 3/3 drops',
|
||||
' // to a healthy listener); size for delivery, not snappiness.',
|
||||
" '--connect-timeout', '3',",
|
||||
" '--max-time', '10',",
|
||||
" '--noproxy', '127.0.0.1',",
|
||||
" '-o', 'NUL',",
|
||||
" '-X', 'POST',",
|
||||
" '-H', 'Content-Type: application/json',",
|
||||
" '-H', `X-Orca-Agent-Hook-Token: ${coords.token}`,",
|
||||
" '--data-binary', '@-',",
|
||||
' url',
|
||||
' ],',
|
||||
" { stdio: ['pipe', 'ignore', 'ignore'] }",
|
||||
' )',
|
||||
" child.on('error', () => {})",
|
||||
" child.stdin.on('error', () => {})",
|
||||
' child.stdin.end(body)',
|
||||
' } catch {',
|
||||
' // Why: the bridge is best-effort; a failed spawn must not surface',
|
||||
' // inside the pi TUI.',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
...getPiAgentStatusWslCurlSourceLines(),
|
||||
...getPiAgentStatusHandlerSourceLines(kind)
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
export function getPiAgentStatusWslCurlSourceLines(): string[] {
|
||||
return [
|
||||
'// Why: WSL-ness and curl.exe presence cannot change within a process',
|
||||
'// lifetime; re-probing /proc and /mnt/c on every failed event would add',
|
||||
'// filesystem work to the per-event path.',
|
||||
'let cachedIsWslRuntime: boolean | null = null',
|
||||
'let cachedWindowsCurlPath: string | null | undefined',
|
||||
'',
|
||||
'function isWslRuntime(): boolean {',
|
||||
' if (cachedIsWslRuntime !== null) return cachedIsWslRuntime',
|
||||
' cachedIsWslRuntime = detectWslRuntime()',
|
||||
' return cachedIsWslRuntime',
|
||||
'}',
|
||||
'',
|
||||
'function detectWslRuntime(): boolean {',
|
||||
' if (process.env.WSL_DISTRO_NAME) return true',
|
||||
' try {',
|
||||
" const fs = require('fs')",
|
||||
" for (const path of ['/proc/sys/kernel/osrelease', '/proc/version']) {",
|
||||
' try {',
|
||||
" const contents = String(fs.readFileSync(path, 'utf8'))",
|
||||
' if (/microsoft|wsl/i.test(contents)) return true',
|
||||
' } catch {',
|
||||
' // Why: probe the next runtime hint when a proc file is absent or unreadable.',
|
||||
' }',
|
||||
' }',
|
||||
' } catch {',
|
||||
' return false',
|
||||
' }',
|
||||
' return false',
|
||||
'}',
|
||||
'',
|
||||
'function resolveWindowsCurlPath(): string | null {',
|
||||
' if (cachedWindowsCurlPath !== undefined) return cachedWindowsCurlPath',
|
||||
' try {',
|
||||
" const fs = require('fs')",
|
||||
" const curlPath = '/mnt/c/Windows/System32/curl.exe'",
|
||||
' cachedWindowsCurlPath = fs.existsSync(curlPath) ? curlPath : null',
|
||||
' } catch {',
|
||||
' cachedWindowsCurlPath = null',
|
||||
' }',
|
||||
' return cachedWindowsCurlPath',
|
||||
'}',
|
||||
'',
|
||||
'// Why: WSL loopback is not the Windows loopback, so use curl.exe on the host.',
|
||||
'function postViaWindowsCurl(body: string, ompRuntime: boolean): void {',
|
||||
' const curlPath = resolveWindowsCurlPath()',
|
||||
' const windowsPort = process.env.ORCA_AGENT_HOOK_PORT',
|
||||
' const windowsToken = process.env.ORCA_AGENT_HOOK_TOKEN',
|
||||
' if (!curlPath || !windowsPort || !windowsToken) return',
|
||||
' // Why: a stale guest endpoint must fall back to current host coordinates.',
|
||||
' const windowsUrl = `http://127.0.0.1:${windowsPort}${resolveHookPath(ompRuntime)}`',
|
||||
' try {',
|
||||
" const { spawn } = require('child_process')",
|
||||
' const child = spawn(',
|
||||
' curlPath,',
|
||||
' [',
|
||||
" '-sS',",
|
||||
' // Why: detached delivery may take seconds under loaded WSL interop.',
|
||||
" '--connect-timeout', '3',",
|
||||
" '--max-time', '10',",
|
||||
" '--noproxy', '127.0.0.1',",
|
||||
" '-o', 'NUL',",
|
||||
" '-X', 'POST',",
|
||||
" '-H', 'Content-Type: application/json',",
|
||||
" '-H', `X-Orca-Agent-Hook-Token: ${windowsToken}`,",
|
||||
" '--data-binary', '@-',",
|
||||
' windowsUrl',
|
||||
' ],',
|
||||
" { stdio: ['pipe', 'ignore', 'ignore'] }",
|
||||
' )',
|
||||
" child.on('error', () => {})",
|
||||
" child.stdin.on('error', () => {})",
|
||||
' child.stdin.end(body)',
|
||||
' } catch {',
|
||||
' // Why: status delivery must not surface inside the agent TUI.',
|
||||
' }',
|
||||
'}',
|
||||
''
|
||||
]
|
||||
}
|
||||
@@ -94,6 +94,10 @@ describe('PiTitlebarExtensionService', () => {
|
||||
rmSync(join(userDataDir, 'pi-agent-overlays'), { recursive: true, force: true })
|
||||
rmSync(join(userDataDir, 'omp-agent-overlays'), { recursive: true, force: true })
|
||||
rmSync(join(userDataDir, 'omp-managed-status-extension'), { recursive: true, force: true })
|
||||
rmSync(join(userDataDir, 'prime-agent-managed-status-extension'), {
|
||||
recursive: true,
|
||||
force: true
|
||||
})
|
||||
})
|
||||
|
||||
function expectPiHomeIntact(): void {
|
||||
@@ -181,6 +185,21 @@ describe('PiTitlebarExtensionService', () => {
|
||||
expectPiHomeIntact()
|
||||
})
|
||||
|
||||
it('writes a Prime status-only extension outside the guest-owned config dir', () => {
|
||||
const svc = new PiTitlebarExtensionService()
|
||||
const env = svc.buildStatusOnlyPtyEnv('prime-agent')
|
||||
const statusPath = join(
|
||||
userDataDir,
|
||||
'prime-agent-managed-status-extension',
|
||||
'orca-agent-status.ts'
|
||||
)
|
||||
|
||||
expect(env).toEqual({ ORCA_PRIME_AGENT_STATUS_EXTENSION: statusPath })
|
||||
expect(readFileSync(statusPath, 'utf8')).toContain('/hook/prime-agent')
|
||||
expect(readFileSync(statusPath, 'utf8')).not.toContain("return '/hook/omp'")
|
||||
expectPiHomeIntact()
|
||||
})
|
||||
|
||||
it('uses the same source dir for multiple PTYs with the same Pi dir', () => {
|
||||
const svc = new PiTitlebarExtensionService()
|
||||
const firstEnv = svc.buildPtyEnv('pty-shared-1', piHome, 'pi')
|
||||
|
||||
@@ -27,7 +27,10 @@ export const isSafeDescendCandidate = sharedIsSafeDescendCandidate
|
||||
|
||||
const PI_AGENT_SUBDIR = 'agent'
|
||||
const ORCA_MANAGED_EXTENSION_MARKER = '@orca-managed-pi-extension'
|
||||
const OMP_MANAGED_STATUS_EXTENSION_DIR = 'omp-managed-status-extension'
|
||||
const MANAGED_STATUS_EXTENSION_DIR: Record<'omp' | 'prime-agent', string> = {
|
||||
omp: 'omp-managed-status-extension',
|
||||
'prime-agent': 'prime-agent-managed-status-extension'
|
||||
}
|
||||
|
||||
type ManagedExtensionWriteResult = 'written' | 'skipped-user-owned' | 'failed'
|
||||
|
||||
@@ -121,8 +124,11 @@ export class PiTitlebarExtensionService {
|
||||
}
|
||||
}
|
||||
|
||||
private writeOmpFallbackStatusExtension(source: string): string | undefined {
|
||||
const fallbackDir = join(app.getPath('userData'), OMP_MANAGED_STATUS_EXTENSION_DIR)
|
||||
private writeFallbackStatusExtension(
|
||||
source: string,
|
||||
kind: keyof typeof MANAGED_STATUS_EXTENSION_DIR
|
||||
): string | undefined {
|
||||
const fallbackDir = join(app.getPath('userData'), MANAGED_STATUS_EXTENSION_DIR[kind])
|
||||
try {
|
||||
mkdirSync(fallbackDir, { recursive: true })
|
||||
} catch {
|
||||
@@ -165,7 +171,7 @@ export class PiTitlebarExtensionService {
|
||||
statusResult === 'written'
|
||||
? statusExtensionPath
|
||||
: kind === 'omp'
|
||||
? this.writeOmpFallbackStatusExtension(statusSource)
|
||||
? this.writeFallbackStatusExtension(statusSource, 'omp')
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
@@ -195,7 +201,7 @@ export class PiTitlebarExtensionService {
|
||||
if (!existsSync(sourceAgentDir) && !materializeDefaultHome) {
|
||||
if (kind === 'omp') {
|
||||
const statusSource = withOrcaManagedExtensionMarker(getPiAgentStatusExtensionSource(kind))
|
||||
const statusExtensionPath = this.writeOmpFallbackStatusExtension(statusSource)
|
||||
const statusExtensionPath = this.writeFallbackStatusExtension(statusSource, 'omp')
|
||||
return statusExtensionPath ? { ORCA_OMP_STATUS_EXTENSION: statusExtensionPath } : {}
|
||||
}
|
||||
return {}
|
||||
@@ -220,6 +226,12 @@ export class PiTitlebarExtensionService {
|
||||
return env
|
||||
}
|
||||
|
||||
buildStatusOnlyPtyEnv(kind: 'prime-agent'): Record<string, string> {
|
||||
const source = withOrcaManagedExtensionMarker(getPiAgentStatusExtensionSource(kind))
|
||||
const statusExtensionPath = this.writeFallbackStatusExtension(source, kind)
|
||||
return statusExtensionPath ? { ORCA_PRIME_AGENT_STATUS_EXTENSION: statusExtensionPath } : {}
|
||||
}
|
||||
|
||||
clearPty(ptyId: string): void {
|
||||
// Why: PTY teardown doesn't know which kind was launched (the daemon
|
||||
// exit path discards the launch command). Sweep both old PTY-scoped
|
||||
|
||||
@@ -526,6 +526,8 @@ describePosix('local PTY shell-ready launch config', () => {
|
||||
'[[ -n "${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="${ORCA_CODEX_HOME}"'
|
||||
const agentTeamsPathRestoreLine = '[[ -n "${ORCA_AGENT_TEAMS_SHIM_DIR:-}" ]] || return 0'
|
||||
const ompWrapperLine = 'command omp --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"'
|
||||
const primeWrapperLine =
|
||||
'command prime-agent --extension "${ORCA_PRIME_AGENT_STATUS_EXTENSION}" "$@"'
|
||||
expect(zshrc).toContain(restoreLine)
|
||||
expect(zlogin).toContain(restoreLine)
|
||||
expect(bashRc).toContain(restoreLine)
|
||||
@@ -547,6 +549,9 @@ describePosix('local PTY shell-ready launch config', () => {
|
||||
expect(zshrc).toContain(ompWrapperLine)
|
||||
expect(zlogin).toContain(ompWrapperLine)
|
||||
expect(bashRc).toContain(ompWrapperLine)
|
||||
expect(zshrc).toContain(primeWrapperLine)
|
||||
expect(zlogin).toContain(primeWrapperLine)
|
||||
expect(bashRc).toContain(primeWrapperLine)
|
||||
})
|
||||
|
||||
// Why: issue #2422 — without OSC 133 C/D markers, bash sessions kept the worktree spinner "working" ~30min after the agent exited.
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
isPowerShellExecutableName
|
||||
} from '../powershell-osc133-bootstrap'
|
||||
import { getPosixOmpShellWrapper } from '../pty/omp-shell-wrapper'
|
||||
import { getPosixPrimeAgentShellWrapper } from '../pty/prime-agent-shell-wrapper'
|
||||
import { buildStartupCommandSubmission } from '../../shared/startup-command-submission'
|
||||
import {
|
||||
getFishShellReadyInitCommand,
|
||||
@@ -129,6 +130,7 @@ __orca_restore_agent_teams_path
|
||||
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
|
||||
[[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}"
|
||||
${getPosixOmpShellWrapper()}
|
||||
${getPosixPrimeAgentShellWrapper()}
|
||||
# Why: Codex must keep using Orca's runtime CODEX_HOME after profile scripts.
|
||||
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"
|
||||
# Why: emit OSC 133 C/D so terminal-command-lifecycle can drop stale agent
|
||||
@@ -245,6 +247,7 @@ if [[ ! -o login ]]; then
|
||||
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
|
||||
[[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}"
|
||||
${getPosixOmpShellWrapper()}
|
||||
${getPosixPrimeAgentShellWrapper()}
|
||||
# Why: Codex must keep using Orca's runtime CODEX_HOME after rc files.
|
||||
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"
|
||||
fi
|
||||
@@ -305,6 +308,7 @@ __orca_restore_agent_teams_path
|
||||
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
|
||||
[[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}"
|
||||
${getPosixOmpShellWrapper()}
|
||||
${getPosixPrimeAgentShellWrapper()}
|
||||
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"
|
||||
${getZshShellReadyMarkerRegistrationBlock(SHELL_READY_MARKER_ESCAPED)}
|
||||
${getZshFinalZdotdirRestoreBlock()}
|
||||
|
||||
@@ -280,13 +280,16 @@ describe('resolveWindowsShellLaunchArgs', () => {
|
||||
expect(existsSync(join(userDataPath, 'shell-ready', 'bash', 'rcfile'))).toBe(true)
|
||||
expect(existsSync(join(userDataPath, 'shell-ready', 'zsh', '.zshenv'))).toBe(true)
|
||||
|
||||
// Why: the point of materializing wrappers for WSL is that a typed `omp`
|
||||
// picks up Orca's status extension; pin that shim end to end.
|
||||
// Why: typed agents need the host-managed status extension in WSL.
|
||||
const bashRcfile = readFileSync(join(userDataPath, 'shell-ready', 'bash', 'rcfile'), 'utf8')
|
||||
const zshLogin = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zlogin'), 'utf8')
|
||||
for (const wrapperFile of [bashRcfile, zshLogin]) {
|
||||
expect(wrapperFile).toContain('command omp --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"')
|
||||
expect(wrapperFile).toContain('omp() { __orca_omp "$@"; }')
|
||||
expect(wrapperFile).toContain(
|
||||
'command prime-agent --extension "${ORCA_PRIME_AGENT_STATUS_EXTENSION}" "$@"'
|
||||
)
|
||||
expect(wrapperFile).toContain('prime-agent() { __orca_prime_agent "$@"; }')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { chmodSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { getPosixPrimeAgentShellWrapper } from './prime-agent-shell-wrapper'
|
||||
|
||||
const describePosix = process.platform === 'win32' ? describe.skip : describe
|
||||
const hasBash = process.platform !== 'win32' && spawnSync('bash', ['--version']).status === 0
|
||||
const itWithBash = hasBash ? it : it.skip
|
||||
const tempDirs: string[] = []
|
||||
|
||||
function makeFixture(): {
|
||||
root: string
|
||||
extensionPath: string
|
||||
capturePath: string
|
||||
endpointPath: string
|
||||
primeHome: string
|
||||
rcfile: string
|
||||
} {
|
||||
const root = mkdtempSync(join(tmpdir(), 'orca-prime-wrapper-'))
|
||||
tempDirs.push(root)
|
||||
const binDir = join(root, 'bin')
|
||||
const primeHome = join(root, 'guest-prime-home')
|
||||
const extensionPath = join(root, 'orca-agent-status.ts')
|
||||
const capturePath = join(root, 'capture')
|
||||
const endpointPath = join(
|
||||
root,
|
||||
'.orca-wsl',
|
||||
'agent-hooks',
|
||||
'instance-testinstance',
|
||||
'endpoint.env'
|
||||
)
|
||||
const rcfile = join(root, 'rcfile')
|
||||
mkdirSync(binDir)
|
||||
mkdirSync(primeHome)
|
||||
mkdirSync(dirname(endpointPath), { recursive: true })
|
||||
writeFileSync(extensionPath, 'export default {}')
|
||||
writeFileSync(endpointPath, 'ORCA_AGENT_HOOK_PORT=4567\nORCA_AGENT_HOOK_TOKEN=current-token\n')
|
||||
writeFileSync(rcfile, getPosixPrimeAgentShellWrapper())
|
||||
writeFileSync(
|
||||
join(binDir, 'prime-agent'),
|
||||
`#!/bin/sh
|
||||
{
|
||||
printf 'HOME=%s\n' "$PRIME_AGENT_CODING_AGENT_DIR"
|
||||
printf 'ENDPOINT=%s\n' "$ORCA_AGENT_HOOK_ENDPOINT"
|
||||
i=0
|
||||
for arg in "$@"; do
|
||||
i=$((i + 1))
|
||||
printf 'ARG%s=%s\n' "$i" "$arg"
|
||||
done
|
||||
} > "$ORCA_CAPTURE_FILE"
|
||||
if [ "\${1:-}" = "config" ]; then
|
||||
printf 'preserved\n' > "$PRIME_AGENT_CODING_AGENT_DIR/config.yml"
|
||||
fi
|
||||
`,
|
||||
{ mode: 0o755 }
|
||||
)
|
||||
chmodSync(join(binDir, 'prime-agent'), 0o755)
|
||||
return { root, extensionPath, capturePath, endpointPath, primeHome, rcfile }
|
||||
}
|
||||
|
||||
function runPrime(
|
||||
fixture: ReturnType<typeof makeFixture>,
|
||||
command: string,
|
||||
extensionPath = fixture.extensionPath
|
||||
): string {
|
||||
const result = spawnSync('bash', ['--noprofile', '--rcfile', fixture.rcfile, '-ic', command], {
|
||||
cwd: fixture.root,
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: fixture.root,
|
||||
PATH: `${join(fixture.root, 'bin')}:${process.env.PATH ?? ''}`,
|
||||
PRIME_AGENT_CODING_AGENT_DIR: fixture.primeHome,
|
||||
ORCA_PRIME_AGENT_STATUS_EXTENSION: extensionPath,
|
||||
ORCA_WSL_HOOK_INSTANCE: 'testinstance',
|
||||
ORCA_AGENT_HOOK_ENDPOINT: '/mnt/c/current/endpoint.cmd',
|
||||
ORCA_AGENT_HOOK_PORT: '4567',
|
||||
ORCA_AGENT_HOOK_TOKEN: 'current-token',
|
||||
ORCA_CAPTURE_FILE: fixture.capturePath
|
||||
},
|
||||
encoding: 'utf8'
|
||||
})
|
||||
expect(result.status, result.stderr).toBe(0)
|
||||
return readFileSync(fixture.capturePath, 'utf8')
|
||||
}
|
||||
|
||||
describePosix('Prime Agent shell wrapper', () => {
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
itWithBash('injects the managed extension into an interactive launch', () => {
|
||||
const fixture = makeFixture()
|
||||
const capture = runPrime(fixture, 'prime-agent ask')
|
||||
|
||||
expect(capture).toContain(`HOME=${fixture.primeHome}`)
|
||||
expect(capture).toContain(`ENDPOINT=${fixture.endpointPath}`)
|
||||
expect(capture).toContain('ARG1=--extension')
|
||||
expect(capture).toContain(`ARG2=${fixture.extensionPath}`)
|
||||
expect(capture).toContain('ARG3=ask')
|
||||
})
|
||||
|
||||
itWithBash('preserves config subcommands and the guest Prime home', () => {
|
||||
const fixture = makeFixture()
|
||||
const capture = runPrime(fixture, 'prime-agent config')
|
||||
|
||||
expect(capture).toContain(`HOME=${fixture.primeHome}`)
|
||||
expect(capture).toContain('ARG1=config')
|
||||
expect(capture).not.toContain('ARG1=--extension')
|
||||
expect(readFileSync(join(fixture.primeHome, 'config.yml'), 'utf8')).toBe('preserved\n')
|
||||
})
|
||||
|
||||
itWithBash.each([
|
||||
'agents',
|
||||
'doctor',
|
||||
'help',
|
||||
'list',
|
||||
'model',
|
||||
'package',
|
||||
'rename',
|
||||
'schedule',
|
||||
'send',
|
||||
'session',
|
||||
'shutdown',
|
||||
'status',
|
||||
'stop',
|
||||
'update'
|
||||
])('preserves the %s subcommand argv', (subcommand) => {
|
||||
const fixture = makeFixture()
|
||||
const capture = runPrime(fixture, `prime-agent ${subcommand}`)
|
||||
|
||||
expect(capture).toContain(`ARG1=${subcommand}`)
|
||||
expect(capture).not.toContain('ARG1=--extension')
|
||||
})
|
||||
|
||||
itWithBash.each(['stop', 'rename'])(
|
||||
'preserves --daemon-socket before the %s management command',
|
||||
(subcommand) => {
|
||||
const fixture = makeFixture()
|
||||
const capture = runPrime(
|
||||
fixture,
|
||||
`prime-agent --daemon-socket /tmp/prime.sock ${subcommand} agent-123`
|
||||
)
|
||||
|
||||
expect(capture).toContain('ARG1=--daemon-socket')
|
||||
expect(capture).toContain('ARG2=/tmp/prime.sock')
|
||||
expect(capture).toContain(`ARG3=${subcommand}`)
|
||||
expect(capture).not.toContain(fixture.extensionPath)
|
||||
}
|
||||
)
|
||||
|
||||
itWithBash('injects the extension after the attach agent operand', () => {
|
||||
const fixture = makeFixture()
|
||||
const capture = runPrime(fixture, 'prime-agent attach agent-123 --follow')
|
||||
|
||||
expect(capture).toContain('ARG1=attach')
|
||||
expect(capture).toContain('ARG2=agent-123')
|
||||
expect(capture).toContain('ARG3=--extension')
|
||||
expect(capture).toContain(`ARG4=${fixture.extensionPath}`)
|
||||
expect(capture).toContain('ARG5=--follow')
|
||||
})
|
||||
|
||||
itWithBash('preserves attach help without inventing an agent operand', () => {
|
||||
const fixture = makeFixture()
|
||||
const capture = runPrime(fixture, 'prime-agent attach --help')
|
||||
|
||||
expect(capture).toContain('ARG1=attach')
|
||||
expect(capture).toContain('ARG2=--help')
|
||||
expect(capture).not.toContain(fixture.extensionPath)
|
||||
})
|
||||
|
||||
itWithBash('does not override an explicit user extension', () => {
|
||||
const fixture = makeFixture()
|
||||
const capture = runPrime(fixture, 'prime-agent --extension /tmp/user-extension.ts ask')
|
||||
|
||||
expect(capture).toContain('ARG1=--extension')
|
||||
expect(capture).toContain('ARG2=/tmp/user-extension.ts')
|
||||
expect(capture).not.toContain(fixture.extensionPath)
|
||||
})
|
||||
|
||||
itWithBash('does not add a second extension to attach', () => {
|
||||
const fixture = makeFixture()
|
||||
const capture = runPrime(
|
||||
fixture,
|
||||
'prime-agent attach agent-123 --extension /tmp/user-extension.ts'
|
||||
)
|
||||
|
||||
expect(capture).toContain('ARG1=attach')
|
||||
expect(capture).toContain('ARG2=agent-123')
|
||||
expect(capture).toContain('ARG3=--extension')
|
||||
expect(capture).toContain('ARG4=/tmp/user-extension.ts')
|
||||
expect(capture).not.toContain(fixture.extensionPath)
|
||||
})
|
||||
|
||||
itWithBash('degrades to the original argv when the managed extension is unavailable', () => {
|
||||
const fixture = makeFixture()
|
||||
const capture = runPrime(fixture, 'prime-agent ask', join(fixture.root, 'missing.ts'))
|
||||
|
||||
expect(capture).toContain('ARG1=ask')
|
||||
expect(capture).not.toContain('ARG1=--extension')
|
||||
})
|
||||
|
||||
itWithBash('keeps the stable guest endpoint path while its relay refreshes stale data', () => {
|
||||
const fixture = makeFixture()
|
||||
writeFileSync(fixture.endpointPath, 'ORCA_AGENT_HOOK_PORT=4567\nORCA_AGENT_HOOK_TOKEN=stale\n')
|
||||
|
||||
const capture = runPrime(fixture, 'prime-agent ask')
|
||||
|
||||
expect(capture).toContain(`ENDPOINT=${fixture.endpointPath}`)
|
||||
expect(capture).toContain('ARG1=--extension')
|
||||
})
|
||||
|
||||
itWithBash('uses the guest endpoint path when the relay binds a fallback port', () => {
|
||||
const fixture = makeFixture()
|
||||
writeFileSync(
|
||||
fixture.endpointPath,
|
||||
'ORCA_AGENT_HOOK_PORT=9876\nORCA_AGENT_HOOK_TOKEN=current-token\n'
|
||||
)
|
||||
|
||||
const capture = runPrime(fixture, 'prime-agent ask')
|
||||
|
||||
expect(capture).toContain(`ENDPOINT=${fixture.endpointPath}`)
|
||||
expect(capture).toContain('ARG1=--extension')
|
||||
})
|
||||
|
||||
itWithBash('does not define wrapper helpers when the bridge env is absent', () => {
|
||||
const fixture = makeFixture()
|
||||
const result = spawnSync(
|
||||
'bash',
|
||||
['--noprofile', '--rcfile', fixture.rcfile, '-ic', 'type -t __orca_prime_agent'],
|
||||
{
|
||||
cwd: fixture.root,
|
||||
env: { ...process.env, ORCA_PRIME_AGENT_STATUS_EXTENSION: '' },
|
||||
encoding: 'utf8'
|
||||
}
|
||||
)
|
||||
|
||||
expect(result.status).not.toBe(0)
|
||||
expect(result.stdout.trim()).toBe('')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
import { wslHookRelayEndpointFilePath } from '../../shared/wsl-hook-relay-contract'
|
||||
|
||||
const PRIME_AGENT_SUBCOMMANDS = [
|
||||
'agents',
|
||||
'config',
|
||||
'doctor',
|
||||
'help',
|
||||
'list',
|
||||
'model',
|
||||
'package',
|
||||
'rename',
|
||||
'schedule',
|
||||
'send',
|
||||
'session',
|
||||
'shutdown',
|
||||
'status',
|
||||
'stop',
|
||||
'update'
|
||||
] as const
|
||||
|
||||
export function getPosixPrimeAgentShellWrapper(): string {
|
||||
const subcommands = PRIME_AGENT_SUBCOMMANDS.join('|')
|
||||
const guestEndpointPath = wslHookRelayEndpointFilePath('${HOME%/}', '${ORCA_WSL_HOOK_INSTANCE}')
|
||||
return `# Why: WSL cannot install into Prime's guest-owned config root from the
|
||||
# Windows host, so pass Orca's status extension only to interactive launches.
|
||||
if [[ -n "\${ORCA_PRIME_AGENT_STATUS_EXTENSION:-}" ]]; then
|
||||
__orca_prime_agent_should_skip_extension() {
|
||||
if [[ "\${1:-}" == "--daemon-socket" && ( "\${3:-}" == "stop" || "\${3:-}" == "rename" ) ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ "\${1:-}" == --daemon-socket=* && ( "\${2:-}" == "stop" || "\${2:-}" == "rename" ) ]]; then
|
||||
return 0
|
||||
fi
|
||||
case "\${1:-}" in
|
||||
help|--help|-h|--version|-v|--extension|--extension=*) return 0 ;;
|
||||
${subcommands}) return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
__orca_prime_agent_has_explicit_extension() {
|
||||
local __orca_arg
|
||||
for __orca_arg in "$@"; do
|
||||
case "$__orca_arg" in
|
||||
--extension|--extension=*) return 0 ;;
|
||||
esac
|
||||
done
|
||||
return 1
|
||||
}
|
||||
__orca_prime_agent() {
|
||||
if [[ "\${1:-}" == "attach" && ( -z "\${2:-}" || "\${2:-}" == -* ) ]]; then
|
||||
command prime-agent "$@"
|
||||
return
|
||||
fi
|
||||
if ! __orca_prime_agent_should_skip_extension "$@" && ! __orca_prime_agent_has_explicit_extension "$@" && [[ -f "\${ORCA_PRIME_AGENT_STATUS_EXTENSION}" ]]; then
|
||||
local __orca_guest_endpoint="${guestEndpointPath}"
|
||||
if [[ -n "\${HOME:-}" && -n "\${ORCA_WSL_HOOK_INSTANCE:-}" ]]; then
|
||||
local ORCA_AGENT_HOOK_ENDPOINT="$__orca_guest_endpoint"
|
||||
export ORCA_AGENT_HOOK_ENDPOINT
|
||||
fi
|
||||
if [[ "\${1:-}" == "attach" && -n "\${2:-}" && "\${2:-}" != -* ]]; then
|
||||
local __orca_attach_agent="$2"
|
||||
shift 2
|
||||
command prime-agent attach "$__orca_attach_agent" --extension "\${ORCA_PRIME_AGENT_STATUS_EXTENSION}" "$@"
|
||||
else
|
||||
command prime-agent --extension "\${ORCA_PRIME_AGENT_STATUS_EXTENSION}" "$@"
|
||||
fi
|
||||
else
|
||||
command prime-agent "$@"
|
||||
fi
|
||||
}
|
||||
prime-agent() { __orca_prime_agent "$@"; }
|
||||
fi
|
||||
`
|
||||
}
|
||||
@@ -48,6 +48,8 @@ describe('addOrcaWslInteropEnv', () => {
|
||||
ORCA_USER_DATA_PATH: 'C:\\Users\\jin\\AppData\\Roaming\\Orca',
|
||||
ORCA_CLI_COMMAND: 'orca-ide',
|
||||
ORCA_OMP_STATUS_EXTENSION: 'C:\\Users\\jin\\.omp\\agent\\extensions\\orca-agent-status.ts',
|
||||
ORCA_PRIME_AGENT_STATUS_EXTENSION:
|
||||
'C:\\Users\\jin\\AppData\\Roaming\\Orca\\prime-agent-managed-status-extension\\orca-agent-status.ts',
|
||||
ORCA_PANE_KEY: 'tab-1:leaf-1',
|
||||
ORCA_TAB_ID: 'tab-1',
|
||||
ORCA_WORKTREE_ID: 'repo::\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo',
|
||||
@@ -56,6 +58,7 @@ describe('addOrcaWslInteropEnv', () => {
|
||||
ORCA_AGENT_HOOK_TOKEN: 'token',
|
||||
ORCA_AGENT_HOOK_ENV: 'dev',
|
||||
ORCA_AGENT_HOOK_VERSION: '1',
|
||||
ORCA_WSL_HOOK_INSTANCE: 'testinstance',
|
||||
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_KIND: 'wsl',
|
||||
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_ID: 'local',
|
||||
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION: 'Ubuntu'
|
||||
@@ -67,6 +70,7 @@ describe('addOrcaWslInteropEnv', () => {
|
||||
expect(env.WSLENV).toContain('ORCA_USER_DATA_PATH/p')
|
||||
expect(env.WSLENV).toContain('ORCA_CLI_COMMAND/u')
|
||||
expect(env.WSLENV).toContain('ORCA_OMP_STATUS_EXTENSION/p')
|
||||
expect(env.WSLENV).toContain('ORCA_PRIME_AGENT_STATUS_EXTENSION/p')
|
||||
expect(env.WSLENV).toContain('ORCA_PANE_KEY/u')
|
||||
expect(env.WSLENV).toContain('ORCA_TAB_ID/u')
|
||||
expect(env.WSLENV).toContain('ORCA_WORKTREE_ID/u')
|
||||
@@ -75,6 +79,7 @@ describe('addOrcaWslInteropEnv', () => {
|
||||
expect(env.WSLENV).toContain('ORCA_AGENT_HOOK_TOKEN/u')
|
||||
expect(env.WSLENV).toContain('ORCA_AGENT_HOOK_ENV/u')
|
||||
expect(env.WSLENV).toContain('ORCA_AGENT_HOOK_VERSION/u')
|
||||
expect(env.WSLENV).toContain('ORCA_WSL_HOOK_INSTANCE/u')
|
||||
expect(env.WSLENV).toContain('ORCA_ORCHESTRATION_COMPATIBILITY_HOST_KIND/u')
|
||||
expect(env.WSLENV).toContain('ORCA_ORCHESTRATION_COMPATIBILITY_HOST_ID/u')
|
||||
expect(env.WSLENV).toContain('ORCA_ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION/u')
|
||||
|
||||
@@ -96,6 +96,7 @@ export function addOrcaWslInteropEnv(env: Record<string, string>): void {
|
||||
'ORCA_WSL_HOOK_INSTANCE/u',
|
||||
'ORCA_OMP_SOURCE_AGENT_DIR/p',
|
||||
'ORCA_OMP_STATUS_EXTENSION/p',
|
||||
'ORCA_PRIME_AGENT_STATUS_EXTENSION/p',
|
||||
...worktreeSetupWslenvEntries(env)
|
||||
]
|
||||
applyWslenvPassthrough(env, passthroughEntries)
|
||||
|
||||
Reference in New Issue
Block a user