Files
orca/src/main/runtime/runtime-account-controller.ts
T
Merge Sim 488b56d62f Read the structured Claude account gate through the auth policy's accessor
The gate resolved the active account from the account-service snapshot's
runtime map; the auth policy resolves it with
getSelectedClaudeAccountIdForTarget(settings, { runtime: 'host' }). Those are
two sources and two resolution rules, and they disagree on a legacy settings
blob that carries the selection only in the flat activeClaudeManagedAccountId:
the accessor falls through to it, a direct read of the runtime map does not. The
gate would then refuse a launch the policy would have run under host-1 — and in
the mirror case a session could be admitted under a policy computed from a
different account than the gate approved.

Read the same settings through the same accessor so agreement is structural
rather than coincidental, and drop the controller accessor that existed only to
reach the snapshot.

No behaviour change for any state both already agreed on; Codex is untouched.
2026-09-03 14:17:48 -07:00

170 lines
5.6 KiB
TypeScript

import type { ClaudeAccountService } from '../claude-accounts/service'
import type {
CodexAccountService,
CodexResetCreditRejectedBeforeProviderReason
} from '../codex-accounts/service'
import type { CodexAccountSelectionTarget } from '../codex-accounts/runtime-selection'
import type { RateLimitService } from '../rate-limits/service'
import type {
ClaudeRateLimitAccountsState,
CodexRateLimitAccountsState
} from '../../shared/managed-account-types'
import type { CodexRateLimitResetOutcome, RateLimitState } from '../../shared/rate-limit-types'
import type { CodexResetCreditExpectedScope } from '../../shared/codex-reset-credit-scope'
import type { CommitMessageAgentEnvironmentResolvers } from '../text-generation/commit-message-agent-environment'
import type { ClaudeAccountSelectionTarget } from '../claude-accounts/runtime-selection'
export type RuntimeAccountServices = {
claudeAccounts: ClaudeAccountService
codexAccounts: CodexAccountService
rateLimits: RateLimitService
}
export type AccountsSnapshot = {
claude: ClaudeRateLimitAccountsState
codex: CodexRateLimitAccountsState
rateLimits: RateLimitState
}
export type CodexRateLimitResetRpcResult = {
scope: CodexResetCreditExpectedScope
snapshot: AccountsSnapshot
} & (
| { outcome: CodexRateLimitResetOutcome }
| {
status: 'rejectedBeforeProvider'
retryDisposition: 'discardAttempt'
reason: CodexResetCreditRejectedBeforeProviderReason
}
)
export class RuntimeAccountController {
private services: RuntimeAccountServices | null = null
private commitMessageAgentEnvironment: CommitMessageAgentEnvironmentResolvers | null = null
setServices(services: RuntimeAccountServices): void {
this.services = services
}
setCommitMessageAgentEnvironment(resolvers: CommitMessageAgentEnvironmentResolvers): void {
this.commitMessageAgentEnvironment = resolvers
}
getCommitMessageAgentEnvironment(): CommitMessageAgentEnvironmentResolvers | undefined {
return this.commitMessageAgentEnvironment ?? undefined
}
getClaudeConfigDirectory(target: ClaudeAccountSelectionTarget): string | null {
return this.services?.claudeAccounts.getRuntimeConfigDir(target) ?? null
}
getSnapshot(): AccountsSnapshot {
const { claudeAccounts, codexAccounts, rateLimits } = this.requireServices()
return {
claude: claudeAccounts.listAccounts(),
codex: codexAccounts.listAccounts(),
rateLimits: rateLimits.getState()
}
}
async refreshForMobile(): Promise<void> {
const { rateLimits } = this.requireServices()
await Promise.allSettled([
rateLimits.refresh(),
rateLimits.fetchInactiveClaudeAccountsOnOpen(),
rateLimits.fetchInactiveCodexAccountsOnOpen()
])
}
async refreshForMobileSubscriber(): Promise<void> {
const { rateLimits } = this.requireServices()
await Promise.allSettled([
rateLimits.refreshIfStale(),
rateLimits.fetchInactiveClaudeAccountsOnOpen(),
rateLimits.fetchInactiveCodexAccountsOnOpen()
])
}
selectClaude(accountId: string | null): Promise<ClaudeRateLimitAccountsState> {
return this.requireServices().claudeAccounts.selectAccount(accountId)
}
selectCodex(accountId: string | null): Promise<CodexRateLimitAccountsState> {
return this.requireServices().codexAccounts.selectAccount(accountId)
}
selectCodexForTarget(
accountId: string | null,
target: CodexAccountSelectionTarget
): Promise<CodexRateLimitAccountsState> {
return this.requireServices().codexAccounts.selectAccountForTarget(accountId, target)
}
async consumeCodexResetCredit(
idempotencyKey: string,
expectedScope: CodexResetCreditExpectedScope
): Promise<CodexRateLimitResetRpcResult> {
const { claudeAccounts, codexAccounts } = this.requireServices()
const result = await codexAccounts.consumeRateLimitResetCredit(idempotencyKey, expectedScope)
const snapshot = {
claude: claudeAccounts.listAccounts(),
codex: result.codex,
rateLimits: result.rateLimits
}
if ('status' in result) {
return {
status: result.status,
retryDisposition: result.retryDisposition,
reason: result.reason,
scope: result.scope,
snapshot
}
}
return { outcome: result.outcome, scope: result.scope, snapshot }
}
removeClaude(accountId: string): Promise<ClaudeRateLimitAccountsState> {
return this.requireServices().claudeAccounts.removeAccount(accountId)
}
addClaudeFromConfigDir(
configDir: string,
options?: {
runtime?: 'host' | 'wsl'
wslDistro?: string | null
previousLegacyCredentialsSha256?: string | null
}
): Promise<ClaudeRateLimitAccountsState> {
return this.requireServices().claudeAccounts.addAccountFromConfigDir(configDir, options)
}
removeCodex(accountId: string): Promise<CodexRateLimitAccountsState> {
return this.requireServices().codexAccounts.removeAccount(accountId)
}
addCodexFromHome(
sourceHome: string,
target?: { runtime?: 'host' | 'wsl'; wslDistro?: string | null }
): Promise<CodexRateLimitAccountsState> {
return this.requireServices().codexAccounts.addAccountFromHome(sourceHome, target)
}
onChanged(listener: (snapshot: AccountsSnapshot) => void): () => void {
const services = this.requireServices()
return services.rateLimits.onStateChange((rateLimits) => {
listener({
claude: services.claudeAccounts.listAccounts(),
codex: services.codexAccounts.listAccounts(),
rateLimits
})
})
}
private requireServices(): RuntimeAccountServices {
if (!this.services) {
throw new Error('Account services are not configured on this runtime')
}
return this.services
}
}