Skip to contents

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 over max_code_length (100,000 characters by default), but there’s no point getting that far.
  • Pick a timeout that suits your workload. The CPU limit from ulimit doesn’t count time spent waiting, and timeout does.
  • 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.