feat: agent-to-agent communication, dynamic agent creation, self-modification tools

Agent-to-agent: host routes messages with channel_type='agent' to target
agent's inbound.db, enriches with sender info, wakes target container.
Bidirectional routing works via inherited routing context.

Dynamic agents: create_agent MCP tool + system action handler creates
agent groups, folders, and optional CLAUDE.md on the fly.

Self-modification: install_packages (apt/npm, requires admin approval),
add_mcp_server (no approval), request_rebuild (builds per-agent-group
Docker image with approved packages). Approval flow reuses interactive
card infrastructure with pending_approvals table.

Also includes fixes from prior session: attachment download, reply context
extraction, message editing (platform message ID tracking), delivery retry
limits, and card update on button click.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-10 01:10:34 +03:00
parent 9af9bc947a
commit d8fbd3b239
24 changed files with 1025 additions and 78 deletions

View File

@@ -14,9 +14,10 @@ import { ensureContainerRuntimeRunning, cleanupOrphans } from './container-runti
import { startActiveDeliveryPoll, startSweepDeliveryPoll, setDeliveryAdapter, stopDeliveryPolls } from './delivery.js';
import { startHostSweep, stopHostSweep } from './host-sweep.js';
import { routeInbound } from './router.js';
import { getPendingQuestion, deletePendingQuestion, getSession } from './db/sessions.js';
import { writeSessionMessage } from './session-manager.js';
import { wakeContainer } from './container-runner.js';
import { getPendingQuestion, deletePendingQuestion, getPendingApproval, deletePendingApproval, getSession } from './db/sessions.js';
import { getAgentGroup, updateAgentGroup } from './db/agent-groups.js';
import { writeSessionMessage, writeSystemResponse } from './session-manager.js';
import { wakeContainer, buildAgentGroupImage } from './container-runner.js';
import { log } from './log.js';
// Channel barrel — each enabled channel self-registers on import.
@@ -83,7 +84,7 @@ async function main(): Promise<void> {
log.warn('No adapter for channel type', { channelType });
return;
}
await adapter.deliver(platformId, threadId, { kind, content: JSON.parse(content), files });
return adapter.deliver(platformId, threadId, { kind, content: JSON.parse(content), files });
},
async setTyping(channelType, platformId, threadId) {
const adapter = getChannelAdapter(channelType);
@@ -125,8 +126,15 @@ function buildConversationConfigs(channelType: string): ConversationConfig[] {
return configs;
}
/** Handle a user's response to an ask_user_question card. */
/** Handle a user's response to an ask_user_question card or an approval card. */
async function handleQuestionResponse(questionId: string, selectedOption: string, userId: string): Promise<void> {
// Check if this is a pending approval (install_packages, request_rebuild)
const approval = getPendingApproval(questionId);
if (approval) {
await handleApprovalResponse(approval, selectedOption, userId);
return;
}
const pq = getPendingQuestion(questionId);
if (!pq) {
log.warn('Pending question not found (may have expired)', { questionId });
@@ -163,6 +171,66 @@ async function handleQuestionResponse(questionId: string, selectedOption: string
await wakeContainer(session);
}
/** Handle an admin's response to an approval card. */
async function handleApprovalResponse(
approval: import('./types.js').PendingApproval,
selectedOption: string,
userId: string,
): Promise<void> {
const session = getSession(approval.session_id);
if (!session) {
deletePendingApproval(approval.approval_id);
return;
}
if (selectedOption === 'Approve') {
const payload = JSON.parse(approval.payload);
if (approval.action === 'install_packages') {
const agentGroup = getAgentGroup(session.agent_group_id);
const containerConfig = agentGroup?.container_config ? JSON.parse(agentGroup.container_config) : {};
if (!containerConfig.packages) containerConfig.packages = { apt: [], npm: [] };
if (payload.apt) containerConfig.packages.apt.push(...payload.apt);
if (payload.npm) containerConfig.packages.npm.push(...payload.npm);
updateAgentGroup(session.agent_group_id, { container_config: JSON.stringify(containerConfig) });
writeSystemResponse(session.agent_group_id, session.id, approval.request_id, 'success', {
message: 'Packages approved. Run request_rebuild to apply.',
approved: { apt: payload.apt, npm: payload.npm },
});
log.info('Package install approved', { approvalId: approval.approval_id, userId });
} else if (approval.action === 'request_rebuild') {
try {
await buildAgentGroupImage(session.agent_group_id);
writeSystemResponse(session.agent_group_id, session.id, approval.request_id, 'success', {
message: 'Container image rebuilt. Changes will take effect on next container start.',
});
log.info('Container rebuild approved and completed', { approvalId: approval.approval_id, userId });
} catch (e) {
writeSystemResponse(session.agent_group_id, session.id, approval.request_id, 'error', {
error: `Rebuild failed: ${e instanceof Error ? e.message : String(e)}`,
});
log.error('Container rebuild failed', { approvalId: approval.approval_id, err: e });
}
}
} else {
// Rejected
writeSystemResponse(session.agent_group_id, session.id, approval.request_id, 'error', {
error: `Request rejected by admin (${userId})`,
});
log.info('Approval rejected', { approvalId: approval.approval_id, action: approval.action, userId });
}
deletePendingApproval(approval.approval_id);
// Wake container so the agent's polling MCP tool picks up the response
if (session) {
await wakeContainer(session);
}
}
/** Graceful shutdown. */
async function shutdown(signal: string): Promise<void> {
log.info('Shutdown signal received', { signal });