These examples put securer inside a Shiny app, a Plumber API, and a batch script. Each one stands alone. None of the code runs when this page is built, so copy what you need into your project and adjust it.
Where securer fits
Shiny / Plumber / Batch script
+------------------------------+
| Application layer | User requests or code snippets
| (UI, routes, lapply) | arrive here.
+---------|--------------------+
|
v
+------------------------------+
| securer | SecureSession or SecureSessionPool
| - Sandbox (Seatbelt/bwrap) | manages child R processes with
| - IPC (Unix domain socket) | OS-level isolation.
| - Resource limits (ulimit) |
+---------|--------------------+
|
v
+------------------------------+
| Sandboxed R child | Code executes here. Tool calls
| - eval(user code) | pause execution and are fulfilled
| - tool call -> host -> back | by the host via IPC.
+------------------------------+
Shiny
This small app lets a user type R code and run it in a sandbox. Each
visitor gets their own SecureSession, started when they
connect and closed when they leave.
library(shiny)
library(securer)
ui <- fluidPage(
titlePanel("Secure R Executor"),
sidebarLayout(
sidebarPanel(
textAreaInput(
"code", "R Code",
value = "1 + 1",
rows = 8
),
actionButton("run", "Run Code")
),
mainPanel(
verbatimTextOutput("result")
)
)
)
server <- function(input, output, session) {
# One SecureSession per Shiny session
secure <- SecureSession$new(sandbox = TRUE)
# Clean up when the user disconnects
session$onSessionEnded(function() {
secure$close()
})
result_val <- reactiveVal("Enter code and click Run.")
observeEvent(input$run, {
result_val(
tryCatch(
{
res <- secure$execute(input$code, timeout = 10)
paste(capture.output(print(res)), collapse = "\n")
},
error = function(e) paste("Error:", conditionMessage(e))
)
)
})
output$result <- renderText(result_val())
}
shinyApp(ui, server)With many visitors at once, one session each gets expensive. Share a
single SecureSessionPool across all of them instead. Each
run takes an idle session from the pool and gives it back when it’s
done.
Plumber API
This API takes R code in a POST request and runs it in a pool of sandboxed sessions. The pool starts with the API and every request shares it.
# plumber.R
library(plumber)
library(securer)
# Start the pool once, when the API starts.
# reset_between_uses = TRUE restarts each session after use, so one
# request can't see variables or packages left by another.
pool <- SecureSessionPool$new(
size = 4,
sandbox = TRUE,
reset_between_uses = TRUE
)
#* Execute R code in a sandboxed session
#* @param code Character string of R code
#* @post /execute
function(req, res, code = "") {
if (nchar(code) == 0) {
res$status <- 400
return(list(error = "Missing 'code' parameter"))
}
tryCatch(
{
result <- pool$execute(code, timeout = 10)
list(result = result)
},
error = function(e) {
res$status <- 500
list(error = conditionMessage(e))
}
)
}
# Start the API
pr <- plumb("plumber.R")
pr$run(host = "0.0.0.0", port = 8080)Some things to tighten before you expose an API like this:
- Check
nchar(code)yourself and reject huge requests early.$execute()already refuses code overmax_code_length(100,000 characters by default), but there’s no point getting that far. - Pick a
timeoutthat suits your workload. The CPU limit fromulimitdoesn’t count time spent waiting, andtimeoutdoes. - If you register tools, cap how often one request can call them with
max_tool_calls. - If error messages go back to people you don’t trust, use
sanitize_errors = TRUE. It removes file paths and process IDs.
SecureSessionPool doesn’t take
max_tool_calls or sanitize_errors. For those
two, manage your own SecureSession objects and pass them to
$new() and $execute().
Batch jobs
Here a pool runs a vector of code snippets. lapply()
goes through them one at a time, but each snippet runs in a session
that’s already started, so you don’t pay the startup cost again and
again.
library(securer)
# Code snippets to evaluate
snippets <- c(
"mean(1:100)",
"sqrt(144)",
"UNDEFINED_VAR + 1", # will error
"paste('hello', 'world')",
"sum(rnorm(1000))"
)
pool <- SecureSessionPool$new(size = 2, sandbox = TRUE)
results <- lapply(snippets, function(code) {
tryCatch(
{
value <- pool$execute(code, timeout = 5)
list(code = code, result = value, error = NA_character_)
},
error = function(e) {
list(code = code, result = NA, error = conditionMessage(e))
}
)
})
pool$close()
# Collect into a data frame for inspection
outcome <- data.frame(
code = vapply(results, `[[`, character(1), "code"),
error = vapply(results, `[[`, character(1), "error"),
stringsAsFactors = FALSE
)
outcome$result <- lapply(results, `[[`, "result")
outcome[, c("code", "error")]
#> code error
#> 1 mean(1:100) <NA>
#> 2 sqrt(144) <NA>
#> 3 UNDEFINED_VAR + 1 object 'UNDEFINED_VAR' not found
#> 4 paste('hello', 'world') <NA>
#> 5 sum(rnorm(1000)) <NA>The result column is a list holding each value. Each
snippet’s error is caught on its own, so one failure doesn’t stop the
batch. If you run snippets in parallel, raise size in
SecureSessionPool$new(). Each session is a separate R
process, so more sessions means more memory.