Files
nanoclaw/container/agent-runner/src/destinations.ts
gavrielc e64bdb3016 refactor(claude-md): split shared base into module fragments, inject name at runtime
Move every agent-specific instruction out of the shared container/CLAUDE.md
so the base is genuinely universal. Persona/identity now comes from the
system-prompt addendum (buildSystemPromptAddendum now takes assistantName
and prepends "# You are {name}"). Per-module instructions live alongside
each MCP tool source:

  container/agent-runner/src/mcp-tools/core.instructions.md
  container/agent-runner/src/mcp-tools/scheduling.instructions.md
  container/agent-runner/src/mcp-tools/self-mod.instructions.md

composeGroupClaudeMd() scans that directory and emits `module-<name>.md`
fragments as symlinks to /app/src/mcp-tools/<name>.instructions.md (valid
via the existing RO source mount). Skill fragments renamed to
`skill-<name>.md` for naming consistency with `module-*` and `mcp-*`.

Mount tightening so composer-managed files can't be clobbered by agent
writes: nested RO mounts for /workspace/agent/CLAUDE.md and
/workspace/agent/.claude-fragments/. CLAUDE.local.md (per-group memory)
stays RW as the only writable CLAUDE.md-family file.

.gitignore: ignore CLAUDE.local.md, .claude-shared.md, .claude-fragments/
everywhere, and simplify groups/ rules to ignore the whole tree (per-
installation state, not tracked).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:14:51 +03:00

136 lines
5.0 KiB
TypeScript

/**
* Destination map — lives in inbound.db's `destinations` table.
*
* The host writes this table before every container wake AND on demand
* (e.g. when a new child agent is created mid-session). The container
* queries the table live on every lookup, so admin changes take effect
* immediately — no restart required.
*
* This table is BOTH the routing map and the container-visible ACL.
* The host re-validates on the delivery side against the central DB,
* so even if this table is stale the host's enforcement is authoritative.
*/
import { getInboundDb } from './db/connection.js';
export interface DestinationEntry {
name: string;
displayName: string;
type: 'channel' | 'agent';
channelType?: string;
platformId?: string;
agentGroupId?: string;
}
interface DestRow {
name: string;
display_name: string | null;
type: 'channel' | 'agent';
channel_type: string | null;
platform_id: string | null;
agent_group_id: string | null;
}
function rowToEntry(row: DestRow): DestinationEntry {
return {
name: row.name,
displayName: row.display_name ?? row.name,
type: row.type,
channelType: row.channel_type ?? undefined,
platformId: row.platform_id ?? undefined,
agentGroupId: row.agent_group_id ?? undefined,
};
}
export function getAllDestinations(): DestinationEntry[] {
const rows = getInboundDb().prepare('SELECT * FROM destinations ORDER BY name').all() as DestRow[];
return rows.map(rowToEntry);
}
export function findByName(name: string): DestinationEntry | undefined {
const row = getInboundDb().prepare('SELECT * FROM destinations WHERE name = ?').get(name) as DestRow | undefined;
return row ? rowToEntry(row) : undefined;
}
/**
* Reverse lookup: given routing fields from an inbound message, find
* which destination they correspond to (what does this agent call the sender?).
*/
export function findByRouting(
channelType: string | null | undefined,
platformId: string | null | undefined,
): DestinationEntry | undefined {
if (!channelType || !platformId) return undefined;
const db = getInboundDb();
const row =
channelType === 'agent'
? (db
.prepare("SELECT * FROM destinations WHERE type = 'agent' AND agent_group_id = ?")
.get(platformId) as DestRow | undefined)
: (db
.prepare("SELECT * FROM destinations WHERE type = 'channel' AND channel_type = ? AND platform_id = ?")
.get(channelType, platformId) as DestRow | undefined);
return row ? rowToEntry(row) : undefined;
}
/**
* Generate the system-prompt addendum: agent identity + destination map.
*
* Identity is injected here (not in the shared CLAUDE.md) because it's
* per-agent-group and changes when the operator renames an agent, while
* the shared base is identical across all agents.
*/
export function buildSystemPromptAddendum(assistantName?: string): string {
const sections: string[] = [];
if (assistantName) {
sections.push(['# You are ' + assistantName, '', `Your name is **${assistantName}**. Use it when the channel asks who you are, when introducing yourself, and when signing any message that explicitly calls for a signature.`].join('\n'));
}
sections.push(buildDestinationsSection());
return sections.join('\n\n');
}
function buildDestinationsSection(): string {
const all = getAllDestinations();
if (all.length === 0) {
return [
'## Sending messages',
'',
'You currently have no configured destinations. You cannot send messages until an admin wires one up.',
].join('\n');
}
// Single-destination shortcut: the agent just writes its response normally.
if (all.length === 1) {
const d = all[0];
const label = d.displayName && d.displayName !== d.name ? ` (${d.displayName})` : '';
return [
'## Sending messages',
'',
`Your messages are delivered to \`${d.name}\`${label}. Just write your response directly — no special wrapping needed.`,
'',
'To mark something as scratchpad (logged but not sent), wrap it in `<internal>...</internal>`.',
'',
'To send a message mid-response (e.g., an acknowledgment before a long task), call the `send_message` MCP tool.',
].join('\n');
}
const lines = ['## Sending messages', '', 'You can send messages to the following destinations:', ''];
for (const d of all) {
const label = d.displayName && d.displayName !== d.name ? ` (${d.displayName})` : '';
lines.push(`- \`${d.name}\`${label}`);
}
lines.push('');
lines.push('To send a message, wrap it in a `<message to="name">...</message>` block.');
lines.push('You can include multiple `<message>` blocks in one response to send to multiple destinations.');
lines.push('Text outside of `<message>` blocks is scratchpad — logged but not sent anywhere.');
lines.push('Use `<internal>...</internal>` to make scratchpad intent explicit.');
lines.push('');
lines.push(
'To send a message mid-response (e.g., an acknowledgment before a long task), call the `send_message` MCP tool with the `to` parameter set to a destination name.',
);
return lines.join('\n');
}