Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
7f2ba67
fix: wait for OpenCode worker composer before first dispatch
nwparker Oct 2, 2026
960bb84
feat(opencode): probe execution-host CLI capabilities
nwparker Oct 2, 2026
6b78423
fix(opencode): select plugin default for execution host loader
nwparker Oct 2, 2026
1b5f4c5
fix(opencode): limit prompt prefill capability to verified release
nwparker Oct 2, 2026
b8cc6fc
feat(opencode): probe launch capabilities on the execution host
nwparker Oct 2, 2026
8c3d344
fix(opencode): select plugin loader for the launched host binary
nwparker Oct 2, 2026
944c72c
fix(opencode): match WSL probe cwd and declared guest environment
nwparker Oct 2, 2026
42788e7
fix(opencode): preserve launch environment deletion boundaries
nwparker Oct 2, 2026
deee1bc
Prepare immutable strict worker readiness restack
nwparker Oct 3, 2026
5c690ca
Prepare immutable execution-host OpenCode capabilities restack
nwparker Oct 3, 2026
dad41e5
Prepare immutable strict worker readiness restack
nwparker Oct 3, 2026
08531ab
Prepare immutable execution-host OpenCode capabilities restack
nwparker Oct 3, 2026
40d9038
STRICT launch CI contract correction
nwparker Oct 3, 2026
37f0be5
CAPS launch CI contract correction
nwparker Oct 3, 2026
7c2ca76
Preserve frozen main and reviewed strict OpenCode layer
nwparker Oct 3, 2026
1b9254e
Preserve frozen main and reviewed caps OpenCode layer
nwparker Oct 3, 2026
2d6636a
fix(opencode): compose reviewed strict launch boundary with current main
nwparker Oct 3, 2026
a158087
fix(opencode): compose reviewed caps launch boundary with current main
nwparker Oct 3, 2026
a62e9ba
fix(opencode): preserve managed account environment in strict launch …
nwparker Oct 3, 2026
0e46eb4
fix(opencode): preserve managed account environment in caps launch bo…
nwparker Oct 3, 2026
4ecbf29
fix(opencode): preserve current Caller main in strict launch boundary
nwparker Oct 3, 2026
c401e91
fix(opencode): preserve current Caller main in caps launch boundary
nwparker Oct 3, 2026
ec6686e
Fix OpenCode caps review boundaries on preserved published source
nwparker Oct 3, 2026
1dfbd55
Preserve merged STRICT and current main in OpenCode caps correction
nwparker Oct 3, 2026
dcec863
Preserve current main with qualified OpenCode caps source
nwparker Oct 3, 2026
b94c542
Fix OpenCode caps standalone build boundary
nwparker Oct 3, 2026
f1f6feb
Preserve current main and qualified OpenCode caps repairs
nwparker Oct 3, 2026
6a83da3
test: initialize Claude prompt state in output retention fixture
nwparker Oct 3, 2026
107d359
Preserve OpenCode caps integration with the reviewed main test fixture
nwparker Oct 3, 2026
d94a3f2
Preserve OpenCode caps integration and current main relay inventory c…
nwparker Oct 3, 2026
a77cea7
Preserve qualified OpenCode caps with current merged account selection
nwparker Oct 4, 2026
e213fe1
Merge current main into the OpenCode caps layer
nwparker Oct 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/main/ipc/pty/ipc/spawn-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { withCodexTerminalServerIsolationEnv } from '../../../../shared/codex-te
import { planCodexNoDaemonLaunch } from '../../../pty/codex-no-daemon-launch-command'
import type { PtyIpcSpawnState } from './spawn-state'
import { applyAgentWorkspaceTrustToSpawn } from '../../../agent-workspace-trust-spawn'
import { prepareOpenCodePtyLaunch } from '../../../opencode/opencode-pty-launch'

