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:
Jinwoo Hong
2026-09-18 00:11:33 -04:00
committed by GitHub
parent 8c6ae79e94
commit 3de77340fc
8 changed files with 86 additions and 13 deletions
+46
View File
@@ -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')
})
})
+12 -8
View File
@@ -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()
})