import { randomUUID } from 'crypto'; import fs from 'fs'; import path from 'path'; import { mergeJsonSettingsObjects, parseJsonSettingsObject } from './cliSettingsArgs'; import type { AnthropicTeamApiKeyHelperMaterial } from './anthropicTeamApiKeyHelper'; import type { TeamProviderId } from '@shared/types'; export type TeamRuntimeSettingsJson = Record; export interface TeamRuntimeSettingsBundle { settingsPath: string; settingsObject: TeamRuntimeSettingsJson; args: string[]; } export interface SplitSettingsJsonArgsResult { settingsFragments: TeamRuntimeSettingsJson[]; passthroughArgs: string[]; } export function splitSettingsJsonArgs(args: string[]): SplitSettingsJsonArgsResult { const settingsFragments: TeamRuntimeSettingsJson[] = []; const passthroughArgs: string[] = []; let index = 0; while (index < args.length) { const arg = args[index]; if (arg === '--settings') { const value = args[index + 1]; if (typeof value === 'string') { const parsed = parseJsonSettingsObject(value); if (parsed) { settingsFragments.push(parsed); index += 2; continue; } passthroughArgs.push(arg, value); index += 2; continue; } } const settingsPrefix = '--settings='; if (arg.startsWith(settingsPrefix)) { const value = arg.slice(settingsPrefix.length); const parsed = parseJsonSettingsObject(value); if (parsed) { settingsFragments.push(parsed); index += 1; continue; } } passthroughArgs.push(arg); index += 1; } return { settingsFragments, passthroughArgs }; } function sanitizeProviderId(providerId: TeamProviderId): string { return providerId.replace(/[^a-zA-Z0-9._-]+/g, '_') || 'provider'; } function stripCompetingAnthropicEnv(settings: TeamRuntimeSettingsJson): TeamRuntimeSettingsJson { const env = settings.env; if (!env || typeof env !== 'object' || Array.isArray(env)) { return settings; } const nextEnv = { ...(env as Record) }; delete nextEnv.ANTHROPIC_API_KEY; delete nextEnv.ANTHROPIC_AUTH_TOKEN; delete nextEnv.CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR; delete nextEnv.CLAUDE_CODE_OAUTH_TOKEN; delete nextEnv.CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR; return { ...settings, env: nextEnv }; } async function writeSettingsFile( filePath: string, settings: TeamRuntimeSettingsJson ): Promise { const dir = path.dirname(filePath); await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 }); if (process.platform !== 'win32') { await fs.promises.chmod(dir, 0o700).catch(() => undefined); } const existing = await fs.promises.lstat(filePath).catch((error: NodeJS.ErrnoException) => { if (error.code === 'ENOENT') { return null; } throw error; }); if (existing?.isSymbolicLink()) { throw new Error(`Refusing to replace symlinked team runtime settings file: ${filePath}`); } const tmpPath = path.join(dir, `.tmp.settings.${randomUUID()}`); try { await fs.promises.writeFile(tmpPath, `${JSON.stringify(settings, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, }); if (process.platform !== 'win32') { await fs.promises.chmod(tmpPath, 0o600).catch(() => undefined); } await fs.promises.rename(tmpPath, filePath); const written = await fs.promises.lstat(filePath); if (!written.isFile() || written.isSymbolicLink()) { throw new Error(`Unsafe team runtime settings file: ${filePath}`); } if (process.platform !== 'win32') { await fs.promises.chmod(filePath, 0o600).catch(() => undefined); } } catch (error) { await fs.promises.rm(tmpPath, { force: true }).catch(() => undefined); throw error; } } export async function materializeTeamRuntimeSettingsBundle(input: { teamName: string; providerId: TeamProviderId; baseSettings?: (TeamRuntimeSettingsJson | null | undefined)[]; anthropicHelper?: AnthropicTeamApiKeyHelperMaterial | null; settingsDirectory?: string | null; }): Promise { const fragments = [...(input.baseSettings ?? [])].filter( (fragment): fragment is TeamRuntimeSettingsJson => !!fragment && typeof fragment === 'object' && !Array.isArray(fragment) ); if (input.anthropicHelper) { fragments.push(input.anthropicHelper.settingsObject); } if (fragments.length === 0) { return null; } const settingsObject = stripCompetingAnthropicEnv( fragments.reduce( (merged, fragment) => mergeJsonSettingsObjects(merged, fragment), {} ) ); if (Object.keys(settingsObject).length === 0) { return null; } const baseDirectory = input.anthropicHelper?.directory ?? input.settingsDirectory; if (!baseDirectory) { return null; } const settingsPath = path.join( baseDirectory, `runtime-settings-${sanitizeProviderId(input.providerId)}.json` ); await writeSettingsFile(settingsPath, settingsObject); return { settingsPath, settingsObject, args: ['--settings', settingsPath], }; }