Preflight Checklist
Problem statement
MOLTIS provides [tools.fs].deny_paths (glob list) that blocks native fs tools (Read, Write, Edit, Glob, Grep) from accessing sensitive paths like /data/secrets/**. This is enforced in FsPathPolicy at crates/tools/src/fs/shared.rs:324-398.
The container-based sandbox (Docker / Podman / apple-container) provides equivalent protection for exec via namespace isolation — host paths like /data/secrets/ simply don't exist inside the container. This is the "right" layer for real isolation.
Gap: operators running without a container runtime (Fly.io containers with no DinD, bare metal, restricted environments, corporate laptops without Docker) land on NoSandbox or restricted-host backends, which provide zero filesystem isolation. For these deployments, exec has no path policy — exec(command="cat /data/secrets/api_key") succeeds regardless of [tools.fs].deny_paths.
In our case: Fly.io container runs no container runtime → auto_detect_backend lands on restricted-host → exec is wide open to filesystem access beyond what tools.fs.deny_paths protects at the fs-tool layer. OS-level permissions provide partial cover, but operators configuring via MOLTIS config alone have no way to express "block exec access to these paths."
Proposed solution
Add fs_deny_paths to [tools.exec] (glob list), evaluated pre-spawn:
[tools.exec]
fs_deny_paths = [\"/data/secrets/**\", \"**/.env*\", \"**/.ssh/**\", \"/etc/shadow\"]
on_opaque = \"deny\" # \"deny\" | \"warn\" | \"allow\" — behavior when command contains $(), backticks, base64, variable expansion
Implementation outline (mirrors existing FsPathPolicy pattern in fs/shared.rs:324):
- Parse the command string with a real shell-aware tokenizer (e.g.,
shell-words crate)
- Extract path-like tokens: absolute paths (
/...), relative paths (./..., ../...), home-relative (~/...), and anything containing / without spaces
- Extract redirect targets:
>, >>, <, 2>, &>
- Canonicalize with
Path::canonicalize to resolve .., ., symlinks
- Match canonicalized paths against the
fs_deny_paths glob list
- If a path matches a deny pattern: return
Err(\"exec denied: path matches fs_deny_paths\")
- If the command contains opaque constructs (
$(...), backticks, <(...), ${VAR}, base64 pipe patterns): honor on_opaque — deny rejects, warn logs and proceeds, allow (default for back-compat) proceeds silently
Insertion point: exec.rs:541 — right before the existing approval check, apply fs_deny_paths first.
Honest caveat — we should acknowledge in the docs: argv-based path scanning catches ~85% of naive accesses (cat /secret, tail /.env, > /etc/passwd) but fails on obfuscation (cat $(echo L2RhdGEv... | base64 -d), VAR=/secret cat \\$VAR). This is not a security guarantee against adversarial prompt injection — it's defense-in-depth against LLM-spontaneous exfil and a steering mechanism for operators.
For security-critical isolation, the sandbox-layer filesystem namespace is still the correct answer (documented clearly). This feature fills the gap for the substantial class of deployments where that isn't available.
Alternatives considered
- Shell-free exec mode (
exec.shell.enabled = false, accept only argv arrays, forbid sh -c): cleaner but breaks too much legitimate functionality (pipes, redirects, git log | head, heredocs). LLM training bias toward shell-string invocation makes this a constant friction. Trivially bypassable if bash/sh remains allowlisted anyway.
- Rely on OS permissions (chmod secrets to root-only): requires running MOLTIS as non-root; many deployments don't. Also doesn't help for paths MOLTIS legitimately needs to read but not via exec (e.g., session state the agent shouldn't corrupt).
- Wait for sandbox runtime availability (require Docker/Podman): excludes a large class of deployments (Fly.io, Deno Deploy, most PaaS).
Category
Sandbox
How important is this to your workflow?
Medium — would be very helpful
Additional context
Mirrors existing pattern: the FsPathPolicy implementation in crates/tools/src/fs/shared.rs:324-398 is the exact structure to parallel. Small, well-bounded patch surface.
Relation to other issues: #814 (env-var prefix bypass) and #815 (strict allowlist / SAFE_BINS opt-out) are complementary but orthogonal — each addresses a different exec-layer gap. All three together give container-less operators meaningful defense-in-depth on exec.
Not a substitute for sandbox. Documentation should steer operators toward container-based sandbox when available; fs_deny_paths is the fallback for when it isn't.
Preflight Checklist
Problem statement
MOLTIS provides
[tools.fs].deny_paths(glob list) that blocks native fs tools (Read,Write,Edit,Glob,Grep) from accessing sensitive paths like/data/secrets/**. This is enforced inFsPathPolicyatcrates/tools/src/fs/shared.rs:324-398.The container-based sandbox (Docker / Podman / apple-container) provides equivalent protection for
execvia namespace isolation — host paths like/data/secrets/simply don't exist inside the container. This is the "right" layer for real isolation.Gap: operators running without a container runtime (Fly.io containers with no DinD, bare metal, restricted environments, corporate laptops without Docker) land on
NoSandboxorrestricted-hostbackends, which provide zero filesystem isolation. For these deployments,exechas no path policy —exec(command="cat /data/secrets/api_key")succeeds regardless of[tools.fs].deny_paths.In our case: Fly.io container runs no container runtime →
auto_detect_backendlands onrestricted-host→ exec is wide open to filesystem access beyond whattools.fs.deny_pathsprotects at the fs-tool layer. OS-level permissions provide partial cover, but operators configuring via MOLTIS config alone have no way to express "block exec access to these paths."Proposed solution
Add
fs_deny_pathsto[tools.exec](glob list), evaluated pre-spawn:Implementation outline (mirrors existing
FsPathPolicypattern infs/shared.rs:324):shell-wordscrate)/...), relative paths (./...,../...), home-relative (~/...), and anything containing/without spaces>,>>,<,2>,&>Path::canonicalizeto resolve..,., symlinksfs_deny_pathsglob listErr(\"exec denied: path matches fs_deny_paths\")$(...), backticks,<(...),${VAR}, base64 pipe patterns): honoron_opaque—denyrejects,warnlogs and proceeds,allow(default for back-compat) proceeds silentlyInsertion point:
exec.rs:541— right before the existing approval check, applyfs_deny_pathsfirst.Honest caveat — we should acknowledge in the docs: argv-based path scanning catches ~85% of naive accesses (
cat /secret,tail /.env,> /etc/passwd) but fails on obfuscation (cat $(echo L2RhdGEv... | base64 -d),VAR=/secret cat \\$VAR). This is not a security guarantee against adversarial prompt injection — it's defense-in-depth against LLM-spontaneous exfil and a steering mechanism for operators.For security-critical isolation, the sandbox-layer filesystem namespace is still the correct answer (documented clearly). This feature fills the gap for the substantial class of deployments where that isn't available.
Alternatives considered
exec.shell.enabled = false, accept only argv arrays, forbidsh -c): cleaner but breaks too much legitimate functionality (pipes, redirects,git log | head, heredocs). LLM training bias toward shell-string invocation makes this a constant friction. Trivially bypassable ifbash/shremains allowlisted anyway.Category
Sandbox
How important is this to your workflow?
Medium — would be very helpful
Additional context
Mirrors existing pattern: the
FsPathPolicyimplementation incrates/tools/src/fs/shared.rs:324-398is the exact structure to parallel. Small, well-bounded patch surface.Relation to other issues: #814 (env-var prefix bypass) and #815 (strict allowlist / SAFE_BINS opt-out) are complementary but orthogonal — each addresses a different exec-layer gap. All three together give container-less operators meaningful defense-in-depth on exec.
Not a substitute for sandbox. Documentation should steer operators toward container-based sandbox when available;
fs_deny_pathsis the fallback for when it isn't.