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 FALSEModes
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.