Files
orca/src/shared/agent-session-caller-env.ts
T
Brennan Benson d3182d4bf1 Native chat: every agent launches through one composed launch path (#27204)
* 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).
2026-10-11 23:27:04 -07:00

43 lines
1.8 KiB
TypeScript

/**
* The Orca-minted agent session id, injected into a structured session's own child processes. When
* it is present it IS the orchestration caller: the CLI sends it in the orchestration envelope and
* the host resolves the session it names, 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: the host refuses a claim that arrives across one, and the SSH
* passthrough never carries the id.
*/
import { ORCA_SESSION_ADDRESS_PREFIX } from './orca-session-address-prefix'
import { isStructuredWorkerHandle } from './structured-worker-handle'
export const ORCA_AGENT_SESSION_ID_ENV = 'ORCA_AGENT_SESSION_ID'
/** The spawn token a structured session's child echoes, so the owner probe can tell a live child
* of that reservation from a same-pid stranger. */
export const ORCA_AGENT_SESSION_SPAWN_TOKEN_ENV = 'ORCA_AGENT_SESSION_SPAWN_TOKEN'
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
}
/**
* The address the host gives this session (`mailboxAddressOf` on its resolved party): a structured
* worker keeps the handle it was minted, any other session is `orca_session_id:<id>`. Only for text that
* must match what the host writes; the CLI spells it without the host resolver.
*/
export function injectedSessionAddress(
env: Readonly<Record<string, string | undefined>> = process.env
): string | undefined {
const sessionId = readInjectedAgentSessionId(env)
if (!sessionId) {
return undefined
}
const ownHandle = env.ORCA_TERMINAL_HANDLE
return isStructuredWorkerHandle(ownHandle)
? ownHandle
: `${ORCA_SESSION_ADDRESS_PREFIX}${sessionId}`
}