189 lines
6.4 KiB
TypeScript
189 lines
6.4 KiB
TypeScript
import { resolve } from "node:path";
|
|
import { loadConfig, type AppConfig } from "../config.js";
|
|
import { parseEnvFile, writeEnvFileUpdates } from "./env-store.js";
|
|
|
|
export type RuntimeNotificationSettings = {
|
|
webhookUrl: string;
|
|
minSeverity: "info" | "warning" | "critical";
|
|
alwaysNotifyTypes: string[];
|
|
};
|
|
|
|
export type SecurityPolicySettings = {
|
|
violationMode: "hard_abort" | "validation_fail";
|
|
allowedBinaries: string[];
|
|
commandTimeoutMs: number;
|
|
inheritedEnv: string[];
|
|
scrubbedEnv: string[];
|
|
};
|
|
|
|
export type LimitSettings = {
|
|
maxConcurrent: number;
|
|
maxSession: number;
|
|
maxRecursiveDepth: number;
|
|
topologyMaxDepth: number;
|
|
topologyMaxRetries: number;
|
|
relationshipMaxChildren: number;
|
|
mergeConflictMaxAttempts: number;
|
|
portBase: number;
|
|
portBlockSize: number;
|
|
portBlockCount: number;
|
|
portPrimaryOffset: number;
|
|
};
|
|
|
|
export type UiConfigSnapshot = {
|
|
envFilePath: string;
|
|
runtimeEvents: RuntimeNotificationSettings;
|
|
security: SecurityPolicySettings;
|
|
limits: LimitSettings;
|
|
paths: {
|
|
stateRoot: string;
|
|
projectContextPath: string;
|
|
runtimeEventLogPath: string;
|
|
claudeTraceLogPath: string;
|
|
securityAuditLogPath: string;
|
|
};
|
|
};
|
|
|
|
function toCsv(values: readonly string[]): string {
|
|
return values.join(",");
|
|
}
|
|
|
|
function sanitizeCsv(values: readonly string[]): string[] {
|
|
const output: string[] = [];
|
|
const seen = new Set<string>();
|
|
|
|
for (const value of values) {
|
|
const normalized = value.trim();
|
|
if (!normalized || seen.has(normalized)) {
|
|
continue;
|
|
}
|
|
seen.add(normalized);
|
|
output.push(normalized);
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
function toRuntimeEvents(config: Readonly<AppConfig>): RuntimeNotificationSettings {
|
|
return {
|
|
webhookUrl: config.runtimeEvents.discordWebhookUrl ?? "",
|
|
minSeverity: config.runtimeEvents.discordMinSeverity,
|
|
alwaysNotifyTypes: [...config.runtimeEvents.discordAlwaysNotifyTypes],
|
|
};
|
|
}
|
|
|
|
function toSecurity(config: Readonly<AppConfig>): SecurityPolicySettings {
|
|
return {
|
|
violationMode: config.security.violationHandling,
|
|
allowedBinaries: [...config.security.shellAllowedBinaries],
|
|
commandTimeoutMs: config.security.commandTimeoutMs,
|
|
inheritedEnv: [...config.security.inheritedEnvVars],
|
|
scrubbedEnv: [...config.security.scrubbedEnvVars],
|
|
};
|
|
}
|
|
|
|
function toLimits(config: Readonly<AppConfig>): LimitSettings {
|
|
return {
|
|
maxConcurrent: config.agentManager.maxConcurrentAgents,
|
|
maxSession: config.agentManager.maxSessionAgents,
|
|
maxRecursiveDepth: config.agentManager.maxRecursiveDepth,
|
|
topologyMaxDepth: config.orchestration.maxDepth,
|
|
topologyMaxRetries: config.orchestration.maxRetries,
|
|
relationshipMaxChildren: config.orchestration.maxChildren,
|
|
mergeConflictMaxAttempts: config.orchestration.mergeConflictMaxAttempts,
|
|
portBase: config.provisioning.portRange.basePort,
|
|
portBlockSize: config.provisioning.portRange.blockSize,
|
|
portBlockCount: config.provisioning.portRange.blockCount,
|
|
portPrimaryOffset: config.provisioning.portRange.primaryPortOffset,
|
|
};
|
|
}
|
|
|
|
function toSnapshot(config: Readonly<AppConfig>, envFilePath: string): UiConfigSnapshot {
|
|
return {
|
|
envFilePath,
|
|
runtimeEvents: toRuntimeEvents(config),
|
|
security: toSecurity(config),
|
|
limits: toLimits(config),
|
|
paths: {
|
|
stateRoot: config.orchestration.stateRoot,
|
|
projectContextPath: config.orchestration.projectContextPath,
|
|
runtimeEventLogPath: config.runtimeEvents.logPath,
|
|
claudeTraceLogPath: config.provider.claudeObservability.logPath,
|
|
securityAuditLogPath: config.security.auditLogPath,
|
|
},
|
|
};
|
|
}
|
|
|
|
function mergeEnv(fileValues: Record<string, string>): NodeJS.ProcessEnv {
|
|
return {
|
|
...process.env,
|
|
...fileValues,
|
|
};
|
|
}
|
|
|
|
export class UiConfigStore {
|
|
private readonly envFilePath: string;
|
|
|
|
constructor(input: { workspaceRoot: string; envFilePath?: string }) {
|
|
this.envFilePath = resolve(input.workspaceRoot, input.envFilePath ?? ".env");
|
|
}
|
|
|
|
getEnvFilePath(): string {
|
|
return this.envFilePath;
|
|
}
|
|
|
|
async readSnapshot(): Promise<UiConfigSnapshot> {
|
|
const parsed = await parseEnvFile(this.envFilePath);
|
|
const config = loadConfig(mergeEnv(parsed.values));
|
|
return toSnapshot(config, this.envFilePath);
|
|
}
|
|
|
|
async updateRuntimeEvents(input: RuntimeNotificationSettings): Promise<UiConfigSnapshot> {
|
|
const alwaysNotifyTypes = sanitizeCsv(input.alwaysNotifyTypes);
|
|
|
|
const updates: Record<string, string> = {
|
|
AGENT_RUNTIME_DISCORD_WEBHOOK_URL: input.webhookUrl.trim(),
|
|
AGENT_RUNTIME_DISCORD_MIN_SEVERITY: input.minSeverity,
|
|
AGENT_RUNTIME_DISCORD_ALWAYS_NOTIFY_TYPES: toCsv(alwaysNotifyTypes),
|
|
};
|
|
|
|
const parsed = await writeEnvFileUpdates(this.envFilePath, updates);
|
|
const config = loadConfig(mergeEnv(parsed.values));
|
|
return toSnapshot(config, this.envFilePath);
|
|
}
|
|
|
|
async updateSecurityPolicy(input: SecurityPolicySettings): Promise<UiConfigSnapshot> {
|
|
const updates: Record<string, string> = {
|
|
AGENT_SECURITY_VIOLATION_MODE: input.violationMode,
|
|
AGENT_SECURITY_ALLOWED_BINARIES: toCsv(sanitizeCsv(input.allowedBinaries)),
|
|
AGENT_SECURITY_COMMAND_TIMEOUT_MS: String(input.commandTimeoutMs),
|
|
AGENT_SECURITY_ENV_INHERIT: toCsv(sanitizeCsv(input.inheritedEnv)),
|
|
AGENT_SECURITY_ENV_SCRUB: toCsv(sanitizeCsv(input.scrubbedEnv)),
|
|
};
|
|
|
|
const parsed = await writeEnvFileUpdates(this.envFilePath, updates);
|
|
const config = loadConfig(mergeEnv(parsed.values));
|
|
return toSnapshot(config, this.envFilePath);
|
|
}
|
|
|
|
async updateLimits(input: LimitSettings): Promise<UiConfigSnapshot> {
|
|
const updates: Record<string, string> = {
|
|
AGENT_MAX_CONCURRENT: String(input.maxConcurrent),
|
|
AGENT_MAX_SESSION: String(input.maxSession),
|
|
AGENT_MAX_RECURSIVE_DEPTH: String(input.maxRecursiveDepth),
|
|
AGENT_TOPOLOGY_MAX_DEPTH: String(input.topologyMaxDepth),
|
|
AGENT_TOPOLOGY_MAX_RETRIES: String(input.topologyMaxRetries),
|
|
AGENT_RELATIONSHIP_MAX_CHILDREN: String(input.relationshipMaxChildren),
|
|
AGENT_MERGE_CONFLICT_MAX_ATTEMPTS: String(input.mergeConflictMaxAttempts),
|
|
AGENT_PORT_BASE: String(input.portBase),
|
|
AGENT_PORT_BLOCK_SIZE: String(input.portBlockSize),
|
|
AGENT_PORT_BLOCK_COUNT: String(input.portBlockCount),
|
|
AGENT_PORT_PRIMARY_OFFSET: String(input.portPrimaryOffset),
|
|
};
|
|
|
|
const parsed = await writeEnvFileUpdates(this.envFilePath, updates);
|
|
const config = loadConfig(mergeEnv(parsed.values));
|
|
return toSnapshot(config, this.envFilePath);
|
|
}
|
|
}
|