Replaces implicit routing context (NANOCLAW_PLATFORM_ID env vars) with
per-agent named destination maps. Agents reference channels and peer
agents by local names; the host re-validates every outbound route against
a new agent_destinations table that is both the routing map and the ACL.
Model changes:
- New migration 004 adds agent_destinations (agent_group_id, local_name,
target_type, target_id). Backfills from existing messaging_group_agents.
- Host writes /workspace/.nanoclaw-destinations.json before every container
wake so admin changes take effect on next start.
- Container loads map at startup, appends system-prompt addendum listing
available destinations and the <message to="name">…</message> syntax.
- Agent main output is parsed for <message to="..."> blocks; each block
becomes a messages_out row with routing resolved via the local map.
Untagged text and <internal>…</internal> are scratchpad (logged only).
- send_message MCP tool now takes `to` (destination name) instead of raw
routing fields. send_to_agent deleted (redundant — agents are just
destinations). send_file/edit_message/add_reaction route via map too.
- Inbound formatter adds from="name" attribute via reverse-lookup so the
agent sees a consistent namespace in both directions.
Permission enforcement:
- Host checks hasDestination() before every channel delivery AND every
agent-to-agent route. Unauthorized messages dropped and logged.
- routeAgentMessage simplified: ~15 lines, no JSON parse, content copied
verbatim (target formatter resolves the sender via its own local map).
- create_agent is admin-only, checked at both the container (tool not
registered for non-admins) and the host (re-check on receive). Inserts
bidirectional destination rows so parent↔child comms work immediately.
Includes path-traversal guard on folder name.
Self-modification cleanup:
- add_mcp_server now requires admin approval (previously had none).
- install_packages validates package names on BOTH sides (container tool
+ host receiver) with strict regex. Max 20 packages per request.
- All three self-mod tools are fire-and-forget: write request, return
immediately with "submitted" message. Admin approval triggers a chat
notification to the requesting agent — no tool-call polling, no 5-min
holds. On rebuild/mcp_server approval, the container is killed so the
next wake picks up new config/image.
- Approval delivery extracted into requestApproval() helper (the one
place where three call sites were literally identical).
Also folded in the phase-1 dynamic import cleanup (create_agent no longer
does `await import('./db/agent-groups.js')`) and removes NANOCLAW_PLATFORM_ID
/ CHANNEL_TYPE / THREAD_ID env-var routing entirely.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
144 lines
5.4 KiB
TypeScript
144 lines
5.4 KiB
TypeScript
/**
|
|
* 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<string, string>) || {},
|
|
}),
|
|
});
|
|
|
|
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];
|