Codumentor logo Codumentor

Security Architecture

This page describes the security architecture of Codumentor, with a focus on the process model: what runs inside the server process, what runs in separate operating-system processes, and how the processes that handle untrusted content are sandboxed.

It is a design reference for security reviews and deployment approval. For the hands-on hardening checklist (authentication, TLS, secrets handling, file permissions), see Security; for host prerequisites and per-plugin setup, see Plugins in Production.

Design summary

Codumentor runs as a single server process. Individual agents, conversations, and users are not separate OS processes — they are concurrent tasks inside the server, separated by authentication and per-user data ownership.

Process-level isolation is instead applied where it provides a real security boundary: around the execution and parsing of untrusted content. Every action where model-generated or externally-sourced content could execute code — shell commands written by the LLM, external coding-agent CLIs, parsing of user-supplied office documents, rendering of fetched web pages — runs in a separate, sandboxed child process with a restricted filesystem view, no network by default, and a scrubbed environment.

The rationale: inside the server process, only Codumentor's own code and operator-approved plugins execute. Model output is data there — it is stored, streamed, and displayed, but never evaluated. The moment model output becomes code (a shell command, a build step, a document parser fed a hostile file), it crosses into a sandboxed child process. Drawing the boundary around execution rather than around agent instances means the boundary is a kernel-enforced one (namespaces and mounts), not merely a memory-separation one.

A second principle is fail-closed behavior: when sandboxing is enabled but cannot be provided (bubblewrap missing, sandbox setup error), the affected tools are disabled and the agent is told why. The product never silently falls back to executing agent-controlled work directly on the host.

The process model

One server process

The server is a FastAPI application hosted by uvicorn — a single asyncio-based OS process started with python -m codumentor ... api (or the frozen binary equivalent). There is no separate daemon: the HTTP API, SSE streaming, the agent engine, the plugin system, the background job queue, and the scheduler all live inside this one process. If the process stops, everything stops; there are no orphaned services to account for.

What runs in-process:

ComponentRole
API layerFastAPI routes, auth middleware, SSE streaming
Agent engineOrchestrates LLM calls and tool invocations per turn
Non-executing toolsFile read/write, knowledge search, and similar tools that perform mediated I/O (see below)
PluginsOperator-enabled extensions; run with full process access (trusted code)
Job queue & schedulerBackground jobs (ingestion, memory storage, scheduled runs) as asyncio workers claiming from a local SQLite queue
Data storesSQLite databases (WAL mode) and the ChromaDB vector store, both on local disk

Agents are tasks, not processes

Each conversation turn runs as an asyncio task inside the server. A single shared agent engine serves all users; the user and session identity are parameters of each turn, not properties of a process.

This is safe because of what does — and does not — happen in-process:

If your review question is "do agents run in separate processes?": no, and by design. Separate processes per agent would isolate trusted code from trusted code, while the actual risk — model-authored commands and hostile file content — would still need a sandbox. Codumentor spends the process boundary where the trust boundary is.

Where child processes appear

flowchart TB subgraph Host["Host (service user)"] subgraph Server["Codumentor server process (FastAPI / asyncio)"] API["API + Auth"] Engine["Agent engine<br/>(turns = asyncio tasks)"] Jobs["Job queue + scheduler"] Stores[("SQLite + vector DB")] end subgraph Sandboxed["Sandboxed child processes (bubblewrap)"] Shell["Agent shell commands"] ExtAgent["External coding agents<br/>(tier 1)"] Office["Office document workers<br/>(.xlsx/.docx/.pptx parsing)"] Setup["Project setup scripts"] MCP["MCP servers<br/>(local stdio)"] end Chrome["Headless Chrome<br/>(host-direct; Chrome's own sandbox<br/>if the binary is profiled)"] subgraph HostSide["Host-side helpers (fixed argv, hardened)"] Git["git push/fetch"] SVN["svn client"] DB["DB CLIs (psql, mysql, ...)"] Tunnel["SSH tunnel to LLM host"] end end LLM["LLM endpoint<br/>(OpenAI-compatible, configured URL)"] Engine --> Sandboxed Engine --> Chrome Engine --> HostSide Server -->|HTTPS| LLM

