import type { AgentManagerLimits } from "./agents/manager.js"; import type { BuiltInProvisioningConfig } from "./agents/provisioning.js"; export type ProviderRuntimeConfig = { codexApiKey?: string; openAiApiKey?: string; openAiBaseUrl?: string; codexSkipGitCheck: boolean; anthropicApiKey?: string; claudeModel?: string; claudeCodePath?: string; }; export type McpRuntimeConfig = { configPath: string; }; export type OrchestrationRuntimeConfig = { stateRoot: string; projectContextPath: string; maxDepth: number; maxRetries: number; maxChildren: number; }; export type DiscoveryRuntimeConfig = { fileRelativePath: string; }; export type AppConfig = { provider: ProviderRuntimeConfig; mcp: McpRuntimeConfig; agentManager: AgentManagerLimits; orchestration: OrchestrationRuntimeConfig; provisioning: BuiltInProvisioningConfig; discovery: DiscoveryRuntimeConfig; }; const DEFAULT_AGENT_MANAGER: AgentManagerLimits = { maxConcurrentAgents: 4, maxSessionAgents: 2, maxRecursiveDepth: 3, }; const DEFAULT_ORCHESTRATION: OrchestrationRuntimeConfig = { stateRoot: ".ai_ops/state", projectContextPath: ".ai_ops/project-context.json", maxDepth: 4, maxRetries: 2, maxChildren: 4, }; const DEFAULT_PROVISIONING: BuiltInProvisioningConfig = { gitWorktree: { rootDirectory: ".ai_ops/worktrees", baseRef: "HEAD", }, portRange: { basePort: 36000, blockSize: 32, blockCount: 512, primaryPortOffset: 0, lockDirectory: ".ai_ops/locks/ports", }, }; const DEFAULT_DISCOVERY: DiscoveryRuntimeConfig = { fileRelativePath: ".agent-context/resources.json", }; function readOptionalString( env: NodeJS.ProcessEnv, key: string, ): string | undefined { const value = env[key]?.trim(); if (!value) { return undefined; } return value; } function readStringWithFallback( env: NodeJS.ProcessEnv, key: string, fallback: string, ): string { return readOptionalString(env, key) ?? fallback; } function readIntegerWithBounds( env: NodeJS.ProcessEnv, key: string, fallback: number, bounds: { min: number; }, ): number { const raw = env[key]?.trim(); if (!raw) { return fallback; } const parsed = Number(raw); if (!Number.isInteger(parsed) || parsed < bounds.min) { throw new Error(`Environment variable ${key} must be an integer >= ${String(bounds.min)}.`); } return parsed; } function readBooleanWithFallback( env: NodeJS.ProcessEnv, key: string, fallback: boolean, ): boolean { const raw = env[key]?.trim(); if (!raw) { return fallback; } if (raw === "true") { return true; } if (raw === "false") { return false; } throw new Error(`Environment variable ${key} must be "true" or "false".`); } function deepFreeze(value: T): Readonly { if (value === null || typeof value !== "object") { return value; } const record = value as Record; for (const nested of Object.values(record)) { deepFreeze(nested); } return Object.freeze(value); } export function loadConfig(env: NodeJS.ProcessEnv = process.env): Readonly { const config: AppConfig = { provider: { codexApiKey: readOptionalString(env, "CODEX_API_KEY"), openAiApiKey: readOptionalString(env, "OPENAI_API_KEY"), openAiBaseUrl: readOptionalString(env, "OPENAI_BASE_URL"), codexSkipGitCheck: readBooleanWithFallback(env, "CODEX_SKIP_GIT_CHECK", true), anthropicApiKey: readOptionalString(env, "ANTHROPIC_API_KEY"), claudeModel: readOptionalString(env, "CLAUDE_MODEL"), claudeCodePath: readOptionalString(env, "CLAUDE_CODE_PATH"), }, mcp: { configPath: readStringWithFallback(env, "MCP_CONFIG_PATH", "./mcp.config.json"), }, agentManager: { maxConcurrentAgents: readIntegerWithBounds( env, "AGENT_MAX_CONCURRENT", DEFAULT_AGENT_MANAGER.maxConcurrentAgents, { min: 1 }, ), maxSessionAgents: readIntegerWithBounds( env, "AGENT_MAX_SESSION", DEFAULT_AGENT_MANAGER.maxSessionAgents, { min: 1 }, ), maxRecursiveDepth: readIntegerWithBounds( env, "AGENT_MAX_RECURSIVE_DEPTH", DEFAULT_AGENT_MANAGER.maxRecursiveDepth, { min: 1 }, ), }, orchestration: { stateRoot: readStringWithFallback( env, "AGENT_STATE_ROOT", DEFAULT_ORCHESTRATION.stateRoot, ), projectContextPath: readStringWithFallback( env, "AGENT_PROJECT_CONTEXT_PATH", DEFAULT_ORCHESTRATION.projectContextPath, ), maxDepth: readIntegerWithBounds( env, "AGENT_TOPOLOGY_MAX_DEPTH", DEFAULT_ORCHESTRATION.maxDepth, { min: 1 }, ), maxRetries: readIntegerWithBounds( env, "AGENT_TOPOLOGY_MAX_RETRIES", DEFAULT_ORCHESTRATION.maxRetries, { min: 0 }, ), maxChildren: readIntegerWithBounds( env, "AGENT_RELATIONSHIP_MAX_CHILDREN", DEFAULT_ORCHESTRATION.maxChildren, { min: 1 }, ), }, provisioning: { gitWorktree: { rootDirectory: readStringWithFallback( env, "AGENT_WORKTREE_ROOT", DEFAULT_PROVISIONING.gitWorktree.rootDirectory, ), baseRef: readStringWithFallback( env, "AGENT_WORKTREE_BASE_REF", DEFAULT_PROVISIONING.gitWorktree.baseRef, ), }, portRange: { basePort: readIntegerWithBounds( env, "AGENT_PORT_BASE", DEFAULT_PROVISIONING.portRange.basePort, { min: 1 }, ), blockSize: readIntegerWithBounds( env, "AGENT_PORT_BLOCK_SIZE", DEFAULT_PROVISIONING.portRange.blockSize, { min: 1 }, ), blockCount: readIntegerWithBounds( env, "AGENT_PORT_BLOCK_COUNT", DEFAULT_PROVISIONING.portRange.blockCount, { min: 1 }, ), primaryPortOffset: readIntegerWithBounds( env, "AGENT_PORT_PRIMARY_OFFSET", DEFAULT_PROVISIONING.portRange.primaryPortOffset, { min: 0 }, ), lockDirectory: readStringWithFallback( env, "AGENT_PORT_LOCK_DIR", DEFAULT_PROVISIONING.portRange.lockDirectory, ), }, }, discovery: { fileRelativePath: readStringWithFallback( env, "AGENT_DISCOVERY_FILE_RELATIVE_PATH", DEFAULT_DISCOVERY.fileRelativePath, ), }, }; return deepFreeze(config); } let configSingleton: Readonly | undefined; export function getConfig(): Readonly { if (!configSingleton) { configSingleton = loadConfig(process.env); } return configSingleton; } export function clearConfigCacheForTests(): void { configSingleton = undefined; }