feat: wire Docker sandboxing and agent routing into daemon

This commit is contained in:
William Valentin
2026-02-06 16:04:14 -08:00
parent fecf02acd1
commit 4dfa242716
2 changed files with 153 additions and 7 deletions
+115 -7
View File
@@ -6,6 +6,7 @@ import { AgentOrchestrator, type DelegationConfig } from '../backends/index.js';
import { SessionStore, SessionManager } from '../session/index.js';
import { HookEngine } from '../hooks/index.js';
import { ToolRegistry, ToolExecutor, ToolPolicy, allBuiltinTools, createWebSearchTools, createProcessTools, ProcessManager } from '../tools/index.js';
import type { Tool } from '../tools/types.js';
import { MemoryStore } from '../memory/index.js';
import { createMemoryTools } from '../tools/builtin/index.js';
import { GatewayServer } from '../gateway/index.js';
@@ -15,6 +16,8 @@ import type { InboundMessage, OutboundMessage } from '../channels/index.js';
import { McpManager } from '../mcp/index.js';
import { SkillRegistry, SkillInstaller, loadAllSkills } from '../skills/index.js';
import { assembleSystemPrompt } from '../prompt/index.js';
import { AgentConfigRegistry, AgentRouter } from '../agents/index.js';
import { DockerSandbox, SandboxManager, createSandboxedShellTool, createSandboxedProcessStartTool } from '../sandbox/index.js';
import { resolve } from 'path';
import { homedir } from 'os';
import { mkdirSync } from 'fs';
@@ -33,6 +36,9 @@ export interface DaemonContext {
mcpManager: McpManager;
skillRegistry: SkillRegistry;
skillInstaller: SkillInstaller;
agentConfigRegistry: AgentConfigRegistry;
agentRouter: AgentRouter;
sandboxManager?: SandboxManager;
}
function loadSystemPrompt(config: Config): string {
@@ -164,15 +170,32 @@ function createMessageRouter(deps: {
toolExecutor: ToolExecutor;
config: Config;
memoryStore?: MemoryStore;
agentConfigRegistry?: AgentConfigRegistry;
agentRouter?: AgentRouter;
sandboxManager?: SandboxManager;
}) {
// Cache agents by session ID to avoid recreating on every message
// Cache agents by session ID + agent config name to avoid recreating on every message
const agents = new Map<string, AgentOrchestrator>();
function getOrCreateAgent(channel: string, senderId: string): AgentOrchestrator {
const sessionId = `${channel}:${senderId}`;
// Resolve agent config name via routing (sender → channel → default fallback)
const agentConfigName = deps.agentRouter?.resolve(channel, senderId);
const agentConfig = agentConfigName ? deps.agentConfigRegistry?.get(agentConfigName) : undefined;
// Include agent config name in cache key so different agents aren't shared
const sessionId = agentConfigName
? `${channel}:${senderId}:${agentConfigName}`
: `${channel}:${senderId}`;
let agent = agents.get(sessionId);
if (!agent) {
const session = deps.sessionManager.getSession(channel, senderId);
// Use agent config overrides where available, falling back to global config
const effectiveSystemPrompt = agentConfig?.systemPrompt ?? deps.systemPrompt;
const effectiveTier = agentConfig?.modelTier ?? deps.config.agents.primary_tier ?? 'default';
const effectiveProvider = deps.config.models.default.provider;
const delegationConfig: DelegationConfig = {
compaction: deps.config.agents.delegation.compaction ?? 'fast',
memory_extraction: deps.config.agents.delegation.memory_extraction ?? 'fast',
@@ -180,13 +203,65 @@ function createMessageRouter(deps: {
tool_summarisation: deps.config.agents.delegation.tool_summarisation ?? 'fast',
complex_reasoning: deps.config.agents.delegation.complex_reasoning ?? 'complex',
};
// Clone the tool registry and replace shell tools with sandboxed versions if configured
let effectiveToolRegistry = deps.toolRegistry;
if (agentConfig?.sandbox && deps.sandboxManager && deps.config.sandbox.enabled) {
effectiveToolRegistry = deps.toolRegistry.clone();
// Lazy sandbox: create the sandboxed tools with a deferred sandbox reference
// The sandbox is created on first use via SandboxManager.getOrCreate()
const sandboxSessionId = sessionId;
const sandboxManager = deps.sandboxManager;
// Create a proxy sandbox that lazily initializes
const lazySandboxShell: Tool = {
name: 'shell.exec',
description: 'Execute a shell command inside a sandboxed container and return stdout/stderr.',
inputSchema: {
type: 'object',
properties: {
command: { type: 'string', description: 'The shell command to execute' },
cwd: { type: 'string', description: 'Working directory inside the container (optional)' },
timeout: { type: 'number', description: 'Timeout in milliseconds (default 30000)' },
},
required: ['command'],
},
execute: async (rawArgs: unknown) => {
const sandbox = await sandboxManager.getOrCreate(sandboxSessionId);
const tool = createSandboxedShellTool(sandbox);
return tool.execute(rawArgs);
},
};
const lazySandboxProcess: Tool = {
name: 'process.start',
description: 'Start a command in the background inside a sandboxed container.',
inputSchema: {
type: 'object',
properties: {
command: { type: 'string', description: 'The shell command to run in the background' },
cwd: { type: 'string', description: 'Working directory inside the container (optional)' },
},
required: ['command'],
},
execute: async (rawArgs: unknown) => {
const sandbox = await sandboxManager.getOrCreate(sandboxSessionId);
const tool = createSandboxedProcessStartTool(sandbox);
return tool.execute(rawArgs);
},
};
effectiveToolRegistry.replace(lazySandboxShell);
effectiveToolRegistry.replace(lazySandboxProcess);
}
agent = new AgentOrchestrator({
modelRouter: deps.modelRouter,
systemPrompt: deps.systemPrompt,
systemPrompt: effectiveSystemPrompt,
session,
toolRegistry: deps.toolRegistry,
toolRegistry: effectiveToolRegistry,
toolExecutor: deps.toolExecutor,
primaryTier: deps.config.agents.primary_tier ?? 'default',
primaryTier: effectiveTier,
delegation: delegationConfig,
maxDelegationDepth: deps.config.agents.max_delegation_depth ?? 3,
compaction: deps.config.compaction.enabled ? {
@@ -198,8 +273,8 @@ function createMessageRouter(deps: {
contextWindow: deps.config.models.default.context_window,
memoryStore: deps.memoryStore,
toolPolicyContext: {
agent: deps.config.agents.primary_tier ?? 'default',
provider: deps.config.models.default.provider,
agent: effectiveTier,
provider: effectiveProvider,
},
});
agents.set(sessionId, agent);
@@ -384,6 +459,33 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
console.log(`Loaded ${skills.length} skill(s) (${available} available)`);
}
// Initialize agent config registry and router
const agentConfigRegistry = new AgentConfigRegistry();
if (config.agent_configs && Object.keys(config.agent_configs).length > 0) {
agentConfigRegistry.loadFromConfig(config.agent_configs);
console.log(`Loaded ${Object.keys(config.agent_configs).length} agent config(s)`);
}
const agentRouter = new AgentRouter(config.routing);
// Initialize sandbox manager if Docker is available
let sandboxManager: SandboxManager | undefined;
if (config.sandbox.enabled) {
const dockerAvailable = await DockerSandbox.isAvailable();
if (dockerAvailable) {
sandboxManager = new SandboxManager(config.sandbox);
console.log(`Docker sandbox enabled (image=${config.sandbox.image}, network=${config.sandbox.network})`);
} else {
console.warn('Docker sandbox enabled but Docker not available — falling back to host execution');
}
}
if (sandboxManager) {
lifecycle.onShutdown(async () => {
await sandboxManager!.destroyAll();
console.log('Docker sandboxes destroyed');
});
}
// Initialize model router
const modelRouter = createModelRouter(config);
@@ -426,6 +528,9 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
toolExecutor,
config,
memoryStore,
agentConfigRegistry,
agentRouter,
sandboxManager,
}));
// Register Telegram adapter
@@ -527,6 +632,9 @@ export async function startDaemon(config: Config): Promise<DaemonContext> {
mcpManager,
skillRegistry,
skillInstaller,
agentConfigRegistry,
agentRouter,
sandboxManager,
};
}
+38
View File
@@ -0,0 +1,38 @@
import { describe, it, expect } from 'vitest';
import { AgentRouter } from '../agents/router.js';
import { AgentConfigRegistry } from '../agents/registry.js';
describe('daemon agent routing integration', () => {
it('resolves agent config for channel messages', () => {
const registry = new AgentConfigRegistry();
registry.loadFromConfig({
assistant: { system_prompt: 'Be helpful.', model_tier: 'default', tool_profile: 'messaging', sandbox: false },
coder: { system_prompt: 'Write code.', model_tier: 'complex', tool_profile: 'coding', sandbox: true },
});
const router = new AgentRouter({
default_agent: 'assistant',
channels: { discord: 'coder' },
senders: { 'telegram:admin': 'coder' },
});
// Discord user gets coder
const discordAgent = router.resolve('discord', 'user123');
expect(discordAgent).toBe('coder');
expect(registry.get(discordAgent!)!.systemPrompt).toBe('Write code.');
// Telegram admin gets coder
const telegramAdmin = router.resolve('telegram', 'admin');
expect(telegramAdmin).toBe('coder');
// Random telegram user gets assistant
const telegramUser = router.resolve('telegram', 'random');
expect(telegramUser).toBe('assistant');
expect(registry.get(telegramUser!)!.systemPrompt).toBe('Be helpful.');
});
it('uses default agent when no routing configured', () => {
const router = new AgentRouter({ channels: {}, senders: {} });
expect(router.resolve('telegram', '123')).toBeUndefined();
});
});