Refactor pipeline policies, MCP registry, and unified config/runtime
This commit is contained in:
277
src/config.ts
Normal file
277
src/config.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user