From 7eebcf74c2f7ea5d4507222efbdd47e50bb0b3fb Mon Sep 17 00:00:00 2001 From: gavrielc Date: Fri, 8 May 2026 22:33:42 +0300 Subject: [PATCH] fix: harden container config DB layer - config-add/remove-package now rebuild image + restart containers - Deduplicate packages in self-mod install_packages handler - Add runtime whitelist guards for SQL column interpolation Co-Authored-By: Claude Opus 4.6 (1M context) --- src/cli/resources/groups.ts | 7 +++++++ src/db/container-configs.ts | 5 +++++ src/modules/self-mod/apply.ts | 10 +++++++--- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/cli/resources/groups.ts b/src/cli/resources/groups.ts index 3b721d9..73ced9c 100644 --- a/src/cli/resources/groups.ts +++ b/src/cli/resources/groups.ts @@ -1,4 +1,5 @@ import type { McpServerConfig } from '../../container-config.js'; +import { buildAgentGroupImage } from '../../container-runner.js'; import { restartAgentGroupContainers } from '../../container-restart.js'; import { getContainerConfig, @@ -186,6 +187,9 @@ registerResource({ } } + await buildAgentGroupImage(id); + restartAgentGroupContainers(id, 'package added via ncl'); + return { added: { apt: apt || null, npm: npm || null } }; }, }, @@ -214,6 +218,9 @@ registerResource({ updateContainerConfigJson(id, 'packages_npm', filtered); } + await buildAgentGroupImage(id); + restartAgentGroupContainers(id, 'package removed via ncl'); + return { removed: { apt: apt || null, npm: npm || null } }; }, }, diff --git a/src/db/container-configs.ts b/src/db/container-configs.ts index 2e1ce9e..207c02b 100644 --- a/src/db/container-configs.ts +++ b/src/db/container-configs.ts @@ -38,6 +38,9 @@ export function ensureContainerConfig(agentGroupId: string): void { .run(agentGroupId, new Date().toISOString()); } +const SCALAR_COLUMNS = new Set(['provider', 'model', 'effort', 'image_tag', 'assistant_name', 'max_messages_per_prompt']); +const JSON_COLUMNS = new Set(['skills', 'mcp_servers', 'packages_apt', 'packages_npm', 'additional_mounts']); + /** Update scalar fields on a config row. Only touches fields present in `updates`. */ export function updateContainerConfigScalars( agentGroupId: string, @@ -53,6 +56,7 @@ export function updateContainerConfigScalars( for (const [key, value] of Object.entries(updates)) { if (value !== undefined) { + if (!SCALAR_COLUMNS.has(key)) throw new Error(`Invalid scalar column: ${key}`); fields.push(`${key} = @${key}`); values[key] = value; } @@ -73,6 +77,7 @@ export function updateContainerConfigJson( column: 'skills' | 'mcp_servers' | 'packages_apt' | 'packages_npm' | 'additional_mounts', value: unknown, ): void { + if (!JSON_COLUMNS.has(column)) throw new Error(`Invalid JSON column: ${column}`); const now = new Date().toISOString(); getDb() .prepare(`UPDATE container_configs SET ${column} = ?, updated_at = ? WHERE agent_group_id = ?`) diff --git a/src/modules/self-mod/apply.ts b/src/modules/self-mod/apply.ts index da5b356..b9753ab 100644 --- a/src/modules/self-mod/apply.ts +++ b/src/modules/self-mod/apply.ts @@ -30,15 +30,19 @@ export const applyInstallPackages: ApprovalHandler = async ({ session, payload, return; } - // Append new packages to existing lists in the DB + // Append new packages to existing lists in the DB (deduplicated) if (payload.apt) { const existing = JSON.parse(configRow.packages_apt) as string[]; - existing.push(...(payload.apt as string[])); + for (const pkg of payload.apt as string[]) { + if (!existing.includes(pkg)) existing.push(pkg); + } updateContainerConfigJson(agentGroup.id, 'packages_apt', existing); } if (payload.npm) { const existing = JSON.parse(configRow.packages_npm) as string[]; - existing.push(...(payload.npm as string[])); + for (const pkg of payload.npm as string[]) { + if (!existing.includes(pkg)) existing.push(pkg); + } updateContainerConfigJson(agentGroup.id, 'packages_npm', existing); }