Merge commit '1d7a3938a1' into brennanb2025/claude-sdk-ready-mainsync

# Conflicts:
#	src/main/claude/claude-structured-launch-resolution.ts
#	src/main/runtime/orca-runtime-get-worktree-ps.ts
#	src/main/runtime/structured-agent-session-runtime.ts
#	src/main/runtime/structured-claude-runtime-adapter.ts
This commit is contained in:
Merge Sim
2026-09-03 14:38:21 -07:00
11 changed files with 277 additions and 64 deletions
@@ -5,6 +5,8 @@ import { describe, expect, it } from 'vitest'
import type { AgentSessionRecord } from '../../shared/agent-session-record'
import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
import type { AgentSessionRecordStore } from '../runtime/agent-session-record-store'
import { AgentSessionPreSpawnError } from '../native-chat/agent-session-wire/structured-agent-session-adapter'
import type { ClaudeManagedAccountGateSettings } from '../native-chat/claude-structured-managed-account-support'
import {
CLAUDE_DEFAULT_SETTING_SOURCES,
CLAUDE_STRUCTURED_BASE_OPTIONS,
@@ -63,6 +65,39 @@ function resolverFor(
})
}
function managedAccount(id: string, managedAuthRuntime: 'host' | 'wsl') {
return {
id,
email: `${id}@example.com`,
managedAuthPath: `/managed/${id}`,
managedAuthRuntime,
authMethod: 'subscription-oauth' as const,
createdAt: 0,
updatedAt: 0,
lastAuthenticatedAt: 0
}
}
const HOST_SELECTED: ClaudeManagedAccountGateSettings = {
claudeManagedAccounts: [managedAccount('host-1', 'host')],
activeClaudeManagedAccountId: 'host-1',
activeClaudeManagedAccountIdsByRuntime: { host: 'host-1', wsl: {} }
}
/** The normalized steady state of a Windows user whose only Claude account is WSL-managed: the
* prune drops the WSL account out of the host slot and persists that. */
const WSL_ONLY_NORMALIZED: ClaudeManagedAccountGateSettings = {
claudeManagedAccounts: [managedAccount('wsl-1', 'wsl')],
activeClaudeManagedAccountId: null,
activeClaudeManagedAccountIdsByRuntime: { host: null, wsl: { Ubuntu: 'wsl-1' } }
}
const RESUMABLE = record({
providerHandleChain: [
{ handle: { provider: 'claude', sessionId: 'provider-current', leafUuid: 'leaf-current' } }
] as AgentSessionRecord['providerHandleChain']
})
describe('claude structured launch resolution', () => {
it('pre-mints a stable provider id and pins interactive setting sources', async () => {
const first = await resolverFor(record())({ identity: IDENTITY })
@@ -301,4 +336,48 @@ describe('claude structured launch resolution', () => {
})
).rejects.toThrow(/CLAUDE_CONFIG_DIR/)
})
/** The account state can change while a session lives, and a reacquire after an unexpected child
* exit re-resolves the launch. Without the gate here, that reacquire spawns under whatever the
* account state has become. */
describe('managed-account gate on every acquisition', () => {
function resolverWithGate(read: () => ClaudeManagedAccountGateSettings | null) {
return createClaudeStructuredLaunchResolver({
store: { getRecord: () => RESUMABLE } as unknown as AgentSessionRecordStore,
resolveWorkspacePath: async (id) => `/repos/${id}`,
resolveCommand: () => '/usr/local/bin/claude',
resolveAuthPolicy: () => ({ stripAuthEnv: false }),
readManagedAccountGate: read
})
}
it('refuses a reacquire once the account state becomes the refused shape', async () => {
let gate: ClaudeManagedAccountGateSettings | null = HOST_SELECTED
const resolve = resolverWithGate(() => gate)
// Created while supported: the launch resolves and would spawn.
await expect(resolve({ identity: identityAt('leaf-current') })).resolves.toMatchObject({
providerSessionId: 'provider-current'
})
gate = WSL_ONLY_NORMALIZED
// Reacquire after the account state changed: refused before anything spawns.
await expect(resolve({ identity: identityAt('leaf-current') })).rejects.toBeInstanceOf(
AgentSessionPreSpawnError
)
})
it('fails closed when the account state cannot be read', async () => {
await expect(
resolverWithGate(() => null)({ identity: identityAt('leaf-current') })
).rejects.toBeInstanceOf(AgentSessionPreSpawnError)
})
it('keeps resolving when no gate is wired, so other embedders are unaffected', async () => {
await expect(
resolverFor(RESUMABLE)({ identity: identityAt('leaf-current') })
).resolves.toMatchObject({ providerSessionId: 'provider-current' })
})
})
})
@@ -15,6 +15,11 @@ import {
CLAUDE_AUTH_SWITCH_SETTLE_TIMEOUT_MS,
whenClaudeAuthSwitchSettles
} from '../claude-accounts/live-pty-gate'
import { AgentSessionPreSpawnError } from '../native-chat/agent-session-wire/structured-agent-session-adapter'
import {
structuredClaudeMatchesActiveManagedAccount,
type ClaudeManagedAccountGateSettings
} from '../native-chat/claude-structured-managed-account-support'
import { resolveClaudeCommand } from '../codex-cli/command'
import type { AgentSessionRecordStore } from '../runtime/agent-session-record-store'
@@ -137,6 +142,8 @@ export type ClaudeStructuredLaunchResolverDeps = {
resolveAuthPolicy: () => Promise<ClaudeStructuredAuthPolicy> | ClaudeStructuredAuthPolicy
/** How long an in-flight account switch may hold a launch before it is refused. */
authSwitchSettleTimeoutMs?: number
/** Account state for the managed-account gate; null when it cannot be read, which refuses. */
readManagedAccountGate?: () => ClaudeManagedAccountGateSettings | null
}
/**
@@ -186,6 +193,17 @@ export function createClaudeStructuredLaunchResolver(
if (record.accountHome.variable !== 'CLAUDE_CONFIG_DIR') {
throw new Error(`claude sessions pin CLAUDE_CONFIG_DIR, not ${record.accountHome.variable}`)
}
// Every acquisition, not just the first: the account state can change under a live session, and
// a reacquire after an unexpected exit would otherwise spawn under whatever it has become.
// Codex has no gate here — it resolves its account on a different path.
if (
deps.readManagedAccountGate &&
!structuredClaudeMatchesActiveManagedAccount(deps.readManagedAccountGate())
) {
throw new AgentSessionPreSpawnError(
'structured Claude is not offered under the active managed Claude account'
)
}
const head = agentSessionProviderHandleChainHead(record.providerHandleChain)
if (
head?.handle.provider === 'claude' &&
@@ -1,11 +1,15 @@
import { describe, expect, it } from 'vitest'
import type { ClaudeRateLimitAccountsState } from '../../shared/managed-account-types'
import { structuredClaudeMatchesActiveManagedAccount } from './claude-structured-managed-account-support'
import { getSelectedClaudeAccountIdForTarget } from '../claude-accounts/runtime-selection'
import {
structuredClaudeMatchesActiveManagedAccount,
type ClaudeManagedAccountGateSettings
} from './claude-structured-managed-account-support'
function account(id: string, managedAuthRuntime: 'host' | 'wsl') {
return {
id,
email: `${id}@example.com`,
managedAuthPath: `/managed/${id}`,
managedAuthRuntime,
authMethod: 'subscription-oauth' as const,
createdAt: 0,
@@ -14,37 +18,34 @@ function account(id: string, managedAuthRuntime: 'host' | 'wsl') {
}
}
function state(overrides: Partial<ClaudeRateLimitAccountsState>): ClaudeRateLimitAccountsState {
return { accounts: [], activeAccountId: null, ...overrides }
function settings(
overrides: Partial<ClaudeManagedAccountGateSettings>
): ClaudeManagedAccountGateSettings {
return { claudeManagedAccounts: [], activeClaudeManagedAccountId: null, ...overrides }
}
describe('structuredClaudeMatchesActiveManagedAccount', () => {
it('allows an unmanaged install, where nothing claims an identity', () => {
expect(structuredClaudeMatchesActiveManagedAccount(state({}))).toBe(true)
expect(structuredClaudeMatchesActiveManagedAccount(settings({}))).toBe(true)
})
it('allows a selected host account, which the runtime syncs into the ambient config', () => {
expect(
structuredClaudeMatchesActiveManagedAccount(
state({
accounts: [account('host-1', 'host')],
activeAccountIdsByRuntime: { host: 'host-1', wsl: {} }
settings({
claudeManagedAccounts: [account('host-1', 'host')],
activeClaudeManagedAccountIdsByRuntime: { host: 'host-1', wsl: {} }
})
)
).toBe(true)
expect(
structuredClaudeMatchesActiveManagedAccount(
state({ accounts: [account('host-1', 'host')], activeAccountId: 'host-1' })
)
).toBe(true)
})
it('refuses a WSL-only managed account, which never reaches the ambient config', () => {
expect(
structuredClaudeMatchesActiveManagedAccount(
state({
accounts: [account('wsl-1', 'wsl')],
activeAccountIdsByRuntime: { host: null, wsl: { Ubuntu: 'wsl-1' } }
settings({
claudeManagedAccounts: [account('wsl-1', 'wsl')],
activeClaudeManagedAccountIdsByRuntime: { host: null, wsl: { Ubuntu: 'wsl-1' } }
})
)
).toBe(false)
@@ -53,30 +54,55 @@ describe('structuredClaudeMatchesActiveManagedAccount', () => {
it('refuses when a host selection names an account that is WSL-bound or missing', () => {
expect(
structuredClaudeMatchesActiveManagedAccount(
state({
accounts: [account('wsl-1', 'wsl')],
activeAccountIdsByRuntime: { host: 'wsl-1', wsl: {} }
settings({
claudeManagedAccounts: [account('wsl-1', 'wsl')],
activeClaudeManagedAccountIdsByRuntime: { host: 'wsl-1', wsl: {} }
})
)
).toBe(false)
expect(
structuredClaudeMatchesActiveManagedAccount(
state({
accounts: [account('host-1', 'host')],
activeAccountIdsByRuntime: { host: 'gone', wsl: {} }
settings({
claudeManagedAccounts: [account('host-1', 'host')],
activeClaudeManagedAccountIdsByRuntime: { host: 'gone', wsl: {} }
})
)
).toBe(false)
})
it('fails closed when the account state cannot be read at all', () => {
it('fails closed when the settings cannot be read at all', () => {
expect(structuredClaudeMatchesActiveManagedAccount(null)).toBe(false)
expect(structuredClaudeMatchesActiveManagedAccount(undefined)).toBe(false)
})
it('fails closed when managed accounts exist but none is active', () => {
it('fails closed when managed accounts exist but no host account is selected', () => {
expect(
structuredClaudeMatchesActiveManagedAccount(state({ accounts: [account('host-1', 'host')] }))
structuredClaudeMatchesActiveManagedAccount(
settings({ claudeManagedAccounts: [account('host-1', 'host')] })
)
).toBe(false)
})
/** The gate and the auth policy must resolve the SAME account. A legacy settings blob carries the
* selection only in the flat `activeClaudeManagedAccountId`, which is where the accessor's
* fall-through lives — reading the runtime map directly silently disagrees with the policy. */
it('resolves the same account as the auth policy on a legacy flat selection', () => {
const legacy = settings({
claudeManagedAccounts: [account('host-1', 'host')],
activeClaudeManagedAccountId: 'host-1'
})
expect(getSelectedClaudeAccountIdForTarget(legacy, { runtime: 'host' })).toBe('host-1')
expect(structuredClaudeMatchesActiveManagedAccount(legacy)).toBe(true)
})
it('agrees with the auth policy that a legacy flat WSL selection is refused', () => {
const legacy = settings({
claudeManagedAccounts: [account('wsl-1', 'wsl')],
activeClaudeManagedAccountId: 'wsl-1'
})
expect(getSelectedClaudeAccountIdForTarget(legacy, { runtime: 'host' })).toBe('wsl-1')
expect(structuredClaudeMatchesActiveManagedAccount(legacy)).toBe(false)
})
})
@@ -1,4 +1,12 @@
import type { ClaudeRateLimitAccountsState } from '../../shared/managed-account-types'
import type { GlobalSettings } from '../../shared/global-settings-types'
import { getSelectedClaudeAccountIdForTarget } from '../claude-accounts/runtime-selection'
export type ClaudeManagedAccountGateSettings = Pick<
GlobalSettings,
| 'claudeManagedAccounts'
| 'activeClaudeManagedAccountId'
| 'activeClaudeManagedAccountIdsByRuntime'
>
/**
* A structured Claude session launches against the ambient Claude config, which the account service
@@ -8,22 +16,38 @@ import type { ClaudeRateLimitAccountsState } from '../../shared/managed-account-
* another. Refuse the structured path there and let the terminal-backed one, which resolves the
* account per runtime, handle that account shape.
*
* Unknown answers refuse: an install with no managed accounts claims no identity and is fine, but
* an active selection this cannot resolve is not evidence that the ambient identity is right.
* Reads the selection through the same accessor the auth policy uses. Resolving it any other way
* lets the two disagree, and a session admitted by this gate would then run under a policy computed
* from a different account than the one approved here.
*
* Unknown answers refuse: an install with no managed accounts claims no identity and is fine, but a
* selection this cannot resolve is not evidence that the ambient identity is right.
*/
export function structuredClaudeMatchesActiveManagedAccount(
accounts: ClaudeRateLimitAccountsState | null | undefined
settings: ClaudeManagedAccountGateSettings | null | undefined
): boolean {
if (!accounts) {
const accounts = settings?.claudeManagedAccounts
if (!settings || !Array.isArray(accounts)) {
return false
}
if (accounts.accounts.length === 0) {
if (accounts.length === 0) {
return true
}
const activeHostId = accounts.activeAccountIdsByRuntime?.host ?? accounts.activeAccountId ?? null
const activeHostId = getSelectedClaudeAccountIdForTarget(settings, { runtime: 'host' })
if (!activeHostId) {
return false
}
const active = accounts.accounts.find((candidate) => candidate.id === activeHostId)
const active = accounts.find((candidate) => candidate.id === activeHostId)
return active ? active.managedAuthRuntime !== 'wsl' : false
}
/** Reads the gate's settings, answering null when they cannot be read so callers refuse. */
export function readClaudeManagedAccountGateSettings(
getSettings: () => ClaudeManagedAccountGateSettings
): ClaudeManagedAccountGateSettings | null {
try {
return getSettings()
} catch {
return null
}
}
@@ -15,6 +15,7 @@ import type { Repo } from '../../shared/repo-types'
import { enrichMissingRepoGitRemoteIdentities } from '../repo-git-remote-identity-enrichment'
import { ensureStructuredAgentSessionHost as installStructuredAgentSessionHost } from './structured-agent-session-runtime'
import { getProfileUserDataPath } from '../orca-profiles/profile-storage-paths'
import { readClaudeManagedAccountGateSettings } from '../native-chat/claude-structured-managed-account-support'
import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
import {
resolveTuiAgentLaunchArgs,
@@ -159,6 +160,9 @@ export class OrcaRuntimeWithGetWorktreePs extends OrcaRuntimeWithStructuredAgent
resolveTuiAgentLaunchEnv('claude', this.requireStore().getSettings().agentDefaultEnv),
resolveClaudeAuthPolicy: () =>
claudeStructuredAuthPolicyForSettings(this.requireStore().getSettings()),
// Same gate and same settings as agentSession.createSupport, re-read on every acquisition.
readClaudeManagedAccountGate: () =>
readClaudeManagedAccountGateSettings(() => this.requireStore().getSettings()),
handoffTransport: this.createStructuredAgentSessionHandoffTransport()
})
}
@@ -4,7 +4,10 @@ import type { AgentSessionOwnerBinding } from '../../shared/agent-session-host-a
import { agentSessionOwnerBindingsEqual } from '../../shared/claimed-agent-pty-owner-snapshot'
import { resolvePinnedCodexRolloutProof } from '../codex/codex-tui-rollout-proof'
import { getStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry'
import { structuredClaudeMatchesActiveManagedAccount } from '../native-chat/claude-structured-managed-account-support'
import {
readClaudeManagedAccountGateSettings,
structuredClaudeMatchesActiveManagedAccount
} from '../native-chat/claude-structured-managed-account-support'
import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
import type { AgentStatusIpcPayload } from '../../shared/agent-status-types'
import { getLocalProjectWorktreeGitOptions } from '../project-runtime-git-options'
@@ -50,14 +53,19 @@ export class OrcaRuntimeWithResolveRecoveredStructuredTuiTranscript extends Orca
worktreeSelector: string,
agent: 'claude' | 'codex'
): Promise<{ supported: boolean; reason?: 'agent' | 'remote' | 'wsl' }> {
const accountState = agent === 'claude' ? this.accounts.readClaudeManagedAccounts() : null
// Same settings the auth policy reads, so the gate and the policy cannot resolve different
// accounts. Unreadable settings fail closed below.
const accountSettings =
agent === 'claude'
? readClaudeManagedAccountGateSettings(() => this.requireStore().getSettings())
: null
const location = await this.resolveStructuredAgentSessionLocation(worktreeSelector)
await this.ensureStructuredAgentSessionHost()
if (getStructuredAgentSessionHost()?.supportsCreate(location, agent)) {
// Claude only: Codex resolves its account through a different path, so its answer is
// untouched here. `wsl` is the closest existing reason — the cause is a WSL-bound account
// rather than a WSL workspace — and no client reads the field, so it stays as-is.
if (agent === 'claude' && !structuredClaudeMatchesActiveManagedAccount(accountState)) {
if (agent === 'claude' && !structuredClaudeMatchesActiveManagedAccount(accountSettings)) {
return { supported: false, reason: 'wsl' }
}
return { supported: true }
@@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import { OrcaRuntimeService } from './orca-runtime'
import { setStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry'
import type { StructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-host'
import type { ClaudeRateLimitAccountsState } from '../../shared/managed-account-types'
import type { ClaudeManagedAccountGateSettings } from '../native-chat/claude-structured-managed-account-support'
vi.mock('electron', () => ({
BrowserWindow: { fromId: vi.fn(() => null) },
@@ -15,6 +15,7 @@ function managedAccount(id: string, managedAuthRuntime: 'host' | 'wsl') {
return {
id,
email: `${id}@example.com`,
managedAuthPath: `/managed/${id}`,
managedAuthRuntime,
authMethod: 'subscription-oauth' as const,
createdAt: 0,
@@ -23,27 +24,23 @@ function managedAccount(id: string, managedAuthRuntime: 'host' | 'wsl') {
}
}
const WSL_ONLY: ClaudeRateLimitAccountsState = {
accounts: [managedAccount('wsl-1', 'wsl')],
activeAccountId: null,
activeAccountIdsByRuntime: { host: null, wsl: { Ubuntu: 'wsl-1' } }
const WSL_ONLY: ClaudeManagedAccountGateSettings = {
claudeManagedAccounts: [managedAccount('wsl-1', 'wsl')],
activeClaudeManagedAccountId: null,
activeClaudeManagedAccountIdsByRuntime: { host: null, wsl: { Ubuntu: 'wsl-1' } }
}
const HOST_SELECTED: ClaudeRateLimitAccountsState = {
accounts: [managedAccount('host-1', 'host')],
activeAccountId: 'host-1',
activeAccountIdsByRuntime: { host: 'host-1', wsl: {} }
const HOST_SELECTED: ClaudeManagedAccountGateSettings = {
claudeManagedAccounts: [managedAccount('host-1', 'host')],
activeClaudeManagedAccountId: 'host-1',
activeClaudeManagedAccountIdsByRuntime: { host: 'host-1', wsl: {} }
}
function runtimeWithAccounts(claude: ClaudeRateLimitAccountsState | null): OrcaRuntimeService {
const runtime = new OrcaRuntimeService()
if (claude) {
runtime.setAccountServices({
claudeAccounts: { listAccounts: () => claude },
codexAccounts: { listAccounts: () => ({ accounts: [], activeAccountId: null }) },
rateLimits: { getState: () => ({}) }
} as never)
}
function runtimeWithAccounts(claude: ClaudeManagedAccountGateSettings | null): OrcaRuntimeService {
// No store at all is the unreadable-settings case the gate must fail closed on.
const runtime = claude
? new OrcaRuntimeService({ getSettings: () => claude } as never)
: new OrcaRuntimeService()
const internal = runtime as unknown as {
resolveStructuredAgentSessionLocation: (selector: string) => Promise<unknown>
ensureStructuredAgentSessionHost: () => Promise<void>
@@ -0,0 +1,56 @@
import { describe, expect, it, vi } from 'vitest'
const installed = vi.hoisted(() => ({ deps: null as Record<string, unknown> | null }))
vi.mock('electron', () => ({
BrowserWindow: { fromId: vi.fn(() => null) },
webContents: { fromId: vi.fn(() => null) },
ipcMain: { on: vi.fn(), removeListener: vi.fn() },
app: { getPath: vi.fn(() => '/tmp') }
}))
vi.mock('./structured-agent-session-runtime', () => ({
ensureStructuredAgentSessionHost: vi.fn(async (deps: Record<string, unknown>) => {
installed.deps = deps
})
}))
import { OrcaRuntimeService } from './orca-runtime'
import type { ClaudeManagedAccountGateSettings } from '../native-chat/claude-structured-managed-account-support'
const SETTINGS = {
claudeManagedAccounts: [],
activeClaudeManagedAccountId: null,
agentDefaultEnv: {},
agentDefaultArgs: {}
} as unknown as ClaudeManagedAccountGateSettings
function readGate(): (() => unknown) | undefined {
const deps: Record<string, unknown> = installed.deps ?? {}
const read = deps['readClaudeManagedAccountGate']
return typeof read === 'function' ? (read as () => unknown) : undefined
}
/** The runtime class this wiring lives on does not typecheck its own `this` calls, so a broken or
* missing gate hookup compiles clean. Pin it behaviourally instead. */
describe('structured Claude managed-account gate wiring', () => {
it('hands the host a gate reader that resolves the live settings', async () => {
installed.deps = null
const runtime = new OrcaRuntimeService({ getSettings: () => SETTINGS } as never)
await runtime.ensureStructuredAgentSessionHost()
const read = readGate()
expect(typeof read).toBe('function')
expect(read?.()).toBe(SETTINGS)
})
it('answers null instead of throwing when the settings cannot be read', async () => {
installed.deps = null
const runtime = new OrcaRuntimeService()
await runtime.ensureStructuredAgentSessionHost()
expect(readGate()?.()).toBeNull()
})
})
@@ -58,15 +58,6 @@ export class RuntimeAccountController {
return this.services?.claudeAccounts.getRuntimeConfigDir(target) ?? null
}
/** Null when the account state cannot be read, so gates can fail closed instead of throwing. */
readClaudeManagedAccounts(): ClaudeRateLimitAccountsState | null {
try {
return this.services?.claudeAccounts.listAccounts() ?? null
} catch {
return null
}
}
getSnapshot(): AccountsSnapshot {
const { claudeAccounts, codexAccounts, rateLimits } = this.requireServices()
return {
@@ -21,6 +21,7 @@ import { StructuredAgentSessionHost } from '../native-chat/agent-session-wire/st
import { StructuredAgentSessionAdapterRouter } from '../native-chat/agent-session-wire/structured-agent-session-adapter-router'
import type { StructuredAgentSessionHandoffTransport } from '../native-chat/agent-session-wire/structured-agent-session-handoff-types'
import { setStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry'
import type { ClaudeManagedAccountGateSettings } from '../native-chat/claude-structured-managed-account-support'
import { AgentSessionRecordStore } from './agent-session-record-store'
import { agentSessionStorePath } from './agent-session-record-store-file'
import { stopOrphanAgentSessionChildren } from './agent-session-orphan-child-reaper'
@@ -72,6 +73,7 @@ export type StructuredAgentSessionRuntimeDeps = {
/** Required. The one production wiring lives in a `@ts-nocheck` file, so this is
* also asserted at install time — an absent policy must not degrade to a guess. */
resolveClaudeAuthPolicy: () => Promise<ClaudeStructuredAuthPolicy> | ClaudeStructuredAuthPolicy
readClaudeManagedAccountGate?: () => ClaudeManagedAccountGateSettings | null
resolveEnvironment?: () => Promise<NodeJS.ProcessEnv>
resolveCodexOverrides?: () => NodeJS.ProcessEnv
onError?: (input: { scope: string; error: unknown }) => void
@@ -200,6 +202,9 @@ async function install(deps: StructuredAgentSessionRuntimeDeps): Promise<Install
? { resolveClaudeLaunchEnv: deps.resolveClaudeLaunchEnv }
: {}),
resolveClaudeAuthPolicy: deps.resolveClaudeAuthPolicy,
...(deps.readClaudeManagedAccountGate
? { readClaudeManagedAccountGate: deps.readClaudeManagedAccountGate }
: {}),
onUnexpectedExit: (event) => {
recoveryChain = recoveryChain.then(async () => {
try {
@@ -14,6 +14,7 @@ import {
resolveSessionFilePath
} from '../native-chat/session-file-resolver'
import { recordAgentSessionProviderHandle } from './agent-session-provider-handle-transition'
import type { ClaudeManagedAccountGateSettings } from '../native-chat/claude-structured-managed-account-support'
import type { AgentSessionRecordStore } from './agent-session-record-store'
export type StructuredClaudeRuntimeAdapterDeps = {
@@ -24,6 +25,7 @@ export type StructuredClaudeRuntimeAdapterDeps = {
/** Managed-account auth state for a Claude launch, mirroring the terminal preflight.
* Required: an absent policy is what silently under-strips. */
resolveClaudeAuthPolicy: () => Promise<ClaudeStructuredAuthPolicy> | ClaudeStructuredAuthPolicy
readClaudeManagedAccountGate?: () => ClaudeManagedAccountGateSettings | null
openClaudeConnection?: ClaudeStructuredSessionAdapterDeps['openConnection']
readProcessStartTime?: ClaudeStructuredSessionAdapterDeps['readProcessStartTime']
onUnexpectedExit: (event: StructuredAgentSessionLifecycleEvent) => void
@@ -39,7 +41,10 @@ export function createStructuredClaudeRuntimeAdapter(
resolveWorkspacePath: deps.resolveWorkspacePath,
resolveCommand: deps.resolveClaudeCommand ?? resolveClaudeCommand,
...(deps.resolveClaudeLaunchEnv ? { resolveEnv: deps.resolveClaudeLaunchEnv } : {}),
resolveAuthPolicy: deps.resolveClaudeAuthPolicy
resolveAuthPolicy: deps.resolveClaudeAuthPolicy,
...(deps.readClaudeManagedAccountGate
? { readManagedAccountGate: deps.readClaudeManagedAccountGate }
: {})
}),
persistHandle: async ({ sessionId, providerSessionId, leafUuid, fence }) => {
const currentFence = store.getRecord(sessionId)?.lease.runtimeFence ?? fence