Trust model

Trusted (runs with server privileges)Untrusted (confined to a sandbox)
Codumentor's own codeLLM output, whenever it is executed (shell commands, build steps, code run by external agents)
Operator configuration (codumentor.yaml)Repository content, when executed (project setup scripts, builds, tests)
Enabled plugins (in-process, unsandboxed — enable only reviewed plugins)User-uploaded office documents (parsed only inside a sandboxed worker)
Operator-configured MCP server commands (you choose which binary runs)The MCP server process — it acts on model-authored tool arguments, so it runs in a sandbox of its own (see MCP servers)
Fetched web content and model-generated diagrams (rendered in headless Chrome — sandboxed only if that binary has an AppArmor userns grant; see Browser rendering)
Configured repo URLs, DB connections, LLM endpoints

Two layers apply to agent-initiated execution, and they are independent:

  1. Approval — by default (shell_always_allow: false) each shell command requires explicit user approval before it runs. Approval gating is enforced in the tool runtime, before any process is spawned.
  2. Sandboxing — once approved, the command still executes inside the sandbox described below. Approval limits what runs; the sandbox limits what it can reach.

The execution sandbox

Sandboxing is provided by the workspace_isolation plugin using bubblewrap (bwrap), the unprivileged container tool also used by Flatpak. It requires Linux and a bwrap build with overlay support — see Plugins in Production → Workspace isolation prerequisites.

When the plugin is active, one policy covers the whole product. The following all run inside the same sandbox construction:

Sandbox properties

Each conversation gets its own sandbox workspace. Every sandboxed process launch has:

DimensionProperty
Filesystem — system/usr, /bin, /lib, /lib64, /etc bind-mounted read-only. /var, /opt, /root, /srv and the host's real /home are not mounted and do not exist inside the sandbox.
Filesystem — repositoriesMounted at /workspace as a kernel overlayfs: the real repositories are the read-only lower layer; all writes go to a per-conversation copy-on-write upper layer on the host. The agent can freely modify "its" repo without touching the real checkout.
Filesystem — home & tmpA synthetic per-session HOME and /tmp (persistent per session by default, configurable to per-invocation tmpfs). The host home directory is never visible.
NetworkNo network by default (--unshare-net, own empty network namespace). See "Network policy" below.
ProcessesOwn PID namespace (--unshare-pid) — sandboxed code cannot see or signal host processes. Own IPC namespace. --die-with-parent ensures no sandbox outlives the server.
System callsA seccomp-BPF syscall filter (--seccomp) blocks the kernel-escape primitives that namespaces alone don't — nested user namespaces (clone/unshare with CLONE_NEWUSER, clone3), mount/pivot_root, ptrace, bpf, perf_event_open, the kernel keyrings, userfaultfd, open_by_handle_at, kexec, io_uring, and the TIOCSTI/TIOCLINUX terminal-injection ioctls. Configurable via sandbox.seccomp (default/log/enforce/off); ships audit-only by default (see "System-call filtering" below).
EnvironmentCleared and rebuilt from a minimal baseline (PATH, HOME, USER, LANG, TERM plus non-interactive git/ssh settings). Server secrets, API keys, and host environment never leak in. Additional variables must be allow-listed by name (sandbox.inherit_env_vars).
File descriptorsAll inherited descriptors are marked close-on-exec before each spawn, so sandboxed processes cannot inherit server sockets or database handles.
Time limitsShell commands are bounded by the configured shell_timeout (hard cap 15 minutes); on expiry the whole process group is killed. External agents (tier 1) additionally get a memory cap (default 2 GiB via user-level systemd-run) and a wall-clock budget.

