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:


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:

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':

  1. Call vmPool.claim() to get a VM
  2. Pass vmId to SandboxedToolProxyService
  3. Run agent loop with proxied tools
  4. 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

  1. VMs NEVER have OAuth tokens. API calls proxy through vitto-api → backend-api (existing CredentialBrokerService pattern).
  2. CostGuard applies per sub-vitto. VM tier doesn't get unlimited iterations.
  3. Network policy: VMs should have outbound internet (for yarn install) but no access to internal services (backend-api, MongoDB).
  4. Cleanup: VM destroyed after task — no state leaks between users. Snapshot only if ServiceAuthState pattern applies.
  5. 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

  1. Unit test: SandboxedToolProxyService with mocked InfrastructureClientService
  2. Integration test: Create VM, exec echo hello, verify stdout, destroy VM
  3. 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


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.ts TIER_TOOLS before writing any code.