Skip to Content
DocsSecuritySandboxes

Sandboxes

A sandbox wraps a ToolHost to add isolation. The Agent calls through the sandbox, the sandbox enforces its policy, then dispatches to the wrapped host.

SandboxWhat it isolates
NoSandbox (default)nothing. Pass-through
FilesystemSandboxpath-typed tool args (rejects escapes, symlinks resolved)
SubprocessSandboxeach tool call runs in a fresh child Python process
OSSandboxkernel-enforced filesystem + network isolation (Seatbelt / Bubblewrap)

FilesystemSandbox

Validates path-typed arguments. Any path that resolves outside the declared roots. Including via symlink, is rejected before the tool runs.

from loomflow import Agent, tool from loomflow.security import FilesystemSandbox from loomflow.tools import InProcessToolHost @tool def read_file(path: str) -> str: """Read file contents.""" return open(path).read() inner = InProcessToolHost([read_file]) sandbox = FilesystemSandbox(inner, roots=["/Users/me/safe-workspace"]) agent = Agent("...", model="claude-opus-4-7", tools=sandbox)

The sandbox auto-detects path-typed arguments by:

  • Argument name, path, file, directory, filename, dirname, etc.
  • Argument value. Strings containing / or \ (treats them as paths even if not auto-named).

Reject reasons returned to the model:

ERROR: path '/etc/passwd' is outside the allowed roots ERROR: path '../../etc/passwd' resolves outside ['/Users/me/safe-workspace'] ERROR: symlink target '/etc/passwd' is outside the allowed roots

SubprocessSandbox

Runs each tool call in a fresh child Python process.

from loomflow import Agent from loomflow.security import SubprocessSandbox from loomflow.tools import InProcessToolHost inner = InProcessToolHost([heavy_tool]) sandbox = SubprocessSandbox( inner, timeout_seconds=30.0, ) agent = Agent("...", model="claude-opus-4-7", tools=sandbox)

What you get:

  • Process isolation. A tool that crashes (segfault, OOM) takes down its own subprocess, not the agent.
  • Hard timeout. The parent kills the child if it exceeds timeout_seconds; the call returns ToolResult.error_(...) with a clear timeout message.
  • Memory boundary. The child’s heap is independent; large intermediates GC by process exit even if the tool leaks them.

SubprocessSandbox provides process isolation only. For kernel-enforced filesystem and network isolation, use OSSandbox.

Constraints

  • Wrapped host must be InProcessToolHost. MCP / external hosts can’t be sandboxed this way (they’re already a process boundary).
  • Tool function and arguments must be picklable. Module-level functions are fine; closures and locally-defined functions can’t cross the process boundary.

Cost

Spawning a Python subprocess adds per-call latency. Don’t use this for fast tools — the spawn dwarfs the work. Pays off when tools take seconds, can crash, or use a lot of memory.

OSSandbox

Kernel-enforced isolation: each tool call runs in a child process launched under the platform’s OS sandbox — Seatbelt (sandbox-exec) on macOS, Bubblewrap (bwrap) on Linux. The generated profile denies by default and allows only reads, plus writes under the declared roots. Network access is blocked unless allow_network=True.

from loomflow import Agent from loomflow.security import OSSandbox from loomflow.tools import InProcessToolHost inner = InProcessToolHost([heavy_tool]) sandbox = OSSandbox( inner, roots=["/work"], # at least one writable root required allow_network=False, # kernel blocks all network access timeout_seconds=30.0, ) agent = Agent("...", model="claude-opus-4-7", tools=sandbox)

On a platform with no backend (Windows, or Linux without bwrap), OSSandbox degrades to FilesystemSandbox(SubprocessSandbox(inner)) — still process-isolated and path-contained, just not kernel-enforced. The active backend is reported via OSSandbox.mode.

Like SubprocessSandbox, the wrapped host must be InProcessToolHost and tool functions and arguments must be picklable.

Combining sandboxes

Wrap layer-by-layer. Each sandbox accepts the layer below it as its inner host:

from loomflow.security import FilesystemSandbox, SubprocessSandbox from loomflow.tools import InProcessToolHost inner = InProcessToolHost([read_file, write_file, edit_file]) fs = FilesystemSandbox(inner, roots=["/work"]) both = SubprocessSandbox(fs, timeout_seconds=60) agent = Agent("...", tools=both)

The agent sees both; tool calls go agent → SubprocessSandbox → FilesystemSandbox → InProcessToolHost → tool function. Both layers get to inspect / reject before the tool runs.

Custom sandboxes

Implement the Sandbox protocol. Wrap a ToolHost and forward the methods you care about:

from collections.abc import AsyncIterator from loomflow.core.protocols import ToolHost from loomflow.core.types import ToolCall, ToolDef, ToolEvent, ToolResult class MySandbox: def __init__(self, inner: ToolHost): self._inner = inner async def list_tools(self) -> list[ToolDef]: return await self._inner.list_tools() async def call(self, call: ToolCall) -> AsyncIterator[ToolEvent]: # Pre-call check if not self._allowed(call): yield ToolEvent.result(call, ToolResult.error_("blocked")) return # Forward to inner async for event in self._inner.call(call): yield event

For kernel-enforced OS-level isolation, use the built-in OSSandbox rather than rolling your own.

Pick the right tool for the job. Filesystem tools → wrap in FilesystemSandbox. Tools that crash or take long → wrap in SubprocessSandbox. Untrusted code needing kernel-level filesystem and network isolation → OSSandbox. MCP servers → already process-isolated, no sandbox needed. The default NoSandbox is fine for trusted in-proc tools.

Last updated on