One property to note for your platform-security review: bubblewrap runs unprivileged, so the process inside the sandbox has the same UID as the Codumentor service user — there is no user-ID separation between server and sandbox. The isolation is namespace- and mount-based: the sandboxed process simply cannot see the server's data directories, host credentials, network, or other processes. This is why we recommend running Codumentor under a dedicated, minimally-privileged service account (see Security): the service user's own privileges are the outer bound of what a sandbox escape could reach.

MCP servers: a long-lived sandbox with different defaults

A local (stdio) MCP server is not per-conversation — one process is started per configured server and shared by every conversation — so it gets its own sandbox rather than borrowing a conversation's. It is confined by default whenever workspace_isolation is enabled, and the profile is deliberately tuned for what these servers are:

DimensionMCP serverAgent shell (for comparison)
NetworkShared by default — most MCP servers exist to call a remote API. Set network: false per server to isolate.No network
Repositories/workspace, read-only (configurable to read-write or hidden)/workspace, copy-on-write overlay
HOMEA private persistent directory per server under ~/.codumentor/mcp-homes/Per-session, discarded on TTL
Processes, syscalls, environmentSame as the table above: own PID/IPC namespace, --die-with-parent, cleared environment, the configured seccomp filter

The exposures this closes are the host credentials in the real $HOME (~/.ssh, ~/.gnupg) and the ability to signal the Codumentor process — neither of which the network namespace was protecting. Per-server configuration, including the mode: off exception for a server that cannot be confined, is documented in the MCP plugin reference.

Overlay writes and their lifetime

Sandbox writes are not silently discarded: the copy-on-write upper layer persists on the host (under ~/.codumentor/workspaces by default) so a conversation can be resumed and its changes reviewed, diffed, or pushed through the controlled push gateway. Upper layers are garbage-collected on a TTL (default 30 days from last use, configurable) and are readable only by the service user. Include this directory in your data-protection and retention planning alongside data/ — it can contain agent-modified copies of repository files.

Network policy

The default is no network inside the sandbox. Consequences and options:

System-call filtering

Namespaces limit what sandboxed code can see; a seccomp-BPF filter limits which kernel entry points it can call — the layer that addresses the kernel local-privilege-escalation escape class (nested user namespaces, io_uring, bpf(), keyctl, userfaultfd, …) that namespaces and same-UID separation don't. bwrap applies the filter after its own setup, so it constrains the sandboxed payload and its descendants but never bwrap itself. The filter is a blocklist (not an allowlist), so ordinary build/test toolchains are unaffected — only the specific escape primitives are refused.

The filter is shipped as a pre-generated BPF blob per CPU architecture (so the product carries no libseccomp runtime dependency), and covers the compat ABIs (x32/i386 on x86-64) to close the multilib bypass. sandbox.seccomp controls it:

Loading is fail-closed: if a filter is requested but its blob can't be loaded (unsupported architecture, broken build), the sandbox is disabled (tools return a clear error) rather than run unfiltered.

Fail-closed guarantees

The sandbox is designed so that degradation disables capability rather than weakening isolation:

If workspace_isolation is not enabled at all, shell commands execute directly on the host as the service user, gated only by the approval flow. For any multi-user or production deployment — certainly for a regulated environment — enable workspace_isolation; the Self-Hosting Walkthrough treats it as part of the standard install.

Browser rendering

The fetch_urls and diagram-rendering features drive a headless Chrome/Chromium instance — a separate OS process rendering untrusted web content. This process runs host-direct, outside the workspace_isolation bwrap, so Chrome's own multi-process sandbox is the only containment layer it has.

Codumentor always attempts to launch with that sandbox enabled, and gives it up only after an observed launch failure — at which point it logs a warning naming the likely cause and retries once with --no-sandbox. The verdict is remembered for the process, so the failed attempt costs a fraction of a second, once. CODUMENTOR_CHROME_SANDBOX=1 forces the sandbox and turns a failure into a hard error instead of a fallback; =0 skips the attempt entirely.

