diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index f5b199b1..0b2dc3d9 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -1252,6 +1252,7 @@ const STALL_CHECK_INTERVAL_MS = 10_000; const STALL_WARNING_THRESHOLD_MS = 20_000; const APP_TEAM_RUNTIME_DISALLOWED_TOOLS = 'TeamDelete,TodoWrite,TaskCreate,TaskUpdate,mcp__agent-teams__team_launch,mcp__agent-teams__team_stop'; +const CLAUDE_TEAM_RUNTIME_SETTINGS_PATH_ENV = 'CLAUDE_TEAM_RUNTIME_SETTINGS_PATH'; const TEAM_JSON_READ_TIMEOUT_MS = 5_000; const TEAM_CONFIG_MAX_BYTES = 10 * 1024 * 1024; const TEAM_INBOX_MAX_BYTES = 2 * 1024 * 1024; @@ -2216,6 +2217,17 @@ function buildCodexCrossProviderSafeEnvPatch(env: NodeJS.ProcessEnv): NodeJS.Pro return envPatch; } +function applyAppManagedRuntimeSettingsPathEnv( + env: NodeJS.ProcessEnv, + settingsPath: string | null +): void { + if (settingsPath) { + env[CLAUDE_TEAM_RUNTIME_SETTINGS_PATH_ENV] = settingsPath; + } else { + delete env[CLAUDE_TEAM_RUNTIME_SETTINGS_PATH_ENV]; + } +} + interface TeamRuntimeAuthContext { teamName?: string; authMaterialId?: string; @@ -2238,6 +2250,7 @@ interface TeamRuntimeLaunchArgsPlan { providerArgs: string[]; extraArgs: string[]; inheritedProviderArgs: string[]; + appManagedSettingsPath: string | null; } type WorkspaceTrustProviderArgsResolver = (input: { @@ -4259,6 +4272,7 @@ export class TeamProvisioningService { providerArgs: rawProviderArgs, extraArgs: rawExtraArgs, inheritedProviderArgs: rawInheritedProviderArgs, + appManagedSettingsPath: null, }; } @@ -4313,6 +4327,7 @@ export class TeamProvisioningService { providerArgs: splitProviderArgs.passthroughArgs, extraArgs: splitExtraArgs.passthroughArgs, inheritedProviderArgs: splitInheritedArgs.passthroughArgs, + appManagedSettingsPath: settingsBundle?.settingsPath ?? null, }; } @@ -14819,6 +14834,10 @@ export class TeamProvisioningService { includeAnthropicHelper: providerId === 'anthropic', contextLabel: `Direct teammate restart (${input.configuredMember.name})`, }); + applyAppManagedRuntimeSettingsPathEnv( + provisioningEnv.env, + runtimeArgsPlan.appManagedSettingsPath + ); const runtimeArgs = mergeJsonSettingsArgs([ '--agent-id', @@ -15010,6 +15029,10 @@ export class TeamProvisioningService { includeAnthropicHelper: providerId === 'anthropic', contextLabel: `Direct process teammate restart (${input.configuredMember.name})`, }); + applyAppManagedRuntimeSettingsPathEnv( + provisioningEnv.env, + runtimeArgsPlan.appManagedSettingsPath + ); const runtimeArgs = mergeJsonSettingsArgs([ '--teammate-runtime', @@ -20257,6 +20280,7 @@ export class TeamProvisioningService { ...runtimeArgsPlan.settingsArgs, ...runtimeArgsPlan.inheritedProviderArgs, ]); + applyAppManagedRuntimeSettingsPathEnv(shellEnv, runtimeArgsPlan.appManagedSettingsPath); const runtimeWarning = buildRuntimeLaunchWarning(request, shellEnv, { geminiRuntimeAuth, promptSize, @@ -21575,6 +21599,7 @@ export class TeamProvisioningService { emitProvisioningCheckpoint(run, 'Resolving cross-provider member launch args'); launchArgs.push(...runtimeArgsPlan.inheritedProviderArgs); const finalLaunchArgs = mergeJsonSettingsArgs(launchArgs); + applyAppManagedRuntimeSettingsPathEnv(shellEnv, runtimeArgsPlan.appManagedSettingsPath); const runtimeWarning = buildRuntimeLaunchWarning(request, shellEnv, { geminiRuntimeAuth, promptSize, diff --git a/src/main/services/team/provisioning/TeamProvisioningDirectRestart.ts b/src/main/services/team/provisioning/TeamProvisioningDirectRestart.ts index a8f46aac..8f352e39 100644 --- a/src/main/services/team/provisioning/TeamProvisioningDirectRestart.ts +++ b/src/main/services/team/provisioning/TeamProvisioningDirectRestart.ts @@ -12,6 +12,7 @@ import type { TeamProviderId } from '@shared/types'; const DIRECT_TMUX_RESTART_ENV_KEYS = [ 'CLAUDE_CONFIG_DIR', 'CLAUDE_TEAM_CONTROL_URL', + 'CLAUDE_TEAM_RUNTIME_SETTINGS_PATH', 'CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST', 'CLAUDE_CODE_USE_OPENAI', 'CLAUDE_CODE_USE_BEDROCK', diff --git a/test/main/services/team/TeamProvisioningDirectRestart.test.ts b/test/main/services/team/TeamProvisioningDirectRestart.test.ts index 71b5102b..70b2d48f 100644 --- a/test/main/services/team/TeamProvisioningDirectRestart.test.ts +++ b/test/main/services/team/TeamProvisioningDirectRestart.test.ts @@ -62,6 +62,7 @@ describe('TeamProvisioningDirectRestart', () => { CLAUDE_CODE_ENTRY_PROVIDER: 'gemini', CLAUDE_CODE_CODEX_BACKEND: 'codex-native', CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD: 'chatgpt', + CLAUDE_TEAM_RUNTIME_SETTINGS_PATH: '/tmp/runtime-settings.json', }, 'codex' ); @@ -74,6 +75,7 @@ describe('TeamProvisioningDirectRestart', () => { expect(assignments).toContain("CLAUDE_CODE_ENTRY_PROVIDER='codex'"); expect(assignments).toContain("CLAUDE_CODE_CODEX_BACKEND='codex-native'"); expect(assignments).toContain("CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD='chatgpt'"); + expect(assignments).toContain("CLAUDE_TEAM_RUNTIME_SETTINGS_PATH='/tmp/runtime-settings.json'"); expect(assignments).toContain("CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST='1'"); }); diff --git a/test/main/services/team/TeamProvisioningMemberMcpConfig.safe-e2e.test.ts b/test/main/services/team/TeamProvisioningMemberMcpConfig.safe-e2e.test.ts index 794080a9..dee558cd 100644 --- a/test/main/services/team/TeamProvisioningMemberMcpConfig.safe-e2e.test.ts +++ b/test/main/services/team/TeamProvisioningMemberMcpConfig.safe-e2e.test.ts @@ -531,6 +531,7 @@ describe('TeamProvisioningService member MCP config safe e2e', () => { settingsArgs: string[]; extraArgs: string[]; inheritedProviderArgs: string[]; + appManagedSettingsPath: string | null; }>; } ).buildTeamRuntimeLaunchArgsPlan = vi.fn(async () => ({ @@ -540,6 +541,7 @@ describe('TeamProvisioningService member MCP config safe e2e', () => { settingsArgs: [], extraArgs: [], inheritedProviderArgs: [], + appManagedSettingsPath: null, })); ( svc as unknown as { updateDirectTmuxRestartMemberConfig: () => Promise } diff --git a/test/main/services/team/TeamProvisioningService.test.ts b/test/main/services/team/TeamProvisioningService.test.ts index b0204629..0494ac95 100644 --- a/test/main/services/team/TeamProvisioningService.test.ts +++ b/test/main/services/team/TeamProvisioningService.test.ts @@ -15538,6 +15538,7 @@ describe('TeamProvisioningService', () => { settingsArgs: [], extraArgs: [], inheritedProviderArgs: [], + appManagedSettingsPath: null, })); (svc as any).materializeDirectProcessNativeBootstrapContext = vi.fn(async () => ({})); (svc as any).updateDirectTmuxRestartMemberConfig = vi.fn(async () => {}); @@ -15637,6 +15638,7 @@ describe('TeamProvisioningService', () => { settingsArgs: [], extraArgs: [], inheritedProviderArgs: [], + appManagedSettingsPath: null, })); (svc as any).materializeDirectProcessNativeBootstrapContext = vi.fn(async () => ({})); (svc as any).updateDirectTmuxRestartMemberConfig = vi.fn(async () => {}); diff --git a/test/main/services/team/TeamProvisioningServicePrepare.test.ts b/test/main/services/team/TeamProvisioningServicePrepare.test.ts index 83ae6dc9..9b7790b3 100644 --- a/test/main/services/team/TeamProvisioningServicePrepare.test.ts +++ b/test/main/services/team/TeamProvisioningServicePrepare.test.ts @@ -3566,6 +3566,7 @@ describe('TeamProvisioningService prepare/auth behavior', () => { expect(result.settingsArgs[0]).toBe('--settings'); expect(result.inheritedProviderArgs).toEqual([]); + expect(result.appManagedSettingsPath).toBe(result.settingsArgs[1]); const settings = JSON.parse(fs.readFileSync(result.settingsArgs[1], 'utf8')); expect(settings.codex.forced_login_method).toBe('chatgpt'); }); @@ -3651,6 +3652,7 @@ describe('TeamProvisioningService prepare/auth behavior', () => { expect(result.settingsArgs).toEqual([]); expect(result.inheritedProviderArgs).toEqual(inheritedProviderArgs); + expect(result.appManagedSettingsPath).toBeNull(); }); it('coalesces inherited JSON settings into Anthropic helper settings without keeping helper path args', async () => { @@ -3685,6 +3687,7 @@ describe('TeamProvisioningService prepare/auth behavior', () => { expect(result.settingsArgs[0]).toBe('--settings'); expect(result.settingsArgs[1]).toContain(helperDir); expect(result.settingsArgs[1]).not.toBe(helperSettingsPath); + expect(result.appManagedSettingsPath).toBe(result.settingsArgs[1]); const settings = JSON.parse(fs.readFileSync(result.settingsArgs[1], 'utf8')); expect(settings.apiKeyHelper).toBe(`'${path.join(helperDir, 'helper.sh')}'`); expect(settings.codex.forced_login_method).toBe('chatgpt'); diff --git a/test/main/services/team/TeamProvisioningServicePrompts.test.ts b/test/main/services/team/TeamProvisioningServicePrompts.test.ts index 8f5d1751..dc3eec6a 100644 --- a/test/main/services/team/TeamProvisioningServicePrompts.test.ts +++ b/test/main/services/team/TeamProvisioningServicePrompts.test.ts @@ -178,6 +178,16 @@ function readRuntimeSettingsFromLaunchArgs(callIndex = 0): Record; } +function readRuntimeSettingsPathFromLaunchArgs(callIndex = 0): string { + const args = vi.mocked(spawnCli).mock.calls[callIndex]?.[1] as string[] | undefined; + const settingsFlagIndex = args?.indexOf('--settings') ?? -1; + const settingsValue = settingsFlagIndex >= 0 ? args?.[settingsFlagIndex + 1] : null; + if (!settingsValue || settingsValue.trim().startsWith('{')) { + throw new Error('Failed to extract runtime settings path from spawn args'); + } + return settingsValue; +} + function registerNoopOpenCodeRuntimeAdapter(svc: TeamProvisioningService): void { svc.setRuntimeAdapterRegistry( new TeamRuntimeAdapterRegistry([ @@ -564,6 +574,9 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () => expect(extractBootstrapSpec().members).toEqual([ expect.objectContaining({ name: 'alice', provider: 'codex' }), ]); + const settingsPath = readRuntimeSettingsPathFromLaunchArgs(); + const launchEnv = vi.mocked(spawnCli).mock.calls[0]?.[2]?.env as NodeJS.ProcessEnv; + expect(launchEnv.CLAUDE_TEAM_RUNTIME_SETTINGS_PATH).toBe(settingsPath); const settings = readRuntimeSettingsFromLaunchArgs(); expect((settings.codex as Record).forced_login_method).toBe('chatgpt'); @@ -1260,6 +1273,9 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () => expect(extractBootstrapSpec().members).toEqual([ expect.objectContaining({ name: 'alice', provider: 'codex' }), ]); + const settingsPath = readRuntimeSettingsPathFromLaunchArgs(); + const launchEnv = vi.mocked(spawnCli).mock.calls[0]?.[2]?.env as NodeJS.ProcessEnv; + expect(launchEnv.CLAUDE_TEAM_RUNTIME_SETTINGS_PATH).toBe(settingsPath); const settings = readRuntimeSettingsFromLaunchArgs(); expect((settings.codex as Record).forced_login_method).toBe('chatgpt');