/** Carries deletions to provider-owned environments, including persistent older daemons. */
export async function buildPtyIpcSpawnOptions(
Expand Down Expand Up @@ -64,6 +65,18 @@ export async function buildPtyIpcSpawnOptions(
ctx.combinedEnvToDelete = removeCodexHomeDeletionRequests(ctx.combinedEnvToDelete)
}
deleteRequestedEnvKeys(ctx.spawnEnv, ctx.combinedEnvToDelete)
ctx.spawnEnv = await prepareOpenCodePtyLaunch({
command: ctx.launchCommand,
agent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined,
env: ctx.spawnEnv,
envToDelete: (ctx.combinedEnvToDelete ??= []),
cwd: ctx.cwd,
connectionId: args.connectionId,
isFreshLaunch: !ctx.preAdoptedStablePane && ctx.launchCommand !== undefined,
...(ctx.codexSelectionTarget.runtime === 'wsl'
? { wsl: { distro: ctx.expectedWslDistro ?? undefined } }
: {})
})
promoteAgentTeamsShimPath(ctx.spawnEnv, ctx.requestedAgentTeamsPath)
ctx.spawnOptions = {
cols: args.cols,
Expand Down
30 changes: 18 additions & 12 deletions src/main/ipc/pty/runtime/spawn-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
import type { RuntimePtySpawnState } from './spawn-state'
import { applyAgentWorkspaceTrustToSpawn } from '../../../agent-workspace-trust-spawn'
import { prepareAntigravityAccountForLaunch } from '../../../antigravity/native-account-launch'
import { prepareOpenCodePtyLaunch } from '../../../opencode/opencode-pty-launch'

/** Headless spawns need the same host-side environment isolation as desktop spawns. */
export async function buildRuntimePtySpawnOptions(
Expand Down Expand Up @@ -85,20 +86,12 @@ export async function buildRuntimePtySpawnOptions(
? getLegacyOpenCodeEnvKeysToDelete(ctx.env, getAppEnvironment().getPath('userData'))
: [],
// Why: ungated, unlike the agent-hook keys — the local provider and the relay host also spread their own process.env into every spawn.
getInheritedAgentSessionStampEnvKeysToDelete(ctx.env)
)
if (ctx.skipCodexHomeEnv) {
ctx.spawnOptions.envToDelete = mergePtyEnvDeletions(
ctx.spawnOptions.envToDelete,
CODEX_HOME_ENV_KEYS
)
} else if (ctx.stripInheritedOrcaCodexHome) {
getInheritedAgentSessionStampEnvKeysToDelete(ctx.env),
ctx.skipCodexHomeEnv ? CODEX_HOME_ENV_KEYS : [],
// Why: the daemon owns a persistent inherited environment that may
// differ from main. ORCA_CODEX_HOME asks it to compare/delete the pair.
ctx.spawnOptions.envToDelete = mergePtyEnvDeletions(ctx.spawnOptions.envToDelete, [
'ORCA_CODEX_HOME'
])
}
ctx.stripInheritedOrcaCodexHome ? ['ORCA_CODEX_HOME'] : []
)
if (ctx.codexResumeHomeSelected) {
ctx.spawnOptions.envToDelete = removeCodexHomeDeletionRequests(ctx.spawnOptions.envToDelete)
}
Expand All @@ -111,6 +104,19 @@ export async function buildRuntimePtySpawnOptions(
env: ctx.env,
envToDelete: ctx.spawnOptions.envToDelete
})
ctx.env = await prepareOpenCodePtyLaunch({
Comment thread
coderabbitai[bot] marked this conversation as resolved.
command: ctx.launchCommand,
agent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined,
env: ctx.env,
envToDelete: (ctx.spawnOptions.envToDelete ??= []),
cwd: ctx.cwd,
connectionId: args.connectionId,
isFreshLaunch: !ctx.preAdoptedStablePane && ctx.launchCommand !== undefined,
...(ctx.codexSelectionTarget.runtime === 'wsl'
? { wsl: { distro: ctx.expectedWslDistro ?? undefined } }
: {})
})
ctx.spawnOptions.env = ctx.env
promoteAgentTeamsShimPath(ctx.env, ctx.requestedAgentTeamsPath)
const noDaemonLaunch = planCodexNoDaemonLaunch({
command: ctx.launchCommand,
Expand Down
2 changes: 2 additions & 0 deletions src/main/opencode/hook-plugin-dispose-host.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type PluginModule = {
const ENV_KEYS = [
'ORCA_PANE_KEY',
'ORCA_OPENCODE_AGENT',
'ORCA_OPENCODE_PLUGIN_API',
'ORCA_AGENT_HOOK_ENDPOINT',
'ORCA_AGENT_HOOK_PORT',
'ORCA_AGENT_HOOK_TOKEN'
Expand Down Expand Up @@ -81,6 +82,7 @@ describe.each(['opencode', 'opencode2'] as const)('%s plugin disposal by host',
}
process.env.ORCA_PANE_KEY = 'tab-1:leaf-1'
process.env.ORCA_OPENCODE_AGENT = agent
process.env.ORCA_OPENCODE_PLUGIN_API = 'v2'
delete process.env.ORCA_AGENT_HOOK_ENDPOINT
process.env.ORCA_AGENT_HOOK_PORT = '59999'
process.env.ORCA_AGENT_HOOK_TOKEN = 'test-token'
Expand Down
51 changes: 49 additions & 2 deletions src/main/opencode/hook-plugin-legacy-tui-ownership.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { pathToFileURL } from 'node:url'
import { getOpenCodePluginSource } from './status-plugin-module-source'
import { writeOpenCodeTuiPlugin } from '../../shared/opencode-tui-plugin-install'
import { fakeLegacyTui, type LegacyTuiEvent } from './opencode-legacy-tui-fixture'
import { readV1Plugin } from './opencode-legacy-plugin-loader-fixture'
import { getLegacyPlugins, readV1Plugin } from './opencode-legacy-plugin-loader-fixture'
import { getOpenCodeCliCapabilities } from '../../shared/opencode-cli-version'

type Post = {
paneKey?: string
Expand All @@ -23,7 +24,8 @@ const ENV_KEYS = [
'ORCA_AGENT_HOOK_ENDPOINT',
'ORCA_AGENT_HOOK_PORT',
'ORCA_AGENT_HOOK_TOKEN',
'ORCA_AGENT_HOOK_OPENCODE_TUI'
'ORCA_AGENT_HOOK_OPENCODE_TUI',
'ORCA_OPENCODE_PLUGIN_API'
] as const
const created = (id: string, parentID?: string): LegacyTuiEvent => ({
type: 'session.created',
Expand All @@ -48,6 +50,7 @@ describe('OpenCode 1 TUI API pane reporting', () => {
savedArgv = process.argv
savedEnv = Object.fromEntries(ENV_KEYS.map((key) => [key, process.env[key]]))
process.argv = ['opencode', 'attach', 'http://127.0.0.1:4096']
process.env.ORCA_OPENCODE_PLUGIN_API = 'v2'
process.env.ORCA_OPENCODE_AGENT = 'opencode'
process.env.ORCA_AGENT_HOOK_PORT = '59999'
process.env.ORCA_AGENT_HOOK_TOKEN = 'fixture'
Expand Down Expand Up @@ -129,6 +132,50 @@ describe('OpenCode 1 TUI API pane reporting', () => {
expect(tui.listenerCount()).toBeGreaterThan(0)
})

it('loads the selected v1 factory through the release fallback once and retains the separate TUI', async () => {
process.env.ORCA_OPENCODE_PLUGIN_API = getOpenCodeCliCapabilities('1.18.30').pluginApi
process.argv = ['opencode', 'serve']
process.env.ORCA_PANE_KEY = PANE_A
const path = join(dir, 'selected-v1-server.mjs')
writeFileSync(path, getOpenCodePluginSource())
const plugin: unknown = await import(pathToFileURL(path).href)
if (typeof plugin !== 'object' || !plugin || !('default' in plugin)) {
throw new Error('No server plugin')
}
expect(readV1Plugin(plugin, path, 'server', 'detect')).toBeUndefined()
const factories = getLegacyPlugins(plugin)
expect(factories).toHaveLength(1)
expect(factories[0]).toBe(plugin.default)
const hooks: unknown = await factories[0]({
client: { session: { get: async () => ({ data: { id: 'ses_root' } }) } }
})
if (
!hooks ||
typeof hooks !== 'object' ||
!('event' in hooks) ||
typeof hooks.event !== 'function' ||
!('dispose' in hooks) ||
typeof hooks.dispose !== 'function'
) {
throw new Error('No legacy event hooks')
}
const dispose = hooks.dispose
cleanups.push(async () => {
await dispose()
})
await hooks.event({
event: {
type: 'session.status',
properties: { sessionID: 'ses_root', status: { type: 'busy' } }
}
})
await settle('SessionBusy:ses_root')
expect(posts.at(-1)).toMatchObject({ opencodeSharedServer: 1 })
process.argv = ['opencode', 'attach', 'http://127.0.0.1:4096']
const tui = await start()
expect(tui.listenerCount()).toBeGreaterThan(0)
})

it('reproduces the release loader rejection of a combined server and TUI entry', () => {
expect(() =>
readV1Plugin({ default: { server() {}, tui() {} } }, 'orca-opencode-status-tui/tui.js', 'tui')
Expand Down
9 changes: 9 additions & 0 deletions src/main/opencode/hook-plugin-module-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe('OpenCode status plugin module contract', () => {
// env. Pin every input here so the run does not depend on the developer's Orca session
// (an inherited ORCA_AGENT_HOOK_ENDPOINT would otherwise redirect the post to a live app).
const ENV_KEYS = [
'ORCA_OPENCODE_PLUGIN_API',
'ORCA_PANE_KEY',
'ORCA_AGENT_HOOK_ENDPOINT',
'ORCA_AGENT_HOOK_PORT',
Expand All @@ -55,6 +56,7 @@ describe('OpenCode status plugin module contract', () => {
// Why: the generated plugin self-disables when this names a different major,
// so an inherited value from the developer's own Orca pane would leave
// `hooks.event` undefined and fail the contract for the wrong reason.
delete process.env.ORCA_OPENCODE_PLUGIN_API
Comment thread
nwparker marked this conversation as resolved.
delete process.env.ORCA_OPENCODE_AGENT
delete process.env.ORCA_AGENT_HOOK_ENDPOINT
process.env.ORCA_AGENT_HOOK_PORT = '59999'
Expand Down Expand Up @@ -86,6 +88,13 @@ describe('OpenCode status plugin module contract', () => {
return (await import(pathToFileURL(pluginPath).href)) as PluginModule
}

it('selects the same factory for a v1 execution host', async () => {
process.env.ORCA_OPENCODE_PLUGIN_API = 'v1'
const module = await loadPluginModule()
expect(module.default).toBeTypeOf('function')
expect(module.default).toBe(module.OrcaOpenCodeStatusPlugin)
})

it('exposes a default export carrying a string id and a callable server()', async () => {
const module = await loadPluginModule()

Expand Down
2 changes: 2 additions & 0 deletions src/main/opencode/hook-plugin-opencode2-setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe.each(['opencode', 'opencode2'] as const)('%s plugin on OpenCode 2', (ag
const ENV_KEYS = [
'ORCA_PANE_KEY',
'ORCA_OPENCODE_AGENT',
'ORCA_OPENCODE_PLUGIN_API',
'ORCA_AGENT_HOOK_ENDPOINT',
'ORCA_AGENT_HOOK_PORT',
'ORCA_AGENT_HOOK_TOKEN'
Expand All @@ -66,6 +67,7 @@ describe.each(['opencode', 'opencode2'] as const)('%s plugin on OpenCode 2', (ag
savedEnv[key] = process.env[key]
}
process.env.ORCA_OPENCODE_AGENT = agent
process.env.ORCA_OPENCODE_PLUGIN_API = 'v2'
delete process.env.ORCA_AGENT_HOOK_ENDPOINT
process.env.ORCA_AGENT_HOOK_PORT = '59999'
process.env.ORCA_AGENT_HOOK_TOKEN = 'test-token'
Expand Down
3 changes: 3 additions & 0 deletions src/main/opencode/hook-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
import { resolveOpenCodeConfigDirectory } from '../../shared/opencode-config-directory'

beforeEach(() => {
vi.stubEnv('ORCA_OPENCODE_PLUGIN_API', 'v2')
setAppEnvironment({
getPath: getPathMock,
getAppPath: () => process.cwd(),
Expand All @@ -43,6 +44,8 @@ beforeEach(() => {
})
})

afterEach(() => vi.unstubAllEnvs())

const { isUsableId, toSafeDirName } = _internals

describe('OpenCode id safety guard', () => {
Expand Down
131 changes: 131 additions & 0 deletions src/main/opencode/opencode-cli-version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { beforeEach, expect, it, vi } from 'vitest'
import { runProcess } from '../../shared/child-process/run-process'
import { probeOpenCodeCliVersion } from './opencode-cli-version'

vi.mock('../../shared/child-process/run-process', () => ({ runProcess: vi.fn() }))
beforeEach(() => vi.mocked(runProcess).mockReset())

it('shares concurrent probes and isolates executable, host and environment identities', async () => {
vi.mocked(runProcess).mockResolvedValue({
code: 0,
signal: null,
stdout: '1.1.23',
stderr: '',
timedOut: false
})
const options = {
executablePath: '/test/opencode',
env: { PATH: '/runtime', XDG_CONFIG_HOME: '/one' },
hostIdentity: 'native'
}
const results = await Promise.all([
probeOpenCodeCliVersion(options),
probeOpenCodeCliVersion(options)
])
expect(results[0].pluginApi).toBe('v1')
expect(runProcess).toHaveBeenCalledTimes(1)
await probeOpenCodeCliVersion({ ...options, executablePath: '/other/opencode' })
await probeOpenCodeCliVersion({ ...options, hostIdentity: 'wsl:ubuntu' })
await probeOpenCodeCliVersion({ ...options, env: { ...options.env, XDG_CONFIG_HOME: '/two' } })
expect(runProcess).toHaveBeenCalledTimes(4)
expect(runProcess).toHaveBeenCalledWith(
expect.objectContaining({ args: ['--version'], timeoutMs: 5_000, maxOutputBytes: 4_096 })
)
})

it('degrades timeouts and missing executables to unknown', async () => {
vi.mocked(runProcess).mockResolvedValueOnce({
code: null,
signal: 'SIGTERM',
stdout: '2.0.16',
stderr: '',
timedOut: true
})
expect(
(await probeOpenCodeCliVersion({ executablePath: '/timeout/opencode', env: {} })).pluginApi
).toBe('unknown')
vi.mocked(runProcess).mockRejectedValueOnce(new Error('ENOENT'))
expect(
(await probeOpenCodeCliVersion({ executablePath: '/missing/opencode', env: {} })).version
).toBeNull()
})

it('shares a bounded execution-host callback without probing the native machine', async () => {
const execute = vi
.fn()
.mockResolvedValue({ code: 0, timedOut: false, stdout: 'opencode v2.0.16' })
const options = { executablePath: 'opencode', env: {}, hostIdentity: 'wsl:private', execute }
const results = await Promise.all([
probeOpenCodeCliVersion(options),
probeOpenCodeCliVersion(options)
])
expect(results[0].promptMode).toBe('prefill')
expect(execute).toHaveBeenCalledTimes(1)
expect(runProcess).not.toHaveBeenCalled()
})

it('shares probes across pane identities while preserving launch-affecting environment keys', async () => {
vi.mocked(runProcess).mockResolvedValue({
code: 0,
signal: null,
stdout: '2.0.16',
stderr: '',
timedOut: false
})
const options = {
executablePath: '/pane-cache/opencode',
cwd: '/workspace',
env: {
PATH: '/runtime',
OPENCODE_CONFIG_DIR: '/config',
ORCA_PANE_KEY: 'pane-1',
ORCA_TAB_ID: 'tab-1',
ORCA_TERMINAL_HANDLE: 'term-1',
ORCA_WORKTREE_ID: 'workspace-1',
ORCA_AGENT_LAUNCH_TOKEN: 'launch-1',
ORCA_AGENT_PANE: 'pane-1',
ORCA_AGENT_LAUNCH: 'launch-1'
},
hostIdentity: 'pane-cache-host'
}
await Promise.all([
probeOpenCodeCliVersion(options),
probeOpenCodeCliVersion({
...options,
env: {
...options.env,
ORCA_PANE_KEY: 'pane-2',
ORCA_TAB_ID: 'tab-2',
ORCA_TERMINAL_HANDLE: 'term-2',
ORCA_WORKTREE_ID: 'workspace-2',
ORCA_AGENT_LAUNCH_TOKEN: 'launch-2',
ORCA_AGENT_PANE: 'pane-2',
ORCA_AGENT_LAUNCH: 'launch-2'
}
})
])
expect(runProcess).toHaveBeenCalledTimes(1)
await probeOpenCodeCliVersion({ ...options, env: { ...options.env, PATH: '/other-runtime' } })
await probeOpenCodeCliVersion({
...options,
env: { ...options.env, OPENCODE_CONFIG_DIR: '/other-config' }
})
await probeOpenCodeCliVersion({ ...options, cwd: '/other-workspace' })
expect(runProcess).toHaveBeenCalledTimes(4)
})

it('separates native and host-callback backends even with the same host identity', async () => {
vi.mocked(runProcess).mockResolvedValue({
code: 0,
signal: null,
stdout: '1.18.32',
stderr: '',
timedOut: false
})
const options = { executablePath: '/backend/opencode', env: {}, hostIdentity: 'backend-host' }
expect((await probeOpenCodeCliVersion(options)).pluginApi).toBe('v1')
const execute = vi.fn(async () => ({ code: 0, timedOut: false, stdout: '2.0.16' }))
expect((await probeOpenCodeCliVersion({ ...options, execute })).pluginApi).toBe('v2')
expect(execute).toHaveBeenCalledOnce()
expect(runProcess).toHaveBeenCalledOnce()
})
Loading
Loading