2026 03 23 Sandbox Placement Router Design
Documentation for Plans
SandboxPlacementRouter — Multi-Server VM Routing
Session: 261 | Date: 2026-03-23 | Status: Implementation ready Predecessor: S260 (infrastructure setup, both servers healthy)
Problem
GoGrab has two Hetzner dedicated servers (AX102-U, AX42-U) but SelfHostedProvider is a single-server HTTP client. Only one server receives VMs. $248/mo of hardware at 50% utilization. No role-based routing, no capacity overflow, no sticky placement.
Solution
Two new files, four modified files:
New: sandbox-server-client.ts
Plain TypeScript class (not NestJS). One instance per server. Handles all HTTP communication with a single sandbox-orchestrator: VM lifecycle, screenshots, input, file ops, snapshots. Extracted from SelfHostedProvider patterns. Config via constructor, not env vars.
New: sandbox-placement-router.ts
NestJS service implementing SandboxProvider interface. Manages N SandboxServerClient instances. Core logic:
- Filter→Score selection: filter by role + health, score by priority + available capacity
- Sticky placement:
Map<sandboxId, serverId>+Map<sessionId, serverId>, rebuilt on startup - Health polling: 30s interval per server, marks unhealthy after 3 consecutive failures
- Overflow: if primary is at capacity, routes to next server with same role
Modified: sandbox-provider.factory.ts
Priority 0: SANDBOX_SERVERS_JSON → PlacementRouter. Falls back to existing chain if not set.
Modified: sandbox-proxy.controller.ts
Replace direct SelfHostedProvider injection with SandboxPlacementRouter for writeFile/readFile/injectSshKey.
Modified: infrastructure.module.ts
Register and export SandboxPlacementRouter.
Modified: computer-use-loop.service.ts
Pass workloadType: 'executive' | 'customer' hint to getOrCreateSandbox().
Server Fleet
| ID | Roles | Priority | Max VMs |
|---|---|---|---|
| ax102-01 | customer-sandbox, git-operations | 1 | 60 |
| ax42-01 | executive-sandbox, customer-sandbox | 2 | 30 |
Adding a New Server
- Run
yarn sandbox:bootstrap --host [email protected] --domain new.gograb.space --api-key <key> - Run
yarn sandbox:deploy:host --host [email protected] - Add JSON entry to
SANDBOX_SERVERS_JSON - Restart backend-ai-api
Zero code changes required.
Future: Persistent VMs per Tenant
Current placement map is in-memory, rebuilt on restart. When persistent VMs ship, upgrade to MongoDB-backed placement. The router's API doesn't change — only the storage layer.
Edge Cases
- Server goes down mid-session → operations return clear error, don't crash
- Backend restarts → placement rebuilt from each server's
/sandboxeslist SANDBOX_SERVERS_JSONnot set → falls back toSANDBOX_SERVER_URL(backward compat)- Both servers at capacity → throws clear error, no infinite retry
- User snapshot lookup → queries all servers, caches result