/** * Self-modification MCP tools: install_packages, add_mcp_server, request_rebuild. * * All three are fire-and-forget — the tool writes a system action row and * returns immediately. The host processes the request (including admin * approval) and notifies the agent via a chat message when complete. * * Package names are sanitized here at the tool boundary AND re-validated on * the host side (defense in depth). */ import { writeMessageOut } from '../db/messages-out.js'; import type { McpToolDefinition } from './types.js'; function log(msg: string): void { console.error(`[mcp-tools] ${msg}`); } function generateId(): string { return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; } function ok(text: string) { return { content: [{ type: 'text' as const, text }] }; } function err(text: string) { return { content: [{ type: 'text' as const, text: `Error: ${text}` }], isError: true }; } const APT_RE = /^[a-z0-9][a-z0-9._+-]*$/; const NPM_RE = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/; const MAX_PACKAGES = 20; export const installPackages: McpToolDefinition = { tool: { name: 'install_packages', description: 'Request installation of apt or npm packages. Requires admin approval. Fire-and-forget: you will receive a notification when the request is approved or rejected. After approval, call request_rebuild to apply the changes.', inputSchema: { type: 'object' as const, properties: { apt: { type: 'array', items: { type: 'string' }, description: 'apt packages to install (names only, no version specs or flags)' }, npm: { type: 'array', items: { type: 'string' }, description: 'npm packages to install globally (names only, no version specs)' }, reason: { type: 'string', description: 'Why these packages are needed' }, }, }, }, async handler(args) { const apt = (args.apt as string[]) || []; const npm = (args.npm as string[]) || []; if (apt.length === 0 && npm.length === 0) return err('At least one apt or npm package is required'); if (apt.length + npm.length > MAX_PACKAGES) return err(`Maximum ${MAX_PACKAGES} packages per request`); const invalidApt = apt.find((p) => !APT_RE.test(p)); if (invalidApt) return err(`Invalid apt package name: "${invalidApt}". Only lowercase letters, digits, and ._+- allowed.`); const invalidNpm = npm.find((p) => !NPM_RE.test(p)); if (invalidNpm) return err(`Invalid npm package name: "${invalidNpm}". No version specs or shell characters.`); const requestId = generateId(); writeMessageOut({ id: requestId, kind: 'system', content: JSON.stringify({ action: 'install_packages', apt, npm, reason: (args.reason as string) || '', }), }); log(`install_packages: ${requestId} → apt=[${apt.join(',')}] npm=[${npm.join(',')}]`); return ok(`Package install request submitted. You will be notified when admin approves or rejects.`); }, }; export const addMcpServer: McpToolDefinition = { tool: { name: 'add_mcp_server', description: "Request adding an MCP server to this agent's configuration. Requires admin approval. Fire-and-forget: you will be notified when approved/rejected. On approval, your container restarts with the new server.", inputSchema: { type: 'object' as const, properties: { name: { type: 'string', description: 'MCP server name (unique identifier)' }, command: { type: 'string', description: 'Command to run the MCP server' }, args: { type: 'array', items: { type: 'string' }, description: 'Command arguments' }, env: { type: 'object', description: 'Environment variables for the server' }, }, required: ['name', 'command'], }, }, async handler(args) { const name = args.name as string; const command = args.command as string; if (!name || !command) return err('name and command are required'); const requestId = generateId(); writeMessageOut({ id: requestId, kind: 'system', content: JSON.stringify({ action: 'add_mcp_server', name, command, args: (args.args as string[]) || [], env: (args.env as Record) || {}, }), }); log(`add_mcp_server: ${requestId} → "${name}" (${command})`); return ok(`MCP server request submitted. You will be notified when admin approves or rejects.`); }, }; export const requestRebuild: McpToolDefinition = { tool: { name: 'request_rebuild', description: 'Request a container rebuild to apply pending package installations. Requires admin approval. Fire-and-forget: you will be notified when approved/rejected. On approval, your container restarts with the new image on the next message.', inputSchema: { type: 'object' as const, properties: { reason: { type: 'string', description: 'Why the rebuild is needed' }, }, }, }, async handler(args) { const requestId = generateId(); writeMessageOut({ id: requestId, kind: 'system', content: JSON.stringify({ action: 'request_rebuild', reason: (args.reason as string) || '', }), }); log(`request_rebuild: ${requestId}`); return ok(`Rebuild request submitted. You will be notified when admin approves or rejects.`); }, }; export const selfModTools: McpToolDefinition[] = [installPackages, addMcpServer, requestRebuild];