Like LLMR::llm_tool(), but the tool runs under confinement: a wall-clock
timeout, a cap on result size, and auditing of where it writes. Each
invocation hashes the input files it was handed and the output files it
produced, so a tool that touches the filesystem leaves an auditable trail.
The returned object is an ordinary llmr_tool, so it passes to agent()
and the tool loop exactly as a plain tool does.
Usage
sandbox_tool(
fn,
name = NULL,
description = NULL,
parameters = NULL,
required = NULL,
mode = c("read_only", "tempdir", "container"),
timeout_s = 30,
max_bytes = 1e+06,
allow_paths = NULL,
env = "minimal",
executor = NULL
)Arguments
- fn
The function to expose, or an existing
llmr_toolto confine. When anllmr_toolis given, its own function, name, description, and schema are reused and the remainingname/description/parameters/requiredarguments are ignored.- name
Tool name shown to the model. Defaults to
"sandboxed_tool".- description
One or two sentences for the model. Defaults to
"A sandboxed tool.".- parameters
A named list of JSON-Schema properties, or a full schema object (as in
LLMR::llm_tool()).- required
Character vector of required argument names.
- mode
The confinement regime.
"read_only": the child runs with its working directory set to a scratch directory; relative writes land there and are hashed and checked againstallow_paths, and any reported write outsideallow_pathsis a violation (the default executor cannot intercept absolute-path writes; see Details)."tempdir": the scratch directory is the sanctioned writable location and is always permitted; reported writes elsewhere are violations."container": confinement is delegated entirely to a suppliedexecutor, which is the way to obtain a hard filesystem boundary.- timeout_s
Wall-clock limit per call, in seconds (default
30). The default executor kills the child process when it elapses; the call then reports a"timeout"status.- max_bytes
Maximum result size in bytes; a larger result (or captured output) is truncated and flagged. Default
1e6.- allow_paths
Character vector of directories outside which a reported written file is a violation. The scratch working directory is always permitted (in
"tempdir"mode). The check applies only to files the executor reports; with the default executor that means files written under the scratch directory, since absolute-path writes elsewhere are not seen (see Details). DefaultNULL(only the scratch directory is allowed).- env
How much of the ambient environment the child sees. Recorded for provenance and passed to the executor; the default executor treats
"minimal"as a hint and does not inherit the parent's global objects.- executor
A function
(fn, args, workdir, timeout_s)implementing the contract above. WhenNULL, a default child-process executor is built for"read_only"/"tempdir"(requirescallr; without it the function runs in-process with a one-time warning that confinement is weak), and"container"mode raises an error asking for an executor.
Value
An llmr_tool carrying a "governance" attribute whose sandbox
element records the mode and allow_paths. Each call returns the tool's
result string with a "sandbox" attribute recording the mode, duration,
byte count, input and output file hashes, and status.
Details
Confinement is carried out by an executor, a function that runs the user
function in a bounded place and reports what it produced. The default
executor runs the function in a child R process (via the callr package),
which is killed when the timeout elapses; mode = "container" requires an
executor to be supplied, because the package does not assume any particular
container runtime. Supplying executor directly is also how the tool is
tested offline: a fake executor returns a canned result and file list, so
the size, timeout, and path checks can be exercised without spawning
anything.
What the default executor actually guarantees. The default child-process
executor runs the user function with its working directory set to a fresh
scratch directory, then snapshots that directory before and after the call to
hash every file written under it. Relative writes therefore land in the
scratch directory and are hashed and checked against allow_paths. The
default executor does not establish a hard filesystem boundary: an
absolute-path write outside the scratch directory (e.g. to /tmp/out or
$HOME/out) happens in the same operating-system namespace as the parent and
is not intercepted, so llmragent_sandbox_violation cannot fire for a write
the executor never sees. The guarantee is therefore "confine and audit writes
within the scratch working directory", not "block absolute-path writes". To
enforce a real boundary against arbitrary absolute paths, supply a
mode = "container" executor (or an OS-level sandbox) that runs the function
in a confined namespace and reports the files it wrote; the allow_paths
check then applies to whatever that executor reports.
The executor contract is executor(fn, args, workdir, timeout_s) returning
a list with elements stdout (character), result (the value, or NULL),
files (a named character vector mapping written paths to content hashes,
or a bare character vector of written paths), status (one of "ok",
"timeout", "error"), and error (a message, or NA).
Examples
if (FALSE) { # \dontrun{
# A tool that runs in a killed-on-timeout child R process.
slow <- sandbox_tool(
function(n) { Sys.sleep(n); "done" },
name = "wait", description = "Sleeps for n seconds, then returns.",
parameters = list(n = list(type = "number")),
mode = "tempdir", timeout_s = 2
)
slow$fn(n = 10) # reports a timeout rather than blocking
# Offline: an injected executor needs no process at all.
double <- sandbox_tool(
function(x) x * 2, name = "double", description = "Doubles x.",
parameters = list(x = list(type = "number")), mode = "tempdir",
executor = function(fn, args, workdir, timeout_s)
list(stdout = "", result = do.call(fn, args),
files = character(0), status = "ok", error = NA)
)
double$fn(x = 21)
} # }