Skip to content

[Feature]: tools.exec.fs_deny_paths — argv-based path deny for container-less deployments #816

@dmitriikeler

Description

@dmitriikeler

Preflight Checklist

  • I have searched existing requests and this hasn't been proposed yet
  • If this request came from a chat session issue, I included relevant session context and redacted secrets

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

  1. Parse the command string with a real shell-aware tokenizer (e.g., shell-words crate)
  2. Extract path-like tokens: absolute paths (/...), relative paths (./..., ../...), home-relative (~/...), and anything containing / without spaces
  3. Extract redirect targets: >, >>, <, 2>, &>
  4. Canonicalize with Path::canonicalize to resolve .., ., symlinks
  5. Match canonicalized paths against the fs_deny_paths glob list
  6. If a path matches a deny pattern: return Err(\"exec denied: path matches fs_deny_paths\")
  7. If the command contains opaque constructs ($(...), backticks, <(...), ${VAR}, base64 pipe patterns): honor on_opaquedeny 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions