S Future Vm Agent Tier Handoff
Documentation for Plans
VM Agent Tier — Comprehensive Handoff
From: S238.5 (Jarvis master spec session) To: Future session (VM tier implementation) Date: 2026-03-19 Purpose: Everything needed to wire sub-Vitto VM execution without re-exploring the codebase. Status: Deferred — in-process tier handles 90% of use cases today.
1. The Goal
Sub-Vittos with vm or browser tier should execute their tools (run_command, write_file, read_file, browser_navigate, etc.) inside a sandboxed Firecracker VM on Hetzner, not in the vitto-api process. This enables:
- Safe
run_commandexecution (install packages, run tests, build projects) - File I/O isolation (sub-Vitto can't read/write vitto-api's filesystem)
- Browser automation in a real Chrome instance
2. What Already Exists (Verified 2026-03-19)
Sub-Vitto Orchestrator — Tier System Defined
File: APIs/backend-vitto-api/src/app/engine/sub-vitto-orchestrator.service.ts
Tier tools are already defined:
in-process: recall_memory, search_decisions, query_docs, task_complete, web_search, fetch_webpage, http_request (7 tools)
vm: all in-process + run_command, write_file, read_file, search_code (11 tools)
browser: all vm + browser_navigate, browser_interact, browser_screenshot (14 tools)
Current fallback (line 151-155): Browser tier falls back to in-process. VM tier is accepted in schema but tools run locally.
Sandbox Orchestrator — Full VM Infrastructure on Hetzner
Location: APIs/sandbox-orchestrator/ (separate app, port 3010)
Status: Production. Used by ComputerUseSupervisorService today.
| Component | File | What It Does |
|---|---|---|
SandboxManagerService |
sandbox/sandbox-manager.service.ts |
Firecracker VM lifecycle: create, destroy, snapshot, restore |
SandboxController |
sandbox/sandbox.controller.ts |
REST API for VM operations |
VmExecutorService |
sandbox/vm-executor.service.ts |
SSH + xdotool commands on VMs |
| Network layer | sandbox/network/ |
TAP devices, IP allocation |
| Streaming | streaming/vnc-proxy.service.ts |
VNC proxy for human takeover |
VM specs: 2 vCPU, 2048 MiB RAM, max 30 concurrent, TTL-based eviction.
REST API (on port 3010):
POST /sandboxes — Create VM (~10s cold, ~500ms from snapshot)
GET /sandboxes/:id — Status + VNC URL
DELETE /sandboxes/:id — Destroy
POST /sandboxes/:id/exec — Run command via SSH
POST /sandboxes/:id/screenshot — PNG base64
POST /sandboxes/:id/{click,type,key,scroll} — Input actions
POST /sandboxes/:id/snapshot — Save VM state
POST /sandboxes/:id/restore — Restore from snapshot
InfrastructureClientService — HTTP Bridge from Vitto to AI-API
File: APIs/backend-vitto-api/src/app/connectivity/infrastructure-client.service.ts (321 lines)
Already connects to backend-ai-api for computer-use sessions. Uses Bearer token auth (BACKEND_API_INTERNAL_TOKEN).
SelfHostedProvider — HTTP Client from AI-API to Sandbox Orchestrator
File: APIs/backend-ai-api/src/app/infrastructure/services/sandbox/self-hosted.provider.ts
Implements SandboxProvider interface. Connects to sandbox-orchestrator via SANDBOX_SERVER_URL env var. Retry logic (2 attempts, exponential backoff). Health monitoring (60s interval).
ProcessSandboxService — Local Process Isolation
File: APIs/backend-vitto-api/src/app/security/sandbox/process-sandbox.service.ts
OS-level isolation for run_command in vitto-api process: ulimit (30s CPU, 256MB RAM), env sanitization, 1MB output cap. This is what sub-Vittos use TODAY for run_command.
3. Architecture: How It Should Work
Sub-Vitto with tier='vm' calls run_command("yarn test")
→ AgentLoopService intercepts tool call
→ SandboxedToolProxyService (NEW) checks tier
→ If tier='vm':
1. Claim or create VM via sandbox-orchestrator
2. SSH exec: "yarn test" on VM
3. Return stdout/stderr to agent loop
4. Agent loop doesn't know it ran remotely
→ If tier='in-process':
Normal local execution via ProcessSandboxService
Connection Flow
sub-vitto-orchestrator.service.ts
→ agentLoop.run() with vm tier
→ SandboxedToolProxyService (NEW, in backend-vitto-api)
→ InfrastructureClientService
→ backend-ai-api /infrastructure/sandbox/*
→ SelfHostedProvider
→ sandbox-orchestrator (Hetzner, port 3010)
→ Firecracker VM (SSH)
Key insight: DON'T bypass backend-ai-api. The SandboxProvider abstraction and health monitoring already live there. SandboxedToolProxyService should call backend-ai-api's sandbox endpoints, NOT the sandbox-orchestrator directly.
4. What Needs to Be Built
File 1 — NEW: APIs/backend-vitto-api/src/app/engine/sandboxed-tool-proxy.service.ts
Purpose: Intercepts tool calls from sub-Vittos with VM tier, routes to remote VM.
Interface:
class SandboxedToolProxyService {
// Called by AgentLoopService when tier='vm' or tier='browser'
async executeOnSandbox(
toolCall: ToolCall,
vmSessionId: string,
): Promise<ToolResult>;
// Claim a VM from the pool (or create new)
async acquireVm(userId: string, tier: 'vm' | 'browser'): Promise<string>;
// Release VM back to pool after sub-vitto completes
async releaseVm(vmSessionId: string): Promise<void>;
}
Tool mapping:
run_command→POST /sandboxes/:id/exec { command }write_file→POST /sandboxes/:id/exec { command: "cat > path <<'EOF'\n{content}\nEOF" }read_file→POST /sandboxes/:id/exec { command: "cat path" }browser_navigate→ Needs Chrome on VM + CDPbrowser_screenshot→POST /sandboxes/:id/screenshot
File 2 — NEW: APIs/backend-vitto-api/src/app/engine/vm-pool.service.ts
Purpose: Warm pool management for instant VM startup.
Interface:
class VmPoolService {
// Pre-boot N VMs from base snapshot (cron on startup)
async warmPool(): Promise<void>;
// Claim a warm VM (fast) or create cold (slow)
async claim(userId: string): Promise<{ vmId: string; cold: boolean }>;
// Return VM to pool (reset state) or destroy
async release(vmId: string, keepAlive: boolean): Promise<void>;
}
Config:
VITTO_VM_WARM_POOL_SIZE=2 # pre-booted VMs
VITTO_VM_MAX_LIFETIME_MS=900000 # 15 min default
VITTO_VM_DEFAULT_ITERATIONS=10
VITTO_VM_DEFAULT_MAX_TOKENS=100000
File 3 — MODIFY: sub-vitto-orchestrator.service.ts
Change: Remove the fallback logic (line 151-155). When tier is 'vm' or 'browser':
- Call
vmPool.claim()to get a VM - Pass vmId to
SandboxedToolProxyService - Run agent loop with proxied tools
- Call
vmPool.release()on completion
File 4 — MODIFY: APIs/backend-ai-api/src/app/infrastructure/ (possibly)
Check: Does backend-ai-api already expose sandbox endpoints for non-computer-use tasks? If not, add a lightweight controller that wraps SelfHostedProvider for sub-vitto use cases.
5. Security Considerations
- VMs NEVER have OAuth tokens. API calls proxy through vitto-api → backend-api (existing
CredentialBrokerServicepattern). - CostGuard applies per sub-vitto. VM tier doesn't get unlimited iterations.
- Network policy: VMs should have outbound internet (for
yarn install) but no access to internal services (backend-api, MongoDB). - Cleanup: VM destroyed after task — no state leaks between users. Snapshot only if
ServiceAuthStatepattern applies. - SSH keys: Ephemeral per-session (existing pattern in sandbox-orchestrator).
6. Chat Visibility
When a sub-Vitto runs on a VM, the existing SubVittoOrchestrationRenderer in chat should show the tier:
┌─ Deploying 2 agents ────────────────────┐
│ ▸ Code Review (in-process) ✓ │
│ ▸ Test Runner (vm 🖥️) ⏳ running... │
│ VM: firecracker-a3f2 | 2 vCPU, 2GB │
│ Running: yarn test (12s elapsed) │
└─────────────────────────────────────────┘
Modify: SubVittoOrchestrationRenderer to show tier badge and VM metadata when subvitto.spawned event includes tier: 'vm'.
7. Testing Strategy
- Unit test:
SandboxedToolProxyServicewith mockedInfrastructureClientService - Integration test: Create VM, exec
echo hello, verify stdout, destroy VM - E2E test: Ask Vitto to "run yarn test on module X" → verify it dispatches a VM sub-vitto, executes remotely, and returns results in chat
8. Dependencies
- Sandbox orchestrator must be running on Hetzner (port 3010)
SANDBOX_SERVER_URLenv var set in backend-ai-apiBACKEND_API_INTERNAL_TOKENenv var set in backend-vitto-api- Base Firecracker image with Node.js + yarn + git pre-installed
9. Estimated Effort
| Task | Sessions | Risk |
|---|---|---|
| SandboxedToolProxyService | 1 | Low (follows existing patterns) |
| VmPoolService | 1 | Medium (warm pool lifecycle) |
| Wire into sub-vitto-orchestrator | 0.5 | Low |
| Chat renderer update | 0.5 | Low |
| Integration testing | 1 | Medium (needs Hetzner access) |
| Total | 4 |
End of handoff. Future agent: read this + check
sub-vitto-orchestrator.service.tsTIER_TOOLS before writing any code.