Skip to contents

securer runs R code written by an LLM in a sandbox, and lets that code call functions you choose. The calls run in your own R session, so the code can reach your data through those functions and nothing else.

The package is experimental, and function names may still change.

Installation

# install.packages("pak")
pak::pak("ian-flores/securer")

Check that it works:

library(securer)
execute_r("1 + 1")
#> [1] 2

A quick look

Give the sandboxed code a function it can call. Here that function is a stand-in for a database query:

library(securer)

tools <- list(
  securer_tool("query_db", "Query a database table",
    fn = function(table, limit) head(get(table, "package:datasets"), limit),
    args = list(table = "character", limit = "numeric"))
)

result <- execute_r('
  data <- query_db("mtcars", 5)
  mean(data$mpg)
', tools = tools, sandbox = TRUE)

When the code reaches query_db(), it pauses. Your session runs the real function, outside the sandbox, and sends the result back. Then the code carries on. The LLM’s code never touches your files, network, or data directly.

Why use it

LLM-written code can do things you didn’t ask for. securer limits what it can do:

Risk What securer does
The code reads or writes your files An OS sandbox lets it write only to a temp directory and read only R’s own files and libraries
The code makes network requests Network access is blocked, by namespaces on Linux and Seatbelt on macOS
The code needs your APIs or databases You register tool functions, which run in your session
The code runs forever A timeout stops it, and the session restarts so you can keep using it
The code uses all your memory Resource limits, with ulimit on Linux and macOS and Job Objects on Windows
The code doesn’t parse It’s checked for syntax errors before it’s sent to the sandbox

Posit’s commons package has its own sandbox for its built-in agents, but you can’t reuse it for an agent you write yourself. securer is meant for that case.

How it works

OS SANDBOXHOST R SESSIONCHILD R PROCESSSEND THE CODEruns itTOOL CALLruns your tool functionpauses on the socketTOOL RESULTcarries onFINAL VALUEhands it back to you
Fig. 1 · A tool call: the code runs in the child, your function runs in the host

The two processes talk over a Unix domain socket. A tool call blocks the child until the parent sends back the result.

Features

Sessions

Starting a sandboxed R process takes a moment. Keep one open to run several pieces of code:

session <- SecureSession$new(tools = tools, sandbox = TRUE)

session$execute('query_db("iris", 3)')
session$execute('query_db("mtcars", 5)')

session$close()

Session pools

If you run many requests at once, start a few sessions ahead of time:

pool <- SecureSessionPool$new(size = 4, tools = tools, sandbox = TRUE)

result <- pool$execute('query_db("iris", 3)')

pool$close()

Resource limits

Cap CPU time, memory, file size, and more. Limits work on every platform, with or without the sandbox:

result <- execute_r("1 + 1",
  limits = list(cpu = 10, memory = 256 * 1024 * 1024)
)

Timeouts

Code that runs too long is stopped. The session restarts itself, so you can use it again straight away:

session <- SecureSession$new()
session$execute("Sys.sleep(100)", timeout = 5)
# Error: Execution timed out after 5 seconds
session$execute("1 + 1")  # still works

Syntax checks

Code that doesn’t parse fails right away, without a trip to the child process:

session$execute("if (TRUE {")  # immediate error

ellmer

Give an ellmer chat a tool that runs R code in the sandbox:

library(ellmer)
chat <- chat_openai()
chat$register_tool(as_ellmer_tool())
chat$chat("Calculate the mean of 1 through 100 using R")

Audit log

Write every session event to a JSONL file, one JSON object per line:

session <- SecureSession$new(audit_log = "session.jsonl")

Verbose mode

Print what the session is doing as it happens:

session <- SecureSession$new(tools = tools, verbose = TRUE)
session$execute('query_db("iris", 3)')
# [securer] Session started (sandbox=false, pid=1234)
# [securer] Tool call: query_db(table=iris, limit=3)
# [securer] Tool result: query_db -> ... (0.00s)
# [securer] Execution complete (0.27s)

Platform support

Platform Sandbox Network blocked Filesystem restricted Resource limits
Linux bubblewrap (bwrap) Yes Yes Yes (ulimit)
macOS Seatbelt (sandbox-exec) Yes Yes Yes (ulimit)
Windows Job Objects and a clean environment No No Yes (memory, CPU, process count)
docker-spawn (any host) A new container per session Yes (--network=none) Yes (container filesystem) Yes (--memory, --cpus, ulimit)

Tool calls, timeouts, and syntax checks work everywhere.

On Windows, securer can only clean the environment and set resource limits. It can’t stop the code from reading files or using the network. If you need that on Windows, use the docker-spawn backend or a Linux VM.

The docker-spawn backend is off by default. Turn it on with SECURER_SANDBOX_MODE=docker-spawn. It needs the docker command and a running Docker daemon. inst/docker/Dockerfile has a reference image.

Security

The sandbox is one of several protections. securer also checks that only the child it started can connect to the socket, limits resources, strips secrets from the child’s environment, and checks tool arguments. The security model vignette (vignette("security-model", package = "securer")) covers what securer protects against and what it doesn’t.

To report a security problem, email the maintainer instead of opening a public issue.

securer is part of a small set of packages for running LLM agents in R more safely:

  • secureguard checks prompts, generated code, and outputs for things like prompt injection and leaked secrets.
  • securetools has ready-made tools (file access, SQL, web requests) with limits built in.
  • securebench measures how well your guardrails work. It complements vitals.

They build on Posit’s tools rather than replacing them. For tracing, use ellmer’s OpenTelemetry support through the otel package. For retrieval (RAG), use ragnar.

Learn more

Found a bug or have an idea? Open an issue.

License

MIT