mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
* 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>
139 lines
4.9 KiB
TypeScript
139 lines
4.9 KiB
TypeScript
import { createHmac, randomUUID } from 'node:crypto'
|
|
import { readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { join, resolve } from 'node:path'
|
|
import {
|
|
RuntimeHostDescriptorSchema,
|
|
type RuntimeHostDescriptor
|
|
} 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
|
|
const InstallationFileSchema = RuntimeHostDescriptorSchema.pick({ installationId: true })
|
|
|
|
type InstallationRead =
|
|
| { kind: 'valid'; installationId: string }
|
|
| { kind: 'missing' }
|
|
| { kind: 'malformed' }
|
|
|
|
function errorCode(error: unknown): unknown {
|
|
return error instanceof Error && 'code' in error ? error.code : undefined
|
|
}
|
|
|
|
function readInstallation(filePath: string): InstallationRead {
|
|
let raw: string
|
|
try {
|
|
raw = readFileSync(filePath, 'utf8')
|
|
} catch (error) {
|
|
if (errorCode(error) === 'ENOENT') {
|
|
return { kind: 'missing' }
|
|
}
|
|
// Why throw: regenerating over a file we merely cannot read would silently fork this host's identity.
|
|
throw error
|
|
}
|
|
if (raw.length > MAX_INSTALLATION_FILE_BYTES) {
|
|
return { kind: 'malformed' }
|
|
}
|
|
try {
|
|
const parsed = InstallationFileSchema.safeParse(JSON.parse(raw))
|
|
return parsed.success
|
|
? { kind: 'valid', installationId: parsed.data.installationId }
|
|
: { kind: 'malformed' }
|
|
} catch {
|
|
return { kind: 'malformed' }
|
|
}
|
|
}
|
|
|
|
/** Reads this profile's installation id, creating it once; concurrent creators converge on one. */
|
|
export async function loadOrCreateHostInstallationId(userDataPath: string): Promise<string> {
|
|
const filePath = join(userDataPath, HOST_INSTALLATION_FILENAME)
|
|
const current = readInstallation(filePath)
|
|
if (current.kind === 'valid') {
|
|
return current.installationId
|
|
}
|
|
// Why the lock and a re-read: a creator or repairer decides only after any racing one has published.
|
|
return await withFileTransactionLock(filePath, async () => {
|
|
const locked = readInstallation(filePath)
|
|
if (locked.kind === 'valid') {
|
|
return locked.installationId
|
|
}
|
|
const installationId = randomUUID()
|
|
const draftPath = `${filePath}.${installationId}.draft`
|
|
try {
|
|
writeFileSync(draftPath, JSON.stringify({ installationId }), { flag: 'wx', mode: 0o600 })
|
|
renameSync(draftPath, filePath)
|
|
} finally {
|
|
rmSync(draftPath, { force: true })
|
|
}
|
|
return installationId
|
|
})
|
|
}
|
|
|
|
/** Keyed by the installation id so the binding never works as a cross-app machine identifier. */
|
|
export function computeHostMachineBinding(
|
|
installationId: string,
|
|
machineIdentity: string,
|
|
userDataPath: string
|
|
): string {
|
|
return createHmac('sha256', installationId)
|
|
.update(`${machineIdentity}\0${resolve(userDataPath)}`)
|
|
.digest('base64url')
|
|
}
|
|
|
|
/** Null when the profile cannot hold an installation id; the binding is dropped when no machine id is readable. */
|
|
export async function loadHostDescriptor(
|
|
userDataPath: string,
|
|
readMachineIdentity: () => string | null = profileStateAccessMachineIdentity
|
|
): Promise<RuntimeHostDescriptor | null> {
|
|
let installationId: string
|
|
try {
|
|
installationId = await loadOrCreateHostInstallationId(userDataPath)
|
|
} catch (error) {
|
|
console.warn('[runtime] Host descriptor unavailable:', error)
|
|
return null
|
|
}
|
|
const machineIdentity = readMachineIdentity()
|
|
return machineIdentity
|
|
? {
|
|
installationId,
|
|
machineBinding: computeHostMachineBinding(installationId, machineIdentity, userDataPath)
|
|
}
|
|
: { installationId }
|
|
}
|
|
|
|
// Keyed by runtime id because the RPC server that owns the profile loads it, while status.get only sees the runtime.
|
|
const publishedByRuntimeId = new Map<string, RuntimeHostDescriptor>()
|
|
|
|
export function publishHostDescriptor(
|
|
runtimeId: string,
|
|
descriptor: RuntimeHostDescriptor | null
|
|
): void {
|
|
if (descriptor) {
|
|
publishedByRuntimeId.set(runtimeId, descriptor)
|
|
} else {
|
|
publishedByRuntimeId.delete(runtimeId)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|