Skip to contents

Deputy checks every tool call against the agent’s permission policy before the tool runs, and counts every request and tool call against its usage limits. Both are set when you create the agent. You can tighten them afterwards, but you can’t loosen them.

Presets

Four functions cover the common cases:

Preset Reads files Writes files Runs code Uses the web
permissions_readonly() yes no no no
permissions_standard() (the default) yes inside the working directory no no
permissions_plan() yes no no read-only web tools
permissions_full() yes yes yes yes

Pass one to Agent$new():

agent <- Agent$new(
  chat = ellmer::chat("openai/gpt-6-luna"),
  tools = tools_file(),
  permissions = permissions_readonly()
)

“Reads files” means any file the R process can read, wherever it is. The working directory limits writes, not reads.

permissions_plan() is meant for planning before acting: it allows tools annotated as read-only, including web fetches and searches, plus ask_user so the model can check its plan with you. permissions_full() allows every tool call.

Build your own policy

Permissions() sets each capability directly:

policy <- Permissions(
  file_read = TRUE,
  file_write = "/path/to/output",
  bash = FALSE,
  r_code = TRUE,
  web = FALSE,
  install_packages = FALSE
)
Argument Controls Default
file_read the built-in file reading tools, for any file the R process can read TRUE
file_write write_file, edit_file and multi_edit: TRUE, FALSE, or a directory to confine them to the current directory
bash run_bash FALSE
r_code run_r_code FALSE
web web tools and any tool annotated as open world FALSE
install_packages package installation FALSE
tool_allowlist if set, only these tools may run NULL
tool_denylist these tools never run NULL
can_use_tool a function that can deny or pause calls the rest of the policy allows (see below) NULL
permission_prompt_tool_name a tool the model may always call to ask for approval NULL
mode "standard", "readonly", "plan" or "full" "standard"

Deputy’s own tools map to these capabilities by name. Other tools are judged by their annotations: a tool annotated as open world needs web = TRUE, and a destructive one needs file writes or shell commands to be allowed. Missing annotations count as the risky answer.

Setting r_code = TRUE or bash = TRUE lets the model run code with your user account’s access to files and the network. Only do that when you trust the model, the prompt and every input it will see. Running R code shows how to run model-written code in an OS sandbox instead.

To see how a policy treats a call without running anything, use permissions_check():

policy <- Permissions(file_write = FALSE)
permissions_check(policy, "write_file", list(path = "notes.md"))
#> <deputy::PermissionResultDeny>
#>  @ decision : chr "deny"
#>  @ reason   : chr "File writing is not allowed"
#>  @ interrupt: logi FALSE

Modes

The mode argument changes how the capabilities are applied:

Mode Allows
"standard" any call the capabilities allow
"readonly" Deputy’s read tools, plus web tools if web = TRUE and custom tools you list in tool_allowlist; never writes, code or destructive tools
"plan" tools annotated as read-only (open-world ones only if web = TRUE)
"full" every call

In every mode, tool_denylist and tool_allowlist are checked first, and the tool named by permission_prompt_tool_name is always allowed. Read-only and plan modes also let an agent delegate to its subagents and retained agents. Every tool call those agents make is also checked against the delegating agent’s permissions, so they are held to the same mode.

Narrow an agent’s permissions

set_permission_mode() switches a running agent to a stricter mode. It can’t switch to a mode that allows more than the agent started with:

Current mode Can switch to
"readonly" "readonly"
"standard" "standard", "readonly"
"plan" "plan", "readonly"
"full" "full", "standard", "plan", "readonly"
agent$set_permission_mode("readonly")

The new mode is combined with the original policy, so directory limits, allow and deny lists and callbacks keep applying. If the new mode takes away web access, Deputy also removes any provider-side web tools, because it can’t intercept their calls. To give an agent more permissions, create a new agent.

Decide each call with a callback

A can_use_tool function adds your own rules on top of a policy. Deputy calls it, in every mode, for each call the rest of the policy allows. It receives the tool name, its arguments and a context list, and returns PermissionResultAllow(), PermissionResultDeny(reason) or PermissionResultPending(reason). Any other return value, or an error, denies the call.

policy <- Permissions(
  file_write = getwd(),
  can_use_tool = function(tool_name, tool_input, context) {
    path <- if (is.character(tool_input$path)) tool_input$path else ""
    if (tool_name == "write_file" && grepl("^\\.env|secrets", path)) {
      return(PermissionResultDeny("Don't write to secret files."))
    }
    PermissionResultAllow()
  }
)

The callback can deny a call or pause it, but it can’t allow a call the rest of the policy denies. The context list includes the working directory, the tool’s annotations and argument types, and the run’s identifiers. A PreToolUse hook is often simpler when you only want to block a few calls, and a PermissionRequest hook can allow a call the policy denies, for example after asking you.

Tools that run on the provider’s servers, such as Anthropic’s web search, can’t be checked per call. An agent whose policy has a callback refuses to register them.

PermissionResultPending() pauses the run until a person approves the call. Human input and approvals explains how to set that up.

Usage limits

UsageLimits() caps what one run can spend. Set defaults on the agent, and override them for a single run:

agent <- Agent$new(
  chat = ellmer::chat("openai/gpt-6-luna"),
  tools = tools_file(),
  usage_limits = UsageLimits(max_requests = 10, max_tool_calls = 20)
)

result <- agent$run_sync(
  "Summarise every R file in R/.",
  usage_limits = UsageLimits(max_tool_calls = 40)
)

Unset fields in a run’s limits fall back to the agent’s. The agent defaults to 25 model requests per run. Limits count only the current run, so a restored conversation starts with a fresh budget.

Limit Stops the run with
max_requests "request_limit"
max_tool_calls "tool_call_limit"
max_input_tokens "input_token_limit"
max_output_tokens "output_token_limit"
max_total_tokens "total_token_limit"
max_cost_usd "cost_limit"

Request and tool-call limits are checked before each request or call. Token and cost limits can only be checked once a response reports its usage, so a run can overshoot them by one response.

Cost comes from ellmer’s price table. If you set max_cost_usd and ellmer can’t price the model, for example because the model is newer than your version of ellmer, the run stops with "cost_unavailable" rather than guess. Likewise, result$cost$total is NA whenever any response couldn’t be priced, and result$cost$missing counts those responses.

By default a limit returns an AgentResult with the reason in stop_reason. With UsageLimits(on_exceed = "error"), it signals an error instead, and agent$last_run() still holds the partial result.

Deputy also stops a run with "tool_loop" when the model makes the same tool call three times in a row and gets the same result each time.

Subagents get the lead agent’s remaining budget, further limited by their own definition. See Subagents.

Permissions are not a sandbox

A permission decides whether the model may call a tool. It can’t limit what a tool does once it runs. run_r_code gets a separate process and a timeout, but that process can read and write anything your user account can. For model-written code that should only touch part of the system, use an OS sandbox such as mcp-repl; see Running R code.

Reading works the same way. file_read = TRUE lets the file tools read any file your user account can, including credential files such as ~/.Renviron or ~/.ssh/. A file_write directory doesn’t limit reads. To keep the model away from particular files, refuse their paths in can_use_tool, or run the agent as a user that can’t read them.

To make sure the results you show a user come from one trusted tool rather than from model text, see Trusted mini-agents.