Know which browser you are running. On Ubuntu 24.04+ the apparmor_restrict_unprivileged_userns=1 default means Chrome's sandbox only works for a binary whose AppArmor profile grants userns. The apparmor package ships such profiles for the system packages (/etc/apparmor.d/chrome/opt/google/chrome/chrome, /etc/apparmor.d/chromium/usr/lib/@{chromium}/@{chromium}), so a distro Chrome/Chromium is sandboxed out of the box. A Playwright-managed Chromium — which is what the mermaid_render / webretrieve install instructions produce, and which find_chrome() prefers — lives under ~/.cache/ms-playwright/ and matches no shipped profile, so its sandbox cannot start and Codumentor falls back to --no-sandbox. See Browser sandbox on Ubuntu 24.04+ for the two ways to fix that.

Check which you are getting: grep "Chrome sandbox" <instance>/logs/codumentor.log — a retrying with --no-sandbox warning means agent-controlled content is rendering unsandboxed.

Host-side subprocesses (by design)

A small set of helpers runs outside the sandbox, because their job is precisely what the sandbox removes: host network access, real-repository writes, or operator credentials. None of them execute agent-controlled code — they transport agent-produced content over a fixed command line. All of them use list-form argv (no shell interpretation, so no shell injection surface):

HelperPurposeHardening
Push gateway (scm_push)Pushes agent commits from the overlay to the real remoteGit hooks disabled (core.hooksPath=/dev/null), credential helpers disabled, file:// protocol disabled, scrubbed environment, credentials passed via 0600 files (never argv), timeout, output redaction
Memory storagePublishes and commits the agent's knowledge-base entries to the real KB repo, then pushesThe memory curator itself runs sandboxed, in a disposable copy of the KB repo; the host then copies out only files under the KB's own directories (everything else is discarded and logged) and commits them in-process. No model-issued command touches the real repo.
SVN clientTalks to the operator-configured SVN serverFixed argv; per-conversation branch switches still run inside the sandbox
Database CLIs (psql, mysql, ...)Connects to operator-configured databasesSQL travels on stdin, never argv; read-only guard at the tool layer
Ingestion / snapshots / SSH tunnelClone/fetch configured repos, snapshot workspaces, tunnel to the LLM hostOperator-trusted inputs only (configured URLs, fixed argv); these run outside any conversation, so there is no agent influence to confine

The dividing rule, applied to all new code: any subprocess that executes or parses agent- or LLM-influenced content must be sandboxed; subprocesses that only transport content over a trusted, fixed-argv path may run host-side.

Network egress map

By design, the server process initiates outbound connections only to endpoints named in the operator's configuration:

There is no telemetry or "phone home" traffic: an air-gapped deployment with a local LLM server makes no external connections at all. Sandboxed agent code has no network unless you configure otherwise, so the egress list above is also the complete data-exfiltration surface to control at the firewall.

Data at rest

Deployment prerequisites and recommendations

For the full checklist see Security; the architecture-relevant items are:

  1. Linux host with bubblewrap ≥ 0.8.0 with overlay support. On current distros the package is sufficient (apt install bubblewrap); the bundled setup script builds from source only if it is not. On Ubuntu 24.04+ unprivileged user namespaces need userns allowed for the bwrap binary — the apparmor package's own bwrap-userns-restrict profile already covers /usr/bin/bwrap, so do not add a second profile named bwrap. Verify with workspace_isolation/setup/check.sh.
  2. Enable workspace_isolation and leave its defaults in place: network isolation on, minimal environment mode, external-agent tier on auto.
  3. Run the server as a dedicated service account with no privileges beyond its data directories and the configured repositories — this account is the outer privilege boundary for both the server and the sandboxes.
  4. Keep the API on loopback behind a reverse proxy with TLS; authenticate via your directory (LDAP) and set auth.jwt_secret from an environment variable.
  5. Treat plugins and MCP server commands as trusted code — they run in-process. Enable only what you have reviewed.

See Also