Files
nanoclaw/src/db/container-configs.ts
gavrielc 31ccc61b27 feat(db): move container config from filesystem to DB
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>
2026-05-08 22:27:55 +03:00

85 lines
3.0 KiB
TypeScript

import type { ContainerConfigRow } from '../types.js';
import { getDb } from './connection.js';
export function getContainerConfig(agentGroupId: string): ContainerConfigRow | undefined {
return getDb().prepare('SELECT * FROM container_configs WHERE agent_group_id = ?').get(agentGroupId) as
| ContainerConfigRow
| undefined;
}
export function getAllContainerConfigs(): ContainerConfigRow[] {
return getDb().prepare('SELECT * FROM container_configs').all() as ContainerConfigRow[];
}
/** Insert a new config row. Caller must supply all JSON fields (use defaults for empty). */
export function createContainerConfig(config: ContainerConfigRow): void {
getDb()
.prepare(
`INSERT INTO container_configs (
agent_group_id, provider, model, effort, image_tag, assistant_name,
max_messages_per_prompt, skills, mcp_servers, packages_apt, packages_npm,
additional_mounts, updated_at
) VALUES (
@agent_group_id, @provider, @model, @effort, @image_tag, @assistant_name,
@max_messages_per_prompt, @skills, @mcp_servers, @packages_apt, @packages_npm,
@additional_mounts, @updated_at
)`,
)
.run(config);
}
/** Create an empty config row with sensible defaults. Idempotent — no-ops if row exists. */
export function ensureContainerConfig(agentGroupId: string): void {
getDb()
.prepare(
`INSERT OR IGNORE INTO container_configs (agent_group_id, updated_at)
VALUES (?, ?)`,
)
.run(agentGroupId, new Date().toISOString());
}
/** Update scalar fields on a config row. Only touches fields present in `updates`. */
export function updateContainerConfigScalars(
agentGroupId: string,
updates: Partial<
Pick<
ContainerConfigRow,
'provider' | 'model' | 'effort' | 'image_tag' | 'assistant_name' | 'max_messages_per_prompt'
>
>,
): void {
const fields: string[] = [];
const values: Record<string, unknown> = { agent_group_id: agentGroupId };
for (const [key, value] of Object.entries(updates)) {
if (value !== undefined) {
fields.push(`${key} = @${key}`);
values[key] = value;
}
}
if (fields.length === 0) return;
fields.push('updated_at = @updated_at');
values.updated_at = new Date().toISOString();
getDb()
.prepare(`UPDATE container_configs SET ${fields.join(', ')} WHERE agent_group_id = @agent_group_id`)
.run(values);
}
/** Overwrite a JSON column wholesale. Used for skills, mcp_servers, packages_*, additional_mounts. */
export function updateContainerConfigJson(
agentGroupId: string,
column: 'skills' | 'mcp_servers' | 'packages_apt' | 'packages_npm' | 'additional_mounts',
value: unknown,
): void {
const now = new Date().toISOString();
getDb()
.prepare(`UPDATE container_configs SET ${column} = ?, updated_at = ? WHERE agent_group_id = ?`)
.run(JSON.stringify(value), now, agentGroupId);
}
export function deleteContainerConfig(agentGroupId: string): void {
getDb().prepare('DELETE FROM container_configs WHERE agent_group_id = ?').run(agentGroupId);
}