Trusted plane
The server process itself: it holds the encrypted secrets, runs the command model, and owns the database. Highest trust.
Subspace runs powerful things (agents with real tools, terminals, coding-agent dispatch) on a machine that also holds your mail, your calendar, and your knowledge. Its security model is built around three ideas working together: a plane split that separates trust levels, a capability broker that decides which secrets any run may touch, and layered approval gates that keep a human in the loop on anything outbound or irreversible. Multi-user access control (page ACLs) and per-tool approvals (tools and approvals) sit on top of this foundation.
Work is organized into three planes by how much authority it is trusted with:
Trusted plane
The server process itself: it holds the encrypted secrets, runs the command model, and owns the database. Highest trust.
Worker plane
The agent engine can optionally run as its own process with a restricted environment, obtaining capabilities through the broker rather than holding ambient credentials.
Sandbox plane
Custom functions run in a QuickJS-WASM sandbox with a curated API and zero ambient authority. The strongest form of untrusted execution.
The --role entrypoint flag selects the split (web, worker, or all, defaulting to
all for a single-user local install). Running the agent engine under --role=worker gives it
a secret-free environment and routes every credential need through the broker, so tool code
never changes, by construction.
Credentials are never scattered through the process environment. They live encrypted in one place and are handed out per run:
Secrets live in ops.secrets as libsodium secretbox rows (a v1: prefix plus
base64 of nonce and ciphertext). The master key lives in <home>/secret.key at mode
0600, minted on first use, and is deliberately not an environment variable. On a
desktop install it can upgrade to the OS keychain. Everything else references a secret by
name only (Authorization: github.mcp_token), never by value.
A tool declares what it needs with requires: [] (its reach: mail.send, web.fetch,
wandb.api, and so on). The broker decrypts a secret on demand at call time, checks it
against the run’s grant set, and writes an audit row for every acquire. A missing grant is
a tool-result denial, never an approval prompt. By default a run’s grant set is the union
of its allowlisted tools’ requires, so only an explicit caps: on the agent definition
narrows reach further.
At boot, any legacy plaintext secret rows are swept and rewritten as encrypted rows, and
environment-provided credentials are imported once into ops.secrets rather than read live.
LLM and speech-to-text keys never leave the trusted server process.
Not all data carries the same trust, and admin omniscience has deliberate exceptions:
| Domain | Rule |
|---|---|
| The OKF disk mirror | Operator trust. Filesystem access to the OKF dump exports the whole graph, so it is treated as operator-level access. |
| Comms (mail, IM, calendar) | Personal. Rows are scoped strictly to their owning user, with no admin bypass, the one exception to admin visibility. |
PTY, os.exec, code.* |
Admin-only. A shell on the host is the highest execution tier; coding-agent dispatch rides the same rule. |
Agents act as their run’s owner: a tool’s authorize check runs against the owner’s live
page ACLs before any gate, and an unreadable page is reported as “no
such page” rather than probed. A permission denial is always a tool result, never an approval
prompt.
On top of permissions sits the human layer. Any outbound or irreversible tool (send mail or
IM, calendar writes, LinkedIn actions, os.exec) pauses the run on an approval card. Gate
composition is explicit:
always gates everything, never gates nothing, auto defers
to the tool’s own needsApproval) is composed with a per-tool trust floor: a tool
marked alwaysGate (like os.exec) is gated no matter what the policy says.See tools and approvals for the authoring side of gates.
Between fully sandboxed functions and admin-only raw exec sits a middle tier for running real code with contained blast radius:
Sandboxed functions
Custom functions run in QuickJS-WASM with a curated, rate-limited host API and quotas. No ambient authority; output is a SubUI JSON tree, never raw HTML, so the sandbox boundary holds.
Sandboxed bash (Docker)
Launchers and coding tasks run in a Docker container adapter rather than on the host.
The docker lane is gated behind SUBSPACE_CONTAINERS=1 (or SUBSPACE_DOCKER_BIN), always
approval-gated, and cannot touch the host filesystem the way raw exec can.
Admin-only exec
The bash lane on the host (SUBSPACE_OS_EXEC=1), terminals, and code cells are full
power, explicitly user-invoked, and admin-only.
Imported content is tiered too, because it is the widest injection surface. Inbound HTML mail
renders only sanitized, in a non-same-origin sandboxed iframe. Extension captures store
extracted Markdown plus a screenshot, never live HTML. Sandbox output reaches React as a JSON
tree, never dangerouslySetInnerHTML, and the SPA ships a strict CSP. One crafted email or
captured page cannot become script with command authority.
In a multi-user instance, isolation is enforced at the data layer, not by UI hiding, and it is audited both ways: