Skip to content

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_COMMANDSls, cat, git status, …
  • DEFAULT_DANGEROUS_COMMANDSrm -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:

  1. ConfigGuard — locks server-side config files (Vite, Tailwind, Postgres) to safe templates so the AI can't smuggle malicious bootstrap code through them.
  2. ProcessJailnode --permission --allow-fs-read=… --allow-fs-write=… calculated from the project root.
  3. ResourceLimitersystemd-run cgroups (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 node user.
  • All ports bind to 127.0.0.1 only — 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. Keep DEFAULT_URL_ALLOWLIST strict.
  • Native Node addons can bypass the Permission Model. Disable child_process and worker_threads in dovault if you accept untrusted projects from public users.

For multi-tenant SaaS deployments, see the Hardening Checklist.

See also