WeaveScope

Sandboxes

Sandboxes let an agent work with files and run shell commands without giving it direct access to your application host. In BeamWeaver, a sandbox is a BeamWeaver.Sandbox implementation adapted into the agent filesystem through BeamWeaver.Filesystem.Sandbox.

When a sandbox-backed filesystem is configured, the agent can use:

  • ls, read_file, write_file, edit_file, glob, and grep

  • execute for shell commands

  • application-side file transfer with upload_files and download_files

Why Use Sandboxes

Use a sandbox when an agent needs to:

  • create, edit, and inspect code or data files,

  • run package managers, tests, compilers, scripts, or CLIs,

  • execute commands that should not run directly on the host,

  • produce artifacts that your application retrieves after the run.

Sandboxes reduce blast radius. They do not make prompt injection harmless. An attacker who controls context can still convince the agent to run commands inside the sandbox, read sandbox files, or exfiltrate sandbox-accessible data if network access exists.

Basic Usage

Create a BeamWeaver.Sandbox, adapt it into a filesystem, and pass it to the agent:

alias BeamWeaver.Agent
alias BeamWeaver.Core.Message
alias BeamWeaver.Filesystem
alias BeamWeaver.Sandbox

sandbox = Sandbox.local(root: "/tmp/my-agent-workspace")

filesystem =
  Filesystem.Sandbox.new(sandbox: sandbox)

{:ok, agent} =
  Agent.build(
    model: "openai:gpt-5.4",
    filesystem: filesystem,
    system_prompt: "You are a coding assistant with sandboxed shell access."
  )

{:ok, _state} =
  Agent.invoke(agent, %{
    messages: [Message.user("Create a hello world script and run it.")]
  })

Because BeamWeaver.Filesystem.Sandbox implements BeamWeaver.Filesystem.Executable, the filesystem middleware includes the execute tool automatically.

Docker Sandbox

BeamWeaver.Sandbox.Docker keeps the BeamWeaver runtime outside the container and runs filesystem and command operations inside Docker:

alias BeamWeaver.Filesystem
alias BeamWeaver.Sandbox

sandbox =
  Sandbox.Docker.new(
    image: "docker.io/library/python:3.11-slim",
    root: "/workspace",
    max_output_bytes: 100_000,
    runtime: "runsc",
    mounts: [
      %{source: "/srv/my-app", target: "/workspace", read_only: false},
      %{source: "/srv/reference", target: "/reference", read_only: true}
    ],
    read_only: true,
    cap_drop: ["ALL"],
    security_opts: ["no-new-privileges"],
    tmpfs: ["/tmp:rw,noexec,nosuid,size=64m"]
  )

filesystem =
  Filesystem.Sandbox.new(sandbox: sandbox)

The Docker adapter starts a container on first use with:

  • --network none

  • --cpus 1

  • --memory 1g

  • --pids-limit 256

  • --workdir set to the sandbox root

Applications may override those resource limits and add canonical host bind mounts, a read-only container root, dropped capabilities, security options, and temporary filesystems. These are adapter options, not a claim that Docker alone is a complete containment boundary. Call Sandbox.Docker.stop/1 when an application owns the sandbox lifecycle explicitly.

Plain Docker is still not a complete security boundary for hostile workloads. For production, use a hardened runtime such as gVisor or Kata when available, or implement a remote sandbox adapter backed by your infrastructure provider.

Direct Sandbox API

You can call the sandbox directly from application code:

alias BeamWeaver.Sandbox

sandbox = Sandbox.local(root: "/tmp/my-agent-workspace")

%Sandbox.WriteResult{error: nil} =
  Sandbox.write(sandbox, "/src/index.py", "print('hello')\n")

%Sandbox.ExecuteResult{exit_code: 0, output: output} =
  Sandbox.execute(sandbox, "python /tmp/my-agent-workspace/src/index.py")

%Sandbox.ReadResult{file_data: %{"content" => content, "encoding" => "utf-8"}} =
  Sandbox.read(sandbox, "/src/index.py")

For agent-facing use, prefer going through BeamWeaver.Filesystem.Sandbox so the same virtual paths, file tools, and permissions pipeline are used.

File Access Planes

There are two distinct file paths into a sandbox:

