fix(team): preserve mixed provider runtime settings
This commit is contained in:
parent
e363394c72
commit
7cc1a59bbc
17 changed files with 812 additions and 17 deletions
|
|
@ -39,7 +39,7 @@ export const usePageSeo = (titleKey: string, descriptionKey: string, options: Pa
|
||||||
const resolvedImage = computed<PageSeoImage>(() => {
|
const resolvedImage = computed<PageSeoImage>(() => {
|
||||||
if (options.image) return options.image;
|
if (options.image) return options.image;
|
||||||
return {
|
return {
|
||||||
url: "/og-image-agent-teams-v5.png",
|
url: "/og-image-agent-teams-v6.png",
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 630,
|
height: 630,
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const props = defineProps<{
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const siteUrl = ((config.public.siteUrl as string) || "https://777genius.github.io/agent-teams-ai").replace(/\/+$/, "");
|
const siteUrl = ((config.public.siteUrl as string) || "https://777genius.github.io/agent-teams-ai").replace(/\/+$/, "");
|
||||||
const ogImage = `${siteUrl}/og-image-agent-teams-v5.png`;
|
const ogImage = `${siteUrl}/og-image-agent-teams-v6.png`;
|
||||||
|
|
||||||
const statusCode = computed(() => props.error?.statusCode || 404);
|
const statusCode = computed(() => props.error?.statusCode || 404);
|
||||||
const isNotFound = computed(() => statusCode.value === 404);
|
const isNotFound = computed(() => statusCode.value === 404);
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const basePrefixedDocsPath = `${baseURL.replace(/\/?$/, "/")}docs`;
|
||||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
const defaultSeoTitle = "Agent Teams - AI Agent Orchestration for Developers";
|
const defaultSeoTitle = "Agent Teams - AI Agent Orchestration for Developers";
|
||||||
const defaultSeoDescription = "Free, open-source desktop app for AI agent teams. Start with a free model with no auth, then connect Claude, Codex, or OpenCode when you need more models.";
|
const defaultSeoDescription = "Free, open-source desktop app for AI agent teams. Start with a free model with no auth, then connect Claude, Codex, or OpenCode when you need more models.";
|
||||||
const defaultSeoImage = `${siteUrl.replace(/\/+$/, "")}/og-image-agent-teams-v5.png`;
|
const defaultSeoImage = `${siteUrl.replace(/\/+$/, "")}/og-image-agent-teams-v6.png`;
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: "2026-01-19",
|
compatibilityDate: "2026-01-19",
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ const publicBaseUrl =
|
||||||
const docsUrl = `${publicBaseUrl}docs/`;
|
const docsUrl = `${publicBaseUrl}docs/`;
|
||||||
const downloadUrl = `${publicBaseUrl}download/`;
|
const downloadUrl = `${publicBaseUrl}download/`;
|
||||||
const ruDownloadUrl = `${publicBaseUrl}ru/download/`;
|
const ruDownloadUrl = `${publicBaseUrl}ru/download/`;
|
||||||
|
const ogImageUrl = `${publicBaseUrl}og-image-agent-teams-v6.png`;
|
||||||
const landingPublicDir = fileURLToPath(new URL("../../public", import.meta.url));
|
const landingPublicDir = fileURLToPath(new URL("../../public", import.meta.url));
|
||||||
|
|
||||||
const rootGuide: DefaultTheme.SidebarItem[] = [
|
const rootGuide: DefaultTheme.SidebarItem[] = [
|
||||||
|
|
@ -173,7 +174,7 @@ export default defineConfig({
|
||||||
["meta", { property: "og:title", content: SITE_TITLE }],
|
["meta", { property: "og:title", content: SITE_TITLE }],
|
||||||
["meta", { property: "og:description", content: SITE_DESCRIPTION }],
|
["meta", { property: "og:description", content: SITE_DESCRIPTION }],
|
||||||
["meta", { property: "og:url", content: docsUrl }],
|
["meta", { property: "og:url", content: docsUrl }],
|
||||||
["meta", { property: "og:image", content: `${publicBaseUrl}og-image.png` }],
|
["meta", { property: "og:image", content: ogImageUrl }],
|
||||||
["meta", { property: "og:image:width", content: "1200" }],
|
["meta", { property: "og:image:width", content: "1200" }],
|
||||||
["meta", { property: "og:image:height", content: "630" }],
|
["meta", { property: "og:image:height", content: "630" }],
|
||||||
["meta", { property: "og:site_name", content: "Agent Teams" }],
|
["meta", { property: "og:site_name", content: "Agent Teams" }],
|
||||||
|
|
@ -181,7 +182,7 @@ export default defineConfig({
|
||||||
["meta", { name: "twitter:card", content: "summary_large_image" }],
|
["meta", { name: "twitter:card", content: "summary_large_image" }],
|
||||||
["meta", { name: "twitter:title", content: SITE_TITLE }],
|
["meta", { name: "twitter:title", content: SITE_TITLE }],
|
||||||
["meta", { name: "twitter:description", content: SITE_DESCRIPTION }],
|
["meta", { name: "twitter:description", content: SITE_DESCRIPTION }],
|
||||||
["meta", { name: "twitter:image", content: `${publicBaseUrl}og-image.png` }],
|
["meta", { name: "twitter:image", content: ogImageUrl }],
|
||||||
[
|
[
|
||||||
"script",
|
"script",
|
||||||
{ type: "application/ld+json" },
|
{ type: "application/ld+json" },
|
||||||
|
|
|
||||||
BIN
landing/public/og-image-agent-teams-v6.png
Normal file
BIN
landing/public/og-image-agent-teams-v6.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 673 KiB |
|
|
@ -15,8 +15,9 @@ export default defineEventHandler((event) => {
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const siteUrl = ((config.public.siteUrl as string) || "https://777genius.github.io/agent-teams-ai").replace(/\/+$/, "");
|
const siteUrl = ((config.public.siteUrl as string) || "https://777genius.github.io/agent-teams-ai").replace(/\/+$/, "");
|
||||||
const toSiteUrl = (path: string) => `${siteUrl}${path === "/" ? "/" : `/${path.replace(/^\/+/, "")}`}`;
|
const toSiteUrl = (path: string) => `${siteUrl}${path === "/" ? "/" : `/${path.replace(/^\/+/, "")}`}`;
|
||||||
const homeImagePaths = ["og-image.png", ...screenshots.map((screenshot) => screenshot.path)];
|
const ogImagePath = "og-image-agent-teams-v6.png";
|
||||||
const downloadImagePaths = ["og-image.png", "logo-192.png"];
|
const homeImagePaths = [ogImagePath, ...screenshots.map((screenshot) => screenshot.path)];
|
||||||
|
const downloadImagePaths = [ogImagePath, "logo-192.png"];
|
||||||
|
|
||||||
setHeader(event, "content-type", "application/xml; charset=utf-8");
|
setHeader(event, "content-type", "application/xml; charset=utf-8");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -523,6 +523,7 @@ async function main() {
|
||||||
|
|
||||||
const uiEnv = {
|
const uiEnv = {
|
||||||
...process.env,
|
...process.env,
|
||||||
|
UV_THREADPOOL_SIZE: process.env.UV_THREADPOOL_SIZE?.trim() || '16',
|
||||||
CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH: resolvedRuntime.binaryPath,
|
CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH: resolvedRuntime.binaryPath,
|
||||||
};
|
};
|
||||||
delete uiEnv.CLAUDE_CLI_PATH;
|
delete uiEnv.CLAUDE_CLI_PATH;
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ export function splitSettingsJsonArgs(args: string[]): SplitSettingsJsonArgsResu
|
||||||
const parsed = parseJsonSettingsObject(value);
|
const parsed = parseJsonSettingsObject(value);
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
settingsFragments.push(parsed);
|
settingsFragments.push(parsed);
|
||||||
|
index += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2198,6 +2198,24 @@ function buildAnthropicCrossProviderDirectAuthEnvPatch(
|
||||||
return envPatch;
|
return envPatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CODEX_CROSS_PROVIDER_SAFE_ENV_KEYS = [
|
||||||
|
'CLAUDE_CODE_CODEX_BACKEND',
|
||||||
|
'CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD',
|
||||||
|
'CODEX_CLI_PATH',
|
||||||
|
'CODEX_HOME',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function buildCodexCrossProviderSafeEnvPatch(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||||
|
const envPatch: NodeJS.ProcessEnv = {};
|
||||||
|
for (const key of CODEX_CROSS_PROVIDER_SAFE_ENV_KEYS) {
|
||||||
|
const value = env[key]?.trim();
|
||||||
|
if (value) {
|
||||||
|
envPatch[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return envPatch;
|
||||||
|
}
|
||||||
|
|
||||||
interface TeamRuntimeAuthContext {
|
interface TeamRuntimeAuthContext {
|
||||||
teamName?: string;
|
teamName?: string;
|
||||||
authMaterialId?: string;
|
authMaterialId?: string;
|
||||||
|
|
@ -2219,6 +2237,7 @@ interface TeamRuntimeLaunchArgsPlan {
|
||||||
runtimeTurnSettledHookArgs: string[];
|
runtimeTurnSettledHookArgs: string[];
|
||||||
providerArgs: string[];
|
providerArgs: string[];
|
||||||
extraArgs: string[];
|
extraArgs: string[];
|
||||||
|
inheritedProviderArgs: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkspaceTrustProviderArgsResolver = (input: {
|
type WorkspaceTrustProviderArgsResolver = (input: {
|
||||||
|
|
@ -4218,6 +4237,7 @@ export class TeamProvisioningService {
|
||||||
launchIdentity?: ProviderModelLaunchIdentity | null;
|
launchIdentity?: ProviderModelLaunchIdentity | null;
|
||||||
envResolution: ProvisioningEnvResolution;
|
envResolution: ProvisioningEnvResolution;
|
||||||
extraArgs?: string[];
|
extraArgs?: string[];
|
||||||
|
inheritedProviderArgs?: string[];
|
||||||
includeAnthropicHelper: boolean;
|
includeAnthropicHelper: boolean;
|
||||||
contextLabel: string;
|
contextLabel: string;
|
||||||
}): Promise<TeamRuntimeLaunchArgsPlan> {
|
}): Promise<TeamRuntimeLaunchArgsPlan> {
|
||||||
|
|
@ -4228,6 +4248,7 @@ export class TeamProvisioningService {
|
||||||
: null;
|
: null;
|
||||||
const rawProviderArgs = input.envResolution.providerArgs ?? [];
|
const rawProviderArgs = input.envResolution.providerArgs ?? [];
|
||||||
const rawExtraArgs = input.extraArgs ?? [];
|
const rawExtraArgs = input.extraArgs ?? [];
|
||||||
|
const rawInheritedProviderArgs = input.inheritedProviderArgs ?? [];
|
||||||
|
|
||||||
if (!helper && resolvedProviderId !== 'anthropic') {
|
if (!helper && resolvedProviderId !== 'anthropic') {
|
||||||
return {
|
return {
|
||||||
|
|
@ -4237,6 +4258,7 @@ export class TeamProvisioningService {
|
||||||
await this.buildRuntimeTurnSettledHookSettingsArgs(resolvedProviderId),
|
await this.buildRuntimeTurnSettledHookSettingsArgs(resolvedProviderId),
|
||||||
providerArgs: rawProviderArgs,
|
providerArgs: rawProviderArgs,
|
||||||
extraArgs: rawExtraArgs,
|
extraArgs: rawExtraArgs,
|
||||||
|
inheritedProviderArgs: rawInheritedProviderArgs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4246,15 +4268,29 @@ export class TeamProvisioningService {
|
||||||
);
|
);
|
||||||
const splitProviderArgs = splitSettingsJsonArgs(providerArgsWithoutHelper);
|
const splitProviderArgs = splitSettingsJsonArgs(providerArgsWithoutHelper);
|
||||||
const splitExtraArgs = splitSettingsJsonArgs(rawExtraArgs);
|
const splitExtraArgs = splitSettingsJsonArgs(rawExtraArgs);
|
||||||
|
const splitInheritedArgs = splitSettingsJsonArgs(rawInheritedProviderArgs);
|
||||||
|
const shouldCoalesceInheritedSettings = splitInheritedArgs.settingsFragments.length > 0;
|
||||||
if (
|
if (
|
||||||
helper &&
|
helper &&
|
||||||
(hasPathBasedSettingsArgs(splitProviderArgs.passthroughArgs) ||
|
(hasPathBasedSettingsArgs(splitProviderArgs.passthroughArgs) ||
|
||||||
hasPathBasedSettingsArgs(splitExtraArgs.passthroughArgs))
|
hasPathBasedSettingsArgs(splitExtraArgs.passthroughArgs) ||
|
||||||
|
hasPathBasedSettingsArgs(splitInheritedArgs.passthroughArgs))
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${input.contextLabel}: app-managed Anthropic API-key helper cannot be combined with path-based --settings. Use inline JSON settings or remove the custom --settings path.`
|
`${input.contextLabel}: app-managed Anthropic API-key helper cannot be combined with path-based --settings. Use inline JSON settings or remove the custom --settings path.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
shouldCoalesceInheritedSettings &&
|
||||||
|
!helper &&
|
||||||
|
(hasPathBasedSettingsArgs(splitProviderArgs.passthroughArgs) ||
|
||||||
|
hasPathBasedSettingsArgs(splitExtraArgs.passthroughArgs) ||
|
||||||
|
hasPathBasedSettingsArgs(splitInheritedArgs.passthroughArgs))
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`${input.contextLabel}: mixed-provider launch cannot combine app-managed inherited settings with path-based --settings. Use inline JSON settings or remove the custom --settings path.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const settingsBundle = await materializeTeamRuntimeSettingsBundle({
|
const settingsBundle = await materializeTeamRuntimeSettingsBundle({
|
||||||
teamName: input.teamName,
|
teamName: input.teamName,
|
||||||
|
|
@ -4264,6 +4300,7 @@ export class TeamProvisioningService {
|
||||||
await this.buildRuntimeTurnSettledHookSettingsObject(resolvedProviderId),
|
await this.buildRuntimeTurnSettledHookSettingsObject(resolvedProviderId),
|
||||||
...splitProviderArgs.settingsFragments,
|
...splitProviderArgs.settingsFragments,
|
||||||
...splitExtraArgs.settingsFragments,
|
...splitExtraArgs.settingsFragments,
|
||||||
|
...splitInheritedArgs.settingsFragments,
|
||||||
],
|
],
|
||||||
anthropicHelper: helper,
|
anthropicHelper: helper,
|
||||||
settingsDirectory: helper ? null : buildRuntimeSettingsTempDirectory(input.teamName),
|
settingsDirectory: helper ? null : buildRuntimeSettingsTempDirectory(input.teamName),
|
||||||
|
|
@ -4275,6 +4312,7 @@ export class TeamProvisioningService {
|
||||||
runtimeTurnSettledHookArgs: [],
|
runtimeTurnSettledHookArgs: [],
|
||||||
providerArgs: splitProviderArgs.passthroughArgs,
|
providerArgs: splitProviderArgs.passthroughArgs,
|
||||||
extraArgs: splitExtraArgs.passthroughArgs,
|
extraArgs: splitExtraArgs.passthroughArgs,
|
||||||
|
inheritedProviderArgs: splitInheritedArgs.passthroughArgs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -20181,6 +20219,7 @@ export class TeamProvisioningService {
|
||||||
launchIdentity,
|
launchIdentity,
|
||||||
envResolution: { ...provisioningEnv, providerArgs: providerArgsForLaunch },
|
envResolution: { ...provisioningEnv, providerArgs: providerArgsForLaunch },
|
||||||
extraArgs: extraCliArgs,
|
extraArgs: extraCliArgs,
|
||||||
|
inheritedProviderArgs: crossProviderMemberArgsForLaunch.args,
|
||||||
includeAnthropicHelper: resolvedProviderId === 'anthropic',
|
includeAnthropicHelper: resolvedProviderId === 'anthropic',
|
||||||
contextLabel: 'Team create launch',
|
contextLabel: 'Team create launch',
|
||||||
});
|
});
|
||||||
|
|
@ -20216,7 +20255,7 @@ export class TeamProvisioningService {
|
||||||
...runtimeArgsPlan.extraArgs,
|
...runtimeArgsPlan.extraArgs,
|
||||||
...runtimeArgsPlan.providerArgs,
|
...runtimeArgsPlan.providerArgs,
|
||||||
...runtimeArgsPlan.settingsArgs,
|
...runtimeArgsPlan.settingsArgs,
|
||||||
...crossProviderMemberArgsForLaunch.args,
|
...runtimeArgsPlan.inheritedProviderArgs,
|
||||||
]);
|
]);
|
||||||
const runtimeWarning = buildRuntimeLaunchWarning(request, shellEnv, {
|
const runtimeWarning = buildRuntimeLaunchWarning(request, shellEnv, {
|
||||||
geminiRuntimeAuth,
|
geminiRuntimeAuth,
|
||||||
|
|
@ -21509,6 +21548,7 @@ export class TeamProvisioningService {
|
||||||
launchIdentity,
|
launchIdentity,
|
||||||
envResolution: { ...provisioningEnv, providerArgs: providerArgsForLaunch },
|
envResolution: { ...provisioningEnv, providerArgs: providerArgsForLaunch },
|
||||||
extraArgs: extraCliArgs,
|
extraArgs: extraCliArgs,
|
||||||
|
inheritedProviderArgs: crossProviderMemberArgsForLaunch.args,
|
||||||
includeAnthropicHelper: resolvedProviderId === 'anthropic',
|
includeAnthropicHelper: resolvedProviderId === 'anthropic',
|
||||||
contextLabel: 'Team launch',
|
contextLabel: 'Team launch',
|
||||||
});
|
});
|
||||||
|
|
@ -21533,7 +21573,7 @@ export class TeamProvisioningService {
|
||||||
// Without this, a codex teammate spawned from an anthropic lead has no way to learn
|
// Without this, a codex teammate spawned from an anthropic lead has no way to learn
|
||||||
// about the required forced_login_method (chatgpt/api) and fails to start.
|
// about the required forced_login_method (chatgpt/api) and fails to start.
|
||||||
emitProvisioningCheckpoint(run, 'Resolving cross-provider member launch args');
|
emitProvisioningCheckpoint(run, 'Resolving cross-provider member launch args');
|
||||||
launchArgs.push(...crossProviderMemberArgsForLaunch.args);
|
launchArgs.push(...runtimeArgsPlan.inheritedProviderArgs);
|
||||||
const finalLaunchArgs = mergeJsonSettingsArgs(launchArgs);
|
const finalLaunchArgs = mergeJsonSettingsArgs(launchArgs);
|
||||||
const runtimeWarning = buildRuntimeLaunchWarning(request, shellEnv, {
|
const runtimeWarning = buildRuntimeLaunchWarning(request, shellEnv, {
|
||||||
geminiRuntimeAuth,
|
geminiRuntimeAuth,
|
||||||
|
|
@ -35178,6 +35218,9 @@ export class TeamProvisioningService {
|
||||||
args.push(...(await this.buildRuntimeTurnSettledHookSettingsArgs(providerId)));
|
args.push(...(await this.buildRuntimeTurnSettledHookSettingsArgs(providerId)));
|
||||||
const providerArgs = env.providerArgs ?? [];
|
const providerArgs = env.providerArgs ?? [];
|
||||||
providerArgsByProvider.set(providerId, providerArgs);
|
providerArgsByProvider.set(providerId, providerArgs);
|
||||||
|
if (providerId === 'codex') {
|
||||||
|
Object.assign(envPatch, buildCodexCrossProviderSafeEnvPatch(env.env));
|
||||||
|
}
|
||||||
if (env.anthropicApiKeyHelper) {
|
if (env.anthropicApiKeyHelper) {
|
||||||
usesAnthropicApiKeyHelper = true;
|
usesAnthropicApiKeyHelper = true;
|
||||||
Object.assign(envPatch, env.anthropicApiKeyHelper.envPatch);
|
Object.assign(envPatch, env.anthropicApiKeyHelper.envPatch);
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ const DIRECT_TMUX_RESTART_ENV_KEYS = [
|
||||||
'CLAUDE_CODE_ENTRY_PROVIDER',
|
'CLAUDE_CODE_ENTRY_PROVIDER',
|
||||||
'CLAUDE_CODE_GEMINI_BACKEND',
|
'CLAUDE_CODE_GEMINI_BACKEND',
|
||||||
'CLAUDE_CODE_CODEX_BACKEND',
|
'CLAUDE_CODE_CODEX_BACKEND',
|
||||||
|
'CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD',
|
||||||
|
'CODEX_CLI_PATH',
|
||||||
'CODEX_HOME',
|
'CODEX_HOME',
|
||||||
CLAUDE_TEAM_ANTHROPIC_AUTH_MODE_ENV,
|
CLAUDE_TEAM_ANTHROPIC_AUTH_MODE_ENV,
|
||||||
CLAUDE_TEAM_ANTHROPIC_API_KEY_HELPER_SETTINGS_PATH_ENV,
|
CLAUDE_TEAM_ANTHROPIC_API_KEY_HELPER_SETTINGS_PATH_ENV,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
// @vitest-environment node
|
// @vitest-environment node
|
||||||
|
import {
|
||||||
|
materializeTeamRuntimeSettingsBundle,
|
||||||
|
splitSettingsJsonArgs,
|
||||||
|
} from '@main/services/runtime/teamRuntimeSettingsBundle';
|
||||||
import { mkdtemp, readFile, rm } from 'fs/promises';
|
import { mkdtemp, readFile, rm } from 'fs/promises';
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { afterEach, describe, expect, it } from 'vitest';
|
import { afterEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { materializeTeamRuntimeSettingsBundle } from '@main/services/runtime/teamRuntimeSettingsBundle';
|
|
||||||
|
|
||||||
describe('teamRuntimeSettingsBundle', () => {
|
describe('teamRuntimeSettingsBundle', () => {
|
||||||
const tempRoots: string[] = [];
|
const tempRoots: string[] = [];
|
||||||
|
|
||||||
|
|
@ -71,4 +72,17 @@ describe('teamRuntimeSettingsBundle', () => {
|
||||||
expect(settings.env.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
|
expect(settings.env.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
|
||||||
expect(settings.hooks.Stop).toHaveLength(1);
|
expect(settings.hooks.Stop).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('splits equals-style JSON settings without dropping later args', () => {
|
||||||
|
expect(
|
||||||
|
splitSettingsJsonArgs([
|
||||||
|
'--settings={"codex":{"forced_login_method":"chatgpt"}}',
|
||||||
|
'--model',
|
||||||
|
'gpt-5.5',
|
||||||
|
])
|
||||||
|
).toEqual({
|
||||||
|
settingsFragments: [{ codex: { forced_login_method: 'chatgpt' } }],
|
||||||
|
passthroughArgs: ['--model', 'gpt-5.5'],
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -303,6 +303,70 @@ liveDescribe('Mixed provider team launch live e2e', () => {
|
||||||
([laneId, lane]) => lane.state === 'active' && laneId === 'secondary:opencode:oscar'
|
([laneId, lane]) => lane.state === 'active' && laneId === 'secondary:opencode:oscar'
|
||||||
)
|
)
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
|
||||||
|
await cleanupMixedProviderSmokeTeam(harness, teamName);
|
||||||
|
|
||||||
|
const relaunchProgressEvents: TeamProvisioningProgress[] = [];
|
||||||
|
await harness.svc.launchTeam(
|
||||||
|
{
|
||||||
|
teamName,
|
||||||
|
cwd: projectPath,
|
||||||
|
providerId: 'anthropic',
|
||||||
|
model: anthropicModel,
|
||||||
|
skipPermissions: true,
|
||||||
|
clearContext: true,
|
||||||
|
},
|
||||||
|
(progress) => {
|
||||||
|
relaunchProgressEvents.push(progress);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitUntil(async () => {
|
||||||
|
const last = relaunchProgressEvents.at(-1);
|
||||||
|
if (last?.state === 'failed') {
|
||||||
|
throw new Error(formatProgressDump(relaunchProgressEvents));
|
||||||
|
}
|
||||||
|
return last?.state === 'ready';
|
||||||
|
}, 360_000);
|
||||||
|
|
||||||
|
await waitUntilWithDiagnostics(async () => {
|
||||||
|
const status = await harness!.svc.getMemberSpawnStatuses(teamName!);
|
||||||
|
if (status.teamLaunchState === 'partial_failure') {
|
||||||
|
throw new Error(
|
||||||
|
await formatMixedLaunchDiagnostics(harness!, teamName!, relaunchProgressEvents)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const memberName of ['alice', 'cody', 'oscar'] as const) {
|
||||||
|
const member = status.statuses[memberName];
|
||||||
|
if (
|
||||||
|
member?.status !== 'online' ||
|
||||||
|
member.launchState !== 'confirmed_alive' ||
|
||||||
|
member.bootstrapConfirmed !== true
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}, 180_000, () => formatMixedLaunchDiagnostics(harness!, teamName!, relaunchProgressEvents));
|
||||||
|
|
||||||
|
await waitUntilWithDiagnostics(async () => {
|
||||||
|
const snapshot = await harness!.svc.getTeamAgentRuntimeSnapshot(teamName!);
|
||||||
|
return (
|
||||||
|
snapshot.members.alice?.providerId === 'anthropic' &&
|
||||||
|
snapshot.members.alice.alive === true &&
|
||||||
|
snapshot.members.cody?.providerId === 'codex' &&
|
||||||
|
snapshot.members.cody.alive === true &&
|
||||||
|
snapshot.members.oscar?.providerId === 'opencode' &&
|
||||||
|
snapshot.members.oscar.alive === true
|
||||||
|
);
|
||||||
|
}, 180_000, () => formatMixedLaunchDiagnostics(harness!, teamName!, relaunchProgressEvents));
|
||||||
|
|
||||||
|
const relaunchedLaneIndex = await readOpenCodeRuntimeLaneIndex(getTeamsBasePath(), teamName);
|
||||||
|
expect(
|
||||||
|
Object.entries(relaunchedLaneIndex.lanes).some(
|
||||||
|
([laneId, lane]) => lane.state === 'active' && laneId === 'secondary:opencode:oscar'
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
},
|
},
|
||||||
480_000
|
480_000
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -57,9 +57,11 @@ describe('TeamProvisioningDirectRestart', () => {
|
||||||
const assignments = buildDirectTmuxRestartEnvAssignments(
|
const assignments = buildDirectTmuxRestartEnvAssignments(
|
||||||
{
|
{
|
||||||
CODEX_HOME: '/tmp/codex home',
|
CODEX_HOME: '/tmp/codex home',
|
||||||
|
CODEX_CLI_PATH: '/opt/codex/bin/codex',
|
||||||
CLAUDE_CODE_USE_GEMINI: '1',
|
CLAUDE_CODE_USE_GEMINI: '1',
|
||||||
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',
|
||||||
},
|
},
|
||||||
'codex'
|
'codex'
|
||||||
);
|
);
|
||||||
|
|
@ -67,9 +69,11 @@ describe('TeamProvisioningDirectRestart', () => {
|
||||||
expect(assignments).toContain("CLAUDECODE='1'");
|
expect(assignments).toContain("CLAUDECODE='1'");
|
||||||
expect(assignments).toContain("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS='1'");
|
expect(assignments).toContain("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS='1'");
|
||||||
expect(assignments).toContain("CODEX_HOME='/tmp/codex home'");
|
expect(assignments).toContain("CODEX_HOME='/tmp/codex home'");
|
||||||
|
expect(assignments).toContain("CODEX_CLI_PATH='/opt/codex/bin/codex'");
|
||||||
expect(assignments).toContain("CLAUDE_CODE_USE_GEMINI=''");
|
expect(assignments).toContain("CLAUDE_CODE_USE_GEMINI=''");
|
||||||
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_PROVIDER_MANAGED_BY_HOST='1'");
|
expect(assignments).toContain("CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST='1'");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -530,6 +530,7 @@ describe('TeamProvisioningService member MCP config safe e2e', () => {
|
||||||
providerArgs: string[];
|
providerArgs: string[];
|
||||||
settingsArgs: string[];
|
settingsArgs: string[];
|
||||||
extraArgs: string[];
|
extraArgs: string[];
|
||||||
|
inheritedProviderArgs: string[];
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
).buildTeamRuntimeLaunchArgsPlan = vi.fn(async () => ({
|
).buildTeamRuntimeLaunchArgsPlan = vi.fn(async () => ({
|
||||||
|
|
@ -538,6 +539,7 @@ describe('TeamProvisioningService member MCP config safe e2e', () => {
|
||||||
providerArgs: [],
|
providerArgs: [],
|
||||||
settingsArgs: [],
|
settingsArgs: [],
|
||||||
extraArgs: [],
|
extraArgs: [],
|
||||||
|
inheritedProviderArgs: [],
|
||||||
}));
|
}));
|
||||||
(
|
(
|
||||||
svc as unknown as { updateDirectTmuxRestartMemberConfig: () => Promise<void> }
|
svc as unknown as { updateDirectTmuxRestartMemberConfig: () => Promise<void> }
|
||||||
|
|
|
||||||
|
|
@ -15537,6 +15537,7 @@ describe('TeamProvisioningService', () => {
|
||||||
providerArgs: [],
|
providerArgs: [],
|
||||||
settingsArgs: [],
|
settingsArgs: [],
|
||||||
extraArgs: [],
|
extraArgs: [],
|
||||||
|
inheritedProviderArgs: [],
|
||||||
}));
|
}));
|
||||||
(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 () => {});
|
||||||
|
|
@ -15635,6 +15636,7 @@ describe('TeamProvisioningService', () => {
|
||||||
providerArgs: [],
|
providerArgs: [],
|
||||||
settingsArgs: [],
|
settingsArgs: [],
|
||||||
extraArgs: [],
|
extraArgs: [],
|
||||||
|
inheritedProviderArgs: [],
|
||||||
}));
|
}));
|
||||||
(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 () => {});
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { buildCodexWorkspaceTrustSettingsArgs } from '@features/workspace-trust/core/domain';
|
||||||
import { OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE } from '@shared/utils/openCodeWindowsAccessDenied';
|
import { OPENCODE_WINDOWS_ACCESS_DENIED_MESSAGE } from '@shared/utils/openCodeWindowsAccessDenied';
|
||||||
import { DEFAULT_PROVIDER_MODEL_SELECTION } from '@shared/utils/providerModelSelection';
|
import { DEFAULT_PROVIDER_MODEL_SELECTION } from '@shared/utils/providerModelSelection';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
|
|
@ -555,6 +556,44 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
|
||||||
expect(result.args).toContain('--anthropic-safe-passthrough');
|
expect(result.args).toContain('--anthropic-safe-passthrough');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('passes only non-secret Codex runtime env to non-Codex leads for cross-provider teammates', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
vi.spyOn(svc as any, 'buildProvisioningEnv').mockResolvedValue({
|
||||||
|
env: {
|
||||||
|
CLAUDE_CODE_CODEX_BACKEND: 'codex-native',
|
||||||
|
CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD: 'chatgpt',
|
||||||
|
CODEX_CLI_PATH: '/Users/tester/.local/bin/codex',
|
||||||
|
CODEX_HOME: '/Users/tester/.codex',
|
||||||
|
OPENAI_API_KEY: 'sk-openai-should-not-leak',
|
||||||
|
CODEX_API_KEY: 'sk-codex-should-not-leak',
|
||||||
|
GEMINI_API_KEY: 'gemini-should-not-leak',
|
||||||
|
ANTHROPIC_API_KEY: 'sk-ant-should-not-leak',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'ant-token-should-not-leak',
|
||||||
|
},
|
||||||
|
authSource: 'codex_runtime',
|
||||||
|
geminiRuntimeAuth: null,
|
||||||
|
providerArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await (svc as any).buildCrossProviderMemberArgs(
|
||||||
|
'anthropic',
|
||||||
|
[{ name: 'jack', providerId: 'codex', model: 'gpt-5.4' }],
|
||||||
|
{ teamRuntimeAuth: { teamName: 'mixed-team', authMaterialId: 'run-1' } }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.envPatch).toMatchObject({
|
||||||
|
CLAUDE_CODE_CODEX_BACKEND: 'codex-native',
|
||||||
|
CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD: 'chatgpt',
|
||||||
|
CODEX_CLI_PATH: '/Users/tester/.local/bin/codex',
|
||||||
|
CODEX_HOME: '/Users/tester/.codex',
|
||||||
|
});
|
||||||
|
expect(result.envPatch.OPENAI_API_KEY).toBeUndefined();
|
||||||
|
expect(result.envPatch.CODEX_API_KEY).toBeUndefined();
|
||||||
|
expect(result.envPatch.GEMINI_API_KEY).toBeUndefined();
|
||||||
|
expect(result.envPatch.ANTHROPIC_API_KEY).toBeUndefined();
|
||||||
|
expect(result.envPatch.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('passes Anthropic-compatible bearer env to non-Anthropic leads without injecting ANTHROPIC_API_KEY', async () => {
|
it('passes Anthropic-compatible bearer env to non-Anthropic leads without injecting ANTHROPIC_API_KEY', async () => {
|
||||||
const svc = new TeamProvisioningService();
|
const svc = new TeamProvisioningService();
|
||||||
vi.spyOn(svc as any, 'buildProvisioningEnv').mockResolvedValue({
|
vi.spyOn(svc as any, 'buildProvisioningEnv').mockResolvedValue({
|
||||||
|
|
@ -3511,6 +3550,427 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
|
||||||
expect(settings.hooks.Stop[0].hooks[0].command).toBe('/bin/true # test-hook');
|
expect(settings.hooks.Stop[0].hooks[0].command).toBe('/bin/true # test-hook');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('coalesces inherited cross-provider JSON settings into the Anthropic runtime settings file', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
|
||||||
|
const result = await (svc as any).buildTeamRuntimeLaunchArgsPlan({
|
||||||
|
teamName: 'anthropic-codex-inherited-settings-team',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchIdentity: null,
|
||||||
|
envResolution: { providerArgs: [] },
|
||||||
|
extraArgs: [],
|
||||||
|
inheritedProviderArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'],
|
||||||
|
includeAnthropicHelper: false,
|
||||||
|
contextLabel: 'Team launch',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.settingsArgs[0]).toBe('--settings');
|
||||||
|
expect(result.inheritedProviderArgs).toEqual([]);
|
||||||
|
const settings = JSON.parse(fs.readFileSync(result.settingsArgs[1], 'utf8'));
|
||||||
|
expect(settings.codex.forced_login_method).toBe('chatgpt');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merges provider, extra, and inherited JSON settings in launch precedence order', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
|
||||||
|
const result = await (svc as any).buildTeamRuntimeLaunchArgsPlan({
|
||||||
|
teamName: 'anthropic-codex-merged-settings-team',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchIdentity: null,
|
||||||
|
envResolution: {
|
||||||
|
providerArgs: [
|
||||||
|
'--settings',
|
||||||
|
'{"codex":{"forced_login_method":"api","nested":{"provider":true}}}',
|
||||||
|
'--provider-passthrough',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
extraArgs: [
|
||||||
|
'--settings={"codex":{"nested":{"extra":true}}}',
|
||||||
|
'--extra-passthrough',
|
||||||
|
],
|
||||||
|
inheritedProviderArgs: [
|
||||||
|
'--settings',
|
||||||
|
'{"codex":{"forced_login_method":"chatgpt","nested":{"inherited":true}}}',
|
||||||
|
'--inherited-passthrough',
|
||||||
|
],
|
||||||
|
includeAnthropicHelper: false,
|
||||||
|
contextLabel: 'Team launch',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.providerArgs).toEqual(['--provider-passthrough']);
|
||||||
|
expect(result.extraArgs).toEqual(['--extra-passthrough']);
|
||||||
|
expect(result.inheritedProviderArgs).toEqual(['--inherited-passthrough']);
|
||||||
|
const settings = JSON.parse(fs.readFileSync(result.settingsArgs[1], 'utf8'));
|
||||||
|
expect(settings.codex).toMatchObject({
|
||||||
|
forced_login_method: 'chatgpt',
|
||||||
|
nested: {
|
||||||
|
provider: true,
|
||||||
|
extra: true,
|
||||||
|
inherited: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('coalesces equals-style inherited settings while preserving inherited passthrough args', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
|
||||||
|
const result = await (svc as any).buildTeamRuntimeLaunchArgsPlan({
|
||||||
|
teamName: 'anthropic-codex-equals-inherited-settings-team',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchIdentity: null,
|
||||||
|
envResolution: { providerArgs: [] },
|
||||||
|
extraArgs: [],
|
||||||
|
inheritedProviderArgs: [
|
||||||
|
'--settings={"codex":{"forced_login_method":"chatgpt"}}',
|
||||||
|
'--safe-inherited-flag',
|
||||||
|
'value',
|
||||||
|
],
|
||||||
|
includeAnthropicHelper: false,
|
||||||
|
contextLabel: 'Team launch',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.inheritedProviderArgs).toEqual(['--safe-inherited-flag', 'value']);
|
||||||
|
const settings = JSON.parse(fs.readFileSync(result.settingsArgs[1], 'utf8'));
|
||||||
|
expect(settings.codex.forced_login_method).toBe('chatgpt');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves inherited settings untouched for non-Anthropic lead providers', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const inheritedProviderArgs = ['--settings', '{"anthropic":{"example":true}}'];
|
||||||
|
|
||||||
|
const result = await (svc as any).buildTeamRuntimeLaunchArgsPlan({
|
||||||
|
teamName: 'codex-lead-anthropic-inherited-settings-team',
|
||||||
|
providerId: 'codex',
|
||||||
|
launchIdentity: null,
|
||||||
|
envResolution: { providerArgs: [] },
|
||||||
|
extraArgs: [],
|
||||||
|
inheritedProviderArgs,
|
||||||
|
includeAnthropicHelper: false,
|
||||||
|
contextLabel: 'Team launch',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.settingsArgs).toEqual([]);
|
||||||
|
expect(result.inheritedProviderArgs).toEqual(inheritedProviderArgs);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('coalesces inherited JSON settings into Anthropic helper settings without keeping helper path args', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const helperDir = path.join(tempRoot, 'anthropic-helper');
|
||||||
|
const helperSettingsPath = path.join(helperDir, 'settings.json');
|
||||||
|
|
||||||
|
const result = await (svc as any).buildTeamRuntimeLaunchArgsPlan({
|
||||||
|
teamName: 'anthropic-helper-codex-inherited-settings-team',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchIdentity: null,
|
||||||
|
envResolution: {
|
||||||
|
providerArgs: ['--settings', helperSettingsPath],
|
||||||
|
anthropicApiKeyHelper: {
|
||||||
|
directory: helperDir,
|
||||||
|
helperPath: path.join(helperDir, 'helper.sh'),
|
||||||
|
keyPath: path.join(helperDir, 'key'),
|
||||||
|
settingsPath: helperSettingsPath,
|
||||||
|
settingsObject: { apiKeyHelper: `'${path.join(helperDir, 'helper.sh')}'` },
|
||||||
|
settingsArgs: ['--settings', helperSettingsPath],
|
||||||
|
envPatch: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraArgs: [],
|
||||||
|
inheritedProviderArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'],
|
||||||
|
includeAnthropicHelper: true,
|
||||||
|
contextLabel: 'Team launch',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.providerArgs).toEqual([]);
|
||||||
|
expect(result.inheritedProviderArgs).toEqual([]);
|
||||||
|
expect(result.settingsArgs[0]).toBe('--settings');
|
||||||
|
expect(result.settingsArgs[1]).toContain(helperDir);
|
||||||
|
expect(result.settingsArgs[1]).not.toBe(helperSettingsPath);
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps Anthropic helper credentials authoritative over inherited helper-like settings', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const helperDir = path.join(tempRoot, 'anthropic-helper-precedence');
|
||||||
|
const helperSettingsPath = path.join(helperDir, 'settings.json');
|
||||||
|
const helperPath = path.join(helperDir, 'helper.sh');
|
||||||
|
|
||||||
|
const result = await (svc as any).buildTeamRuntimeLaunchArgsPlan({
|
||||||
|
teamName: 'anthropic-helper-precedence-team',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchIdentity: null,
|
||||||
|
envResolution: {
|
||||||
|
providerArgs: ['--settings', helperSettingsPath],
|
||||||
|
anthropicApiKeyHelper: {
|
||||||
|
directory: helperDir,
|
||||||
|
helperPath,
|
||||||
|
keyPath: path.join(helperDir, 'key'),
|
||||||
|
settingsPath: helperSettingsPath,
|
||||||
|
settingsObject: { apiKeyHelper: `'${helperPath}'` },
|
||||||
|
settingsArgs: ['--settings', helperSettingsPath],
|
||||||
|
envPatch: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraArgs: [],
|
||||||
|
inheritedProviderArgs: [
|
||||||
|
'--settings',
|
||||||
|
'{"apiKeyHelper":"\\"/tmp/bad-helper.sh\\"","codex":{"forced_login_method":"chatgpt"}}',
|
||||||
|
],
|
||||||
|
includeAnthropicHelper: true,
|
||||||
|
contextLabel: 'Team launch',
|
||||||
|
});
|
||||||
|
|
||||||
|
const settings = JSON.parse(fs.readFileSync(result.settingsArgs[1], 'utf8'));
|
||||||
|
expect(settings.apiKeyHelper).toBe(`'${helperPath}'`);
|
||||||
|
expect(JSON.stringify(settings)).not.toContain('/tmp/bad-helper.sh');
|
||||||
|
expect(settings.codex.forced_login_method).toBe('chatgpt');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('coalesces multiple non-primary provider settings without leaking provider secrets into env patch', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
vi.spyOn(svc as any, 'buildProvisioningEnv').mockImplementation(
|
||||||
|
(providerId: unknown) => {
|
||||||
|
const resolvedProviderId = typeof providerId === 'string' ? providerId : undefined;
|
||||||
|
if (resolvedProviderId === 'codex') {
|
||||||
|
return Promise.resolve({
|
||||||
|
env: {
|
||||||
|
CLAUDE_CODE_CODEX_BACKEND: 'codex-native',
|
||||||
|
CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD: 'chatgpt',
|
||||||
|
CODEX_CLI_PATH: '/opt/codex',
|
||||||
|
CODEX_HOME: '/Users/tester/.codex',
|
||||||
|
CODEX_API_KEY: 'sk-codex-should-not-leak',
|
||||||
|
OPENAI_API_KEY: 'sk-openai-should-not-leak',
|
||||||
|
},
|
||||||
|
authSource: 'codex_runtime',
|
||||||
|
geminiRuntimeAuth: null,
|
||||||
|
providerArgs: [
|
||||||
|
'--settings',
|
||||||
|
'{"codex":{"forced_login_method":"chatgpt"}}',
|
||||||
|
'--codex-passthrough',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (resolvedProviderId === 'gemini') {
|
||||||
|
return Promise.resolve({
|
||||||
|
env: {
|
||||||
|
GEMINI_API_KEY: 'gemini-should-not-leak',
|
||||||
|
GOOGLE_APPLICATION_CREDENTIALS: '/tmp/gcp-creds.json',
|
||||||
|
},
|
||||||
|
authSource: 'gemini_api_key',
|
||||||
|
geminiRuntimeAuth: null,
|
||||||
|
providerArgs: [
|
||||||
|
'--settings',
|
||||||
|
'{"gemini":{"auth_refresh":"gcp"}}',
|
||||||
|
'--gemini-passthrough',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve({
|
||||||
|
env: {},
|
||||||
|
authSource: 'none',
|
||||||
|
geminiRuntimeAuth: null,
|
||||||
|
providerArgs: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const crossProvider = await (svc as any).buildCrossProviderMemberArgs(
|
||||||
|
'anthropic',
|
||||||
|
[
|
||||||
|
{ name: 'cody', providerId: 'codex', model: 'gpt-5.4' },
|
||||||
|
{ name: 'gina', providerId: 'gemini', model: 'gemini-2.5-pro' },
|
||||||
|
],
|
||||||
|
{ teamRuntimeAuth: { teamName: 'mixed-team', authMaterialId: 'run-1' } }
|
||||||
|
);
|
||||||
|
const result = await (svc as any).buildTeamRuntimeLaunchArgsPlan({
|
||||||
|
teamName: 'anthropic-codex-gemini-inherited-settings-team',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchIdentity: null,
|
||||||
|
envResolution: { providerArgs: [] },
|
||||||
|
extraArgs: [],
|
||||||
|
inheritedProviderArgs: crossProvider.args,
|
||||||
|
includeAnthropicHelper: false,
|
||||||
|
contextLabel: 'Team launch',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(crossProvider.envPatch).toMatchObject({
|
||||||
|
CLAUDE_CODE_CODEX_BACKEND: 'codex-native',
|
||||||
|
CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD: 'chatgpt',
|
||||||
|
CODEX_CLI_PATH: '/opt/codex',
|
||||||
|
CODEX_HOME: '/Users/tester/.codex',
|
||||||
|
});
|
||||||
|
expect(crossProvider.envPatch.CODEX_API_KEY).toBeUndefined();
|
||||||
|
expect(crossProvider.envPatch.OPENAI_API_KEY).toBeUndefined();
|
||||||
|
expect(crossProvider.envPatch.GEMINI_API_KEY).toBeUndefined();
|
||||||
|
expect(crossProvider.envPatch.GOOGLE_APPLICATION_CREDENTIALS).toBeUndefined();
|
||||||
|
expect(result.inheritedProviderArgs).toEqual(['--codex-passthrough', '--gemini-passthrough']);
|
||||||
|
const settings = JSON.parse(fs.readFileSync(result.settingsArgs[1], 'utf8'));
|
||||||
|
expect(settings.codex.forced_login_method).toBe('chatgpt');
|
||||||
|
expect(settings.gemini.auth_refresh).toBe('gcp');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('coalesces workspace trust patches after inherited cross-provider args are patched', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
const trustOverride = 'projects."/repo".trust_level="trusted"';
|
||||||
|
const inheritedProviderArgs = (svc as any).applyWorkspaceTrustArgPatches({
|
||||||
|
args: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'],
|
||||||
|
patches: [
|
||||||
|
{
|
||||||
|
id: 'codex-trust',
|
||||||
|
owner: 'workspace-trust',
|
||||||
|
targetProvider: 'codex',
|
||||||
|
targetSurface: 'cross_provider_member_args',
|
||||||
|
dialect: 'claude-codex-runtime-settings',
|
||||||
|
args: buildCodexWorkspaceTrustSettingsArgs([trustOverride]),
|
||||||
|
dedupeKey: 'codex-trust',
|
||||||
|
sourceWorkspaceIds: ['workspace-1'],
|
||||||
|
reason: 'Codex native trust is carried through sibling runtime settings.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
targetProvider: 'codex',
|
||||||
|
targetSurface: 'cross_provider_member_args',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await (svc as any).buildTeamRuntimeLaunchArgsPlan({
|
||||||
|
teamName: 'anthropic-codex-workspace-trust-team',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchIdentity: null,
|
||||||
|
envResolution: { providerArgs: [] },
|
||||||
|
extraArgs: [],
|
||||||
|
inheritedProviderArgs,
|
||||||
|
includeAnthropicHelper: false,
|
||||||
|
contextLabel: 'Team launch',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.inheritedProviderArgs).toEqual([]);
|
||||||
|
const settings = JSON.parse(fs.readFileSync(result.settingsArgs[1], 'utf8'));
|
||||||
|
expect(settings.codex).toMatchObject({
|
||||||
|
forced_login_method: 'chatgpt',
|
||||||
|
agent_teams_workspace_trust: {
|
||||||
|
config_overrides: [trustOverride],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects path-based settings when inherited mixed-provider settings must be coalesced', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
(svc as any).buildTeamRuntimeLaunchArgsPlan({
|
||||||
|
teamName: 'anthropic-codex-path-settings-team',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchIdentity: null,
|
||||||
|
envResolution: { providerArgs: [] },
|
||||||
|
extraArgs: ['--settings', '/tmp/custom-settings.json'],
|
||||||
|
inheritedProviderArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'],
|
||||||
|
includeAnthropicHelper: false,
|
||||||
|
contextLabel: 'Team launch',
|
||||||
|
})
|
||||||
|
).rejects.toThrow('mixed-provider launch cannot combine app-managed inherited settings');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects provider path-based settings when inherited mixed-provider settings must be coalesced', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
(svc as any).buildTeamRuntimeLaunchArgsPlan({
|
||||||
|
teamName: 'anthropic-codex-provider-path-settings-team',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchIdentity: null,
|
||||||
|
envResolution: { providerArgs: ['--settings', '/tmp/provider-settings.json'] },
|
||||||
|
extraArgs: [],
|
||||||
|
inheritedProviderArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'],
|
||||||
|
includeAnthropicHelper: false,
|
||||||
|
contextLabel: 'Team launch',
|
||||||
|
})
|
||||||
|
).rejects.toThrow('mixed-provider launch cannot combine app-managed inherited settings');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects inherited path-based settings alongside inherited mixed-provider JSON settings', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
(svc as any).buildTeamRuntimeLaunchArgsPlan({
|
||||||
|
teamName: 'anthropic-codex-inherited-path-settings-team',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchIdentity: null,
|
||||||
|
envResolution: { providerArgs: [] },
|
||||||
|
extraArgs: [],
|
||||||
|
inheritedProviderArgs: [
|
||||||
|
'--settings',
|
||||||
|
'{"codex":{"forced_login_method":"chatgpt"}}',
|
||||||
|
'--settings',
|
||||||
|
'/tmp/inherited-custom-settings.json',
|
||||||
|
],
|
||||||
|
includeAnthropicHelper: false,
|
||||||
|
contextLabel: 'Team launch',
|
||||||
|
})
|
||||||
|
).rejects.toThrow('mixed-provider launch cannot combine app-managed inherited settings');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects dangling path-based settings when inherited mixed-provider settings must be coalesced', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
(svc as any).buildTeamRuntimeLaunchArgsPlan({
|
||||||
|
teamName: 'anthropic-codex-dangling-settings-team',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchIdentity: null,
|
||||||
|
envResolution: { providerArgs: [] },
|
||||||
|
extraArgs: ['--settings'],
|
||||||
|
inheritedProviderArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'],
|
||||||
|
includeAnthropicHelper: false,
|
||||||
|
contextLabel: 'Team launch',
|
||||||
|
})
|
||||||
|
).rejects.toThrow('mixed-provider launch cannot combine app-managed inherited settings');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects equals-style path settings when inherited mixed-provider settings must be coalesced', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
(svc as any).buildTeamRuntimeLaunchArgsPlan({
|
||||||
|
teamName: 'anthropic-codex-equals-path-settings-team',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchIdentity: null,
|
||||||
|
envResolution: { providerArgs: ['--settings=/tmp/provider-settings.json'] },
|
||||||
|
extraArgs: [],
|
||||||
|
inheritedProviderArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'],
|
||||||
|
includeAnthropicHelper: false,
|
||||||
|
contextLabel: 'Team launch',
|
||||||
|
})
|
||||||
|
).rejects.toThrow('mixed-provider launch cannot combine app-managed inherited settings');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects inherited path-based settings when Anthropic helper settings are app-managed', async () => {
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
(svc as any).buildTeamRuntimeLaunchArgsPlan({
|
||||||
|
teamName: 'anthropic-helper-inherited-path-settings-team',
|
||||||
|
providerId: 'anthropic',
|
||||||
|
launchIdentity: null,
|
||||||
|
envResolution: {
|
||||||
|
providerArgs: [],
|
||||||
|
anthropicApiKeyHelper: {
|
||||||
|
directory: '/tmp/anthropic-helper',
|
||||||
|
helperPath: '/tmp/anthropic-helper/helper.sh',
|
||||||
|
keyPath: '/tmp/anthropic-helper/key',
|
||||||
|
settingsPath: '/tmp/anthropic-helper/settings.json',
|
||||||
|
settingsObject: { apiKeyHelper: "'/tmp/anthropic-helper/helper.sh'" },
|
||||||
|
settingsArgs: ['--settings', '/tmp/anthropic-helper/settings.json'],
|
||||||
|
envPatch: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraArgs: [],
|
||||||
|
inheritedProviderArgs: ['--settings', '/tmp/custom-settings.json'],
|
||||||
|
includeAnthropicHelper: true,
|
||||||
|
contextLabel: 'Team launch',
|
||||||
|
})
|
||||||
|
).rejects.toThrow('app-managed Anthropic API-key helper cannot be combined');
|
||||||
|
});
|
||||||
|
|
||||||
it('adds Codex turn-settled env when Codex is only a secondary member provider', async () => {
|
it('adds Codex turn-settled env when Codex is only a secondary member provider', async () => {
|
||||||
const svc = new TeamProvisioningService();
|
const svc = new TeamProvisioningService();
|
||||||
svc.setRuntimeTurnSettledEnvironmentProvider(async ({ provider }) =>
|
svc.setRuntimeTurnSettledEnvironmentProvider(async ({ provider }) =>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
|
import { AGENT_BLOCK_CLOSE, AGENT_BLOCK_OPEN } from '@shared/constants/agentBlocks';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { AGENT_BLOCK_CLOSE, AGENT_BLOCK_OPEN } from '@shared/constants/agentBlocks';
|
|
||||||
|
|
||||||
const hoisted = vi.hoisted(() => ({
|
const hoisted = vi.hoisted(() => ({
|
||||||
paths: {
|
paths: {
|
||||||
claudeRoot: '',
|
claudeRoot: '',
|
||||||
|
|
@ -109,12 +107,13 @@ vi.mock('@main/utils/pathDecoder', async (importOriginal) => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver';
|
||||||
|
import { TeamRuntimeAdapterRegistry } from '@main/services/team/runtime/TeamRuntimeAdapter';
|
||||||
import {
|
import {
|
||||||
buildAddMemberSpawnMessage,
|
buildAddMemberSpawnMessage,
|
||||||
buildRestartMemberSpawnMessage,
|
buildRestartMemberSpawnMessage,
|
||||||
TeamProvisioningService,
|
TeamProvisioningService,
|
||||||
} from '@main/services/team/TeamProvisioningService';
|
} from '@main/services/team/TeamProvisioningService';
|
||||||
import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver';
|
|
||||||
import { execCli, spawnCli } from '@main/utils/childProcess';
|
import { execCli, spawnCli } from '@main/utils/childProcess';
|
||||||
import { setAppDataBasePath } from '@main/utils/pathDecoder';
|
import { setAppDataBasePath } from '@main/utils/pathDecoder';
|
||||||
|
|
||||||
|
|
@ -166,6 +165,41 @@ function extractBootstrapSpec(callIndex = 0): {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readRuntimeSettingsFromLaunchArgs(callIndex = 0): Record<string, unknown> {
|
||||||
|
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) {
|
||||||
|
throw new Error('Failed to extract runtime settings from spawn args');
|
||||||
|
}
|
||||||
|
if (settingsValue.trim().startsWith('{')) {
|
||||||
|
return JSON.parse(settingsValue) as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
return JSON.parse(fs.readFileSync(settingsValue, 'utf8')) as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerNoopOpenCodeRuntimeAdapter(svc: TeamProvisioningService): void {
|
||||||
|
svc.setRuntimeAdapterRegistry(
|
||||||
|
new TeamRuntimeAdapterRegistry([
|
||||||
|
{
|
||||||
|
providerId: 'opencode',
|
||||||
|
prepare: vi.fn(async (input: { model?: string }) => ({
|
||||||
|
ok: true,
|
||||||
|
providerId: 'opencode',
|
||||||
|
modelId: input.model ?? null,
|
||||||
|
diagnostics: [],
|
||||||
|
warnings: [],
|
||||||
|
})),
|
||||||
|
launch: vi.fn(async () => {
|
||||||
|
throw new Error('OpenCode side lane launch should not run in this test');
|
||||||
|
}),
|
||||||
|
reconcile: vi.fn(async () => ({ members: {}, warnings: [], diagnostics: [] })),
|
||||||
|
stop: vi.fn(async () => ({ stopped: true, members: {}, warnings: [], diagnostics: [] })),
|
||||||
|
} as any,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
describe('TeamProvisioningService prompt content (solo mode discipline)', () => {
|
describe('TeamProvisioningService prompt content (solo mode discipline)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
@ -477,6 +511,65 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () =>
|
||||||
await svc.cancelProvisioning(runId);
|
await svc.cancelProvisioning(runId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('coalesces codex cross-provider launch overrides into createTeam Anthropic runtime settings', async () => {
|
||||||
|
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/fake/claude');
|
||||||
|
const { child } = createFakeChild();
|
||||||
|
vi.mocked(spawnCli).mockReturnValue(child as any);
|
||||||
|
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
registerNoopOpenCodeRuntimeAdapter(svc);
|
||||||
|
(svc as any).buildProvisioningEnv = vi.fn(async (providerId: string | undefined) =>
|
||||||
|
providerId === 'codex'
|
||||||
|
? {
|
||||||
|
env: {
|
||||||
|
CLAUDE_CODE_CODEX_BACKEND: 'codex-native',
|
||||||
|
CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD: 'chatgpt',
|
||||||
|
},
|
||||||
|
authSource: 'codex_runtime',
|
||||||
|
geminiRuntimeAuth: null,
|
||||||
|
providerArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'],
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
env: {},
|
||||||
|
authSource: 'none',
|
||||||
|
geminiRuntimeAuth: null,
|
||||||
|
providerArgs: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
(svc as any).validateAgentTeamsMcpRuntime = vi.fn(async () => {});
|
||||||
|
(svc as any).startFilesystemMonitor = vi.fn();
|
||||||
|
(svc as any).pathExists = vi.fn(async () => false);
|
||||||
|
|
||||||
|
const { runId } = await svc.createTeam(
|
||||||
|
{
|
||||||
|
teamName: 'anthropic-codex-create-team',
|
||||||
|
cwd: process.cwd(),
|
||||||
|
members: [
|
||||||
|
{ name: 'alice', role: 'developer', providerId: 'codex' },
|
||||||
|
{
|
||||||
|
name: 'bob',
|
||||||
|
role: 'reviewer',
|
||||||
|
providerId: 'opencode',
|
||||||
|
model: 'minimax-m2.5-free',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
providerId: 'anthropic',
|
||||||
|
},
|
||||||
|
() => {}
|
||||||
|
);
|
||||||
|
|
||||||
|
const launchArgs = vi.mocked(spawnCli).mock.calls[0]?.[1] as string[];
|
||||||
|
expect(launchArgs).not.toContain('{"codex":{"forced_login_method":"chatgpt"}}');
|
||||||
|
expect(launchArgs.join(' ')).not.toContain('minimax-m2.5-free');
|
||||||
|
expect(extractBootstrapSpec().members).toEqual([
|
||||||
|
expect.objectContaining({ name: 'alice', provider: 'codex' }),
|
||||||
|
]);
|
||||||
|
const settings = readRuntimeSettingsFromLaunchArgs();
|
||||||
|
expect((settings.codex as Record<string, unknown>).forced_login_method).toBe('chatgpt');
|
||||||
|
|
||||||
|
await svc.cancelProvisioning(runId);
|
||||||
|
});
|
||||||
|
|
||||||
it('blocks Codex xhigh launch effort until runtime exposes reasoning config passthrough', async () => {
|
it('blocks Codex xhigh launch effort until runtime exposes reasoning config passthrough', async () => {
|
||||||
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/fake/codex');
|
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/fake/codex');
|
||||||
vi.mocked(spawnCli).mockReset();
|
vi.mocked(spawnCli).mockReset();
|
||||||
|
|
@ -1065,4 +1158,111 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () =>
|
||||||
|
|
||||||
await svc.cancelProvisioning(runId);
|
await svc.cancelProvisioning(runId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('coalesces codex cross-provider launch overrides into launchTeam Anthropic runtime settings', async () => {
|
||||||
|
const teamName = 'anthropic-codex-launch-team';
|
||||||
|
const teamDir = path.join(tempTeamsBase, teamName);
|
||||||
|
fs.mkdirSync(teamDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(teamDir, 'config.json'),
|
||||||
|
JSON.stringify({
|
||||||
|
name: teamName,
|
||||||
|
members: [
|
||||||
|
{ name: 'team-lead', agentType: 'team-lead', providerId: 'anthropic' },
|
||||||
|
{
|
||||||
|
name: 'alice',
|
||||||
|
agentType: 'teammate',
|
||||||
|
role: 'developer',
|
||||||
|
providerId: 'codex',
|
||||||
|
model: 'gpt-5.4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bob',
|
||||||
|
agentType: 'teammate',
|
||||||
|
role: 'reviewer',
|
||||||
|
providerId: 'opencode',
|
||||||
|
model: 'minimax-m2.5-free',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/fake/claude');
|
||||||
|
const { child } = createFakeChild();
|
||||||
|
vi.mocked(spawnCli).mockReturnValue(child as any);
|
||||||
|
|
||||||
|
const svc = new TeamProvisioningService();
|
||||||
|
registerNoopOpenCodeRuntimeAdapter(svc);
|
||||||
|
(svc as any).buildProvisioningEnv = vi.fn(async (providerId: string | undefined) =>
|
||||||
|
providerId === 'codex'
|
||||||
|
? {
|
||||||
|
env: {
|
||||||
|
CLAUDE_CODE_CODEX_BACKEND: 'codex-native',
|
||||||
|
CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD: 'chatgpt',
|
||||||
|
},
|
||||||
|
authSource: 'codex_runtime',
|
||||||
|
geminiRuntimeAuth: null,
|
||||||
|
providerArgs: ['--settings', '{"codex":{"forced_login_method":"chatgpt"}}'],
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
env: {},
|
||||||
|
authSource: 'none',
|
||||||
|
geminiRuntimeAuth: null,
|
||||||
|
providerArgs: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
(svc as any).resolveProviderDefaultModel = vi.fn(async (providerId: string | undefined) =>
|
||||||
|
providerId === 'codex' ? 'gpt-5.4' : 'opus'
|
||||||
|
);
|
||||||
|
(svc as any).normalizeTeamConfigForLaunch = vi.fn(async () => {});
|
||||||
|
(svc as any).updateConfigProjectPath = vi.fn(async () => {});
|
||||||
|
(svc as any).restorePrelaunchConfig = vi.fn(async () => {});
|
||||||
|
(svc as any).assertConfigLeadOnlyForLaunch = vi.fn(async () => {});
|
||||||
|
(svc as any).persistLaunchStateSnapshot = vi.fn(async () => {});
|
||||||
|
(svc as any).resolveLaunchExpectedMembers = vi.fn(async () => ({
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
name: 'alice',
|
||||||
|
role: 'developer',
|
||||||
|
providerId: 'codex',
|
||||||
|
model: 'gpt-5.4',
|
||||||
|
isolation: 'worktree',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bob',
|
||||||
|
role: 'reviewer',
|
||||||
|
providerId: 'opencode',
|
||||||
|
model: 'minimax-m2.5-free',
|
||||||
|
isolation: 'worktree',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
source: 'config-fallback',
|
||||||
|
warning: undefined,
|
||||||
|
}));
|
||||||
|
(svc as any).validateAgentTeamsMcpRuntime = vi.fn(async () => {});
|
||||||
|
(svc as any).pathExists = vi.fn(async () => false);
|
||||||
|
(svc as any).startFilesystemMonitor = vi.fn();
|
||||||
|
|
||||||
|
const { runId } = await svc.launchTeam(
|
||||||
|
{
|
||||||
|
teamName,
|
||||||
|
cwd: process.cwd(),
|
||||||
|
providerId: 'anthropic',
|
||||||
|
clearContext: true,
|
||||||
|
} as any,
|
||||||
|
() => {}
|
||||||
|
);
|
||||||
|
|
||||||
|
const launchArgs = vi.mocked(spawnCli).mock.calls[0]?.[1] as string[];
|
||||||
|
expect(launchArgs).not.toContain('{"codex":{"forced_login_method":"chatgpt"}}');
|
||||||
|
expect(launchArgs.join(' ')).not.toContain('minimax-m2.5-free');
|
||||||
|
expect(extractBootstrapSpec().members).toEqual([
|
||||||
|
expect.objectContaining({ name: 'alice', provider: 'codex' }),
|
||||||
|
]);
|
||||||
|
const settings = readRuntimeSettingsFromLaunchArgs();
|
||||||
|
expect((settings.codex as Record<string, unknown>).forced_login_method).toBe('chatgpt');
|
||||||
|
|
||||||
|
await svc.cancelProvisioning(runId);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue