feat(orchestration): inject the Orca session id into structured children and let the CLI act as it

Every structured session's child (native Claude, native Codex, and the terminal
view) carries ORCA_AGENT_SESSION_ID and reaches the Orca CLI. The CLI sends the id
in the orchestration envelope; when present it is the caller, and a caller flag
naming anyone else is refused before any request. The id is stripped from
inherited PTY env and from the SSH host-CLI passthrough, and crosses into WSL so
the host can refuse the cross-host claim.
This commit is contained in:
Brennan Benson
2026-09-25 18:41:13 -07:00
parent 1c2cf120e3
commit 933b4d60fa
29 changed files with 234 additions and 90 deletions
@@ -0,0 +1,8 @@
/**
* Why: a structured chat exports its own orchestration caller identity to every child, including a
* test runner it launches. Inherited, it would decide which CLI identity branch a test exercises
* depending on who ran the suite; suites that need one set it themselves.
*/
for (const name of ['ORCA_AGENT_SESSION_ID', 'ORCA_STRUCTURED_SESSION']) {
delete process.env[name]
}
+1
View File
@@ -4,6 +4,7 @@
"../electron.vite.config.*",
"./build-plugins/**/*",
"./scripts/vitest-host-ports-setup.ts",
"./scripts/vitest-caller-identity-env-setup.ts",
"../src/main/**/*",
"../src/renderer/src/lib/skill-freshness-display-status.ts",
"../src/preload/**/*",
+2 -1
View File
@@ -26,7 +26,8 @@ export default defineConfig({
setupFiles: [
resolve('config/scripts/happy-dom-offscreen-canvas.ts'),
resolve('config/scripts/happy-dom-mutation-observer-retention.ts'),
resolve('config/scripts/vitest-host-ports-setup.ts')
resolve('config/scripts/vitest-host-ports-setup.ts'),
resolve('config/scripts/vitest-caller-identity-env-setup.ts')
],
include: [
'src/**/*.test.ts',
@@ -13,7 +13,7 @@ import { startCheckKeepalive } from './check-keepalive'
import { callOrchestrationMutation } from './mutation-request'
import { getOptionalPositiveIntegerValueFlag } from './numeric-flags'
import { flushOrchestrationStdout, resolveCompatibilityCliCommand } from './runtime-compatibility'
import { resolveOrchestrationTerminalHandle } from './terminal-identity'
import { orchestrationCallerLabel, resolveOrchestrationTerminalHandle } from './terminal-identity'
type CheckResult = {
messages: MessageSummary[]
@@ -41,12 +41,16 @@ export const ORCHESTRATION_CHECK_HANDLER: Record<string, CommandHandler> = {
const timeoutMs = getOptionalPositiveIntegerValueFlag(flags, 'timeout-ms')
const explicitTerminal = getOptionalStringFlag(flags, 'terminal')
const terminal = await resolveOrchestrationTerminalHandle(flags, cwd, client, 'terminal')
// Why: a session names itself by its id alone; a terminal view's pane is not its identity.
const paneKey =
explicitTerminal || terminal === undefined ? undefined : process.env.ORCA_PANE_KEY
const callerLabel = orchestrationCallerLabel(terminal)
const stopKeepalive = wait ? startCheckKeepalive(timeoutMs) : null
let result: Awaited<ReturnType<typeof client.call<CheckResult>>>
try {
result = await callOrchestrationMutation<CheckResult>(client, flags, 'orchestration.check', {
terminal,
terminalPaneKey: explicitTerminal ? undefined : process.env.ORCA_PANE_KEY || undefined,
terminalPaneKey: paneKey || undefined,
// Why: old runtimes degrade peek to non-consuming all mode instead of destructive mark-read.
unread: flags.has('unread') ? true : peek ? false : undefined,
peek: peek ? true : undefined,
@@ -68,9 +72,9 @@ export const ORCHESTRATION_CHECK_HANDLER: Record<string, CommandHandler> = {
}
result = {
...result,
result: prepareOrchestrationCheckOutput(result.result, terminal, flags.has('format'))
result: prepareOrchestrationCheckOutput(result.result, callerLabel, flags.has('format'))
}
printResult(result, json, (value) => formatOrchestrationCheckText(value, terminal))
printResult(result, json, (value) => formatOrchestrationCheckText(value, callerLabel))
const compatibilityAck = result.result.legacyCompatibility?.ackMessageIds
if (compatibilityAck && compatibilityAck.length > 0) {
await flushOrchestrationStdout()
@@ -2,6 +2,7 @@ import type { CommandHandler } from '../../dispatch'
import { printResult } from '../../format'
import { getOptionalStringFlag, getRequiredStringFlag } from '../../flags'
import { RuntimeClientError } from '../../runtime-client'
import { readInjectedAgentSessionId } from '../../../shared/agent-session-caller-env'
import { requireWorkerDoneSettlement } from '../orchestration-worker-settlement'
import { getOptionalStructuredMessagePayload } from './message-payload'
import { callOrchestrationMutation } from './mutation-request'
@@ -75,7 +76,8 @@ export const ORCHESTRATION_SEND_HANDLER: Record<string, CommandHandler> = {
if (
(type === 'worker_done' || type === 'heartbeat') &&
!getOptionalStringFlag(flags, 'from') &&
!process.env.ORCA_TERMINAL_HANDLE
!process.env.ORCA_TERMINAL_HANDLE &&
!readInjectedAgentSessionId()
) {
// Why: focus isn't lifecycle authority — an identity-less subprocess must fail closed rather than guess the worker.
throwNoActiveSenderTerminal()
@@ -94,7 +96,8 @@ export const ORCHESTRATION_SEND_HANDLER: Record<string, CommandHandler> = {
threadId: getOptionalStringFlag(flags, 'thread-id'),
payload: getOptionalStructuredMessagePayload(flags),
// Why: pane key is the remint-stable sender identity the runtime verifies lifecycle ownership against; older runtimes strip it.
senderPaneKey: process.env.ORCA_PANE_KEY || undefined,
// A session names itself by its id alone.
senderPaneKey: from === undefined ? undefined : process.env.ORCA_PANE_KEY || undefined,
waitForLifecycleSettlement: type === 'worker_done' ? true : undefined,
devMode: isDevCliInvocation()
}
@@ -103,8 +103,8 @@ export const ORCHESTRATION_QUESTION_HANDLER: Record<string, CommandHandler> = {
resolveOrchestrationCliExecutable(),
'orchestration',
'ask',
'--from',
from,
// A session's resume is flagless: its injected id names it again.
...(from ? ['--from', from] : []),
...(dispatchCapability ? ['--dispatch-capability', dispatchCapability] : []),
'--resume',
messageId,
@@ -39,7 +39,9 @@ export const ORCHESTRATION_RUN_HANDLERS: Record<string, CommandHandler> = {
run: { id: string; objective: string } | null
}>('orchestration.runCurrent', { from })
printResult(result, json, (r) =>
r.run ? `${r.run.id} ${r.run.objective}` : 'No Run is bound to this terminal.'
r.run
? `${r.run.id} ${r.run.objective}`
: `No Run is bound to this ${from === undefined ? 'session' : 'terminal'}.`
)
},
@@ -3,14 +3,25 @@ import { getOptionalStringFlag } from '../../flags'
import { RuntimeClientError } from '../../runtime-client'
import { getTerminalHandle } from '../../selectors'
import { isStructuredSessionWithoutIdentity } from '../../../shared/structured-session-marker'
import { readInjectedAgentSessionId } from '../../../shared/agent-session-caller-env'
import { normalizeOrchestrationActor } from '../../../shared/orchestration-actor'
import { isStructuredWorkerHandle } from '../../../shared/structured-worker-handle'
/**
* The caller's terminal handle, or `undefined` when an injected agent session id names the caller:
* the orchestration envelope carries that id and the host binds the caller param to it, so nothing
* is resolved or guessed here.
*/
export async function resolveOrchestrationTerminalHandle(
flags: Map<string, string | boolean>,
cwd: string,
client: RuntimeClient,
flagName: 'from' | 'terminal',
options: { validateEnvHandle?: boolean } = {}
): Promise<string> {
): Promise<string | undefined> {
if (resolveInjectedSessionCaller(flags, flagName)) {
return undefined
}
const explicit = getOptionalStringFlag(flags, flagName)
if (explicit) {
return explicit
@@ -157,11 +168,51 @@ function getClientErrorMessage(err: unknown): string | undefined {
return typeof message === 'string' ? message : undefined
}
/**
* The injected session id when this command runs as an agent session. The id wins over every other
* identity this process carries; a caller flag may restate that same session but never name
* another, and a conflicting one is refused here, before any request is sent.
*/
export function resolveInjectedSessionCaller(
flags: Map<string, string | boolean>,
flagName: 'from' | 'terminal'
): string | undefined {
const sessionId = readInjectedAgentSessionId()
if (!sessionId) {
return undefined
}
const declared = getOptionalStringFlag(flags, flagName)
if (declared !== undefined && !namesInjectedSession(declared, sessionId)) {
throw new RuntimeClientError(
'consumer_fenced',
`This command runs as agent session ${sessionId}, so --${flagName} ${declared} would act as a ` +
`different caller. Drop --${flagName}: this session's orchestration commands already act as ` +
`session:${sessionId}. No request was sent.`
)
}
return sessionId
}
/** The session's own spellings, plus the handle a structured worker session was minted. */
function namesInjectedSession(value: string, sessionId: string): boolean {
if (normalizeOrchestrationActor(value)?.id === sessionId) {
return true
}
const ownHandle = process.env.ORCA_TERMINAL_HANDLE
return isStructuredWorkerHandle(ownHandle) && value === ownHandle
}
/** How check output names its caller: the handle, or the session's address. */
export function orchestrationCallerLabel(handle: string | undefined): string {
const sessionId = handle === undefined ? readInjectedAgentSessionId() : undefined
return handle ?? (sessionId ? `session:${sessionId}` : 'unknown')
}
export async function resolveCoordinatorTerminalHandle(
flags: Map<string, string | boolean>,
cwd: string,
client: RuntimeClient
): Promise<string> {
): Promise<string | undefined> {
return await resolveOrchestrationTerminalHandle(flags, cwd, client, 'from', {
validateEnvHandle: true
})
@@ -1,6 +1,9 @@
import { getOptionalStringFlag } from '../../flags'
import type { RuntimeClient } from '../../runtime-client'
import { resolveOrchestrationTerminalHandle } from './terminal-identity'
import {
resolveInjectedSessionCaller,
resolveOrchestrationTerminalHandle
} from './terminal-identity'
/** Which Run `worker-list` enumerated, and why. Additive: old readers ignore it. */
export type WorkerListRunScope = { run?: string; source: 'flag' | 'bound' | 'all' }
@@ -20,6 +23,9 @@ export async function resolveWorkerListRunScope(
if (explicit) {
return { run: explicit, source: 'flag' }
}
// Outside the catch: a --terminal naming someone other than this session is refused, never
// widened into an unscoped listing.
resolveInjectedSessionCaller(flags, 'terminal')
try {
const terminal = await resolveOrchestrationTerminalHandle(flags, cwd, client, 'terminal')
const current = await client.call<{ run: { id: string } | null }>('orchestration.runCurrent', {
@@ -1,12 +1,17 @@
import { randomUUID } from 'node:crypto'
import { readInjectedAgentSessionId } from '../../shared/agent-session-caller-env'
import { readOrchestrationCompatibilityEvidence } from '../../shared/orchestration-compatibility-evidence'
import type { RuntimeOrchestrationEnvelope } from '../../shared/runtime-rpc-envelope'
export function createOrchestrationCompatibilityEnvelope(
env: NodeJS.ProcessEnv
): RuntimeOrchestrationEnvelope {
const evidence = readOrchestrationCompatibilityEvidence(env)
// Read here, from this CLI's own environment only: the SSH paths build evidence from a remote
// shell's environment, where a session id could never name a session on this host.
const agentSessionId = readInjectedAgentSessionId(env)
return {
compatibilityInvocationId: randomUUID(),
orchestrationCompatibilityEvidence: readOrchestrationCompatibilityEvidence(env)
orchestrationCompatibilityEvidence: agentSessionId ? { ...evidence, agentSessionId } : evidence
}
}
@@ -8,7 +8,7 @@ import type { AgentSessionJournalIdentity } from '../../shared/agent-session-jou
import { agentSessionProviderHandleChainHead } from '../../shared/agent-session-provider-handle'
import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
import { withCliRuntimeOnPath } from '../../shared/node-cli-command-resolution'
import { structuredWorkerChildIdentityEnv } from '../runtime/structured-worker-child-identity-env'
import { structuredSessionChildIdentityEnv } from '../runtime/structured-session-child-identity-env'
import {
CLAUDE_AUTH_ENV_CONFLICT_MESSAGE,
CLAUDE_AUTH_SWITCH_IN_PROGRESS_MESSAGE,
@@ -283,9 +283,8 @@ export function createClaudeStructuredLaunchResolver(
(await deps.resolvePermissionMode?.()) ?? 'default'
)
const { command, env } = await resolveClaudeStructuredInvocation(deps, (base) =>
// Only a dispatched structured worker gets the orchestration identity and the Orca CLI on
// PATH; an ordinary chat session's env passes through untouched.
structuredWorkerChildIdentityEnv(record.sessionId, {
// Every structured session speaks orchestration as itself: its injected id and the Orca CLI.
structuredSessionChildIdentityEnv(record.sessionId, {
...base,
// The turn translator relies on Claude's authoritative idle frame when no result arrives.
[CLAUDE_SESSION_STATE_EVENTS_ENV]: '1'
@@ -1,6 +1,6 @@
import type { CodexStructuredLaunch } from './codex-structured-session-state'
import { CODEX_SPAWN_TOKEN_ENV } from './codex-structured-owner-identity'
import { structuredWorkerChildIdentityEnv } from '../runtime/structured-worker-child-identity-env'
import { structuredSessionChildIdentityEnv } from '../runtime/structured-session-child-identity-env'
export function buildCodexStructuredChildEnvironment(
launch: CodexStructuredLaunch,
@@ -8,9 +8,8 @@ export function buildCodexStructuredChildEnvironment(
sessionId: string
): Record<string, string> {
return {
// Only a dispatched structured worker gets the orchestration identity and the Orca CLI on
// PATH; an ordinary chat session's env passes through untouched.
...structuredWorkerChildIdentityEnv(sessionId, {
// Every structured session speaks orchestration as itself: its injected id and the Orca CLI.
...structuredSessionChildIdentityEnv(sessionId, {
...launch.env,
...(launch.codexHome ? { CODEX_HOME: launch.codexHome } : {})
}),
+10 -4
View File
@@ -8,7 +8,11 @@ import {
type PiAgentKind
} from '../../../../shared/pi-agent-kind'
import { readSessionShellStartupEnvVar } from '../../../pty/shell-startup-env'
import { AGENT_HOOK_RUNTIME_ENV_KEYS, CLAUDE_CHILD_SESSION_STAMP_ENV_KEYS } from './spawn-env-keys'
import {
AGENT_HOOK_RUNTIME_ENV_KEYS,
CLAUDE_CHILD_SESSION_STAMP_ENV_KEYS,
ORCA_AGENT_SESSION_CALLER_ENV_KEYS
} from './spawn-env-keys'
export function readEnvWithProcessFallback(
baseEnv: Record<string, string>,
@@ -133,13 +137,15 @@ export function getInheritedAgentHookEnvKeysToDelete(
return AGENT_HOOK_RUNTIME_ENV_KEYS.filter((key) => env[key] === undefined)
}
export function getInheritedClaudeSessionStampEnvKeysToDelete(
export function getInheritedAgentSessionStampEnvKeysToDelete(
spawnEnv: Record<string, string> | undefined
): string[] {
const env = spawnEnv ?? {}
// Why: strip only values inherited from the pty host; a caller that explicitly
// provides a stamp (deliberately spawning a nested Claude child) keeps it.
return CLAUDE_CHILD_SESSION_STAMP_ENV_KEYS.filter((key) => env[key] === undefined)
// provides a stamp (a nested Claude child, a structured session's terminal view) keeps it.
return [...CLAUDE_CHILD_SESSION_STAMP_ENV_KEYS, ...ORCA_AGENT_SESSION_CALLER_ENV_KEYS].filter(
(key) => env[key] === undefined
)
}
// Why: a nested terminal can inherit prior OpenCode/Pi/OMP overlay env; restore the user's recorded source dir, else strip only Orca-owned values.
@@ -1,3 +1,6 @@
import { ORCA_AGENT_SESSION_ID_ENV } from '../../../../shared/agent-session-caller-env'
import { ORCA_STRUCTURED_SESSION_ENV } from '../../../../shared/structured-session-marker'
export const AGENT_HOOK_RUNTIME_ENV_KEYS = [
'ORCA_AGENT_HOOK_PORT',
'ORCA_AGENT_HOOK_TOKEN',
@@ -15,3 +18,9 @@ export const CLAUDE_CHILD_SESSION_STAMP_ENV_KEYS = [
'CLAUDE_CODE_SESSION_ID',
'CLAUDE_CODE_BRIDGE_SESSION_ID'
] as const
// Why: Orca writes these only into a structured session's own spawn, so an inherited value means a pty host launched from inside one — every pane would claim that session as its orchestration caller.
export const ORCA_AGENT_SESSION_CALLER_ENV_KEYS = [
ORCA_AGENT_SESSION_ID_ENV,
ORCA_STRUCTURED_SESSION_ENV
] as const
+2 -2
View File
@@ -7,7 +7,7 @@ import {
mergePtyEnvDeletions,
removeCodexHomeDeletionRequests,
getInheritedAgentHookEnvKeysToDelete,
getInheritedClaudeSessionStampEnvKeysToDelete
getInheritedAgentSessionStampEnvKeysToDelete
} from '../host-env/pi-agent'
import { promoteAgentTeamsShimPath, deleteRequestedEnvKeys } from '../host-env/path'
import { beginPtySpawnForWorktree } from '../host-env/fresh-spawn-routing'
@@ -43,7 +43,7 @@ export async function buildPtyIpcSpawnOptions(
// Why: disable old hosts without removing ORCA_REAL_* while their Windows shim remains on PATH.
ctx.isDaemonHostSpawn || args.connectionId ? LEGACY_TERMINAL_SHIM_REMOTE_ENV_KEYS : [],
ctx.isDaemonHostSpawn ? getInheritedAgentHookEnvKeysToDelete(ctx.spawnEnv) : [],
getInheritedClaudeSessionStampEnvKeysToDelete(ctx.spawnEnv),
getInheritedAgentSessionStampEnvKeysToDelete(ctx.spawnEnv),
ctx.skipCodexHomeEnv ? CODEX_HOME_ENV_KEYS : [],
// Why: the persistent daemon compares its own merged CODEX_HOME pair;
// main cannot safely decide ownership for a process it may not parent.
+2 -2
View File
@@ -9,7 +9,7 @@ import {
mergePtyEnvDeletions,
removeCodexHomeDeletionRequests,
getInheritedAgentHookEnvKeysToDelete,
getInheritedClaudeSessionStampEnvKeysToDelete
getInheritedAgentSessionStampEnvKeysToDelete
} from '../host-env/pi-agent'
import { promoteAgentTeamsShimPath, deleteRequestedEnvKeys } from '../host-env/path'
import {
@@ -73,7 +73,7 @@ export async function buildRuntimePtySpawnOptions(
ctx.isDaemonHostSpawn || args.connectionId ? LEGACY_TERMINAL_SHIM_REMOTE_ENV_KEYS : [],
ctx.isDaemonHostSpawn ? getInheritedAgentHookEnvKeysToDelete(ctx.env) : [],
// Why: ungated, unlike the agent-hook keys — the local provider and the relay host also spread their own process.env into every spawn.
getInheritedClaudeSessionStampEnvKeysToDelete(ctx.env)
getInheritedAgentSessionStampEnvKeysToDelete(ctx.env)
)
if (ctx.skipCodexHomeEnv) {
ctx.spawnOptions.envToDelete = mergePtyEnvDeletions(
+4
View File
@@ -9,6 +9,7 @@ import {
SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV
} from '../../shared/setup-agent-sequencing'
import { getShellReadyWrapperRoot } from '../providers/local-pty-shell-ready-wrapper-root'
import { ORCA_AGENT_SESSION_ID_ENV } from '../../shared/agent-session-caller-env'
import { ORCA_IMAGE_PROTOCOL_ENV } from '../../shared/terminal-image-protocol'
const WSLENV_ENTRY_SEPARATOR = ':'
@@ -74,6 +75,9 @@ export function addOrcaWslInteropEnv(env: Record<string, string>): void {
// Why: wsl.exe only imports selected Windows env vars, so WSL needs the wrapper root, pane identity, and hook/OMP coordinates at start.
const passthroughEntries = [
'ORCA_TERMINAL_HANDLE/u',
// Why: a structured session's terminal view in a WSL shell must still present its id, so the
// host refuses the cross-host claim instead of the pane handle silently becoming its caller.
`${ORCA_AGENT_SESSION_ID_ENV}/u`,
'ORCA_USER_DATA_PATH/p',
// Why /p: the guest reads the content-addressed wrapper tree through /mnt/c,
// and it cannot derive the hash segment from ORCA_USER_DATA_PATH alone.
@@ -19,8 +19,8 @@ const {
} = await import('./orchestration-structured-worker-session')
const { isUnknownWorkerStartOutcome } = await import('./orchestration/worker/worker-topology')
const { structuredWorkerIdentities } = await import('../../structured-worker-identity')
const { structuredWorkerChildIdentityEnv } =
await import('../../structured-worker-child-identity-env')
const { structuredSessionChildIdentityEnv } =
await import('../../structured-session-child-identity-env')
function installHost() {
const hold = vi.fn(async () => {})
@@ -83,7 +83,7 @@ describe('structured worker session hold', () => {
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 = structuredWorkerChildIdentityEnv(args.envelope.sessionId, {})
envAtSpawn = structuredSessionChildIdentityEnv(args.envelope.sessionId, {})
return { ok: true, value: { sessionId: args.envelope.sessionId } }
})
const created = await createStructuredWorkerSession({
@@ -43,7 +43,6 @@ const PARTY_NAMING_FIELDS = ['to', 'from', 'terminal', 'callerTerminalHandle'] a
const NAMES_NO_RESOLVED_PARTY: Readonly<Record<string, string>> = {
'orchestration.run from': 'retired; refused before any handler',
'orchestration.runShow from': 'reads a Run by id; `from` is unused',
'orchestration.dispatchShow from': '`from` only fills the preview preamble text',
'orchestration.workerStart terminal': 'adopts an existing PTY pane, which a session never has',
'orchestration.federationAttachStart terminal': 'names the remote worker terminal',
'orchestration.workerTerminalUserInput terminal': 'names the worker terminal'
@@ -59,6 +58,7 @@ const MINIMAL_PARAMS: Readonly<Record<string, Record<string, unknown>>> = {
'orchestration.reply': { id: 'msg_missing', body: 'b' },
'orchestration.ask': { question: 'q', to: 'term_worker' },
'orchestration.dispatch': { task: 'task_missing', to: 'term_worker' },
'orchestration.dispatchShow': { task: 'task_missing', preamble: true },
'orchestration.gateCreate': { task: 'task_missing', question: 'q' },
'orchestration.gateResolve': { id: 'gate_missing', resolution: 'r' },
'orchestration.gateList': {},
@@ -50,6 +50,8 @@ export const ORCHESTRATION_CALLER_PARAM: Readonly<Record<string, CallerParam>> =
'orchestration.reply': 'from',
'orchestration.ask': 'from',
'orchestration.dispatch': 'from',
// The preview names its caller as the coordinator, exactly as the dispatch it previews does.
'orchestration.dispatchShow': 'from',
'orchestration.gateCreate': 'from',
'orchestration.gateResolve': 'from',
'orchestration.gateList': 'from',
@@ -5,7 +5,7 @@ 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 { structuredWorkerChildIdentityEnv } from './structured-worker-child-identity-env'
import { structuredSessionChildIdentityEnv } from './structured-session-child-identity-env'
import {
mintStructuredWorkerHandle,
mintStructuredWorkerPaneKey,
@@ -56,7 +56,7 @@ afterEach(() => {
}
})
describe('structuredWorkerChildIdentityEnv', () => {
describe('structuredSessionChildIdentityEnv', () => {
it('marks an ordinary chat session as having NO identity, and grants it nothing', () => {
// The marker names nothing — no handle, no pane key, no session id, no token — so it cannot be
// replayed or impersonated, and it does not reach the hook, agent-row or mobile-projection
@@ -66,7 +66,7 @@ describe('structuredWorkerChildIdentityEnv', () => {
pinPlatform('linux')
installFakeAppEnvironment({ isPackaged: () => true, getPath: () => USER_DATA })
const childEnv = { PATH: '/usr/bin' }
const env = structuredWorkerChildIdentityEnv(SESSION_ID, childEnv)
const env = structuredSessionChildIdentityEnv(SESSION_ID, childEnv)
expect(env).toEqual({ PATH: '/usr/bin', ORCA_STRUCTURED_SESSION: '1' })
expect(env.ORCA_TERMINAL_HANDLE).toBeUndefined()
expect(env.ORCA_PANE_KEY).toBeUndefined()
@@ -82,7 +82,7 @@ describe('structuredWorkerChildIdentityEnv', () => {
pinPlatform('linux')
installFakeAppEnvironment({ isPackaged: () => true, getPath: () => USER_DATA })
const handle = registerWorker()
const env = structuredWorkerChildIdentityEnv(SESSION_ID, { PATH: '/usr/bin:/bin' })
const env = structuredSessionChildIdentityEnv(SESSION_ID, { PATH: '/usr/bin:/bin' })
expect(env.ORCA_TERMINAL_HANDLE).toBe(handle)
expect(env.ORCA_CLI_COMMAND).toBe('orca')
expect(env.PATH).toBe(`${SHIM_DIR}:/usr/bin:/bin`)
@@ -92,7 +92,7 @@ describe('structuredWorkerChildIdentityEnv', () => {
pinPlatform('darwin')
installFakeAppEnvironment({ isPackaged: () => true, getPath: () => USER_DATA })
registerWorker()
const env = structuredWorkerChildIdentityEnv(SESSION_ID, { PATH: '/usr/bin' })
const env = structuredSessionChildIdentityEnv(SESSION_ID, { PATH: '/usr/bin' })
expect(env.PATH).toBe(`${join(RESOURCES, 'bin')}:/usr/bin`)
})
@@ -100,7 +100,7 @@ describe('structuredWorkerChildIdentityEnv', () => {
pinPlatform('win32')
installFakeAppEnvironment({ isPackaged: () => true, getPath: () => USER_DATA })
registerWorker()
const env = structuredWorkerChildIdentityEnv(SESSION_ID, { Path: 'C:\\Windows' })
const env = structuredSessionChildIdentityEnv(SESSION_ID, { Path: 'C:\\Windows' })
expect(env.Path).toBe(`${join(RESOURCES, 'bin')};C:\\Windows`)
expect(env.PATH).toBeUndefined()
})
@@ -109,7 +109,7 @@ describe('structuredWorkerChildIdentityEnv', () => {
pinPlatform('darwin')
installFakeAppEnvironment({ isPackaged: () => false, getPath: () => USER_DATA })
registerWorker()
const env = structuredWorkerChildIdentityEnv(SESSION_ID, { PATH: '/usr/bin' })
const env = structuredSessionChildIdentityEnv(SESSION_ID, { PATH: '/usr/bin' })
expect(env.PATH).toBe(`${join(USER_DATA, 'cli', 'bin')}:/usr/bin`)
})
@@ -119,7 +119,7 @@ describe('structuredWorkerChildIdentityEnv', () => {
pinPlatform('linux')
installFakeAppEnvironment({ isPackaged: () => true, getPath: () => USER_DATA })
registerWorker()
const env = structuredWorkerChildIdentityEnv(SESSION_ID, { PATH: '/usr/bin' })
const env = structuredSessionChildIdentityEnv(SESSION_ID, { PATH: '/usr/bin' })
expect(env.ORCA_PANE_KEY).toBeUndefined()
expect(Object.keys(env).filter((key) => key.includes('PANE'))).toEqual([])
})
@@ -140,7 +140,7 @@ describe('structuredWorkerChildIdentityEnv', () => {
installFakeAppEnvironment({ isPackaged: () => true, getPath: () => USER_DATA })
registerWorker()
expect(
structuredWorkerChildIdentityEnv(SESSION_ID, { PATH: '/usr/bin' }).ORCA_CLI_COMMAND
structuredSessionChildIdentityEnv(SESSION_ID, { PATH: '/usr/bin' }).ORCA_CLI_COMMAND
).not.toBe('orca-ide')
})
})
@@ -1,62 +1,67 @@
/**
* The orchestration identity — and the CLI reachability — a structured worker's own child needs
* to speak for itself.
* 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.
*
* Without `ORCA_TERMINAL_HANDLE` the worker's Bash tool has nothing to pass as `--from`, and
* `resolveOrchestrationTerminalHandle` falls back to a cwd lookup that returns whichever leaf in
* the worktree comes first. Two attacks follow from that: a bare `check` reads and consumes a
* SIBLING's dispatch mailbox, and a bare `send --type worker_done` can settle a sibling's
* context-only dispatch, a tier that has no capability token to reject on.
* 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
* `orca orchestration check` acts as this session instead of guessing a terminal: with no pane of
* its own, every guess landed on a sibling, and a destructive `check` consumed that sibling's mail.
* Identity by session id assumes one machine and one user; crossing a host boundary (SSH, a paired
* peer) re-opens that decision, and the host refuses a session claim from across one.
*
* A dispatched structured worker also keeps the `structworker_` handle it was minted, for the
* handle-based surfaces outside orchestration; for orchestration the id wins and the host maps it
* back to that handle, so the worker keeps one identity.
*
* `ORCA_CLI_COMMAND: 'orca'` is honest ONLY because of the PATH prepend below. Orca's Linux CLI
* installs as `orca-ide` so it never claims GNOME Orca's /usr/bin/orca (stablyai/orca#7904), and
* on packaged macOS/Windows the bundled launcher is reachable only from the app's own resources
* dir. A PTY worker gets that treatment from `buildPtyHostEnv`; a structured worker has no PTY,
* so it applies the SAME function here rather than a second, drifting copy of the rule.
* dir. A PTY agent gets that treatment from `buildPtyHostEnv`; a structured session has no PTY, so
* it applies the SAME function here rather than a second, drifting copy of the rule.
*
* Deliberately NOT `ORCA_PANE_KEY`. Claude structured sessions run hooks, and a pane key in their
* environment starts flowing into hook-emitted agent-status payloads and the hook-attestation,
* agent-row and mobile-projection pipelines, every one of which assumes a pane key names a live
* PTY leaf. It would also open `selectExactWorkerProviderSession`, which is fail-closed today
* precisely because a structured session emits no hook agent status. The CLI needs none of it once
* the handle is present.
* precisely because a structured session emits no hook agent status.
*
* A session that is not a dispatched worker gets ONE variable, `ORCA_STRUCTURED_SESSION`, and it
* names nothing: no handle, no pane key, no session id, no token. Its only meaning is "this child
* is a structured session with no orchestration identity", which is what a verb needs in order to
* REFUSE rather than guess one. Because it names nothing it cannot be replayed, cannot impersonate,
* and cannot flow into the hook, agent-row or mobile-projection pipelines the way a pane key would
* — which is why it is a different decision from withholding `ORCA_PANE_KEY`, not a reversal of it.
* Without it, `check` fell through to the active-terminal guess and destructively consumed a
* SIBLING pane's oldest unread batch; `requireUnambiguous` only narrows that, because with exactly
* one terminal pane in the worktree the guess still resolves — to a sibling.
* The identity-less marker (`ORCA_STRUCTURED_SESSION`) is no longer written: a child with an id is
* never identity-less, so a marker inherited from an Orca launched inside an older session is inert.
*
* The handle is read from the registry at spawn time, so an in-host recovery respawn re-bakes the
* SAME handle rather than a stale or fresh one.
*/
import { getAppEnvironment, hasAppEnvironment } from '../../shared/app-environment'
import { ORCA_AGENT_SESSION_ID_ENV } from '../../shared/agent-session-caller-env'
import { prependOrcaCliDirToChildPath } from '../cli/orca-cli-child-path'
import { ORCA_STRUCTURED_SESSION_ENV } from '../../shared/structured-session-marker'
import { structuredWorkerIdentities } from './structured-worker-identity'
export function structuredWorkerChildIdentityEnv(
export function structuredSessionChildIdentityEnv(
sessionId: string,
childEnv: Record<string, string>
): Record<string, string> {
const identity = structuredWorkerIdentities.getBySessionId(sessionId)
if (!identity) {
return { ...childEnv, [ORCA_STRUCTURED_SESSION_ENV]: '1' }
}
const env: Record<string, string> = {
...childEnv,
ORCA_TERMINAL_HANDLE: identity.handle,
...(identity ? { ORCA_TERMINAL_HANDLE: identity.handle } : {}),
[ORCA_AGENT_SESSION_ID_ENV]: sessionId,
ORCA_CLI_COMMAND: 'orca'
}
applyOrcaCliPath(env)
return env
}
/**
* The same session id for its terminal view, so switching views never changes who the session is.
* Same-host only, as above: the host refuses the claim from a terminal that runs in WSL or over SSH.
*/
export function structuredSessionTerminalViewEnv(
sessionId: string | undefined
): Record<string, string> {
return sessionId ? { [ORCA_AGENT_SESSION_ID_ENV]: sessionId } : {}
}
/**
* A host with no app environment installed — a plain-Node fork, or a unit test — has no userData
* root to resolve, and inventing one would write a shim into the wrong directory.
@@ -5,7 +5,7 @@ import {
structuredAgentSessionTabId
} from '../../shared/structured-agent-session-projection'
import { selectExactWorkerProviderSession } from './orchestration/worker-provider-session'
import { structuredWorkerChildIdentityEnv } from './structured-worker-child-identity-env'
import { structuredSessionChildIdentityEnv } from './structured-session-child-identity-env'
import {
StructuredWorkerIdentityRegistry,
isStructuredWorkerHandle,
@@ -294,7 +294,7 @@ describe('structured workers stay outside the PTY-only fail-closed paths', () =>
hostScope: { kind: 'local', hostId: 'local' }
})
try {
const env = structuredWorkerChildIdentityEnv(SESSION_ID, {})
const env = structuredSessionChildIdentityEnv(SESSION_ID, {})
// Registered, so this is a populated env — not the empty one an unregistered session gets,
// which would satisfy the pane-key assertion for the wrong reason.
expect(env.ORCA_TERMINAL_HANDLE).toBe(handle)
@@ -26,14 +26,19 @@ import {
} from '../../shared/structured-agent-session-projection'
import { isTerminalLeafId, makePaneKey, parsePaneKey } from '../../shared/stable-pane-id'
import { isOrcaSessionId, type OrcaSessionId } from '../../shared/orca-session-address'
import {
STRUCTURED_WORKER_HANDLE_PREFIX,
isStructuredWorkerHandle
} from '../../shared/structured-worker-handle'
import {
parseWorkerTerminalHostScope,
type WorkerTerminalHostScope
} from './orchestration/worker-terminal-process-liveness'
// Deliberately not `term_`: `issueHandle` revalidates the renderer graph epoch against the
// renderer-driven leaves map, so a main-minted `term_` leaf evaporates on the next window reload.
export const STRUCTURED_WORKER_HANDLE_PREFIX = 'structworker_'
export {
STRUCTURED_WORKER_HANDLE_PREFIX,
isStructuredWorkerHandle
} from '../../shared/structured-worker-handle'
export const STRUCTURED_WORKER_INCARNATION_PREFIX = 'structured:'
export type StructuredWorkerIdentity = {
@@ -47,10 +52,6 @@ export type StructuredWorkerIdentity = {
hostScope: WorkerTerminalHostScope
}
export function isStructuredWorkerHandle(handle: string | null | undefined): boolean {
return typeof handle === 'string' && handle.startsWith(STRUCTURED_WORKER_HANDLE_PREFIX)
}
export function mintStructuredWorkerHandle(): string {
return `${STRUCTURED_WORKER_HANDLE_PREFIX}${randomUUID()}`
}
@@ -13,6 +13,8 @@ import {
ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION_ENV,
ORCHESTRATION_COMPATIBILITY_HOST_KIND_ENV
} from '../../shared/orchestration-compatibility-evidence'
import { ORCA_AGENT_SESSION_ID_ENV } from '../../shared/agent-session-caller-env'
import { ORCA_STRUCTURED_SESSION_ENV } from '../../shared/structured-session-marker'
import {
REMOTE_ARTIFACT_INPUT_ENV,
sshArtifactSourceKey,
@@ -131,6 +133,10 @@ export function buildHostCliEnv(args: {
delete env[ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION_ENV]
delete env[ORCHESTRATION_COMPATIBILITY_ATTACHMENT_ENV]
delete env[REMOTE_ARTIFACT_INPUT_ENV]
// Why: a remote command must never claim a local agent session. The host's env carries one only
// when Orca was launched inside a session, and identity by session id is same-host only.
delete env[ORCA_AGENT_SESSION_ID_ENV]
delete env[ORCA_STRUCTURED_SESSION_ENV]
if (args.runtimeAuthority) {
env[ORCHESTRATION_COMPATIBILITY_HOST_KIND_ENV] = 'ssh'
env[ORCHESTRATION_COMPATIBILITY_HOST_ID_ENV] = args.runtimeAuthority.targetId
+17
View File
@@ -0,0 +1,17 @@
/**
* The Orca-minted agent session id, injected into a structured session's own child processes, in
* native chat and in terminal view alike. When it is present it IS the orchestration caller: the
* CLI sends it in the orchestration envelope and the host resolves it to the session's actor, so no
* terminal is resolved or guessed on its behalf.
*
* Identity by session id assumes one machine and one user. A host boundary (SSH, a paired peer,
* WSL) re-opens that decision, which is why nothing forwards the id across one as a caller.
*/
export const ORCA_AGENT_SESSION_ID_ENV = 'ORCA_AGENT_SESSION_ID'
export function readInjectedAgentSessionId(
env: Readonly<Record<string, string | undefined>> = process.env
): string | undefined {
const value = env[ORCA_AGENT_SESSION_ID_ENV]?.trim()
return value ? value : undefined
}
+7 -6
View File
@@ -1,11 +1,12 @@
import { isAgentSessionId } from './agent-session-record'
import { STRUCTURED_WORKER_HANDLE_PREFIX } from './structured-worker-handle'
/**
* The Orca session id is the id Orca minted for a structured session (its session record id), never
* the provider's own session id. Orchestration stores, bare, the one the agent is addressed by: for
* a `/clear`ed chat, its lineage root's, not the live session's. Mail addresses the session as
* `session:<id>`, beside `run:<id>` and `dispatch:<id>`, and derives that spelling here rather than
* storing it.
* The Orca session id is the id Orca minted for a structured session (its session record id, the
* value of `ORCA_AGENT_SESSION_ID`), never the provider's own session id. Orchestration stores,
* bare, the one the agent is addressed by: for a `/clear`ed chat, its lineage root's, not the live
* session's. Mail addresses the session as `session:<id>`, beside `run:<id>` and `dispatch:<id>`,
* and derives that spelling here rather than storing it.
*
* Where the session runs is not part of the id; it is read from the session record when needed. PTY
* agents have none today, and never a pane-keyed one: a pane outlives the agent in it, so such an id
@@ -24,7 +25,7 @@ export type OrcaSessionAddress = string & { readonly [orcaSessionAddressBrand]:
// Terminal handles (`term_` from the PTY runtime, `structworker_` from structured-worker-identity)
// share the session-id charset. A handle is never a session, so one handed over by mistake must not
// become a durable Orca session id.
const TERMINAL_HANDLE_PREFIXES = ['term_', 'structworker_'] as const
const TERMINAL_HANDLE_PREFIXES = ['term_', STRUCTURED_WORKER_HANDLE_PREFIX] as const
export function isOrcaSessionId(id: string): id is OrcaSessionId {
return isAgentSessionId(id) && !TERMINAL_HANDLE_PREFIXES.some((prefix) => id.startsWith(prefix))
+13 -6
View File
@@ -1,13 +1,20 @@
import { readInjectedAgentSessionId } from './agent-session-caller-env'
/**
* The marker a structured chat session's child carries when it has NO orchestration identity.
* The marker a structured chat session's child carried when it had NO orchestration identity.
*
* It names nothing on purpose — no handle, no pane key, no session id, no token — so it grants no
* authority and cannot be replayed or impersonated. Its only job is to let a CLI verb that would
* otherwise GUESS an implicit terminal refuse instead: a structured session has no pane, so every
* guess resolves to a sibling, and `orchestration check` is destructive by default.
* Current hosts no longer write it: every structured child carries its injected session id
* instead. The reader stays for a child spawned by an Orca that predates injection, which can still
* reach a newer CLI through a global install — it must refuse, not guess, because a structured
* session has no pane, so every implicit-terminal guess resolves to a sibling and
* `orchestration check` is destructive by default.
*/
export const ORCA_STRUCTURED_SESSION_ENV = 'ORCA_STRUCTURED_SESSION'
/** An injected session id is an identity, so a child carrying one is never identity-less. */
export function isStructuredSessionWithoutIdentity(env: NodeJS.ProcessEnv = process.env): boolean {
return (env[ORCA_STRUCTURED_SESSION_ENV] ?? '').length > 0
return (
(env[ORCA_STRUCTURED_SESSION_ENV] ?? '').length > 0 &&
readInjectedAgentSessionId(env) === undefined
)
}
+7
View File
@@ -0,0 +1,7 @@
// Deliberately not `term_`: `issueHandle` revalidates the renderer graph epoch against the
// renderer-driven leaves map, so a main-minted `term_` leaf evaporates on the next window reload.
export const STRUCTURED_WORKER_HANDLE_PREFIX = 'structworker_'
export function isStructuredWorkerHandle(handle: string | null | undefined): boolean {
return typeof handle === 'string' && handle.startsWith(STRUCTURED_WORKER_HANDLE_PREFIX)
}