Skip to contents

Permissions control what an agent is allowed to do. They sit between the LLM and tool execution, checking every tool call before it runs. deputy ships with sensible presets and makes it easy to build custom policies.

Permission Presets

library(deputy)

# Read-only: only read_file and list_files are allowed
permissions_readonly()

# Standard (default): file read/write in working dir, R code, no bash/web
permissions_standard()

# Full: everything allowed (use with caution!)
permissions_full()

Each preset returns a Permissions object. Pass it to Agent$new():

agent <- Agent$new(
  chat = ellmer::chat_anthropic(),
  tools = tools_all(),
  permissions = permissions_readonly()
)

Custom Permissions

For fine-grained control, create a Permissions object directly:

perms <- Permissions$new(
  file_read = TRUE,
  file_write = "/path/to/allowed/dir",
  bash = FALSE,
  r_code = TRUE,
  web = FALSE,
  install_packages = FALSE,
  max_turns = 10,
  max_cost_usd = 0.50
)

Fields:

Field Type Description
file_read logical Allow reading files
file_write logical / path Allow writing (optionally restricted to a directory)
bash logical Allow bash commands
r_code logical Allow R code execution
web logical Allow web access
install_packages logical Allow package installation
max_turns integer Maximum agentic turns (default 25)
max_cost_usd numeric / NULL Maximum spend in USD

Permission Modes

The mode field provides broad policy shortcuts:

Mode Behaviour
"default" Check each tool against the policy fields above
"readonly" Only allow tools annotated as read_only_hint = TRUE
"acceptEdits" Auto-approve file writes without prompting
"bypassPermissions" Allow everything (dangerous!)
perms <- Permissions$new(mode = "readonly")

Tool Annotations

Tools carry annotations that describe their behaviour. The permission system uses these annotations to make decisions:

# A read-only tool
tool_safe <- ellmer::tool(
  fun = function(x) x,
  name = "safe_tool",
  description = "A safe, read-only tool",
  arguments = list(x = ellmer::type_string("Input")),
  annotations = ellmer::tool_annotations(
    read_only_hint = TRUE,
    destructive_hint = FALSE
  )
)

In "readonly" mode, only tools with read_only_hint = TRUE are allowed. In "default" mode, tools with destructive_hint = TRUE may be blocked depending on the policy.

Custom Permission Callbacks

For complex logic, provide a can_use_tool callback:

perms <- Permissions$new(
  can_use_tool = function(tool_name, tool_input, context) {
    # Block writes to sensitive files
    if (tool_name == "write_file") {
      if (grepl("^\\.env|secrets|credentials", tool_input$path)) {
        return(PermissionResultDeny(
          reason = "Cannot write to sensitive files"
        ))
      }
    }
    PermissionResultAllow()
  }
)

The callback receives:

  • tool_name – Name of the tool being called
  • tool_input – Named list of arguments
  • context – List with working_dir and tool_annotations

It must return PermissionResultAllow() or PermissionResultDeny(reason).

Example: Read-Only Agent

A read-only agent can explore files but cannot change anything:

library(deputy)

chat <- ellmer::chat_anthropic(model = "claude-sonnet-4-20250514")
agent <- Agent$new(
  chat = chat,
  tools = tools_file(),
  permissions = permissions_readonly()
)

result <- agent$run_sync("What files are in the current directory?")
cat(result$response)

Cost and Turn Limits

Permissions also enforce resource limits:

perms <- Permissions$new(
  max_turns = 10,      # Stop after 10 agentic turns
  max_cost_usd = 1.00  # Stop if cost exceeds $1.00
)

When a limit is reached, the agent stops and the AgentResult$stop_reason indicates what happened ("max_turns" or "cost_limit").