commons allows users to build and deploy agents that can run SQL queries against live databases and execute arbitrary R code. This raises several security- and governance-related questions. How do you ensure that an agent can’t delete production data? Can model-generated R code interfere with a Posit Connect deployment? Can an agent show users tables, rows, or business context that they shouldn’t be able to see?
This vignette explains the boundaries that commons provides and the responsibilities that remain with the application author and server administrator.
commons’ harness
A commons agent is an ellmer chat with a system prompt and a set of tools. The system prompt excerpts available data sources and business context. Tools let the model retrieve more context, call a trusted calculation, run a SQL query, or execute R code.
We assume that a model might make any request allowed by its tools. Application security should therefore not depend on the model following an instruction like “never reveal sensitive data.” Instead, only give an agent access to data that the current user of the application is allowed to see. The system prompt, tool arguments, and tool results are also sent to the model provider, so the agent should not have access to data that you do not trust that provider to process.
SQL code execution with run_sql()
Destructive actions
run_sql() accepts a single statement beginning with
SELECT or WITH. commons rejects stacked
statements and statements beginning with operations such as
INSERT, UPDATE, DELETE,
DROP, or GRANT. For data frames and pins,
which commons loads into its own DuckDB database, it also disables
extension loading and external filesystem access.
These checks provide defense in depth, but they are not a SQL parser or a database sandbox. When you supply a DBI connection, commons queries that connection as-is. The primary safeguard against destructive SQL is therefore database-enforced read-only access.
Data access
The tables argument to data_source()
controls which tables commons describes to the model. It is not an
authorization boundary: run_sql() can query any object
available to the connection.
On Posit Connect, viewer OAuth integrations can give an interactive application the current viewer’s Snowflake or Databricks credentials. If the application creates its connection from those credentials, the warehouse continues to enforce that viewer’s existing access policies, including row- and column-level security. commons snapshots the connection’s principal, active role, and namespace when it creates a Snowflake or Databricks data source, and rejects subsequent operations if that identity changes.
Viewer credentials are not automatic: commons uses the DBI connection supplied by the application. When using viewer credentials, create the connection and the commons agent inside the Shiny server function so that each session has the correct database identity.
When viewer credentials are not available, use a service account with access to only the data needed by the application. Every viewer then has the same database permissions, so share the application only with users who should have that access.
run_r() and its R process
The code requested through run_r() does not run in the
main application process. Each commons agent instead gets a persistent R
subprocess, created on first use. Its state remains available to later
run_r() calls made by the same agent.
OS-level sandboxing
On Linux (which underlies Posit Connect), commons combines a filesystem sandbox with seccomp system-call filters. It uses Landlock when the kernel supports it and otherwise falls back to unprivileged user and mount namespaces. If neither filesystem mechanism is available, or if seccomp is unavailable, commons refuses to create an agent.
The filesystem sandbox gives the subprocess read access to R, installed packages, and required operating-system directories. It can write only to temporary directories used by the subprocess and its communication with the main R process. The seccomp filters prevent it from inspecting another process’s memory or changing mount and namespace configuration.
By default, the subprocess cannot create network sockets. An
application author can opt in to unrestricted network access with
commons(network = "full"); the filesystem sandbox remains
in place, but R code can then contact services reachable from the
deployment and transmit data from result handles. Only enable network
access when that egress is required and acceptable.
For local development on macOS, commons applies a similar filesystem
and network policy using Seatbelt. Windows has no OS-level
run_r() sandbox. Deployed Connect applications run on Linux
and use the Linux mechanisms described above.
By default, commons refuses to create an agent when an OS-level sandbox is not available. An application author can enable a local-development fallback explicitly:
options(commons.allow_unsafe_fallback = TRUE)commons always uses OS-level sandboxing when it is available. This option cannot disable or bypass it; it only permits commons to fall back to best-effort R guardrails when no OS-level sandbox is available.
The fallback places checks around ordinary R functions while model-authored code evaluates. It limits filesystem reads to R, installed packages, and the worker directory, limits writes to the worker directory, denies subprocess functions, and follows the requested network policy. These checks reduce accidental access and damage, but native code and other R mechanisms can bypass them. They are not a sandbox or security boundary and should not be enabled in a deployment.
Communication with the main app process
The subprocess starts with a small allowlist of environment variables rather than inheriting the application’s complete environment. In particular, API keys, session tokens, and database URLs are not inherited.
Communication happens through callr. commons serializes registered result handles and measure source into the subprocess, then receives captured output and plots in return. Function environments and database connections are not transferred.
Permissioning facts
The rows returned by SQL are not the only potentially sensitive
information available to an agent. Dataset descriptions and glossary
entries from data-dict.yaml are included in the system
prompt. Table documentation and sample values are supplied when a table
is first used, context documents are available through search, and
measure source can be read by the model in run_r().
Only include facts and source code that may be shared with both the application’s viewers and its model provider. If one audience should not see a fact, use separate applications with separate context or place the facts behind viewer credentials rather than asking the model to hide it from that audience.
Logging trajectories
Trajectory logging is disabled by default. With
commons(log = TRUE), commons asks ellmer to include message
content in OpenTelemetry spans. These spans contain full conversation
histories, including user messages, model responses, tool requests, and
tool results. Treat them as another copy of the data available to the
agent.
On Connect, both the server administrator and application author must
opt in for the data to flow. The administrator must enable OpenTelemetry
and allow content instrumentation; the application author sets
commons(log = TRUE). commons enables Content
Observability for the content item when needed, although capture
begins only after the content process restarts.
Content owners and collaborators can read Connect trajectories with
trajectory_read(). If a conversation is flagged or
annotated in trajectory_review(), the reviewer writes a
Markdown document containing the complete reviewer-visible conversation
and its tool activity. Store these files only in an appropriately
protected location, and carry out reviews in an environment trusted to
handle internal user data, such as Posit
Workbench. Retention and access controls for the underlying
OpenTelemetry data are determined by Connect or the configured exporter,
not by commons.
