/** * Per-group container config, stored as a plain JSON file at * `groups//container.json`. Replaces the former * `agent_groups.container_config` DB column. * * Shape: * { * mcpServers: { [name]: { command, args, env } } * packages: { apt: string[], npm: string[] } * imageTag?: string // set by buildAgentGroupImage on rebuild * additionalMounts?: Array<{hostPath, containerPath, readonly}> * } * * All fields are optional — a missing file or a partial file both resolve * to sensible defaults. Writes are atomic-enough (write-then-rename is not * worth the ceremony here since there's only one writer in practice: the * host, from the delivery thread that processes approved system actions). */ import fs from 'fs'; import path from 'path'; import { GROUPS_DIR } from './config.js'; export interface McpServerConfig { command: string; args?: string[]; env?: Record; } export interface AdditionalMountConfig { hostPath: string; containerPath: string; readonly?: boolean; } export interface ContainerConfig { mcpServers: Record; packages: { apt: string[]; npm: string[] }; imageTag?: string; additionalMounts: AdditionalMountConfig[]; } function emptyConfig(): ContainerConfig { return { mcpServers: {}, packages: { apt: [], npm: [] }, additionalMounts: [], }; } function configPath(folder: string): string { return path.join(GROUPS_DIR, folder, 'container.json'); } /** * Read the container config for a group, returning sensible defaults for * any missing fields (or an entirely empty config if the file is absent). * Never throws for missing / malformed files — corruption logs a warning * via console.error and falls back to empty. */ export function readContainerConfig(folder: string): ContainerConfig { const p = configPath(folder); if (!fs.existsSync(p)) return emptyConfig(); try { const raw = JSON.parse(fs.readFileSync(p, 'utf8')) as Partial; return { mcpServers: raw.mcpServers ?? {}, packages: { apt: raw.packages?.apt ?? [], npm: raw.packages?.npm ?? [], }, imageTag: raw.imageTag, additionalMounts: raw.additionalMounts ?? [], }; } catch (err) { console.error(`[container-config] failed to parse ${p}: ${String(err)}`); return emptyConfig(); } } /** * Write the container config for a group, creating the groups// * directory if necessary. Pretty-printed JSON so diffs in the activation * flow are reviewable. */ export function writeContainerConfig(folder: string, config: ContainerConfig): void { const p = configPath(folder); const dir = path.dirname(p); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(p, JSON.stringify(config, null, 2) + '\n'); } /** * Apply a mutator function to a group's container config and persist the * result. Convenient for append-style changes like `install_packages` and * `add_mcp_server` handlers. */ export function updateContainerConfig(folder: string, mutate: (config: ContainerConfig) => void): ContainerConfig { const config = readContainerConfig(folder); mutate(config); writeContainerConfig(folder, config); return config; } /** * Initialize an empty container.json for a group if one doesn't already * exist. Idempotent — used from `group-init.ts`. */ export function initContainerConfig(folder: string): boolean { const p = configPath(folder); if (fs.existsSync(p)) return false; writeContainerConfig(folder, emptyConfig()); return true; }