SuperPi.Env.SandboxRunner (super_pi v0.1.0)

Copy Markdown View Source

4-Layer Safe Tool & Process Execution Sandbox (REQ-SAFE-01..REQ-SAFE-03, REQ-PBT-05).

Guarantees that tool execution NEVER crashes the calling :gen_statem conversation process or the BEAM VM:

  • Layer 1 (BEAM Process & Heap Isolation): Runs every tool function inside an unlinked Task.Supervisor.async_nolink(SuperPi.ToolTaskSupervisor, ...) process with a configurable :max_heap_size flag (%{size: max_heap_words, kill: true, error_logger: true}). If a tool raises an exception, calls exit/1, times out, or exceeds :max_heap_size, the supervisor catches the :DOWN message and returns {:error, %{type: ..., reason: ...}}.
  • Layer 2 (Backpressure OS Process Execution + 4 MiB Spill Files): Executes external commands with streaming output capture and automatically spills outputs exceeding 4 MiB (4_194_304 bytes) to temporary files while returning a bounded preview.

Summary

Functions

Executes an external OS command safely using Exile streaming with backpressure and 4 MiB spill-to-disk protection (REQ-SAFE-02).

Runs fun.() inside an unlinked supervised task with :max_heap_size enforcement and a strict timeout. Never raises or crashes the caller.

Starts fun as an unlinked task under SuperPi.ToolTaskSupervisor that does not outlive its caller: a watcher process kills the task when the caller exits. A task that runs an OS command sends {:os_pid, pid} to the watcher (an arity-1 fun gets the watcher pid), and the watcher kills that process tree first. The caller receives {ref, result} or {:DOWN, ref, ...}, as with Task.Supervisor.async_nolink/2.

Functions

exec_command(cmd, args \\ [], opts \\ [])

@spec exec_command(String.t(), [String.t()], keyword()) ::
  {:ok,
   %{exit_code: integer(), output: String.t(), spilled_path: String.t() | nil}}
  | {:error, map()}

Executes an external OS command safely using Exile streaming with backpressure and 4 MiB spill-to-disk protection (REQ-SAFE-02).

run_isolated(fun, opts \\ [])

@spec run_isolated((-> term()), keyword()) :: {:ok, term()} | {:error, map()}

Runs fun.() inside an unlinked supervised task with :max_heap_size enforcement and a strict timeout. Never raises or crashes the caller.

start_owned(fun)

@spec start_owned((-> term()) | (pid() -> term())) :: Task.t()

Starts fun as an unlinked task under SuperPi.ToolTaskSupervisor that does not outlive its caller: a watcher process kills the task when the caller exits. A task that runs an OS command sends {:os_pid, pid} to the watcher (an arity-1 fun gets the watcher pid), and the watcher kills that process tree first. The caller receives {ref, result} or {:DOWN, ref, ...}, as with Task.Supervisor.async_nolink/2.

Nested tasks chain: cancelling a conversation kills its tool task, which ends the sandbox task under it, which ends the command and its OS processes.