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: known Deputy read tools are allowed
permissions_readonly()

# Plan: annotated read-only tools plus ask_user
permissions_plan()

# Standard (default): accessible reads, workspace-scoped writes, 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
)

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
can_use_tool function Apply custom permission decisions
tool_allowlist character Deny tools not explicitly listed
tool_denylist character Always deny explicitly listed tools
permission_prompt_tool_name character Dedicated approval tool used in gating messages

Permission Modes

The mode field provides broad policy shortcuts:

Mode Behaviour
"standard" Check each tool against the configured capabilities
"readonly" Allow known Deputy read tools and explicit allowlist entries within configured capabilities
"plan" Allow read-only annotated tools within configured capabilities and the human approval prompt tool
"full" Allow every tool (dangerous!)
perms <- Permissions$new(mode = "readonly")

permissions_plan() creates a planning-oriented policy:

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

Changing an Agent’s Mode

An agent’s configured permissions are an authority ceiling. Calling set_permission_mode() may preserve or narrow that authority, but it cannot widen it or replace it with an incomparable policy:

Current mode Allowed target modes
"readonly" "readonly"
"standard" "standard", "readonly"
"plan" "plan", "readonly"
"full" "full", "standard", "plan", "readonly"

Reapplying the current mode is an exact no-op. For an allowed narrowing, Deputy intersects the target mode with the existing capabilities. Custom restrictions, tool gates, callbacks, and directory-scoped write roots therefore remain authoritative.

agent$set_permission_mode("readonly")

Create a newly configured Agent when broader or incomparable authority is required.

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
  )
)

Annotations describe behavior; they do not independently grant authority. In "readonly" mode, Deputy recognizes its built-in read tools and explicit allowlist entries, while still denying known writes, destructive tools, and open-world tools when web access is disabled. Unknown tools do not become authorized merely by declaring read_only_hint = TRUE. In "standard" mode, annotations participate in the configured capability checks.

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)

Run Limits

Permissions decide whether a tool may run. Configure resource limits separately with UsageLimits() on the agent or an individual run:

agent <- Agent$new(
  chat = ellmer::chat("openai"),
  tools = tools_file(),
  permissions = permissions_standard(),
  usage_limits = UsageLimits(
    max_requests = 10,
    max_tool_calls = 20,
    max_cost_usd = 1
  )
)

When a limit is reached, the agent stops and the AgentResult$stop_reason identifies the limit, such as "request_limit", "tool_call_limit", or "cost_limit".