feat: named destinations + permission enforcement + fire-and-forget self-mod
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>
This commit is contained in:
@@ -11,6 +11,9 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { DATA_DIR } from './config.js';
|
||||
import { getAgentGroup } from './db/agent-groups.js';
|
||||
import { getDestinations } from './db/agent-destinations.js';
|
||||
import { getMessagingGroup } from './db/messaging-groups.js';
|
||||
import { createSession, findSession, findSessionByAgentGroup, getSession, updateSession } from './db/sessions.js';
|
||||
import { log } from './log.js';
|
||||
import { INBOUND_SCHEMA, OUTBOUND_SCHEMA } from './db/schema.js';
|
||||
@@ -128,6 +131,46 @@ export function initSessionFolder(agentGroupId: string, sessionId: string): void
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the destination map file into the session folder.
|
||||
* Called before every container wake so admin changes take effect on next start.
|
||||
* The container loads this at startup to know what destinations exist.
|
||||
*/
|
||||
export function writeDestinationsFile(agentGroupId: string, sessionId: string): void {
|
||||
const dir = sessionDir(agentGroupId, sessionId);
|
||||
if (!fs.existsSync(dir)) return;
|
||||
|
||||
const rows = getDestinations(agentGroupId);
|
||||
const destinations: Array<Record<string, unknown>> = [];
|
||||
|
||||
for (const row of rows) {
|
||||
if (row.target_type === 'channel') {
|
||||
const mg = getMessagingGroup(row.target_id);
|
||||
if (!mg) continue;
|
||||
destinations.push({
|
||||
name: row.local_name,
|
||||
displayName: mg.name ?? row.local_name,
|
||||
type: 'channel',
|
||||
channelType: mg.channel_type,
|
||||
platformId: mg.platform_id,
|
||||
});
|
||||
} else if (row.target_type === 'agent') {
|
||||
const ag = getAgentGroup(row.target_id);
|
||||
if (!ag) continue;
|
||||
destinations.push({
|
||||
name: row.local_name,
|
||||
displayName: ag.name,
|
||||
type: 'agent',
|
||||
agentGroupId: ag.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const filePath = path.join(dir, '.nanoclaw-destinations.json');
|
||||
fs.writeFileSync(filePath, JSON.stringify({ destinations }, null, 2));
|
||||
log.debug('Destination map written', { sessionId, count: destinations.length });
|
||||
}
|
||||
|
||||
/** Write a message to a session's inbound DB (messages_in). Host-only. */
|
||||
export function writeSessionMessage(
|
||||
agentGroupId: string,
|
||||
|
||||
Reference in New Issue
Block a user