Source of truth for container runtime config moves from
groups/<folder>/container.json to a new container_configs table.
The file becomes a materialized view written at spawn time.
- New container_configs table with scalar columns (provider, model,
effort, image_tag, assistant_name, max_messages_per_prompt) and
JSON columns (mcp_servers, packages_apt, packages_npm, skills,
additional_mounts)
- Startup backfill seeds DB from existing container.json files
- materializeContainerJson() replaces readContainerConfig + ensureRuntimeFields
- Self-mod handlers (install_packages, add_mcp_server) write to DB
- Provider cascade simplified: session -> container_configs -> 'claude'
- ncl groups config-{get,update,add-mcp-server,remove-mcp-server,
add-package,remove-package} custom operations
- restartAgentGroupContainers() helper for config change propagation
- Container side unchanged (still reads /workspace/agent/container.json)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
90 lines
3.1 KiB
TypeScript
90 lines
3.1 KiB
TypeScript
/**
|
|
* Container config types and materialization.
|
|
*
|
|
* Source of truth is the `container_configs` table in the central DB.
|
|
* This module provides:
|
|
* - Type definitions for the file shape (read by the container runner)
|
|
* - `materializeContainerJson()` — writes `groups/<folder>/container.json`
|
|
* from the DB at spawn time
|
|
* - `configFromDb()` — builds a `ContainerConfig` from a DB row + agent group
|
|
*/
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import { GROUPS_DIR } from './config.js';
|
|
import { getContainerConfig } from './db/container-configs.js';
|
|
import { getAgentGroup } from './db/agent-groups.js';
|
|
import type { AgentGroup, ContainerConfigRow } from './types.js';
|
|
|
|
export interface McpServerConfig {
|
|
command: string;
|
|
args?: string[];
|
|
env?: Record<string, string>;
|
|
instructions?: string;
|
|
}
|
|
|
|
export interface AdditionalMountConfig {
|
|
hostPath: string;
|
|
containerPath: string;
|
|
readonly?: boolean;
|
|
}
|
|
|
|
/** Shape of the materialized `container.json` file read by the container runner. */
|
|
export interface ContainerConfig {
|
|
mcpServers: Record<string, McpServerConfig>;
|
|
packages: { apt: string[]; npm: string[] };
|
|
imageTag?: string;
|
|
additionalMounts: AdditionalMountConfig[];
|
|
skills: string[] | 'all';
|
|
provider?: string;
|
|
groupName?: string;
|
|
assistantName?: string;
|
|
agentGroupId?: string;
|
|
maxMessagesPerPrompt?: number;
|
|
model?: string;
|
|
effort?: string;
|
|
}
|
|
|
|
/** Build a `ContainerConfig` from a DB row + agent group identity. */
|
|
export function configFromDb(row: ContainerConfigRow, group: AgentGroup): ContainerConfig {
|
|
return {
|
|
mcpServers: JSON.parse(row.mcp_servers) as Record<string, McpServerConfig>,
|
|
packages: {
|
|
apt: JSON.parse(row.packages_apt) as string[],
|
|
npm: JSON.parse(row.packages_npm) as string[],
|
|
},
|
|
imageTag: row.image_tag ?? undefined,
|
|
additionalMounts: JSON.parse(row.additional_mounts) as AdditionalMountConfig[],
|
|
skills: JSON.parse(row.skills) as string[] | 'all',
|
|
provider: row.provider ?? undefined,
|
|
groupName: group.name,
|
|
assistantName: row.assistant_name ?? group.name,
|
|
agentGroupId: group.id,
|
|
maxMessagesPerPrompt: row.max_messages_per_prompt ?? undefined,
|
|
model: row.model ?? undefined,
|
|
effort: row.effort ?? undefined,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Materialize `container.json` from the DB. Called at spawn time so the
|
|
* container always sees fresh config. Returns the `ContainerConfig` for
|
|
* use by the caller (buildMounts, buildContainerArgs, etc.).
|
|
*/
|
|
export function materializeContainerJson(agentGroupId: string): ContainerConfig {
|
|
const group = getAgentGroup(agentGroupId);
|
|
if (!group) throw new Error(`Agent group not found: ${agentGroupId}`);
|
|
|
|
const row = getContainerConfig(agentGroupId);
|
|
if (!row) throw new Error(`Container config not found for agent group: ${agentGroupId}`);
|
|
|
|
const config = configFromDb(row, group);
|
|
|
|
const p = path.join(GROUPS_DIR, group.folder, 'container.json');
|
|
const dir = path.dirname(p);
|
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
fs.writeFileSync(p, JSON.stringify(config, null, 2) + '\n');
|
|
|
|
return config;
|
|
}
|