Plane Who uses it BeamWeaver surface Use
Agent filesystem tools The model read_file, write_file, edit_file, ls, glob, grep, execute Work performed during the agent run.
File transfer API Application code BeamWeaver.Sandbox.upload_files/3, download_files/3, or BeamWeaver.Filesystem.upload_files/3, download_files/3 Seed inputs before a run and retrieve artifacts after a run.

Seed files before invocation:

alias BeamWeaver.Filesystem

filesystem = Filesystem.Sandbox.new(sandbox: sandbox)

[
  %Filesystem.UploadResult{path: "/src/index.py", error: nil},
  %Filesystem.UploadResult{path: "/pyproject.toml", error: nil}
] =
  Filesystem.upload_files(filesystem, [
    {"/src/index.py", "print('hello')\n"},
    {"/pyproject.toml", "[project]\nname = 'my-app'\n"}
  ])

Retrieve artifacts after the run:

[%Filesystem.DownloadResult{path: "/src/index.py", content: content, error: nil}] =
  Filesystem.download_files(filesystem, ["/src/index.py"])

Use absolute virtual paths. BeamWeaver rejects path traversal such as .., ~, Windows drive paths, and null bytes.

Command Execution

execute returns an exit code, output, optional error, truncation flag, and redacted native metadata:

%BeamWeaver.Filesystem.Executable.ExecuteResult{
  exit_code: 0,
  output: "Python 3.11.8\n",
  error: nil,
  truncated: false,
  metadata: %{command_id: command_id}
} =
  BeamWeaver.Filesystem.Executable.execute(filesystem, "python --version")

Timeouts must be integers from 1 to 3600 seconds:

BeamWeaver.Filesystem.Executable.execute(
  filesystem,
  "pytest",
  timeout: 120
)

BeamWeaver.Sandbox.Local and BeamWeaver.Sandbox.Docker truncate large command output according to max_output_bytes. BeamWeaver.Filesystem.Sandbox exposes constants for the current adapter limits used by the filesystem bridge:

BeamWeaver.Filesystem.Sandbox.max_binary_bytes()
BeamWeaver.Filesystem.Sandbox.max_output_bytes()

For a narrower command surface, use BeamWeaver.Agent.Middleware.ShellTool with an allow-list policy instead of exposing a general-purpose execute tool.

Execution Metadata And Telemetry

BeamWeaver.Sandbox.execute/3 adds native execution metadata without changing successful return semantics. Local adapters get command IDs and timeout metadata automatically. Remote adapters can add provider-specific facts such as:

  • :provider_id

  • :sandbox_id

  • :snapshot_id

  • :command_id

  • :reconnect_count

  • :timeout_ms

  • :exit_code

  • :retryable

  • :raw_status

  • :output_unavailable

Credential-shaped fields such as API keys, tokens, private keys, signed URLs, and secret mount/env fields are redacted before metadata reaches telemetry or tracing.

Sandbox execution emits native telemetry:

Event Use
[:beam_weaver, :sandbox, :execute, :start] Command execution started.
[:beam_weaver, :sandbox, :execute, :stop] Command execution completed.
[:beam_weaver, :sandbox, :execute, :timeout] Backend reported a timeout-shaped result.
[:beam_weaver, :sandbox, :execute, :exception] The adapter raised or exited before returning a result.

Provider Registry

Use BeamWeaver.Sandbox.Registry when sandbox providers come from runtime configuration:

alias BeamWeaver.Sandbox.Registry

{:ok, registry} =
  Registry.new(
    providers: [
      %{
        id: :remote,
        module: MyApp.RemoteSandboxProvider,
        config: %{region: "us-east-1", api_key: System.fetch_env!("SANDBOX_API_KEY")},
        capabilities: %{sandbox_id: true, snapshot: true, mounts: true}
      }
    ]
  )

{:ok, sandbox} =
  Registry.build(registry, :remote,
    sandbox_id: "sbx_123",
    snapshot_id: "snap_456",
    mounts: [%{name: "repo", path: "/workspace"}]
  )

Provider modules implement c:BeamWeaver.Sandbox.Provider.build/2 and return a normal sandbox backend. The registry validates duplicate IDs, provider modules, unsupported lifecycle options, and whether the built value implements BeamWeaver.Sandbox.

Lifecycle And Scoping

BeamWeaver does not manage hosted sandbox lifecycles for you. Your application chooses how sandboxes are created, reused, and cleaned up.

