fix(cli): check the runtime source before sending (#27206)

* fix(cli): check the runtime source before sending

A terminal Orca launches now carries which Orca launched it
(ORCA_RUNTIME_SOURCE_ID = the profile's installation id,
ORCA_RUNTIME_SOURCE_INCARNATION = the launching runtime id). Before a
request is sent, the CLI compares that with the runtime the profile's
runtime record names. Same runtime: sent as before. Different runtime:
a read-only status.get asks which profile it serves; the same profile (a
restart or update) is accepted, anything else is refused before the
request reaches it.

The vars cross into WSL through WSLENV. The SSH bridge stamps the
bridging runtime itself rather than trusting the remote env. The
existing orchestration compatibility stamp is untouched.

* fix(orcad): drop an inherited runtime source at startup

* fix(cli): an explicit switch to another profile is not fenced

The stamp also carries the launching runtime's userData path. A CLI that
dials a different profile than the one that launched its terminal, and
not the platform default an unpinned terminal falls back to, was pointed
there on purpose (orca-dev from a packaged Orca's terminal), so the
fence does not apply.

* refactor(cli): reuse the shared userData path comparison

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
OrcaWin
2026-10-10 03:19:14 -07:00
committed by GitHub
co-authored by m4air
parent bbe9a11456
commit 7aa91f730a
18 changed files with 502 additions and 4 deletions
+7
View File
@@ -21,6 +21,7 @@ import {
didAnotherRuntimeHandleTerminalPrompt
} from './terminal-prompt-mutation-recovery'
import { markEnvironmentUsed } from './environments'
import { createRuntimeSourceFence, type RuntimeSourceFence } from './runtime-source-fence'
import { resolveRemotePairing } from './runtime-remote-pairing'
import {
ORCHESTRATION_CONTRACT_RUNTIME_CAPABILITY,
@@ -56,6 +57,7 @@ export class RuntimeClient {
private readonly orchestrationCompatibility = createOrchestrationCompatibilityEnvelope(
process.env
)
private readonly runtimeSourceFence: RuntimeSourceFence
// Why: browser commands trigger first-time session init (agent-browser connect +
// CDP proxy setup) which can take 15-30s. 60s accommodates cold start without
@@ -75,6 +77,7 @@ export class RuntimeClient {
this.originalArgs = originalArgs ? [...originalArgs] : undefined
this.remotePairing = resolveRemotePairing(userDataPath, remotePairingCode, environmentSelector)
this.remoteCompat = new RemoteRuntimeCompatGate(userDataPath, environmentSelector)
this.runtimeSourceFence = createRuntimeSourceFence(process.env, userDataPath)
}
get isRemote(): boolean {
@@ -162,6 +165,10 @@ export class RuntimeClient {
return response
}
const metadata = readMetadata(this.userDataPath)
// Why outside recover(): a refusal here happens before anything is sent, so there is nothing to retry.
await this.runtimeSourceFence.check(metadata, method, (target) =>
sendRequest<RuntimeStatus>(target, 'status.get', undefined, this.requestTimeoutMs)
)
let response
try {
response = await sendRequest<TResult>(metadata, method, params, effectiveTimeoutMs, envelope)
@@ -0,0 +1,178 @@
import { createServer, type Server } from 'node:net'
import { randomUUID } from 'node:crypto'
import { mkdtempSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
RUNTIME_SOURCE_ID_ENV,
RUNTIME_SOURCE_INCARNATION_ENV,
RUNTIME_SOURCE_PROFILE_PATH_ENV
} from '../../shared/runtime-source-env'
import { RuntimeClient, RuntimeClientError } from '../runtime-client'
import { resolveFencedRuntimeSource } from './runtime-source-fence'
const SOURCE_A = '11111111-1111-4111-8111-111111111111'
const SOURCE_B = '22222222-2222-4222-8222-222222222222'
const servers = new Set<Server>()
const savedEnv = {
id: process.env[RUNTIME_SOURCE_ID_ENV],
incarnation: process.env[RUNTIME_SOURCE_INCARNATION_ENV],
profilePath: process.env[RUNTIME_SOURCE_PROFILE_PATH_ENV]
}
function restoreEnv(name: string, value: string | undefined): void {
if (value === undefined) {
delete process.env[name]
} else {
process.env[name] = value
}
}
afterEach(async () => {
restoreEnv(RUNTIME_SOURCE_ID_ENV, savedEnv.id)
restoreEnv(RUNTIME_SOURCE_INCARNATION_ENV, savedEnv.incarnation)
restoreEnv(RUNTIME_SOURCE_PROFILE_PATH_ENV, savedEnv.profilePath)
await Promise.all(
[...servers].map((server) => new Promise<void>((resolve) => server.close(() => resolve())))
)
servers.clear()
})
type FakeRuntime = { userDataPath: string; methods: string[] }
/** A live runtime on its own socket that answers every method and records what reached it. */
async function startRuntime(args: {
runtimeId: string
installationId?: string
}): Promise<FakeRuntime> {
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-source-fence-'))
const isWindows = process.platform === 'win32'
const endpoint = isWindows
? `\\\\.\\pipe\\orca-source-fence-${randomUUID()}`
: join(userDataPath, 'runtime.sock')
const methods: string[] = []
const server = createServer((socket) => {
let buffer = ''
socket.setEncoding('utf8')
socket.on('data', (chunk: string) => {
buffer += chunk
const newline = buffer.indexOf('\n')
if (newline === -1) {
return
}
const request: { id: string; method: string } = JSON.parse(buffer.slice(0, newline))
methods.push(request.method)
const result =
request.method === 'status.get'
? {
runtimeId: args.runtimeId,
...(args.installationId
? { hostDescriptor: { installationId: args.installationId } }
: {})
}
: { ok: true }
socket.end(
`${JSON.stringify({ id: request.id, ok: true, result, _meta: { runtimeId: args.runtimeId } })}\n`
)
})
})
servers.add(server)
await new Promise<void>((resolve) => server.listen(endpoint, resolve))
writeFileSync(
join(userDataPath, 'orca-runtime.json'),
JSON.stringify({
runtimeId: args.runtimeId,
pid: 1,
transports: [{ kind: isWindows ? 'named-pipe' : 'unix', endpoint }],
authToken: 'token',
startedAt: 1
})
)
return { userDataPath, methods }
}
function launchedBy(sourceId: string, incarnation: string, profilePath?: string): void {
process.env[RUNTIME_SOURCE_ID_ENV] = sourceId
process.env[RUNTIME_SOURCE_INCARNATION_ENV] = incarnation
restoreEnv(RUNTIME_SOURCE_PROFILE_PATH_ENV, profilePath)
}
async function expectRefusal(promise: Promise<unknown>, code: string): Promise<void> {
const error = await promise.then(
() => null,
(caught: unknown) => caught
)
expect(error).toBeInstanceOf(RuntimeClientError)
expect(error).toMatchObject({ code })
}
describe('RuntimeClient runtime-source fence', () => {
it('refuses another live runtime before the request reaches it', async () => {
const runtime = await startRuntime({ runtimeId: 'runtime-b', installationId: SOURCE_B })
launchedBy(SOURCE_A, 'runtime-a')
const client = new RuntimeClient(runtime.userDataPath, 2_000, null, null, 'orca')
await expectRefusal(
client.call('terminal.send', { terminal: 't1', text: 'hi' }),
'runtime_source_mismatch'
)
await expectRefusal(client.call('worktree.list'), 'runtime_source_mismatch')
expect(runtime.methods).toEqual(['status.get', 'status.get'])
})
it('accepts a restarted runtime of the same source and probes it only once', async () => {
const runtime = await startRuntime({ runtimeId: 'runtime-a2', installationId: SOURCE_A })
launchedBy(SOURCE_A, 'runtime-a1')
const client = new RuntimeClient(runtime.userDataPath, 2_000, null, null, 'orca')
await client.call('terminal.send', { terminal: 't1', text: 'hi' })
await client.call('worktree.list')
expect(runtime.methods).toEqual(['status.get', 'terminal.send', 'worktree.list'])
})
it('sends without a probe when the launching runtime is still the live one', async () => {
const runtime = await startRuntime({ runtimeId: 'runtime-a', installationId: SOURCE_A })
launchedBy(SOURCE_A, 'runtime-a')
const client = new RuntimeClient(runtime.userDataPath, 2_000, null, null, 'orca')
await client.call('worktree.list')
expect(runtime.methods).toEqual(['worktree.list'])
})
it('refuses when the live runtime cannot say which profile it serves', async () => {
const runtime = await startRuntime({ runtimeId: 'runtime-old' })
launchedBy(SOURCE_A, 'runtime-a')
const client = new RuntimeClient(runtime.userDataPath, 2_000, null, null, 'orca')
await expectRefusal(client.call('worktree.list'), 'runtime_source_unverifiable')
expect(runtime.methods).toEqual(['status.get'])
})
it('keeps the unfenced path for a CLI started outside an Orca terminal', async () => {
const runtime = await startRuntime({ runtimeId: 'runtime-b', installationId: SOURCE_B })
delete process.env[RUNTIME_SOURCE_ID_ENV]
delete process.env[RUNTIME_SOURCE_INCARNATION_ENV]
const client = new RuntimeClient(runtime.userDataPath, 2_000, null, null, 'orca')
await client.call('worktree.list')
expect(runtime.methods).toEqual(['worktree.list'])
})
it('lets an explicit switch to another profile through, e.g. orca-dev from a packaged terminal', async () => {
const runtime = await startRuntime({ runtimeId: 'runtime-dev', installationId: SOURCE_B })
launchedBy(SOURCE_A, 'runtime-a', join(tmpdir(), 'orca-packaged-profile'))
const client = new RuntimeClient(runtime.userDataPath, 2_000, null, null, 'orca')
await client.call('worktree.list')
expect(runtime.methods).toEqual(['worktree.list'])
})
it('still fences a terminal whose CLI fell back to the platform default profile', () => {
const stamp = { sourceId: SOURCE_A, incarnation: 'runtime-a', profilePath: '/profiles/custom' }
expect(resolveFencedRuntimeSource(stamp, '/profiles/default', '/profiles/default')).toBe(stamp)
expect(resolveFencedRuntimeSource(stamp, '/profiles/custom', '/profiles/default')).toBe(stamp)
expect(resolveFencedRuntimeSource(stamp, '/profiles/dev', '/profiles/default')).toBeNull()
})
})
+87
View File
@@ -0,0 +1,87 @@
import type { RuntimeMetadata } from '../../shared/runtime-bootstrap'
import { isSameUserDataPath } from '../../shared/serve-user-data-path'
import { readRuntimeSourceStamp, type RuntimeSourceStamp } from '../../shared/runtime-source-env'
import type { RuntimeStatus } from '../../shared/runtime-types'
import { getPlatformUserDataPath } from './metadata'
import { RuntimeClientError, type RuntimeRpcResponse } from './types'
type StatusProbe = (metadata: RuntimeMetadata) => Promise<RuntimeRpcResponse<RuntimeStatus>>
/**
* The stamp to enforce for a CLI dialing `userDataPath`. Explicitly choosing another profile (e.g.
* `orca-dev` from a packaged Orca's terminal) is the user's target, not a fallback, so it is not fenced.
*/
export function resolveFencedRuntimeSource(
stamp: RuntimeSourceStamp | null,
userDataPath: string,
platformDefaultPath: string | null
): RuntimeSourceStamp | null {
if (!stamp?.profilePath || isSameUserDataPath(userDataPath, stamp.profilePath)) {
return stamp
}
// Why: the platform default is what an unpinned terminal falls back to, which is the case to fence.
return platformDefaultPath !== null && isSameUserDataPath(userDataPath, platformDefaultPath)
? stamp
: null
}
/**
* Refuses, before the request is sent, a runtime other than the one that launched this process.
* A new incarnation of the same source (restart, update) is accepted so surviving panes keep working.
*/
export class RuntimeSourceFence {
private readonly verifiedRuntimeIds = new Set<string>()
constructor(private readonly expected: RuntimeSourceStamp | null) {
if (expected) {
this.verifiedRuntimeIds.add(expected.incarnation)
}
}
async check(metadata: RuntimeMetadata, method: string, probe: StatusProbe): Promise<void> {
// Why status.get is exempt: it is the read-only probe itself.
if (!this.expected || method === 'status.get') {
return
}
if (this.verifiedRuntimeIds.has(metadata.runtimeId)) {
return
}
// Why trust the answer: the transport rejects a response from any runtime but metadata's.
const response = await probe(metadata)
if (response.ok === false) {
throw new RuntimeClientError(
'runtime_source_unverifiable',
`Could not confirm that the running Orca is the one that launched this terminal: ${response.error.message}`
)
}
const sourceId = response.result.hostDescriptor?.installationId
if (!sourceId) {
throw new RuntimeClientError(
'runtime_source_unverifiable',
'The running Orca does not report which profile it serves, so this terminal cannot confirm it is the Orca that launched it. Nothing was sent. Open a new terminal from the Orca you want to control.'
)
}
if (sourceId !== this.expected.sourceId) {
throw new RuntimeClientError(
'runtime_source_mismatch',
'The running Orca is not the one that launched this terminal (another profile or instance took over its runtime record). Nothing was sent. Open a new terminal from the Orca you want to control.'
)
}
this.verifiedRuntimeIds.add(metadata.runtimeId)
}
}
export function createRuntimeSourceFence(
env: Readonly<Record<string, string | undefined>>,
userDataPath: string
): RuntimeSourceFence {
let platformDefaultPath: string | null = null
try {
platformDefaultPath = getPlatformUserDataPath()
} catch {
// Why: no resolvable default (Windows without APPDATA) means nothing can fall back to it.
}
return new RuntimeSourceFence(
resolveFencedRuntimeSource(readRuntimeSourceStamp(env), userDataPath, platformDefaultPath)
)
}
+6
View File
@@ -5,6 +5,8 @@ import { isAgentStatusHooksEnabled } from '../../../agent-hooks/managed-agent-ho
import { isSafePtySessionId } from '../../../daemon/pty-session-id'
import { isNativeWindowsLocalPtySpawn } from '../../../runtime/terminal-model-query-authority'
import { stampWslOrchestrationCompatibilityHost } from '../../../pty/wsl-orca-env'
import { stampRuntimeSourceEnv } from '../../../../shared/runtime-source-env'
import { getRuntimeSourceStamp } from '../../../runtime/host-descriptor'
import { ensureCodexStateDbBackfillRecoveryStarted } from '../../../codex/codex-state-db-backfill-recovery'
import { buildPtyHostEnv } from '../host-env/assembly'
import {
@@ -160,6 +162,10 @@ export async function assemblePtyIpcSpawnCodexEnv(ctx: PtyIpcSpawnState): Promis
ctx.deps.runtime?.getOrchestrationCompatibilityHostId?.(),
ctx.codexSelectionTarget.runtime === 'wsl' ? ctx.expectedWslDistro : null
)
stampRuntimeSourceEnv(
ctx.env,
getRuntimeSourceStamp(ctx.deps.runtime, getAppEnvironment().getPath('userData'))
)
promoteAgentTeamsShimPath(ctx.env, ctx.requestedAgentTeamsPath)
} catch (err) {
// Why: buildPtyHostEnv has fs side-effects (Pi/OMP install); clear per-PTY state on throw, but only minted ids — caller ids may name existing PTYs.
@@ -11,6 +11,8 @@ import {
} from '../../../pty/wsl-orca-env'
import type { CodexAccountSelectionTarget } from '../../../codex-accounts/runtime-selection'
import { buildPtyHostEnv } from '../host-env/assembly'
import { stampRuntimeSourceEnv } from '../../../../shared/runtime-source-env'
import { getRuntimeSourceStamp } from '../../../runtime/host-descriptor'
import {
getCompatibleSelectedCodexHomePath,
shouldStripInheritedOrcaCodexHome
@@ -113,6 +115,10 @@ export function configureLocalPtyProvider(args: {
runtime?.getOrchestrationCompatibilityHostId?.(),
ctx?.isWsl === true ? ctx.wslDistro : null
)
stampRuntimeSourceEnv(
env,
getRuntimeSourceStamp(runtime, getAppEnvironment().getPath('userData'))
)
if (ctx?.isWsl === true) {
addOrcaWslInteropEnv(env)
}
@@ -35,6 +35,8 @@ import { resolveLocalProjectRuntimeForWorktreeId } from '../../../local-project-
import { resolveManagedSshHostLoginShell } from '../../../pty/managed-ssh-host-login-shell'
import { resolvePathEnvKey } from '../../../pty/windows-environment-path'
import { stampWslOrchestrationCompatibilityHost } from '../../../pty/wsl-orca-env'
import { stampRuntimeSourceEnv } from '../../../../shared/runtime-source-env'
import { getRuntimeSourceStamp } from '../../../runtime/host-descriptor'
import { ensureCodexStateDbBackfillRecoveryStarted } from '../../../codex/codex-state-db-backfill-recovery'
import { clearProviderPtyState } from '../provider/state-cleanup'
import { awaitExplicitPiOmpGuestReadiness } from '../../../agent-hooks/wsl-pi-omp-guest-readiness'
@@ -284,6 +286,10 @@ export async function prepareRuntimePtySpawn(
ctx.deps.runtime?.getOrchestrationCompatibilityHostId?.(),
ctx.codexSelectionTarget.runtime === 'wsl' ? ctx.expectedWslDistro : null
)
stampRuntimeSourceEnv(
ctx.env,
getRuntimeSourceStamp(ctx.deps.runtime, getAppEnvironment().getPath('userData'))
)
promoteAgentTeamsShimPath(ctx.env, ctx.requestedAgentTeamsPath)
} catch (error) {
// Why: host-env setup can materialize agent hooks/extensions before failing.
+3
View File
@@ -37,6 +37,7 @@ import {
changedAiVaultSearchSettings,
type AiVaultSearchSettings
} from '../../shared/ai-vault-search-settings'
import { stampRuntimeSourceEnv } from '../../shared/runtime-source-env'
export { parseArgs }
@@ -133,6 +134,8 @@ export type OrcadHandle = {
*/
export async function startOrcad(options: OrcadOptions = {}): Promise<OrcadHandle> {
installOrcadHostAdapters()
// Why: an orcad started from an Orca terminal must not fence its own CLI children to that Orca.
stampRuntimeSourceEnv(process.env, null)
const { readiness, instance, stop } = await startOrcadWithHost(
resolveUserDataPath(),
(registerCleanup) => startOrcadRuntime(options, registerCleanup),
+7 -1
View File
@@ -81,7 +81,10 @@ describe('addOrcaWslInteropEnv', () => {
ORCA_WSL_HOOK_INSTANCE: 'testinstance',
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_KIND: 'wsl',
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_ID: 'local',
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION: 'Ubuntu'
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION: 'Ubuntu',
ORCA_RUNTIME_SOURCE_ID: '11111111-1111-4111-8111-111111111111',
ORCA_RUNTIME_SOURCE_INCARNATION: 'runtime-a',
ORCA_RUNTIME_SOURCE_PROFILE_PATH: 'C:\\Users\\a\\AppData\\Roaming\\orca'
}
addOrcaWslInteropEnv(env)
@@ -109,6 +112,9 @@ describe('addOrcaWslInteropEnv', () => {
expect(env.WSLENV).toContain('ORCA_ORCHESTRATION_COMPATIBILITY_HOST_KIND/u')
expect(env.WSLENV).toContain('ORCA_ORCHESTRATION_COMPATIBILITY_HOST_ID/u')
expect(env.WSLENV).toContain('ORCA_ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION/u')
expect(env.WSLENV).toContain('ORCA_RUNTIME_SOURCE_ID/u')
expect(env.WSLENV).toContain('ORCA_RUNTIME_SOURCE_INCARNATION/u')
expect(env.WSLENV).toContain('ORCA_RUNTIME_SOURCE_PROFILE_PATH/u')
})
it('overwrites caller host evidence with native runtime WSL authority', () => {
+9
View File
@@ -10,6 +10,11 @@ import {
} from '../../shared/setup-agent-sequencing'
import { getShellReadyWrapperRoot } from '../providers/local-pty-shell-ready-wrapper-root'
import { ORCA_IMAGE_PROTOCOL_ENV } from '../../shared/terminal-image-protocol'
import {
RUNTIME_SOURCE_ID_ENV,
RUNTIME_SOURCE_INCARNATION_ENV,
RUNTIME_SOURCE_PROFILE_PATH_ENV
} from '../../shared/runtime-source-env'
const WSLENV_ENTRY_SEPARATOR = ':'
@@ -95,6 +100,10 @@ export function addOrcaWslInteropEnv(env: Record<string, string>): void {
'ORCA_ORCHESTRATION_COMPATIBILITY_HOST_KIND/u',
'ORCA_ORCHESTRATION_COMPATIBILITY_HOST_ID/u',
'ORCA_ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION/u',
`${RUNTIME_SOURCE_ID_ENV}/u`,
`${RUNTIME_SOURCE_INCARNATION_ENV}/u`,
// Why /u: compared with the Windows CLI's own userData path, never read as a guest path.
`${RUNTIME_SOURCE_PROFILE_PATH_ENV}/u`,
'ORCA_AGENT_HOOK_PORT/u',
'ORCA_AGENT_HOOK_TOKEN/u',
'ORCA_AGENT_HOOK_ENV/u',
+26 -1
View File
@@ -7,7 +7,9 @@ import {
HOST_INSTALLATION_FILENAME,
computeHostMachineBinding,
loadHostDescriptor,
loadOrCreateHostInstallationId
getRuntimeSourceStamp,
loadOrCreateHostInstallationId,
publishHostDescriptor
} from './host-descriptor'
function profile(): string {
@@ -96,3 +98,26 @@ describe('loadHostDescriptor', () => {
warn.mockRestore()
})
})
describe('getRuntimeSourceStamp', () => {
it('names the profile as the source and the runtime id as the incarnation', () => {
const installationId = '0b4c3d5e-1f2a-4b6c-8d7e-9f0a1b2c3d4e'
publishHostDescriptor('runtime-stamp-1', { installationId })
try {
expect(
getRuntimeSourceStamp({ getRuntimeId: () => 'runtime-stamp-1' }, '/profiles/a')
).toEqual({
sourceId: installationId,
incarnation: 'runtime-stamp-1',
profilePath: '/profiles/a'
})
} finally {
publishHostDescriptor('runtime-stamp-1', null)
}
})
it('stamps nothing before a descriptor is published or without a runtime', () => {
expect(getRuntimeSourceStamp({ getRuntimeId: () => 'runtime-unpublished' })).toBeNull()
expect(getRuntimeSourceStamp(undefined)).toBeNull()
})
})
+17
View File
@@ -7,6 +7,7 @@ import {
} from '../../shared/runtime-host-descriptor'
import { profileStateAccessMachineIdentity } from '../persistence/profile-state/profile-state-access-identity'
import { withFileTransactionLock } from '../file-transaction-lock'
import type { RuntimeSourceStamp } from '../../shared/runtime-source-env'
export const HOST_INSTALLATION_FILENAME = 'host-installation.json'
const MAX_INSTALLATION_FILE_BYTES = 4 * 1024
@@ -119,3 +120,19 @@ export function publishHostDescriptor(
export function getPublishedHostDescriptor(runtimeId: string): RuntimeHostDescriptor | null {
return publishedByRuntimeId.get(runtimeId) ?? null
}
/** Null until this runtime publishes a descriptor; unstamped processes keep the unfenced CLI path. */
export function getRuntimeSourceStamp(
runtime: { getRuntimeId?: () => string } | null | undefined,
userDataPath?: string
): RuntimeSourceStamp | null {
const runtimeId = runtime?.getRuntimeId?.()
const descriptor = runtimeId ? getPublishedHostDescriptor(runtimeId) : null
return runtimeId && descriptor
? {
sourceId: descriptor.installationId,
incarnation: runtimeId,
...(userDataPath ? { profilePath: userDataPath } : {})
}
: null
}
@@ -132,6 +132,35 @@ describe('buildHostCliEnv', () => {
expect(env.ORCA_CLI_COMMAND).toBe('orca')
})
it('stamps this runtime as the source and ignores remote or inherited stamps', () => {
const remoteSource = {
ORCA_RUNTIME_SOURCE_ID: 'remote-chosen-source',
ORCA_RUNTIME_SOURCE_INCARNATION: 'remote-chosen-runtime'
}
const build = (runtimeSource?: { sourceId: string; incarnation: string } | null) =>
buildHostCliEnv({
bridgeCredential: 'sshb_test',
hostEnv: {
ORCA_RUNTIME_SOURCE_ID: 'parent-source',
ORCA_RUNTIME_SOURCE_INCARNATION: 'parent-runtime'
},
remoteEnv: remoteSource,
userDataPath: '/host/user-data',
remoteCwd: '/srv/repo',
runtimeSource
})
expect(build({ sourceId: 'this-source', incarnation: 'this-runtime' })).toMatchObject({
ORCA_RUNTIME_SOURCE_ID: 'this-source',
ORCA_RUNTIME_SOURCE_INCARNATION: 'this-runtime',
ORCA_RUNTIME_SOURCE_PROFILE_PATH: '/host/user-data',
ORCA_USER_DATA_PATH: '/host/user-data'
})
const unstamped = build(null)
expect(unstamped.ORCA_RUNTIME_SOURCE_ID).toBeUndefined()
expect(unstamped.ORCA_RUNTIME_SOURCE_INCARNATION).toBeUndefined()
})
it('never lets a remote command claim a local agent session', () => {
// The host's env carries a session id when Orca was launched inside a structured session; the
// remote shell's own is from another machine. Session identity is same-host only.
@@ -16,6 +16,7 @@ import {
import { ORCA_AGENT_SESSION_ID_ENV } from '../../shared/agent-session-caller-env'
import { ORCA_STRUCTURED_SESSION_ENV } from '../../shared/structured-session-marker'
import { ORCA_SSH_BRIDGE_CREDENTIAL_ENV } from '../../shared/ssh-bridge-credential-env'
import { stampRuntimeSourceEnv, type RuntimeSourceStamp } from '../../shared/runtime-source-env'
import {
sshBridgeCredentials,
type SshBridgeCallerScope,
@@ -75,6 +76,8 @@ export type HostCliPassthroughOptions = {
entryExists?: (path: string) => boolean
killTimeoutMs?: number
credentials?: SshBridgeCredentialRegistry
/** The runtime this bridge belongs to; the host CLI must reach exactly that one. */
runtimeSource?: RuntimeSourceStamp | null
}
/** Thrown when the host CLI entry cannot be launched at all; callers fall back
@@ -115,6 +118,7 @@ export function buildHostCliEnv(args: {
bridgeCredential: string
runtimeAuthority?: SshCliRuntimeAuthority
artifactInput?: RemoteArtifactInput
runtimeSource?: RuntimeSourceStamp | null
}): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = { ...args.hostEnv }
for (const key of REMOTE_CONTEXT_ENV_VARS) {
@@ -152,6 +156,12 @@ export function buildHostCliEnv(args: {
delete env.ORCA_REMOTE_PAIRING
delete env.ORCA_ENVIRONMENT
env[ORCA_SSH_BRIDGE_CREDENTIAL_ENV] = args.bridgeCredential
// Why: stamped from this process, never from the remote env, so remote input cannot pick the runtime.
// The host CLI dials exactly this profile, so the stamp names the same one.
stampRuntimeSourceEnv(
env,
args.runtimeSource ? { ...args.runtimeSource, profilePath: args.userDataPath } : null
)
if (args.runtimeAuthority) {
env[ORCHESTRATION_COMPATIBILITY_HOST_KIND_ENV] = 'ssh'
env[ORCHESTRATION_COMPATIBILITY_HOST_ID_ENV] = args.runtimeAuthority.targetId
@@ -219,7 +229,8 @@ export async function runHostOrcaCliPassthrough(
remoteCwd: request.cwd,
bridgeCredential: credential.token,
runtimeAuthority: request.runtimeAuthority,
artifactInput: request.artifactInput
artifactInput: request.artifactInput,
runtimeSource: options.runtimeSource
})
try {
+5 -1
View File
@@ -8,6 +8,7 @@ import type { RpcResponse } from '../runtime/rpc/core'
import { RpcDispatcher } from '../runtime/rpc/dispatcher'
import { ALL_RPC_METHODS } from '../runtime/rpc/methods'
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
import { getRuntimeSourceStamp } from '../runtime/host-descriptor'
import {
HostCliUnavailableError,
runHostOrcaCliPassthrough,
@@ -84,7 +85,10 @@ export async function runRemoteOrcaCli(
let passthroughFailure: HostCliUnavailableError | null = null
try {
return await runHostOrcaCliPassthrough(request, passthroughOptions)
return await runHostOrcaCliPassthrough(request, {
runtimeSource: getRuntimeSourceStamp(runtime),
...passthroughOptions
})
} catch (err) {
if (!(err instanceof HostCliUnavailableError)) {
throw err
@@ -393,6 +393,25 @@ describe('configureOrcaUserDataPathEnv', () => {
expect(configuredUserDataPath).toBe('/tmp/current-orca-user-data')
})
it('drops a runtime source inherited from the Orca terminal that started this Orca', async () => {
const { app } = await import('electron')
const { configureOrcaUserDataPathEnv } = await import('./configure-process')
const saved = { ...process.env }
process.env.ORCA_RUNTIME_SOURCE_ID = 'parent-source'
process.env.ORCA_RUNTIME_SOURCE_INCARNATION = 'parent-runtime'
app.setPath('userData', '/tmp/current-orca-user-data')
let inherited: (string | undefined)[] = []
try {
configureOrcaUserDataPathEnv()
inherited = [process.env.ORCA_RUNTIME_SOURCE_ID, process.env.ORCA_RUNTIME_SOURCE_INCARNATION]
} finally {
process.env = saved
}
expect(inherited).toEqual([undefined, undefined])
})
})
describe('shouldInstallManagedHooks', () => {
+3
View File
@@ -8,6 +8,7 @@ import { getMainE2EConfig } from '../e2e-config'
import { DISABLED_CHROMIUM_FEATURES } from './disabled-chromium-features'
import { readHttp1CompatibilityMarker } from './http1-compatibility-marker'
import { checkServeUserDataPath } from './serve-user-data-path-guard'
import { stampRuntimeSourceEnv } from '../../shared/runtime-source-env'
import {
hasMissingProfileStateDatabaseWithRetainedAuthority,
readActiveProfileId,
@@ -243,6 +244,8 @@ export function configureOrcaUserDataPathEnv(): void {
}
// Why: relaunches can inherit a stale ORCA_USER_DATA_PATH; canonicalize before CLI-shared modules build runtime-home paths.
process.env.ORCA_USER_DATA_PATH = userDataPath
// Why: an Orca started from another Orca's terminal must not fence its own CLI children to the parent.
stampRuntimeSourceEnv(process.env, null)
}
export function shouldInstallManagedHooks(isDev: boolean): boolean {
+32
View File
@@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest'
import { readRuntimeSourceStamp, stampRuntimeSourceEnv } from './runtime-source-env'
describe('runtime source env', () => {
it('round-trips a stamp and replaces an inherited one', () => {
const env: Record<string, string | undefined> = {
ORCA_RUNTIME_SOURCE_ID: 'parent',
ORCA_RUNTIME_SOURCE_INCARNATION: 'parent-runtime'
}
stampRuntimeSourceEnv(env, { sourceId: 'child', incarnation: 'child-runtime' })
expect(readRuntimeSourceStamp(env)).toEqual({ sourceId: 'child', incarnation: 'child-runtime' })
})
it('clears an inherited stamp when this runtime has none', () => {
const env: Record<string, string | undefined> = {
ORCA_RUNTIME_SOURCE_ID: 'parent',
ORCA_RUNTIME_SOURCE_INCARNATION: 'parent-runtime'
}
stampRuntimeSourceEnv(env, null)
expect(env).toEqual({})
})
it('treats a partial or oversized stamp as no stamp', () => {
expect(readRuntimeSourceStamp({ ORCA_RUNTIME_SOURCE_ID: 'only-id' })).toBeNull()
expect(
readRuntimeSourceStamp({
ORCA_RUNTIME_SOURCE_ID: 'x'.repeat(4_097),
ORCA_RUNTIME_SOURCE_INCARNATION: 'runtime'
})
).toBeNull()
})
})
+50
View File
@@ -0,0 +1,50 @@
// Which Orca runtime launched a process. Kept separate from the orchestration compatibility stamp,
// whose WSL/SSH union carries caller attestation and must stay untouched.
export const RUNTIME_SOURCE_ID_ENV = 'ORCA_RUNTIME_SOURCE_ID'
export const RUNTIME_SOURCE_INCARNATION_ENV = 'ORCA_RUNTIME_SOURCE_INCARNATION'
export const RUNTIME_SOURCE_PROFILE_PATH_ENV = 'ORCA_RUNTIME_SOURCE_PROFILE_PATH'
export type RuntimeSourceStamp = {
/** The launching profile's installation id; survives restarts. */
sourceId: string
/** The launching runtime's per-process id; only a hint that skips the probe. */
incarnation: string
/** The launching runtime's userData path, so an explicit switch to another profile is not refused. */
profilePath?: string
}
const MAX_SOURCE_FIELD_LENGTH = 4_096
function boundedValue(value: string | undefined): string | undefined {
const trimmed = value?.trim()
return trimmed && trimmed.length <= MAX_SOURCE_FIELD_LENGTH ? trimmed : undefined
}
export function readRuntimeSourceStamp(
env: Readonly<Record<string, string | undefined>>
): RuntimeSourceStamp | null {
const sourceId = boundedValue(env[RUNTIME_SOURCE_ID_ENV])
const incarnation = boundedValue(env[RUNTIME_SOURCE_INCARNATION_ENV])
const profilePath = boundedValue(env[RUNTIME_SOURCE_PROFILE_PATH_ENV])
if (!sourceId || !incarnation) {
return null
}
return profilePath ? { sourceId, incarnation, profilePath } : { sourceId, incarnation }
}
/** Replaces any inherited stamp, so a nested Orca never passes on its parent's source. */
export function stampRuntimeSourceEnv(
env: Record<string, string | undefined>,
stamp: RuntimeSourceStamp | null
): void {
delete env[RUNTIME_SOURCE_ID_ENV]
delete env[RUNTIME_SOURCE_INCARNATION_ENV]
delete env[RUNTIME_SOURCE_PROFILE_PATH_ENV]
if (stamp) {
env[RUNTIME_SOURCE_ID_ENV] = stamp.sourceId
env[RUNTIME_SOURCE_INCARNATION_ENV] = stamp.incarnation
if (stamp.profilePath) {
env[RUNTIME_SOURCE_PROFILE_PATH_ENV] = stamp.profilePath
}
}
}