/** * Approval handlers for self-modification actions. * * The approvals module calls these when an admin clicks Approve on a * pending_approvals row whose action matches. Each handler mutates the * container config in the DB, rebuilds/kills the container as needed, * and writes an on_wake message so the fresh container picks up where * the old one left off. * * install_packages: update DB + rebuild image + kill container + on_wake. * add_mcp_server: update DB + kill container + on_wake. */ import { buildAgentGroupImage, killContainer, wakeContainer } from '../../container-runner.js'; import { getAgentGroup } from '../../db/agent-groups.js'; import { getContainerConfig, updateContainerConfigJson } from '../../db/container-configs.js'; import { getSession } from '../../db/sessions.js'; import type { McpServerConfig } from '../../container-config.js'; import { log } from '../../log.js'; import { writeSessionMessage } from '../../session-manager.js'; import type { ApprovalHandler } from '../approvals/index.js'; export const applyInstallPackages: ApprovalHandler = async ({ session, payload, userId, notify }) => { const agentGroup = getAgentGroup(session.agent_group_id); if (!agentGroup) { notify('install_packages approved but agent group missing.'); return; } const configRow = getContainerConfig(agentGroup.id); if (!configRow) { notify('install_packages approved but container config missing.'); return; } // Append new packages to existing lists in the DB (deduplicated) if (payload.apt) { const existing = JSON.parse(configRow.packages_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[]; for (const pkg of payload.npm as string[]) { if (!existing.includes(pkg)) existing.push(pkg); } updateContainerConfigJson(agentGroup.id, 'packages_npm', existing); } const pkgs = [ ...((payload.apt as string[] | undefined) || []), ...((payload.npm as string[] | undefined) || []), ].join(', '); log.info('Package install approved', { agentGroupId: session.agent_group_id, userId }); try { await buildAgentGroupImage(session.agent_group_id); writeSessionMessage(session.agent_group_id, session.id, { id: `appr-note-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, kind: 'chat', timestamp: new Date().toISOString(), platformId: session.agent_group_id, channelType: 'agent', threadId: null, content: JSON.stringify({ text: `Packages installed (${pkgs}) and container rebuilt. Verify the new packages are available (e.g. run them or check versions) and report the result to the user.`, sender: 'system', senderId: 'system', }), onWake: 1, }); killContainer(session.id, 'rebuild applied', () => { const s = getSession(session.id); if (s) wakeContainer(s); }); log.info('Container rebuild completed (bundled with install)', { agentGroupId: session.agent_group_id }); } catch (e) { notify( `Packages added to config (${pkgs}) but rebuild failed: ${e instanceof Error ? e.message : String(e)}. Tell the user — an admin will need to retry the install_packages request or inspect the build logs.`, ); log.error('Bundled rebuild failed after install approval', { agentGroupId: session.agent_group_id, err: e }); } }; export const applyAddMcpServer: ApprovalHandler = async ({ session, payload, userId, notify }) => { const agentGroup = getAgentGroup(session.agent_group_id); if (!agentGroup) { notify('add_mcp_server approved but agent group missing.'); return; } const configRow = getContainerConfig(agentGroup.id); if (!configRow) { notify('add_mcp_server approved but container config missing.'); return; } // Add the new MCP server to the existing map in the DB const servers = JSON.parse(configRow.mcp_servers) as Record; servers[payload.name as string] = { command: payload.command as string, args: (payload.args as string[]) || [], env: (payload.env as Record) || {}, }; updateContainerConfigJson(agentGroup.id, 'mcp_servers', servers); writeSessionMessage(session.agent_group_id, session.id, { id: `appr-note-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, kind: 'chat', timestamp: new Date().toISOString(), platformId: session.agent_group_id, channelType: 'agent', threadId: null, content: JSON.stringify({ text: `MCP server "${payload.name}" added. Verify it's available (e.g. list your tools) and report the result to the user.`, sender: 'system', senderId: 'system', }), onWake: 1, }); killContainer(session.id, 'mcp server added', () => { const s = getSession(session.id); if (s) wakeContainer(s); }); log.info('MCP server add approved', { agentGroupId: session.agent_group_id, userId }); };