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

@@ -62,7 +62,7 @@ describe('migrations', () => {
const db = initTestDb();
runMigrations(db);
const row = db.prepare('SELECT MAX(version) as v FROM schema_version').get() as { v: number };
expect(row.v).toBe(2);
expect(row.v).toBe(3);
});
});

View File

@@ -109,3 +109,14 @@ export function updateMessagingGroupAgent(
export function deleteMessagingGroupAgent(id: string): void {
getDb().prepare('DELETE FROM messaging_group_agents WHERE id = ?').run(id);
}
/** Get all messaging groups wired to an agent group (reverse lookup). */
export function getMessagingGroupsByAgentGroup(agentGroupId: string): MessagingGroup[] {
return getDb()
.prepare(
`SELECT mg.* FROM messaging_groups mg
JOIN messaging_group_agents mga ON mga.messaging_group_id = mg.id
WHERE mga.agent_group_id = ?`,
)
.all(agentGroupId) as MessagingGroup[];
}

View File

@@ -0,0 +1,18 @@
import type { Migration } from './index.js';
export const migration003: Migration = {
version: 3,
name: 'pending-approvals',
up(db) {
db.exec(`
CREATE TABLE pending_approvals (
approval_id TEXT PRIMARY KEY,
session_id TEXT NOT NULL REFERENCES sessions(id),
request_id TEXT NOT NULL,
action TEXT NOT NULL,
payload TEXT NOT NULL,
created_at TEXT NOT NULL
);
`);
},
};

View File

@@ -3,6 +3,7 @@ import type Database from 'better-sqlite3';
import { log } from '../../log.js';
import { migration001 } from './001-initial.js';
import { migration002 } from './002-chat-sdk-state.js';
import { migration003 } from './003-pending-approvals.js';
export interface Migration {
version: number;
@@ -10,7 +11,7 @@ export interface Migration {
up: (db: Database.Database) => void;
}
const migrations: Migration[] = [migration001, migration002];
const migrations: Migration[] = [migration001, migration002, migration003];
export function runMigrations(db: Database.Database): void {
db.exec(`

View File

@@ -93,11 +93,13 @@ CREATE TABLE messages_in (
content TEXT NOT NULL
);
-- Host tracks which messages_out IDs have been delivered.
-- Host tracks delivery outcomes for messages_out IDs.
-- Avoids writing to outbound.db (container-owned).
CREATE TABLE delivered (
message_out_id TEXT PRIMARY KEY,
delivered_at TEXT NOT NULL
message_out_id TEXT PRIMARY KEY,
platform_message_id TEXT,
status TEXT NOT NULL DEFAULT 'delivered',
delivered_at TEXT NOT NULL
);
`;

View File

@@ -1,4 +1,4 @@
import type { PendingQuestion, Session } from '../types.js';
import type { PendingApproval, PendingQuestion, Session } from '../types.js';
import { getDb } from './connection.js';
// ── Sessions ──
@@ -90,3 +90,24 @@ export function getPendingQuestion(questionId: string): PendingQuestion | undefi
export function deletePendingQuestion(questionId: string): void {
getDb().prepare('DELETE FROM pending_questions WHERE question_id = ?').run(questionId);
}
// ── Pending Approvals ──
export function createPendingApproval(pa: PendingApproval): void {
getDb()
.prepare(
`INSERT INTO pending_approvals (approval_id, session_id, request_id, action, payload, created_at)
VALUES (@approval_id, @session_id, @request_id, @action, @payload, @created_at)`,
)
.run(pa);
}
export function getPendingApproval(approvalId: string): PendingApproval | undefined {
return getDb().prepare('SELECT * FROM pending_approvals WHERE approval_id = ?').get(approvalId) as
| PendingApproval
| undefined;
}
export function deletePendingApproval(approvalId: string): void {
getDb().prepare('DELETE FROM pending_approvals WHERE approval_id = ?').run(approvalId);
}