fix(team): propagate managed runtime settings env

This commit is contained in:
777genius 2026-05-27 18:56:24 +03:00
parent 5403a2cea9
commit 77e08af03f
7 changed files with 51 additions and 0 deletions

View file

@ -1252,6 +1252,7 @@ const STALL_CHECK_INTERVAL_MS = 10_000;
const STALL_WARNING_THRESHOLD_MS = 20_000; const STALL_WARNING_THRESHOLD_MS = 20_000;
const APP_TEAM_RUNTIME_DISALLOWED_TOOLS = const APP_TEAM_RUNTIME_DISALLOWED_TOOLS =
'TeamDelete,TodoWrite,TaskCreate,TaskUpdate,mcp__agent-teams__team_launch,mcp__agent-teams__team_stop'; '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_JSON_READ_TIMEOUT_MS = 5_000;
const TEAM_CONFIG_MAX_BYTES = 10 * 1024 * 1024; const TEAM_CONFIG_MAX_BYTES = 10 * 1024 * 1024;
const TEAM_INBOX_MAX_BYTES = 2 * 1024 * 1024; const TEAM_INBOX_MAX_BYTES = 2 * 1024 * 1024;
@ -2216,6 +2217,17 @@ function buildCodexCrossProviderSafeEnvPatch(env: NodeJS.ProcessEnv): NodeJS.Pro
return envPatch; 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 { interface TeamRuntimeAuthContext {
teamName?: string; teamName?: string;
authMaterialId?: string; authMaterialId?: string;
@ -2238,6 +2250,7 @@ interface TeamRuntimeLaunchArgsPlan {
providerArgs: string[]; providerArgs: string[];
extraArgs: string[]; extraArgs: string[];
inheritedProviderArgs: string[]; inheritedProviderArgs: string[];
appManagedSettingsPath: string | null;
} }
type WorkspaceTrustProviderArgsResolver = (input: { type WorkspaceTrustProviderArgsResolver = (input: {
@ -4259,6 +4272,7 @@ export class TeamProvisioningService {
providerArgs: rawProviderArgs, providerArgs: rawProviderArgs,
extraArgs: rawExtraArgs, extraArgs: rawExtraArgs,
inheritedProviderArgs: rawInheritedProviderArgs, inheritedProviderArgs: rawInheritedProviderArgs,
appManagedSettingsPath: null,
}; };
} }
@ -4313,6 +4327,7 @@ export class TeamProvisioningService {
providerArgs: splitProviderArgs.passthroughArgs, providerArgs: splitProviderArgs.passthroughArgs,
extraArgs: splitExtraArgs.passthroughArgs, extraArgs: splitExtraArgs.passthroughArgs,
inheritedProviderArgs: splitInheritedArgs.passthroughArgs, inheritedProviderArgs: splitInheritedArgs.passthroughArgs,
appManagedSettingsPath: settingsBundle?.settingsPath ?? null,
}; };
} }
@ -14819,6 +14834,10 @@ export class TeamProvisioningService {
includeAnthropicHelper: providerId === 'anthropic', includeAnthropicHelper: providerId === 'anthropic',
contextLabel: `Direct teammate restart (${input.configuredMember.name})`, contextLabel: `Direct teammate restart (${input.configuredMember.name})`,
}); });
applyAppManagedRuntimeSettingsPathEnv(
provisioningEnv.env,
runtimeArgsPlan.appManagedSettingsPath
);
const runtimeArgs = mergeJsonSettingsArgs([ const runtimeArgs = mergeJsonSettingsArgs([
'--agent-id', '--agent-id',
@ -15010,6 +15029,10 @@ export class TeamProvisioningService {
includeAnthropicHelper: providerId === 'anthropic', includeAnthropicHelper: providerId === 'anthropic',
contextLabel: `Direct process teammate restart (${input.configuredMember.name})`, contextLabel: `Direct process teammate restart (${input.configuredMember.name})`,
}); });
applyAppManagedRuntimeSettingsPathEnv(
provisioningEnv.env,
runtimeArgsPlan.appManagedSettingsPath
);
const runtimeArgs = mergeJsonSettingsArgs([ const runtimeArgs = mergeJsonSettingsArgs([
'--teammate-runtime', '--teammate-runtime',
@ -20257,6 +20280,7 @@ export class TeamProvisioningService {
...runtimeArgsPlan.settingsArgs, ...runtimeArgsPlan.settingsArgs,
...runtimeArgsPlan.inheritedProviderArgs, ...runtimeArgsPlan.inheritedProviderArgs,
]); ]);
applyAppManagedRuntimeSettingsPathEnv(shellEnv, runtimeArgsPlan.appManagedSettingsPath);
const runtimeWarning = buildRuntimeLaunchWarning(request, shellEnv, { const runtimeWarning = buildRuntimeLaunchWarning(request, shellEnv, {
geminiRuntimeAuth, geminiRuntimeAuth,
promptSize, promptSize,
@ -21575,6 +21599,7 @@ export class TeamProvisioningService {
emitProvisioningCheckpoint(run, 'Resolving cross-provider member launch args'); emitProvisioningCheckpoint(run, 'Resolving cross-provider member launch args');
launchArgs.push(...runtimeArgsPlan.inheritedProviderArgs); launchArgs.push(...runtimeArgsPlan.inheritedProviderArgs);
const finalLaunchArgs = mergeJsonSettingsArgs(launchArgs); const finalLaunchArgs = mergeJsonSettingsArgs(launchArgs);
applyAppManagedRuntimeSettingsPathEnv(shellEnv, runtimeArgsPlan.appManagedSettingsPath);
const runtimeWarning = buildRuntimeLaunchWarning(request, shellEnv, { const runtimeWarning = buildRuntimeLaunchWarning(request, shellEnv, {
geminiRuntimeAuth, geminiRuntimeAuth,
promptSize, promptSize,

View file

@ -12,6 +12,7 @@ import type { TeamProviderId } from '@shared/types';
const DIRECT_TMUX_RESTART_ENV_KEYS = [ const DIRECT_TMUX_RESTART_ENV_KEYS = [
'CLAUDE_CONFIG_DIR', 'CLAUDE_CONFIG_DIR',
'CLAUDE_TEAM_CONTROL_URL', 'CLAUDE_TEAM_CONTROL_URL',
'CLAUDE_TEAM_RUNTIME_SETTINGS_PATH',
'CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST', 'CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST',
'CLAUDE_CODE_USE_OPENAI', 'CLAUDE_CODE_USE_OPENAI',
'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_BEDROCK',

View file

@ -62,6 +62,7 @@ describe('TeamProvisioningDirectRestart', () => {
CLAUDE_CODE_ENTRY_PROVIDER: 'gemini', CLAUDE_CODE_ENTRY_PROVIDER: 'gemini',
CLAUDE_CODE_CODEX_BACKEND: 'codex-native', CLAUDE_CODE_CODEX_BACKEND: 'codex-native',
CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD: 'chatgpt', CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD: 'chatgpt',
CLAUDE_TEAM_RUNTIME_SETTINGS_PATH: '/tmp/runtime-settings.json',
}, },
'codex' 'codex'
); );
@ -74,6 +75,7 @@ describe('TeamProvisioningDirectRestart', () => {
expect(assignments).toContain("CLAUDE_CODE_ENTRY_PROVIDER='codex'"); expect(assignments).toContain("CLAUDE_CODE_ENTRY_PROVIDER='codex'");
expect(assignments).toContain("CLAUDE_CODE_CODEX_BACKEND='codex-native'"); expect(assignments).toContain("CLAUDE_CODE_CODEX_BACKEND='codex-native'");
expect(assignments).toContain("CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD='chatgpt'"); 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'"); expect(assignments).toContain("CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST='1'");
}); });

View file

@ -531,6 +531,7 @@ describe('TeamProvisioningService member MCP config safe e2e', () => {
settingsArgs: string[]; settingsArgs: string[];
extraArgs: string[]; extraArgs: string[];
inheritedProviderArgs: string[]; inheritedProviderArgs: string[];
appManagedSettingsPath: string | null;
}>; }>;
} }
).buildTeamRuntimeLaunchArgsPlan = vi.fn(async () => ({ ).buildTeamRuntimeLaunchArgsPlan = vi.fn(async () => ({
@ -540,6 +541,7 @@ describe('TeamProvisioningService member MCP config safe e2e', () => {
settingsArgs: [], settingsArgs: [],
extraArgs: [], extraArgs: [],
inheritedProviderArgs: [], inheritedProviderArgs: [],
appManagedSettingsPath: null,
})); }));
( (
svc as unknown as { updateDirectTmuxRestartMemberConfig: () => Promise<void> } svc as unknown as { updateDirectTmuxRestartMemberConfig: () => Promise<void> }

View file

@ -15538,6 +15538,7 @@ describe('TeamProvisioningService', () => {
settingsArgs: [], settingsArgs: [],
extraArgs: [], extraArgs: [],
inheritedProviderArgs: [], inheritedProviderArgs: [],
appManagedSettingsPath: null,
})); }));
(svc as any).materializeDirectProcessNativeBootstrapContext = vi.fn(async () => ({})); (svc as any).materializeDirectProcessNativeBootstrapContext = vi.fn(async () => ({}));
(svc as any).updateDirectTmuxRestartMemberConfig = vi.fn(async () => {}); (svc as any).updateDirectTmuxRestartMemberConfig = vi.fn(async () => {});
@ -15637,6 +15638,7 @@ describe('TeamProvisioningService', () => {
settingsArgs: [], settingsArgs: [],
extraArgs: [], extraArgs: [],
inheritedProviderArgs: [], inheritedProviderArgs: [],
appManagedSettingsPath: null,
})); }));
(svc as any).materializeDirectProcessNativeBootstrapContext = vi.fn(async () => ({})); (svc as any).materializeDirectProcessNativeBootstrapContext = vi.fn(async () => ({}));
(svc as any).updateDirectTmuxRestartMemberConfig = vi.fn(async () => {}); (svc as any).updateDirectTmuxRestartMemberConfig = vi.fn(async () => {});

View file

@ -3566,6 +3566,7 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
expect(result.settingsArgs[0]).toBe('--settings'); expect(result.settingsArgs[0]).toBe('--settings');
expect(result.inheritedProviderArgs).toEqual([]); expect(result.inheritedProviderArgs).toEqual([]);
expect(result.appManagedSettingsPath).toBe(result.settingsArgs[1]);
const settings = JSON.parse(fs.readFileSync(result.settingsArgs[1], 'utf8')); const settings = JSON.parse(fs.readFileSync(result.settingsArgs[1], 'utf8'));
expect(settings.codex.forced_login_method).toBe('chatgpt'); expect(settings.codex.forced_login_method).toBe('chatgpt');
}); });
@ -3651,6 +3652,7 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
expect(result.settingsArgs).toEqual([]); expect(result.settingsArgs).toEqual([]);
expect(result.inheritedProviderArgs).toEqual(inheritedProviderArgs); expect(result.inheritedProviderArgs).toEqual(inheritedProviderArgs);
expect(result.appManagedSettingsPath).toBeNull();
}); });
it('coalesces inherited JSON settings into Anthropic helper settings without keeping helper path args', async () => { 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[0]).toBe('--settings');
expect(result.settingsArgs[1]).toContain(helperDir); expect(result.settingsArgs[1]).toContain(helperDir);
expect(result.settingsArgs[1]).not.toBe(helperSettingsPath); expect(result.settingsArgs[1]).not.toBe(helperSettingsPath);
expect(result.appManagedSettingsPath).toBe(result.settingsArgs[1]);
const settings = JSON.parse(fs.readFileSync(result.settingsArgs[1], 'utf8')); const settings = JSON.parse(fs.readFileSync(result.settingsArgs[1], 'utf8'));
expect(settings.apiKeyHelper).toBe(`'${path.join(helperDir, 'helper.sh')}'`); expect(settings.apiKeyHelper).toBe(`'${path.join(helperDir, 'helper.sh')}'`);
expect(settings.codex.forced_login_method).toBe('chatgpt'); expect(settings.codex.forced_login_method).toBe('chatgpt');

View file

@ -178,6 +178,16 @@ function readRuntimeSettingsFromLaunchArgs(callIndex = 0): Record<string, unknow
return JSON.parse(fs.readFileSync(settingsValue, 'utf8')) as Record<string, unknown>; return JSON.parse(fs.readFileSync(settingsValue, 'utf8')) as Record<string, unknown>;
} }
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 { function registerNoopOpenCodeRuntimeAdapter(svc: TeamProvisioningService): void {
svc.setRuntimeAdapterRegistry( svc.setRuntimeAdapterRegistry(
new TeamRuntimeAdapterRegistry([ new TeamRuntimeAdapterRegistry([
@ -564,6 +574,9 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () =>
expect(extractBootstrapSpec().members).toEqual([ expect(extractBootstrapSpec().members).toEqual([
expect.objectContaining({ name: 'alice', provider: 'codex' }), 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(); const settings = readRuntimeSettingsFromLaunchArgs();
expect((settings.codex as Record<string, unknown>).forced_login_method).toBe('chatgpt'); expect((settings.codex as Record<string, unknown>).forced_login_method).toBe('chatgpt');
@ -1260,6 +1273,9 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () =>
expect(extractBootstrapSpec().members).toEqual([ expect(extractBootstrapSpec().members).toEqual([
expect.objectContaining({ name: 'alice', provider: 'codex' }), 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(); const settings = readRuntimeSettingsFromLaunchArgs();
expect((settings.codex as Record<string, unknown>).forced_login_method).toBe('chatgpt'); expect((settings.codex as Record<string, unknown>).forced_login_method).toBe('chatgpt');