agent-ecosystem/src/main/services/team/provisioning/TeamProvisioningDirectRestart.ts

190 lines
5.4 KiB
TypeScript

import * as path from 'path';
import {
ANTHROPIC_HELPER_MODE_COMPETING_AUTH_ENV_KEYS,
CLAUDE_TEAM_ANTHROPIC_API_KEY_HELPER_SETTINGS_PATH_ENV,
CLAUDE_TEAM_ANTHROPIC_AUTH_MODE_API_KEY_HELPER,
CLAUDE_TEAM_ANTHROPIC_AUTH_MODE_ENV,
} from '../../runtime/anthropicTeamApiKeyHelper';
import type { TeamProviderId } from '@shared/types';
const DIRECT_TMUX_RESTART_ENV_KEYS = [
'CLAUDE_CONFIG_DIR',
'CLAUDE_TEAM_CONTROL_URL',
'CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST',
'CLAUDE_CODE_USE_OPENAI',
'CLAUDE_CODE_USE_BEDROCK',
'CLAUDE_CODE_USE_VERTEX',
'CLAUDE_CODE_USE_FOUNDRY',
'CLAUDE_CODE_USE_GEMINI',
'CLAUDE_CODE_ENTRY_PROVIDER',
'CLAUDE_CODE_GEMINI_BACKEND',
'CLAUDE_CODE_CODEX_BACKEND',
'CLAUDE_CODE_CODEX_FORCED_LOGIN_METHOD',
'CODEX_CLI_PATH',
'CODEX_HOME',
CLAUDE_TEAM_ANTHROPIC_AUTH_MODE_ENV,
CLAUDE_TEAM_ANTHROPIC_API_KEY_HELPER_SETTINGS_PATH_ENV,
'ANTHROPIC_BASE_URL',
'ANTHROPIC_AWS_WORKSPACE_ID',
'ANTHROPIC_AWS_API_KEY',
'ANTHROPIC_API_KEY',
'ANTHROPIC_AUTH_TOKEN',
'GEMINI_BASE_URL',
'GEMINI_API_VERSION',
'GEMINI_API_KEY',
'CODEX_API_KEY',
'OPENAI_API_KEY',
'GOOGLE_APPLICATION_CREDENTIALS',
'GOOGLE_CLOUD_PROJECT',
'GOOGLE_CLOUD_PROJECT_ID',
'GCLOUD_PROJECT',
'HTTPS_PROXY',
'https_proxy',
'HTTP_PROXY',
'http_proxy',
'NO_PROXY',
'no_proxy',
'SSL_CERT_FILE',
'NODE_EXTRA_CA_CERTS',
'REQUESTS_CA_BUNDLE',
'CURL_CA_BUNDLE',
] as const;
const DIRECT_TMUX_PROVIDER_SELECTION_ENV_KEYS = [
'CLAUDE_CODE_USE_OPENAI',
'CLAUDE_CODE_USE_BEDROCK',
'CLAUDE_CODE_USE_VERTEX',
'CLAUDE_CODE_USE_FOUNDRY',
'CLAUDE_CODE_USE_GEMINI',
'CLAUDE_CODE_ENTRY_PROVIDER',
] as const;
const INTERACTIVE_SHELL_COMMANDS = new Set([
'bash',
'zsh',
'sh',
'fish',
'nu',
'pwsh',
'powershell',
'cmd',
'cmd.exe',
]);
export function shellQuote(value: string): string {
if (value.length === 0) {
return "''";
}
return `'${value.replace(/'/g, `'\\''`)}'`;
}
export function isInteractiveShellCommand(command: string | undefined): boolean {
const normalized = command?.trim().toLowerCase();
if (!normalized) {
return false;
}
return INTERACTIVE_SHELL_COMMANDS.has(path.basename(normalized));
}
function getDirectRestartEntryProvider(providerId: TeamProviderId): string {
return providerId === 'codex' || providerId === 'gemini' ? providerId : 'anthropic';
}
export function isAnthropicCompatibleBaseUrl(baseUrl?: string | null): boolean {
const trimmed = baseUrl?.trim();
if (!trimmed) {
return false;
}
try {
const url = new URL(trimmed);
return (
(url.protocol === 'http:' || url.protocol === 'https:') &&
!url.username &&
!url.password &&
url.hostname !== 'api.anthropic.com' &&
url.hostname !== 'api-staging.anthropic.com'
);
} catch {
return false;
}
}
export function hasAnthropicCompatibleAuthTokenEnv(env: NodeJS.ProcessEnv): boolean {
return Boolean(
isAnthropicCompatibleBaseUrl(env.ANTHROPIC_BASE_URL) && env.ANTHROPIC_AUTH_TOKEN?.trim()
);
}
export function buildDirectTmuxRestartEnvAssignments(
env: NodeJS.ProcessEnv,
providerId: TeamProviderId
): string {
const assignments = new Map<string, string>();
assignments.set('CLAUDECODE', '1');
assignments.set('CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS', '1');
for (const key of DIRECT_TMUX_RESTART_ENV_KEYS) {
const value = env[key];
if (typeof value === 'string' && value.length > 0) {
assignments.set(key, value);
}
}
for (const key of DIRECT_TMUX_PROVIDER_SELECTION_ENV_KEYS) {
assignments.set(key, '');
}
assignments.set('CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST', '1');
assignments.set('CLAUDE_CODE_ENTRY_PROVIDER', getDirectRestartEntryProvider(providerId));
if (providerId === 'anthropic') {
if (hasAnthropicCompatibleAuthTokenEnv(env)) {
assignments.set('ANTHROPIC_BASE_URL', env.ANTHROPIC_BASE_URL?.trim() ?? '');
assignments.set('ANTHROPIC_AUTH_TOKEN', env.ANTHROPIC_AUTH_TOKEN?.trim() ?? '');
if (!env.ANTHROPIC_API_KEY?.trim()) {
assignments.set('ANTHROPIC_API_KEY', '');
}
} else if (!isAnthropicCompatibleBaseUrl(env.ANTHROPIC_BASE_URL)) {
assignments.set('ANTHROPIC_AUTH_TOKEN', '');
}
}
if (
providerId === 'anthropic' &&
env[CLAUDE_TEAM_ANTHROPIC_AUTH_MODE_ENV] === CLAUDE_TEAM_ANTHROPIC_AUTH_MODE_API_KEY_HELPER
) {
assignments.set(
CLAUDE_TEAM_ANTHROPIC_AUTH_MODE_ENV,
CLAUDE_TEAM_ANTHROPIC_AUTH_MODE_API_KEY_HELPER
);
const settingsPath = env[CLAUDE_TEAM_ANTHROPIC_API_KEY_HELPER_SETTINGS_PATH_ENV];
if (typeof settingsPath === 'string') {
assignments.set(CLAUDE_TEAM_ANTHROPIC_API_KEY_HELPER_SETTINGS_PATH_ENV, settingsPath);
}
for (const key of ANTHROPIC_HELPER_MODE_COMPETING_AUTH_ENV_KEYS) {
assignments.set(key, '');
}
}
return [...assignments.entries()].map(([key, value]) => `${key}=${shellQuote(value)}`).join(' ');
}
export function buildDirectTmuxRestartCommand(input: {
cwd: string;
env: NodeJS.ProcessEnv;
providerId: TeamProviderId;
binaryPath: string;
args: string[];
}): string {
const envAssignments = buildDirectTmuxRestartEnvAssignments(input.env, input.providerId);
const command = [
'cd',
shellQuote(input.cwd),
'&&',
'env',
envAssignments,
shellQuote(input.binaryPath),
...input.args.map(shellQuote),
].join(' ');
return `(${command}); __claude_teammate_exit=$?; printf '\\n__CLAUDE_TEAMMATE_EXIT__:%s\\n' "$__claude_teammate_exit"`;
}