mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 10:37:35 +00:00
* test(chat): snapshot every native-chat agent's spawned child as a parity oracle Captures the exact child each native-chat agent spawns (Claude, Codex, Grok, OpenCode, OMP, Pi), worktree and floating, fresh and resumed, plus a registered worker with visuals off, at the real spawn boundary (the process host's spawn) and through production's own host dependency wiring. The inherited environment holds every pane, hook, caller, terminal-handle and visuals key a parent Orca pane could hand down, so the snapshot records today's per-agent differences as they are. Each case also records the launch's permission posture, which for Codex (thread policy) and the ACP agents (full access) acts only after spawn. Each agent also runs fresh and resumed with every launch setting moved off its default: a Command setting naming another binary, the permission posture flipped, an environment overlay over inherited and shell keys, and only named login-shell variables inherited (the fake login shell carries its own PATH and a variable no case names). A second test fails when a registered agent has no snapshot, since the oracle lists its agents by hand. Test only: the launch-path refactor that follows must leave both tests and the snapshot file unchanged. * refactor(chat): one composed launch for every native-chat agent Every native-chat agent (Claude, Codex, Grok, OpenCode, OMP, Pi) now builds its child through one composer instead of six hand-written resolvers. Each agent contributes only its own part (command, args, agent env, keys it must strip); the shared composer admits the session record, reads the launch directory, base environment, launch overrides and visuals once each (memoized, in the agent's original order), sets cwd, and seals the child. The seal (sealStructuredSessionChild) is the one place that names a child: session id, structured marker, worker terminal handle, the Orca CLI env, the spawn token under one shared constant, and the per-launch list of inherited keys to delete, minus any key the seal itself sets. Claude's delete list now reaches its SDK spawn after buildClaudeChildProcessEnv's merge. Behaviour is unchanged: the parity snapshot from the previous commit passes byte for byte, including each agent's existing differences (Claude and Codex keep inherited pane and hook env, Pi has no visuals and always full access, the ACP history read stays unsealed). Admission refusals keep their reasons; only log-only wording moved to the shared check. Drops launch dependencies no production caller wires (resolveLaunchEnv, resolveCodexOverrides) and folds the per-agent env overlay hooks into the existing resolveAgentLaunchEnv. * refactor(chat): only the seal names a native-chat child Follow-ups from review of the composed launch: - The spawn token is required on every resolveLaunch. The one read that starts no child, ACP's restart-recovery history read, now has its own path (composeStructuredLaunchRead, deps.readLaunch) that returns the agent's own env unsealed, as before. Agent parts receive the session identity only, never the token. - structuredSessionChildIdentityEnv is gone; the seal is the only way to build a child's identity env, and tests go through it. - A launch's environment is the runtime's resolveAgentEnvironment, so the base-plus-overlay rule exists once. - Claude's connection test now covers a non-empty envToDelete applied after the inherited env is merged. No change to any spawned child: the parity snapshot is unchanged. * ci: run the login-shell contract job for the renamed child-env seal The seal moved from structured-session-child-identity-env to structured-session-child-env, but the PR job classifier still listed the old path, so a change to only the seal would skip the live login-shell contract job. The prefix now covers the seal, its test and its test support (which the login-shell harness imports).
64 lines
2.4 KiB
TypeScript
64 lines
2.4 KiB
TypeScript
/**
|
|
* Host scan for processes carrying an Orca agent-session spawn token.
|
|
*
|
|
* The token is the only PID-reuse-safe identity element a child is guaranteed to carry, and it
|
|
* lives in the child's environment — which only Linux lets another process read (`/proc/<pid>/environ`).
|
|
* macOS and Windows answer `null`, meaning "this host cannot enumerate", NEVER "no process carries
|
|
* it": reporting an empty result there would free a reservation whose child is alive and hand a
|
|
* second writer to the same provider session.
|
|
*/
|
|
|
|
import { readFile, readdir } from 'node:fs/promises'
|
|
import { ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV } from '../../shared/agent-session-caller-env'
|
|
import { spawnTokenFromEnvironBlock } from './agent-session-spawn-token-readback'
|
|
|
|
export type AgentSessionSpawnTokenScan = ReadonlyMap<string, readonly number[]>
|
|
|
|
export type AgentSessionSpawnTokenScanEvidence =
|
|
| { status: 'verified'; processes: AgentSessionSpawnTokenScan }
|
|
| { status: 'unverifiable'; processes: null; platform: NodeJS.Platform }
|
|
|
|
/** Tokens observed on this host, or null when the platform cannot answer at all. */
|
|
export async function scanAgentSessionSpawnTokenProcesses(
|
|
platform: NodeJS.Platform = process.platform,
|
|
variable: string = ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV
|
|
): Promise<AgentSessionSpawnTokenScan | null> {
|
|
if (platform !== 'linux') {
|
|
return null
|
|
}
|
|
let entries: string[]
|
|
try {
|
|
entries = await readdir('/proc')
|
|
} catch {
|
|
return null
|
|
}
|
|
const observed = new Map<string, number[]>()
|
|
for (const entry of entries) {
|
|
const pid = Number(entry)
|
|
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
continue
|
|
}
|
|
let token: string | null
|
|
try {
|
|
token = spawnTokenFromEnvironBlock(await readFile(`/proc/${pid}/environ`, 'utf-8'), variable)
|
|
} catch {
|
|
// A process that exited mid-scan, or one this user may not read, is not evidence either way.
|
|
continue
|
|
}
|
|
if (token === null) {
|
|
continue
|
|
}
|
|
observed.set(token, [...(observed.get(token) ?? []), pid])
|
|
}
|
|
return observed
|
|
}
|
|
|
|
/** Pids carrying one specific token, or null when the host could not enumerate. */
|
|
export async function findAgentSessionSpawnTokenProcesses(
|
|
spawnToken: string,
|
|
scan: () => Promise<AgentSessionSpawnTokenScan | null> = scanAgentSessionSpawnTokenProcesses
|
|
): Promise<number[] | null> {
|
|
const observed = await scan()
|
|
return observed === null ? null : [...(observed.get(spawnToken) ?? [])]
|
|
}
|