Files
ai_ops/src/config.ts

278 lines
6.8 KiB
TypeScript

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<T>(value: T): Readonly<T> {
if (value === null || typeof value !== "object") {
return value;
}
const record = value as Record<string, unknown>;
for (const nested of Object.values(record)) {
deepFreeze(nested);
}
return Object.freeze(value);
}
export function loadConfig(env: NodeJS.ProcessEnv = process.env): Readonly<AppConfig> {
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<AppConfig> | undefined;
export function getConfig(): Readonly<AppConfig> {
if (!configSingleton) {
configSingleton = loadConfig(process.env);
}
return configSingleton;
}
export function clearConfigCacheForTests(): void {
configSingleton = undefined;
}