mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
fix: apply managed Claude auth to Agent Teams (#21356)
* fix: apply managed Claude auth to agent teams * test: update agent teams auth launch expectation * refactor: derive agent teams auth deletions
This commit is contained in:
@@ -130,4 +130,50 @@ describe('orca claude-teams CLI handler', () => {
|
||||
expect(spawnEnv.PATH).toBe('/shim:/usr/bin')
|
||||
}
|
||||
)
|
||||
|
||||
it.skipIf(isWindows)('removes managed auth variables before spawning Claude', async () => {
|
||||
const previousApiKey = process.env.ANTHROPIC_API_KEY
|
||||
process.env.ANTHROPIC_API_KEY = 'sk-ant-inherited'
|
||||
callMock.mockResolvedValueOnce({
|
||||
result: {
|
||||
launch: {
|
||||
env: {
|
||||
CLAUDE_CONFIG_DIR: '/managed/claude',
|
||||
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1'
|
||||
},
|
||||
envToDelete: ['ANTHROPIC_API_KEY']
|
||||
}
|
||||
}
|
||||
})
|
||||
try {
|
||||
await runClaudeTeams()
|
||||
} finally {
|
||||
if (previousApiKey === undefined) {
|
||||
delete process.env.ANTHROPIC_API_KEY
|
||||
} else {
|
||||
process.env.ANTHROPIC_API_KEY = previousApiKey
|
||||
}
|
||||
}
|
||||
|
||||
const spawnEnv = spawnMock.mock.calls.at(-1)?.[2].env as SpawnEnv
|
||||
expect(spawnEnv.ANTHROPIC_API_KEY).toBeUndefined()
|
||||
expect(spawnEnv.CLAUDE_CONFIG_DIR).toBe('/managed/claude')
|
||||
})
|
||||
|
||||
it.skipIf(isWindows)('preserves API-key auth when no managed deletion is requested', async () => {
|
||||
const previousApiKey = process.env.ANTHROPIC_API_KEY
|
||||
process.env.ANTHROPIC_API_KEY = 'sk-ant-system'
|
||||
try {
|
||||
await runClaudeTeams()
|
||||
} finally {
|
||||
if (previousApiKey === undefined) {
|
||||
delete process.env.ANTHROPIC_API_KEY
|
||||
} else {
|
||||
process.env.ANTHROPIC_API_KEY = previousApiKey
|
||||
}
|
||||
}
|
||||
|
||||
const spawnEnv = spawnMock.mock.calls.at(-1)?.[2].env as SpawnEnv
|
||||
expect(spawnEnv.ANTHROPIC_API_KEY).toBe('sk-ant-system')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -73,16 +73,20 @@ export const CORE_HANDLERS: Record<string, CommandHandler> = {
|
||||
'orca claude-teams must be run inside an Orca terminal.'
|
||||
)
|
||||
}
|
||||
const response = await client.call<{ launch: { env: Record<string, string> } }>(
|
||||
'agentTeams.prepareLaunch',
|
||||
{
|
||||
paneKey,
|
||||
env: envRecord()
|
||||
}
|
||||
)
|
||||
const inheritedEnv = envRecord()
|
||||
const response = await client.call<{
|
||||
launch: { env: Record<string, string>; envToDelete?: string[] }
|
||||
}>('agentTeams.prepareLaunch', {
|
||||
paneKey,
|
||||
env: inheritedEnv,
|
||||
prepareAuth: true
|
||||
})
|
||||
for (const key of response.result.launch.envToDelete ?? []) {
|
||||
delete inheritedEnv[key]
|
||||
}
|
||||
process.exitCode = await runClaudeAgentTeams(
|
||||
{
|
||||
...envRecord(),
|
||||
...inheritedEnv,
|
||||
...response.result.launch.env
|
||||
},
|
||||
rawArgs ?? []
|
||||
|
||||
@@ -153,6 +153,7 @@ describe('orca cli worktree awareness', () => {
|
||||
|
||||
expect(callMock).toHaveBeenCalledWith('agentTeams.prepareLaunch', {
|
||||
paneKey: 'tab-1:11111111-1111-4111-8111-111111111111',
|
||||
prepareAuth: true,
|
||||
env: expect.objectContaining({
|
||||
ORCA_PANE_KEY: 'tab-1:11111111-1111-4111-8111-111111111111'
|
||||
})
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
ensureClaudeAgentTeamsShimDir,
|
||||
resolveClaudeAgentTeamsShimBin
|
||||
} from './claude-agent-teams-shim-env'
|
||||
import { applyClaudeEnvPatch } from '../claude-accounts/environment'
|
||||
|
||||
export class OrcaRuntimeWithResolveTerminalSplitSourceAuthority extends OrcaRuntimeWithSplitPtyBackedTerminal {
|
||||
protected resolveTerminalSplitSourceAuthority(
|
||||
@@ -109,6 +110,7 @@ export class OrcaRuntimeWithResolveTerminalSplitSourceAuthority extends OrcaRunt
|
||||
async prepareClaudeAgentTeamsLeader(args: {
|
||||
paneKey: string
|
||||
baseEnv?: Record<string, string>
|
||||
prepareAuth?: boolean
|
||||
}): Promise<{ env: Record<string, string> }> {
|
||||
const handle = this.getTerminalHandleForPaneKey(args.paneKey)
|
||||
if (!handle) {
|
||||
@@ -116,26 +118,38 @@ export class OrcaRuntimeWithResolveTerminalSplitSourceAuthority extends OrcaRunt
|
||||
}
|
||||
return await this.prepareClaudeAgentTeamsLeaderForHandle({
|
||||
handle,
|
||||
baseEnv: args.baseEnv
|
||||
baseEnv: args.baseEnv,
|
||||
prepareAuth: args.prepareAuth
|
||||
})
|
||||
}
|
||||
|
||||
async prepareClaudeAgentTeamsLeaderForHandle(args: {
|
||||
handle: string
|
||||
baseEnv?: Record<string, string>
|
||||
}): Promise<{ env: Record<string, string> }> {
|
||||
prepareAuth?: boolean
|
||||
}): Promise<{ env: Record<string, string>; envToDelete?: string[] }> {
|
||||
const baseEnv = {
|
||||
...process.env,
|
||||
...args.baseEnv
|
||||
}
|
||||
const inheritedEnvKeys = new Set(Object.keys(baseEnv))
|
||||
const auth = args.prepareAuth && this.prepareClaudeAuth ? await this.prepareClaudeAuth() : null
|
||||
if (auth) {
|
||||
applyClaudeEnvPatch(baseEnv, auth.envPatch, { stripAuthEnv: auth.stripAuthEnv })
|
||||
}
|
||||
const envToDelete = auth?.stripAuthEnv
|
||||
? [...inheritedEnvKeys].filter((key) => !(key in baseEnv))
|
||||
: undefined
|
||||
const shimDir = await ensureClaudeAgentTeamsShimDir()
|
||||
const shimBin = resolveClaudeAgentTeamsShimBin(baseEnv)
|
||||
return this.claudeAgentTeams.createLaunchEnv({
|
||||
const launch = this.claudeAgentTeams.createLaunchEnv({
|
||||
leaderHandle: args.handle,
|
||||
baseEnv,
|
||||
shimDir,
|
||||
shimBin
|
||||
})
|
||||
const env = auth ? { ...auth.envPatch, ...launch.env } : launch.env
|
||||
return envToDelete ? { env, envToDelete } : { env }
|
||||
}
|
||||
|
||||
// Why: a leader handle that never binds to a PTY (lost pane race) has no exit
|
||||
|
||||
@@ -3,6 +3,7 @@ import { OrcaRuntimeWithLinearCommands } from './orca-runtime-linear-commands'
|
||||
import type { RuntimeStore } from './runtime-store-contract'
|
||||
import type { StatsCollector } from '../stats/collector'
|
||||
import type { IPtyProvider } from '../providers/types'
|
||||
import type { PrepareClaudeAuth } from '../ipc/pty/host-env/types'
|
||||
import type { RuntimeTerminalAgentStatusEvent } from './runtime-terminal-contracts'
|
||||
import type { TerminalSideEffectBatch } from '../../shared/terminal-side-effect-facts'
|
||||
import type { AgentStatusIpcPayload } from '../../shared/agent-status-types'
|
||||
@@ -41,12 +42,15 @@ import { registerConptyDa1OverrideInstaller } from './terminal-model-query-autho
|
||||
import { registerTerminalViewAttributesApplier } from './terminal-view-attribute-store'
|
||||
|
||||
export class OrcaRuntimeWithStateFields extends OrcaRuntimeWithLinearCommands {
|
||||
protected readonly prepareClaudeAuth?: PrepareClaudeAuth
|
||||
|
||||
constructor(
|
||||
store: RuntimeStore | null = null,
|
||||
stats?: StatsCollector,
|
||||
deps?: {
|
||||
getLocalProvider?: () => IPtyProvider
|
||||
getSshProvider?: (connectionId: string) => IPtyProvider | undefined
|
||||
prepareClaudeAuth?: PrepareClaudeAuth
|
||||
onPtyStopped?: (ptyId: string) => void
|
||||
onTerminalAgentStatus?: (event: RuntimeTerminalAgentStatusEvent) => void
|
||||
onTerminalSideEffects?: (batch: TerminalSideEffectBatch) => void
|
||||
@@ -103,6 +107,7 @@ export class OrcaRuntimeWithStateFields extends OrcaRuntimeWithLinearCommands {
|
||||
) {
|
||||
super()
|
||||
this.store = store
|
||||
this.prepareClaudeAuth = deps?.prepareClaudeAuth
|
||||
store?.onSettingsChanged?.((updates) => {
|
||||
if ('experimentalStructuredNativeChat' in updates) {
|
||||
this.notifyMobileSessionTabsChanged()
|
||||
|
||||
@@ -189,7 +189,8 @@ export const TERMINAL_LIFECYCLE_METHODS = [
|
||||
handler: async (params, { runtime }) => ({
|
||||
launch: await runtime.prepareClaudeAgentTeamsLeader({
|
||||
paneKey: params.paneKey,
|
||||
baseEnv: params.env
|
||||
baseEnv: params.env,
|
||||
prepareAuth: params.prepareAuth
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -72,6 +72,7 @@ export function initializeMainProcessRuntime(): OrcaRuntimeService {
|
||||
// `orca serve`, which never opens one, and the fleet path runs there too.
|
||||
const observedPaneIdentities = new AgentStatusObservedPaneIdentities()
|
||||
const runtime = new OrcaRuntimeService(store, stats, {
|
||||
prepareClaudeAuth: (target) => state.claudeRuntimeAuth!.prepareForClaudeLaunch(target),
|
||||
agentSessionClaimSigner: loadAgentSessionClaimSigner(
|
||||
getProfileUserDataPath(),
|
||||
getProfileUserDataPath()
|
||||
|
||||
@@ -234,5 +234,6 @@ export const AgentTeamsTmuxCompat = z.object({
|
||||
|
||||
export const AgentTeamsPrepareLaunch = z.object({
|
||||
paneKey: requiredString('Missing pane key'),
|
||||
env: z.record(z.string(), z.string()).optional()
|
||||
env: z.record(z.string(), z.string()).optional(),
|
||||
prepareAuth: z.boolean().optional()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user