278 lines
6.8 KiB
TypeScript
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;
|
|
}
|