Condukt.Sandbox behaviour (Condukt v0.16.5)

Copy Markdown View Source

Filesystem and process-execution capabilities exposed to tools.

A sandbox is a runtime-swappable backend for the operations a tool needs to reach the outside world: read/write/edit files, run commands, glob, and grep. Built-in sandboxes:

  • Condukt.Sandbox.Local runs against the host filesystem and spawns real processes via MuonTrap.
  • Condukt.Sandbox.Virtual (in :condukt_bashkit_nif) runs inside an in-memory virtual filesystem and a Rust-implemented bash interpreter, with no host process spawning by default.

Why a sandbox

Built-in tools (Condukt.Tools.Read, Tools.Write, Tools.Edit, Tools.Bash, Tools.Glob, Tools.Grep) are sandbox-agnostic: they declare one tool name and JSON schema to the LLM and route every primitive call through the active sandbox. This means the same agent definition can run against the host filesystem in development and against an isolated virtual filesystem in production by changing one option at start_link/1.

Configuring

Pass :sandbox at session start (or as the agent module's sandbox/0 callback). Accepted forms:

# module only — uses defaults (Local resolves :cwd to File.cwd!())
sandbox: Condukt.Sandbox.Local

# module + init opts
sandbox: {Condukt.Sandbox.Local, cwd: "/path/to/project"}

# already-initialized struct (advanced)
sandbox: Condukt.Sandbox.new(Condukt.Sandbox.Local, cwd: "/tmp")

When :sandbox is omitted, sessions default to {Condukt.Sandbox.Local, cwd: <:cwd opt or File.cwd!()>} so existing agents continue to behave as they did before sandboxes existed.

Writing a custom sandbox

Implement the Condukt.Sandbox behaviour. init/1 builds the per-session state; shutdown/1 releases it; the rest are I/O primitives. Tools dispatch through this module's facade (Condukt.Sandbox.read/2, .exec/3, etc.) so custom sandboxes work with every built-in tool automatically.

Tool authoring rule

If your tool reads or writes files, or runs subprocesses, route through the Condukt.Sandbox.* facade rather than calling File.*, System.cmd/3, or MuonTrap.cmd/3 directly. Direct calls bypass the sandbox and break the ability to swap one in. Tools that touch unrelated systems (HTTP APIs, databases, in-process state) have nothing to sandbox and are unaffected.

Summary

Callbacks

Replaces the unique occurrence of old_text with new_text. Returns the count of pre-edit occurrences alongside the post-edit content so callers can decide how to surface ambiguity or no-op edits.

Runs a shell command. Options

Returns paths matching pattern. Options

Searches file contents for pattern (regex). Options

Initializes per-session sandbox state.

Mounts a host directory into the sandbox at vfs_path. Sandboxes that have no separate VFS (like Local) should return {:error, :not_supported}.

Reads a file's raw bytes.

Releases any resources held by the sandbox state.

Writes raw bytes to a file, creating parent directories as needed.

Callbacks

edit_file(state, path, old_text, new_text)

@callback edit_file(
  state :: term(),
  path :: binary(),
  old_text :: binary(),
  new_text :: binary()
) ::
  {:ok, %{occurrences: non_neg_integer(), content: binary()}} | {:error, term()}

Replaces the unique occurrence of old_text with new_text. Returns the count of pre-edit occurrences alongside the post-edit content so callers can decide how to surface ambiguity or no-op edits.

exec(state, command, opts)

@callback exec(state :: term(), command :: binary(), opts :: keyword()) ::
  {:ok, %{output: binary(), exit_code: integer()}} | {:error, :timeout | term()}

Runs a shell command. Options:

  • :cwd — working directory
  • :env — list of {key, value} strings to layer onto the base env
  • :timeout — milliseconds before the command is killed

glob(state, pattern, opts)

(optional)
@callback glob(state :: term(), pattern :: binary(), opts :: keyword()) ::
  {:ok, [binary()]} | {:error, term()}

Returns paths matching pattern. Options:

  • :cwd — base directory; pattern is resolved relative to it
  • :limit — maximum number of paths to return

grep(state, pattern, opts)

(optional)
@callback grep(state :: term(), pattern :: binary(), opts :: keyword()) ::
  {:ok, [%{path: binary(), line_number: pos_integer(), line: binary()}]}
  | {:error, term()}

Searches file contents for pattern (regex). Options:

  • :path — directory to search (default: cwd)
  • :glob — glob filter applied to file paths
  • :case_sensitive — defaults to true
  • :limit — maximum matches to return

init(opts)

@callback init(opts :: keyword()) :: {:ok, state :: term()} | {:error, term()}

Initializes per-session sandbox state.

mount(state, host_path, vfs_path)

(optional)
@callback mount(state :: term(), host_path :: binary(), vfs_path :: binary()) ::
  :ok | {:error, :not_supported | term()}

Mounts a host directory into the sandbox at vfs_path. Sandboxes that have no separate VFS (like Local) should return {:error, :not_supported}.

read_file(state, path)

@callback read_file(state :: term(), path :: binary()) ::
  {:ok, binary()} | {:error, term()}

Reads a file's raw bytes.

shutdown(state)

@callback shutdown(state :: term()) :: :ok

Releases any resources held by the sandbox state.

write_file(state, path, content)

@callback write_file(state :: term(), path :: binary(), content :: binary()) ::
  :ok | {:error, term()}

Writes raw bytes to a file, creating parent directories as needed.

Functions

edit(sandbox, path, old_text, new_text)

exec(sandbox, command, opts \\ [])

glob(sandbox, pattern, opts \\ [])

grep(sandbox, pattern, opts \\ [])

mount(sandbox, host_path, vfs_path)

new(module, opts \\ [])

Constructs a sandbox handle by initializing module with opts.

Returns {:ok, sandbox} so callers can surface init failures (an in-memory sandbox that fails to allocate, a virtual sandbox whose NIF refused to start, etc).

read(sandbox, path)

resolve(sandbox)

Normalizes a user-supplied :sandbox option into a t().

Accepts an already-built struct, a bare module, or a {module, opts} tuple. Returns {:ok, sandbox} or {:error, reason}.

shutdown(sandbox)

Releases the sandbox state.

write(sandbox, path, content)