mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 11:07:42 +00:00
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:
@@ -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]
|
||||
}
|
||||
@@ -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/**/*",
|
||||
|
||||
@@ -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 } : {})
|
||||
}),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
|
||||
+9
-9
@@ -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')
|
||||
})
|
||||
})
|
||||
+31
-26
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user