115 lines
3.6 KiB
TypeScript
115 lines
3.6 KiB
TypeScript
/**
|
|
* Per-group container config, stored as a plain JSON file at
|
|
* `groups/<folder>/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<string, string>;
|
|
}
|
|
|
|
export interface AdditionalMountConfig {
|
|
hostPath: string;
|
|
containerPath: string;
|
|
readonly?: boolean;
|
|
}
|
|
|
|
export interface ContainerConfig {
|
|
mcpServers: Record<string, McpServerConfig>;
|
|
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<ContainerConfig>;
|
|
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/<folder>/
|
|
* 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;
|
|
}
|