feat(v2): builder-agent self-modification WIP + container-config as per-group file
Checkpoints the builder-agent dev-agent/worktree/swap flow (create_dev_agent, request_swap, classifier, deadman, promote) before pivoting to a unified draft-activate approach with OS-level RO enforcement. Lifts container_config out of the agent_groups row into groups/<folder>/container.json so install_packages, add_mcp_server, and rebuild flows can eventually route through the same draft path as source edits. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
117
src/container-config.ts
Normal file
117
src/container-config.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user