mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
Structured Claude launches against the ambient Claude config, which the account service keeps in sync with the selected HOST account. A WSL-bound managed account lives inside the distro and is never synced there, so on Windows a structured session would authenticate as whatever the ambient identity happens to be while the UI names the WSL account — the user is told one identity and given another. That was unreachable only because nothing offered structured Claude on win32. Enabling it makes it reachable, so gate it here rather than patching the auth layer: refuse the structured path when the active managed Claude account is WSL-bound, and let the terminal-backed path — which resolves the account per runtime — handle that account shape. The answer rides the agentSession.createSupport seam the renderer already consumes, so no new capability and no renderer knowledge of account internals. A create the host declines becomes the definitive refusal the launch fallback already turns into a legacy native chat tab, with no error toast. Unknown answers refuse. An install with no managed accounts claims no identity and is fine, but an active selection that cannot be resolved — or account state that cannot be read at all — is not evidence that the ambient identity is right. Claude only. Codex resolves its account through a different path and its createSupport answer is untouched, as is every Codex routing decision.
179 lines
5.9 KiB
TypeScript
179 lines
5.9 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
|
|
}
|
|
|
|
/** 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 {
|
|
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
|
|
}
|
|
}
|