Common patterns:

Scope Pattern Use
Per invocation Create a sandbox before invoking the agent, tear it down afterward. One-off tasks and strongest cleanup guarantees.
Per thread Store a sandbox ID by thread_id and reuse it for follow-up turns. Coding sessions where files and dependencies should persist within one conversation.
Per agent or assistant Store a sandbox ID by agent name or assistant ID. Shared workspaces, cached dependencies, or long-lived coding environments.

For per-thread reuse, resolve the sandbox from trusted runtime config or application state before building or invoking the agent:

thread_id = get_in(config, ["configurable", "thread_id"])
sandbox = MyApp.Sandboxes.get_or_create_for_thread!(thread_id)

{:ok, agent} =
  BeamWeaver.Agent.build(
    model: "openai:gpt-5.4",
    filesystem: BeamWeaver.Filesystem.Sandbox.new(sandbox: sandbox)
  )

Always configure cleanup. Docker containers, VM instances, and remote devboxes consume resources until they are stopped or expire through provider TTL.

Integration Patterns

Agent Outside Sandbox

The BeamWeaver application runs on your server. The agent receives sandbox tools and uses provider APIs through a BeamWeaver.Sandbox adapter.

This is the recommended BeamWeaver pattern because:

  • model and application credentials stay outside the sandbox,

  • agent logic can change without rebuilding sandbox images,

  • sandbox failures do not erase the graph or agent state,

  • multiple sandboxes can be used by different agents or tasks.

Agent Inside Sandbox

You can also run the whole BeamWeaver application inside a container or remote environment and talk to it over HTTP, RPC, or a queue. That pattern can mirror local development, but it usually means secrets and agent state live inside the sandbox. Treat that as a separate deployment architecture rather than a BeamWeaver filesystem adapter.

Custom Sandbox Adapters

Implement BeamWeaver.Sandbox to support another provider:

defmodule MyApp.RemoteSandbox do
  @behaviour BeamWeaver.Sandbox

  alias BeamWeaver.Sandbox

  defstruct [:id, :client]

  def execute(%__MODULE__{} = sandbox, command, opts) do
    case MyProvider.run_command(sandbox.client, sandbox.id, command, opts) do
      {:ok, %{exit_code: code, output: output, command_id: command_id}} ->
        %Sandbox.ExecuteResult{
          exit_code: code,
          output: output,
          metadata: %{
            provider_id: "my_provider",
            sandbox_id: sandbox.id,
            command_id: command_id,
            raw_status: "finished"
          }
        }

      {:error, reason} ->
        %Sandbox.ExecuteResult{
          exit_code: nil,
          output: "",
          error: to_string(reason),
          metadata: %{provider_id: "my_provider", sandbox_id: sandbox.id}
        }
    end
  end

  def upload_files(%__MODULE__{} = sandbox, files, _opts) do
    Enum.map(files, fn {path, content} ->
      case MyProvider.upload(sandbox.client, sandbox.id, path, IO.iodata_to_binary(content)) do
        :ok -> %Sandbox.UploadResult{path: path}
        {:error, reason} -> %Sandbox.UploadResult{path: path, error: to_string(reason)}
      end
    end)
  end

  # Implement write/read/edit/ls/glob/grep/download_files as provider-native
  # calls or by composing them from execute.
end

Then adapt it into the agent filesystem:

filesystem =
  BeamWeaver.Filesystem.Sandbox.new(
    sandbox: %MyApp.RemoteSandbox{id: sandbox_id, client: client}
  )

Security Considerations

Sandboxes protect the host from direct filesystem and shell access, but they do not protect secrets placed inside the sandbox from the agent.

Do not put API keys, database credentials, cloud tokens, or production secrets inside a sandbox. Prefer host-side tools that perform authenticated operations without revealing credentials to the model or sandbox.

If a sandbox must access external services:

  • use short-lived, least-privilege credentials,

  • block or restrict outbound network access where possible,

  • add human-in-the-loop approval before expensive or risky commands,

  • audit command output and generated artifacts before using them,

  • treat everything produced inside the sandbox as untrusted input.

BeamWeaver.Filesystem.Permission rules apply to model-visible file tools, but they do not restrict execute. A shell command can read or write files through the sandbox environment directly. Enforce command policy in the sandbox adapter, with ShellTool, through provider network/filesystem controls, or with human review.