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).
138 lines
5.8 KiB
TypeScript
138 lines
5.8 KiB
TypeScript
// Each agent's composed launch resolver over test doubles, built the way its registration builds
|
|
// it, so resolver tests exercise the shared launch path rather than a copy of it.
|
|
|
|
import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types'
|
|
import type { AcpLaunchSpec } from '../acp/acp-launch-specs'
|
|
import { acpStructuredAgentDefinition } from '../acp/acp-structured-agent-definitions'
|
|
import {
|
|
acpStructuredLaunchPart,
|
|
type AcpStructuredLaunchPartDeps
|
|
} from '../acp/acp-structured-launch-resolution'
|
|
import { CLAUDE_STRUCTURED_AGENT } from '../claude/claude-structured-agent-definition'
|
|
import type { ClaudeEnvDeps } from '../claude/claude-structured-child-env'
|
|
import {
|
|
claudeStructuredLaunchPart,
|
|
type ClaudeStructuredLaunchPartDeps
|
|
} from '../claude/claude-structured-launch-resolution'
|
|
import { CODEX_STRUCTURED_AGENT } from '../codex/codex-structured-agent-definition'
|
|
import {
|
|
codexStructuredLaunchPart,
|
|
type CodexStructuredLaunchPartDeps
|
|
} from '../codex/codex-structured-launch-resolution'
|
|
import type { PrepareNativeChatVisuals } from '../native-chat/native-chat-visuals-delivery'
|
|
import type { StructuredAgentCommandSettings } from '../native-chat/structured-agent-command-resolution'
|
|
import { PI_RPC_AGENT } from '../pi/rpc-agent-definition'
|
|
import { piRpcLaunchPart, type PiRpcLaunchPartDeps } from '../pi/rpc-launch-resolution'
|
|
import type { StructuredAgentDefinition } from '../native-chat/agent-session-wire/structured-agent-definition'
|
|
import {
|
|
composeStructuredLaunch,
|
|
composeStructuredLaunchRead,
|
|
type StructuredAgentLaunchPartResolver,
|
|
type StructuredAgentLaunchSources
|
|
} from './structured-agent-launch-composition'
|
|
|
|
export const TEST_SPAWN_TOKEN = 'test-spawn-token'
|
|
|
|
export type LaunchTestSources = Pick<StructuredAgentLaunchSources, 'store'> & {
|
|
resolveWorkspacePath?: (workspaceId: string) => Promise<string>
|
|
/** The base env; none is an empty one. */
|
|
resolveEnvironment?: () => Promise<NodeJS.ProcessEnv>
|
|
resolveLaunchEnv?: (agent: string) => Record<string, string>
|
|
resolveCommandSettings?: () => StructuredAgentCommandSettings
|
|
prepareVisuals?: PrepareNativeChatVisuals
|
|
}
|
|
|
|
function definedEnv(env: NodeJS.ProcessEnv): Record<string, string> {
|
|
return Object.fromEntries(
|
|
Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined)
|
|
)
|
|
}
|
|
|
|
function launchSources(deps: LaunchTestSources): StructuredAgentLaunchSources {
|
|
const resolveEnvironment = deps.resolveEnvironment
|
|
const resolveBaseEnvironment = async () => definedEnv((await resolveEnvironment?.()) ?? {})
|
|
return {
|
|
store: deps.store,
|
|
resolveWorkspacePath: deps.resolveWorkspacePath ?? (async (id) => `/repos/${id}`),
|
|
resolveBaseEnvironment,
|
|
resolveAgentEnvironment: async (agent) => ({
|
|
...(await resolveBaseEnvironment()),
|
|
...deps.resolveLaunchEnv?.(agent)
|
|
}),
|
|
...(deps.resolveLaunchEnv ? { resolveAgentLaunchEnv: deps.resolveLaunchEnv } : {}),
|
|
...(deps.resolveCommandSettings ? { resolveCommandSettings: deps.resolveCommandSettings } : {}),
|
|
...(deps.prepareVisuals ? { prepareVisuals: deps.prepareVisuals } : {})
|
|
}
|
|
}
|
|
|
|
/** Sealed with a spawn token, as an adapter's start calls it; with none, the unsealed read. */
|
|
function withTestSpawnToken<L>(
|
|
definition: StructuredAgentDefinition,
|
|
sources: StructuredAgentLaunchSources,
|
|
part: StructuredAgentLaunchPartResolver<L>,
|
|
defaultToken: string | null
|
|
) {
|
|
const resolve = composeStructuredLaunch(definition, sources, part)
|
|
const read = composeStructuredLaunchRead(definition, sources, part)
|
|
return (input: { identity: AgentSessionJournalIdentity; spawnToken?: string | null }) => {
|
|
const spawnToken = input.spawnToken === undefined ? defaultToken : input.spawnToken
|
|
return spawnToken === null
|
|
? read(input.identity)
|
|
: resolve({ identity: input.identity, spawnToken })
|
|
}
|
|
}
|
|
|
|
/** Unsealed unless a token is passed: the env an agent part produced, before the seal. */
|
|
export function createCodexStructuredLaunchResolver(
|
|
deps: LaunchTestSources & CodexStructuredLaunchPartDeps
|
|
) {
|
|
return withTestSpawnToken(
|
|
CODEX_STRUCTURED_AGENT,
|
|
launchSources(deps),
|
|
codexStructuredLaunchPart(deps),
|
|
null
|
|
)
|
|
}
|
|
|
|
/** Unsealed unless a token is passed: the env an agent part produced, before the seal. */
|
|
export function createAcpStructuredLaunchResolver(
|
|
spec: AcpLaunchSpec,
|
|
deps: LaunchTestSources & AcpStructuredLaunchPartDeps
|
|
) {
|
|
return withTestSpawnToken(
|
|
acpStructuredAgentDefinition(spec),
|
|
launchSources(deps),
|
|
acpStructuredLaunchPart(spec, deps),
|
|
null
|
|
)
|
|
}
|
|
|
|
/** Unsealed unless a token is passed: the env an agent part produced, before the seal. */
|
|
export function createPiRpcLaunchResolver(deps: LaunchTestSources & PiRpcLaunchPartDeps) {
|
|
const resolve = withTestSpawnToken(PI_RPC_AGENT, launchSources(deps), piRpcLaunchPart(deps), null)
|
|
return (identity: AgentSessionJournalIdentity, spawnToken: string | null = null) =>
|
|
resolve({ identity, spawnToken })
|
|
}
|
|
|
|
/** Sealed with `TEST_SPAWN_TOKEN` unless the test passes another: Claude's launch env has always
|
|
* carried the session's identity. `resolveEnv` is Claude's overlay; `resolveInheritedEnv`, its
|
|
* base, defaults to this process's env as a launch without one inherits it. */
|
|
export function createClaudeStructuredLaunchResolver(
|
|
deps: Omit<LaunchTestSources, 'resolveEnvironment' | 'resolveLaunchEnv'> &
|
|
Pick<ClaudeEnvDeps, 'resolveInheritedEnv'> & {
|
|
resolveEnv?: () => Record<string, string> | undefined
|
|
} & ClaudeStructuredLaunchPartDeps
|
|
) {
|
|
const { resolveEnv, resolveInheritedEnv } = deps
|
|
return withTestSpawnToken(
|
|
CLAUDE_STRUCTURED_AGENT,
|
|
launchSources({
|
|
...deps,
|
|
resolveEnvironment: resolveInheritedEnv ?? (async () => process.env),
|
|
...(resolveEnv ? { resolveLaunchEnv: () => resolveEnv() ?? {} } : {})
|
|
}),
|
|
claudeStructuredLaunchPart(deps),
|
|
TEST_SPAWN_TOKEN
|
|
)
|
|
}
|