This vignette covers session lifecycle, output handling, audit
logging, and session pooling. See also
vignette("quickstart") and
vignette("security-model").
Persistent sessions
SecureSession keeps a child R process alive across
multiple $execute() calls. State persists between
executions:
library(securer)
tools <- list(
securer_tool("add", "Add two numbers",
fn = function(a, b) a + b,
args = list(a = "numeric", b = "numeric"))
)
session <- SecureSession$new(tools = tools, sandbox = TRUE)
session$execute("x <- add(10, 20)")
session$execute("x * 2")
#> [1] 60
session$close()Always call $close() when done, or use
with_secure_session() for automatic cleanup:
result <- with_secure_session(function(session) {
session$execute("x <- 10")
session$execute("x * 2")
}, sandbox = FALSE)Agent safety options
SecureSession has several options for hardening LLM
agent deployments:
-
max_code_length($execute()): Reject code exceeding a character limit (default 100,000). -
max_executions($new()): Cap total$execute()calls per session. -
pre_execute_hook($new()): Callback returningFALSEto block execution. -
sanitize_errors($new()): Strip paths, PIDs, and hostnames from errors.
session <- SecureSession$new(
max_executions = 100,
pre_execute_hook = function(code) {
# Block code that mentions system()
!grepl("system\\(", code)
},
sanitize_errors = TRUE
)See vignette("security-model") for the full threat model
and defense layers.
Execution timeouts
$execute() accepts a timeout in seconds
(default 30). On timeout the child is killed and the session
auto-recovers:
session <- SecureSession$new()
session$execute("Sys.sleep(60)", timeout = 5)
#> Error: Execution timed out
session$is_alive()
#> [1] TRUE
session$close()Error handling
Errors in the child process, in tool execution, and from unknown tool names are all propagated to the host as standard R errors:
Streaming output
Pass output_handler to receive child output as it
arrives:
session <- SecureSession$new()
session$execute(
'for (i in 1:5) cat("Step", i, "\\n")',
output_handler = function(line) message("[child] ", line)
)
session$close()Output is always available as attr(result, "output") on
the return value.
Output and tool call limits
Both max_output_lines and max_tool_calls
are per-execution caps:
session <- SecureSession$new()
# Cap accumulated output lines
result <- session$execute(
'for (i in 1:1000) cat("line", i, "\\n")',
max_output_lines = 100
)
# Cap tool calls in one execution
session$execute("for (i in 1:10) add(i, i)", max_tool_calls = 5)
#> Error: Maximum tool calls (5) exceeded
session$close()Session lifecycle
$restart() resets the child process;
$is_alive() checks if it is running:
session <- SecureSession$new(sandbox = FALSE)
session$execute("x <- 42")
session$restart()
# State is gone after restart
session$execute("exists('x')")
#> [1] FALSE
session$is_alive()
#> [1] TRUE
session$close()Audit logging
Pass audit_log to the constructor to write structured
JSONL entries:
session <- SecureSession$new(audit_log = "securer-audit.jsonl")
session$execute("1 + 1")
session$close()Each line is a JSON object with timestamp,
event, and session_id fields plus
event-specific data. Events: session_start (pid),
session_close, session_restart,
execute_start (code), execute_complete
(elapsed), execute_error (error),
execute_timeout (timeout_secs), tool_call
(tool, args), tool_result (tool, elapsed). Example:
{"timestamp":"2024-01-15T10:30:00.000Z","event":"tool_call","session_id":"sess_abc123","tool":"add","args":{"a":1,"b":2}}The log file is created with 0600 permissions. Code
fields longer than 10,000 characters are truncated.
Session pooling
Why session pooling?
Creating a SecureSession starts a child R process, sets
up the sandbox, and establishes the IPC socket. This takes 0.5–1 second.
For Plumber APIs or multi-user Shiny apps, that per-request overhead
adds up fast. Session pooling pre-warms a fixed number of sessions at
startup and reuses them across requests.
Session pool lifecycle
Initialize Acquire Execute Release
+-------------+ +-------------+ +-------------+ +-------------+
| Pre-warm N |-->| Caller gets |-->| Run code in |-->| Return to |
| sessions at | | idle session| | acquired | | idle pool |
| startup | | from pool | | session | | (or restart |
| | | | | | | if dead) |
+-------------+ +-------------+ +-------------+ +-------------+
SecureSessionPool pre-warms multiple sessions for
low-latency execution:
pool <- SecureSessionPool$new(size = 4, sandbox = TRUE)
pool$execute("1 + 1")
#> [1] 2
pool$status() # returns list(total, busy, idle, dead)
pool$close()Set reset_between_uses = TRUE to restart sessions after
each execution, preventing state leakage between callers:
pool <- SecureSessionPool$new(
size = 2,
sandbox = FALSE,
reset_between_uses = TRUE
)
pool$execute("x <- 99")
pool$execute("exists('x')")
#> [1] FALSE
pool$close()Dead sessions are automatically restarted on acquire. The pool is
not thread-safe; create separate instances when using
parallel or future.