docs: add Docker sandbox and multi-agent routing design/implementation plans
This commit is contained in:
@@ -0,0 +1,173 @@
|
|||||||
|
# P2: Docker Sandboxing + Multi-Agent Routing — Design
|
||||||
|
|
||||||
|
**Date:** 2026-02-06
|
||||||
|
**Status:** Approved
|
||||||
|
**Priority:** P2 (completes all P2 work)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature 1: Docker Sandboxing
|
||||||
|
|
||||||
|
### Goal
|
||||||
|
|
||||||
|
Channel sessions (Telegram, Discord, Slack, WhatsApp) execute `shell.exec` and `process.start` inside Docker containers. TUI and local WebSocket sessions continue running on the host.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
Tool-level wrapping: sandboxed versions of dangerous tools (`shell.exec`, `process.start`) delegate to `docker exec` inside a per-session container. All other tools (file.read, web.fetch, memory.*, etc.) run on the host unchanged.
|
||||||
|
|
||||||
|
```
|
||||||
|
src/sandbox/
|
||||||
|
docker.ts — DockerSandbox class (create/exec/destroy containers via CLI)
|
||||||
|
docker.test.ts — Tests (mocked Docker CLI)
|
||||||
|
manager.ts — SandboxManager (session→container mapping + lifecycle)
|
||||||
|
manager.test.ts — Tests
|
||||||
|
tools.ts — createSandboxedShellTool(), createSandboxedProcessStartTool()
|
||||||
|
tools.test.ts — Tests
|
||||||
|
index.ts — Barrel export
|
||||||
|
```
|
||||||
|
|
||||||
|
### Config Schema
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
sandbox:
|
||||||
|
enabled: false # opt-in
|
||||||
|
image: "node:22-slim" # base container image
|
||||||
|
workspace_dir: "/workspace" # mount path inside container
|
||||||
|
network: "none" # container network mode (none/bridge/host)
|
||||||
|
memory_limit: "512m" # memory limit per container
|
||||||
|
cpu_limit: "1.0" # CPU limit per container
|
||||||
|
timeout_seconds: 300 # auto-kill timeout per container
|
||||||
|
```
|
||||||
|
|
||||||
|
### DockerSandbox Class
|
||||||
|
|
||||||
|
Wraps Docker CLI via `child_process.execFile` (no Docker SDK dependency):
|
||||||
|
- `create()` — `docker create` with resource limits, bind mount, network mode
|
||||||
|
- `start()` — `docker start`
|
||||||
|
- `exec(command, opts)` — `docker exec` with timeout, returns stdout/stderr
|
||||||
|
- `destroy()` — `docker rm -f`
|
||||||
|
- `isRunning()` — `docker inspect` check
|
||||||
|
|
||||||
|
### SandboxManager
|
||||||
|
|
||||||
|
- `getOrCreate(sessionId, config)` — Lazy container creation on first tool call
|
||||||
|
- `destroy(sessionId)` — Stop and remove container
|
||||||
|
- `destroyAll()` — Shutdown hook for daemon cleanup
|
||||||
|
|
||||||
|
### Sandboxed Tools
|
||||||
|
|
||||||
|
- `createSandboxedShellTool(sandbox)` — Same `Tool` interface as `shell.exec`, but runs via `sandbox.exec(command)`. Preserves cwd (translated to container path), timeout, output truncation.
|
||||||
|
- `createSandboxedProcessStartTool(sandbox)` — Wraps `process.start` to spawn via `docker exec -d` (detached mode).
|
||||||
|
|
||||||
|
### Per-Session ToolRegistry
|
||||||
|
|
||||||
|
When sandbox is active for a channel session, the daemon creates a cloned `ToolRegistry` that replaces `shell.exec` and `process.start` with sandboxed versions. All other tools reference the shared host registry.
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
- Docker not installed → log warning at startup, fall through to host execution
|
||||||
|
- Container creation fails → log error, return tool error (not crash)
|
||||||
|
- Container timeout → `docker rm -f`, return timeout error
|
||||||
|
- Docker daemon unavailable → graceful degradation with clear error messages
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature 2: Multi-Agent Routing
|
||||||
|
|
||||||
|
### Goal
|
||||||
|
|
||||||
|
Named agent configurations that can be assigned to channels, senders, or sender patterns. Each agent config specifies its own system prompt, model tier, tool profile, and sandbox setting.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/agents/
|
||||||
|
registry.ts — AgentConfigRegistry (stores named AgentConfig objects)
|
||||||
|
router.ts — AgentRouter (resolves {channel, senderId} → AgentConfig)
|
||||||
|
router.test.ts — Tests
|
||||||
|
index.ts — Barrel export
|
||||||
|
```
|
||||||
|
|
||||||
|
### Config Schema
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
agent_configs:
|
||||||
|
assistant:
|
||||||
|
system_prompt: "You are a helpful assistant."
|
||||||
|
model_tier: default
|
||||||
|
tool_profile: messaging
|
||||||
|
sandbox: true
|
||||||
|
|
||||||
|
coder:
|
||||||
|
system_prompt: "You are a coding assistant. Focus on writing clean code."
|
||||||
|
model_tier: complex
|
||||||
|
tool_profile: coding
|
||||||
|
sandbox: true
|
||||||
|
|
||||||
|
routing:
|
||||||
|
default_agent: assistant
|
||||||
|
channels:
|
||||||
|
discord: coder
|
||||||
|
senders:
|
||||||
|
"telegram:12345": coder
|
||||||
|
"slack:U0*": assistant
|
||||||
|
```
|
||||||
|
|
||||||
|
### AgentConfigRegistry
|
||||||
|
|
||||||
|
Stores parsed `AgentConfig` objects by name:
|
||||||
|
- `register(config)` — Add a named config
|
||||||
|
- `get(name)` — Look up by name
|
||||||
|
- `list()` — All registered configs
|
||||||
|
- `loadFromConfig(rawConfig)` — Parse from validated YAML
|
||||||
|
|
||||||
|
### AgentConfig Type
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface AgentConfig {
|
||||||
|
name: string;
|
||||||
|
systemPrompt?: string; // overrides global system prompt
|
||||||
|
modelTier?: ModelTier; // fast/default/complex/local
|
||||||
|
toolProfile?: ToolProfile; // minimal/messaging/coding/full
|
||||||
|
toolOverrides?: ToolOverrideConfig;
|
||||||
|
sandbox?: boolean; // use Docker sandbox (if globally enabled)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### AgentRouter
|
||||||
|
|
||||||
|
Resolves which `AgentConfig` to use for a given message:
|
||||||
|
1. Check `senders` map — exact match first, then glob patterns (via `minimatch`)
|
||||||
|
2. Check `channels` map — channel name match
|
||||||
|
3. Fall back to `routing.default_agent`
|
||||||
|
|
||||||
|
### Daemon Integration
|
||||||
|
|
||||||
|
The `createMessageRouter()` function changes:
|
||||||
|
1. On message: `agentRouter.resolve(channel, senderId)` returns agent config name
|
||||||
|
2. Cache key: `${channel}:${senderId}:${agentConfigName}` (agent change = new orchestrator)
|
||||||
|
3. Create `AgentOrchestrator` with resolved config's system prompt, model tier, tool policy
|
||||||
|
4. If sandbox enabled for this config + globally: create per-session sandboxed ToolRegistry
|
||||||
|
5. Otherwise: use shared host ToolRegistry
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modified Files
|
||||||
|
|
||||||
|
- `src/config/schema.ts` — Add `sandboxSchema`, `agentConfigSchema`, `routingSchema`
|
||||||
|
- `src/config/index.ts` — Export new types
|
||||||
|
- `src/daemon/index.ts` — Wire SandboxManager + AgentRouter into message handler
|
||||||
|
- `src/tools/registry.ts` — Add `clone()` method for per-session copies
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- All Docker interactions mocked (no real Docker in tests)
|
||||||
|
- Agent router tested with config fixtures (exact, glob, channel, default fallback)
|
||||||
|
- Sandboxed tools tested with mocked Docker CLI exec
|
||||||
|
- Integration tested via daemon message handler with mocked dependencies
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- No new npm dependencies (Docker CLI, `minimatch` already available or trivially implemented)
|
||||||
|
- Runtime: Docker must be installed on host for sandbox feature to work (graceful degradation if absent)
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user