ExMonty.Mount (ExMonty v0.5.0)

Copy Markdown View Source

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 raise PermissionError.
  • :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.

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.

Bang version of new/0.

Releases a lease back to its source mount. Idempotent — calling on an already-released lease returns :ok.

Types

add_error()

@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()}

add_opts()

@type add_opts() :: [{:write_bytes_limit, pos_integer()}]

list_entry()

@type list_entry() :: %{
  virtual: String.t(),
  host: String.t(),
  mode: mode(),
  write_bytes_limit: pos_integer() | :unlimited
}

mode()

@type mode() :: :read_only | :read_write | :overlay

t()

@opaque t()

Functions

add(mount, virtual_path, host_path, mode, opts \\ [])

@spec add(t(), String.t(), String.t(), mode(), add_opts()) ::
  {:ok, t()} | {:error, add_error()}

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.

add!(mount, virtual_path, host_path, mode, opts \\ [])

@spec add!(t(), String.t(), String.t(), mode(), add_opts()) :: t()

Bang version of add/5. Raises on error.

checkout(mount)

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

count(mount)

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

list(mount)

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

new()

@spec new() :: {:ok, t()}

Creates an empty mount table.

new!()

@spec new!() :: t()

Bang version of new/0.

release(lease)

@spec release(ExMonty.Mount.Lease.t()) :: :ok

Releases a lease back to its source mount. Idempotent — calling on an already-released lease returns :ok.