fix(team): harden packaged mcp runtime
This commit is contained in:
parent
71bfab1758
commit
587c974c63
13 changed files with 271 additions and 51 deletions
|
|
@ -392,6 +392,10 @@ async function createOpenCodeRuntimeAdapterRegistry(
|
||||||
bridgeEnv.AGENT_TEAMS_MCP_CLAUDE_DIR = getClaudeBasePath();
|
bridgeEnv.AGENT_TEAMS_MCP_CLAUDE_DIR = getClaudeBasePath();
|
||||||
const useHttpMcpBridge = isOpenCodeMcpHttpBridgeEnabled(bridgeEnv);
|
const useHttpMcpBridge = isOpenCodeMcpHttpBridgeEnabled(bridgeEnv);
|
||||||
const explicitLocalMcpLaunchEnv = snapshotOpenCodeLocalMcpLaunchEnv(bridgeEnv);
|
const explicitLocalMcpLaunchEnv = snapshotOpenCodeLocalMcpLaunchEnv(bridgeEnv);
|
||||||
|
delete bridgeEnv.ELECTRON_RUN_AS_NODE;
|
||||||
|
if (explicitLocalMcpLaunchEnv) {
|
||||||
|
copyOpenCodeLocalMcpLaunchEnv(explicitLocalMcpLaunchEnv, bridgeEnv);
|
||||||
|
}
|
||||||
delete bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL;
|
delete bridgeEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL;
|
||||||
const applyMcpLaunchSpecEnv = async (
|
const applyMcpLaunchSpecEnv = async (
|
||||||
targetEnv: NodeJS.ProcessEnv,
|
targetEnv: NodeJS.ProcessEnv,
|
||||||
|
|
@ -408,12 +412,12 @@ async function createOpenCodeRuntimeAdapterRegistry(
|
||||||
});
|
});
|
||||||
const mcpEntry = mcpLaunchSpec.args[0];
|
const mcpEntry = mcpLaunchSpec.args[0];
|
||||||
if (mcpEntry) {
|
if (mcpEntry) {
|
||||||
for (const [key, value] of Object.entries(mcpLaunchSpec.env ?? {})) {
|
|
||||||
targetEnv[key] = value;
|
|
||||||
}
|
|
||||||
targetEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND = mcpLaunchSpec.command;
|
targetEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND = mcpLaunchSpec.command;
|
||||||
targetEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY = mcpEntry;
|
targetEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY = mcpEntry;
|
||||||
targetEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON = JSON.stringify(mcpLaunchSpec.args);
|
targetEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON = JSON.stringify(mcpLaunchSpec.args);
|
||||||
|
targetEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENV_JSON = JSON.stringify(
|
||||||
|
mcpLaunchSpec.env ?? {}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ const logger = createLogger('Runtime:AgentTeamsMcpLaunchEnv');
|
||||||
const MCP_COMMAND_ENV = 'CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND';
|
const MCP_COMMAND_ENV = 'CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND';
|
||||||
const MCP_ENTRY_ENV = 'CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY';
|
const MCP_ENTRY_ENV = 'CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY';
|
||||||
const MCP_ARGS_JSON_ENV = 'CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON';
|
const MCP_ARGS_JSON_ENV = 'CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON';
|
||||||
|
const MCP_ENV_JSON_ENV = 'CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENV_JSON';
|
||||||
|
const ELECTRON_RUN_AS_NODE_ENV = 'ELECTRON_RUN_AS_NODE';
|
||||||
|
|
||||||
export type AgentTeamsMcpLaunchEnv = Record<string, string | undefined>;
|
export type AgentTeamsMcpLaunchEnv = Record<string, string | undefined>;
|
||||||
|
|
||||||
|
|
@ -17,11 +19,24 @@ export function hasAgentTeamsMcpLocalLaunchEnv(env: AgentTeamsMcpLaunchEnv): boo
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureLegacyMcpChildEnvJson(env: AgentTeamsMcpLaunchEnv): void {
|
||||||
|
if (env[MCP_ENV_JSON_ENV]?.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const electronRunAsNode = env[ELECTRON_RUN_AS_NODE_ENV]?.trim();
|
||||||
|
if (electronRunAsNode) {
|
||||||
|
env[MCP_ENV_JSON_ENV] = JSON.stringify({
|
||||||
|
[ELECTRON_RUN_AS_NODE_ENV]: electronRunAsNode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function ensureAgentTeamsMcpLocalLaunchEnv(
|
export async function ensureAgentTeamsMcpLocalLaunchEnv(
|
||||||
env: AgentTeamsMcpLaunchEnv,
|
env: AgentTeamsMcpLaunchEnv,
|
||||||
resolveLaunchSpec: () => Promise<McpLaunchSpec> = resolveAgentTeamsMcpLaunchSpec
|
resolveLaunchSpec: () => Promise<McpLaunchSpec> = resolveAgentTeamsMcpLaunchSpec
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (hasAgentTeamsMcpLocalLaunchEnv(env)) {
|
if (hasAgentTeamsMcpLocalLaunchEnv(env)) {
|
||||||
|
ensureLegacyMcpChildEnvJson(env);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,12 +48,10 @@ export async function ensureAgentTeamsMcpLocalLaunchEnv(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(launchSpec.env ?? {})) {
|
|
||||||
env[key] = value;
|
|
||||||
}
|
|
||||||
env[MCP_COMMAND_ENV] = command;
|
env[MCP_COMMAND_ENV] = command;
|
||||||
env[MCP_ENTRY_ENV] = entry;
|
env[MCP_ENTRY_ENV] = entry;
|
||||||
env[MCP_ARGS_JSON_ENV] = JSON.stringify(launchSpec.args);
|
env[MCP_ARGS_JSON_ENV] = JSON.stringify(launchSpec.args);
|
||||||
|
env[MCP_ENV_JSON_ENV] = JSON.stringify(launchSpec.env ?? {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Unable to resolve Agent Teams MCP local launch env: ${
|
`Unable to resolve Agent Teams MCP local launch env: ${
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { providerConnectionService } from './ProviderConnectionService';
|
||||||
import type { CliProviderId, TeamProviderId } from '@shared/types';
|
import type { CliProviderId, TeamProviderId } from '@shared/types';
|
||||||
|
|
||||||
type ProviderEnvTargetId = CliProviderId | TeamProviderId | undefined;
|
type ProviderEnvTargetId = CliProviderId | TeamProviderId | undefined;
|
||||||
|
const ELECTRON_RUN_AS_NODE_ENV = 'ELECTRON_RUN_AS_NODE';
|
||||||
|
|
||||||
export interface ProviderAwareCliEnvOptions {
|
export interface ProviderAwareCliEnvOptions {
|
||||||
binaryPath?: string | null;
|
binaryPath?: string | null;
|
||||||
|
|
@ -29,6 +30,10 @@ export interface ProviderAwareCliEnvResult {
|
||||||
providerArgs: string[];
|
providerArgs: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeGlobalElectronRunAsNodeEnv(env: NodeJS.ProcessEnv): void {
|
||||||
|
delete env[ELECTRON_RUN_AS_NODE_ENV];
|
||||||
|
}
|
||||||
|
|
||||||
export async function buildProviderAwareCliEnv(
|
export async function buildProviderAwareCliEnv(
|
||||||
options: ProviderAwareCliEnvOptions = {}
|
options: ProviderAwareCliEnvOptions = {}
|
||||||
): Promise<ProviderAwareCliEnvResult> {
|
): Promise<ProviderAwareCliEnvResult> {
|
||||||
|
|
@ -79,6 +84,7 @@ export async function buildProviderAwareCliEnv(
|
||||||
options.providerBackendId,
|
options.providerBackendId,
|
||||||
...storedApiKeyAccessArgs
|
...storedApiKeyAccessArgs
|
||||||
);
|
);
|
||||||
|
removeGlobalElectronRunAsNodeEnv(env);
|
||||||
return {
|
return {
|
||||||
env,
|
env,
|
||||||
connectionIssues: {},
|
connectionIssues: {},
|
||||||
|
|
@ -92,22 +98,25 @@ export async function buildProviderAwareCliEnv(
|
||||||
options.providerBackendId,
|
options.providerBackendId,
|
||||||
...storedApiKeyAccessArgs
|
...storedApiKeyAccessArgs
|
||||||
);
|
);
|
||||||
|
removeGlobalElectronRunAsNodeEnv(env);
|
||||||
|
|
||||||
return {
|
const providerArgs = await providerConnectionService.getConfiguredConnectionLaunchArgs(
|
||||||
env,
|
|
||||||
providerArgs: await providerConnectionService.getConfiguredConnectionLaunchArgs(
|
|
||||||
env,
|
env,
|
||||||
resolvedProviderId,
|
resolvedProviderId,
|
||||||
options.providerBackendId,
|
options.providerBackendId,
|
||||||
options.binaryPath
|
options.binaryPath
|
||||||
),
|
);
|
||||||
connectionIssues: await providerConnectionService.getConfiguredConnectionIssues(
|
const connectionIssues = await providerConnectionService.getConfiguredConnectionIssues(
|
||||||
env,
|
env,
|
||||||
[resolvedProviderId],
|
[resolvedProviderId],
|
||||||
resolvedProviderId === 'codex' || resolvedProviderId === 'gemini'
|
resolvedProviderId === 'codex' || resolvedProviderId === 'gemini'
|
||||||
? { [resolvedProviderId]: options.providerBackendId?.trim() || undefined }
|
? { [resolvedProviderId]: options.providerBackendId?.trim() || undefined }
|
||||||
: undefined
|
: undefined
|
||||||
),
|
);
|
||||||
|
return {
|
||||||
|
env,
|
||||||
|
providerArgs,
|
||||||
|
connectionIssues,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,6 +125,7 @@ export async function buildProviderAwareCliEnv(
|
||||||
env,
|
env,
|
||||||
...storedApiKeyAccessArgs
|
...storedApiKeyAccessArgs
|
||||||
);
|
);
|
||||||
|
removeGlobalElectronRunAsNodeEnv(env);
|
||||||
return {
|
return {
|
||||||
env,
|
env,
|
||||||
connectionIssues: {},
|
connectionIssues: {},
|
||||||
|
|
@ -124,9 +134,11 @@ export async function buildProviderAwareCliEnv(
|
||||||
}
|
}
|
||||||
|
|
||||||
await providerConnectionService.applyAllConfiguredConnectionEnv(env, ...storedApiKeyAccessArgs);
|
await providerConnectionService.applyAllConfiguredConnectionEnv(env, ...storedApiKeyAccessArgs);
|
||||||
|
removeGlobalElectronRunAsNodeEnv(env);
|
||||||
|
const connectionIssues = await providerConnectionService.getConfiguredConnectionIssues(env);
|
||||||
return {
|
return {
|
||||||
env,
|
env,
|
||||||
connectionIssues: await providerConnectionService.getConfiguredConnectionIssues(env),
|
connectionIssues,
|
||||||
providerArgs: [],
|
providerArgs: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -405,7 +405,12 @@ function buildStatePath(): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLaunchSpecHash(launchSpec: McpLaunchSpec): string {
|
function buildLaunchSpecHash(launchSpec: McpLaunchSpec): string {
|
||||||
return sha256Hex(JSON.stringify({ command: launchSpec.command, args: launchSpec.args }));
|
const env = launchSpec.env
|
||||||
|
? Object.fromEntries(
|
||||||
|
Object.entries(launchSpec.env).sort(([left], [right]) => left.localeCompare(right))
|
||||||
|
)
|
||||||
|
: {};
|
||||||
|
return sha256Hex(JSON.stringify({ command: launchSpec.command, args: launchSpec.args, env }));
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildExpectedIdentity(
|
function buildExpectedIdentity(
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ const logger = createLogger('Service:TeamMcpConfigBuilder');
|
||||||
const MCP_CONFIG_PREFIX = 'agent-teams-mcp-';
|
const MCP_CONFIG_PREFIX = 'agent-teams-mcp-';
|
||||||
const MCP_CONFIG_REMOVE_RETRY_DELAYS_MS = [25, 75, 150] as const;
|
const MCP_CONFIG_REMOVE_RETRY_DELAYS_MS = [25, 75, 150] as const;
|
||||||
const NODE_RUNTIME_PROBE_TIMEOUT_MS = 5_000;
|
const NODE_RUNTIME_PROBE_TIMEOUT_MS = 5_000;
|
||||||
|
const ELECTRON_NODE_RUNTIME_PROBE_TIMEOUT_MS = 5_000;
|
||||||
/**
|
/**
|
||||||
* Stale configs older than this are removed on startup (best-effort).
|
* Stale configs older than this are removed on startup (best-effort).
|
||||||
* 7 days is intentionally long: respawnAfterAuthFailure() reuses saved
|
* 7 days is intentionally long: respawnAfterAuthFailure() reuses saved
|
||||||
|
|
@ -93,20 +94,21 @@ function getWorkspaceRoot(): string {
|
||||||
|
|
||||||
function shouldUsePackagedElectronNodeRuntime(): boolean {
|
function shouldUsePackagedElectronNodeRuntime(): boolean {
|
||||||
return (
|
return (
|
||||||
isPackagedApp() &&
|
isPackagedApp() && typeof process.execPath === 'string' && process.execPath.trim().length > 0
|
||||||
process.platform === 'linux' &&
|
|
||||||
typeof process.execPath === 'string' &&
|
|
||||||
process.execPath.trim().length > 0
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPackagedElectronNodeEnv(): Record<string, string> {
|
||||||
|
return {
|
||||||
|
[ELECTRON_RUN_AS_NODE_ENV]: '1',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildPackagedElectronNodeLaunchSpec(entry: string): McpLaunchSpec {
|
function buildPackagedElectronNodeLaunchSpec(entry: string): McpLaunchSpec {
|
||||||
return {
|
return {
|
||||||
command: process.execPath,
|
command: process.execPath.trim(),
|
||||||
args: [entry],
|
args: [entry],
|
||||||
env: {
|
env: getPackagedElectronNodeEnv(),
|
||||||
[ELECTRON_RUN_AS_NODE_ENV]: '1',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,9 +202,11 @@ async function hasValidServerCopy(dir: string): Promise<boolean> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let _resolvedNodePath: string | undefined;
|
let _resolvedNodePath: string | undefined;
|
||||||
|
let _packagedElectronNodeRuntimeProbe: { ok: true } | { ok: false; error: unknown } | undefined;
|
||||||
|
|
||||||
export function clearResolvedNodePathForTests(): void {
|
export function clearResolvedNodePathForTests(): void {
|
||||||
_resolvedNodePath = undefined;
|
_resolvedNodePath = undefined;
|
||||||
|
_packagedElectronNodeRuntimeProbe = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitProgress(
|
function emitProgress(
|
||||||
|
|
@ -323,6 +327,37 @@ async function probeNodeRuntimePath(
|
||||||
return { ok: false, error: lastError ?? 'no Node.js candidates were available' };
|
return { ok: false, error: lastError ?? 'no Node.js candidates were available' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function probePackagedElectronNodeRuntime(
|
||||||
|
options?: McpLaunchSpecResolveOptions
|
||||||
|
): Promise<{ ok: true } | { ok: false; error: unknown }> {
|
||||||
|
if (_packagedElectronNodeRuntimeProbe) {
|
||||||
|
return _packagedElectronNodeRuntimeProbe;
|
||||||
|
}
|
||||||
|
|
||||||
|
emitProgress(options, 'electron-node-runtime', 'Checking bundled Electron Node runtime...');
|
||||||
|
try {
|
||||||
|
const { stdout } = await execCli(
|
||||||
|
process.execPath.trim(),
|
||||||
|
['-e', 'process.stdout.write("agent-teams-electron-node-ok")'],
|
||||||
|
{
|
||||||
|
encoding: 'utf-8',
|
||||||
|
timeout: ELECTRON_NODE_RUNTIME_PROBE_TIMEOUT_MS,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
...getPackagedElectronNodeEnv(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (stdout.trim() !== 'agent-teams-electron-node-ok') {
|
||||||
|
throw new Error('Electron Node runtime probe did not return the expected marker');
|
||||||
|
}
|
||||||
|
_packagedElectronNodeRuntimeProbe = { ok: true };
|
||||||
|
} catch (error) {
|
||||||
|
_packagedElectronNodeRuntimeProbe = { ok: false, error };
|
||||||
|
}
|
||||||
|
return _packagedElectronNodeRuntimeProbe;
|
||||||
|
}
|
||||||
|
|
||||||
async function probeShellNodeRuntimePath(
|
async function probeShellNodeRuntimePath(
|
||||||
options?: McpLaunchSpecResolveOptions
|
options?: McpLaunchSpecResolveOptions
|
||||||
): Promise<{ ok: true; path: string } | { ok: false; error: unknown }> {
|
): Promise<{ ok: true; path: string } | { ok: false; error: unknown }> {
|
||||||
|
|
@ -473,6 +508,8 @@ export async function resolveAgentTeamsMcpLaunchSpec(
|
||||||
checked.push(packagedEntry);
|
checked.push(packagedEntry);
|
||||||
if (await pathExists(packagedEntry)) {
|
if (await pathExists(packagedEntry)) {
|
||||||
if (shouldUsePackagedElectronNodeRuntime()) {
|
if (shouldUsePackagedElectronNodeRuntime()) {
|
||||||
|
const electronProbe = await probePackagedElectronNodeRuntime(options);
|
||||||
|
if (electronProbe.ok) {
|
||||||
emitProgress(
|
emitProgress(
|
||||||
options,
|
options,
|
||||||
'electron-node-runtime-found',
|
'electron-node-runtime-found',
|
||||||
|
|
@ -480,6 +517,17 @@ export async function resolveAgentTeamsMcpLaunchSpec(
|
||||||
);
|
);
|
||||||
return buildPackagedElectronNodeLaunchSpec(packagedEntry);
|
return buildPackagedElectronNodeLaunchSpec(packagedEntry);
|
||||||
}
|
}
|
||||||
|
logger.warn(
|
||||||
|
`Bundled Electron Node runtime is unavailable for Agent Teams MCP; falling back to Node.js runtime: ${stringifyError(
|
||||||
|
electronProbe.error
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
emitProgress(
|
||||||
|
options,
|
||||||
|
'electron-node-runtime-fallback',
|
||||||
|
'Bundled Electron Node runtime unavailable, resolving Node.js fallback...'
|
||||||
|
);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
command: await resolveNodePath(options),
|
command: await resolveNodePath(options),
|
||||||
args: [packagedEntry],
|
args: [packagedEntry],
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ const LOCAL_MCP_LAUNCH_ENV_KEYS = [
|
||||||
'CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY',
|
'CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY',
|
||||||
'CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON',
|
'CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON',
|
||||||
] as const;
|
] as const;
|
||||||
|
const OPTIONAL_LOCAL_MCP_LAUNCH_ENV_KEYS = ['CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENV_JSON'] as const;
|
||||||
|
const LEGACY_LOCAL_MCP_CHILD_ENV_KEYS = ['ELECTRON_RUN_AS_NODE'] as const;
|
||||||
|
|
||||||
export type OpenCodeMcpBridgeEnv = Record<string, string | undefined>;
|
export type OpenCodeMcpBridgeEnv = Record<string, string | undefined>;
|
||||||
|
|
||||||
|
|
@ -17,6 +19,17 @@ export function hasOpenCodeLocalMcpLaunchEnv(env: OpenCodeMcpBridgeEnv): boolean
|
||||||
return LOCAL_MCP_LAUNCH_ENV_KEYS.every((key) => Boolean(env[key]?.trim()));
|
return LOCAL_MCP_LAUNCH_ENV_KEYS.every((key) => Boolean(env[key]?.trim()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildLegacyLocalMcpEnvJson(env: OpenCodeMcpBridgeEnv): string | null {
|
||||||
|
const legacyEnv: Record<string, string> = {};
|
||||||
|
for (const key of LEGACY_LOCAL_MCP_CHILD_ENV_KEYS) {
|
||||||
|
const value = env[key]?.trim();
|
||||||
|
if (value) {
|
||||||
|
legacyEnv[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.keys(legacyEnv).length > 0 ? JSON.stringify(legacyEnv) : null;
|
||||||
|
}
|
||||||
|
|
||||||
export function shouldEnsureOpenCodeLocalMcpLaunchEnv(input: {
|
export function shouldEnsureOpenCodeLocalMcpLaunchEnv(input: {
|
||||||
httpBridgeEnabled: boolean;
|
httpBridgeEnabled: boolean;
|
||||||
mcpUrl: string | undefined;
|
mcpUrl: string | undefined;
|
||||||
|
|
@ -36,6 +49,20 @@ export function copyOpenCodeLocalMcpLaunchEnv(
|
||||||
delete targetEnv[key];
|
delete targetEnv[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const key of OPTIONAL_LOCAL_MCP_LAUNCH_ENV_KEYS) {
|
||||||
|
const value = sourceEnv[key]?.trim();
|
||||||
|
if (value) {
|
||||||
|
targetEnv[key] = value;
|
||||||
|
} else {
|
||||||
|
delete targetEnv[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!targetEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENV_JSON?.trim()) {
|
||||||
|
const legacyEnvJson = buildLegacyLocalMcpEnvJson(sourceEnv);
|
||||||
|
if (legacyEnvJson) {
|
||||||
|
targetEnv.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENV_JSON = legacyEnvJson;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function snapshotOpenCodeLocalMcpLaunchEnv(
|
export function snapshotOpenCodeLocalMcpLaunchEnv(
|
||||||
|
|
@ -54,4 +81,10 @@ export function clearOpenCodeLocalMcpLaunchEnv(env: OpenCodeMcpBridgeEnv): void
|
||||||
for (const key of LOCAL_MCP_LAUNCH_ENV_KEYS) {
|
for (const key of LOCAL_MCP_LAUNCH_ENV_KEYS) {
|
||||||
delete env[key];
|
delete env[key];
|
||||||
}
|
}
|
||||||
|
for (const key of OPTIONAL_LOCAL_MCP_LAUNCH_ENV_KEYS) {
|
||||||
|
delete env[key];
|
||||||
|
}
|
||||||
|
for (const key of LEGACY_LOCAL_MCP_CHILD_ENV_KEYS) {
|
||||||
|
delete env[key];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -276,7 +276,7 @@ describe('buildProviderAwareCliEnv', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('propagates Agent Teams MCP launch env overrides for OpenCode provider commands', async () => {
|
it('serializes Agent Teams MCP launch env overrides for OpenCode provider commands', async () => {
|
||||||
resolveAgentTeamsMcpLaunchSpecMock.mockResolvedValue({
|
resolveAgentTeamsMcpLaunchSpecMock.mockResolvedValue({
|
||||||
command: '/opt/Agent Teams AI/agent-teams-ai',
|
command: '/opt/Agent Teams AI/agent-teams-ai',
|
||||||
args: ['/app/mcp-server/index.js'],
|
args: ['/app/mcp-server/index.js'],
|
||||||
|
|
@ -287,9 +287,13 @@ describe('buildProviderAwareCliEnv', () => {
|
||||||
|
|
||||||
const result = await buildProviderAwareCliEnv({
|
const result = await buildProviderAwareCliEnv({
|
||||||
providerId: 'opencode',
|
providerId: 'opencode',
|
||||||
|
env: { ELECTRON_RUN_AS_NODE: 'inherited-global-value' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.env.ELECTRON_RUN_AS_NODE).toBe('1');
|
expect(result.env.ELECTRON_RUN_AS_NODE).toBeUndefined();
|
||||||
|
expect(result.env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENV_JSON).toBe(
|
||||||
|
'{"ELECTRON_RUN_AS_NODE":"1"}'
|
||||||
|
);
|
||||||
expect(result.env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND).toBe(
|
expect(result.env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND).toBe(
|
||||||
'/opt/Agent Teams AI/agent-teams-ai'
|
'/opt/Agent Teams AI/agent-teams-ai'
|
||||||
);
|
);
|
||||||
|
|
@ -304,6 +308,7 @@ describe('buildProviderAwareCliEnv', () => {
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: 'custom-node',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: 'custom-node',
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: '/custom/mcp.js',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: '/custom/mcp.js',
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: '["/custom/mcp.js"]',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: '["/custom/mcp.js"]',
|
||||||
|
ELECTRON_RUN_AS_NODE: '1',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -311,6 +316,10 @@ describe('buildProviderAwareCliEnv', () => {
|
||||||
expect(result.env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND).toBe('custom-node');
|
expect(result.env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND).toBe('custom-node');
|
||||||
expect(result.env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY).toBe('/custom/mcp.js');
|
expect(result.env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY).toBe('/custom/mcp.js');
|
||||||
expect(result.env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON).toBe('["/custom/mcp.js"]');
|
expect(result.env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON).toBe('["/custom/mcp.js"]');
|
||||||
|
expect(result.env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENV_JSON).toBe(
|
||||||
|
'{"ELECTRON_RUN_AS_NODE":"1"}'
|
||||||
|
);
|
||||||
|
expect(result.env.ELECTRON_RUN_AS_NODE).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows OpenCode auto-update only behind an explicit app override', async () => {
|
it('allows OpenCode auto-update only behind an explicit app override', async () => {
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,21 @@ function sha256Hex(value: string): string {
|
||||||
return createHash('sha256').update(value).digest('hex');
|
return createHash('sha256').update(value).digest('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLaunchSpecHash(launchSpec: { command: string; args: string[] }): string {
|
type TestLaunchSpec = {
|
||||||
return sha256Hex(JSON.stringify({ command: launchSpec.command, args: launchSpec.args }));
|
command: string;
|
||||||
|
args: string[];
|
||||||
|
env?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildLaunchSpecHash(launchSpec: TestLaunchSpec): string {
|
||||||
|
const env = launchSpec.env
|
||||||
|
? Object.fromEntries(
|
||||||
|
Object.entries(launchSpec.env).sort(([left], [right]) => left.localeCompare(right))
|
||||||
|
)
|
||||||
|
: {};
|
||||||
|
return sha256Hex(
|
||||||
|
JSON.stringify({ command: launchSpec.command, args: launchSpec.args, env })
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createTempStatePath(): Promise<{ root: string; statePath: string }> {
|
async function createTempStatePath(): Promise<{ root: string; statePath: string }> {
|
||||||
|
|
@ -58,7 +71,7 @@ async function createTempStatePath(): Promise<{ root: string; statePath: string
|
||||||
|
|
||||||
function buildIdentity(input: {
|
function buildIdentity(input: {
|
||||||
port: number;
|
port: number;
|
||||||
launchSpec: { command: string; args: string[] };
|
launchSpec: TestLaunchSpec;
|
||||||
ownerInstanceId?: string;
|
ownerInstanceId?: string;
|
||||||
}) {
|
}) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -77,7 +90,7 @@ function buildIdentity(input: {
|
||||||
function buildState(input: {
|
function buildState(input: {
|
||||||
port: number;
|
port: number;
|
||||||
pid?: number | null;
|
pid?: number | null;
|
||||||
launchSpec: { command: string; args: string[] };
|
launchSpec: TestLaunchSpec;
|
||||||
ownerInstanceId?: string;
|
ownerInstanceId?: string;
|
||||||
}) {
|
}) {
|
||||||
const identity = buildIdentity(input);
|
const identity = buildIdentity(input);
|
||||||
|
|
@ -433,6 +446,45 @@ describe('AgentTeamsMcpHttpServer', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('ignores persistent state when the MCP child env changed', async () => {
|
||||||
|
const { root, statePath } = await createTempStatePath();
|
||||||
|
const previousLaunchSpec = { command: 'node', args: ['mcp-server/dist/index.js'] };
|
||||||
|
const currentLaunchSpec = {
|
||||||
|
command: 'node',
|
||||||
|
args: ['mcp-server/dist/index.js'],
|
||||||
|
env: { ELECTRON_RUN_AS_NODE: '1' },
|
||||||
|
};
|
||||||
|
await writeFile(
|
||||||
|
statePath,
|
||||||
|
`${JSON.stringify(
|
||||||
|
buildState({ port: 41029, pid: 51235, launchSpec: previousLaunchSpec }),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}\n`
|
||||||
|
);
|
||||||
|
const child = new FakeChildProcess();
|
||||||
|
const spawnProcess = vi.fn(() => child as unknown as ChildProcess);
|
||||||
|
const server = new AgentTeamsMcpHttpServer({
|
||||||
|
statePath,
|
||||||
|
disableOrphanCleanup: true,
|
||||||
|
resolveLaunchSpec: async () => currentLaunchSpec,
|
||||||
|
allocatePort: async () => 41030,
|
||||||
|
spawnProcess,
|
||||||
|
waitForPort: vi.fn(async () => undefined),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const handle = await server.ensureStarted();
|
||||||
|
|
||||||
|
expect(handle.port).toBe(41030);
|
||||||
|
expect(handle.diagnostics).toContain('opencode_app_mcp_state_ignored:identity_mismatch');
|
||||||
|
expect(spawnProcess).toHaveBeenCalledTimes(1);
|
||||||
|
} finally {
|
||||||
|
await rm(root, { recursive: true, force: true });
|
||||||
|
vi.mocked(console.warn).mockClear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('adopts a healthy matching MCP HTTP server on the configured stable port', async () => {
|
it('adopts a healthy matching MCP HTTP server on the configured stable port', async () => {
|
||||||
const { root, statePath } = await createTempStatePath();
|
const { root, statePath } = await createTempStatePath();
|
||||||
const launchSpec = { command: 'node', args: ['mcp-server/dist/index.js'] };
|
const launchSpec = { command: 'node', args: ['mcp-server/dist/index.js'] };
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
clearOpenCodeLocalMcpLaunchEnv,
|
clearOpenCodeLocalMcpLaunchEnv,
|
||||||
copyOpenCodeLocalMcpLaunchEnv,
|
copyOpenCodeLocalMcpLaunchEnv,
|
||||||
|
|
@ -8,6 +6,7 @@ import {
|
||||||
shouldEnsureOpenCodeLocalMcpLaunchEnv,
|
shouldEnsureOpenCodeLocalMcpLaunchEnv,
|
||||||
snapshotOpenCodeLocalMcpLaunchEnv,
|
snapshotOpenCodeLocalMcpLaunchEnv,
|
||||||
} from '@main/services/team/opencode/bridge/OpenCodeMcpBridgeEnv';
|
} from '@main/services/team/opencode/bridge/OpenCodeMcpBridgeEnv';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe('OpenCodeMcpBridgeEnv', () => {
|
describe('OpenCodeMcpBridgeEnv', () => {
|
||||||
it('uses the app-owned HTTP MCP bridge by default', () => {
|
it('uses the app-owned HTTP MCP bridge by default', () => {
|
||||||
|
|
@ -61,6 +60,7 @@ describe('OpenCodeMcpBridgeEnv', () => {
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: 'node',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: 'node',
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: 'mcp-server/dist/index.js',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: 'mcp-server/dist/index.js',
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: '["mcp-server/dist/index.js"]',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: '["mcp-server/dist/index.js"]',
|
||||||
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENV_JSON: '{"ELECTRON_RUN_AS_NODE":"1"}',
|
||||||
},
|
},
|
||||||
target
|
target
|
||||||
);
|
);
|
||||||
|
|
@ -68,6 +68,9 @@ describe('OpenCodeMcpBridgeEnv', () => {
|
||||||
expect(target.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND).toBe('node');
|
expect(target.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND).toBe('node');
|
||||||
expect(target.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY).toBe('mcp-server/dist/index.js');
|
expect(target.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY).toBe('mcp-server/dist/index.js');
|
||||||
expect(target.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON).toBe('["mcp-server/dist/index.js"]');
|
expect(target.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON).toBe('["mcp-server/dist/index.js"]');
|
||||||
|
expect(target.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENV_JSON).toBe(
|
||||||
|
'{"ELECTRON_RUN_AS_NODE":"1"}'
|
||||||
|
);
|
||||||
expect(target.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL).toBe('http://127.0.0.1:41001/mcp');
|
expect(target.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL).toBe('http://127.0.0.1:41001/mcp');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -101,6 +104,7 @@ describe('OpenCodeMcpBridgeEnv', () => {
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: ' node ',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: ' node ',
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: ' mcp-server/dist/index.js ',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: ' mcp-server/dist/index.js ',
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: ' ["mcp-server/dist/index.js"] ',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: ' ["mcp-server/dist/index.js"] ',
|
||||||
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENV_JSON: ' {"ELECTRON_RUN_AS_NODE":"1"} ',
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL: 'http://127.0.0.1:41001/mcp',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL: 'http://127.0.0.1:41001/mcp',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -111,9 +115,25 @@ describe('OpenCodeMcpBridgeEnv', () => {
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: 'node',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: 'node',
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: 'mcp-server/dist/index.js',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: 'mcp-server/dist/index.js',
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: '["mcp-server/dist/index.js"]',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: '["mcp-server/dist/index.js"]',
|
||||||
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENV_JSON: '{"ELECTRON_RUN_AS_NODE":"1"}',
|
||||||
});
|
});
|
||||||
expect(hasOpenCodeLocalMcpLaunchEnv(snapshot ?? {})).toBe(true);
|
expect(hasOpenCodeLocalMcpLaunchEnv(snapshot ?? {})).toBe(true);
|
||||||
expect(env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND).toBeUndefined();
|
expect(env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND).toBeUndefined();
|
||||||
|
expect(env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENV_JSON).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('migrates legacy MCP child env into the local MCP env JSON snapshot', () => {
|
||||||
|
const snapshot = snapshotOpenCodeLocalMcpLaunchEnv({
|
||||||
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: 'node',
|
||||||
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: 'mcp-server/dist/index.js',
|
||||||
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: '["mcp-server/dist/index.js"]',
|
||||||
|
ELECTRON_RUN_AS_NODE: '1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(snapshot?.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENV_JSON).toBe(
|
||||||
|
'{"ELECTRON_RUN_AS_NODE":"1"}'
|
||||||
|
);
|
||||||
|
expect(snapshot?.ELECTRON_RUN_AS_NODE).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removes local MCP launch env when explicitly requested', () => {
|
it('removes local MCP launch env when explicitly requested', () => {
|
||||||
|
|
@ -121,7 +141,9 @@ describe('OpenCodeMcpBridgeEnv', () => {
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: 'node',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: 'node',
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: 'mcp-server/dist/index.js',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: 'mcp-server/dist/index.js',
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: '["mcp-server/dist/index.js"]',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: '["mcp-server/dist/index.js"]',
|
||||||
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENV_JSON: '{"ELECTRON_RUN_AS_NODE":"1"}',
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL: 'http://127.0.0.1:41001/mcp',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL: 'http://127.0.0.1:41001/mcp',
|
||||||
|
ELECTRON_RUN_AS_NODE: '1',
|
||||||
};
|
};
|
||||||
|
|
||||||
clearOpenCodeLocalMcpLaunchEnv(env);
|
clearOpenCodeLocalMcpLaunchEnv(env);
|
||||||
|
|
@ -129,6 +151,8 @@ describe('OpenCodeMcpBridgeEnv', () => {
|
||||||
expect(env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND).toBeUndefined();
|
expect(env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND).toBeUndefined();
|
||||||
expect(env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY).toBeUndefined();
|
expect(env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY).toBeUndefined();
|
||||||
expect(env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON).toBeUndefined();
|
expect(env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON).toBeUndefined();
|
||||||
|
expect(env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENV_JSON).toBeUndefined();
|
||||||
|
expect(env.ELECTRON_RUN_AS_NODE).toBeUndefined();
|
||||||
expect(env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL).toBe('http://127.0.0.1:41001/mcp');
|
expect(env.CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL).toBe('http://127.0.0.1:41001/mcp');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -77,10 +77,10 @@ liveDescribe('OpenCode mixed recovery live e2e', () => {
|
||||||
PATH: withBunOnPath(process.env.PATH ?? ''),
|
PATH: withBunOnPath(process.env.PATH ?? ''),
|
||||||
XDG_DATA_HOME: path.join(tempDir, 'xdg-data-single'),
|
XDG_DATA_HOME: path.join(tempDir, 'xdg-data-single'),
|
||||||
AGENT_TEAMS_MCP_CLAUDE_DIR: tempClaudeRoot,
|
AGENT_TEAMS_MCP_CLAUDE_DIR: tempClaudeRoot,
|
||||||
...mcpLaunchSpec.env,
|
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: mcpLaunchSpec.command,
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: mcpLaunchSpec.command,
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: mcpLaunchSpec.args[0] ?? '',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: mcpLaunchSpec.args[0] ?? '',
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: JSON.stringify(mcpLaunchSpec.args),
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: JSON.stringify(mcpLaunchSpec.args),
|
||||||
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENV_JSON: JSON.stringify(mcpLaunchSpec.env ?? {}),
|
||||||
};
|
};
|
||||||
const bridgeClient = new OpenCodeBridgeCommandClient({
|
const bridgeClient = new OpenCodeBridgeCommandClient({
|
||||||
binaryPath: orchestratorCli,
|
binaryPath: orchestratorCli,
|
||||||
|
|
@ -190,10 +190,10 @@ liveDescribe('OpenCode mixed recovery live e2e', () => {
|
||||||
PATH: withBunOnPath(process.env.PATH ?? ''),
|
PATH: withBunOnPath(process.env.PATH ?? ''),
|
||||||
XDG_DATA_HOME: path.join(tempDir, 'xdg-data-multi'),
|
XDG_DATA_HOME: path.join(tempDir, 'xdg-data-multi'),
|
||||||
AGENT_TEAMS_MCP_CLAUDE_DIR: tempClaudeRoot,
|
AGENT_TEAMS_MCP_CLAUDE_DIR: tempClaudeRoot,
|
||||||
...mcpLaunchSpec.env,
|
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: mcpLaunchSpec.command,
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: mcpLaunchSpec.command,
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: mcpLaunchSpec.args[0] ?? '',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: mcpLaunchSpec.args[0] ?? '',
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: JSON.stringify(mcpLaunchSpec.args),
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: JSON.stringify(mcpLaunchSpec.args),
|
||||||
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENV_JSON: JSON.stringify(mcpLaunchSpec.env ?? {}),
|
||||||
};
|
};
|
||||||
const bridgeClient = new OpenCodeBridgeCommandClient({
|
const bridgeClient = new OpenCodeBridgeCommandClient({
|
||||||
binaryPath: orchestratorCli,
|
binaryPath: orchestratorCli,
|
||||||
|
|
|
||||||
|
|
@ -69,10 +69,10 @@ liveDescribe('OpenCode team provisioning live e2e', () => {
|
||||||
PATH: withBunOnPath(process.env.PATH ?? ''),
|
PATH: withBunOnPath(process.env.PATH ?? ''),
|
||||||
XDG_DATA_HOME: path.join(tempDir, 'xdg-data'),
|
XDG_DATA_HOME: path.join(tempDir, 'xdg-data'),
|
||||||
AGENT_TEAMS_MCP_CLAUDE_DIR: tempClaudeRoot,
|
AGENT_TEAMS_MCP_CLAUDE_DIR: tempClaudeRoot,
|
||||||
...mcpLaunchSpec.env,
|
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: mcpLaunchSpec.command,
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: mcpLaunchSpec.command,
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: mcpLaunchSpec.args[0] ?? '',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: mcpLaunchSpec.args[0] ?? '',
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: JSON.stringify(mcpLaunchSpec.args),
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: JSON.stringify(mcpLaunchSpec.args),
|
||||||
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENV_JSON: JSON.stringify(mcpLaunchSpec.env ?? {}),
|
||||||
};
|
};
|
||||||
const bridgeClient = new OpenCodeBridgeCommandClient({
|
const bridgeClient = new OpenCodeBridgeCommandClient({
|
||||||
binaryPath: orchestratorCli,
|
binaryPath: orchestratorCli,
|
||||||
|
|
@ -228,10 +228,10 @@ liveDescribe('OpenCode team provisioning live e2e', () => {
|
||||||
PATH: withBunOnPath(process.env.PATH ?? ''),
|
PATH: withBunOnPath(process.env.PATH ?? ''),
|
||||||
XDG_DATA_HOME: path.join(tempDir, 'xdg-data-default-model'),
|
XDG_DATA_HOME: path.join(tempDir, 'xdg-data-default-model'),
|
||||||
AGENT_TEAMS_MCP_CLAUDE_DIR: tempClaudeRoot,
|
AGENT_TEAMS_MCP_CLAUDE_DIR: tempClaudeRoot,
|
||||||
...mcpLaunchSpec.env,
|
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: mcpLaunchSpec.command,
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: mcpLaunchSpec.command,
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: mcpLaunchSpec.args[0] ?? '',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: mcpLaunchSpec.args[0] ?? '',
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: JSON.stringify(mcpLaunchSpec.args),
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: JSON.stringify(mcpLaunchSpec.args),
|
||||||
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENV_JSON: JSON.stringify(mcpLaunchSpec.env ?? {}),
|
||||||
};
|
};
|
||||||
const bridgeClient = new OpenCodeBridgeCommandClient({
|
const bridgeClient = new OpenCodeBridgeCommandClient({
|
||||||
binaryPath: orchestratorCli,
|
binaryPath: orchestratorCli,
|
||||||
|
|
|
||||||
|
|
@ -339,6 +339,9 @@ describe('TeamMcpConfigBuilder', () => {
|
||||||
HOME: '/Users/tester',
|
HOME: '/Users/tester',
|
||||||
};
|
};
|
||||||
hoisted.resolveInteractiveShellEnvMock.mockResolvedValue(hoisted.cachedShellEnv);
|
hoisted.resolveInteractiveShellEnvMock.mockResolvedValue(hoisted.cachedShellEnv);
|
||||||
|
hoisted.execCliMock.mockImplementationOnce(async () => {
|
||||||
|
throw new Error('Electron-as-Node unavailable');
|
||||||
|
});
|
||||||
hoisted.execCliMock.mockImplementationOnce(async (command, _args, options) => {
|
hoisted.execCliMock.mockImplementationOnce(async (command, _args, options) => {
|
||||||
expect(command).toBe('node');
|
expect(command).toBe('node');
|
||||||
const env = options?.env as NodeJS.ProcessEnv | undefined;
|
const env = options?.env as NodeJS.ProcessEnv | undefined;
|
||||||
|
|
@ -355,18 +358,27 @@ describe('TeamMcpConfigBuilder', () => {
|
||||||
expect(hoisted.resolveInteractiveShellEnvMock).not.toHaveBeenCalled();
|
expect(hoisted.resolveInteractiveShellEnvMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses the packaged Electron Node runtime for Linux packaged MCP launches', async () => {
|
it.each(['linux', 'darwin', 'win32'] as const)(
|
||||||
|
'uses the packaged Electron Node runtime for %s packaged MCP launches',
|
||||||
|
async (platform) => {
|
||||||
const platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
|
const platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
|
||||||
const execPathDescriptor = Object.getOwnPropertyDescriptor(process, 'execPath');
|
const execPathDescriptor = Object.getOwnPropertyDescriptor(process, 'execPath');
|
||||||
const electronBinary = '/opt/Agent Teams AI/agent-teams-ai';
|
const electronBinary =
|
||||||
|
platform === 'win32'
|
||||||
|
? 'C:\\Program Files\\Agent Teams AI\\agent-teams-ai.exe'
|
||||||
|
: '/opt/Agent Teams AI/agent-teams-ai';
|
||||||
setPackagedMode(true, '3.0.0');
|
setPackagedMode(true, '3.0.0');
|
||||||
const resourcesDir = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-resources-'));
|
const resourcesDir = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-resources-'));
|
||||||
createdDirs.push(resourcesDir);
|
createdDirs.push(resourcesDir);
|
||||||
createPackagedServerBundle(resourcesDir, '// packaged linux server');
|
createPackagedServerBundle(resourcesDir, '// packaged server');
|
||||||
setResourcesPath(resourcesDir);
|
setResourcesPath(resourcesDir);
|
||||||
|
hoisted.execCliMock.mockResolvedValue({
|
||||||
|
stdout: 'agent-teams-electron-node-ok',
|
||||||
|
stderr: '',
|
||||||
|
});
|
||||||
|
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: 'linux',
|
value: platform,
|
||||||
configurable: true,
|
configurable: true,
|
||||||
});
|
});
|
||||||
Object.defineProperty(process, 'execPath', {
|
Object.defineProperty(process, 'execPath', {
|
||||||
|
|
@ -391,7 +403,14 @@ describe('TeamMcpConfigBuilder', () => {
|
||||||
expect(server?.command).toBe(electronBinary);
|
expect(server?.command).toBe(electronBinary);
|
||||||
expect(server?.args).toEqual([expectedEntry]);
|
expect(server?.args).toEqual([expectedEntry]);
|
||||||
expect(server?.env?.ELECTRON_RUN_AS_NODE).toBe('1');
|
expect(server?.env?.ELECTRON_RUN_AS_NODE).toBe('1');
|
||||||
expect(hoisted.execCliMock).not.toHaveBeenCalled();
|
expect(hoisted.execCliMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(hoisted.execCliMock).toHaveBeenCalledWith(
|
||||||
|
electronBinary,
|
||||||
|
['-e', 'process.stdout.write("agent-teams-electron-node-ok")'],
|
||||||
|
expect.objectContaining({
|
||||||
|
env: expect.objectContaining({ ELECTRON_RUN_AS_NODE: '1' }),
|
||||||
|
})
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
if (platformDescriptor) {
|
if (platformDescriptor) {
|
||||||
Object.defineProperty(process, 'platform', platformDescriptor);
|
Object.defineProperty(process, 'platform', platformDescriptor);
|
||||||
|
|
@ -400,7 +419,8 @@ describe('TeamMcpConfigBuilder', () => {
|
||||||
Object.defineProperty(process, 'execPath', execPathDescriptor);
|
Object.defineProperty(process, 'execPath', execPathDescriptor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
it('falls back to strict shell env lookup when fast Node lookup cannot resolve Node', async () => {
|
it('falls back to strict shell env lookup when fast Node lookup cannot resolve Node', async () => {
|
||||||
mockBuiltWorkspaceEntryAvailable();
|
mockBuiltWorkspaceEntryAvailable();
|
||||||
|
|
|
||||||
|
|
@ -76,10 +76,10 @@ export async function createOpenCodeLiveHarness(input: {
|
||||||
PATH: withBunOnPath(process.env.PATH ?? ''),
|
PATH: withBunOnPath(process.env.PATH ?? ''),
|
||||||
AGENT_TEAMS_MCP_CLAUDE_DIR: getClaudeBasePath(),
|
AGENT_TEAMS_MCP_CLAUDE_DIR: getClaudeBasePath(),
|
||||||
CLAUDE_TEAM_CONTROL_URL: controlApi.baseUrl,
|
CLAUDE_TEAM_CONTROL_URL: controlApi.baseUrl,
|
||||||
...mcpLaunchSpec.env,
|
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: mcpLaunchSpec.command,
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_COMMAND: mcpLaunchSpec.command,
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: mcpLaunchSpec.args[0] ?? '',
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: mcpLaunchSpec.args[0] ?? '',
|
||||||
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: JSON.stringify(mcpLaunchSpec.args),
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: JSON.stringify(mcpLaunchSpec.args),
|
||||||
|
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENV_JSON: JSON.stringify(mcpLaunchSpec.env ?? {}),
|
||||||
};
|
};
|
||||||
const turnSettledEnv = await buildMemberWorkSyncRuntimeTurnSettledEnvironment({
|
const turnSettledEnv = await buildMemberWorkSyncRuntimeTurnSettledEnvironment({
|
||||||
teamsBasePath: getTeamsBasePath(),
|
teamsBasePath: getTeamsBasePath(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue