Sandboxing User Projects¶
When the AI generates code, that code may execute on your server: npm install runs, the Vite dev server boots, and tools the AI calls (shell, fetch) may touch the filesystem. Doable is built so no user project can damage the host or read other users' data, no matter what the model is asked to do.
This page explains the layers; for the security model in plain English see Security Model.
Layered defense¶
┌────────────────────────────────────────────────────────────┐
│ 1. Policy layer (docore/policy) │
│ Allow-lists for tools, MCP servers, shell commands, │
│ URL fetches; per-workspace overrides. │
├────────────────────────────────────────────────────────────┤
│ 2. Permission handler sandbox (docore/sandbox) │
│ Wraps the AI's tool execution; rejects calls that │
│ violate the policy and audits everything. │
├────────────────────────────────────────────────────────────┤
│ 3. Process isolator (docore/isolator) │
│ Runs each user's session in a child process — picks │
│ the right backend per OS: │
│ Linux: nsjail / systemd-run │
│ Windows: Job Object │
│ macOS / fallback: direct (no isolation) │
├────────────────────────────────────────────────────────────┤
│ 4. dovault — runtime jail for spawned processes │
│ Node Permission Model (--allow-fs-read=...) + │
│ resource limits (cgroups / V8 heap caps). │
├────────────────────────────────────────────────────────────┤
│ 5. OS containment (Docker / cgroups / firewall) │
│ The whole API container runs as the unprivileged │
│ `node` user, on a private Docker network, with │
│ 127.0.0.1-only port bindings. │
└────────────────────────────────────────────────────────────┘
Layers 1–4 ship as code in this repo. Layer 5 is your responsibility but is configured automatically by both setup-server.sh and docker/setup.sh.
Layer 1 — Policy¶
Lives in packages/docore/src/policy/.
A policy answers questions like:
- Which tool names is the AI allowed to call?
- Which MCP servers are connected to this workspace?
- Which shell commands are safe (
ls,git status) vs dangerous (rm -rf,curl | sh)? - Which URLs may the AI
fetch()?
Defaults live in policy/defaults.ts:
DEFAULT_SAFE_COMMANDS—ls,cat,git status, …DEFAULT_DANGEROUS_COMMANDS—rm -rf,sudo,chmod,wget | sh, …DEFAULT_TRAVERSAL_PATTERNS— block../../,/etc/passwd, etc.DEFAULT_URL_ALLOWLIST— package registries, GitHub, common CDNs.
Workspaces can override these in Workspace Settings → AI → Tools. Changes are persisted by policy/store.ts (file-backed by default, can be DB-backed).
Layer 2 — Sandbox handler¶
packages/docore/src/sandbox.ts builds a permission handler that the Copilot SDK calls before executing each tool invocation. It:
- Looks up the matching policy.
- Returns
allow/deny/ask. - Writes an audit entry (
SandboxAuditEntry) for every decision.
The audit log is consumed by the Doable admin UI (Workspace settings → Audit) so you can see exactly what the agent tried to do.
Layer 3 — Process isolator¶
packages/docore/src/isolator.ts decides how to spawn each AI session's worker process. Backends:
| Backend | Where it runs | What it adds |
|---|---|---|
NsjailBackend |
Linux with nsjail installed | Mount-namespace + seccomp jail |
SystemdBackend |
Linux with systemd | Per-process cgroups (memory, CPU, tasks) |
JobObjectBackend |
Windows | Win32 Job Objects with kill-on-close + memory limit |
DirectBackend |
Anywhere | No isolation — fallback for dev / macOS |
Doable picks the strongest available backend on startup; you can pin it explicitly via DoCoreEngineOptions.
Layer 4 — dovault¶
packages/dovault is a thin, zero-runtime-dependency wrapper around Node's built-in Permission Model plus optional OS resource limits.
Three composable layers:
- ConfigGuard — locks server-side config files (Vite, Tailwind, Postgres) to safe templates so the AI can't smuggle malicious bootstrap code through them.
- ProcessJail —
node --permission --allow-fs-read=… --allow-fs-write=…calculated from the project root. - ResourceLimiter —
systemd-runcgroups (MemoryMax=150M,CPUQuota=30%,TasksMax=32) on Linux; V8 heap cap (--max-old-space-size) elsewhere.
When the API spawns a Vite dev server for a project, it goes through Vault.spawn(), which composes all three layers. See services/api/src/projects/dev-server.ts.
Layer 5 — OS containment¶
The defaults from the bundled installers:
- All app processes run as the unprivileged
nodeuser. - All ports bind to
127.0.0.1only — see Network Binding. - UFW firewall: deny incoming except 22/80/443.
- Docker: each service is a separate container on a private bridge network; the database is unreachable from the public internet.
- fail2ban watches SSH.
What can still go wrong¶
No sandbox is perfect:
- A motivated attacker who can call arbitrary tools could exhaust your AI provider quota — set workspace-level credit limits.
- A misconfigured policy that allows arbitrary
fetch()to attacker-controlled URLs lets an agent exfiltrate code. KeepDEFAULT_URL_ALLOWLISTstrict. - Native Node addons can bypass the Permission Model. Disable
child_processandworker_threadsindovaultif you accept untrusted projects from public users.
For multi-tenant SaaS deployments, see the Hardening Checklist.