diff --git a/config/scripts/pr-code-change-scope.mjs b/config/scripts/pr-code-change-scope.mjs index e04946456e4..d5594a0d168 100644 --- a/config/scripts/pr-code-change-scope.mjs +++ b/config/scripts/pr-code-change-scope.mjs @@ -135,7 +135,7 @@ const SHELL_PREFIXES = [ 'src/main/zsh-', 'src/main/runtime/structured-session-cli-login-shell', 'src/main/runtime/structured-session-login-shell-test-harness', - 'src/main/runtime/structured-session-child-identity-env', + 'src/main/runtime/structured-session-child-env', 'src/renderer/src/components/terminal-pane/fish-color-scheme', 'src/shared/fish-', 'src/shared/pty-reply-echo-shapes', diff --git a/config/scripts/pr-code-change-scope.test.mjs b/config/scripts/pr-code-change-scope.test.mjs index 5f80a7112da..d2ca541077c 100644 --- a/config/scripts/pr-code-change-scope.test.mjs +++ b/config/scripts/pr-code-change-scope.test.mjs @@ -294,7 +294,8 @@ describe('per-job path classification', () => { ) for (const file of [ 'src/main/runtime/structured-session-login-shell-test-harness.ts', - 'src/main/runtime/structured-session-child-identity-env.ts' + 'src/main/runtime/structured-session-child-env.ts', + 'src/main/runtime/structured-session-child-env.test-support.ts' ]) { expectClassification([file], { shell_contracts: true, diff --git a/src/main/acp/acp-launch-specs.ts b/src/main/acp/acp-launch-specs.ts index 4bab75d6de0..5353d2b91b5 100644 --- a/src/main/acp/acp-launch-specs.ts +++ b/src/main/acp/acp-launch-specs.ts @@ -2,11 +2,6 @@ // speaks protocol extensions). Nothing outside `acp-dialects/` branches on an agent's name. import { join } from 'node:path' -import { - ORCA_SCRUB_SAFE_LAUNCH_ENV, - ORCA_SCRUB_SAFE_PANE_ENV -} from '../../shared/agent-hook-scrub-safe-env' -import { AGENT_HOOK_RUNTIME_ENV_KEYS } from '../ipc/pty/host-env/spawn-env-keys' import type { AcpDialect } from './acp-dialects/acp-dialect' import { GROK_ACP_DIALECT } from './acp-dialects/grok-dialect' import { OMP_ACP_DIALECT } from './acp-dialects/omp-dialect' @@ -201,17 +196,3 @@ export const ACP_LAUNCH_SPECS: readonly AcpLaunchSpec[] = [ export function acpLaunchSpecFor(agent: string): AcpLaunchSpec | null { return ACP_LAUNCH_SPECS.find((spec) => spec.agent === agent) ?? null } - -/** - * A pane's identity in the inherited environment would let the agent's own Orca status hooks - * report for this session too; the structured session is its one status producer. - */ -export const ACP_CHILD_ENV_TO_DELETE: readonly string[] = [ - 'ORCA_PANE_KEY', - 'ORCA_TAB_ID', - 'ORCA_WORKTREE_ID', - 'ORCA_AGENT_LAUNCH_TOKEN', - ORCA_SCRUB_SAFE_PANE_ENV, - ORCA_SCRUB_SAFE_LAUNCH_ENV, - ...AGENT_HOOK_RUNTIME_ENV_KEYS -] diff --git a/src/main/acp/acp-recovery-history.test.ts b/src/main/acp/acp-recovery-history.test.ts index 402b9e2f76d..97a4b230c1b 100644 --- a/src/main/acp/acp-recovery-history.test.ts +++ b/src/main/acp/acp-recovery-history.test.ts @@ -77,7 +77,7 @@ async function windowFor(load: JournalLoad | null, messages: AcpStoredUserMessag const window = await readAcpRecoveryHistory( { spec: { readStoredUserMessages }, - resolveLaunch: async () => launch, + readLaunch: async () => launch, readJournal: () => load }, identity @@ -175,7 +175,7 @@ describe('ACP restart recovery from the agent store', () => { readAcpRecoveryHistory( { spec: { readStoredUserMessages }, - resolveLaunch: async () => ({ ...launch, resume: null }), + readLaunch: async () => ({ ...launch, resume: null }), readJournal: () => journal(submission('held', 'x', 'unknown', 1)) }, identity @@ -189,7 +189,7 @@ describe('ACP restart recovery from the agent store', () => { readAcpRecoveryHistory( { spec: {}, - resolveLaunch: async () => launch, + readLaunch: async () => launch, readJournal: () => journal(submission('held', 'x', 'unknown', 1)) }, identity @@ -207,7 +207,7 @@ describe('ACP restart recovery from the agent store', () => { throw new Error('database is locked') } }, - resolveLaunch: async () => launch, + readLaunch: async () => launch, readJournal: () => journal(submission('held', 'x', 'unknown', 1)), logger: { warn, error: vi.fn() } }, diff --git a/src/main/acp/acp-recovery-history.ts b/src/main/acp/acp-recovery-history.ts index d00a21a6d86..07ee22aa410 100644 --- a/src/main/acp/acp-recovery-history.ts +++ b/src/main/acp/acp-recovery-history.ts @@ -96,7 +96,7 @@ function recoveryWindow( /** The window for one chat; null for an agent whose store Orca does not read. */ export async function readAcpRecoveryHistory( - input: Pick & { + input: Pick & { spec: Pick }, identity: AgentSessionJournalIdentity @@ -113,7 +113,7 @@ export async function readAcpRecoveryHistory( if (!load || inDoubt.length === 0) { return null } - const launch = await waitForPromiseWithSignal(input.resolveLaunch({ identity }), signal) + const launch = await waitForPromiseWithSignal(input.readLaunch(identity), signal) if (!launch.resume) { return null } diff --git a/src/main/acp/acp-structured-acquire.ts b/src/main/acp/acp-structured-acquire.ts index bef2d4c3a16..926a83623c9 100644 --- a/src/main/acp/acp-structured-acquire.ts +++ b/src/main/acp/acp-structured-acquire.ts @@ -14,14 +14,9 @@ import { type StructuredAgentSessionAcquireInput } from '../native-chat/agent-session-wire/structured-agent-session-adapter' import { providerTimelineSink } from '../native-chat/agent-session-timeline/provider-timeline-plan' -import { - providerSpawnedProcessIdentity, - PROVIDER_SPAWN_TOKEN_ENV -} from '../provider-process/provider-spawned-process-identity' -import { structuredSessionChildIdentityEnv } from '../runtime/structured-session-child-identity-env' +import { providerSpawnedProcessIdentity } from '../provider-process/provider-spawned-process-identity' import { AcpAgentError } from './acp-errors' import { acpAuthenticationRequired, acpSignInRequiredRefusal } from './acp-turn-failures' -import { ACP_CHILD_ENV_TO_DELETE } from './acp-launch-specs' import { ACP_REOPEN_FAILED, acpReopenTakeover, @@ -83,7 +78,7 @@ export async function acquireAcpStructuredSession(input: { } closedBeforeSpawn() const launch: AcpStructuredLaunch = await deps - .resolveLaunch({ identity: acquire.identity }) + .resolveLaunch({ identity: acquire.identity, spawnToken: acquire.spawnToken }) .catch((error: unknown) => { throw new AgentSessionPreSpawnError(error) }) @@ -118,11 +113,8 @@ export async function acquireAcpStructuredSession(input: { command: launch.command, args: launch.args, cwd: launch.cwd, - env: { - ...structuredSessionChildIdentityEnv(sessionId, launch.env), - [PROVIDER_SPAWN_TOKEN_ENV]: acquire.spawnToken - }, - envToDelete: [...ACP_CHILD_ENV_TO_DELETE, ...launch.envToDelete] + env: launch.env, + envToDelete: launch.envToDelete }, { clientInfo: { name: 'orca', version: '1' }, diff --git a/src/main/acp/acp-structured-adapter.test-support.ts b/src/main/acp/acp-structured-adapter.test-support.ts index b2bd9a3976e..fe80d84ddd1 100644 --- a/src/main/acp/acp-structured-adapter.test-support.ts +++ b/src/main/acp/acp-structured-adapter.test-support.ts @@ -170,9 +170,9 @@ export async function openAcpAdapterRig( const lifecycle: StructuredAgentSessionLifecycleEvent[] = [] const settled: AcpAdapterRig['settled'] = [] const spec = options.spec ?? GROK - const adapter = new AcpStructuredSessionAdapter({ - spec, - resolveLaunch: async () => ({ + const resolveLaunch: AcpStructuredSessionAdapterDeps['resolveLaunch'] = + options.deps?.resolveLaunch ?? + (async () => ({ spec, command: `/opt/bin/${spec.command}`, args: spec.args({ fullAccess: false, pluginDir: null }), @@ -182,7 +182,12 @@ export async function openAcpAdapterRig( fullAccess: false, resume: null, ...options.launch - }), + })) + const adapter = new AcpStructuredSessionAdapter({ + spec, + resolveLaunch, + // A double starts nothing, so a recovery read sees the launch a start would. + readLaunch: (identity) => resolveLaunch({ identity, spawnToken: 'recovery-read' }), connect: (launch, connectionOptions) => { spawned.push('spawn') const child = new FakeAcpChild(launch, connectionOptions) diff --git a/src/main/acp/acp-structured-context-clear.test.ts b/src/main/acp/acp-structured-context-clear.test.ts index 0dd327cfc86..e3177ec37a1 100644 --- a/src/main/acp/acp-structured-context-clear.test.ts +++ b/src/main/acp/acp-structured-context-clear.test.ts @@ -1,7 +1,7 @@ +import { createAcpStructuredLaunchResolver } from '../runtime/structured-agent-launch-composition.test-support' import { expect, it, vi } from 'vitest' import { agentSessionRecordFixture } from '../../shared/agent-session-record.test-fixture' import { ACP_LAUNCH_SPECS } from './acp-launch-specs' -import { createAcpStructuredLaunchResolver } from './acp-structured-launch-resolution' import { openCodeAcpAccountBinding } from '../opencode/opencode-structured-account-home' it.each(ACP_LAUNCH_SPECS)( diff --git a/src/main/acp/acp-structured-host-restore-failed.test.ts b/src/main/acp/acp-structured-host-restore-failed.test.ts index bf6c7a363c8..f4024117ec9 100644 --- a/src/main/acp/acp-structured-host-restore-failed.test.ts +++ b/src/main/acp/acp-structured-host-restore-failed.test.ts @@ -18,7 +18,7 @@ import { GROK, GROK_CONFIG_OPTIONS, PROVIDER_SESSION } from './acp-structured-ad import { ACP_HANDLE_TRANSPORT } from './acp-structured-agent-definitions' import { replayJournal } from '../native-chat/agent-session-journal/journal-open' import { AgentSessionJournal } from '../native-chat/agent-session-journal/journal-store' -import { createAcpStructuredLaunchResolver } from './acp-structured-launch-resolution' +import { createAcpStructuredLaunchResolver } from '../runtime/structured-agent-launch-composition.test-support' import { attachParams, openHostRig, @@ -76,6 +76,12 @@ async function openRestoreRig(code: number, message: string) { throw new Error('launch resolver not ready') } return resolver.resolve(input) + }, + readLaunch: (identity) => { + if (!resolver.resolve) { + throw new Error('launch resolver not ready') + } + return resolver.resolve({ identity }) } } }) diff --git a/src/main/acp/acp-structured-launch-resolution-omp.test.ts b/src/main/acp/acp-structured-launch-resolution-omp.test.ts index 42a107717d9..c432ef1337d 100644 --- a/src/main/acp/acp-structured-launch-resolution-omp.test.ts +++ b/src/main/acp/acp-structured-launch-resolution-omp.test.ts @@ -1,13 +1,11 @@ +import { createAcpStructuredLaunchResolver } from '../runtime/structured-agent-launch-composition.test-support' import { describe, expect, it, vi } from 'vitest' import type { AgentSessionRecord } from '../../shared/agent-session-record' import { agentSessionRecordFixture } from '../../shared/agent-session-record.test-fixture' import { NATIVE_CHAT_VISUALS_DIR_ENV } from '../native-chat/native-chat-visuals-delivery' import { createProviderSpawnSpec } from '../provider-process/provider-process-supervisor' -import { ACP_CHILD_ENV_TO_DELETE, acpLaunchSpecFor } from './acp-launch-specs' -import { - createAcpStructuredLaunchResolver, - type AcpStructuredLaunchResolverDeps -} from './acp-structured-launch-resolution' +import { acpLaunchSpecFor } from './acp-launch-specs' +import { STRUCTURED_CHILD_ENV_TO_DELETE } from '../runtime/structured-session-child-env' const OMP = acpLaunchSpecFor('omp')! const identity = { @@ -32,7 +30,7 @@ function resolver( options: { launchEnv?: Record fullAccess?: boolean - probeVersion?: AcpStructuredLaunchResolverDeps['probeVersion'] + probeVersion?: Parameters[1]['probeVersion'] } = {} ) { return createAcpStructuredLaunchResolver(OMP, { @@ -65,14 +63,17 @@ describe('OMP ACP launch resolution', () => { ANTHROPIC_API_KEY: 'sk-test' } const launch = await resolver(ompRecord(), { launchEnv: userEnv })({ identity }) - // Only an inherited visuals folder: this chat has none. - expect(launch.envToDelete).toEqual([NATIVE_CHAT_VISUALS_DIR_ENV]) + // Only the shared pane and hook keys, and an inherited visuals folder: this chat has none. + expect(launch.envToDelete).toEqual([ + ...STRUCTURED_CHILD_ENV_TO_DELETE, + NATIVE_CHAT_VISUALS_DIR_ENV + ]) const child = createProviderSpawnSpec( { command: launch.command, args: launch.args, env: launch.env, - envToDelete: [...ACP_CHILD_ENV_TO_DELETE, ...launch.envToDelete] + envToDelete: [...STRUCTURED_CHILD_ENV_TO_DELETE, ...launch.envToDelete] }, { ORCA_PANE_KEY: 'tab-1:pane-1' }, 'darwin' @@ -125,7 +126,9 @@ describe('OMP ACP launch resolution', () => { }) it('asks the version of exactly the binary, folder and environment it spawns', async () => { - const asked: Parameters>[0][] = [] + const asked: Parameters< + NonNullable[1]['probeVersion']> + >[0][] = [] const launch = await resolver(ompRecord(), { probeVersion: async (input, supports) => (asked.push(input), supports('17.0.5')) })({ identity }) diff --git a/src/main/acp/acp-structured-launch-resolution-opencode.test.ts b/src/main/acp/acp-structured-launch-resolution-opencode.test.ts index c508a992f68..61ad1b54ace 100644 --- a/src/main/acp/acp-structured-launch-resolution-opencode.test.ts +++ b/src/main/acp/acp-structured-launch-resolution-opencode.test.ts @@ -1,3 +1,4 @@ +import { createAcpStructuredLaunchResolver } from '../runtime/structured-agent-launch-composition.test-support' import { describe, expect, it, vi } from 'vitest' import type { AgentSessionRecord } from '../../shared/agent-session-record' import { agentSessionRecordFixture } from '../../shared/agent-session-record.test-fixture' @@ -6,11 +7,8 @@ import { restoreManagedDataAccountEnvironment } from '../../shared/managed-data- import { createProviderSpawnSpec } from '../provider-process/provider-process-supervisor' import { openCodeAcpAccountBinding } from '../opencode/opencode-structured-account-home' import { scrubOpenCodeAcpEnvironment } from '../opencode/opencode-acp-environment' -import { ACP_CHILD_ENV_TO_DELETE, acpLaunchSpecFor } from './acp-launch-specs' -import { - createAcpStructuredLaunchResolver, - type AcpStructuredLaunchResolverDeps -} from './acp-structured-launch-resolution' +import { acpLaunchSpecFor } from './acp-launch-specs' +import { STRUCTURED_CHILD_ENV_TO_DELETE } from '../runtime/structured-session-child-env' const PROFILE = '123e4567-e89b-42d3-a456-426614174000' @@ -61,7 +59,7 @@ function resolver( launchEnv?: Record inheritedEnv?: NodeJS.ProcessEnv base?: Record - probeVersion?: AcpStructuredLaunchResolverDeps['probeVersion'] + probeVersion?: Parameters[1]['probeVersion'] } = {} ) { return createAcpStructuredLaunchResolver(OPENCODE, { @@ -106,7 +104,7 @@ describe('OpenCode ACP launch resolution', () => { command: launch.command, args: launch.args, env: launch.env, - envToDelete: [...ACP_CHILD_ENV_TO_DELETE, ...launch.envToDelete] + envToDelete: [...STRUCTURED_CHILD_ENV_TO_DELETE, ...launch.envToDelete] }, {}, 'darwin' @@ -159,7 +157,7 @@ describe('OpenCode ACP launch resolution', () => { command: launch.command, args: launch.args, env: launch.env, - envToDelete: [...ACP_CHILD_ENV_TO_DELETE, ...launch.envToDelete] + envToDelete: [...STRUCTURED_CHILD_ENV_TO_DELETE, ...launch.envToDelete] }, inheritedEnv, 'darwin' @@ -179,7 +177,7 @@ describe('OpenCode ACP launch resolution', () => { command: launch.command, args: launch.args, env: launch.env, - envToDelete: [...ACP_CHILD_ENV_TO_DELETE, ...launch.envToDelete] + envToDelete: [...STRUCTURED_CHILD_ENV_TO_DELETE, ...launch.envToDelete] }, inheritedEnv, 'darwin' @@ -215,7 +213,7 @@ describe('OpenCode ACP launch resolution', () => { it('refuses a record that pins a single directory instead of an OpenCode account', async () => { await expect( resolver(openCodeRecord({ variable: 'XDG_DATA_HOME', path: '/data' }))({ identity }) - ).rejects.toThrow(/pinned data account/) + ).rejects.toThrow(/sessions pin opencode/) }) it('runs an `opencode` that is stable 2.x', async () => { @@ -245,7 +243,9 @@ describe('OpenCode ACP launch resolution', () => { }) it('asks the version of exactly the binary, folder and environment it spawns', async () => { - const asked: Parameters>[0][] = [] + const asked: Parameters< + NonNullable[1]['probeVersion']> + >[0][] = [] const launch = await resolver(openCodeRecord(UNMANAGED), { probeVersion: async (input, supports) => (asked.push(input), supports('1.18.31')) })({ identity }) diff --git a/src/main/acp/acp-structured-launch-resolution.test.ts b/src/main/acp/acp-structured-launch-resolution.test.ts index ebc4bd22185..3296c2a1fa1 100644 --- a/src/main/acp/acp-structured-launch-resolution.test.ts +++ b/src/main/acp/acp-structured-launch-resolution.test.ts @@ -1,3 +1,4 @@ +import { createAcpStructuredLaunchResolver } from '../runtime/structured-agent-launch-composition.test-support' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -17,9 +18,9 @@ import { spellProviderTimelineKey } from '../native-chat/agent-session-timeline/provider-timeline-identity' import { createProviderSpawnSpec } from '../provider-process/provider-process-supervisor' -import { ACP_CHILD_ENV_TO_DELETE, acpLaunchSpecFor } from './acp-launch-specs' +import { acpLaunchSpecFor } from './acp-launch-specs' +import { STRUCTURED_CHILD_ENV_TO_DELETE } from '../runtime/structured-session-child-env' import { acpSessionNotRestoredItem } from './acp-session-reopen-failure' -import { createAcpStructuredLaunchResolver } from './acp-structured-launch-resolution' const GROK = acpLaunchSpecFor('grok')! const identity = { @@ -270,7 +271,7 @@ describe('ACP launch resolution', () => { it('refuses a record pinned to another host or another agent', async () => { const remote = grokRecord() remote.location = { ...remote.location, executionHostId: 'ssh:box' } - await expect(resolver(remote).resolve({ identity })).rejects.toThrow(/run on this runtime/) + await expect(resolver(remote).resolve({ identity })).rejects.toThrow(/run on the local host/) await expect(resolver(agentSessionRecordFixture()).resolve({ identity })).rejects.toThrow( /claude session/ ) @@ -295,7 +296,7 @@ describe('Grok status: the structured session is the only producer', () => { command: 'grok', args: ['agent', 'stdio'], env: { GROK_HOME: '/home/user/.grok' }, - envToDelete: ACP_CHILD_ENV_TO_DELETE + envToDelete: STRUCTURED_CHILD_ENV_TO_DELETE }, inherited, 'win32' diff --git a/src/main/acp/acp-structured-launch-resolution.ts b/src/main/acp/acp-structured-launch-resolution.ts index 13e59ab127e..2f820c0c419 100644 --- a/src/main/acp/acp-structured-launch-resolution.ts +++ b/src/main/acp/acp-structured-launch-resolution.ts @@ -6,14 +6,12 @@ import { delimiter } from 'node:path' import { homedir } from 'node:os' -import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types' import { parseAgentJournalItemKey } from '../../shared/agent-session-journal-item-key' import { agentSessionProviderHandleChainHead, agentSessionProviderHandleKey, type AgentSessionProviderHandleChain } from '../../shared/agent-session-provider-handle' -import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' import type { AgentSessionAccountHome } from '../../shared/agent-session-account-home' import type { resolveCliCommand } from '../../shared/node-cli-command-resolution' import { agentSessionRefusalError } from '../../shared/agent-session-wire-refusals' @@ -29,8 +27,8 @@ import { providerTimelineKeyPart, spelledProviderTimelineItemKey } from '../native-chat/agent-session-timeline/provider-timeline-identity' -import type { AgentSessionRecordStore } from '../runtime/agent-session-record-store' -import { resolveAgentSessionLaunchDirectory } from '../runtime/agent-session-launch-directory' +import type { StructuredAgentLaunchPartResolver } from '../runtime/structured-agent-launch-composition' +import { STRUCTURED_CHILD_ENV_TO_DELETE } from '../runtime/structured-session-child-env' import { NATIVE_CHAT_VISUALS_DIR_ENV, withNativeChatVisualsEnv @@ -44,6 +42,7 @@ export type AcpStructuredLaunch = { command: string args: string[] cwd: string + /** The child's sealed environment; a read that starts no child gets the agent's own. */ env: Record /** Keys the child must not inherit from this process's own environment. */ envToDelete: string[] @@ -62,25 +61,27 @@ export type AcpStructuredLaunch = { } | null } -export type AcpStructuredLaunchResolverDeps = { - store: Pick +export type AcpStructuredLaunchPartDeps = { /** The chat's journal as it stands, read without opening it; null when it has none. */ readJournal: (sessionId: string) => JournalLoad | null - resolveWorkspacePath: (workspaceId: string) => Promise - /** The shared base env; re-read per acquisition. */ - resolveEnvironment: () => Promise> - /** The user's per-agent environment overlay from settings. */ - resolveLaunchEnv?: (agent: string) => Record /** The Agent Permissions setting's bypass posture for this agent, re-read per acquisition. */ resolveFullAccess?: (agent: string) => boolean - /** The user's per-agent Command setting, re-read per acquisition; none runs the stock binary. */ - resolveCommandSettings?: () => StructuredAgentCommandSettings resolveCommand?: typeof resolveCliCommand homePath?: string /** The environment the child process inherits at spawn; this process's own by default. */ inheritedEnv?: NodeJS.ProcessEnv probeVersion?: typeof probeAgentCliVersion -} & AcpLaunchVisualsDeps +} & Pick + +/** What a launch's binary and environment are read from; a catalog probe reads the same. */ +export type AcpLaunchInvocationDeps = { + /** The shared base env; re-read per acquisition. */ + resolveEnvironment: () => Promise> + /** The user's per-agent environment overlay from settings. */ + resolveLaunchEnv?: (agent: string) => Record + /** The user's per-agent Command setting, re-read per acquisition; none runs the stock binary. */ + resolveCommandSettings?: () => StructuredAgentCommandSettings +} & Pick /** The binary a launch with `env` spawns: the user's Command setting when set, else the stock * binary from PATH, then the agent's own install directories. A create's version check resolves @@ -142,16 +143,6 @@ export async function acpLaunchVersionSupported( return (await probeAcpLaunchVersion(spec, launch, false, probe)).supported } -export type AcpLaunchInvocationDeps = Pick< - AcpStructuredLaunchResolverDeps, - | 'resolveEnvironment' - | 'resolveLaunchEnv' - | 'resolveCommandSettings' - | 'resolveCommand' - | 'homePath' - | 'inheritedEnv' -> - /** The binary and environment any child of this agent runs under for `accountHome`: a session's * launch and a catalog probe both resolve here, so neither can reach another account or install. */ export async function resolveAcpLaunchInvocation( @@ -173,31 +164,28 @@ export async function resolveAcpLaunchInvocation( return { command, env, envToDelete } } -export function createAcpStructuredLaunchResolver( +/** An ACP agent's part of a launch: its binary under the pinned account, the release check, the + * visuals skill, its arguments and the provider session to reattach. */ +export function acpStructuredLaunchPart( spec: AcpLaunchSpec, - deps: AcpStructuredLaunchResolverDeps -): (input: { identity: AgentSessionJournalIdentity }) => Promise { - return async ({ identity }) => { - const record = deps.store.getRecord(identity.sessionId) - if (!record) { - throw new Error(`no durable agent-session record for ${identity.sessionId}`) - } - if (record.provider !== spec.agent) { - throw new Error(`session ${identity.sessionId} is a ${record.provider} session`) - } - const { location, accountHome } = record - // A session pinned elsewhere belongs to that host's runtime; starting it here would put a - // second writer on the same provider session. - if (location.executionHostId !== LOCAL_EXECUTION_HOST_ID || location.wslDistro !== null) { - throw new Error( - `${spec.agent} structured sessions run on this runtime's host, not ${location.executionHostId}` - ) - } - const { env, envToDelete, command } = await resolveAcpLaunchInvocation(spec, accountHome, deps) - const cwd = await resolveAgentSessionLaunchDirectory(deps, record) - const visuals = spec.visualsSkill - ? ((await deps.prepareVisuals?.(identity.sessionId)) ?? null) - : null + deps: AcpStructuredLaunchPartDeps +): StructuredAgentLaunchPartResolver { + return async (basis, identity) => { + const { record } = basis + const { env, envToDelete, command } = await resolveAcpLaunchInvocation( + spec, + record.accountHome, + { + resolveEnvironment: basis.baseEnvironment, + resolveLaunchEnv: () => basis.launchEnv(), + resolveCommandSettings: basis.commandSettings, + ...(deps.resolveCommand ? { resolveCommand: deps.resolveCommand } : {}), + ...(deps.homePath ? { homePath: deps.homePath } : {}), + ...(deps.inheritedEnv ? { inheritedEnv: deps.inheritedEnv } : {}) + } + ) + const cwd = await basis.launchDirectory() + const visuals = spec.visualsSkill ? await basis.visuals() : null // Again at every launch: the binary on PATH may have changed since the chat was created. const launchVersion = await probeAcpLaunchVersion( spec, @@ -220,29 +208,33 @@ export function createAcpStructuredLaunchResolver( inherited: deps.inheritedEnv ?? process.env }) : null - // Without visuals, a folder Orca itself inherited (started from a chat) names another chat's. - const childEnv = withNativeChatVisualsEnv({ ...env, ...skill?.env }, skill ? visuals : null) const fullAccess = deps.resolveFullAccess?.(spec.agent) ?? false const head = agentSessionProviderHandleChainHead(record.providerHandleChain) return { - spec, - command, - args: spec.args({ fullAccess, pluginDir: skill?.pluginDir ?? null }), - cwd, - env: childEnv, - envToDelete: skill ? envToDelete : [...envToDelete, NATIVE_CHAT_VISUALS_DIR_ENV], - fullAccess, - resume: head - ? { - sessionId: head.handle.nativeId, - key: agentSessionProviderHandleKey(head.handle), - mayBeUnsaved: () => - head.origin === 'created' && - nothingExchangedOn(deps.readJournal, identity.sessionId, head.handle.nativeId), - unannouncedLosses: () => - unannouncedLosses(deps.readJournal, identity.sessionId, record.providerHandleChain) - } - : null + launch: { + spec, + command, + args: spec.args({ fullAccess, pluginDir: skill?.pluginDir ?? null }), + fullAccess, + resume: head + ? { + sessionId: head.handle.nativeId, + key: agentSessionProviderHandleKey(head.handle), + mayBeUnsaved: () => + head.origin === 'created' && + nothingExchangedOn(deps.readJournal, identity.sessionId, head.handle.nativeId), + unannouncedLosses: () => + unannouncedLosses(deps.readJournal, identity.sessionId, record.providerHandleChain) + } + : null + }, + env: withNativeChatVisualsEnv({ ...env, ...skill?.env }, skill ? visuals : null), + inheritedEnvToDelete: [ + ...STRUCTURED_CHILD_ENV_TO_DELETE, + ...envToDelete, + // Without visuals, a folder Orca itself inherited (started from a chat) names another chat's. + ...(skill ? [] : [NATIVE_CHAT_VISUALS_DIR_ENV]) + ] } } } @@ -250,7 +242,7 @@ export function createAcpStructuredLaunchResolver( /** Proof the chat holds no turn of provider session `nativeId`; a journal that does not read whole, * or does not read at all, proves nothing. */ function nothingExchangedOn( - readJournal: AcpStructuredLaunchResolverDeps['readJournal'], + readJournal: AcpStructuredLaunchPartDeps['readJournal'], sessionId: string, nativeId: string ): boolean { @@ -278,7 +270,7 @@ function nothingExchangedOn( /** Derived on every start, never stored: a row an attach failure dropped is written by the next * start that succeeds. A journal that does not read whole proves nothing, so none is written. */ function unannouncedLosses( - readJournal: AcpStructuredLaunchResolverDeps['readJournal'], + readJournal: AcpStructuredLaunchPartDeps['readJournal'], sessionId: string, chain: AgentSessionProviderHandleChain ): string[] { diff --git a/src/main/acp/acp-structured-launch-visuals.test.ts b/src/main/acp/acp-structured-launch-visuals.test.ts index 5ca0b057d4d..b607d973458 100644 --- a/src/main/acp/acp-structured-launch-visuals.test.ts +++ b/src/main/acp/acp-structured-launch-visuals.test.ts @@ -1,13 +1,10 @@ +import { createAcpStructuredLaunchResolver } from '../runtime/structured-agent-launch-composition.test-support' import { describe, expect, it, vi } from 'vitest' import type { AgentSessionRecord } from '../../shared/agent-session-record' import { agentSessionRecordFixture } from '../../shared/agent-session-record.test-fixture' import { NATIVE_CHAT_VISUALS_DIR_ENV } from '../native-chat/native-chat-visuals-delivery' import { resolveProviderChildEnv } from '../provider-process/provider-process-launch' import { acpLaunchSpecFor, type AcpLaunchSpec } from './acp-launch-specs' -import { - createAcpStructuredLaunchResolver, - type AcpStructuredLaunchResolverDeps -} from './acp-structured-launch-resolution' const SKILL = { pluginDir: '/app/plugin', skillsRoot: '/app/plugin/skills' } const FOLDER = '/state/native-chat-visuals/0123' @@ -40,14 +37,14 @@ function resolve( version?: string inheritedEnv?: NodeJS.ProcessEnv launchEnv?: Record - deps?: Partial + deps?: Partial[1]> spec?: Partial } = {} ) { const spec = { ...acpLaunchSpecFor(agent)!, ...options.spec } - const probeVersion = vi.fn>( - async (_input, supports) => supports(options.version ?? '1.0.46') - ) + const probeVersion = vi.fn< + NonNullable[1]['probeVersion']> + >(async (_input, supports) => supports(options.version ?? '1.0.46')) const prepareVisuals = vi.fn(async () => options.visuals === false ? null : { folder: FOLDER, skill: SKILL } ) diff --git a/src/main/acp/acp-structured-session-adapter-deps.ts b/src/main/acp/acp-structured-session-adapter-deps.ts index 521e62c2c0b..64f4d120791 100644 --- a/src/main/acp/acp-structured-session-adapter-deps.ts +++ b/src/main/acp/acp-structured-session-adapter-deps.ts @@ -18,7 +18,13 @@ export const ACP_OPTION_WRITE_TIMEOUT_MS = 30_000 export type AcpStructuredSessionAdapterDeps = { spec: AcpLaunchSpec - resolveLaunch: (input: { identity: AgentSessionJournalIdentity }) => Promise + /** The child to spawn, sealed with the reservation's token. */ + resolveLaunch: (input: { + identity: AgentSessionJournalIdentity + spawnToken: string + }) => Promise + /** The same launch for a read that starts no child (restart recovery): the agent's env, unsealed. */ + readLaunch: (identity: AgentSessionJournalIdentity) => Promise /** The chat's journal as it stands, read without opening it; null when it has none. */ readJournal?: (sessionId: string) => JournalLoad | null /** Starts the agent's process and owns its protocol: `createAcpAgentConnection` in production. */ diff --git a/src/main/acp/acp-structured-session-adapter.test.ts b/src/main/acp/acp-structured-session-adapter.test.ts index 7299c13c6e3..291dfe000d6 100644 --- a/src/main/acp/acp-structured-session-adapter.test.ts +++ b/src/main/acp/acp-structured-session-adapter.test.ts @@ -10,7 +10,7 @@ import { AgentSessionAcquisitionRootExitObservedError } from '../native-chat/agent-session-wire/structured-agent-session-adapter' import { GENERIC_ACP_DIALECT } from './acp-dialects/acp-dialect' -import { ACP_CHILD_ENV_TO_DELETE } from './acp-launch-specs' +import { STRUCTURED_CHILD_ENV_TO_DELETE } from '../runtime/structured-session-child-env' import { AGENT_HOOK_RUNTIME_ENV_KEYS } from '../ipc/pty/host-env/spawn-env-keys' import { GROK, @@ -21,6 +21,8 @@ import { type AcpAdapterRig } from './acp-structured-adapter.test-support' import type { AgentJournalMessageItem } from '../../shared/agent-session-journal-types' +import { createAcpStructuredLaunchResolver } from '../runtime/structured-agent-launch-composition.test-support' +import { agentSessionRecordFixture } from '../native-chat/agent-session-record-test-fixture' afterEach(async () => { await closeProviderTimelineRigs() @@ -65,10 +67,30 @@ describe('ACP structured session adapter: acquire', () => { }) it('launches without any pane identity, so the agent own status hooks stay silent', async () => { - const rig = await openAcpAdapterRig() + const record = agentSessionRecordFixture({ + sessionId: SESSION, + provider: 'grok', + accountHome: { variable: 'GROK_HOME', path: '/home/work/.grok' } + }) + // The launch its registration composes: the shared seal names the child, not this adapter. + const rig = await openAcpAdapterRig({ + deps: { + resolveLaunch: createAcpStructuredLaunchResolver(GROK, { + store: { getRecord: () => record, pinLaunchDirectory: async () => record }, + resolveWorkspacePath: async () => '/workspace/project', + resolveEnvironment: async () => ({ + PATH: '/usr/bin', + ORCA_PANE_KEY: 'tab-1:pane-1', + ORCA_AGENT_HOOK_PORT: '1234' + }), + resolveCommand: () => '/opt/bin/grok', + readJournal: () => null + }) + } + }) await rig.acquire() const launch = rig.child().launch - expect(launch.envToDelete).toEqual(expect.arrayContaining([...ACP_CHILD_ENV_TO_DELETE])) + expect(launch.envToDelete).toEqual(expect.arrayContaining([...STRUCTURED_CHILD_ENV_TO_DELETE])) expect(launch.envToDelete).toEqual( expect.arrayContaining(['ORCA_PANE_KEY', 'ORCA_AGENT_PANE', ...AGENT_HOOK_RUNTIME_ENV_KEYS]) ) diff --git a/src/main/claude/claude-agent-sdk-contract-pins.test.ts b/src/main/claude/claude-agent-sdk-contract-pins.test.ts index 65e536d6bd7..487011dbdf6 100644 --- a/src/main/claude/claude-agent-sdk-contract-pins.test.ts +++ b/src/main/claude/claude-agent-sdk-contract-pins.test.ts @@ -1,3 +1,4 @@ +import { createClaudeStructuredLaunchResolver } from '../runtime/structured-agent-launch-composition.test-support' import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -15,7 +16,6 @@ import { spawnProcess } from '@orca/process-host' import type { AgentSessionRecord } from '../../shared/agent-session-record' import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' import { claudeQuerySettingsReader } from './claude-agent-sdk-control-requests' -import { createClaudeStructuredLaunchResolver } from './claude-structured-launch-resolution' // Contract pins for @anthropic-ai/claude-agent-sdk, run against the real SDK // driving a scripted fake CLI (never the real Claude binary). These tests exist diff --git a/src/main/claude/claude-model-catalog-probe.ts b/src/main/claude/claude-model-catalog-probe.ts index 8dbaf3b6e73..5b85e86266a 100644 --- a/src/main/claude/claude-model-catalog-probe.ts +++ b/src/main/claude/claude-model-catalog-probe.ts @@ -8,8 +8,9 @@ import { AgentModelCatalogUnavailableError } from '../native-chat/agent-model-ca import { claudeConfigDirEnvPatch } from './claude-config-dir-pin' import { resolveClaudeStructuredInvocation, - type ClaudeStructuredLaunchResolverDeps + type ClaudeStructuredLaunchPartDeps } from './claude-structured-launch-resolution' +import type { ClaudeEnvDeps } from './claude-structured-child-env' import type { AgentModelCatalogProbe, AgentModelCatalogSuccess @@ -19,13 +20,11 @@ import { runAgentModelCatalogListing } from '../native-chat/agent-model-catalog/agent-model-catalog-probe-runner' -export type ClaudeModelCatalogProbeDeps = Pick< - ClaudeStructuredLaunchResolverDeps, - 'resolveCommand' | 'resolveEnv' | 'resolveInheritedEnv' | 'resolveAuthPolicy' -> & { - /** Test seam; production runs the shared supervised listing. */ - runListing?: typeof runAgentModelCatalogListing -} +export type ClaudeModelCatalogProbeDeps = ClaudeEnvDeps & + Pick & { + /** Test seam; production runs the shared supervised listing. */ + runListing?: typeof runAgentModelCatalogListing + } /** * Lists models without a live session: one `list_models` control request, under the SAME binary diff --git a/src/main/claude/claude-stream-json-connection.test.ts b/src/main/claude/claude-stream-json-connection.test.ts index a275b75d93d..5466c540355 100644 --- a/src/main/claude/claude-stream-json-connection.test.ts +++ b/src/main/claude/claude-stream-json-connection.test.ts @@ -221,6 +221,25 @@ describe('Claude stream-json connection', () => { expect(report.argv).toContain(`--session-id=${SESSION_ID}`) }) + it("strips the launch's inherited keys after Orca's own env is merged in", async () => { + vi.stubEnv('ORCA_PANE_KEY', 'tab-1:pane-1') + vi.stubEnv('ORCA_AGENT_HOOK_PORT', '4242') + vi.stubEnv('ORCA_CONNECTION_MARKER', 'inherited') + const scenario = scriptScenario([HOLD_OPEN]) + await open({ + ...launchFor(scenario, { ORCA_AGENT_SESSION_ID: 'f7a1c0de-1111-4222-8333-444455556666' }), + envToDelete: ['ORCA_PANE_KEY', 'ORCA_AGENT_HOOK_PORT'] + }) + + await until(() => readReportSafely(scenario), 'the scripted CLI report') + const env = childEnv() + expect(env.ORCA_PANE_KEY).toBeUndefined() + expect(env.ORCA_AGENT_HOOK_PORT).toBeUndefined() + // Only the listed keys go: the rest of the inherited env and the launch's own still arrive. + expect(env.ORCA_CONNECTION_MARKER).toBe('inherited') + expect(env.ORCA_AGENT_SESSION_ID).toBe('f7a1c0de-1111-4222-8333-444455556666') + }) + it('leaves the default CLI home unpinned so macOS Keychain OAuth keeps working', async () => { const scenario = scriptScenario([HOLD_OPEN]) await open(launchFor(scenario)) diff --git a/src/main/claude/claude-stream-json-connection.ts b/src/main/claude/claude-stream-json-connection.ts index d22c842ed7e..828a58e043a 100644 --- a/src/main/claude/claude-stream-json-connection.ts +++ b/src/main/claude/claude-stream-json-connection.ts @@ -54,6 +54,15 @@ export type ClaudeStreamJsonLaunch = { options: ClaudeStructuredSdkOptions cwd: string env?: Record + /** Removed after the child's env is built over Orca's own, as every provider child's are. */ + envToDelete?: readonly string[] +} + +function withoutKeys(env: Record, keys: readonly string[]): Record { + for (const key of keys) { + delete env[key] + } + return env } export type ClaudeStreamJsonConnectionHandlers = { @@ -141,10 +150,13 @@ export async function openClaudeStreamJsonConnection( // exactly the ambient ANTHROPIC_* auth leak this lane already shipped once. // Orca's own CLAUDE_CONFIG_DIR is dropped for the same reason the launch drops the // shell's: the record's pin in `launch.env` must be the only home the child sees. - env: buildClaudeChildProcessEnv(launch.env, { - inheritedEnv: withoutInheritedClaudeConfigDir(process.env), - scrubConfiguredChildSessionStamps: true - }), + env: withoutKeys( + buildClaudeChildProcessEnv(launch.env, { + inheritedEnv: withoutInheritedClaudeConfigDir(process.env), + scrubConfiguredChildSessionStamps: true + }), + launch.envToDelete ?? [] + ), pathToClaudeCodeExecutable: launch.pathToClaudeCodeExecutable, spawnClaudeCodeProcess: spawner.spawn, ...(handlers.canUseTool ? { canUseTool: handlers.canUseTool } : {}), diff --git a/src/main/claude/claude-structured-acquisition-launch.ts b/src/main/claude/claude-structured-acquisition-launch.ts index 66f0bf0d665..cf7a868e518 100644 --- a/src/main/claude/claude-structured-acquisition-launch.ts +++ b/src/main/claude/claude-structured-acquisition-launch.ts @@ -89,7 +89,7 @@ export async function resolveClaudeAcquisitionLaunch(args: { } : input.identity const launch = await deps - .resolveLaunch({ identity: launchIdentity }) + .resolveLaunch({ identity: launchIdentity, spawnToken: input.spawnToken }) .catch((error: unknown) => { throw error instanceof AgentSessionPreSpawnError ? error diff --git a/src/main/claude/claude-structured-auth-parity.test.ts b/src/main/claude/claude-structured-auth-parity.test.ts index c2bee84dfc9..7ee2e59ff6b 100644 --- a/src/main/claude/claude-structured-auth-parity.test.ts +++ b/src/main/claude/claude-structured-auth-parity.test.ts @@ -1,7 +1,7 @@ +import { createClaudeStructuredLaunchResolver } from '../runtime/structured-agent-launch-composition.test-support' import { describe, expect, it, vi } from 'vitest' import type { AgentSessionRecord } from '../../shared/agent-session-record' import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' -import { createClaudeStructuredLaunchResolver } from './claude-structured-launch-resolution' const SESSION_ID = 'orca-session-auth' const IDENTITY = { sessionId: SESSION_ID } as Parameters< diff --git a/src/main/claude/claude-structured-child-env.ts b/src/main/claude/claude-structured-child-env.ts index 8c9cc21cfc8..8db2a6b0903 100644 --- a/src/main/claude/claude-structured-child-env.ts +++ b/src/main/claude/claude-structured-child-env.ts @@ -5,7 +5,6 @@ import { withCliRuntimeOnPath } from '../../shared/node-cli-command-resolution' import { applyClaudeEnvPatch } from '../claude-accounts/environment' import { resolveClaudeCommand } from '../codex-cli/command' import { withoutInheritedClaudeConfigDir } from './claude-config-dir-pin' -import type { ClaudeStructuredLaunchResolverDeps } from './claude-structured-launch-resolution' function cloneDefinedEnv(env: NodeJS.ProcessEnv | Record): Record { const next: Record = {} @@ -17,10 +16,15 @@ function cloneDefinedEnv(env: NodeJS.ProcessEnv | Record): Recor return next } -export type ClaudeEnvDeps = Pick< - ClaudeStructuredLaunchResolverDeps, - 'resolveCommand' | 'resolveEnv' | 'resolveInheritedEnv' -> +export type ClaudeEnvDeps = { + resolveCommand?: () => string + resolveEnv?: () => + | Promise | undefined> + | Record + | undefined + /** The env the child inherits before auth stripping; absent inherits Orca's own process env. */ + resolveInheritedEnv?: () => Promise> +} /** What a child's env is built from, before any auth policy applies to it. */ export type ClaudeChildEnvSources = { @@ -46,16 +50,17 @@ export async function resolveClaudeChildEnvSources( // every account, as it does in a terminal. export function claudeChildEnv( sources: ClaudeChildEnvSources, - decorateEnv: (env: Record) => Record = (env) => env + decorateEnv: (env: Record) => Record = (env) => env, + /** False when the session's seal pairs the CLI runtime itself, after the session's identity. */ + { runtimeOnPath = true }: { runtimeOnPath?: boolean } = {} ): Record { - return withCliRuntimeOnPath( - sources.command, - decorateEnv({ - ...withoutInheritedClaudeConfigDir(sources.inheritedEnv, process.platform), - ...sources.overlay - }), - { platform: process.platform } - ) + const env = decorateEnv({ + ...withoutInheritedClaudeConfigDir(sources.inheritedEnv, process.platform), + ...sources.overlay + }) + return runtimeOnPath + ? withCliRuntimeOnPath(sources.command, env, { platform: process.platform }) + : env } /** The env a `--version` probe runs with: the launch's own env, PATH and shims included, minus the diff --git a/src/main/claude/claude-structured-launch-account-routing.test.ts b/src/main/claude/claude-structured-launch-account-routing.test.ts index eda02246f19..247d676da8f 100644 --- a/src/main/claude/claude-structured-launch-account-routing.test.ts +++ b/src/main/claude/claude-structured-launch-account-routing.test.ts @@ -1,3 +1,4 @@ +import { createClaudeStructuredLaunchResolver } from '../runtime/structured-agent-launch-composition.test-support' import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join, resolve as resolvePath } from 'node:path' @@ -10,7 +11,6 @@ import { type ClaudeProfileRouterSettings } from '../claude-accounts/claude-profile-router' import { installClaudeProfileRouter } from '../claude-accounts/claude-profile-installed-router' -import { createClaudeStructuredLaunchResolver } from './claude-structured-launch-resolution' const roots: string[] = [] afterEach(() => { diff --git a/src/main/claude/claude-structured-launch-resolution.test.ts b/src/main/claude/claude-structured-launch-resolution.test.ts index 963df6ce769..d85ce3a70be 100644 --- a/src/main/claude/claude-structured-launch-resolution.test.ts +++ b/src/main/claude/claude-structured-launch-resolution.test.ts @@ -1,3 +1,4 @@ +import { createClaudeStructuredLaunchResolver } from '../runtime/structured-agent-launch-composition.test-support' import { chmodSync, mkdtempSync, mkdirSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { delimiter, dirname, join } from 'node:path' @@ -9,9 +10,7 @@ import { CLAUDE_DEFAULT_SETTING_SOURCES, CLAUDE_SESSION_STATE_EVENTS_ENV, CLAUDE_STRUCTURED_BASE_OPTIONS, - claudeSessionIdForOrcaSession, - createClaudeStructuredLaunchResolver, - type ClaudeStructuredLaunchResolverDeps + claudeSessionIdForOrcaSession } from './claude-structured-launch-resolution' import { claudeStructuredPermissionModeForSettings } from './claude-structured-permission-mode' import { CLAUDE_THINKING_DISPLAY_FLAG, type ClaudeCliFlag } from './claude-cli-flag-support' @@ -574,7 +573,7 @@ describe('claude structured launch resolution', () => { describe('readable Claude thinking', () => { const launchWith = ( - cliFlags?: ClaudeStructuredLaunchResolverDeps['cliFlags'], + cliFlags?: Parameters[0]['cliFlags'], command = '/usr/local/bin/claude', launchArgs: string[] = [] ) => diff --git a/src/main/claude/claude-structured-launch-resolution.ts b/src/main/claude/claude-structured-launch-resolution.ts index eb2cf04ae97..f7d937ccab5 100644 --- a/src/main/claude/claude-structured-launch-resolution.ts +++ b/src/main/claude/claude-structured-launch-resolution.ts @@ -5,11 +5,8 @@ import type { Options as ClaudeAgentSdkOptions, PermissionMode } from '@anthropic-ai/claude-agent-sdk' -import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types' import { agentSessionProviderHandleRoot } from '../../shared/agent-session-provider-handle' import { claudeProviderHandleLeafUuid } from '../../shared/agent-session-provider-handle-encoding' -import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' -import { structuredSessionChildIdentityEnv } from '../runtime/structured-session-child-identity-env' import type { ClaudeStructuredAuthPolicy } from '../claude-accounts/claude-structured-auth-policy' import { claudeChildEnv, @@ -21,17 +18,12 @@ import { import { claudeStructuredLaunchArgs } from './claude-structured-launch-args' import type { ClaudeCliFlagSupport } from './claude-cli-flag-support' import { resolveClaudeLaunchFlags } from './claude-structured-launch-flags' -import { - withNativeChatVisualsEnv, - type PrepareNativeChatVisuals -} from '../native-chat/native-chat-visuals-delivery' +import { withNativeChatVisualsEnv } from '../native-chat/native-chat-visuals-delivery' import { claudeLaunchResumesTranscript, resolveClaudeStructuredLaunchHome } from './claude-structured-launch-home' -import type { AgentSessionRecordStore } from '../runtime/agent-session-record-store' -import { resolveAgentSessionLaunchDirectory } from '../runtime/agent-session-launch-directory' -import { CLAUDE_STRUCTURED_AGENT } from './claude-structured-agent-definition' +import type { StructuredAgentLaunchPartResolver } from '../runtime/structured-agent-launch-composition' import { claudeSessionIdForOrcaSession } from './claude-structured-session-id' export { claudeSessionIdForOrcaSession } @@ -92,7 +84,9 @@ export type ClaudeStructuredLaunch = { account?: ClaudeStructuredInvocation['account'] options: ClaudeStructuredSdkOptions cwd: string + /** The child's sealed environment, and the inherited keys it must not keep. */ env?: Record + envToDelete?: readonly string[] claudeConfigDir: string providerSessionId: string /** The previous head leaf, carried into the publication link; never a resume argument. */ @@ -104,17 +98,9 @@ export type ClaudeStructuredLaunch = { continuesChain: boolean } -export type ClaudeStructuredLaunchResolverDeps = { - store: Pick +export type ClaudeStructuredLaunchPartDeps = { resolveLaunchArgs: () => Promise | string[] - resolveWorkspacePath: (workspaceId: string) => Promise resolveCommand?: () => string - resolveEnv?: () => - | Promise | undefined> - | Record - | undefined - /** The env the child inherits before auth stripping; absent inherits Orca's own process env. */ - resolveInheritedEnv?: () => Promise> /** Required, so every caller states which login a failed sign-in names rather than inherit a * guess. Build it with claudeStructuredAuthPolicyForSettings. */ resolveAuthPolicy: () => Promise | ClaudeStructuredAuthPolicy @@ -125,8 +111,6 @@ export type ClaudeStructuredLaunchResolverDeps = { attachmentDirectory?: string /** Which version-gated flags this CLI takes. Absent ⇒ none is ever passed. */ cliFlags?: Pick - /** This chat's visuals folder and skill; absent or null ⇒ the chat gets neither. */ - prepareVisuals?: PrepareNativeChatVisuals /** Whether Claude wrote a transcript for this id; defaults to the transcript resolver. */ hasTranscript?: (input: { providerSessionId: string @@ -148,47 +132,32 @@ export type ClaudeStructuredInvocation = { * drift there heals on the next refresh. */ export async function resolveClaudeStructuredInvocation( - deps: ClaudeEnvDeps & Pick, + deps: ClaudeEnvDeps & Pick, decorateEnv: (env: Record) => Record = (env) => env, /** Already resolved by a caller that needed them earlier; read again otherwise. */ - resolvedSources?: ClaudeChildEnvSources + resolvedSources?: ClaudeChildEnvSources, + options?: Parameters[2] ): Promise { const sources = resolvedSources ?? (await resolveClaudeChildEnvSources(deps)) const auth = await deps.resolveAuthPolicy() // Why never strip: a shell proxy's key must travel with its ANTHROPIC_BASE_URL, on every account. return { command: sources.command, - env: claudeChildEnv(sources, decorateEnv), + env: claudeChildEnv(sources, decorateEnv, options), account: auth.account } } -export function createClaudeStructuredLaunchResolver( - deps: ClaudeStructuredLaunchResolverDeps -): (input: { identity: AgentSessionJournalIdentity }) => Promise { - return async ({ identity }) => { - const record = deps.store.getRecord(identity.sessionId) - if (!record) { - throw new Error(`no durable agent-session record for ${identity.sessionId}`) - } - if (record.provider !== 'claude') { - throw new Error(`session ${identity.sessionId} is a ${record.provider} session`) - } - if ( - record.location.executionHostId !== LOCAL_EXECUTION_HOST_ID || - record.location.wslDistro !== null - ) { - throw new Error( - `claude structured sessions run on the local host, not ${record.location.executionHostId}` - ) - } +/** Claude's part of a launch: its resume decision, flags, permission posture, account home and + * auth, with the account-switch recheck last. */ +export function claudeStructuredLaunchPart( + deps: ClaudeStructuredLaunchPartDeps +): StructuredAgentLaunchPartResolver { + return async (basis, identity) => { + const { record } = basis const accountHome = requireLegacyAgentSessionAccountHome(record.accountHome) - const pinned = CLAUDE_STRUCTURED_AGENT.accountHomeVariable - if (accountHome.variable !== pinned) { - throw new Error(`claude sessions pin ${pinned}, not ${accountHome.variable}`) - } const router = getClaudeProfileRouter() - // A Claude record's chain holds only Claude handles; the attach admission refuses anything else. + // A Claude record's chain holds only Claude handles; the launch admission refuses anything else. const active = activeProviderContext(record) const head = active.head?.handle ?? null if ( @@ -206,11 +175,21 @@ export function createClaudeStructuredLaunchResolver( active.pendingClear ? record.providerContextBoundary?.operationId : undefined ) const continuesChain = head !== null - const cwd = await resolveAgentSessionLaunchDirectory(deps, record) - const sources = await resolveClaudeChildEnvSources(deps) + const cwd = await basis.launchDirectory() + const envDeps: ClaudeEnvDeps & Pick = { + ...(deps.resolveCommand ? { resolveCommand: deps.resolveCommand } : {}), + resolveEnv: () => basis.launchEnv(), + resolveInheritedEnv: basis.baseEnvironment, + resolveAuthPolicy: deps.resolveAuthPolicy + } + const sources = await resolveClaudeChildEnvSources(envDeps) // Asked as soon as the spawn's cwd and PATH are known, so it overlaps what is left to resolve. const probeLaunch = { command: sources.command, cwd, env: claudeProbeEnv(sources) } - const launchFlags = resolveClaudeLaunchFlags(deps, record.sessionId, probeLaunch) + const launchFlags = resolveClaudeLaunchFlags( + { ...(deps.cliFlags ? { cliFlags: deps.cliFlags } : {}), prepareVisuals: basis.visuals }, + record.sessionId, + probeLaunch + ) const configured = claudeStructuredLaunchArgs(await deps.resolveLaunchArgs()) const { thinkingDisplayArgs, visuals } = await launchFlags const additionalDirectories = [ @@ -237,48 +216,50 @@ export function createClaudeStructuredLaunchResolver( const resumedWithoutRouter = router ? undefined : await resumes(accountHome.path) // Last: it rechecks the account switch, which may have begun during any await above. const { command, env, account } = await resolveClaudeStructuredInvocation( - deps, + envDeps, (base) => - // Every structured session speaks orchestration as itself: its injected id and the Orca CLI. - structuredSessionChildIdentityEnv( - record.sessionId, - withNativeChatVisualsEnv( - { - ...base, - // The turn translator relies on Claude's authoritative idle frame when no result arrives. - [CLAUDE_SESSION_STATE_EVENTS_ENV]: '1' - }, - visuals?.visuals ?? null - ) + withNativeChatVisualsEnv( + { + ...base, + // The turn translator relies on Claude's authoritative idle frame when no result arrives. + [CLAUDE_SESSION_STATE_EVENTS_ENV]: '1' + }, + visuals?.visuals ?? null ), - sources + sources, + { runtimeOnPath: false } ) const launchHome = await resolveClaudeStructuredLaunchHome(router, env, accountHome.path) const resumesTranscript = resumedWithoutRouter ?? (await resumes(launchHome)) return { - pathToClaudeCodeExecutable: command, - account, - options: { - ...CLAUDE_STRUCTURED_BASE_OPTIONS, - ...permission, - ...(additionalDirectories.length ? { additionalDirectories } : {}), - ...(visuals?.pluginDir ? { plugins: [{ type: 'local', path: visuals.pluginDir }] } : {}), - extraArgs: { - ...configured.extraArgs, - ...CLAUDE_STRUCTURED_BASE_OPTIONS.extraArgs, - ...permission.extraArgs, - ...thinkingDisplayArgs + launch: { + pathToClaudeCodeExecutable: command, + account, + options: { + ...CLAUDE_STRUCTURED_BASE_OPTIONS, + ...permission, + ...(additionalDirectories.length ? { additionalDirectories } : {}), + ...(visuals?.pluginDir ? { plugins: [{ type: 'local', path: visuals.pluginDir }] } : {}), + extraArgs: { + ...configured.extraArgs, + ...CLAUDE_STRUCTURED_BASE_OPTIONS.extraArgs, + ...permission.extraArgs, + ...thinkingDisplayArgs + }, + // Claude owns where a resumed conversation continues; the stored leaf is Orca's bookkeeping. + ...(resumesTranscript ? { resume: providerSessionId } : { sessionId: providerSessionId }) }, - // Claude owns where a resumed conversation continues; the stored leaf is Orca's bookkeeping. - ...(resumesTranscript ? { resume: providerSessionId } : { sessionId: providerSessionId }) + claudeConfigDir: launchHome, + providerSessionId, + resumeLeafUuid: resumesTranscript ? leafUuid : null, + resumesTranscript, + continuesChain }, - cwd, env, - claudeConfigDir: launchHome, - providerSessionId, - resumeLeafUuid: resumesTranscript ? leafUuid : null, - resumesTranscript, - continuesChain + // Every structured session speaks orchestration as itself; Claude strips no inherited key. + inheritedEnvToDelete: [], + // Its own runtime's directory goes ahead of this app's CLI directory on PATH. + cliRuntimeCommand: command } } } diff --git a/src/main/claude/claude-structured-launch-visuals.test.ts b/src/main/claude/claude-structured-launch-visuals.test.ts index b7596703601..6d94fe88af0 100644 --- a/src/main/claude/claude-structured-launch-visuals.test.ts +++ b/src/main/claude/claude-structured-launch-visuals.test.ts @@ -1,3 +1,4 @@ +import { createClaudeStructuredLaunchResolver } from '../runtime/structured-agent-launch-composition.test-support' import { describe, expect, it, vi } from 'vitest' import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types' import { agentSessionRecordFixture } from '../native-chat/agent-session-record-test-fixture' @@ -11,7 +12,6 @@ import { CLAUDE_PLUGIN_DIR_FLAG, type ClaudeCliFlag } from './claude-cli-flag-support' -import { createClaudeStructuredLaunchResolver } from './claude-structured-launch-resolution' const SESSION_ID = 'orca-session-visuals' const VISUALS: NativeChatVisualsLaunch = { diff --git a/src/main/claude/claude-structured-owner-identity.test.ts b/src/main/claude/claude-structured-owner-identity.test.ts index 376b9d83a49..a2488662a73 100644 --- a/src/main/claude/claude-structured-owner-identity.test.ts +++ b/src/main/claude/claude-structured-owner-identity.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from 'vitest' -import { CLAUDE_SPAWN_TOKEN_ENV, claudeProcessIdentity } from './claude-structured-owner-identity' +import { claudeProcessIdentity } from './claude-structured-owner-identity' +import { ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV } from '../../shared/agent-session-caller-env' import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding' const IDENTITY = { @@ -12,7 +13,7 @@ const IDENTITY = { describe('claude structured owner identity', () => { it('exports the spawn token env and records the observed process identity', async () => { - expect(CLAUDE_SPAWN_TOKEN_ENV).toBe('ORCA_AGENT_SESSION_SPAWN_TOKEN') + expect(ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV).toBe('ORCA_AGENT_SESSION_SPAWN_TOKEN') await expect( claudeProcessIdentity( { identity: IDENTITY, spawnToken: 'spawn-a', pid: 4242 }, diff --git a/src/main/claude/claude-structured-owner-identity.ts b/src/main/claude/claude-structured-owner-identity.ts index 65589b53aa4..ab74eb8b218 100644 --- a/src/main/claude/claude-structured-owner-identity.ts +++ b/src/main/claude/claude-structured-owner-identity.ts @@ -24,10 +24,6 @@ export function claudeProviderHandleLink(input: { } } -/** The child echoes its spawn token here so the owner probe can tell a live - * child of this reservation from a same-pid stranger. */ -export const CLAUDE_SPAWN_TOKEN_ENV = 'ORCA_AGENT_SESSION_SPAWN_TOKEN' - const START_TIME_READ_ATTEMPTS = 3 export async function claudeProcessIdentity( diff --git a/src/main/claude/claude-structured-real-cli-visuals.test.ts b/src/main/claude/claude-structured-real-cli-visuals.test.ts index 7e3ff7c5c95..2365b85d735 100644 --- a/src/main/claude/claude-structured-real-cli-visuals.test.ts +++ b/src/main/claude/claude-structured-real-cli-visuals.test.ts @@ -1,3 +1,4 @@ +import { createClaudeStructuredLaunchResolver } from '../runtime/structured-agent-launch-composition.test-support' // The real Claude CLI, launched through Orca's own launch resolution with a chat's visuals: the // CLI must report the bundled skill and its plugin in its own init frame. Opt-in, like every // real-CLI suite: it spends one short turn on the signed-in account. @@ -13,7 +14,6 @@ import { nativeChatVisualsFolderFor } from '../native-chat/native-chat-visuals-f import { NATIVE_CHAT_VISUALS_SKILL_NAME } from '../native-chat/native-chat-visuals-skill-location' import { createClaudeCliFlagSupport } from './claude-cli-flag-support' import { readClaudeInit } from './claude-structured-init-proof' -import { createClaudeStructuredLaunchResolver } from './claude-structured-launch-resolution' import { realClaudeAuthenticated, realClaudeAvailable, diff --git a/src/main/claude/claude-structured-session-acquisition.ts b/src/main/claude/claude-structured-session-acquisition.ts index a518e13d561..1cf0ded65df 100644 --- a/src/main/claude/claude-structured-session-acquisition.ts +++ b/src/main/claude/claude-structured-session-acquisition.ts @@ -14,7 +14,7 @@ import { readClaudeInit } from './claude-structured-init-proof' import { claudeConfigDirEnvPatch } from './claude-config-dir-pin' -import { CLAUDE_SPAWN_TOKEN_ENV, claudeProcessIdentity } from './claude-structured-owner-identity' +import { claudeProcessIdentity } from './claude-structured-owner-identity' import { ClaudePromptRegistry } from './claude-structured-prompt-replies' import { adoptClaudeStructuredSpawnOptions } from './claude-structured-spawn-options' import { observeClaudeFastModeFacts } from './claude-structured-session-options' @@ -169,7 +169,6 @@ export async function acquireClaudeSession({ cwd: launch.cwd, env: { ...launch.env, - [CLAUDE_SPAWN_TOKEN_ENV]: input.spawnToken, // Compared against what the child would otherwise inherit, so the record's // account home still wins over a diverging overlay without a needless pin. // (`process` is shadowed by a local later in this function, so it is not named here.) @@ -177,7 +176,8 @@ export async function acquireClaudeSession({ launch.claudeConfigDir, launch.env ? { env: launch.env } : {} ) - } + }, + ...(launch.envToDelete ? { envToDelete: launch.envToDelete } : {}) }, { onMessage, diff --git a/src/main/claude/claude-structured-session-adapter.test.ts b/src/main/claude/claude-structured-session-adapter.test.ts index 0897917718d..a24a869ec12 100644 --- a/src/main/claude/claude-structured-session-adapter.test.ts +++ b/src/main/claude/claude-structured-session-adapter.test.ts @@ -7,7 +7,7 @@ import { AgentSessionPromptAnswerRejectedError } from '../native-chat/agent-session-wire/structured-agent-session-adapter' import type { ClaudeStreamJsonConnection } from './claude-stream-json-connection' -import { CLAUDE_SPAWN_TOKEN_ENV } from './claude-structured-owner-identity' +import { ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV as SPAWN_TOKEN_ENV } from '../../shared/agent-session-caller-env' import { ClaudeControlRequestError, ClaudeControlRequestTimeoutError @@ -44,7 +44,7 @@ describe('ClaudeStructuredSessionAdapter.acquire', () => { expect(claude.connections[0].launch).toMatchObject({ cwd: '/work/repo', env: { - [CLAUDE_SPAWN_TOKEN_ENV]: 'spawn-9', + [SPAWN_TOKEN_ENV]: 'spawn-9', CLAUDE_CONFIG_DIR: '/accounts/claude' } }) @@ -391,7 +391,7 @@ describe('ClaudeStructuredSessionAdapter.acquire', () => { ANTHROPIC_AUTH_TOKEN: 'configured-token', ANTHROPIC_BASE_URL: 'https://gateway.example.test', CLAUDE_CONFIG_DIR: '/wrong/account', - [CLAUDE_SPAWN_TOKEN_ENV]: 'wrong-token' + [SPAWN_TOKEN_ENV]: 'wrong-token' } }) @@ -401,7 +401,7 @@ describe('ClaudeStructuredSessionAdapter.acquire', () => { ANTHROPIC_AUTH_TOKEN: 'configured-token', ANTHROPIC_BASE_URL: 'https://gateway.example.test', CLAUDE_CONFIG_DIR: '/accounts/claude', - [CLAUDE_SPAWN_TOKEN_ENV]: 'spawn-9' + [SPAWN_TOKEN_ENV]: 'spawn-9' }) }) @@ -412,7 +412,7 @@ describe('ClaudeStructuredSessionAdapter.acquire', () => { await adapter.acquire({ identity: identityFor(), fence: 7, spawnToken: 'spawn-9' }) // Pinning the CLI's own default suppresses the macOS Keychain and breaks claude.ai login. - expect(claude.connections[0].launch.env).toEqual({ [CLAUDE_SPAWN_TOKEN_ENV]: 'spawn-9' }) + expect(claude.connections[0].launch.env).toEqual({ [SPAWN_TOKEN_ENV]: 'spawn-9' }) }) it('re-pins the account home when the launch env would send the child elsewhere', async () => { @@ -427,7 +427,7 @@ describe('ClaudeStructuredSessionAdapter.acquire', () => { expect(claude.connections[0].launch.env).toEqual({ CLAUDE_CONFIG_DIR: accountHome, - [CLAUDE_SPAWN_TOKEN_ENV]: 'spawn-9' + [SPAWN_TOKEN_ENV]: 'spawn-9' }) }) diff --git a/src/main/claude/claude-structured-session-recovery.test.ts b/src/main/claude/claude-structured-session-recovery.test.ts index e84ff7dbe5f..fbee2504466 100644 --- a/src/main/claude/claude-structured-session-recovery.test.ts +++ b/src/main/claude/claude-structured-session-recovery.test.ts @@ -413,7 +413,8 @@ describe('ClaudeStructuredSessionAdapter close and exit recovery', () => { identity: { ...identityFor(), providerHandle: claudeProviderHandle(PROVIDER_SESSION_ID, 'observed-retained-leaf') - } + }, + spawnToken: expect.any(String) }) expect(oldPrompt.settled()).toBe(true) expect(events.filter((event) => event.type === 'ended')).toEqual([ diff --git a/src/main/claude/claude-structured-session-state.ts b/src/main/claude/claude-structured-session-state.ts index 000626f4637..2dd2849402a 100644 --- a/src/main/claude/claude-structured-session-state.ts +++ b/src/main/claude/claude-structured-session-state.ts @@ -100,6 +100,7 @@ export type ClaudeStructuredSessionAdapterDeps = { atRestCommands?: ClaudeAtRestCommandCatalog resolveLaunch: (input: { identity: AgentSessionJournalIdentity + spawnToken: string }) => Promise onEvent?: (event: ClaudeStructuredSessionEvent) => void /** Direct settlement path for provider-proven late dispatch outcomes. */ diff --git a/src/main/claude/claude-structured-session-test-support.ts b/src/main/claude/claude-structured-session-test-support.ts index c29c6693591..9be029def0c 100644 --- a/src/main/claude/claude-structured-session-test-support.ts +++ b/src/main/claude/claude-structured-session-test-support.ts @@ -2,6 +2,7 @@ import type { AgentJournalMessageItem, AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types' +import { ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV } from '../../shared/agent-session-caller-env' import type { ClaudeStreamJsonConnection, ClaudeStreamJsonConnectionHandlers, @@ -223,10 +224,7 @@ export function fakeClaude( const replayUuid = configuredReplayUuid === undefined ? `user-uuid-${replayIndex}` : configuredReplayUuid if (replayUuid !== null) { - handlers.onMessage?.({ - ...message, - uuid: replayUuid - }) + handlers.onMessage?.({ ...message, uuid: replayUuid }) } } }, @@ -269,7 +267,7 @@ export function adapterAtPublishFor( onDispatchSettledLate?: ClaudeStructuredSessionAdapterDeps['onDispatchSettledLate'] ): ClaudeStructuredSessionAdapter { return new ClaudeStructuredSessionAdapter({ - resolveLaunch: async () => ({ + resolveLaunch: async ({ spawnToken }) => ({ pathToClaudeCodeExecutable: 'claude', options: {}, cwd: '/work/repo', @@ -278,7 +276,9 @@ export function adapterAtPublishFor( resumeLeafUuid: null, resumesTranscript: false, continuesChain: false, - ...launch + ...launch, + // As the composed launch seals it: the reservation's token is the child's, whatever came before. + env: { ...launch.env, [ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV]: spawnToken } }), onEvent: (event) => events.push(event), openConnection: claude.openConnection, diff --git a/src/main/claude/claude-structured-turn-resume-point.test.ts b/src/main/claude/claude-structured-turn-resume-point.test.ts index f24757cd6ab..5fdbe7f0291 100644 --- a/src/main/claude/claude-structured-turn-resume-point.test.ts +++ b/src/main/claude/claude-structured-turn-resume-point.test.ts @@ -1,3 +1,4 @@ +import { createClaudeStructuredLaunchResolver } from '../runtime/structured-agent-launch-composition.test-support' import { afterEach, describe, expect, it, vi } from 'vitest' import { agentSessionLeaseFixture, @@ -7,7 +8,6 @@ import type { AgentSessionRecord } from '../../shared/agent-session-record' import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' import type { AgentSessionRecordStore } from '../runtime/agent-session-record-store' import { reviseAgentSessionProviderResumePoint } from '../runtime/agent-session-provider-handle-transition' -import { createClaudeStructuredLaunchResolver } from './claude-structured-launch-resolution' import { ClaudeStructuredSessionAdapter, type ClaudeStructuredSessionAdapterDeps, diff --git a/src/main/codex/codex-app-server-teardown.integration.test.ts b/src/main/codex/codex-app-server-teardown.integration.test.ts index c2b09c66444..6b7e1b23ccf 100644 --- a/src/main/codex/codex-app-server-teardown.integration.test.ts +++ b/src/main/codex/codex-app-server-teardown.integration.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { CODEX_SPAWN_TOKEN_ENV } from './codex-structured-owner-identity' +import { ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV } from '../../shared/agent-session-caller-env' import { openCodexAppServerConnection, type CodexAppServerConnection @@ -57,7 +57,7 @@ async function openServer( command: process.execPath, args: ['-e', FORCE_KILL_APP_SERVER], env: { - [CODEX_SPAWN_TOKEN_ENV]: `teardown-test-${process.pid}-${iteration}`, + [ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV]: `teardown-test-${process.pid}-${iteration}`, ...(exitMode ? { ORCA_TEST_PROVIDER_EXIT_MODE: exitMode } : {}) } }, diff --git a/src/main/codex/codex-model-catalog-probe.ts b/src/main/codex/codex-model-catalog-probe.ts index 8605f14a33c..77da423188a 100644 --- a/src/main/codex/codex-model-catalog-probe.ts +++ b/src/main/codex/codex-model-catalog-probe.ts @@ -6,7 +6,7 @@ import { runCodexAppServerSession } from './codex-app-server-session' import { fetchCodexModelCatalogListing } from './codex-structured-model-catalog' import { resolveCodexStructuredInvocation, - type CodexStructuredLaunchResolverDeps + type CodexStructuredInvocationDeps } from './codex-structured-launch-resolution' import { requireLegacyAgentSessionAccountHome } from '../../shared/agent-session-account-home' import { AGENT_MODEL_CATALOG_PROBE_TIMEOUT_MS } from '../native-chat/agent-model-catalog/agent-model-catalog-probe-runner' @@ -15,10 +15,7 @@ import type { AgentModelCatalogSuccess } from '../native-chat/agent-model-catalog/agent-model-catalog-store' -export type CodexModelCatalogProbeDeps = Pick< - CodexStructuredLaunchResolverDeps, - 'resolveCommand' | 'resolveEnvironment' -> & { +export type CodexModelCatalogProbeDeps = CodexStructuredInvocationDeps & { resolveAccountKind?: (home: string) => AgentSessionAccountKind | undefined /** The sync a launch runs on this home first, so the probe reads the login a launch would. */ prepareHome?: (home: string) => void diff --git a/src/main/codex/codex-structured-child-environment.test.ts b/src/main/codex/codex-structured-child-environment.test.ts index 517f9effb90..e92742c26ce 100644 --- a/src/main/codex/codex-structured-child-environment.test.ts +++ b/src/main/codex/codex-structured-child-environment.test.ts @@ -1,7 +1,10 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { openCodexAppServerConnection } from './codex-app-server-connection' -import { CODEX_SPAWN_TOKEN_ENV } from './codex-structured-owner-identity' -import { buildCodexStructuredChildEnvironment } from './codex-structured-child-environment' +import { ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV } from '../../shared/agent-session-caller-env' +import { sealStructuredSessionChild } from '../runtime/structured-session-child-env' +import { createCodexStructuredLaunchResolver } from '../runtime/structured-agent-launch-composition.test-support' +import { agentSessionRecordFixture } from '../native-chat/agent-session-record-test-fixture' +import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types' import { mintStructuredWorkerHandle, mintStructuredWorkerPaneKey, @@ -14,29 +17,53 @@ const DEV_CLI_BIN_DIR = /^[^:;]*[\\/]cli[\\/]bin$/ // The dev launcher by absolute path: a login shell's profile cannot reorder it behind a global. const DEV_CLI_LAUNCHER = /^[^:;]*[\\/]cli[\\/]bin[\\/]orca-dev$/ -describe('buildCodexStructuredChildEnvironment', () => { - it('keeps shell exports while pinned launch values win', () => { +/** The sealed env a Codex launch of `sessionId` spawns with, over a shell env of `env`. */ +async function codexChildEnv( + env: Record, + sessionId: string +): Promise> { + const record = agentSessionRecordFixture({ + sessionId, + provider: 'codex', + accountHome: { variable: 'CODEX_HOME', path: '/pinned/home' } + }) + const launch = await createCodexStructuredLaunchResolver({ + store: { getRecord: () => record, pinLaunchDirectory: vi.fn() }, + resolveEnvironment: async () => env, + resolveCommand: () => 'codex', + resolveLaunchArgs: () => [] + })({ + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a launch reads only the session id of its identity here. + identity: { sessionId } as AgentSessionJournalIdentity, + spawnToken: 'spawn-token' + }) + return launch.env +} + +function sealed(sessionId: string): Record { + return sealStructuredSessionChild({ + sessionId, + spawnToken: 'spawn-token', + env: {}, + inheritedEnvToDelete: [] + }).env +} + +describe('the Codex child environment', () => { + it('keeps shell exports while pinned launch values win', async () => { expect( - buildCodexStructuredChildEnvironment( + await codexChildEnv( { - command: 'codex', - args: ['app-server'], - cwd: '/worktree', - codexHome: '/pinned/home', - resumeThreadId: null, - env: { - EXAMPLE_GATEWAY_TOKEN: 'shell-exported', - CODEX_HOME: '/shell/home', - ORCA_CLI_BIN_DIR: '/inherited/unowned-cli' - } + EXAMPLE_GATEWAY_TOKEN: 'shell-exported', + CODEX_HOME: '/shell/home', + ORCA_CLI_BIN_DIR: '/inherited/unowned-cli' }, - 'spawn-token', 'session-not-a-worker' ) ).toEqual({ EXAMPLE_GATEWAY_TOKEN: 'shell-exported', CODEX_HOME: '/pinned/home', - [CODEX_SPAWN_TOKEN_ENV]: 'spawn-token', + [ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV]: 'spawn-token', ORCA_AGENT_SESSION_ID: 'session-not-a-worker', ORCA_STRUCTURED_SESSION: '1', ORCA_CLI_COMMAND: expect.stringMatching(DEV_CLI_LAUNCHER), @@ -50,17 +77,9 @@ describe('buildCodexStructuredChildEnvironment', () => { }) it('names every session by its id, and adds the handle only for a registered worker', () => { - const launch = { - command: 'codex', - args: ['app-server'], - cwd: '/worktree', - codexHome: null, - resumeThreadId: null, - env: {} - } const sessionId = 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d' - expect(buildCodexStructuredChildEnvironment(launch, 'spawn-token', sessionId)).toEqual({ - [CODEX_SPAWN_TOKEN_ENV]: 'spawn-token', + expect(sealed(sessionId)).toEqual({ + [ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV]: 'spawn-token', // Not a worker, so no handle: the id alone names this chat as a caller. ORCA_AGENT_SESSION_ID: sessionId, ORCA_STRUCTURED_SESSION: '1', @@ -83,7 +102,7 @@ describe('buildCodexStructuredChildEnvironment', () => { hostScope: { kind: 'local', hostId: 'local' } }) try { - const env = buildCodexStructuredChildEnvironment(launch, 'spawn-token', sessionId) + const env = sealed(sessionId) expect(env.ORCA_TERMINAL_HANDLE).toBe(handle) expect(env.ORCA_AGENT_SESSION_ID).toBe(sessionId) expect(env.ORCA_CLI_COMMAND).toMatch(DEV_CLI_LAUNCHER) @@ -126,18 +145,7 @@ describe('the spawned Codex child', () => { // survives the merge — an Orca launched inside another session inherits that session's id. vi.stubEnv('ORCA_AGENT_SESSION_ID', 'a0b1c2d3-0000-4000-8000-00000000abcd') const sessionId = 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d' - const env = buildCodexStructuredChildEnvironment( - { - command: process.execPath, - args: ['-e', ENV_REPORTING_APP_SERVER], - cwd: process.cwd(), - codexHome: null, - resumeThreadId: null, - env: {} - }, - 'spawn-token', - sessionId - ) + const env = sealed(sessionId) const connection = await openCodexAppServerConnection({ command: process.execPath, args: ['-e', ENV_REPORTING_APP_SERVER], diff --git a/src/main/codex/codex-structured-child-environment.ts b/src/main/codex/codex-structured-child-environment.ts deleted file mode 100644 index c34e0b3a3c8..00000000000 --- a/src/main/codex/codex-structured-child-environment.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { CodexStructuredLaunch } from './codex-structured-session-state' -import { CODEX_SPAWN_TOKEN_ENV } from './codex-structured-owner-identity' -import { structuredSessionChildIdentityEnv } from '../runtime/structured-session-child-identity-env' -import { - NATIVE_CHAT_VISUALS_DIR_ENV, - withNativeChatVisualsEnv -} from '../native-chat/native-chat-visuals-delivery' - -export function buildCodexStructuredChildEnvironment( - launch: CodexStructuredLaunch, - spawnToken: string, - sessionId: string -): Record { - return { - // Every structured session speaks orchestration as itself: its injected id and the Orca CLI. - ...structuredSessionChildIdentityEnv( - sessionId, - withNativeChatVisualsEnv( - { - ...launch.env, - ...(launch.codexHome ? { CODEX_HOME: launch.codexHome } : {}) - }, - launch.visuals ?? null - ) - ), - [CODEX_SPAWN_TOKEN_ENV]: spawnToken - } -} - -/** The child's env overlay, and the keys removed from what it inherits: without visuals, a folder - * Orca itself inherited (started from a chat) names another chat's folder. */ -export function codexStructuredChildEnvironment( - launch: CodexStructuredLaunch, - spawnToken: string, - sessionId: string -): { env: Record; envToDelete?: readonly string[] } { - return { - env: buildCodexStructuredChildEnvironment(launch, spawnToken, sessionId), - ...(launch.visuals ? {} : { envToDelete: [NATIVE_CHAT_VISUALS_DIR_ENV] }) - } -} diff --git a/src/main/codex/codex-structured-launch-resolution.test.ts b/src/main/codex/codex-structured-launch-resolution.test.ts index 817e98defe7..53b35f69b97 100644 --- a/src/main/codex/codex-structured-launch-resolution.test.ts +++ b/src/main/codex/codex-structured-launch-resolution.test.ts @@ -1,3 +1,4 @@ +import { createCodexStructuredLaunchResolver } from '../runtime/structured-agent-launch-composition.test-support' import { mkdtempSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -6,9 +7,9 @@ import { FLOATING_TERMINAL_WORKTREE_ID } from '../../shared/constants' import type { AgentSessionRecord } from '../../shared/agent-session-record' import type { AgentSessionProviderHandleLink } from '../../shared/agent-session-provider-handle' import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' -import { createCodexStructuredLaunchResolver } from './codex-structured-launch-resolution' import { codexStructuredPermissionPolicyForSettings } from './codex-structured-permission-policy' import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding' +import { NATIVE_CHAT_VISUALS_DIR_ENV } from '../native-chat/native-chat-visuals-delivery' const { isWindowsProcessStartTimeAvailable } = vi.hoisted(() => ({ isWindowsProcessStartTimeAvailable: vi.fn(() => true) @@ -165,7 +166,9 @@ describe('codex structured launch resolution', () => { codexHome: '/home/work/.codex', resumeThreadId: null, // Every launch now carries a posture; neither one is left for config.toml to decide. - permissionPolicy: { approvalPolicy: 'on-request', sandbox: 'workspace-write' } + permissionPolicy: { approvalPolicy: 'on-request', sandbox: 'workspace-write' }, + env: { CODEX_HOME: '/home/work/.codex' }, + envToDelete: [NATIVE_CHAT_VISUALS_DIR_ENV] }) }) diff --git a/src/main/codex/codex-structured-launch-resolution.ts b/src/main/codex/codex-structured-launch-resolution.ts index 86b3f222773..ecf312233fb 100644 --- a/src/main/codex/codex-structured-launch-resolution.ts +++ b/src/main/codex/codex-structured-launch-resolution.ts @@ -6,36 +6,35 @@ // must name the thread this session actually proved — never one a caller asks // for, which is how a resume becomes a fork wearing a resume's name. -import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types' import { requireLegacyAgentSessionAccountHome } from '../../shared/agent-session-account-home' import { agentSessionProviderHandleChainHead } from '../../shared/agent-session-provider-handle' -import { isNativeLocalLocation } from '../../shared/execution-host' import { resolveCodexCommand } from '../codex-cli/command' -import type { AgentSessionRecordStore } from '../runtime/agent-session-record-store' -import { resolveAgentSessionLaunchDirectory } from '../runtime/agent-session-launch-directory' +import type { StructuredAgentLaunchPartResolver } from '../runtime/structured-agent-launch-composition' import type { CodexStructuredLaunch } from './codex-structured-session-adapter' import type { CodexStructuredPermissionPolicy } from './codex-structured-permission-policy' import { resolvePinnedCodexRolloutProof } from './codex-pinned-rollout-proof' import { codexStructuredLaunchArgs } from './codex-structured-launch-args' -import { CODEX_STRUCTURED_AGENT } from './codex-structured-agent-definition' -import type { PrepareNativeChatVisuals } from '../native-chat/native-chat-visuals-delivery' +import { + NATIVE_CHAT_VISUALS_DIR_ENV, + withNativeChatVisualsEnv +} from '../native-chat/native-chat-visuals-delivery' -export type CodexStructuredLaunchResolverDeps = { - store: Pick +export type CodexStructuredLaunchPartDeps = { resolveLaunchArgs: () => Promise | string[] - /** Absolute path of a workspace on this host. Rejects when the workspace no - * longer resolves, which is the case a stale mobile client hits. */ - resolveWorkspacePath: (workspaceId: string) => Promise /** Overridden in tests; production scans the boot-cached PATH and version-manager dirs. */ resolveCommand?: (options?: { pathEnv?: string | null; homePath?: string }) => string - /** Fresh shell/configured environment for this spawn; never written to the session record. */ - resolveEnvironment?: () => Promise resolveRollout?: typeof resolvePinnedCodexRolloutProof /** The user's Agent Permissions setting as thread policy, re-read per acquisition. * States both postures outright — a resume inherits the last one for any field left absent. */ resolvePermissionPolicy?: () => CodexStructuredPermissionPolicy - /** This chat's visuals folder and skill; absent or null ⇒ the chat gets neither. */ - prepareVisuals?: PrepareNativeChatVisuals +} + +export type CodexStructuredInvocationDeps = Pick< + CodexStructuredLaunchPartDeps, + 'resolveCommand' +> & { + /** Fresh shell/configured environment for this spawn; never written to the session record. */ + resolveEnvironment?: () => Promise } export type CodexStructuredInvocation = { @@ -51,7 +50,7 @@ export type CodexStructuredInvocation = { * drift there heals on the next refresh. */ export async function resolveCodexStructuredInvocation( - deps: Pick + deps: CodexStructuredInvocationDeps ): Promise { const environment = await deps.resolveEnvironment?.() const pathEnv = environment?.PATH ?? environment?.Path ?? null @@ -63,63 +62,57 @@ export async function resolveCodexStructuredInvocation( return { command, environment } } -export function createCodexStructuredLaunchResolver( - deps: CodexStructuredLaunchResolverDeps -): (input: { identity: AgentSessionJournalIdentity }) => Promise { - return async ({ identity }) => { - const record = deps.store.getRecord(identity.sessionId) - if (!record) { - throw new Error(`no durable agent-session record for ${identity.sessionId}`) - } - const { location } = record +/** Codex's part of a launch: its binary, thread arguments, account home, resume proof and visuals. */ +export function codexStructuredLaunchPart( + deps: CodexStructuredLaunchPartDeps +): StructuredAgentLaunchPartResolver { + return async (basis) => { + const { record } = basis const accountHome = requireLegacyAgentSessionAccountHome(record.accountHome) - if (record.provider !== 'codex') { - throw new Error(`session ${identity.sessionId} is a ${record.provider} session`) - } - // This adapter spawns a child on the machine the runtime itself runs on. - // A session pinned elsewhere belongs to that host's runtime, and quietly - // starting it here would put a second writer on the same thread. - if (!isNativeLocalLocation(location)) { - throw new Error( - `codex structured sessions run on the local host, not ${location.executionHostId}` - ) - } - const pinned = CODEX_STRUCTURED_AGENT.accountHomeVariable - if (accountHome.variable !== pinned) { - throw new Error(`codex sessions pin ${pinned}, not ${accountHome.variable}`) - } - const { command, environment } = await resolveCodexStructuredInvocation(deps) + const { command } = await resolveCodexStructuredInvocation({ + ...(deps.resolveCommand ? { resolveCommand: deps.resolveCommand } : {}), + resolveEnvironment: basis.environment + }) + const environment = await basis.environment() const args = codexStructuredLaunchArgs(await deps.resolveLaunchArgs()) const permissionPolicy = deps.resolvePermissionPolicy?.() const head = agentSessionProviderHandleChainHead(record.providerHandleChain) - // A Codex record's chain holds only Codex handles; the attach admission refuses anything else. + // A Codex record's chain holds only Codex handles; the launch admission refuses anything else. const resumeThreadId = head?.handle.nativeId ?? null // The same saved options every turn sends, so the thread and its turns name one model. const model = record.options?.model - const visuals = (await deps.prepareVisuals?.(record.sessionId)) ?? null + const visuals = await basis.visuals() + // Before the rollout scan, so a missing floating folder is still the refusal that wins. + await basis.launchDirectory() return { - command, - args: [...args, 'app-server'], - cwd: await resolveAgentSessionLaunchDirectory(deps, record), - codexHome: accountHome.path, - ...(environment ? { env: { ...environment } as Record } : {}), - // An empty chain is a session that has never proved a thread, so it - // starts one; anything else resumes the last link this session proved. - resumeThreadId, - // Only a thread this session created may still be one Codex never saved: a resumed, - // forked or adopted head names a conversation Codex held. - ...(resumeThreadId && head?.origin === 'created' ? { supersedeIfUnsaved: true } : {}), - ...(permissionPolicy ? { permissionPolicy } : {}), - ...(model ? { model } : {}), - ...(visuals ? { visuals } : {}), - ...(resumeThreadId - ? { - resumePath: await (deps.resolveRollout ?? resolvePinnedCodexRolloutProof)( - accountHome.path, - resumeThreadId - ) - } - : {}) + launch: { + command, + args: [...args, 'app-server'], + codexHome: accountHome.path, + // An empty chain is a session that has never proved a thread, so it + // starts one; anything else resumes the last link this session proved. + resumeThreadId, + // Only a thread this session created may still be one Codex never saved: a resumed, + // forked or adopted head names a conversation Codex held. + ...(resumeThreadId && head?.origin === 'created' ? { supersedeIfUnsaved: true } : {}), + ...(permissionPolicy ? { permissionPolicy } : {}), + ...(model ? { model } : {}), + ...(visuals ? { visuals } : {}), + ...(resumeThreadId + ? { + resumePath: await (deps.resolveRollout ?? resolvePinnedCodexRolloutProof)( + accountHome.path, + resumeThreadId + ) + } + : {}) + }, + env: withNativeChatVisualsEnv( + { ...environment, ...(accountHome.path ? { CODEX_HOME: accountHome.path } : {}) }, + visuals + ), + // Without visuals, a folder Orca itself inherited (started from a chat) names another chat's. + inheritedEnvToDelete: visuals ? [] : [NATIVE_CHAT_VISUALS_DIR_ENV] } } } diff --git a/src/main/codex/codex-structured-owner-identity.ts b/src/main/codex/codex-structured-owner-identity.ts index 00dd0bfb61f..40afec21cec 100644 --- a/src/main/codex/codex-structured-owner-identity.ts +++ b/src/main/codex/codex-structured-owner-identity.ts @@ -12,10 +12,6 @@ import { readProcessStartTimeMs } from '../runtime/agent-session-process-identit // must describe the thread Codex actually opened, never the one a client asked // for. -/** The child echoes its spawn token here so the owner probe can tell a live - * child of THIS reservation from a same-pid stranger. */ -export const CODEX_SPAWN_TOKEN_ENV = 'ORCA_AGENT_SESSION_SPAWN_TOKEN' - const START_TIME_READ_ATTEMPTS = 3 /** diff --git a/src/main/codex/codex-structured-session-acquire.ts b/src/main/codex/codex-structured-session-acquire.ts index 21eed4e0c4c..3b0d3b2e1af 100644 --- a/src/main/codex/codex-structured-session-acquire.ts +++ b/src/main/codex/codex-structured-session-acquire.ts @@ -22,7 +22,6 @@ import { codexProviderHandleLink, codexSpawnedProcessIdentity } from './codex-structured-owner-identity' -import { codexStructuredChildEnvironment } from './codex-structured-child-environment' import { openCodexThread } from './codex-structured-thread-open' import { withCodexVisualsThreadConfig } from './codex-structured-visuals' import { @@ -115,7 +114,7 @@ export async function acquireCodexStructuredSession(input: { } acquisitions.assertCurrent(sessionId, attempt) const launch = await deps - .resolveLaunch({ identity: acquireInput.identity }) + .resolveLaunch({ identity: acquireInput.identity, spawnToken: acquireInput.spawnToken }) .catch((error: unknown) => { throw new AgentSessionPreSpawnError(error) }) @@ -126,7 +125,8 @@ export async function acquireCodexStructuredSession(input: { command: launch.command, args: launch.args, cwd: launch.cwd, - ...codexStructuredChildEnvironment(launch, acquireInput.spawnToken, sessionId) + ...(launch.env ? { env: launch.env } : {}), + ...(launch.envToDelete ? { envToDelete: launch.envToDelete } : {}) }, { onNotification: codexAcquisitionNotificationHandler({ diff --git a/src/main/codex/codex-structured-session-adapter.test.ts b/src/main/codex/codex-structured-session-adapter.test.ts index 26991e59689..2fcd0a41f6b 100644 --- a/src/main/codex/codex-structured-session-adapter.test.ts +++ b/src/main/codex/codex-structured-session-adapter.test.ts @@ -4,7 +4,7 @@ import { type openCodexAppServerConnection } from './codex-app-server-connection' import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink' -import { CODEX_SPAWN_TOKEN_ENV } from './codex-structured-owner-identity' +import { ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV } from '../../shared/agent-session-caller-env' import { CodexStructuredSessionAdapter, type CodexStructuredLaunch, @@ -20,6 +20,8 @@ import { identityFor } from './codex-structured-session-adapter-fixture' import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding' +import { createCodexStructuredLaunchResolver } from '../runtime/structured-agent-launch-composition.test-support' +import { agentSessionRecordFixture } from '../native-chat/agent-session-record-test-fixture' import { AgentModelCatalogStore } from '../native-chat/agent-model-catalog/agent-model-catalog-store' describe('CodexStructuredSessionAdapter.acquire', () => { @@ -124,7 +126,20 @@ describe('CodexStructuredSessionAdapter.acquire', () => { it('starts a new thread and reports the process and link the lease will prove', async () => { const codex = fakeCodex() - const adapter = adapterFor(codex, { codexHome: '/codex/home' }) + const record = agentSessionRecordFixture({ + sessionId: 'session-1', + provider: 'codex', + accountHome: { variable: 'CODEX_HOME', path: '/codex/home' } + }) + // The launch its registration composes: the shared seal names the child, not this adapter. + const adapter = adapterFor(codex, {}, [], { + resolveLaunch: createCodexStructuredLaunchResolver({ + store: { getRecord: () => record, pinLaunchDirectory: vi.fn() }, + resolveWorkspacePath: async () => '/work/repo', + resolveCommand: () => 'codex', + resolveLaunchArgs: () => [] + }) + }) const acquisition = await adapter.acquire({ identity: identityFor('session-1'), @@ -133,7 +148,7 @@ describe('CodexStructuredSessionAdapter.acquire', () => { }) expect(codex.connections[0].launch.env).toEqual({ - [CODEX_SPAWN_TOKEN_ENV]: 'spawn-9', + [ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV]: 'spawn-9', CODEX_HOME: '/codex/home', ORCA_AGENT_SESSION_ID: 'session-1', ORCA_STRUCTURED_SESSION: '1', diff --git a/src/main/codex/codex-structured-session-state.ts b/src/main/codex/codex-structured-session-state.ts index f319ff63633..70536db781d 100644 --- a/src/main/codex/codex-structured-session-state.ts +++ b/src/main/codex/codex-structured-session-state.ts @@ -44,7 +44,9 @@ export type CodexStructuredLaunch = { permissionPolicy?: CodexStructuredPermissionPolicy /** The model the session chose; the thread opens on it so its first turn is not a switch. */ model?: string + /** The child's sealed environment, and the inherited keys it must not keep. */ env?: Record + envToDelete?: readonly string[] /** This chat's visuals folder and skill; absent when the chat has no visuals. */ visuals?: NativeChatVisualsLaunch } @@ -90,6 +92,7 @@ export type CodexStructuredSessionAdapterDeps = { resolveAccountKind?: (home: string) => AgentSessionAccountKind | undefined resolveLaunch: (input: { identity: AgentSessionJournalIdentity + spawnToken: string }) => Promise onEvent?: (event: CodexStructuredSessionEvent) => void /** Where bookkeeping a close or exit does after the child is gone reports a failure. */ diff --git a/src/main/codex/codex-structured-visuals.test.ts b/src/main/codex/codex-structured-visuals.test.ts index 93430c3d6af..9be5ae455b3 100644 --- a/src/main/codex/codex-structured-visuals.test.ts +++ b/src/main/codex/codex-structured-visuals.test.ts @@ -5,7 +5,15 @@ import { } from '../native-chat/native-chat-visuals-delivery' import { CodexAppServerRequestError } from './codex-app-server-request-error' import { CodexAppServerTimeoutError } from './codex-app-server-session' -import { acquired, fakeCodex, THREAD_ID } from './codex-structured-session-adapter-fixture' +import { + adapterFor, + fakeCodex, + identityFor, + THREAD_ID +} from './codex-structured-session-adapter-fixture' +import { createCodexStructuredLaunchResolver } from '../runtime/structured-agent-launch-composition.test-support' +import { agentSessionRecordFixture } from '../native-chat/agent-session-record-test-fixture' +import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding' import { CODEX_VISUALS_SETUP_BUDGET_MS, prepareCodexThreadForVisuals @@ -147,11 +155,51 @@ describe('setting up a Codex app-server for a chat with visuals', () => { }) }) +/** A chat acquired through the launch its registration composes, over a shell env of `env`. */ +async function acquired( + codex: ReturnType, + launch: { + visuals?: NativeChatVisualsLaunch + resumeThreadId?: string | null + env?: Record + } +): Promise { + const record = agentSessionRecordFixture({ + sessionId: 'session-1', + provider: 'codex', + accountHome: { variable: 'CODEX_HOME', path: '/home/work/.codex' }, + providerHandleChain: launch.resumeThreadId + ? [ + { + linkId: 'link-1', + handle: codexProviderHandle(launch.resumeThreadId), + origin: 'resumed', + mintedAtFence: 1, + observedAt: 1 + } + ] + : [] + }) + const adapter = adapterFor(codex, {}, [], { + resolveLaunch: createCodexStructuredLaunchResolver({ + store: { getRecord: () => record, pinLaunchDirectory: vi.fn() }, + resolveWorkspacePath: async () => '/work/repo', + resolveEnvironment: async () => launch.env ?? {}, + resolveCommand: () => 'codex', + resolveLaunchArgs: () => [], + resolveRollout: async () => null, + resolvePermissionPolicy: () => MANUAL, + prepareVisuals: async () => launch.visuals ?? null + }) + }) + await adapter.acquire({ identity: identityFor('session-1'), fence: 7, spawnToken: 'spawn-9' }) +} + describe('a Codex chat acquisition with visuals', () => { it('sets up skills and the folder after initialize and before the thread opens, on start and resume', async () => { for (const resumeThreadId of [null, THREAD_ID]) { const codex = fakeCodex({ 'config/read': configWithRoots(['/home/me/scratch']) }) - await acquired(codex, { visuals: VISUALS, permissionPolicy: MANUAL, resumeThreadId }) + await acquired(codex, { visuals: VISUALS, resumeThreadId }) const connection = codex.connections[0]! const methods = connection.calls.map((call) => call.method) const open = resumeThreadId ? 'thread/resume' : 'thread/start' @@ -167,10 +215,7 @@ describe('a Codex chat acquisition with visuals', () => { it('opens the thread exactly as before for a chat without visuals', async () => { const codex = fakeCodex() - await acquired(codex, { - permissionPolicy: MANUAL, - env: { [NATIVE_CHAT_VISUALS_DIR_ENV]: '/other' } - }) + await acquired(codex, { env: { [NATIVE_CHAT_VISUALS_DIR_ENV]: '/other' } }) const connection = codex.connections[0]! const methods = connection.calls.map((call) => call.method) expect(methods[0]).toBe('thread/start') diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-recovery-exits.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-recovery-exits.test.ts index c657daa574f..ff3967c3634 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-recovery-exits.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-recovery-exits.test.ts @@ -5,7 +5,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest' import { spawnProcess } from '@orca/process-host' -import { CODEX_SPAWN_TOKEN_ENV } from '../../codex/codex-structured-owner-identity' +import { ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV } from '../../../shared/agent-session-caller-env' import type { AgentSessionRecordStore } from '../../runtime/agent-session-record-store' import { openTestAgentSessionRecordStore } from '../../runtime/agent-session-record-store-test-harness' import { readProcessStartTimeMs } from '../../runtime/agent-session-process-identity-probe' @@ -39,7 +39,7 @@ async function spawnOwner(spawnToken: string) { const child = spawnProcess({ program: process.execPath, args: ['-e', 'setInterval(() => {}, 1_000)'], - env: { ...process.env, [CODEX_SPAWN_TOKEN_ENV]: spawnToken } + env: { ...process.env, [ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV]: spawnToken } }) spawnedOwners.add(child) const pid = child.pid diff --git a/src/main/native-chat/native-chat-inline-visuals-setting.test.ts b/src/main/native-chat/native-chat-inline-visuals-setting.test.ts index 9b2d56aa49d..e3cdffb56d6 100644 --- a/src/main/native-chat/native-chat-inline-visuals-setting.test.ts +++ b/src/main/native-chat/native-chat-inline-visuals-setting.test.ts @@ -1,11 +1,13 @@ +import { + createClaudeStructuredLaunchResolver, + createCodexStructuredLaunchResolver +} from '../runtime/structured-agent-launch-composition.test-support' import { existsSync, mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' -import { createClaudeStructuredLaunchResolver } from '../claude/claude-structured-launch-resolution' import { CLAUDE_PLUGIN_DIR_FLAG, type ClaudeCliFlag } from '../claude/claude-cli-flag-support' -import { createCodexStructuredLaunchResolver } from '../codex/codex-structured-launch-resolution' import { adapterFor, fakeCodex, diff --git a/src/main/native-chat/native-chat-visuals-delivery.test.ts b/src/main/native-chat/native-chat-visuals-delivery.test.ts index 72faec6dfdb..331e27eb878 100644 --- a/src/main/native-chat/native-chat-visuals-delivery.test.ts +++ b/src/main/native-chat/native-chat-visuals-delivery.test.ts @@ -16,7 +16,9 @@ import { } from './native-chat-visuals-delivery' import { nativeChatVisualsFolderFor, nativeChatVisualsRootFor } from './native-chat-visuals-folder' import { buildClaudeChildProcessEnv } from '../claude/claude-child-process-environment' -import { codexStructuredChildEnvironment } from '../codex/codex-structured-child-environment' +import { createCodexStructuredLaunchResolver } from '../runtime/structured-agent-launch-composition.test-support' +import { agentSessionRecordFixture } from './agent-session-record-test-fixture' +import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types' import { resolveProviderChildEnv } from '../provider-process/provider-process-launch' import { NATIVE_CHAT_VISUALS_SKILL_NAME, @@ -116,7 +118,7 @@ describe('preparing a chat for visuals', () => { ) }) - it("never hands a chat another chat's folder that Orca itself inherited", () => { + it("never hands a chat another chat's folder that Orca itself inherited", async () => { const inherited = { PATH: '/bin', [NATIVE_CHAT_VISUALS_DIR_ENV]: '/parent-chat' } const visuals = { folder: '/mine', skill: SKILL } for (const platform of ['darwin', 'win32'] as const) { @@ -127,24 +129,27 @@ describe('preparing a chat for visuals', () => { '/mine' ) } - const codex = (withVisuals: boolean) => + const record = agentSessionRecordFixture({ + sessionId: 'chat-1', + provider: 'codex', + accountHome: { variable: 'CODEX_HOME', path: '/home/work/.codex' } + }) + const codex = async (withVisuals: boolean) => resolveProviderChildEnv( - codexStructuredChildEnvironment( - { - command: 'codex', - args: ['app-server'], - cwd: '/w', - codexHome: null, - resumeThreadId: null, - ...(withVisuals ? { visuals } : {}) - }, - 'spawn-token', - 'chat-1' - ), + await createCodexStructuredLaunchResolver({ + store: { getRecord: () => record, pinLaunchDirectory: vi.fn() }, + resolveCommand: () => 'codex', + resolveLaunchArgs: () => [], + prepareVisuals: async () => (withVisuals ? visuals : null) + })({ + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a launch reads only the session id of its identity here. + identity: { sessionId: 'chat-1' } as AgentSessionJournalIdentity, + spawnToken: 'spawn-token' + }), inherited ) - expect(codex(false)).not.toHaveProperty(NATIVE_CHAT_VISUALS_DIR_ENV) - expect(codex(true)[NATIVE_CHAT_VISUALS_DIR_ENV]).toBe('/mine') + expect(await codex(false)).not.toHaveProperty(NATIVE_CHAT_VISUALS_DIR_ENV) + expect((await codex(true))[NATIVE_CHAT_VISUALS_DIR_ENV]).toBe('/mine') }) it('names only this chat folder to the agent', () => { diff --git a/src/main/pi/rpc-launch-resolution.test.ts b/src/main/pi/rpc-launch-resolution.test.ts index fd594886053..8c9a28ce844 100644 --- a/src/main/pi/rpc-launch-resolution.test.ts +++ b/src/main/pi/rpc-launch-resolution.test.ts @@ -1,3 +1,4 @@ +import { createPiRpcLaunchResolver } from '../runtime/structured-agent-launch-composition.test-support' import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -9,7 +10,7 @@ import { agentSessionProviderHandleKey } from '../../shared/agent-session-provid import { closeTestJournalHostDatabase } from '../native-chat/agent-session-journal/journal-host-database-test-support' import { openTestAgentSessionRecordStore } from '../runtime/agent-session-record-store-test-harness' import { buildPiRpcLaunch } from './rpc-launch' -import { createPiRpcLaunchResolver, piRpcProviderLink } from './rpc-launch-resolution' +import { piRpcProviderLink } from './rpc-launch-resolution' const probeVersion = vi.fn(async (..._input: unknown[]) => true) @@ -237,7 +238,7 @@ describe('Pi host launch resolution', () => { ...record, location: { ...record.location, executionHostId: 'ssh:remote' } }) - await expect(h.resolver(identity)).rejects.toThrow('another execution host') + await expect(h.resolver(identity)).rejects.toThrow('run on the local host') vi.spyOn(h.store, 'getRecord').mockReturnValue({ ...record, accountHome: { variable: 'CODEX_HOME', path: '/wrong' } diff --git a/src/main/pi/rpc-launch-resolution.ts b/src/main/pi/rpc-launch-resolution.ts index c783eb91f24..d1abf1af1ea 100644 --- a/src/main/pi/rpc-launch-resolution.ts +++ b/src/main/pi/rpc-launch-resolution.ts @@ -7,30 +7,26 @@ import { agentSessionProviderHandleKey, type AgentSessionProviderHandleLink } from '../../shared/agent-session-provider-handle' -import { isLegacyAgentSessionAccountHome } from '../../shared/agent-session-account-home' -import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types' -import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host' +import { requireLegacyAgentSessionAccountHome } from '../../shared/agent-session-account-home' import type { resolveCliCommand } from '../../shared/node-cli-command-resolution' import { isStableCliVersionOnLine, probeAgentCliVersion } from '../agent-cli-version-probe' import { resolveStructuredAgentCommand, type StructuredAgentCommandSettings } from '../native-chat/structured-agent-command-resolution' -import type { AgentSessionRecordStore } from '../runtime/agent-session-record-store' -import { resolveAgentSessionLaunchDirectory } from '../runtime/agent-session-launch-directory' +import type { StructuredAgentLaunchPartResolver } from '../runtime/structured-agent-launch-composition' import { isWindowsProcessStartTimeAvailable } from '../windows/windows-process-table' -import type { PiRpcLaunchOptions } from './rpc-launch' +import { + PI_RPC_SESSION_ENV_TO_DELETE, + withoutPiCallerEnv, + type PiRpcLaunchOptions +} from './rpc-launch' export type PiRpcResolvedLaunch = PiRpcLaunchOptions & { previous: AgentSessionProviderHandleLink | null replacement?: 'unsaved' | 'restore-failed' } -export type PiRpcLaunchResolverDeps = { - store: AgentSessionRecordStore - resolveWorkspacePath: (id: string) => Promise - resolveEnvironment: () => Promise - /** The user's Pi Command setting, re-read per acquisition; none runs the stock binary. */ - resolveCommandSettings?: () => StructuredAgentCommandSettings +export type PiRpcLaunchPartDeps = { resolveCommand?: typeof resolveCliCommand probeVersion?: typeof probeAgentCliVersion resolveFullAccess?: () => boolean @@ -102,39 +98,21 @@ async function sameDirectory(left: string, right: string): Promise { return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b } -/** Reads the execution host's durable record; never resolves another host's files or account. */ -export function createPiRpcLaunchResolver( - deps: PiRpcLaunchResolverDeps -): (identity: AgentSessionJournalIdentity) => Promise { - return async (identity) => { - const record = deps.store.getRecord(identity.sessionId) - if (!record || record.provider !== 'pi') { - throw new Error('Pi session record unavailable') - } - if ( - record.location.executionHostId !== LOCAL_EXECUTION_HOST_ID || - record.location.wslDistro !== null - ) { - throw new Error('Pi structured session belongs to another execution host') - } +/** Pi's part of a launch: its account directory, session file or fork, binary and release check. */ +export function piRpcLaunchPart( + deps: PiRpcLaunchPartDeps +): StructuredAgentLaunchPartResolver { + return async (basis) => { + const { record } = basis if (process.platform === 'win32' && !isWindowsProcessStartTimeAvailable()) { throw new Error('Pi structured sessions require Windows process creation-time proof') } - const accountHome = record.accountHome - if ( - !isLegacyAgentSessionAccountHome(accountHome) || - accountHome.variable !== 'PI_CODING_AGENT_DIR' - ) { - throw new Error('Pi account home does not match its binary') + const accountHome = requireLegacyAgentSessionAccountHome(record.accountHome) + const cwd = await basis.launchDirectory() + const env: Record = { + ...(await basis.environment()), + PI_CODING_AGENT_DIR: accountHome.path } - const cwd = await resolveAgentSessionLaunchDirectory(deps, record) - const env: Record = {} - for (const [key, value] of Object.entries(await deps.resolveEnvironment())) { - if (value !== undefined) { - env[key] = value - } - } - env.PI_CODING_AGENT_DIR = accountHome.path const previous = agentSessionProviderHandleChainHead(record.providerHandleChain) let sessionFile: string | undefined let forkFile: string | undefined @@ -157,7 +135,7 @@ export function createPiRpcLaunchResolver( forkFile = source } } - const command = resolvePiRpcCommand(env, deps.resolveCommandSettings?.(), deps.resolveCommand) + const command = resolvePiRpcCommand(env, basis.commandSettings(), deps.resolveCommand) // Again at every launch: the binary on PATH may have changed since the chat was created. if (!(await piRpcVersionSupported({ command, cwd, env }, deps.probeVersion))) { throw agentSessionRefusalError('structured_agent_session_unsupported', { @@ -165,14 +143,16 @@ export function createPiRpcLaunchResolver( }) } return { - command, - cwd, - env, - fullAccess: deps.resolveFullAccess?.() ?? true, - previous, - ...(sessionFile ? { sessionFile } : {}), - ...(forkFile ? { forkFile } : {}), - ...(replacement ? { replacement } : {}) + launch: { + command, + fullAccess: deps.resolveFullAccess?.() ?? true, + previous, + ...(sessionFile ? { sessionFile } : {}), + ...(forkFile ? { forkFile } : {}), + ...(replacement ? { replacement } : {}) + }, + env: withoutPiCallerEnv(env), + inheritedEnvToDelete: PI_RPC_SESSION_ENV_TO_DELETE } } } diff --git a/src/main/pi/rpc-launch.test.ts b/src/main/pi/rpc-launch.test.ts index 678e84dadf3..782a7b12925 100644 --- a/src/main/pi/rpc-launch.test.ts +++ b/src/main/pi/rpc-launch.test.ts @@ -1,6 +1,12 @@ import { describe, expect, it } from 'vitest' import { resolveProviderChildEnv } from '../provider-process/provider-process-launch' -import { buildPiRpcLaunch } from './rpc-launch' +import { sealStructuredSessionChild } from '../runtime/structured-session-child-env' +import { + buildPiRpcLaunch, + PI_RPC_SESSION_ENV_TO_DELETE, + piRpcSessionlessEnvironment, + withoutPiCallerEnv +} from './rpc-launch' const options = { command: 'host-resolved-pi', cwd: '/host/folder', fullAccess: true } describe('Pi RPC launch', () => { @@ -28,11 +34,11 @@ describe('Pi RPC launch', () => { const launch = buildPiRpcLaunch({ ...options, extraArgs: ['--extension', '/host/my extension.ts'], - env: { + ...piRpcSessionlessEnvironment({ PI_CODING_AGENT_DIR: '/host/account', ORCA_PANE_KEY: 'other-pane', ORCA_AGENT_PANE: 'alias' - } + }) }) const env = resolveProviderChildEnv(launch, { ORCA_AGENT_HOOK_TOKEN: 'inherited', @@ -91,12 +97,22 @@ describe('Pi RPC launch', () => { ORCA_TERMINAL_HANDLE: 'structworker_stale', ORCA_AGENT_SESSION_SPAWN_TOKEN: 'old-token' } - expect(resolveProviderChildEnv(buildPiRpcLaunch(options), inherited)).toEqual({}) + expect( + resolveProviderChildEnv( + buildPiRpcLaunch({ ...options, ...piRpcSessionlessEnvironment({}) }), + inherited + ) + ).toEqual({}) + // As a structured launch seals it: no caller of its own env, then this session's identity. const childEnv = resolveProviderChildEnv( buildPiRpcLaunch({ ...options, - env: inherited, - structuredSession: { id: 'this-session', spawnToken: 'this-token' } + ...sealStructuredSessionChild({ + sessionId: 'this-session', + spawnToken: 'this-token', + env: withoutPiCallerEnv(inherited), + inheritedEnvToDelete: PI_RPC_SESSION_ENV_TO_DELETE + }) }), inherited ) diff --git a/src/main/pi/rpc-launch.ts b/src/main/pi/rpc-launch.ts index 98fba1dfb93..b5836287701 100644 --- a/src/main/pi/rpc-launch.ts +++ b/src/main/pi/rpc-launch.ts @@ -1,13 +1,7 @@ -import { - ORCA_SCRUB_SAFE_LAUNCH_ENV, - ORCA_SCRUB_SAFE_PANE_ENV -} from '../../shared/agent-hook-scrub-safe-env' -import { - AGENT_HOOK_RUNTIME_ENV_KEYS, - ORCA_AGENT_SESSION_CALLER_ENV_KEYS -} from '../ipc/pty/host-env/spawn-env-keys' +import { ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV } from '../../shared/agent-session-caller-env' +import { ORCA_AGENT_SESSION_CALLER_ENV_KEYS } from '../ipc/pty/host-env/spawn-env-keys' import type { ProviderProcessLaunch } from '../provider-process/provider-process-launch' -import { structuredSessionChildIdentityEnv } from '../runtime/structured-session-child-identity-env' +import { STRUCTURED_CHILD_ENV_TO_DELETE } from '../runtime/structured-session-child-env' export type PiRpcLaunchOptions = { /** Binary and paths are resolved by the execution host before building the launch. */ @@ -16,26 +10,43 @@ export type PiRpcLaunchOptions = { fullAccess: boolean extraArgs?: readonly string[] env?: Record + envToDelete?: readonly string[] sessionFile?: string forkFile?: string - structuredSession?: { id: string; spawnToken: string } } -const CHILD_ENV_TO_DELETE: readonly string[] = [ - 'ORCA_PANE_KEY', - 'ORCA_TAB_ID', - 'ORCA_WORKTREE_ID', - 'ORCA_AGENT_LAUNCH_TOKEN', - ORCA_SCRUB_SAFE_PANE_ENV, - ORCA_SCRUB_SAFE_LAUNCH_ENV, - ...AGENT_HOOK_RUNTIME_ENV_KEYS -] const CALLER_ENV_TO_DELETE = [ ...ORCA_AGENT_SESSION_CALLER_ENV_KEYS, 'ORCA_TERMINAL_HANDLE', - 'ORCA_AGENT_SESSION_SPAWN_TOKEN' + ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV ] +/** What a structured Pi session strips from its inheritance; the seal keeps a worker's handle. */ +export const PI_RPC_SESSION_ENV_TO_DELETE: readonly string[] = [ + ...STRUCTURED_CHILD_ENV_TO_DELETE, + 'ORCA_TERMINAL_HANDLE' +] + +/** `env` naming no caller: a structured session's seal names its own. */ +export function withoutPiCallerEnv(env: Readonly>): Record { + const next = { ...env } + for (const key of CALLER_ENV_TO_DELETE) { + delete next[key] + } + return next +} + +/** A Pi child that is no structured session (a catalog listing) names no caller at all. */ +export function piRpcSessionlessEnvironment(env: Readonly>): { + env: Record + envToDelete: readonly string[] +} { + return { + env: withoutPiCallerEnv(env), + envToDelete: [...STRUCTURED_CHILD_ENV_TO_DELETE, ...CALLER_ENV_TO_DELETE] + } +} + /** Pi stores its own sessions; an explicit session file is shared with terminal resumes. */ export function buildPiRpcLaunch(options: PiRpcLaunchOptions): ProviderProcessLaunch { if (!options.fullAccess) { @@ -50,16 +61,6 @@ export function buildPiRpcLaunch(options: PiRpcLaunchOptions): ProviderProcessLa ) { throw new Error('Pi fork requires one source session file') } - const env = { ...options.env } - for (const key of CALLER_ENV_TO_DELETE) { - delete env[key] - } - const childEnv = options.structuredSession - ? { - ...structuredSessionChildIdentityEnv(options.structuredSession.id, env), - ORCA_AGENT_SESSION_SPAWN_TOKEN: options.structuredSession.spawnToken - } - : env const args = [...(options.extraArgs ?? [])] let provider = false let model = false @@ -106,14 +107,7 @@ export function buildPiRpcLaunch(options: PiRpcLaunchOptions): ProviderProcessLa ...(options.sessionFile ? ['--session', options.sessionFile] : []), ...(options.forkFile ? ['--fork', options.forkFile] : []) ], - env: childEnv, - envToDelete: [ - ...CHILD_ENV_TO_DELETE, - ...(options.structuredSession - ? Object.hasOwn(childEnv, 'ORCA_TERMINAL_HANDLE') - ? [] - : ['ORCA_TERMINAL_HANDLE'] - : CALLER_ENV_TO_DELETE) - ] + env: { ...options.env }, + envToDelete: [...(options.envToDelete ?? [])] } } diff --git a/src/main/pi/rpc-model-catalog-probe.ts b/src/main/pi/rpc-model-catalog-probe.ts index 137068a8d62..6bdb1b63ae9 100644 --- a/src/main/pi/rpc-model-catalog-probe.ts +++ b/src/main/pi/rpc-model-catalog-probe.ts @@ -9,7 +9,7 @@ import type { } from '../native-chat/agent-model-catalog/agent-model-catalog-store' import { runAgentModelCatalogListing } from '../native-chat/agent-model-catalog/agent-model-catalog-probe-runner' import type { StructuredAgentCommandSettings } from '../native-chat/structured-agent-command-resolution' -import { buildPiRpcLaunch } from './rpc-launch' +import { buildPiRpcLaunch, piRpcSessionlessEnvironment } from './rpc-launch' import { piRpcVersionSupported, resolvePiRpcCommand } from './rpc-launch-resolution' import { PI_UNMAPPED_THINKING_LEVELS } from './rpc-options' @@ -52,7 +52,12 @@ export function createPiModelCatalogProbe(deps: PiModelCatalogProbeDeps): AgentM throw new Error(`pi at ${command} does not run structured chats`) } // The chat launch's own env rules; only the arguments differ. - const launch = buildPiRpcLaunch({ command, cwd, env, fullAccess: true }) + const launch = buildPiRpcLaunch({ + command, + cwd, + ...piRpcSessionlessEnvironment(env), + fullAccess: true + }) const stdout = await (deps.runListing ?? runAgentModelCatalogListing)( { ...launch, args: ['--list-models'] }, { site: 'pi-model-catalog-probe', signal: options?.signal } diff --git a/src/main/pi/rpc-runtime-registration.ts b/src/main/pi/rpc-runtime-registration.ts index ac78e88301a..a8e97e5e308 100644 --- a/src/main/pi/rpc-runtime-registration.ts +++ b/src/main/pi/rpc-runtime-registration.ts @@ -5,41 +5,30 @@ import { isAgentSessionPreSpawnError } from '../native-chat/agent-session-wire/s import { supportsSupervisedProviderChildLocation } from '../provider-process/supervised-provider-child-location' import type { StructuredAgentAdapterContext, - StructuredAgentModelCatalogContext, StructuredAgentRuntimeAdapter, StructuredAgentRuntimeRegistration } from '../runtime/structured-agent-runtime-registrations' +import { + composeStructuredLaunch, + structuredAgentLaunchSources +} from '../runtime/structured-agent-launch-composition' import { PI_RPC_AGENT } from './rpc-agent-definition' import { - createPiRpcLaunchResolver, + piRpcLaunchPart, piRpcVersionSupported, resolvePiRpcCommand } from './rpc-launch-resolution' import { PiRpcSessionAdapter } from './rpc-session-adapter' import { createPiModelCatalogProbe } from './rpc-model-catalog-probe' -/** The environment every Pi child starts from: a chat's launch and a catalog listing alike. */ -function piEnvironment({ - deps, - environment -}: StructuredAgentModelCatalogContext): () => Promise { - return async () => ({ - ...(await environment.resolveBaseEnvironment()), - ...deps.resolveAgentLaunchEnv?.('pi') - }) -} - function createPiRpcAdapter(context: StructuredAgentAdapterContext): StructuredAgentRuntimeAdapter { const { deps } = context return new PiRpcSessionAdapter({ - resolveLaunch: createPiRpcLaunchResolver({ - store: context.store, - resolveWorkspacePath: deps.resolveWorkspacePath, - resolveEnvironment: piEnvironment(context), - ...(deps.resolveAgentCommandSettings - ? { resolveCommandSettings: deps.resolveAgentCommandSettings } - : {}) - }), + resolveLaunch: composeStructuredLaunch( + PI_RPC_AGENT, + structuredAgentLaunchSources(context), + piRpcLaunchPart({}) + ), ...(deps.openPiConnection ? { openConnection: deps.openPiConnection } : {}), ...(deps.readProcessStartTime ? { readProcessStartTime: deps.readProcessStartTime } : {}), onLifecycle: context.deliverLifecycle, @@ -66,7 +55,8 @@ export const PI_RPC_RUNTIME_REGISTRATION: StructuredAgentRuntimeRegistration = { // `--list-models` marks no model as the one Pi runs by default. listingNamesConfiguredModel: false, probe: createPiModelCatalogProbe({ - resolveEnvironment: piEnvironment(context), + // The environment a chat's launch starts from too. + resolveEnvironment: () => context.environment.resolveAgentEnvironment('pi'), ...(context.deps.resolveAgentCommandSettings ? { resolveCommandSettings: context.deps.resolveAgentCommandSettings } : {}) diff --git a/src/main/pi/rpc-session-adapter.ts b/src/main/pi/rpc-session-adapter.ts index 7d25e4fb8aa..139268fe0a7 100644 --- a/src/main/pi/rpc-session-adapter.ts +++ b/src/main/pi/rpc-session-adapter.ts @@ -24,9 +24,10 @@ import { supportsSupervisedProviderChildLocation } from '../provider-process/sup import { ClaudeDispatchContentError } from '../claude/claude-structured-dispatch-content' export type PiRpcSessionAdapterDeps = PiRpcSessionDeps & { - resolveLaunch: ( + resolveLaunch: (input: { identity: StructuredAgentSessionAcquireInput['identity'] - ) => Promise + spawnToken: string + }) => Promise readProcessStartTime?: (pid: number) => Promise } @@ -69,7 +70,7 @@ export class PiRpcSessionAdapter implements StructuredAgentSessionAdapter { let launch: PiRpcResolvedLaunch try { launch = await waitForPromiseWithSignal( - this.deps.resolveLaunch(input.identity), + this.deps.resolveLaunch({ identity: input.identity, spawnToken: input.spawnToken }), attempt.signal ) } catch (error) { @@ -78,10 +79,7 @@ export class PiRpcSessionAdapter implements StructuredAgentSessionAdapter { if (attempt.signal.aborted) { throw new AgentSessionPreSpawnError(new Error('Pi closed while starting')) } - const spec = buildPiRpcLaunch({ - ...launch, - structuredSession: { id, spawnToken: input.spawnToken } - }) + const spec = buildPiRpcLaunch(launch) const fresh = !launch.sessionFile && !launch.forkFile session = new PiRpcSession(input, randomUUID(), spec, this.deps, fresh) this.sessions.set(id, session) diff --git a/src/main/provider-process/provider-spawned-process-identity.ts b/src/main/provider-process/provider-spawned-process-identity.ts index b0f04b29f27..a1f0e5403e2 100644 --- a/src/main/provider-process/provider-spawned-process-identity.ts +++ b/src/main/provider-process/provider-spawned-process-identity.ts @@ -4,10 +4,6 @@ import { readProcessStartTimeMs } from '../runtime/agent-session-process-identit // What the lease records about a provider child: the process a later owner probe re-proves. -/** The child echoes its spawn token here so the owner probe can tell a live child of THIS - * reservation from a same-pid stranger. */ -export const PROVIDER_SPAWN_TOKEN_ENV = 'ORCA_AGENT_SESSION_SPAWN_TOKEN' - const START_TIME_READ_ATTEMPTS = 3 type SpawnedIdentityInput = { diff --git a/src/main/runtime/__snapshots__/structured-agent-launch-parity.test.ts.snap b/src/main/runtime/__snapshots__/structured-agent-launch-parity.test.ts.snap new file mode 100644 index 00000000000..9d61ef0b418 --- /dev/null +++ b/src/main/runtime/__snapshots__/structured-agent-launch-parity.test.ts.snap @@ -0,0 +1,2891 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`native-chat launch parity > claude floating fresh 1`] = ` +{ + "child": { + "args": [ + "--output-format", + "stream-json", + "--verbose", + "--input-format", + "stream-json", + "--permission-prompt-tool", + "stdio", + "--setting-sources=user,project,local", + "--permission-mode", + "default", + "--include-partial-messages", + "--add-dir", + "/parity/extra", + "--add-dir", + "/state/agent-session-attachments", + "--add-dir", + "/state/native-chat-visuals/993224b4fe85990df64db926ca5c0346", + "--plugin-dir", + "/resources/native-chat-visuals", + "--session-id=ef4c28b6-1b32-45ba-8fec-d9ebb4e9bc59", + "--replay-user-messages", + "--thinking-display", + "summarized", + ], + "command": "/bin/claude", + "cwd": "/floating", + "env": { + "CLAUDE_AGENT_SDK_VERSION": "", + "CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS": "1", + "CLAUDE_CODE_ENTRYPOINT": "sdk-ts", + "CLAUDE_CODE_SDK_READS_SESSION_STATE": "1", + "CLAUDE_CONFIG_DIR": "/accounts/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_HOOK_ENDPOINT": "parent-orca_agent_hook_endpoint", + "ORCA_AGENT_HOOK_ENV": "parent-orca_agent_hook_env", + "ORCA_AGENT_HOOK_PORT": "parent-orca_agent_hook_port", + "ORCA_AGENT_HOOK_TOKEN": "parent-orca_agent_hook_token", + "ORCA_AGENT_HOOK_TRANSPORT": "parent-orca_agent_hook_transport", + "ORCA_AGENT_HOOK_VERSION": "parent-orca_agent_hook_version", + "ORCA_AGENT_LAUNCH": "parent-scrub-safe-launch", + "ORCA_AGENT_LAUNCH_TOKEN": "parent-launch-token", + "ORCA_AGENT_PANE": "parent-scrub-safe-pane", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000003", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/993224b4fe85990df64db926ca5c0346", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS": "parent-orca_claude_agent_status_settings", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_PANE_KEY": "tab_parent:parent-pane", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TAB_ID": "tab_parent", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "ORCA_WORKTREE_ID": "parent-worktree", + "PARITY_OVERLAY": "claude", + "PATH": "/bin:/orca-cli/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [], + "supervisor": { + "closeRequest": "stdin-end-and-sigterm", + "cwd": "/floating", + "lifetime": "session", + "nodeEnv": {}, + }, + }, + "launchPosture": {}, + "pinnedLaunchDirectories": [ + "/floating", + ], +} +`; + +exports[`native-chat launch parity > claude floating resumed 1`] = ` +{ + "child": { + "args": [ + "--output-format", + "stream-json", + "--verbose", + "--input-format", + "stream-json", + "--permission-prompt-tool", + "stdio", + "--resume=00000000-0000-4000-8000-000000000004", + "--setting-sources=user,project,local", + "--permission-mode", + "default", + "--include-partial-messages", + "--add-dir", + "/parity/extra", + "--add-dir", + "/state/agent-session-attachments", + "--add-dir", + "/state/native-chat-visuals/008bf60b9309e6141ae7eb98acb2a35d", + "--plugin-dir", + "/resources/native-chat-visuals", + "--replay-user-messages", + "--thinking-display", + "summarized", + ], + "command": "/bin/claude", + "cwd": "/floating-pinned", + "env": { + "CLAUDE_AGENT_SDK_VERSION": "", + "CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS": "1", + "CLAUDE_CODE_ENTRYPOINT": "sdk-ts", + "CLAUDE_CODE_SDK_READS_SESSION_STATE": "1", + "CLAUDE_CONFIG_DIR": "/accounts/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_HOOK_ENDPOINT": "parent-orca_agent_hook_endpoint", + "ORCA_AGENT_HOOK_ENV": "parent-orca_agent_hook_env", + "ORCA_AGENT_HOOK_PORT": "parent-orca_agent_hook_port", + "ORCA_AGENT_HOOK_TOKEN": "parent-orca_agent_hook_token", + "ORCA_AGENT_HOOK_TRANSPORT": "parent-orca_agent_hook_transport", + "ORCA_AGENT_HOOK_VERSION": "parent-orca_agent_hook_version", + "ORCA_AGENT_LAUNCH": "parent-scrub-safe-launch", + "ORCA_AGENT_LAUNCH_TOKEN": "parent-launch-token", + "ORCA_AGENT_PANE": "parent-scrub-safe-pane", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000004", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/008bf60b9309e6141ae7eb98acb2a35d", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS": "parent-orca_claude_agent_status_settings", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_PANE_KEY": "tab_parent:parent-pane", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TAB_ID": "tab_parent", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "ORCA_WORKTREE_ID": "parent-worktree", + "PARITY_OVERLAY": "claude", + "PATH": "/bin:/orca-cli/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [], + "supervisor": { + "closeRequest": "stdin-end-and-sigterm", + "cwd": "/floating-pinned", + "lifetime": "session", + "nodeEnv": {}, + }, + }, + "launchPosture": {}, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > claude worktree fresh 1`] = ` +{ + "child": { + "args": [ + "--output-format", + "stream-json", + "--verbose", + "--input-format", + "stream-json", + "--permission-prompt-tool", + "stdio", + "--setting-sources=user,project,local", + "--permission-mode", + "default", + "--include-partial-messages", + "--add-dir", + "/parity/extra", + "--add-dir", + "/state/agent-session-attachments", + "--add-dir", + "/state/native-chat-visuals/11e594f481958c10e3015d0bf0447a22", + "--plugin-dir", + "/resources/native-chat-visuals", + "--session-id=f00702b6-4b6d-4a6c-b685-4215832dab6b", + "--replay-user-messages", + "--thinking-display", + "summarized", + ], + "command": "/bin/claude", + "cwd": "/worktree", + "env": { + "CLAUDE_AGENT_SDK_VERSION": "", + "CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS": "1", + "CLAUDE_CODE_ENTRYPOINT": "sdk-ts", + "CLAUDE_CODE_SDK_READS_SESSION_STATE": "1", + "CLAUDE_CONFIG_DIR": "/accounts/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_HOOK_ENDPOINT": "parent-orca_agent_hook_endpoint", + "ORCA_AGENT_HOOK_ENV": "parent-orca_agent_hook_env", + "ORCA_AGENT_HOOK_PORT": "parent-orca_agent_hook_port", + "ORCA_AGENT_HOOK_TOKEN": "parent-orca_agent_hook_token", + "ORCA_AGENT_HOOK_TRANSPORT": "parent-orca_agent_hook_transport", + "ORCA_AGENT_HOOK_VERSION": "parent-orca_agent_hook_version", + "ORCA_AGENT_LAUNCH": "parent-scrub-safe-launch", + "ORCA_AGENT_LAUNCH_TOKEN": "parent-launch-token", + "ORCA_AGENT_PANE": "parent-scrub-safe-pane", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000001", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/11e594f481958c10e3015d0bf0447a22", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS": "parent-orca_claude_agent_status_settings", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_PANE_KEY": "tab_parent:parent-pane", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TAB_ID": "tab_parent", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "ORCA_WORKTREE_ID": "parent-worktree", + "PARITY_OVERLAY": "claude", + "PATH": "/bin:/orca-cli/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [], + "supervisor": { + "closeRequest": "stdin-end-and-sigterm", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": {}, + }, + }, + "launchPosture": {}, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > claude worktree fresh configured 1`] = ` +{ + "child": { + "args": [ + "--output-format", + "stream-json", + "--verbose", + "--input-format", + "stream-json", + "--permission-prompt-tool", + "stdio", + "--setting-sources=user,project,local", + "--permission-mode", + "default", + "--include-partial-messages", + "--add-dir", + "/parity/configured", + "--add-dir", + "/state/agent-session-attachments", + "--add-dir", + "/state/native-chat-visuals/eaed3477e8b61885d231829e3d12e45d", + "--plugin-dir", + "/resources/native-chat-visuals", + "--session-id=942320a9-dfba-44e2-983c-202bdc54c4c5", + "--replay-user-messages", + "--dangerously-skip-permissions", + "--thinking-display", + "summarized", + ], + "command": "/custom-bin/claude-custom", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_AGENT_SDK_VERSION": "", + "CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS": "1", + "CLAUDE_CODE_ENTRYPOINT": "sdk-ts", + "CLAUDE_CODE_SDK_READS_SESSION_STATE": "1", + "CLAUDE_CONFIG_DIR": "/accounts/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "overlay", + "LANG": "C", + "ORCA_AGENT_HOOK_ENDPOINT": "parent-orca_agent_hook_endpoint", + "ORCA_AGENT_HOOK_ENV": "parent-orca_agent_hook_env", + "ORCA_AGENT_HOOK_PORT": "parent-orca_agent_hook_port", + "ORCA_AGENT_HOOK_TOKEN": "parent-orca_agent_hook_token", + "ORCA_AGENT_HOOK_TRANSPORT": "parent-orca_agent_hook_transport", + "ORCA_AGENT_HOOK_VERSION": "parent-orca_agent_hook_version", + "ORCA_AGENT_LAUNCH": "parent-scrub-safe-launch", + "ORCA_AGENT_LAUNCH_TOKEN": "parent-launch-token", + "ORCA_AGENT_PANE": "parent-scrub-safe-pane", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000031", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/eaed3477e8b61885d231829e3d12e45d", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS": "parent-orca_claude_agent_status_settings", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_PANE_KEY": "tab_parent:parent-pane", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TAB_ID": "tab_parent", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "ORCA_WORKTREE_ID": "parent-worktree", + "PARITY_OVERLAY": "claude-configured", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + }, + "envToDelete": [], + "supervisor": { + "closeRequest": "stdin-end-and-sigterm", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": {}, + }, + }, + "launchPosture": {}, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > claude worktree fresh worker 1`] = ` +{ + "child": { + "args": [ + "--output-format", + "stream-json", + "--verbose", + "--input-format", + "stream-json", + "--permission-prompt-tool", + "stdio", + "--setting-sources=user,project,local", + "--permission-mode", + "default", + "--include-partial-messages", + "--add-dir", + "/parity/extra", + "--add-dir", + "/state/agent-session-attachments", + "--session-id=605d826f-82d0-4002-8f87-818cdfd51395", + "--replay-user-messages", + "--thinking-display", + "summarized", + ], + "command": "/bin/claude", + "cwd": "/worktree", + "env": { + "CLAUDE_AGENT_SDK_VERSION": "", + "CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS": "1", + "CLAUDE_CODE_ENTRYPOINT": "sdk-ts", + "CLAUDE_CODE_SDK_READS_SESSION_STATE": "1", + "CLAUDE_CONFIG_DIR": "/accounts/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_HOOK_ENDPOINT": "parent-orca_agent_hook_endpoint", + "ORCA_AGENT_HOOK_ENV": "parent-orca_agent_hook_env", + "ORCA_AGENT_HOOK_PORT": "parent-orca_agent_hook_port", + "ORCA_AGENT_HOOK_TOKEN": "parent-orca_agent_hook_token", + "ORCA_AGENT_HOOK_TRANSPORT": "parent-orca_agent_hook_transport", + "ORCA_AGENT_HOOK_VERSION": "parent-orca_agent_hook_version", + "ORCA_AGENT_LAUNCH": "parent-scrub-safe-launch", + "ORCA_AGENT_LAUNCH_TOKEN": "parent-launch-token", + "ORCA_AGENT_PANE": "parent-scrub-safe-pane", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000005", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS": "parent-orca_claude_agent_status_settings", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_PANE_KEY": "tab_parent:parent-pane", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TAB_ID": "tab_parent", + "ORCA_TERMINAL_HANDLE": "structworker_00000000-0000-4000-8000-000000000005", + "ORCA_USER_DATA_PATH": "/user-data", + "ORCA_WORKTREE_ID": "parent-worktree", + "PARITY_OVERLAY": "claude", + "PATH": "/bin:/orca-cli/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [], + "supervisor": { + "closeRequest": "stdin-end-and-sigterm", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": {}, + }, + }, + "launchPosture": {}, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > claude worktree resumed 1`] = ` +{ + "child": { + "args": [ + "--output-format", + "stream-json", + "--verbose", + "--input-format", + "stream-json", + "--permission-prompt-tool", + "stdio", + "--resume=00000000-0000-4000-8000-000000000002", + "--setting-sources=user,project,local", + "--permission-mode", + "default", + "--include-partial-messages", + "--add-dir", + "/parity/extra", + "--add-dir", + "/state/agent-session-attachments", + "--add-dir", + "/state/native-chat-visuals/e79acd97ac88086665d85a762f43d533", + "--plugin-dir", + "/resources/native-chat-visuals", + "--replay-user-messages", + "--thinking-display", + "summarized", + ], + "command": "/bin/claude", + "cwd": "/worktree", + "env": { + "CLAUDE_AGENT_SDK_VERSION": "", + "CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS": "1", + "CLAUDE_CODE_ENTRYPOINT": "sdk-ts", + "CLAUDE_CODE_SDK_READS_SESSION_STATE": "1", + "CLAUDE_CONFIG_DIR": "/accounts/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_HOOK_ENDPOINT": "parent-orca_agent_hook_endpoint", + "ORCA_AGENT_HOOK_ENV": "parent-orca_agent_hook_env", + "ORCA_AGENT_HOOK_PORT": "parent-orca_agent_hook_port", + "ORCA_AGENT_HOOK_TOKEN": "parent-orca_agent_hook_token", + "ORCA_AGENT_HOOK_TRANSPORT": "parent-orca_agent_hook_transport", + "ORCA_AGENT_HOOK_VERSION": "parent-orca_agent_hook_version", + "ORCA_AGENT_LAUNCH": "parent-scrub-safe-launch", + "ORCA_AGENT_LAUNCH_TOKEN": "parent-launch-token", + "ORCA_AGENT_PANE": "parent-scrub-safe-pane", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000002", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/e79acd97ac88086665d85a762f43d533", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS": "parent-orca_claude_agent_status_settings", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_PANE_KEY": "tab_parent:parent-pane", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TAB_ID": "tab_parent", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "ORCA_WORKTREE_ID": "parent-worktree", + "PARITY_OVERLAY": "claude", + "PATH": "/bin:/orca-cli/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [], + "supervisor": { + "closeRequest": "stdin-end-and-sigterm", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": {}, + }, + }, + "launchPosture": {}, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > claude worktree resumed configured 1`] = ` +{ + "child": { + "args": [ + "--output-format", + "stream-json", + "--verbose", + "--input-format", + "stream-json", + "--permission-prompt-tool", + "stdio", + "--resume=00000000-0000-4000-8000-000000000032", + "--setting-sources=user,project,local", + "--permission-mode", + "default", + "--include-partial-messages", + "--add-dir", + "/parity/configured", + "--add-dir", + "/state/agent-session-attachments", + "--add-dir", + "/state/native-chat-visuals/b0c5ad35009f2e02d85f43df8b152891", + "--plugin-dir", + "/resources/native-chat-visuals", + "--replay-user-messages", + "--dangerously-skip-permissions", + "--thinking-display", + "summarized", + ], + "command": "/custom-bin/claude-custom", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_AGENT_SDK_VERSION": "", + "CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS": "1", + "CLAUDE_CODE_ENTRYPOINT": "sdk-ts", + "CLAUDE_CODE_SDK_READS_SESSION_STATE": "1", + "CLAUDE_CONFIG_DIR": "/accounts/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "overlay", + "LANG": "C", + "ORCA_AGENT_HOOK_ENDPOINT": "parent-orca_agent_hook_endpoint", + "ORCA_AGENT_HOOK_ENV": "parent-orca_agent_hook_env", + "ORCA_AGENT_HOOK_PORT": "parent-orca_agent_hook_port", + "ORCA_AGENT_HOOK_TOKEN": "parent-orca_agent_hook_token", + "ORCA_AGENT_HOOK_TRANSPORT": "parent-orca_agent_hook_transport", + "ORCA_AGENT_HOOK_VERSION": "parent-orca_agent_hook_version", + "ORCA_AGENT_LAUNCH": "parent-scrub-safe-launch", + "ORCA_AGENT_LAUNCH_TOKEN": "parent-launch-token", + "ORCA_AGENT_PANE": "parent-scrub-safe-pane", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000032", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/b0c5ad35009f2e02d85f43df8b152891", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS": "parent-orca_claude_agent_status_settings", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_PANE_KEY": "tab_parent:parent-pane", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TAB_ID": "tab_parent", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "ORCA_WORKTREE_ID": "parent-worktree", + "PARITY_OVERLAY": "claude-configured", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + }, + "envToDelete": [], + "supervisor": { + "closeRequest": "stdin-end-and-sigterm", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": {}, + }, + }, + "launchPosture": {}, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > codex floating fresh 1`] = ` +{ + "child": { + "args": [ + "-c", + "model_reasoning_effort=high", + "app-server", + ], + "command": "/bin/codex", + "cwd": "/floating", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/accounts/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_HOOK_ENDPOINT": "parent-orca_agent_hook_endpoint", + "ORCA_AGENT_HOOK_ENV": "parent-orca_agent_hook_env", + "ORCA_AGENT_HOOK_PORT": "parent-orca_agent_hook_port", + "ORCA_AGENT_HOOK_TOKEN": "parent-orca_agent_hook_token", + "ORCA_AGENT_HOOK_TRANSPORT": "parent-orca_agent_hook_transport", + "ORCA_AGENT_HOOK_VERSION": "parent-orca_agent_hook_version", + "ORCA_AGENT_LAUNCH": "parent-scrub-safe-launch", + "ORCA_AGENT_LAUNCH_TOKEN": "parent-launch-token", + "ORCA_AGENT_PANE": "parent-scrub-safe-pane", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000008", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/fe9eeff554ea6c98aca483076d97614a", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS": "parent-orca_claude_agent_status_settings", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_PANE_KEY": "tab_parent:parent-pane", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TAB_ID": "tab_parent", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "ORCA_WORKTREE_ID": "parent-worktree", + "PARITY_OVERLAY": "codex", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/floating", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "permissionPolicy": { + "approvalPolicy": "on-request", + "sandbox": "workspace-write", + }, + }, + "pinnedLaunchDirectories": [ + "/floating", + ], +} +`; + +exports[`native-chat launch parity > codex floating resumed 1`] = ` +{ + "child": { + "args": [ + "-c", + "model_reasoning_effort=high", + "app-server", + ], + "command": "/bin/codex", + "cwd": "/floating-pinned", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/accounts/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_HOOK_ENDPOINT": "parent-orca_agent_hook_endpoint", + "ORCA_AGENT_HOOK_ENV": "parent-orca_agent_hook_env", + "ORCA_AGENT_HOOK_PORT": "parent-orca_agent_hook_port", + "ORCA_AGENT_HOOK_TOKEN": "parent-orca_agent_hook_token", + "ORCA_AGENT_HOOK_TRANSPORT": "parent-orca_agent_hook_transport", + "ORCA_AGENT_HOOK_VERSION": "parent-orca_agent_hook_version", + "ORCA_AGENT_LAUNCH": "parent-scrub-safe-launch", + "ORCA_AGENT_LAUNCH_TOKEN": "parent-launch-token", + "ORCA_AGENT_PANE": "parent-scrub-safe-pane", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000009", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/f3e028aaa6530055f01a5e1e849f6765", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS": "parent-orca_claude_agent_status_settings", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_PANE_KEY": "tab_parent:parent-pane", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TAB_ID": "tab_parent", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "ORCA_WORKTREE_ID": "parent-worktree", + "PARITY_OVERLAY": "codex", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/floating-pinned", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "permissionPolicy": { + "approvalPolicy": "on-request", + "sandbox": "workspace-write", + }, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > codex worktree fresh 1`] = ` +{ + "child": { + "args": [ + "-c", + "model_reasoning_effort=high", + "app-server", + ], + "command": "/bin/codex", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/accounts/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_HOOK_ENDPOINT": "parent-orca_agent_hook_endpoint", + "ORCA_AGENT_HOOK_ENV": "parent-orca_agent_hook_env", + "ORCA_AGENT_HOOK_PORT": "parent-orca_agent_hook_port", + "ORCA_AGENT_HOOK_TOKEN": "parent-orca_agent_hook_token", + "ORCA_AGENT_HOOK_TRANSPORT": "parent-orca_agent_hook_transport", + "ORCA_AGENT_HOOK_VERSION": "parent-orca_agent_hook_version", + "ORCA_AGENT_LAUNCH": "parent-scrub-safe-launch", + "ORCA_AGENT_LAUNCH_TOKEN": "parent-launch-token", + "ORCA_AGENT_PANE": "parent-scrub-safe-pane", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000006", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/a8353ed0bde97e7845f0996d60267ea5", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS": "parent-orca_claude_agent_status_settings", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_PANE_KEY": "tab_parent:parent-pane", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TAB_ID": "tab_parent", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "ORCA_WORKTREE_ID": "parent-worktree", + "PARITY_OVERLAY": "codex", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "permissionPolicy": { + "approvalPolicy": "on-request", + "sandbox": "workspace-write", + }, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > codex worktree fresh configured 1`] = ` +{ + "child": { + "args": [ + "-c", + "model_reasoning_effort=low", + "app-server", + ], + "command": "/custom-bin/codex-custom", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/accounts/codex", + "HOME": "/home", + "INHERITED_ONLY": "overlay", + "LANG": "C", + "ORCA_AGENT_HOOK_ENDPOINT": "parent-orca_agent_hook_endpoint", + "ORCA_AGENT_HOOK_ENV": "parent-orca_agent_hook_env", + "ORCA_AGENT_HOOK_PORT": "parent-orca_agent_hook_port", + "ORCA_AGENT_HOOK_TOKEN": "parent-orca_agent_hook_token", + "ORCA_AGENT_HOOK_TRANSPORT": "parent-orca_agent_hook_transport", + "ORCA_AGENT_HOOK_VERSION": "parent-orca_agent_hook_version", + "ORCA_AGENT_LAUNCH": "parent-scrub-safe-launch", + "ORCA_AGENT_LAUNCH_TOKEN": "parent-launch-token", + "ORCA_AGENT_PANE": "parent-scrub-safe-pane", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000033", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/0a57e3c82f8be1ecdb1249602bb0e48d", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS": "parent-orca_claude_agent_status_settings", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_PANE_KEY": "tab_parent:parent-pane", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TAB_ID": "tab_parent", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "ORCA_WORKTREE_ID": "parent-worktree", + "PARITY_OVERLAY": "codex-configured", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + }, + "envToDelete": [], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "permissionPolicy": { + "approvalPolicy": "never", + "sandbox": "danger-full-access", + }, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > codex worktree fresh worker 1`] = ` +{ + "child": { + "args": [ + "-c", + "model_reasoning_effort=high", + "app-server", + ], + "command": "/bin/codex", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/accounts/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_HOOK_ENDPOINT": "parent-orca_agent_hook_endpoint", + "ORCA_AGENT_HOOK_ENV": "parent-orca_agent_hook_env", + "ORCA_AGENT_HOOK_PORT": "parent-orca_agent_hook_port", + "ORCA_AGENT_HOOK_TOKEN": "parent-orca_agent_hook_token", + "ORCA_AGENT_HOOK_TRANSPORT": "parent-orca_agent_hook_transport", + "ORCA_AGENT_HOOK_VERSION": "parent-orca_agent_hook_version", + "ORCA_AGENT_LAUNCH": "parent-scrub-safe-launch", + "ORCA_AGENT_LAUNCH_TOKEN": "parent-launch-token", + "ORCA_AGENT_PANE": "parent-scrub-safe-pane", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000010", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS": "parent-orca_claude_agent_status_settings", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_PANE_KEY": "tab_parent:parent-pane", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TAB_ID": "tab_parent", + "ORCA_TERMINAL_HANDLE": "structworker_00000000-0000-4000-8000-000000000010", + "ORCA_USER_DATA_PATH": "/user-data", + "ORCA_WORKTREE_ID": "parent-worktree", + "PARITY_OVERLAY": "codex", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [ + "ORCA_CHAT_VISUALS_DIR", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "permissionPolicy": { + "approvalPolicy": "on-request", + "sandbox": "workspace-write", + }, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > codex worktree resumed 1`] = ` +{ + "child": { + "args": [ + "-c", + "model_reasoning_effort=high", + "app-server", + ], + "command": "/bin/codex", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/accounts/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_HOOK_ENDPOINT": "parent-orca_agent_hook_endpoint", + "ORCA_AGENT_HOOK_ENV": "parent-orca_agent_hook_env", + "ORCA_AGENT_HOOK_PORT": "parent-orca_agent_hook_port", + "ORCA_AGENT_HOOK_TOKEN": "parent-orca_agent_hook_token", + "ORCA_AGENT_HOOK_TRANSPORT": "parent-orca_agent_hook_transport", + "ORCA_AGENT_HOOK_VERSION": "parent-orca_agent_hook_version", + "ORCA_AGENT_LAUNCH": "parent-scrub-safe-launch", + "ORCA_AGENT_LAUNCH_TOKEN": "parent-launch-token", + "ORCA_AGENT_PANE": "parent-scrub-safe-pane", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000007", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/6b7be92641aec20b00f9c40280433e0a", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS": "parent-orca_claude_agent_status_settings", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_PANE_KEY": "tab_parent:parent-pane", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TAB_ID": "tab_parent", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "ORCA_WORKTREE_ID": "parent-worktree", + "PARITY_OVERLAY": "codex", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "permissionPolicy": { + "approvalPolicy": "on-request", + "sandbox": "workspace-write", + }, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > codex worktree resumed configured 1`] = ` +{ + "child": { + "args": [ + "-c", + "model_reasoning_effort=low", + "app-server", + ], + "command": "/custom-bin/codex-custom", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/accounts/codex", + "HOME": "/home", + "INHERITED_ONLY": "overlay", + "LANG": "C", + "ORCA_AGENT_HOOK_ENDPOINT": "parent-orca_agent_hook_endpoint", + "ORCA_AGENT_HOOK_ENV": "parent-orca_agent_hook_env", + "ORCA_AGENT_HOOK_PORT": "parent-orca_agent_hook_port", + "ORCA_AGENT_HOOK_TOKEN": "parent-orca_agent_hook_token", + "ORCA_AGENT_HOOK_TRANSPORT": "parent-orca_agent_hook_transport", + "ORCA_AGENT_HOOK_VERSION": "parent-orca_agent_hook_version", + "ORCA_AGENT_LAUNCH": "parent-scrub-safe-launch", + "ORCA_AGENT_LAUNCH_TOKEN": "parent-launch-token", + "ORCA_AGENT_PANE": "parent-scrub-safe-pane", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000034", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/4c105fab16e20f574f686cfe2d55d927", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS": "parent-orca_claude_agent_status_settings", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_PANE_KEY": "tab_parent:parent-pane", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TAB_ID": "tab_parent", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "ORCA_WORKTREE_ID": "parent-worktree", + "PARITY_OVERLAY": "codex-configured", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + }, + "envToDelete": [], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "permissionPolicy": { + "approvalPolicy": "never", + "sandbox": "danger-full-access", + }, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > grok floating fresh 1`] = ` +{ + "child": { + "args": [ + "agent", + "--always-approve", + "--plugin-dir", + "/resources/native-chat-visuals", + "stdio", + ], + "command": "/bin/grok", + "cwd": "/floating", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "GROK_HOME": "/accounts/grok", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000013", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/205832d622938c215d9919d8002db900", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "grok", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [ + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/floating", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": true, + }, + "pinnedLaunchDirectories": [ + "/floating", + ], +} +`; + +exports[`native-chat launch parity > grok floating resumed 1`] = ` +{ + "child": { + "args": [ + "agent", + "--always-approve", + "--plugin-dir", + "/resources/native-chat-visuals", + "stdio", + ], + "command": "/bin/grok", + "cwd": "/floating-pinned", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "GROK_HOME": "/accounts/grok", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000014", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/ccd97507ebec4aa1657cda85a2bfc454", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "grok", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [ + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/floating-pinned", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": true, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > grok worktree fresh 1`] = ` +{ + "child": { + "args": [ + "agent", + "--always-approve", + "--plugin-dir", + "/resources/native-chat-visuals", + "stdio", + ], + "command": "/bin/grok", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "GROK_HOME": "/accounts/grok", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000011", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/dd7df116468181b64e6126c7fec56f82", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "grok", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [ + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": true, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > grok worktree fresh configured 1`] = ` +{ + "child": { + "args": [ + "agent", + "--plugin-dir", + "/resources/native-chat-visuals", + "stdio", + ], + "command": "/custom-bin/grok-custom", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "GROK_HOME": "/accounts/grok", + "HOME": "/home", + "INHERITED_ONLY": "overlay", + "LANG": "C", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000035", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/4c6c945b7f4f83e068aec56a1037f603", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "grok-configured", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + }, + "envToDelete": [ + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": false, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > grok worktree fresh worker 1`] = ` +{ + "child": { + "args": [ + "agent", + "--always-approve", + "stdio", + ], + "command": "/bin/grok", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "GROK_HOME": "/accounts/grok", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000015", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "structworker_00000000-0000-4000-8000-000000000015", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "grok", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [ + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CHAT_VISUALS_DIR", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": true, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > grok worktree resumed 1`] = ` +{ + "child": { + "args": [ + "agent", + "--always-approve", + "--plugin-dir", + "/resources/native-chat-visuals", + "stdio", + ], + "command": "/bin/grok", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "GROK_HOME": "/accounts/grok", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000012", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/5ba39b7cfa537ea49862ef2aa91047d0", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "grok", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [ + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": true, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > grok worktree resumed configured 1`] = ` +{ + "child": { + "args": [ + "agent", + "--plugin-dir", + "/resources/native-chat-visuals", + "stdio", + ], + "command": "/custom-bin/grok-custom", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "GROK_HOME": "/accounts/grok", + "HOME": "/home", + "INHERITED_ONLY": "overlay", + "LANG": "C", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000036", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/4513a9571ec9fe3b6c2cee8782d16d37", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "grok-configured", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + }, + "envToDelete": [ + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": false, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > omp floating fresh 1`] = ` +{ + "child": { + "args": [ + "acp", + "--plugin-dir", + "/resources/native-chat-visuals", + ], + "command": "/bin/omp", + "cwd": "/floating", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000023", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/04e2e3dee61c2161997f73ad32af9fad", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "omp", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "PI_CODING_AGENT_DIR": "/accounts/omp", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [ + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/floating", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": false, + }, + "pinnedLaunchDirectories": [ + "/floating", + ], +} +`; + +exports[`native-chat launch parity > omp floating resumed 1`] = ` +{ + "child": { + "args": [ + "acp", + "--plugin-dir", + "/resources/native-chat-visuals", + ], + "command": "/bin/omp", + "cwd": "/floating-pinned", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000024", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/52b586c587fa58fc8ce47971e57d8d10", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "omp", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "PI_CODING_AGENT_DIR": "/accounts/omp", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [ + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/floating-pinned", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": false, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > omp worktree fresh 1`] = ` +{ + "child": { + "args": [ + "acp", + "--plugin-dir", + "/resources/native-chat-visuals", + ], + "command": "/bin/omp", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000021", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/a28c50c0e7e8aef4cef08e0d88a19f2d", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "omp", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "PI_CODING_AGENT_DIR": "/accounts/omp", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [ + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": false, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > omp worktree fresh configured 1`] = ` +{ + "child": { + "args": [ + "acp", + "--plugin-dir", + "/resources/native-chat-visuals", + ], + "command": "/custom-bin/omp-custom", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "overlay", + "LANG": "C", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000039", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/7eca093e243e678793121c8537d04cc0", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "omp-configured", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "PI_CODING_AGENT_DIR": "/accounts/omp", + "SHELL_ONLY": "shell", + }, + "envToDelete": [ + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": false, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > omp worktree fresh worker 1`] = ` +{ + "child": { + "args": [ + "acp", + ], + "command": "/bin/omp", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000025", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "structworker_00000000-0000-4000-8000-000000000025", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "omp", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "PI_CODING_AGENT_DIR": "/accounts/omp", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [ + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CHAT_VISUALS_DIR", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": false, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > omp worktree resumed 1`] = ` +{ + "child": { + "args": [ + "acp", + "--plugin-dir", + "/resources/native-chat-visuals", + ], + "command": "/bin/omp", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000022", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/c9a6a6942b3dd2bfaf1304d9bf0db2bc", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "omp", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "PI_CODING_AGENT_DIR": "/accounts/omp", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [ + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": false, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > omp worktree resumed configured 1`] = ` +{ + "child": { + "args": [ + "acp", + "--plugin-dir", + "/resources/native-chat-visuals", + ], + "command": "/custom-bin/omp-custom", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "overlay", + "LANG": "C", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000040", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/3d37237436bb2343509e2f2d6240ec5c", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "omp-configured", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "PI_CODING_AGENT_DIR": "/accounts/omp", + "SHELL_ONLY": "shell", + }, + "envToDelete": [ + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": false, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > opencode floating fresh 1`] = ` +{ + "child": { + "args": [ + "acp", + ], + "command": "/bin/opencode", + "cwd": "/floating", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "OPENCODE_CLIENT": "acp", + "OPENCODE_CONFIG_CONTENT": "{"skills":{"paths":["/resources/native-chat-visuals/skills"]}}", + "OPENCODE_ENABLE_QUESTION_TOOL": "false", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000018", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/d8b72865af2aa80c73fb957764c415b2", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "opencode", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [ + "OPENCODE_AUTH_CONTENT", + "OPENCODE_CONFIG_DIR", + "OPENCODE_DB", + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_DATA_ACCOUNT_PROVIDER", + "ORCA_OPENCODE_CONFIG_DIR", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + "XDG_DATA_HOME", + "XDG_STATE_HOME", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/floating", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": false, + }, + "pinnedLaunchDirectories": [ + "/floating", + ], +} +`; + +exports[`native-chat launch parity > opencode floating resumed 1`] = ` +{ + "child": { + "args": [ + "acp", + ], + "command": "/bin/opencode", + "cwd": "/floating-pinned", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "OPENCODE_CLIENT": "acp", + "OPENCODE_CONFIG_CONTENT": "{"skills":{"paths":["/resources/native-chat-visuals/skills"]}}", + "OPENCODE_ENABLE_QUESTION_TOOL": "false", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000019", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/142777cee33f20ee78b32b50361f8e9c", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "opencode", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [ + "OPENCODE_AUTH_CONTENT", + "OPENCODE_CONFIG_DIR", + "OPENCODE_DB", + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_DATA_ACCOUNT_PROVIDER", + "ORCA_OPENCODE_CONFIG_DIR", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + "XDG_DATA_HOME", + "XDG_STATE_HOME", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/floating-pinned", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": false, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > opencode worktree fresh 1`] = ` +{ + "child": { + "args": [ + "acp", + ], + "command": "/bin/opencode", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "OPENCODE_CLIENT": "acp", + "OPENCODE_CONFIG_CONTENT": "{"skills":{"paths":["/resources/native-chat-visuals/skills"]}}", + "OPENCODE_ENABLE_QUESTION_TOOL": "false", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000016", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/1ebe6393ce8358dbebca3fa0236d990c", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "opencode", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [ + "OPENCODE_AUTH_CONTENT", + "OPENCODE_CONFIG_DIR", + "OPENCODE_DB", + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_DATA_ACCOUNT_PROVIDER", + "ORCA_OPENCODE_CONFIG_DIR", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + "XDG_DATA_HOME", + "XDG_STATE_HOME", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": false, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > opencode worktree fresh configured 1`] = ` +{ + "child": { + "args": [ + "acp", + ], + "command": "/custom-bin/opencode-custom", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "overlay", + "LANG": "C", + "OPENCODE_CLIENT": "acp", + "OPENCODE_CONFIG_CONTENT": "{"skills":{"paths":["/resources/native-chat-visuals/skills"]}}", + "OPENCODE_ENABLE_QUESTION_TOOL": "false", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000037", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/083c5a4185dc6c2750d599f361c671d1", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "opencode-configured", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + }, + "envToDelete": [ + "OPENCODE_AUTH_CONTENT", + "OPENCODE_CONFIG_DIR", + "OPENCODE_DB", + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_DATA_ACCOUNT_PROVIDER", + "ORCA_OPENCODE_CONFIG_DIR", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + "XDG_DATA_HOME", + "XDG_STATE_HOME", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": false, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > opencode worktree fresh worker 1`] = ` +{ + "child": { + "args": [ + "acp", + ], + "command": "/bin/opencode", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "OPENCODE_CLIENT": "acp", + "OPENCODE_ENABLE_QUESTION_TOOL": "false", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000020", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "structworker_00000000-0000-4000-8000-000000000020", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "opencode", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [ + "OPENCODE_AUTH_CONTENT", + "OPENCODE_CONFIG_DIR", + "OPENCODE_DB", + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CHAT_VISUALS_DIR", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_DATA_ACCOUNT_PROVIDER", + "ORCA_OPENCODE_CONFIG_DIR", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + "XDG_DATA_HOME", + "XDG_STATE_HOME", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": false, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > opencode worktree resumed 1`] = ` +{ + "child": { + "args": [ + "acp", + ], + "command": "/bin/opencode", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "OPENCODE_CLIENT": "acp", + "OPENCODE_CONFIG_CONTENT": "{"skills":{"paths":["/resources/native-chat-visuals/skills"]}}", + "OPENCODE_ENABLE_QUESTION_TOOL": "false", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000017", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/f22d18cd47801ffbd620f7a8fd45d117", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "opencode", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [ + "OPENCODE_AUTH_CONTENT", + "OPENCODE_CONFIG_DIR", + "OPENCODE_DB", + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_DATA_ACCOUNT_PROVIDER", + "ORCA_OPENCODE_CONFIG_DIR", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + "XDG_DATA_HOME", + "XDG_STATE_HOME", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": false, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > opencode worktree resumed configured 1`] = ` +{ + "child": { + "args": [ + "acp", + ], + "command": "/custom-bin/opencode-custom", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "overlay", + "LANG": "C", + "OPENCODE_CLIENT": "acp", + "OPENCODE_CONFIG_CONTENT": "{"skills":{"paths":["/resources/native-chat-visuals/skills"]}}", + "OPENCODE_ENABLE_QUESTION_TOOL": "false", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000038", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/state/native-chat-visuals/9825b24d9014b859f02ff26f814792b0", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "term_parent", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "opencode-configured", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "SHELL_ONLY": "shell", + }, + "envToDelete": [ + "OPENCODE_AUTH_CONTENT", + "OPENCODE_CONFIG_DIR", + "OPENCODE_DB", + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_DATA_ACCOUNT_PROVIDER", + "ORCA_OPENCODE_CONFIG_DIR", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + "XDG_DATA_HOME", + "XDG_STATE_HOME", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": false, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > pi floating fresh 1`] = ` +{ + "child": { + "args": [ + "--mode", + "rpc", + ], + "command": "/bin/pi", + "cwd": "/floating", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000028", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/parent/visuals", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "pi", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "PI_CODING_AGENT_DIR": "/accounts/pi", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [ + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_TERMINAL_HANDLE", + "ORCA_WORKTREE_ID", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/floating", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": true, + }, + "pinnedLaunchDirectories": [ + "/floating", + ], +} +`; + +exports[`native-chat launch parity > pi floating resumed 1`] = ` +{ + "child": { + "args": [ + "--mode", + "rpc", + "--session", + "/pi-sessions/00000000-0000-4000-8000-000000000029.jsonl", + ], + "command": "/bin/pi", + "cwd": "/floating-pinned", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000029", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/parent/visuals", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "pi", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "PI_CODING_AGENT_DIR": "/accounts/pi", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [ + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_TERMINAL_HANDLE", + "ORCA_WORKTREE_ID", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/floating-pinned", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": true, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > pi worktree fresh 1`] = ` +{ + "child": { + "args": [ + "--mode", + "rpc", + ], + "command": "/bin/pi", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000026", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/parent/visuals", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "pi", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "PI_CODING_AGENT_DIR": "/accounts/pi", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [ + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_TERMINAL_HANDLE", + "ORCA_WORKTREE_ID", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": true, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > pi worktree fresh configured 1`] = ` +{ + "child": { + "args": [ + "--mode", + "rpc", + ], + "command": "/custom-bin/pi-custom", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "overlay", + "LANG": "C", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000041", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/parent/visuals", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "pi-configured", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "PI_CODING_AGENT_DIR": "/accounts/pi", + "SHELL_ONLY": "shell", + }, + "envToDelete": [ + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_TERMINAL_HANDLE", + "ORCA_WORKTREE_ID", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": true, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > pi worktree fresh worker 1`] = ` +{ + "child": { + "args": [ + "--mode", + "rpc", + ], + "command": "/bin/pi", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000030", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/parent/visuals", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_TERMINAL_HANDLE": "structworker_00000000-0000-4000-8000-000000000030", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "pi", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "PI_CODING_AGENT_DIR": "/accounts/pi", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [ + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_WORKTREE_ID", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": true, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > pi worktree resumed 1`] = ` +{ + "child": { + "args": [ + "--mode", + "rpc", + "--session", + "/pi-sessions/00000000-0000-4000-8000-000000000027.jsonl", + ], + "command": "/bin/pi", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "process", + "LANG": "en_US.UTF-8", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000027", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/parent/visuals", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "pi", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "PI_CODING_AGENT_DIR": "/accounts/pi", + "SHELL_ONLY": "shell", + "SHELL_UNLISTED": "shell", + }, + "envToDelete": [ + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_TERMINAL_HANDLE", + "ORCA_WORKTREE_ID", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": true, + }, + "pinnedLaunchDirectories": [], +} +`; + +exports[`native-chat launch parity > pi worktree resumed configured 1`] = ` +{ + "child": { + "args": [ + "--mode", + "rpc", + "--session", + "/pi-sessions/00000000-0000-4000-8000-000000000042.jsonl", + ], + "command": "/custom-bin/pi-custom", + "cwd": "/worktree", + "env": { + "ANTHROPIC_API_KEY": "parent-api-key", + "CLAUDE_CODE_SESSION_ID": "parent-claude-session", + "CLAUDE_CONFIG_DIR": "/parent/claude", + "CODEX_HOME": "/parent/codex", + "HOME": "/home", + "INHERITED_ONLY": "overlay", + "LANG": "C", + "ORCA_AGENT_SESSION_ID": "00000000-0000-4000-8000-000000000042", + "ORCA_AGENT_SESSION_SPAWN_TOKEN": "parity-spawn-token", + "ORCA_CHAT_VISUALS_DIR": "/parent/visuals", + "ORCA_CLI_BIN_DIR": "/orca-cli/bin", + "ORCA_CLI_COMMAND": "/orca-cli/bin/orca", + "ORCA_DATA_ACCOUNT_PROVIDER": "parent-account", + "ORCA_OPENCODE_CONFIG_DIR": "/parent/opencode-overlay", + "ORCA_STRUCTURED_SESSION": "1", + "ORCA_USER_DATA_PATH": "/user-data", + "PARITY_OVERLAY": "pi-configured", + "PATH": "/orca-cli/bin:/bin:/opt/login-shell/bin:/usr/bin:/bin", + "PI_CODING_AGENT_DIR": "/accounts/pi", + "SHELL_ONLY": "shell", + }, + "envToDelete": [ + "ORCA_AGENT_HOOK_ENDPOINT", + "ORCA_AGENT_HOOK_ENV", + "ORCA_AGENT_HOOK_PORT", + "ORCA_AGENT_HOOK_TOKEN", + "ORCA_AGENT_HOOK_TRANSPORT", + "ORCA_AGENT_HOOK_VERSION", + "ORCA_AGENT_LAUNCH", + "ORCA_AGENT_LAUNCH_TOKEN", + "ORCA_AGENT_PANE", + "ORCA_CLAUDE_AGENT_STATUS_SETTINGS", + "ORCA_PANE_KEY", + "ORCA_TAB_ID", + "ORCA_TERMINAL_HANDLE", + "ORCA_WORKTREE_ID", + ], + "supervisor": { + "closeRequest": "stdin-end", + "cwd": "/worktree", + "lifetime": "session", + "nodeEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + }, + }, + }, + "launchPosture": { + "fullAccess": true, + }, + "pinnedLaunchDirectories": [], +} +`; diff --git a/src/main/runtime/agent-session-spawn-token-process-scan.ts b/src/main/runtime/agent-session-spawn-token-process-scan.ts index dac5fe8fcc6..c402cfc93e7 100644 --- a/src/main/runtime/agent-session-spawn-token-process-scan.ts +++ b/src/main/runtime/agent-session-spawn-token-process-scan.ts @@ -9,7 +9,7 @@ */ import { readFile, readdir } from 'node:fs/promises' -import { CODEX_SPAWN_TOKEN_ENV } from '../codex/codex-structured-owner-identity' +import { ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV } from '../../shared/agent-session-caller-env' import { spawnTokenFromEnvironBlock } from './agent-session-spawn-token-readback' export type AgentSessionSpawnTokenScan = ReadonlyMap @@ -21,7 +21,7 @@ export type AgentSessionSpawnTokenScanEvidence = /** Tokens observed on this host, or null when the platform cannot answer at all. */ export async function scanAgentSessionSpawnTokenProcesses( platform: NodeJS.Platform = process.platform, - variable: string = CODEX_SPAWN_TOKEN_ENV + variable: string = ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV ): Promise { if (platform !== 'linux') { return null diff --git a/src/main/runtime/agent-session-spawn-token-readback.ts b/src/main/runtime/agent-session-spawn-token-readback.ts index 23f435fec89..7f983e797a2 100644 --- a/src/main/runtime/agent-session-spawn-token-readback.ts +++ b/src/main/runtime/agent-session-spawn-token-readback.ts @@ -7,11 +7,11 @@ import { readFile } from 'node:fs/promises' import type { AgentSessionProcessIdentity } from '../../shared/agent-session-record' -import { CODEX_SPAWN_TOKEN_ENV } from '../codex/codex-structured-owner-identity' +import { ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV } from '../../shared/agent-session-caller-env' export function spawnTokenFromEnvironBlock( block: string, - variable: string = CODEX_SPAWN_TOKEN_ENV + variable: string = ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV ): string | null { for (const entry of block.split('\0')) { if (entry.startsWith(`${variable}=`)) { diff --git a/src/main/runtime/claude-structured-session-integration.test.ts b/src/main/runtime/claude-structured-session-integration.test.ts index a2eaee178c4..dedde503349 100644 --- a/src/main/runtime/claude-structured-session-integration.test.ts +++ b/src/main/runtime/claude-structured-session-integration.test.ts @@ -12,7 +12,7 @@ import { } from '../../shared/protocol-version' import { fakeClaude } from './claude-structured-fake-connection-test-fixture' import { claudeSessionIdForOrcaSession } from '../claude/claude-structured-launch-resolution' -import { CLAUDE_SPAWN_TOKEN_ENV } from '../claude/claude-structured-owner-identity' +import { ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV } from '../../shared/agent-session-caller-env' import { attachFingerprintFields } from '../native-chat/agent-session-wire/structured-agent-session-attach' import { getStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry' import type { StructuredAgentSessionStatusSink } from '../native-chat/agent-session-wire/structured-agent-session-status-feed' @@ -245,7 +245,7 @@ beforeEach(async () => { resolveCodexCommand: () => '/usr/local/bin/codex', resolveClaudeCommand: () => '/usr/local/bin/claude', readProcessStartTime: async (pid: number) => pid * 10, - resolveClaudeLaunchEnv: () => claudeLaunchEnv, + resolveAgentLaunchEnv: (agent) => (agent === 'claude' ? claudeLaunchEnv : {}), // Hermetic: never the developer's real login shell. resolveEnvironment: async () => shellEnv, resolveShellEnvironmentPolicy: () => shellEnvironmentPolicy, @@ -582,7 +582,7 @@ describe('a structured Claude session over agentSession.*', () => { ANTHROPIC_AUTH_TOKEN: 'configured-token', ANTHROPIC_BASE_URL: 'https://gateway.example.test', CLAUDE_CONFIG_DIR: join(root, 'claude-home'), - [CLAUDE_SPAWN_TOKEN_ENV]: expect.any(String) + [ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV]: expect.any(String) }) // System auth: the user's own shell key is their sign-in, exactly as on the // terminal path, and the configured overlay still wins over it. diff --git a/src/main/runtime/orca-runtime-get-worktree-ps.ts b/src/main/runtime/orca-runtime-get-worktree-ps.ts index 8002bac661d..c7fe146b604 100644 --- a/src/main/runtime/orca-runtime-get-worktree-ps.ts +++ b/src/main/runtime/orca-runtime-get-worktree-ps.ts @@ -210,10 +210,6 @@ export class OrcaRuntimeWithGetWorktreePs extends OrcaRuntimeWithStartTuiIdleVis resolveStructuredAgentCommand('codex', this.requireStore().getSettings(), options), resolveLaunchArgs: (agent) => structuredAgentConfiguredArgs(agent, this.requireStore().getSettings()), - resolveLaunchEnvOverlay: () => - resolveTuiAgentLaunchEnv('codex', this.requireStore().getSettings().agentDefaultEnv), - resolveClaudeLaunchEnv: () => - resolveTuiAgentLaunchEnv('claude', this.requireStore().getSettings().agentDefaultEnv), // Wired only here, so a test runtime never runs a real `claude --version`. claudeCliFlags: claudeCliFlagSupport, nativeChatVisuals: { diff --git a/src/main/runtime/rpc/methods/orchestration-structured-worker-session.test.ts b/src/main/runtime/rpc/methods/orchestration-structured-worker-session.test.ts index 390148eb6c4..ad8a0b4ebe5 100644 --- a/src/main/runtime/rpc/methods/orchestration-structured-worker-session.test.ts +++ b/src/main/runtime/rpc/methods/orchestration-structured-worker-session.test.ts @@ -22,8 +22,7 @@ const { } = await import('./orchestration-structured-worker-session') const { isUnknownWorkerStartOutcome } = await import('./orchestration/worker/worker-topology') const { structuredWorkerIdentities } = await import('../../structured-worker-identity') -const { structuredSessionChildIdentityEnv } = - await import('../../structured-session-child-identity-env') +const { sealedChildEnv } = await import('../../structured-session-child-env.test-support') // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a host stub carrying only the members the worker start reaches. function installHost(location = { executionHostId: 'local', wslDistro: null as string | null }) { @@ -85,7 +84,7 @@ describe('structured worker session', () => { createSpy.mockImplementation(async (args: { envelope: { sessionId: string } }) => { // `attach` is what spawns the provider child, and the child's env is read from the registry // at spawn time. Registering afterwards ships a worker with no ORCA_TERMINAL_HANDLE. - envAtSpawn = structuredSessionChildIdentityEnv(args.envelope.sessionId, {}) + envAtSpawn = sealedChildEnv(args.envelope.sessionId) return { ok: true, value: { sessionId: args.envelope.sessionId } } }) const created = await createStructuredWorkerSession({ diff --git a/src/main/runtime/structured-agent-launch-composition.test-support.ts b/src/main/runtime/structured-agent-launch-composition.test-support.ts new file mode 100644 index 00000000000..0ee9a7a6866 --- /dev/null +++ b/src/main/runtime/structured-agent-launch-composition.test-support.ts @@ -0,0 +1,137 @@ +// Each agent's composed launch resolver over test doubles, built the way its registration builds +// it, so resolver tests exercise the shared launch path rather than a copy of it. + +import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types' +import type { AcpLaunchSpec } from '../acp/acp-launch-specs' +import { acpStructuredAgentDefinition } from '../acp/acp-structured-agent-definitions' +import { + acpStructuredLaunchPart, + type AcpStructuredLaunchPartDeps +} from '../acp/acp-structured-launch-resolution' +import { CLAUDE_STRUCTURED_AGENT } from '../claude/claude-structured-agent-definition' +import type { ClaudeEnvDeps } from '../claude/claude-structured-child-env' +import { + claudeStructuredLaunchPart, + type ClaudeStructuredLaunchPartDeps +} from '../claude/claude-structured-launch-resolution' +import { CODEX_STRUCTURED_AGENT } from '../codex/codex-structured-agent-definition' +import { + codexStructuredLaunchPart, + type CodexStructuredLaunchPartDeps +} from '../codex/codex-structured-launch-resolution' +import type { PrepareNativeChatVisuals } from '../native-chat/native-chat-visuals-delivery' +import type { StructuredAgentCommandSettings } from '../native-chat/structured-agent-command-resolution' +import { PI_RPC_AGENT } from '../pi/rpc-agent-definition' +import { piRpcLaunchPart, type PiRpcLaunchPartDeps } from '../pi/rpc-launch-resolution' +import type { StructuredAgentDefinition } from '../native-chat/agent-session-wire/structured-agent-definition' +import { + composeStructuredLaunch, + composeStructuredLaunchRead, + type StructuredAgentLaunchPartResolver, + type StructuredAgentLaunchSources +} from './structured-agent-launch-composition' + +export const TEST_SPAWN_TOKEN = 'test-spawn-token' + +export type LaunchTestSources = Pick & { + resolveWorkspacePath?: (workspaceId: string) => Promise + /** The base env; none is an empty one. */ + resolveEnvironment?: () => Promise + resolveLaunchEnv?: (agent: string) => Record + resolveCommandSettings?: () => StructuredAgentCommandSettings + prepareVisuals?: PrepareNativeChatVisuals +} + +function definedEnv(env: NodeJS.ProcessEnv): Record { + return Object.fromEntries( + Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined) + ) +} + +function launchSources(deps: LaunchTestSources): StructuredAgentLaunchSources { + const resolveEnvironment = deps.resolveEnvironment + const resolveBaseEnvironment = async () => definedEnv((await resolveEnvironment?.()) ?? {}) + return { + store: deps.store, + resolveWorkspacePath: deps.resolveWorkspacePath ?? (async (id) => `/repos/${id}`), + resolveBaseEnvironment, + resolveAgentEnvironment: async (agent) => ({ + ...(await resolveBaseEnvironment()), + ...deps.resolveLaunchEnv?.(agent) + }), + ...(deps.resolveLaunchEnv ? { resolveAgentLaunchEnv: deps.resolveLaunchEnv } : {}), + ...(deps.resolveCommandSettings ? { resolveCommandSettings: deps.resolveCommandSettings } : {}), + ...(deps.prepareVisuals ? { prepareVisuals: deps.prepareVisuals } : {}) + } +} + +/** Sealed with a spawn token, as an adapter's start calls it; with none, the unsealed read. */ +function withTestSpawnToken( + definition: StructuredAgentDefinition, + sources: StructuredAgentLaunchSources, + part: StructuredAgentLaunchPartResolver, + defaultToken: string | null +) { + const resolve = composeStructuredLaunch(definition, sources, part) + const read = composeStructuredLaunchRead(definition, sources, part) + return (input: { identity: AgentSessionJournalIdentity; spawnToken?: string | null }) => { + const spawnToken = input.spawnToken === undefined ? defaultToken : input.spawnToken + return spawnToken === null + ? read(input.identity) + : resolve({ identity: input.identity, spawnToken }) + } +} + +/** Unsealed unless a token is passed: the env an agent part produced, before the seal. */ +export function createCodexStructuredLaunchResolver( + deps: LaunchTestSources & CodexStructuredLaunchPartDeps +) { + return withTestSpawnToken( + CODEX_STRUCTURED_AGENT, + launchSources(deps), + codexStructuredLaunchPart(deps), + null + ) +} + +/** Unsealed unless a token is passed: the env an agent part produced, before the seal. */ +export function createAcpStructuredLaunchResolver( + spec: AcpLaunchSpec, + deps: LaunchTestSources & AcpStructuredLaunchPartDeps +) { + return withTestSpawnToken( + acpStructuredAgentDefinition(spec), + launchSources(deps), + acpStructuredLaunchPart(spec, deps), + null + ) +} + +/** Unsealed unless a token is passed: the env an agent part produced, before the seal. */ +export function createPiRpcLaunchResolver(deps: LaunchTestSources & PiRpcLaunchPartDeps) { + const resolve = withTestSpawnToken(PI_RPC_AGENT, launchSources(deps), piRpcLaunchPart(deps), null) + return (identity: AgentSessionJournalIdentity, spawnToken: string | null = null) => + resolve({ identity, spawnToken }) +} + +/** Sealed with `TEST_SPAWN_TOKEN` unless the test passes another: Claude's launch env has always + * carried the session's identity. `resolveEnv` is Claude's overlay; `resolveInheritedEnv`, its + * base, defaults to this process's env as a launch without one inherits it. */ +export function createClaudeStructuredLaunchResolver( + deps: Omit & + Pick & { + resolveEnv?: () => Record | undefined + } & ClaudeStructuredLaunchPartDeps +) { + const { resolveEnv, resolveInheritedEnv } = deps + return withTestSpawnToken( + CLAUDE_STRUCTURED_AGENT, + launchSources({ + ...deps, + resolveEnvironment: resolveInheritedEnv ?? (async () => process.env), + ...(resolveEnv ? { resolveLaunchEnv: () => resolveEnv() ?? {} } : {}) + }), + claudeStructuredLaunchPart(deps), + TEST_SPAWN_TOKEN + ) +} diff --git a/src/main/runtime/structured-agent-launch-composition.ts b/src/main/runtime/structured-agent-launch-composition.ts new file mode 100644 index 00000000000..f471be3a01f --- /dev/null +++ b/src/main/runtime/structured-agent-launch-composition.ts @@ -0,0 +1,210 @@ +// The one launch path every native-chat agent's resolver runs through. The shared steps live here: +// admitting the record, its launch directory, the base environment and the user's overlay, the +// chat's visuals, and sealing the child. An agent's own part never sets the directory or the +// sealed environment; it only adds what is that agent's. + +import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types' +import type { AgentSessionRecord } from '../../shared/agent-session-record' +import { isNativeLocalLocation } from '../../shared/execution-host' +import type { StructuredAgentDefinition } from '../native-chat/agent-session-wire/structured-agent-definition' +import { agentDrivesSession } from '../native-chat/agent-session-wire/structured-agent-session-provider-support' +import type { StructuredAgentCommandSettings } from '../native-chat/structured-agent-command-resolution' +import { + createNativeChatVisualsDelivery, + type NativeChatVisualsLaunch, + type PrepareNativeChatVisuals +} from '../native-chat/native-chat-visuals-delivery' +import { resolveAgentSessionLaunchDirectory } from './agent-session-launch-directory' +import type { AgentSessionRecordStore } from './agent-session-record-store' +import type { StructuredAgentAdapterContext } from './structured-agent-runtime-registrations' +import { sealStructuredSessionChild } from './structured-session-child-env' + +/** Where a launch's shared inputs come from. */ +export type StructuredAgentLaunchSources = { + store: Pick + resolveWorkspacePath: (workspaceId: string) => Promise + resolveBaseEnvironment: () => Promise> + /** The base environment with the user's overlay for `agent` laid over it. */ + resolveAgentEnvironment: (agent: string) => Promise> + /** The user's per-agent environment overlay from settings. */ + resolveAgentLaunchEnv?: (agent: string) => Record + resolveCommandSettings?: () => StructuredAgentCommandSettings + prepareVisuals?: PrepareNativeChatVisuals +} + +/** What an agent's part reads. Each read runs once, when the part first asks, so every agent keeps + * its own order: a refusal it states before reading the directory still leaves a floating record + * unpinned. */ +export type StructuredAgentLaunchBasis = { + /** Exists, belongs to this agent, runs on this host, and pins what this agent drives. */ + record: AgentSessionRecord + launchDirectory(): Promise + baseEnvironment(): Promise> + /** The user's overlay for this agent; empty when none. */ + launchEnv(): Record + /** The base environment with the user's overlay laid over it. */ + environment(): Promise> + /** Null when the chat gets no visuals. */ + visuals(): Promise + commandSettings(): StructuredAgentCommandSettings +} + +export type StructuredLaunchRequest = { + identity: AgentSessionJournalIdentity + /** The reservation's token, which the seal hands the child. */ + spawnToken: string +} + +export type StructuredAgentLaunchPart = { + launch: Omit + /** The agent's environment, before the seal names the child. */ + env: Record + /** Inherited keys this agent's child must not keep; required, so an agent states even none. */ + inheritedEnvToDelete: readonly string[] + /** See `sealStructuredSessionChild`. */ + cliRuntimeCommand?: string +} + +/** An agent's own part of a launch; it sees the session, never the spawn token. */ +export type StructuredAgentLaunchPartResolver = ( + basis: StructuredAgentLaunchBasis, + identity: AgentSessionJournalIdentity +) => Promise> + +export type ComposedStructuredLaunch = Omit & { + cwd: string + env: Record + envToDelete: string[] +} + +/** The child to spawn, sealed with the reservation's token. */ +export type StructuredAgentLaunchResolver = ( + request: StructuredLaunchRequest +) => Promise> + +/** The same launch for a read that starts no child: the agent's own env, unsealed. */ +export type StructuredAgentLaunchReader = ( + identity: AgentSessionJournalIdentity +) => Promise> + +function once(read: () => T): () => T { + let result: { value: T } | null = null + return () => (result ??= { value: read() }).value +} + +function admittedRecord( + definition: StructuredAgentDefinition, + store: StructuredAgentLaunchSources['store'], + sessionId: string +): AgentSessionRecord { + const record = store.getRecord(sessionId) + if (!record) { + throw new Error(`no durable agent-session record for ${sessionId}`) + } + if (record.provider !== definition.agent) { + throw new Error(`session ${sessionId} is a ${record.provider} session`) + } + const { location } = record + // A session pinned elsewhere belongs to that host's runtime; starting it here would put a second + // writer on the same provider session. + if (!isNativeLocalLocation(location)) { + throw new Error( + `${definition.agent} structured sessions run on the local host, not ${location.executionHostId}` + ) + } + const agents = { definition: (agent: string) => (agent === definition.agent ? definition : null) } + if (!agentDrivesSession(agents, record)) { + const pin = definition.accountHomeVariable ?? definition.accountLocatorKind + throw new Error( + `session ${sessionId}'s account home or handles are not ${definition.agent}'s: its sessions pin ${pin} and hold ${definition.handleTransport} handles` + ) + } + return record +} + +async function composedPart( + definition: StructuredAgentDefinition, + sources: StructuredAgentLaunchSources, + part: StructuredAgentLaunchPartResolver, + identity: AgentSessionJournalIdentity +): Promise & { cwd: string }> { + const record = admittedRecord(definition, sources.store, identity.sessionId) + const launchEnv = once(() => sources.resolveAgentLaunchEnv?.(definition.agent) ?? {}) + const basis: StructuredAgentLaunchBasis = { + record, + launchDirectory: once(() => resolveAgentSessionLaunchDirectory(sources, record)), + baseEnvironment: once(sources.resolveBaseEnvironment), + launchEnv, + environment: once(() => sources.resolveAgentEnvironment(definition.agent)), + visuals: once(async () => (await sources.prepareVisuals?.(record.sessionId)) ?? null), + commandSettings: once(() => sources.resolveCommandSettings?.() ?? {}) + } + const composed = await part(basis, identity) + return { ...composed, cwd: await basis.launchDirectory() } +} + +export function composeStructuredLaunch( + definition: StructuredAgentDefinition, + sources: StructuredAgentLaunchSources, + part: StructuredAgentLaunchPartResolver +): StructuredAgentLaunchResolver { + return async ({ identity, spawnToken }) => { + const { launch, cwd, env, inheritedEnvToDelete, cliRuntimeCommand } = await composedPart( + definition, + sources, + part, + identity + ) + const child = sealStructuredSessionChild({ + sessionId: identity.sessionId, + spawnToken, + env, + inheritedEnvToDelete, + ...(cliRuntimeCommand ? { cliRuntimeCommand } : {}) + }) + return { ...launch, cwd, env: child.env, envToDelete: child.envToDelete } + } +} + +export function composeStructuredLaunchRead( + definition: StructuredAgentDefinition, + sources: StructuredAgentLaunchSources, + part: StructuredAgentLaunchPartResolver +): StructuredAgentLaunchReader { + return async (identity) => { + const { launch, cwd, env, inheritedEnvToDelete } = await composedPart( + definition, + sources, + part, + identity + ) + return { ...launch, cwd, env, envToDelete: [...inheritedEnvToDelete] } + } +} + +/** The shared launch inputs of the runtime an agent's adapter is built in. */ +export function structuredAgentLaunchSources( + context: Pick +): StructuredAgentLaunchSources { + const { deps } = context + return { + store: context.store, + resolveWorkspacePath: deps.resolveWorkspacePath, + resolveBaseEnvironment: context.environment.resolveBaseEnvironment, + resolveAgentEnvironment: context.environment.resolveAgentEnvironment, + ...(deps.resolveAgentLaunchEnv ? { resolveAgentLaunchEnv: deps.resolveAgentLaunchEnv } : {}), + ...(deps.resolveAgentCommandSettings + ? { resolveCommandSettings: deps.resolveAgentCommandSettings } + : {}), + // Wired by the real hosts only, so a test runtime never loads the bundled skill. + ...(deps.nativeChatVisuals + ? { + prepareVisuals: createNativeChatVisualsDelivery({ + stateDirectory: deps.stateDirectory, + logger: deps.logger, + isEnabled: deps.nativeChatVisuals.isEnabled + }) + } + : {}) + } +} diff --git a/src/main/runtime/structured-agent-launch-parity-coverage.test.ts b/src/main/runtime/structured-agent-launch-parity-coverage.test.ts new file mode 100644 index 00000000000..c2a6615c9f5 --- /dev/null +++ b/src/main/runtime/structured-agent-launch-parity-coverage.test.ts @@ -0,0 +1,24 @@ +// A newly registered agent must get launch parity snapshots: the oracle lists its agents by hand. + +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { expect, it } from 'vitest' +import { STRUCTURED_AGENT_RUNTIME_REGISTRATIONS } from './structured-agent-runtime-registrations' + +const SNAPSHOT = new URL( + './__snapshots__/structured-agent-launch-parity.test.ts.snap', + import.meta.url +) + +it('snapshots the launch of every registered native-chat agent, and of no other', () => { + const snapshots = readFileSync(fileURLToPath(SNAPSHOT), 'utf8') + const snapshotted = new Set( + [...snapshots.matchAll(/^exports\[`native-chat launch parity > (\S+) /gm)].map( + ([, agent]) => agent + ) + ) + const registered = STRUCTURED_AGENT_RUNTIME_REGISTRATIONS.map( + ({ definition }) => definition.agent + ) + expect([...snapshotted].sort()).toEqual([...registered].sort()) +}) diff --git a/src/main/runtime/structured-agent-launch-parity.test.ts b/src/main/runtime/structured-agent-launch-parity.test.ts new file mode 100644 index 00000000000..40b498046b4 --- /dev/null +++ b/src/main/runtime/structured-agent-launch-parity.test.ts @@ -0,0 +1,664 @@ +// Parity oracle for native-chat launches: the exact child every agent spawns, read at the spawn +// boundary, with production's own dependency wiring. A refactor of the launch path must leave the +// snapshot file unchanged. + +import { chmod, mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' +import type { GlobalSettings } from '../../shared/global-settings-types' +import type * as ProcessHostModule from '@orca/process-host' +import type { ProcessSpec } from '@orca/process-host/process-spec' +import type * as AcpAdapterModule from '../acp/acp-structured-session-adapter' +import type * as ClaudeAdapterModule from '../claude/claude-structured-session-adapter' +import type * as CodexAdapterModule from '../codex/codex-structured-session-adapter' +import type * as PiAdapterModule from '../pi/rpc-session-adapter' +import type * as ManagedProviderProcessModule from '../provider-process/managed-provider-process' +import type { ProviderProcessLaunch } from '../provider-process/provider-process-launch' +import type * as StructuredAgentSessionRuntimeModule from './structured-agent-session-runtime' +import type { StructuredAgentSessionRuntimeDeps } from './structured-agent-session-runtime' + +const capture = vi.hoisted(() => { + const pendingEnvToDelete: readonly string[] = [] + const spawns: { spec: ProcessSpec; envToDelete: readonly string[] }[] = [] + const productionDeps: StructuredAgentSessionRuntimeDeps[] = [] + const launches: unknown[] = [] + /** What an adapter's launch resolved to; some of it (permission posture) acts only after spawn. */ + const record = (launch: T): T => { + launches.push(launch) + return launch + } + return { pendingEnvToDelete, spawns, productionDeps, launches, record } +}) + +/** Thrown in place of a real child: the start ends right where the process would begin. */ +class SpawnCaptured extends Error {} + +vi.mock('@orca/process-host', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + spawnProcess: (spec: ProcessSpec) => { + capture.spawns.push({ spec, envToDelete: capture.pendingEnvToDelete }) + capture.pendingEnvToDelete = [] + throw new SpawnCaptured('spawn captured') + } + } +}) +vi.mock('../provider-process/managed-provider-process', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + spawnManagedProviderProcess: ( + launch: ProviderProcessLaunch, + options: Parameters[1] + ) => { + capture.pendingEnvToDelete = launch.envToDelete ?? [] + return actual.spawnManagedProviderProcess(launch, options) + } + } +}) +vi.mock('./structured-agent-session-runtime', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + ensureStructuredAgentSessionHost: async (deps: StructuredAgentSessionRuntimeDeps) => { + capture.productionDeps.push(deps) + } + } +}) +// Each adapter as production builds it, with its resolved launch recorded on the way to the spawn. +vi.mock('../codex/codex-structured-session-adapter', async (importOriginal) => { + const actual = await importOriginal() + class RecordingAdapter extends actual.CodexStructuredSessionAdapter { + constructor(deps: ConstructorParameters[0]) { + super({ ...deps, resolveLaunch: (input) => deps.resolveLaunch(input).then(capture.record) }) + } + } + return { ...actual, CodexStructuredSessionAdapter: RecordingAdapter } +}) +vi.mock('../claude/claude-structured-session-adapter', async (importOriginal) => { + const actual = await importOriginal() + class RecordingAdapter extends actual.ClaudeStructuredSessionAdapter { + constructor(deps: ConstructorParameters[0]) { + super({ ...deps, resolveLaunch: (input) => deps.resolveLaunch(input).then(capture.record) }) + } + } + return { ...actual, ClaudeStructuredSessionAdapter: RecordingAdapter } +}) +vi.mock('../acp/acp-structured-session-adapter', async (importOriginal) => { + const actual = await importOriginal() + class RecordingAdapter extends actual.AcpStructuredSessionAdapter { + constructor(deps: ConstructorParameters[0]) { + super({ ...deps, resolveLaunch: (input) => deps.resolveLaunch(input).then(capture.record) }) + } + } + return { ...actual, AcpStructuredSessionAdapter: RecordingAdapter } +}) +vi.mock('../pi/rpc-session-adapter', async (importOriginal) => { + const actual = await importOriginal() + class RecordingAdapter extends actual.PiRpcSessionAdapter { + constructor(deps: ConstructorParameters[0]) { + super({ ...deps, resolveLaunch: (input) => deps.resolveLaunch(input).then(capture.record) }) + } + } + return { ...actual, PiRpcSessionAdapter: RecordingAdapter } +}) +vi.mock('electron', () => ({ + BrowserWindow: { fromId: vi.fn(() => null) }, + webContents: { fromId: vi.fn(() => null) }, + ipcMain: { on: vi.fn(), removeListener: vi.fn() }, + app: { getPath: vi.fn(() => '/tmp') } +})) + +import { setAppEnvironment } from '../../shared/app-environment' +import { AGENT_SESSION_RECORD_SCHEMA_VERSION } from '../../shared/agent-session-record' +import type { AgentSessionAccountHome, AgentSessionRecord } from '../../shared/agent-session-record' +import type { AgentSessionProviderHandle } from '../../shared/agent-session-provider-handle' +import { + claudeProviderHandle, + codexProviderHandle +} from '../../shared/agent-session-provider-handle-encoding' +import { FLOATING_TERMINAL_WORKTREE_ID } from '../../shared/constants' +import { + ORCA_SCRUB_SAFE_LAUNCH_ENV, + ORCA_SCRUB_SAFE_PANE_ENV +} from '../../shared/agent-hook-scrub-safe-env' +import { STRUCTURED_WORKER_HANDLE_PREFIX } from '../../shared/structured-worker-handle' +import { YOLO_TUI_AGENT_ARGS } from '../../shared/tui-agent-permissions' +import { + AGENT_HOOK_RUNTIME_ENV_KEYS, + ORCA_AGENT_SESSION_CALLER_ENV_KEYS +} from '../ipc/pty/host-env/spawn-env-keys' +import { NATIVE_CHAT_VISUALS_DIR_ENV } from '../native-chat/native-chat-visuals-delivery' +import type { StructuredAgentSessionAcquireInput } from '../native-chat/agent-session-wire/structured-agent-session-adapter' +import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink' +import type { JournalHostDatabase } from '../native-chat/agent-session-journal/journal-host-database' +import { createClaudeCliFlagSupport } from '../claude/claude-cli-flag-support' +import type { AgentSessionRecordStore } from './agent-session-record-store' +import { OrcaRuntimeService } from './orca-runtime' +import { + STRUCTURED_AGENT_RUNTIME_REGISTRATIONS, + type StructuredAgentAdapterContext, + type StructuredAgentRuntimeAdapter +} from './structured-agent-runtime-registrations' +import { createStructuredAgentEnvironmentResolvers } from './structured-agent-shell-environment' +import { createStructuredAgentSessionDispatchFollowUps } from './structured-agent-session-dispatch-followups' +import { + mintStructuredWorkerPaneKey, + structuredWorkerIdentities, + structuredWorkerProcessIncarnation +} from './structured-worker-identity' + +const AGENTS = ['claude', 'codex', 'grok', 'opencode', 'omp', 'pi'] as const +type Agent = (typeof AGENTS)[number] + +/** What each fake binary prints for `--version`: a release every launch gate accepts. */ +const VERSIONS: Record = { + claude: '2.1.120 (Claude Code)', + codex: 'codex-cli 0.130.0', + grok: '1.0.50', + opencode: '2.0.20', + omp: '17.1.0', + pi: '1.1.0' +} + +type LaunchCase = { + agent: Agent + workspace: 'worktree' | 'floating' + start: 'fresh' | 'resumed' + /** Registered as a dispatched worker, with the chat's visuals setting off. */ + worker: boolean + /** Every launch setting a user can change, set away from its default. */ + configured: boolean +} + +const CASES: readonly LaunchCase[] = AGENTS.flatMap((agent) => [ + ...(['worktree', 'floating'] as const).flatMap((workspace) => + (['fresh', 'resumed'] as const).map((start) => ({ + agent, + workspace, + start, + worker: false, + configured: false + })) + ), + { + agent, + workspace: 'worktree' as const, + start: 'fresh' as const, + worker: true, + configured: false + } +]).concat( + // Last, so every case before them keeps its session id. + AGENTS.flatMap((agent) => + (['fresh', 'resumed'] as const).map((start) => ({ + agent, + workspace: 'worktree' as const, + start, + worker: false, + configured: true + })) + ) +) + +const SPAWN_TOKEN = 'parity-spawn-token' +const WORKSPACE_ID = 'parity-worktree' + +function caseName(launch: LaunchCase): string { + const suffix = `${launch.worker ? ' worker' : ''}${launch.configured ? ' configured' : ''}` + return `${launch.agent} ${launch.workspace} ${launch.start}${suffix}` +} + +function sessionIdFor(index: number): string { + return `00000000-0000-4000-8000-${String(index + 1).padStart(12, '0')}` +} + +let root = '' +let settings: Partial = {} +let deps: StructuredAgentSessionRuntimeDeps +const records = new Map() +const pins: { sessionId: string; directory: string }[] = [] +const savedEnv = process.env + +async function writeExecutable(path: string, script: string): Promise { + await writeFile(path, `#!/bin/sh\n${script}\n`) + await chmod(path, 0o755) +} + +/** A fake install: every agent's binary answers `--version`, beside a `node` (Claude's runtime). */ +async function installFakeBinaries(bin: string): Promise { + await mkdir(bin, { recursive: true }) + await mkdir(join(root, 'custom-bin'), { recursive: true }) + for (const agent of AGENTS) { + await writeExecutable( + join(bin, agent), + `if [ "$1" = "--version" ]; then echo "${VERSIONS[agent]}"; exit 0; fi\nexit 1` + ) + // What a Command setting names instead of the agent's own binary. + await writeExecutable( + join(root, 'custom-bin', `${agent}-custom`), + `if [ "$1" = "--version" ]; then echo "${VERSIONS[agent]}"; exit 0; fi\nexit 1` + ) + } + await writeExecutable(join(bin, 'node'), 'exit 0') +} + +/** The settings every default case runs with. */ +function defaultSettings(): Partial { + return { + agentDefaultArgs: { + claude: '--add-dir /parity/extra', + codex: '-c model_reasoning_effort=high' + }, + agentDefaultEnv: Object.fromEntries(AGENTS.map((agent) => [agent, { PARITY_OVERLAY: agent }])), + nativeChatInlineVisuals: true + } satisfies Partial +} + +/** A Command setting per agent, permissions flipped from each agent's default posture, an overlay + * over inherited and shell keys, and only named shell variables inherited. */ +function configuredSettings(): Partial { + return { + agentCmdOverrides: Object.fromEntries( + AGENTS.map((agent) => [agent, join(root, 'custom-bin', `${agent}-custom`)]) + ), + agentDefaultArgs: { + claude: `${YOLO_TUI_AGENT_ARGS.claude} --add-dir /parity/configured`, + codex: `${YOLO_TUI_AGENT_ARGS.codex} -c model_reasoning_effort=low`, + grok: '', + opencode: '', + omp: '', + pi: '' + }, + agentDefaultEnv: Object.fromEntries( + AGENTS.map((agent) => [ + agent, + { PARITY_OVERLAY: `${agent}-configured`, INHERITED_ONLY: 'overlay', LANG: 'C' } + ]) + ), + nativeChatInheritShellEnvironment: false, + nativeChatShellEnvironmentVariables: ['SHELL_ONLY'] + } satisfies Partial +} + +/** The live settings the host reads for `launch`. */ +function settingsFor(launch: LaunchCase): Partial { + return { + ...defaultSettings(), + ...(launch.configured ? configuredSettings() : {}), + nativeChatInlineVisuals: !launch.worker + } +} + +/** Every pane, hook, caller, handle, visuals and auth key a parent Orca pane could hand down. */ +function inheritedEnvironment(): Record { + const env: Record = { + PATH: `${join(root, 'bin')}:/usr/bin:/bin`, + HOME: join(root, 'home'), + LANG: 'en_US.UTF-8', + INHERITED_ONLY: 'process', + ORCA_PANE_KEY: 'tab_parent:parent-pane', + ORCA_TAB_ID: 'tab_parent', + ORCA_WORKTREE_ID: 'parent-worktree', + ORCA_AGENT_LAUNCH_TOKEN: 'parent-launch-token', + [ORCA_SCRUB_SAFE_PANE_ENV]: 'parent-scrub-safe-pane', + [ORCA_SCRUB_SAFE_LAUNCH_ENV]: 'parent-scrub-safe-launch', + ORCA_TERMINAL_HANDLE: 'term_parent', + ORCA_AGENT_SESSION_SPAWN_TOKEN: 'parent-spawn-token', + [NATIVE_CHAT_VISUALS_DIR_ENV]: '/parent/visuals', + ORCA_CLI_COMMAND: '/parent/orca', + ORCA_CLI_BIN_DIR: '/parent/bin', + ORCA_USER_DATA_PATH: '/parent/user-data', + ORCA_OPENCODE_CONFIG_DIR: '/parent/opencode-overlay', + ORCA_DATA_ACCOUNT_PROVIDER: 'parent-account', + CLAUDE_CONFIG_DIR: '/parent/claude', + CLAUDE_CODE_SESSION_ID: 'parent-claude-session', + ANTHROPIC_API_KEY: 'parent-api-key', + CODEX_HOME: '/parent/codex', + NODE_OPTIONS: '--max-old-space-size=4096' + } + for (const key of [...AGENT_HOOK_RUNTIME_ENV_KEYS, ...ORCA_AGENT_SESSION_CALLER_ENV_KEYS]) { + env[key] = `parent-${key.toLowerCase()}` + } + return env +} + +function accountHomeFor(agent: Agent): AgentSessionAccountHome { + const directory = (name: string) => join(root, 'accounts', name) + switch (agent) { + case 'claude': + return { variable: 'CLAUDE_CONFIG_DIR', path: directory('claude') } + case 'codex': + return { variable: 'CODEX_HOME', path: directory('codex') } + case 'grok': + return { variable: 'GROK_HOME', path: directory('grok') } + case 'opencode': + return { kind: 'opencode', locator: { kind: 'unmanaged' } } + case 'omp': + return { variable: 'PI_CODING_AGENT_DIR', path: directory('omp') } + case 'pi': + return { variable: 'PI_CODING_AGENT_DIR', path: directory('pi') } + } +} + +async function resumedHandleFor( + agent: Agent, + sessionId: string, + launchDirectory: string +): Promise { + switch (agent) { + case 'claude': + return claudeProviderHandle(sessionId, '11111111-1111-4111-8111-111111111111') + case 'codex': + return codexProviderHandle(`thread-${sessionId}`) + case 'pi': { + const file = join(root, 'pi-sessions', `${sessionId}.jsonl`) + await mkdir(join(root, 'pi-sessions'), { recursive: true }) + await writeFile(file, `${JSON.stringify({ type: 'session', cwd: launchDirectory })}\n`) + return { transport: 'jsonl-rpc', agent: 'pi', nativeId: file } + } + case 'grok': + case 'opencode': + case 'omp': + return { transport: 'acp', agent, nativeId: `acp-session-${sessionId}` } + } +} + +async function recordFor( + launch: LaunchCase, + sessionId: string +): Promise<{ + record: AgentSessionRecord + handle: AgentSessionProviderHandle | null +}> { + const floating = launch.workspace === 'floating' + const directory = floating ? join(root, 'floating-pinned') : join(root, 'worktree') + // A resumed floating chat is held to its pin; a fresh one is pinned by its first launch. + const pinned = floating && launch.start === 'resumed' + const handle = + launch.start === 'resumed' ? await resumedHandleFor(launch.agent, sessionId, directory) : null + const record: AgentSessionRecord = { + schemaVersion: AGENT_SESSION_RECORD_SCHEMA_VERSION, + sessionId, + provider: launch.agent, + location: { + executionHostId: 'local', + wslDistro: null, + workspaceId: floating ? FLOATING_TERMINAL_WORKTREE_ID : WORKSPACE_ID, + workspaceKind: 'folder' + }, + providerHandleChain: handle + ? [ + { + linkId: 'link-1', + handle, + origin: 'created', + mintedAtFence: 1, + observedAt: 1_000 + } + ] + : [], + accountHome: accountHomeFor(launch.agent), + ...(pinned ? { launchDirectory: directory } : {}), + lease: { + sessionId, + runtimeKind: 'native', + runtimeFence: 1, + handoffStage: null, + provenHandleLinkId: null, + ownerProcess: null, + reservedSpawnToken: SPAWN_TOKEN, + leaseDeadlineAt: 0, + lastRenewedAt: 0, + handoffOperationId: null, + journalCheckpoint: null, + claimKeyId: 'parity-key', + claimStatus: 'released', + unreconciled: false, + deathEvidence: null + }, + createdAt: 1_000, + updatedAt: 1_000 + } + return { record, handle } +} + +function eventSink(): StructuredAgentSessionEventSink { + return { + appendItem: () => {}, + appendTombstone: () => {}, + publish: () => {}, + tryAppendTransition: () => ({ accepted: true }), + journalItems: () => null + } +} + +function adapterContext(): StructuredAgentAdapterContext { + const store = { + getRecord: (sessionId: string) => records.get(sessionId) ?? null, + pinLaunchDirectory: async (sessionId: string, directory: string) => { + pins.push({ sessionId, directory }) + const record = records.get(sessionId) + if (record) { + records.set(sessionId, { ...record, launchDirectory: directory }) + } + }, + listVisibleSessionIds: () => [] + } + return { + deps, + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a launch reads records and pins a floating directory; nothing else of the store runs before the spawn. + store: store as unknown as AgentSessionRecordStore, + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the journal is read only after a start succeeds, and every start here ends at its spawn. + journalDatabase: { db: null } as unknown as JournalHostDatabase, + environment: createStructuredAgentEnvironmentResolvers(deps), + deliverLifecycle: () => {}, + followUps: createStructuredAgentSessionDispatchFollowUps({ + host: () => null, + logger: deps.logger + }), + host: () => null + } +} + +const POSTURE_KEYS = new Set([ + 'permissionPolicy', + 'fullAccess', + 'permissionMode', + 'allowDangerouslySkipPermissions' +]) + +/** A launch's permission posture: Codex's thread policy and ACP's full access act after spawn. */ +function postureOf(launch: unknown): Record { + const entries = typeof launch === 'object' && launch !== null ? Object.entries(launch) : [] + const options = entries.find(([key]) => key === 'options')?.[1] + const nested = typeof options === 'object' && options !== null ? Object.entries(options) : [] + return Object.fromEntries([...entries, ...nested].filter(([key]) => POSTURE_KEYS.has(key))) +} + +/** Paths under the run's temp root and this process's own binary read the same on every run. */ +function normalized(value: string): string { + return value + .split(root) + .join('') + .split(process.execPath) + .join('') + .split(process.cwd()) + .join('') +} + +/** Keys whose value is the installed SDK's own, which a dependency bump changes. */ +const DEPENDENCY_VALUES = new Set(['CLAUDE_AGENT_SDK_VERSION']) + +function sortedEnv(env: NodeJS.ProcessEnv): Record { + return Object.fromEntries( + Object.entries(env) + .filter((entry): entry is [string, string] => entry[1] !== undefined) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, value]) => [key, DEPENDENCY_VALUES.has(key) ? '' : normalized(value)]) + ) +} + +/** The child as the provider sees it: the supervisor's wrapper unwrapped, its spec decoded. */ +function childOf(capture: { spec: ProcessSpec; envToDelete: readonly string[] }) { + const { spec } = capture + const env = { ...spec.env } + const encoded = env.ORCA_PROVIDER_SUPERVISOR_SPEC + delete env.ORCA_PROVIDER_SUPERVISOR_SPEC + delete env.ELECTRON_RUN_AS_NODE + const supervised = spec.args?.[0] === '-e' && encoded !== undefined + const argv = supervised ? (spec.args ?? []).slice((spec.args ?? []).indexOf('--') + 1) : [] + const supervisor: Record | null = supervised + ? JSON.parse(Buffer.from(encoded, 'base64').toString('utf8')) + : null + return { + command: normalized(supervised ? (argv[0] ?? '') : spec.program), + args: (supervised ? argv.slice(1) : (spec.args ?? [])).map(normalized), + cwd: normalized(spec.cwd ?? ''), + env: sortedEnv(env), + envToDelete: [...new Set(capture.envToDelete)].sort(), + supervisor: supervisor + ? { + cwd: normalized(String(supervisor.cwd)), + closeRequest: supervisor.closeRequest, + lifetime: supervisor.lifetime, + nodeEnv: supervisor.nodeEnv + } + : null + } +} + +function installAppEnvironment(): void { + setAppEnvironment({ + getPath: (name) => join(root, name === 'userData' ? 'user-data' : name), + getAppPath: () => process.cwd(), + getVersion: () => '1.0.0', + isPackaged: () => false, + getCliLauncherPath: () => join(root, 'orca-cli', 'bin', 'orca'), + onWillQuit: () => {}, + exit: () => {}, + getAppMetrics: () => [] + }) +} + +beforeAll(async () => { + root = await realpath(await mkdtemp(join(tmpdir(), 'orca-launch-parity-'))) + await installFakeBinaries(join(root, 'bin')) + // Loaded first: the SDK writes into this process's environment once, when imported. + await import('@anthropic-ai/claude-agent-sdk') + for (const directory of ['worktree', 'floating', 'floating-pinned', 'home', 'state']) { + await mkdir(join(root, directory), { recursive: true }) + } + process.env = inheritedEnvironment() + installAppEnvironment() + settings = defaultSettings() + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the host wiring reads only the store's getSettings method. + const runtime = new OrcaRuntimeService({ + getSettings: () => settings + } as never) + await runtime.ensureStructuredAgentSessionHost() + const production = capture.productionDeps.at(-1) + if (!production) { + throw new Error('the runtime installed no structured host') + } + deps = { + ...production, + stateDirectory: join(root, 'state'), + logger: { warn: () => {}, error: () => {} }, + resolveWorkspacePath: async (workspaceId) => + join(root, workspaceId === FLOATING_TERMINAL_WORKTREE_ID ? 'floating' : 'worktree'), + // The login shell's variables, read once per host like production's snapshot: its own PATH, + // one variable the configured cases name and one they do not. + resolveEnvironment: async () => ({ + PATH: `${join(root, 'bin')}:/opt/login-shell/bin:/usr/bin:/bin`, + HOME: join(root, 'home'), + LANG: 'en_US.UTF-8', + SHELL_ONLY: 'shell', + SHELL_UNLISTED: 'shell' + }), + // A version answered at once, so a slow machine cannot drop a version-gated flag. + claudeCliFlags: createClaudeCliFlagSupport({ + probe: async () => VERSIONS.claude, + keyOf: async (command, cwd) => `${command}\n${cwd}`, + budgetMs: 60_000, + now: () => 0 + }) + } +}) + +// Again after the shared setup's own per-test environment, so this app's CLI paths are the run's; +// and each case inherits the same environment whatever ran before it. +beforeEach(() => { + installAppEnvironment() + process.env = inheritedEnvironment() +}) + +afterAll(async () => { + process.env = savedEnv + await rm(root, { recursive: true, force: true }) +}) + +describe.skipIf(process.platform === 'win32')('native-chat launch parity', () => { + it.each(CASES.map((launch, index) => [caseName(launch), launch, index] as const))( + '%s', + async (_name, launch, index) => { + const registration = STRUCTURED_AGENT_RUNTIME_REGISTRATIONS.find( + ({ definition }) => definition.agent === launch.agent + ) + if (!registration) { + throw new Error(`no registration for ${launch.agent}`) + } + const sessionId = sessionIdFor(index) + const { record, handle } = await recordFor(launch, sessionId) + records.set(sessionId, record) + settings = settingsFor(launch) + const workerHandle = `${STRUCTURED_WORKER_HANDLE_PREFIX}${sessionId}` + if (launch.worker) { + structuredWorkerIdentities.register({ + handle: workerHandle, + sessionId, + agent: launch.agent, + paneKey: mintStructuredWorkerPaneKey(sessionId), + processIncarnation: structuredWorkerProcessIncarnation(sessionId), + worktreeId: WORKSPACE_ID, + hostScope: { kind: 'local', hostId: 'local' } + }) + } + const adapter: StructuredAgentRuntimeAdapter = registration.createAdapter(adapterContext()) + const input: StructuredAgentSessionAcquireInput = { + identity: { + sessionId, + workspaceId: record.location.workspaceId, + hostId: 'local', + agent: launch.agent, + providerHandle: handle + }, + fence: 1, + spawnToken: SPAWN_TOKEN, + events: eventSink() + } + capture.spawns.length = 0 + capture.launches.length = 0 + pins.length = 0 + try { + const outcome = await adapter.acquire(input).then( + () => null, + (error: unknown) => error + ) + expect(capture.spawns, String(outcome)).toHaveLength(1) + expect(capture.launches).toHaveLength(1) + const [spawn] = capture.spawns + expect({ + child: childOf(spawn!), + launchPosture: postureOf(capture.launches[0]), + pinnedLaunchDirectories: pins.map(({ directory }) => normalized(directory)) + }).toMatchSnapshot() + } finally { + structuredWorkerIdentities.forget(workerHandle) + await adapter.closeAll().catch(() => {}) + } + } + ) +}) diff --git a/src/main/runtime/structured-agent-launch-support-probe-omp.test.ts b/src/main/runtime/structured-agent-launch-support-probe-omp.test.ts index dc7fbe00db5..c759cfdeeb1 100644 --- a/src/main/runtime/structured-agent-launch-support-probe-omp.test.ts +++ b/src/main/runtime/structured-agent-launch-support-probe-omp.test.ts @@ -1,3 +1,4 @@ +import { createAcpStructuredLaunchResolver } from './structured-agent-launch-composition.test-support' import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -5,7 +6,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { GlobalSettings } from '../../shared/global-settings-types' import { agentSessionRecordFixture } from '../../shared/agent-session-record.test-fixture' import { acpLaunchSpecFor } from '../acp/acp-launch-specs' -import { createAcpStructuredLaunchResolver } from '../acp/acp-structured-launch-resolution' import { structuredAgentSupportsLaunch } from './structured-agent-launch-support' const { loginShell } = vi.hoisted(() => { diff --git a/src/main/runtime/structured-agent-launch-support-probe.test.ts b/src/main/runtime/structured-agent-launch-support-probe.test.ts index d02690261c9..02640b3bcba 100644 --- a/src/main/runtime/structured-agent-launch-support-probe.test.ts +++ b/src/main/runtime/structured-agent-launch-support-probe.test.ts @@ -1,3 +1,4 @@ +import { createAcpStructuredLaunchResolver } from './structured-agent-launch-composition.test-support' import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -5,7 +6,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { GlobalSettings } from '../../shared/global-settings-types' import { agentSessionRecordFixture } from '../../shared/agent-session-record.test-fixture' import { acpLaunchSpecFor } from '../acp/acp-launch-specs' -import { createAcpStructuredLaunchResolver } from '../acp/acp-structured-launch-resolution' import { structuredAgentSupportsLaunch } from './structured-agent-launch-support' const { loginShell } = vi.hoisted(() => { diff --git a/src/main/runtime/structured-agent-launch-support.test.ts b/src/main/runtime/structured-agent-launch-support.test.ts index 7a4c0cfbcbe..6fd15027c15 100644 --- a/src/main/runtime/structured-agent-launch-support.test.ts +++ b/src/main/runtime/structured-agent-launch-support.test.ts @@ -1,10 +1,10 @@ +import { createAcpStructuredLaunchResolver } from './structured-agent-launch-composition.test-support' import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { agentSessionRecordFixture } from '../../shared/agent-session-record.test-fixture' import { acpLaunchSpecFor } from '../acp/acp-launch-specs' -import { createAcpStructuredLaunchResolver } from '../acp/acp-structured-launch-resolution' import { structuredAgentSupportsLaunch } from './structured-agent-launch-support' const { probeAgentCliVersion, loginShell } = vi.hoisted(() => { diff --git a/src/main/runtime/structured-agent-model-catalog-discovery.test.ts b/src/main/runtime/structured-agent-model-catalog-discovery.test.ts index 9845c33166b..e405e865d1f 100644 --- a/src/main/runtime/structured-agent-model-catalog-discovery.test.ts +++ b/src/main/runtime/structured-agent-model-catalog-discovery.test.ts @@ -33,8 +33,7 @@ function context(): StructuredAgentModelCatalogContext { deps: { stateDirectory: '/state' } as StructuredAgentModelCatalogContext['deps'], environment: { resolveBaseEnvironment: unused, - resolveCodexEnvironment: unused, - resolveClaudeInheritedEnv: unused + resolveAgentEnvironment: unused } } } @@ -118,8 +117,7 @@ describe('the model catalog contract on every registration', () => { } as StructuredAgentModelCatalogContext['deps'], environment: { resolveBaseEnvironment: env, - resolveCodexEnvironment: env, - resolveClaudeInheritedEnv: env + resolveAgentEnvironment: env } }) const variables = { claude: 'CLAUDE_CONFIG_DIR', codex: 'CODEX_HOME', grok: 'GROK_HOME' } diff --git a/src/main/runtime/structured-agent-model-catalog-discovery.ts b/src/main/runtime/structured-agent-model-catalog-discovery.ts index 5bf0b86428e..1efd83c2635 100644 --- a/src/main/runtime/structured-agent-model-catalog-discovery.ts +++ b/src/main/runtime/structured-agent-model-catalog-discovery.ts @@ -7,6 +7,15 @@ import { createClaudeModelCatalogProbe } from '../claude/claude-model-catalog-pr import { createCodexModelCatalogProbe } from '../codex/codex-model-catalog-probe' import type { AgentModelCatalogDiscovery } from '../native-chat/agent-model-catalog/agent-model-catalog-discovery' import type { StructuredAgentModelCatalogContext } from './structured-agent-runtime-registrations' +import type { StructuredAgentSessionRuntimeDeps } from './structured-agent-session-runtime' + +/** Claude's overlay, laid over its base env minus an inherited config dir; absent when none. */ +export function claudeLaunchEnvOverlay( + deps: Pick +): { resolveEnv?: () => Record } { + const resolve = deps.resolveAgentLaunchEnv + return resolve ? { resolveEnv: () => resolve('claude') } : {} +} export function codexModelCatalogDiscovery({ deps, @@ -21,7 +30,7 @@ export function codexModelCatalogDiscovery({ ...(deps.prepareCodexCatalogProbeHome ? { prepareHome: deps.prepareCodexCatalogProbeHome } : {}), - resolveEnvironment: environment.resolveCodexEnvironment, + resolveEnvironment: () => environment.resolveAgentEnvironment('codex'), ...(deps.resolveCodexCommand ? { resolveCommand: deps.resolveCodexCommand } : {}) }) } @@ -36,10 +45,10 @@ export function claudeModelCatalogDiscovery({ // Claude's settings or env can pick a model other than the one its listing recommends. listingNamesConfiguredModel: false, probe: createClaudeModelCatalogProbe({ - resolveInheritedEnv: environment.resolveClaudeInheritedEnv, + resolveInheritedEnv: environment.resolveBaseEnvironment, resolveAuthPolicy: deps.resolveClaudeAuthPolicy, ...(deps.resolveClaudeCommand ? { resolveCommand: deps.resolveClaudeCommand } : {}), - ...(deps.resolveClaudeLaunchEnv ? { resolveEnv: deps.resolveClaudeLaunchEnv } : {}) + ...claudeLaunchEnvOverlay(deps) }) } } diff --git a/src/main/runtime/structured-agent-model-catalog-first-frame.test.ts b/src/main/runtime/structured-agent-model-catalog-first-frame.test.ts index 7f8eb465fb5..196923df35e 100644 --- a/src/main/runtime/structured-agent-model-catalog-first-frame.test.ts +++ b/src/main/runtime/structured-agent-model-catalog-first-frame.test.ts @@ -35,8 +35,7 @@ function context(): StructuredAgentModelCatalogContext { deps: { stateDirectory: '/state' } as StructuredAgentModelCatalogContext['deps'], environment: { resolveBaseEnvironment: unused, - resolveCodexEnvironment: unused, - resolveClaudeInheritedEnv: unused + resolveAgentEnvironment: unused } } } diff --git a/src/main/runtime/structured-agent-runtime-registrations.ts b/src/main/runtime/structured-agent-runtime-registrations.ts index ef919351db4..59afa69a92c 100644 --- a/src/main/runtime/structured-agent-runtime-registrations.ts +++ b/src/main/runtime/structured-agent-runtime-registrations.ts @@ -1,6 +1,7 @@ // Each runtime registration owns its adapter, location support and account resolution at start. -import { createCodexStructuredLaunchResolver } from '../codex/codex-structured-launch-resolution' +import { codexStructuredLaunchPart } from '../codex/codex-structured-launch-resolution' +import { claudeStructuredLaunchPart } from '../claude/claude-structured-launch-resolution' import { supportsCodexStructuredLocation } from '../codex/codex-structured-location-support' import { supportsClaudeStructuredLocation } from '../claude/claude-structured-location-support' import { supportsSupervisedProviderChildLocation } from '../provider-process/supervised-provider-child-location' @@ -23,10 +24,6 @@ import type { StructuredAgentDefinition } from '../native-chat/agent-session-wir import type { StructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-host' import { agentModelCatalogStore } from '../native-chat/agent-model-catalog/agent-model-catalog-store' import { agentSessionAttachmentStoreRoot } from '../native-chat/agent-session-attachments/agent-session-attachment-references' -import { - createNativeChatVisualsDelivery, - type PrepareNativeChatVisuals -} from '../native-chat/native-chat-visuals-delivery' import type { JournalHostDatabase } from '../native-chat/agent-session-journal/journal-host-database' import { replayJournal } from '../native-chat/agent-session-journal/journal-open' import type { AgentSessionRecordStore } from './agent-session-record-store' @@ -45,7 +42,7 @@ import { acpStructuredAgentDefinition } from '../acp/acp-structured-agent-defini import { createAcpAgentConnection } from '../acp/acp-agent-connection' import { acpLaunchVersionSupported, - createAcpStructuredLaunchResolver, + acpStructuredLaunchPart, resolveAcpLaunchCommand } from '../acp/acp-structured-launch-resolution' import { AcpStructuredSessionAdapter } from '../acp/acp-structured-session-adapter' @@ -53,9 +50,15 @@ import { PI_RPC_RUNTIME_REGISTRATION } from '../pi/rpc-runtime-registration' import type { AgentModelCatalogDiscovery } from '../native-chat/agent-model-catalog/agent-model-catalog-discovery' import { acpModelCatalogDiscovery, + claudeLaunchEnvOverlay, claudeModelCatalogDiscovery, codexModelCatalogDiscovery } from './structured-agent-model-catalog-discovery' +import { + composeStructuredLaunch, + composeStructuredLaunchRead, + structuredAgentLaunchSources +} from './structured-agent-launch-composition' /** What an agent's adapter is built from: the open store and the runtime around it. */ export type StructuredAgentAdapterContext = { @@ -124,35 +127,21 @@ export type StructuredAgentRuntimeRegistration = { ) => Promise } -function nativeChatVisualsFor(deps: StructuredAgentSessionRuntimeDeps): { - prepareVisuals?: PrepareNativeChatVisuals -} { - return deps.nativeChatVisuals - ? { - prepareVisuals: createNativeChatVisualsDelivery({ - stateDirectory: deps.stateDirectory, - logger: deps.logger, - isEnabled: deps.nativeChatVisuals.isEnabled - }) - } - : {} -} - function createCodexAdapter(context: StructuredAgentAdapterContext): StructuredAgentRuntimeAdapter { - const { deps, store, followUps, host } = context + const { deps, followUps, host } = context return new CodexStructuredSessionAdapter({ resolveAccountKind: deps.resolveCodexAccountKind, - resolveLaunch: createCodexStructuredLaunchResolver({ - store, - resolveWorkspacePath: deps.resolveWorkspacePath, - resolveEnvironment: context.environment.resolveCodexEnvironment, - resolveLaunchArgs: () => deps.resolveLaunchArgs('codex'), - ...(deps.resolveCodexPermissionPolicy - ? { resolvePermissionPolicy: deps.resolveCodexPermissionPolicy } - : {}), - ...(deps.resolveCodexCommand ? { resolveCommand: deps.resolveCodexCommand } : {}), - ...nativeChatVisualsFor(deps) - }), + resolveLaunch: composeStructuredLaunch( + CODEX_STRUCTURED_AGENT, + structuredAgentLaunchSources(context), + codexStructuredLaunchPart({ + resolveLaunchArgs: () => deps.resolveLaunchArgs('codex'), + ...(deps.resolveCodexPermissionPolicy + ? { resolvePermissionPolicy: deps.resolveCodexPermissionPolicy } + : {}), + ...(deps.resolveCodexCommand ? { resolveCommand: deps.resolveCodexCommand } : {}) + }) + ), ...(deps.openCodexConnection ? { openConnection: deps.openCodexConnection } : {}), ...(deps.readProcessStartTime ? { readProcessStartTime: deps.readProcessStartTime } : {}), modelCatalog: agentModelCatalogStore, @@ -174,20 +163,28 @@ function createClaudeAdapter( context: StructuredAgentAdapterContext ): StructuredAgentRuntimeAdapter { const { deps, store, followUps, host } = context + const overlay = claudeLaunchEnvOverlay(deps).resolveEnv return createStructuredClaudeRuntimeAdapter({ store, resolveWorkspacePath: deps.resolveWorkspacePath, + resolveLaunch: composeStructuredLaunch( + CLAUDE_STRUCTURED_AGENT, + structuredAgentLaunchSources(context), + claudeStructuredLaunchPart({ + resolveLaunchArgs: () => deps.resolveLaunchArgs('claude'), + ...(deps.resolveClaudeCommand ? { resolveCommand: deps.resolveClaudeCommand } : {}), + resolveAuthPolicy: deps.resolveClaudeAuthPolicy, + ...(deps.resolveClaudePermissionMode + ? { resolvePermissionMode: deps.resolveClaudePermissionMode } + : {}), + attachmentDirectory: agentSessionAttachmentStoreRoot(deps.stateDirectory), + ...(deps.claudeCliFlags ? { cliFlags: deps.claudeCliFlags } : {}) + }) + ), ...(deps.resolveClaudeCommand ? { resolveClaudeCommand: deps.resolveClaudeCommand } : {}), ...(deps.claudeCliFlags ? { claudeCliFlags: deps.claudeCliFlags } : {}), - ...nativeChatVisualsFor(deps), - ...(deps.resolveClaudeLaunchEnv ? { resolveClaudeLaunchEnv: deps.resolveClaudeLaunchEnv } : {}), - resolveClaudeInheritedEnv: context.environment.resolveClaudeInheritedEnv, - resolveClaudeLaunchArgs: () => deps.resolveLaunchArgs('claude'), - resolveClaudeAuthPolicy: deps.resolveClaudeAuthPolicy, - ...(deps.resolveClaudePermissionMode - ? { resolveClaudePermissionMode: deps.resolveClaudePermissionMode } - : {}), - attachmentDirectory: agentSessionAttachmentStoreRoot(deps.stateDirectory), + ...(overlay ? { resolveClaudeLaunchEnv: overlay } : {}), + resolveClaudeInheritedEnv: context.environment.resolveBaseEnvironment, onLifecycleEvent: context.deliverLifecycle, logger: deps.logger, onChildWorkEvidence: (sessionId, evidence) => @@ -224,27 +221,21 @@ function acpRegistration(spec: AcpLaunchSpec): StructuredAgentRuntimeRegistratio resolveAccountHome: ({ launchEnv }) => spec.account.resolve({ launchEnv }), modelCatalog: (context) => acpModelCatalogDiscovery(spec, context), createAdapter: (context) => { - const { deps, store, followUps } = context + const { deps, followUps } = context const readJournal = (sessionId: string) => replayJournal(context.journalDatabase.db, sessionId) + const definition = acpStructuredAgentDefinition(spec) + const sources = structuredAgentLaunchSources(context) + const part = acpStructuredLaunchPart(spec, { + readJournal, + ...(deps.resolveAgentFullAccess ? { resolveFullAccess: deps.resolveAgentFullAccess } : {}), + logger: deps.logger + }) return new AcpStructuredSessionAdapter({ spec, readJournal, - resolveLaunch: createAcpStructuredLaunchResolver(spec, { - store, - readJournal, - resolveWorkspacePath: deps.resolveWorkspacePath, - resolveEnvironment: context.environment.resolveBaseEnvironment, - ...(deps.resolveAgentLaunchEnv ? { resolveLaunchEnv: deps.resolveAgentLaunchEnv } : {}), - ...(deps.resolveAgentCommandSettings - ? { resolveCommandSettings: deps.resolveAgentCommandSettings } - : {}), - ...(deps.resolveAgentFullAccess - ? { resolveFullAccess: deps.resolveAgentFullAccess } - : {}), - ...nativeChatVisualsFor(deps), - logger: deps.logger - }), + resolveLaunch: composeStructuredLaunch(definition, sources, part), + readLaunch: composeStructuredLaunchRead(definition, sources, part), connect: (launch, options) => createAcpAgentConnection(launch, options), onChildWorkEvidence: (sessionId, evidence) => context.host()?.publishChildWorkEvidence(sessionId, evidence), diff --git a/src/main/runtime/structured-agent-session-integration.test.ts b/src/main/runtime/structured-agent-session-integration.test.ts index 49ca125b1d1..7232960bfa9 100644 --- a/src/main/runtime/structured-agent-session-integration.test.ts +++ b/src/main/runtime/structured-agent-session-integration.test.ts @@ -357,7 +357,10 @@ beforeEach(async () => { } }, resolveShellEnvironmentPolicy: () => shellEnvironmentPolicy, - resolveCodexOverrides: () => { + resolveAgentLaunchEnv: (agent): Record => { + if (agent !== 'codex') { + return {} + } codexOverrideReads += 1 return { CODEX_PROFILE: configuredCodexProfile } }, diff --git a/src/main/runtime/structured-agent-session-runtime.ts b/src/main/runtime/structured-agent-session-runtime.ts index 7566249677b..64e44ab74c5 100644 --- a/src/main/runtime/structured-agent-session-runtime.ts +++ b/src/main/runtime/structured-agent-session-runtime.ts @@ -118,9 +118,6 @@ export type StructuredAgentSessionRuntimeDeps = { readProcessStartTime?: CodexStructuredSessionAdapterDeps['readProcessStartTime'] /** Required, and asserted at install time — saved Arguments must never be silently omitted. */ resolveLaunchArgs: (provider: AgentSessionRecord['provider']) => Promise | string[] - resolveLaunchEnv?: () => Promise - resolveLaunchEnvOverlay?: () => Promise> | Record - resolveClaudeLaunchEnv?: () => Promise> | Record /** Required, and asserted at install time — an absent policy must not degrade to a guess. */ resolveClaudeAuthPolicy: () => Promise | ClaudeStructuredAuthPolicy /** The user's Agent Permissions setting for Claude; absent means prompting. */ @@ -129,14 +126,13 @@ export type StructuredAgentSessionRuntimeDeps = { resolveCodexPermissionPolicy?: () => CodexStructuredPermissionPolicy /** The same setting for a protocol-driven (ACP) agent: whether it runs with full access. */ resolveAgentFullAccess?: (agent: string) => boolean - /** The user's per-agent environment overlay, for agents with no lane-specific resolver. */ + /** The user's per-agent environment overlay from settings, laid over every agent's base env. */ resolveAgentLaunchEnv?: (agent: string) => Record /** The settings a per-agent Command override is read from, for the same agents. */ resolveAgentCommandSettings?: () => StructuredAgentCommandSettings resolveEnvironment?: () => Promise /** Which login-shell variables Codex and Claude children inherit; absent inherits all. */ resolveShellEnvironmentPolicy?: () => NativeChatShellEnvironmentPolicy - resolveCodexOverrides?: () => NodeJS.ProcessEnv /** Where every failure the runtime and its host carry on past is reported. Required, so no path * can drop one: the desktop and headless hosts both pass the trace-file logger. */ logger: StructuredAgentSessionLogger diff --git a/src/main/runtime/structured-agent-session-spawn-token-descendants.test.ts b/src/main/runtime/structured-agent-session-spawn-token-descendants.test.ts index eb4e962051e..aee2c18d58a 100644 --- a/src/main/runtime/structured-agent-session-spawn-token-descendants.test.ts +++ b/src/main/runtime/structured-agent-session-spawn-token-descendants.test.ts @@ -8,7 +8,7 @@ import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { CODEX_SPAWN_TOKEN_ENV } from '../codex/codex-structured-owner-identity' +import { ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV } from '../../shared/agent-session-caller-env' import type { AgentSessionRecordStore } from './agent-session-record-store' import { openTestAgentSessionRecordStore } from './agent-session-record-store-test-harness' import type { AgentSessionReserveRequest } from './agent-session-reservation-admission' @@ -50,7 +50,10 @@ const SESSION = 'session-alpha' const MINTED_TOKEN = 'spawn-minted-by-this-store' function environ(token: string | null): string { - return ['PATH=/usr/bin', ...(token ? [`${CODEX_SPAWN_TOKEN_ENV}=${token}`] : [])].join('\0') + return [ + 'PATH=/usr/bin', + ...(token ? [`${ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV}=${token}`] : []) + ].join('\0') } function reserveRequest(): AgentSessionReserveRequest { diff --git a/src/main/runtime/structured-agent-shell-environment.test.ts b/src/main/runtime/structured-agent-shell-environment.test.ts index ff82b19c691..ba1484d0903 100644 --- a/src/main/runtime/structured-agent-shell-environment.test.ts +++ b/src/main/runtime/structured-agent-shell-environment.test.ts @@ -68,17 +68,22 @@ describe('structuredAgentBaseEnvironment', () => { }) describe('createStructuredAgentEnvironmentResolvers', () => { - it('gives Codex and Claude the same base, with overlays on Codex only', async () => { + it("gives every agent the same base, with each agent's own overlay on it", async () => { const resolvers = createStructuredAgentEnvironmentResolvers({ resolveEnvironment: async () => ({ PATH: '/shell/bin', SHELL_ONLY: '1' }), resolveShellEnvironmentPolicy: () => INHERIT_ALL, - resolveCodexOverrides: () => ({ CODEX_PROFILE: 'p' }) + resolveAgentLaunchEnv: (agent): Record => + agent === 'codex' ? { CODEX_PROFILE: 'p' } : {} }) - expect(await resolvers.resolveClaudeInheritedEnv()).toEqual({ + expect(await resolvers.resolveBaseEnvironment()).toEqual({ PATH: '/shell/bin', SHELL_ONLY: '1' }) - expect(await resolvers.resolveCodexEnvironment()).toEqual({ + expect(await resolvers.resolveAgentEnvironment('claude')).toEqual({ + PATH: '/shell/bin', + SHELL_ONLY: '1' + }) + expect(await resolvers.resolveAgentEnvironment('codex')).toEqual({ PATH: '/shell/bin', SHELL_ONLY: '1', CODEX_PROFILE: 'p' diff --git a/src/main/runtime/structured-agent-shell-environment.ts b/src/main/runtime/structured-agent-shell-environment.ts index 12021f3615a..d851b1df868 100644 --- a/src/main/runtime/structured-agent-shell-environment.ts +++ b/src/main/runtime/structured-agent-shell-environment.ts @@ -90,23 +90,22 @@ export async function resolveHostAgentBaseEnvironment( export type StructuredAgentEnvironmentSources = { resolveEnvironment?: () => Promise resolveShellEnvironmentPolicy?: () => NativeChatShellEnvironmentPolicy - resolveLaunchEnv?: () => Promise - resolveLaunchEnvOverlay?: () => Promise> | Record - resolveCodexOverrides?: () => NodeJS.ProcessEnv + /** The user's per-agent environment overlay from settings. */ + resolveAgentLaunchEnv?: (agent: string) => Record } /** - * Both providers' child envs over one login-shell snapshot, taken once at install. + * Every agent's child env over one login-shell snapshot, taken once at install. * The policy and overlays are re-read per acquisition, so a settings change reaches * the next chat without a restart. */ export function createStructuredAgentEnvironmentResolvers( sources: StructuredAgentEnvironmentSources ): { - resolveCodexEnvironment: () => Promise - resolveClaudeInheritedEnv: () => Promise> /** The shared base every agent's child env starts from, before its own overlay. */ resolveBaseEnvironment: () => Promise> + /** The base with the user's overlay for `agent` laid over it. */ + resolveAgentEnvironment: (agent: string) => Promise> } { const shellEnvironment = (sources.resolveEnvironment ?? resolveLoginShellEnvironment)() const resolveBase = async (): Promise> => @@ -115,13 +114,10 @@ export function createStructuredAgentEnvironmentResolvers( policy: sources.resolveShellEnvironmentPolicy?.() ?? nativeChatShellEnvironmentPolicy(null) }) return { - resolveCodexEnvironment: async () => ({ + resolveBaseEnvironment: resolveBase, + resolveAgentEnvironment: async (agent) => ({ ...(await resolveBase()), - ...(await sources.resolveLaunchEnv?.()), - ...(await sources.resolveLaunchEnvOverlay?.()), - ...sources.resolveCodexOverrides?.() - }), - resolveClaudeInheritedEnv: resolveBase, - resolveBaseEnvironment: resolveBase + ...sources.resolveAgentLaunchEnv?.(agent) + }) } } diff --git a/src/main/runtime/structured-claude-attachment-grant.test.ts b/src/main/runtime/structured-claude-attachment-grant.test.ts index 8899691f1e6..9f9666b3d1c 100644 --- a/src/main/runtime/structured-claude-attachment-grant.test.ts +++ b/src/main/runtime/structured-claude-attachment-grant.test.ts @@ -1,44 +1,27 @@ // The chat attachment store reaches Claude's launch as an added directory: the runtime hands its -// root to the Claude adapter, and the adapter to the launch resolver. Drop either hand-off and -// Claude asks before reading every attached document. +// root to Claude's part of the launch its registration composes. Drop that hand-off and Claude asks +// before reading every attached document. import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import type * as LaunchResolution from '../claude/claude-structured-launch-resolution' -import type * as ClaudeRuntimeAdapter from './structured-claude-runtime-adapter' type GrantDeps = { attachmentDirectory?: string } const captured = vi.hoisted(() => { - const adapter: GrantDeps[] = [] - const resolver: GrantDeps[] = [] - return { adapter, resolver } -}) - -vi.mock('./structured-claude-runtime-adapter', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - createStructuredClaudeRuntimeAdapter: ( - deps: Parameters[0] - ) => { - captured.adapter.push(deps) - return actual.createStructuredClaudeRuntimeAdapter(deps) - } - } + const part: GrantDeps[] = [] + return { part } }) vi.mock('../claude/claude-structured-launch-resolution', async (importOriginal) => { const actual = await importOriginal() return { ...actual, - createClaudeStructuredLaunchResolver: ( - deps: Parameters[0] - ) => { - captured.resolver.push(deps) - return actual.createClaudeStructuredLaunchResolver(deps) + claudeStructuredLaunchPart: (deps: Parameters[0]) => { + captured.part.push(deps) + return actual.claudeStructuredLaunchPart(deps) } } }) @@ -61,7 +44,7 @@ afterEach(async () => { }) describe("structured Claude's read grant for chat attachments", () => { - it('hands the store root from the runtime through the adapter to the launch resolver', async () => { + it("hands the store root from the runtime to Claude's part of the launch", async () => { stateDirectory = await mkdtemp(join(tmpdir(), 'orca-attachment-grant-')) const directory = stateDirectory await ensureStructuredAgentSessionHost({ @@ -76,7 +59,6 @@ describe("structured Claude's read grant for chat attachments", () => { }) const root = agentSessionAttachmentStoreRoot(directory) - expect(captured.adapter.at(-1)?.attachmentDirectory).toBe(root) - expect(captured.resolver.at(-1)?.attachmentDirectory).toBe(root) + expect(captured.part.at(-1)?.attachmentDirectory).toBe(root) }) }) diff --git a/src/main/runtime/structured-claude-pending-rewind.test.ts b/src/main/runtime/structured-claude-pending-rewind.test.ts index 90d9b7f19a3..5aafd5711ad 100644 --- a/src/main/runtime/structured-claude-pending-rewind.test.ts +++ b/src/main/runtime/structured-claude-pending-rewind.test.ts @@ -24,6 +24,7 @@ import { } from '../native-chat/agent-session-wire/structured-agent-session-host-test-data' import type { AgentSessionRecordStore } from './agent-session-record-store' import { openTestAgentSessionRecordStore } from './agent-session-record-store-test-harness' +import { createClaudeStructuredLaunchResolver } from './structured-agent-launch-composition.test-support' import { createStructuredClaudeRuntimeAdapter } from './structured-claude-runtime-adapter' import { openTestJournalHostDatabase } from '../native-chat/agent-session-journal/journal-host-database-test-support' import { recordingStructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support' @@ -116,9 +117,12 @@ beforeEach(async () => { adapter = createStructuredClaudeRuntimeAdapter({ store, resolveWorkspacePath: async (id) => `/repos/${id}`, - resolveClaudeCommand: () => '/usr/local/bin/claude', - resolveClaudeLaunchArgs: () => [], - resolveClaudeAuthPolicy: () => ({ account: 'system' }), + resolveLaunch: createClaudeStructuredLaunchResolver({ + store, + resolveCommand: () => '/usr/local/bin/claude', + resolveLaunchArgs: () => [], + resolveAuthPolicy: () => ({ account: 'system' }) + }), openClaudeConnection: claude.openConnection, readProcessStartTime: async () => HOST_TEST_NOW, onLifecycleEvent: () => {} diff --git a/src/main/runtime/structured-claude-runtime-adapter.ts b/src/main/runtime/structured-claude-runtime-adapter.ts index 865b9e262e3..ae489b9a601 100644 --- a/src/main/runtime/structured-claude-runtime-adapter.ts +++ b/src/main/runtime/structured-claude-runtime-adapter.ts @@ -1,9 +1,6 @@ -import type { PermissionMode } from '@anthropic-ai/claude-agent-sdk' import type { AgentSessionRecord } from '../../shared/agent-session-record' import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding' import { resolveClaudeCommand } from '../codex-cli/command' -import type { ClaudeStructuredAuthPolicy } from '../claude-accounts/claude-structured-auth-policy' -import { createClaudeStructuredLaunchResolver } from '../claude/claude-structured-launch-resolution' import { ClaudeStructuredSessionAdapter, type ClaudeStructuredSessionAdapterDeps @@ -20,27 +17,19 @@ import { ClaudeAtRestCommandCatalog } from '../claude/claude-at-rest-commands' import { openClaudeStreamJsonConnection } from '../claude/claude-stream-json-connection' import type { ClaudeCliFlagSupport } from '../claude/claude-cli-flag-support' import { prewarmClaudeCliFlags } from '../claude/claude-cli-flag-prewarm' -import type { PrepareNativeChatVisuals } from '../native-chat/native-chat-visuals-delivery' export type StructuredClaudeRuntimeAdapterDeps = { store: AgentSessionRecordStore resolveWorkspacePath: (workspaceId: string) => Promise - resolveClaudeLaunchArgs: () => Promise | string[] + /** The registration's composed launch: the shared launch path with Claude's part. */ + resolveLaunch: ClaudeStructuredSessionAdapterDeps['resolveLaunch'] + /** The binary and env the startup version prewarm asks, as a launch would. */ resolveClaudeCommand?: () => string - /** Which version-gated flags a Claude CLI takes; absent never passes one. */ - claudeCliFlags?: ClaudeCliFlagSupport - /** Each chat's visuals folder and skill; absent leaves chats without visuals. */ - prepareVisuals?: PrepareNativeChatVisuals resolveClaudeLaunchEnv?: () => Promise> | Record /** The env a Claude child inherits before auth stripping; absent inherits Orca's own. */ resolveClaudeInheritedEnv?: () => Promise> - /** Managed-account auth state for a Claude launch, mirroring the terminal preflight. - * Required: an absent policy is what silently under-strips. */ - resolveClaudeAuthPolicy: () => Promise | ClaudeStructuredAuthPolicy - /** The user's Agent Permissions setting for Claude; absent means prompting. */ - resolveClaudePermissionMode?: () => Promise | PermissionMode - /** Where the host stores chat attachments; granted to the agent as a readable directory. */ - attachmentDirectory?: string + /** Which version-gated flags a Claude CLI takes; absent never passes one. */ + claudeCliFlags?: ClaudeCliFlagSupport openClaudeConnection?: ClaudeStructuredSessionAdapterDeps['openConnection'] readProcessStartTime?: ClaudeStructuredSessionAdapterDeps['readProcessStartTime'] onLifecycleEvent: (event: StructuredAgentSessionLifecycleEvent) => void @@ -105,23 +94,7 @@ export function createStructuredClaudeRuntimeAdapter( atRestCommands: new ClaudeAtRestCommandCatalog({ resolveWorkspacePath: deps.resolveWorkspacePath }), - resolveLaunch: createClaudeStructuredLaunchResolver({ - store, - resolveWorkspacePath: deps.resolveWorkspacePath, - resolveLaunchArgs: deps.resolveClaudeLaunchArgs, - resolveCommand: deps.resolveClaudeCommand ?? resolveClaudeCommand, - ...(deps.resolveClaudeLaunchEnv ? { resolveEnv: deps.resolveClaudeLaunchEnv } : {}), - ...(deps.resolveClaudeInheritedEnv - ? { resolveInheritedEnv: deps.resolveClaudeInheritedEnv } - : {}), - resolveAuthPolicy: deps.resolveClaudeAuthPolicy, - ...(deps.resolveClaudePermissionMode - ? { resolvePermissionMode: deps.resolveClaudePermissionMode } - : {}), - ...(deps.attachmentDirectory ? { attachmentDirectory: deps.attachmentDirectory } : {}), - ...(deps.claudeCliFlags ? { cliFlags: deps.claudeCliFlags } : {}), - ...(deps.prepareVisuals ? { prepareVisuals: deps.prepareVisuals } : {}) - }), + resolveLaunch: deps.resolveLaunch, persistHandle: async ({ sessionId, providerSessionId, leafUuid, fence }) => { const currentFence = store.getRecord(sessionId)?.lease.runtimeFence ?? fence const observedAt = Date.now() diff --git a/src/main/runtime/structured-codex-session-rpc-test-harness.ts b/src/main/runtime/structured-codex-session-rpc-test-harness.ts index ff9839227aa..ebb54a15061 100644 --- a/src/main/runtime/structured-codex-session-rpc-test-harness.ts +++ b/src/main/runtime/structured-codex-session-rpc-test-harness.ts @@ -227,7 +227,8 @@ export async function openStructuredCodexRpcHarness( EXAMPLE_GATEWAY_TOKEN: 'shell-exported', CODEX_HOME: '/shell/home' }), - resolveCodexOverrides: () => ({ CODEX_PROFILE: 'configured' }), + resolveAgentLaunchEnv: (agent): Record => + agent === 'codex' ? { CODEX_PROFILE: 'configured' } : {}, openCodexConnection: codex.openConnection, readProcessStartTime: async () => 1_700_000_000_000 }) diff --git a/src/main/runtime/structured-session-child-env.test-support.ts b/src/main/runtime/structured-session-child-env.test-support.ts new file mode 100644 index 00000000000..217acf17aeb --- /dev/null +++ b/src/main/runtime/structured-session-child-env.test-support.ts @@ -0,0 +1,16 @@ +import { sealStructuredSessionChild } from './structured-session-child-env' + +export const SEALED_TEST_SPAWN_TOKEN = 'spawn-1' + +/** The env the seal hands a session's child, with nothing inherited to strip. */ +export function sealedChildEnv( + sessionId: string, + env: Record = {} +): Record { + return sealStructuredSessionChild({ + sessionId, + spawnToken: SEALED_TEST_SPAWN_TOKEN, + env, + inheritedEnvToDelete: [] + }).env +} diff --git a/src/main/runtime/structured-session-child-identity-env.test.ts b/src/main/runtime/structured-session-child-env.test.ts similarity index 88% rename from src/main/runtime/structured-session-child-identity-env.test.ts rename to src/main/runtime/structured-session-child-env.test.ts index 6d773fc0852..81fe971c13d 100644 --- a/src/main/runtime/structured-session-child-identity-env.test.ts +++ b/src/main/runtime/structured-session-child-env.test.ts @@ -5,7 +5,10 @@ import { installFakeAppEnvironment } from '../../../config/scripts/vitest-host-p const shim = vi.hoisted(() => ({ ensureLinuxTerminalOrcaCliShimDir: vi.fn() })) vi.mock('../cli/linux-terminal-orca-cli-shim', () => shim) -import { structuredSessionChildIdentityEnv } from './structured-session-child-identity-env' +import { + SEALED_TEST_SPAWN_TOKEN, + sealedChildEnv +} from './structured-session-child-env.test-support' import { mintStructuredWorkerHandle, mintStructuredWorkerPaneKey, @@ -56,14 +59,14 @@ afterEach(() => { } }) -describe('structuredSessionChildIdentityEnv', () => { +describe('sealStructuredSessionChild identity', () => { it("gives an ordinary chat session its own id and this app's CLI, and no terminal identity", () => { // The id names the caller, so a bare `orca orchestration check` acts as this session instead of // guessing a terminal — every guess landed on a sibling pane, and `check` consumed its mail. pinPlatform('linux') installFakeAppEnvironment({ isPackaged: () => true, getPath: () => USER_DATA }) const childEnv = { PATH: '/usr/bin' } - const env = structuredSessionChildIdentityEnv(SESSION_ID, childEnv) + const env = sealedChildEnv(SESSION_ID, childEnv) expect(env).toEqual({ PATH: `${SHIM_DIR}:/usr/bin`, ORCA_AGENT_SESSION_ID: SESSION_ID, @@ -72,7 +75,8 @@ describe('structuredSessionChildIdentityEnv', () => { ORCA_CLI_COMMAND: join(SHIM_DIR, 'orca'), ORCA_CLI_BIN_DIR: SHIM_DIR, // The instance that minted the id, so any current CLI dials it rather than the default. - ORCA_USER_DATA_PATH: USER_DATA + ORCA_USER_DATA_PATH: USER_DATA, + ORCA_AGENT_SESSION_SPAWN_TOKEN: SEALED_TEST_SPAWN_TOKEN }) // A chat names itself by its id alone: no handle, no pane key. expect(env.ORCA_TERMINAL_HANDLE).toBeUndefined() @@ -82,7 +86,7 @@ describe('structuredSessionChildIdentityEnv', () => { it('replaces an id inherited from an Orca launched inside another session', () => { installFakeAppEnvironment({ isPackaged: () => false, getPath: () => USER_DATA }) - const env = structuredSessionChildIdentityEnv(SESSION_ID, { + const env = sealedChildEnv(SESSION_ID, { ORCA_AGENT_SESSION_ID: 'a0b1c2d3-0000-4000-8000-00000000abcd', PATH: '/usr/bin' }) @@ -94,7 +98,7 @@ describe('structuredSessionChildIdentityEnv', () => { // one identity; the handle stays for the handle-based surfaces outside orchestration. installFakeAppEnvironment({ isPackaged: () => false, getPath: () => USER_DATA }) const handle = registerWorker() - const env = structuredSessionChildIdentityEnv(SESSION_ID, { PATH: '/usr/bin' }) + const env = sealedChildEnv(SESSION_ID, { PATH: '/usr/bin' }) expect(env.ORCA_AGENT_SESSION_ID).toBe(SESSION_ID) expect(env.ORCA_TERMINAL_HANDLE).toBe(handle) }) @@ -107,7 +111,7 @@ describe('structuredSessionChildIdentityEnv', () => { getPath: () => USER_DATA, getCliLauncherPath: () => launcher }) - const env = structuredSessionChildIdentityEnv(SESSION_ID, { + const env = sealedChildEnv(SESSION_ID, { PATH: '/usr/bin', ORCA_USER_DATA_PATH: '/other/profile' }) @@ -127,7 +131,7 @@ describe('structuredSessionChildIdentityEnv', () => { // installs as `orca-ide` on Linux (stablyai/orca#7904) — and the dispatch hangs to timeout. pinPlatform('linux') installFakeAppEnvironment({ isPackaged: () => true, getPath: () => USER_DATA }) - const env = structuredSessionChildIdentityEnv(SESSION_ID, { PATH: '/usr/bin:/bin' }) + const env = sealedChildEnv(SESSION_ID, { PATH: '/usr/bin:/bin' }) expect(env.ORCA_CLI_COMMAND).toBe(join(SHIM_DIR, 'orca')) expect(env.PATH).toBe(`${SHIM_DIR}:/usr/bin:/bin`) }) @@ -135,7 +139,7 @@ describe('structuredSessionChildIdentityEnv', () => { it('on packaged macOS, through the bundled CLI dir', () => { pinPlatform('darwin') installFakeAppEnvironment({ isPackaged: () => true, getPath: () => USER_DATA }) - const env = structuredSessionChildIdentityEnv(SESSION_ID, { PATH: '/usr/bin' }) + const env = sealedChildEnv(SESSION_ID, { PATH: '/usr/bin' }) expect(env.PATH).toBe(`${join(RESOURCES, 'bin')}:/usr/bin`) expect(env.ORCA_CLI_COMMAND).toBe(join(RESOURCES, 'bin', 'orca')) }) @@ -143,7 +147,7 @@ describe('structuredSessionChildIdentityEnv', () => { it('on packaged Windows, through the bundled CLI dir under the env block spelling', () => { pinPlatform('win32') installFakeAppEnvironment({ isPackaged: () => true, getPath: () => USER_DATA }) - const env = structuredSessionChildIdentityEnv(SESSION_ID, { Path: 'C:\\Windows' }) + const env = sealedChildEnv(SESSION_ID, { Path: 'C:\\Windows' }) expect(env.Path).toBe(`${join(RESOURCES, 'bin')};C:\\Windows`) expect(env.PATH).toBeUndefined() // The native launcher: `orca.cmd` refuses message bodies cmd.exe would mangle. @@ -154,7 +158,7 @@ describe('structuredSessionChildIdentityEnv', () => { it('unpackaged, through the dev launcher dir', () => { pinPlatform('darwin') installFakeAppEnvironment({ isPackaged: () => false, getPath: () => USER_DATA }) - const env = structuredSessionChildIdentityEnv(SESSION_ID, { PATH: '/usr/bin' }) + const env = sealedChildEnv(SESSION_ID, { PATH: '/usr/bin' }) expect(env.PATH).toBe(`${join(USER_DATA, 'cli', 'bin')}:/usr/bin`) expect(env.ORCA_CLI_COMMAND).toBe(join(USER_DATA, 'cli', 'bin', 'orca-dev')) }) @@ -167,7 +171,7 @@ describe('structuredSessionChildIdentityEnv', () => { installFakeAppEnvironment({ isPackaged: () => true, getPath: () => USER_DATA }) shim.ensureLinuxTerminalOrcaCliShimDir.mockReturnValue(null) vi.spyOn(console, 'warn').mockImplementation(() => {}) - const env = structuredSessionChildIdentityEnv(SESSION_ID, { + const env = sealedChildEnv(SESSION_ID, { PATH: '/usr/bin', ORCA_CLI_COMMAND: '/Applications/Other Orca.app/Contents/Resources/bin/orca', ORCA_USER_DATA_PATH: '/data/other-orca' @@ -184,7 +188,7 @@ describe('structuredSessionChildIdentityEnv', () => { pinPlatform('linux') installFakeAppEnvironment({ isPackaged: () => true, getPath: () => USER_DATA }) registerWorker() - const env = structuredSessionChildIdentityEnv(SESSION_ID, { PATH: '/usr/bin' }) + const env = sealedChildEnv(SESSION_ID, { PATH: '/usr/bin' }) expect(env.ORCA_PANE_KEY).toBeUndefined() expect(Object.keys(env).filter((key) => key.includes('PANE'))).toEqual([]) }) @@ -204,8 +208,6 @@ describe('structuredSessionChildIdentityEnv', () => { pinPlatform('linux') installFakeAppEnvironment({ isPackaged: () => true, getPath: () => USER_DATA }) registerWorker() - expect( - structuredSessionChildIdentityEnv(SESSION_ID, { PATH: '/usr/bin' }).ORCA_CLI_COMMAND - ).not.toBe('orca-ide') + expect(sealedChildEnv(SESSION_ID, { PATH: '/usr/bin' }).ORCA_CLI_COMMAND).not.toBe('orca-ide') }) }) diff --git a/src/main/runtime/structured-session-child-identity-env.ts b/src/main/runtime/structured-session-child-env.ts similarity index 64% rename from src/main/runtime/structured-session-child-identity-env.ts rename to src/main/runtime/structured-session-child-env.ts index a7026483f7a..30c51e89155 100644 --- a/src/main/runtime/structured-session-child-identity-env.ts +++ b/src/main/runtime/structured-session-child-env.ts @@ -1,6 +1,9 @@ /** + * The last env step every native-chat child passes (`sealStructuredSessionChild`): Orca's identity + * for it, its spawn token, and the inherited keys its agent says it must not keep. + * * The orchestration identity — and the CLI reachability — a structured session's own child needs to - * speak for itself. Both providers' native launches (Claude, Codex) build their child env here. + * speak for itself. * * Every structured session carries `ORCA_AGENT_SESSION_ID`, the id Orca minted for it — never the * provider's, which rotates on `/clear`. The CLI sends it as the caller, so a bare @@ -45,15 +48,73 @@ */ import { getAppEnvironment, hasAppEnvironment } from '../../shared/app-environment' -import { ORCA_AGENT_SESSION_ID_ENV } from '../../shared/agent-session-caller-env' +import { + ORCA_AGENT_SESSION_ID_ENV, + ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV +} from '../../shared/agent-session-caller-env' +import { + ORCA_SCRUB_SAFE_LAUNCH_ENV, + ORCA_SCRUB_SAFE_PANE_ENV +} from '../../shared/agent-hook-scrub-safe-env' import { ORCA_STRUCTURED_SESSION_ENV } from '../../shared/structured-session-marker' +import { withCliRuntimeOnPath } from '../../shared/node-cli-command-resolution' import { prependOrcaCliDirToChildPath } from '../cli/orca-cli-child-path' +import { AGENT_HOOK_RUNTIME_ENV_KEYS } from '../ipc/pty/host-env/spawn-env-keys' import { resolveStructuredWorkerIdentityForSession } from './structured-worker-authority' -export function structuredSessionChildIdentityEnv( +/** + * A pane's identity in the inherited environment would let the agent's own Orca status hooks + * report for this session too; the structured session is its one status producer. + */ +export const STRUCTURED_CHILD_ENV_TO_DELETE: readonly string[] = [ + 'ORCA_PANE_KEY', + 'ORCA_TAB_ID', + 'ORCA_WORKTREE_ID', + 'ORCA_AGENT_LAUNCH_TOKEN', + ORCA_SCRUB_SAFE_PANE_ENV, + ORCA_SCRUB_SAFE_LAUNCH_ENV, + ...AGENT_HOOK_RUNTIME_ENV_KEYS +] + +export type SealedStructuredSessionChild = { + env: Record + /** Removed from what the child inherits, after `env` is laid over it. */ + envToDelete: string[] +} + +/** + * `env` named as this session's child. `inheritedEnvToDelete` is required so every agent states + * what it strips, none included; a key the seal itself names the child by is never stripped (a + * registered worker keeps its handle). `cliRuntimeCommand`: a Node CLI whose own directory goes + * first on PATH, after the identity's, so it runs on the runtime beside it. + */ +export function sealStructuredSessionChild(input: { + sessionId: string + spawnToken: string + env: Record + inheritedEnvToDelete: readonly string[] + cliRuntimeCommand?: string +}): SealedStructuredSessionChild { + const identity = childIdentity(input.sessionId, input.env) + const named = new Set([ + ORCA_AGENT_SESSION_ID_ENV, + ORCA_STRUCTURED_SESSION_ENV, + ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV, + ...(identity.workerHandle ? ['ORCA_TERMINAL_HANDLE'] : []) + ]) + const env = input.cliRuntimeCommand + ? withCliRuntimeOnPath(input.cliRuntimeCommand, identity.env, { platform: process.platform }) + : identity.env + return { + env: { ...env, [ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV]: input.spawnToken }, + envToDelete: input.inheritedEnvToDelete.filter((key) => !named.has(key)) + } +} + +function childIdentity( sessionId: string, childEnv: Record -): Record { +): { env: Record; workerHandle: string | null } { const identity = resolveStructuredWorkerIdentityForSession(sessionId, null) const env: Record = { ...childEnv, @@ -62,7 +123,7 @@ export function structuredSessionChildIdentityEnv( [ORCA_STRUCTURED_SESSION_ENV]: '1' } applyThisAppCli(env) - return env + return { env, workerHandle: identity?.handle ?? null } } /** diff --git a/src/main/runtime/structured-session-login-shell-test-harness.ts b/src/main/runtime/structured-session-login-shell-test-harness.ts index b81f6f966c1..e267a6b8c27 100644 --- a/src/main/runtime/structured-session-login-shell-test-harness.ts +++ b/src/main/runtime/structured-session-login-shell-test-harness.ts @@ -9,7 +9,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { installFakeAppEnvironment } from '../../../config/scripts/vitest-host-ports-setup' import { runProcess } from '@orca/process-host' -import { structuredSessionChildIdentityEnv } from './structured-session-child-identity-env' +import { sealedChildEnv } from './structured-session-child-env.test-support' const SESSION_ID = 'f7a1c0de-1111-4222-8333-444455556666' @@ -40,7 +40,7 @@ export function createLoginShellHarness(): LoginShellHarness { writeFileSync(join(home, '.zprofile'), prependGlobal) writeFileSync(join(home, '.bash_profile'), prependGlobal) installFakeAppEnvironment({ isPackaged: () => false, getPath: () => userData }) - const env = structuredSessionChildIdentityEnv(SESSION_ID, { HOME: home, PATH: '/usr/bin:/bin' }) + const env = sealedChildEnv(SESSION_ID, { HOME: home, PATH: '/usr/bin:/bin' }) return { run: async (spec) => (await runProcess({ ...spec, env })).stdout, dispose: () => rmSync(root, { recursive: true, force: true }) diff --git a/src/main/runtime/structured-worker-cleared-session.test.ts b/src/main/runtime/structured-worker-cleared-session.test.ts index c4d07df01fb..76029954dec 100644 --- a/src/main/runtime/structured-worker-cleared-session.test.ts +++ b/src/main/runtime/structured-worker-cleared-session.test.ts @@ -36,8 +36,7 @@ const { listAddressableStructuredWorkers } = await import('./orchestration/structured-worker-group-addressing') const { readStructuredLineageJournalPage, STRUCTURED_JOURNAL_PAGE_LIMIT } = await import('./orchestration/structured-worker-journal-page') -const { structuredSessionChildIdentityEnv } = - await import('./structured-session-child-identity-env') +const { sealedChildEnv } = await import('./structured-session-child-env.test-support') const { foundAgentSessionRecord } = await import('./agent-session-record-founding') const { applyAgentSessionRestartAdjudication } = await import('./agent-session-restart-lease-transitions') @@ -489,9 +488,7 @@ describe.each(['live', 'at-rest'] as const)( it("gives the successor's child the worker's handle", () => { const identity = registerWorker() - expect(structuredSessionChildIdentityEnv(SUCCESSOR, {}).ORCA_TERMINAL_HANDLE).toBe( - identity.handle - ) + expect(sealedChildEnv(SUCCESSOR).ORCA_TERMINAL_HANDLE).toBe(identity.handle) }) it("records the user's takeover when they type into the successor", () => { diff --git a/src/main/runtime/structured-worker-identity.test.ts b/src/main/runtime/structured-worker-identity.test.ts index 17ba897de47..1df016f3caf 100644 --- a/src/main/runtime/structured-worker-identity.test.ts +++ b/src/main/runtime/structured-worker-identity.test.ts @@ -5,7 +5,7 @@ import { structuredAgentSessionTabId } from '../../shared/structured-agent-session-projection' import { selectExactWorkerProviderSession } from './orchestration/worker-provider-session' -import { structuredSessionChildIdentityEnv } from './structured-session-child-identity-env' +import { sealedChildEnv } from './structured-session-child-env.test-support' import { StructuredWorkerIdentityRegistry, isStructuredWorkerHandle, @@ -300,7 +300,7 @@ describe('structured workers stay outside the PTY-only fail-closed paths', () => hostScope: { kind: 'local', hostId: 'local' } }) try { - const env = structuredSessionChildIdentityEnv(SESSION_ID, {}) + const env = sealedChildEnv(SESSION_ID) // Registered, so the worker's handle is present; without it the pane-key assertion would pass // for the wrong reason. expect(env.ORCA_TERMINAL_HANDLE).toBe(handle) diff --git a/src/shared/agent-session-caller-env.ts b/src/shared/agent-session-caller-env.ts index ca3d286614b..5fa753f51bf 100644 --- a/src/shared/agent-session-caller-env.ts +++ b/src/shared/agent-session-caller-env.ts @@ -12,6 +12,10 @@ import { isStructuredWorkerHandle } from './structured-worker-handle' export const ORCA_AGENT_SESSION_ID_ENV = 'ORCA_AGENT_SESSION_ID' +/** The spawn token a structured session's child echoes, so the owner probe can tell a live child + * of that reservation from a same-pid stranger. */ +export const ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV = 'ORCA_AGENT_SESSION_SPAWN_TOKEN' + export function readInjectedAgentSessionId( env: Readonly> = process.env ): string | undefined {