Host filesystem mounts for sandboxed Python execution.
A mount maps a virtual path inside the sandbox (e.g. /data) to a real
host directory (e.g. /var/lib/myapp/data) with a configurable access
policy. Path canonicalisation, boundary checks, and symlink-escape
detection are enforced by upstream monty regardless of mode.
Mounts are stateful — overlay writes and write_bytes_used accumulate
on the mount across runs. Discard accumulated state by constructing a
fresh mount.
Modes
:read_only— reads pass through to the host; writes raisePermissionError.:read_write— reads and writes both hit the real disk. Footgun: sandbox code can modify real host files. Use with care.:overlay— reads fall through to the host; writes are captured in-memory; the host directory is untouched.
Examples
# Read-only access to a data directory
{:ok, mounts} = ExMonty.Mount.new()
{:ok, mounts} = ExMonty.Mount.add(mounts, "/data", "/var/lib/myapp/data", :read_only)
ExMonty.Sandbox.run(
"""
from pathlib import Path
Path("/data/users.csv").read_text()
""",
mounts: mounts
)
# Pipe-friendly with `add!/5`
mounts =
ExMonty.Mount.new!()
|> ExMonty.Mount.add!("/data", "/var/lib/myapp/data", :read_only)
|> ExMonty.Mount.add!("/scratch", "/tmp/sandbox-scratch", :overlay)
|> ExMonty.Mount.add!("/output", "/var/lib/myapp/output", :read_write,
write_bytes_limit: 10_000_000)Lease lifecycle
Sandbox.run checks out a Lease for the duration of the run and
releases it via try/after. Most users never touch checkout/1 /
release/1 directly. While a lease is alive against a mount,
add/5 and concurrent checkout/1 return {:error, :mount_in_use}.
count/1 and list/1 keep working — they read from a side-channel
that survives a lease.
Summary
Functions
Adds a mount point. Returns {:ok, mount} on success.
Bang version of add/5. Raises on error.
Takes a lease for the duration of a single sandbox run. While the lease
is alive, add/5 against this mount returns {:error, :mount_in_use}.
Returns the number of configured mounts.
Returns the configured mounts as a list of maps.
Creates an empty mount table.
Releases a lease back to its source mount. Idempotent — calling on an
already-released lease returns :ok.
Types
@type add_error() :: :invalid_virtual_path | :invalid_mode | :host_path_not_found | :host_path_not_directory | :host_path_canonicalize_failed | :mount_in_use | {:already_mounted, virtual :: String.t()}
@type add_opts() :: [{:write_bytes_limit, pos_integer()}]
@type list_entry() :: %{ virtual: String.t(), host: String.t(), mode: mode(), write_bytes_limit: pos_integer() | :unlimited }
@type mode() :: :read_only | :read_write | :overlay
@opaque t()
Functions
Adds a mount point. Returns {:ok, mount} on success.
See module doc for mode semantics. add_opts accepts:
:write_bytes_limit— cumulative cap (in bytes) on writes against this mount across all runs. Counter does not reset between runs; construct a fresh mount to reset.
Bang version of add/5. Raises on error.
@spec checkout(t()) :: {:ok, ExMonty.Mount.Lease.t()} | {:error, :mount_in_use}
Takes a lease for the duration of a single sandbox run. While the lease
is alive, add/5 against this mount returns {:error, :mount_in_use}.
Most users don't call this directly — Sandbox.run handles it.
@spec count(t()) :: non_neg_integer()
Returns the number of configured mounts.
Permissive under lease — reads from the descriptor side-channel without contending with an active run.
@spec list(t()) :: [list_entry()]
Returns the configured mounts as a list of maps.
Permissive under lease (v1 returns descriptor-only state — no live
write_bytes_used until upstream exposes a public getter).
@spec new() :: {:ok, t()}
Creates an empty mount table.
@spec new!() :: t()
Bang version of new/0.
@spec release(ExMonty.Mount.Lease.t()) :: :ok
Releases a lease back to its source mount. Idempotent — calling on an
already-released lease returns :ok.