> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentscope.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Permission System

> Fine-grained control over which tools your agents can execute and when

## Overview

The permission system intercepts every tool call an agent makes and produces one of three decisions: **allow** the tool to execute, **deny** it, or **ask the user** for confirmation.

The system combines static configuration with dynamic runtime analysis. Three components drive the decision together:

* **Rules** — explicit allow/deny/ask patterns per tool and command, evaluated with highest priority. Rules have two sources: statically pre-configured in `PermissionContext`, or dynamically added when the user accepts a **suggested rule** during an ASK prompt. Suggestions are auto-generated from the current tool call, so accepting one means future identical calls are handled automatically without prompting again.
* **Mode** — a static global policy set at configuration time; determines default behavior for all calls that match no rules (e.g., `EXPLORE` makes the agent read-only; `DONT_ASK` silently denies unmatched calls).
* **Built-in checks** — dynamic runtime analysis performed by the tools themselves against actual call inputs: read-only command detection (parsing the bash command at call time) and dangerous path protection (checking the real file path or command target). Tools can mark a safety ASK as **bypass-immune** via `PermissionDecision.bypass_immune=True`; the engine then honors it across `DEFAULT`, `ACCEPT_EDITS`, and `DONT_ASK` (allow rules cannot silence it). `BYPASS` mode is the one exception — it explicitly opts out of all safety ASKs by design.

```mermaid theme={null}
sequenceDiagram
    participant LLM
    participant PS as Permission System
    participant Tool
    participant User

    LLM->>PS: Tool Call
    Note over PS: Built-in Checks · Rules · Mode

    alt ALLOW
        PS->>Tool: execute
        Tool->>LLM: result
    else DENY
        PS->>LLM: denied
    else ASK + Suggestions
        PS->>User: ASK + Suggestions
        alt User approves
            User->>Tool: allow
            Tool->>LLM: result
            User-->>PS: accept suggested rule
        else User denies
            User->>PS: deny
            PS->>LLM: denied
        end
    end
```

Below is the decision flow for each mode. ASK outcomes trigger user confirmation; if the user accepts the auto-generated suggested rule, it is persisted for future calls.

<Tabs>
  <Tab title="DEFAULT">
    ```mermaid theme={null}
    flowchart TD
        A([Tool Call]) --> D1{Deny Rules?}
        D1 -->|Match| DENY([DENY])
        D1 -->|No| D2{Ask Rules?}
        D2 -->|Match| ASK([ASK])
        D2 -->|No| D3[Tool check_permissions]
        D3 -->|ALLOW| ALLOW([ALLOW])
        D3 -->|DENY| DENY
        D3 -->|"Safety ASK (bypass_immune)"| ASK
        D3 -->|PASSTHROUGH / other ASK| D4{Allow Rules?}
        D4 -->|Match| ALLOW
        D4 -->|No| ASK
        style DENY fill:#ff6b6b,color:#fff
        style ALLOW fill:#51cf66,color:#fff
        style ASK fill:#ffd43b,color:#333
    ```
  </Tab>

  <Tab title="EXPLORE">
    ```mermaid theme={null}
    flowchart TD
        A([Tool Call]) --> E1{Deny Rules?}
        E1 -->|Match| DENY([DENY])
        E1 -->|No| E2{Ask Rules?}
        E2 -->|Match| ASK([ASK])
        E2 -->|No| E3{check_read_only?}
        E3 -->|True| ALLOW([ALLOW])
        E3 -->|False| DENY
        style DENY fill:#ff6b6b,color:#fff
        style ALLOW fill:#51cf66,color:#fff
        style ASK fill:#ffd43b,color:#333
    ```
  </Tab>

  <Tab title="ACCEPT_EDITS">
    ```mermaid theme={null}
    flowchart TD
        A([Tool Call]) --> AE1{Deny Rules?}
        AE1 -->|Match| DENY([DENY])
        AE1 -->|No| AE2{Ask Rules?}
        AE2 -->|Match| ASK([ASK])
        AE2 -->|No| AE3{check_read_only?}
        AE3 -->|True| ALLOW([ALLOW])
        AE3 -->|False| AE4[Tool check_permissions]
        AE4 -->|ALLOW| ALLOW
        AE4 -->|DENY| DENY
        AE4 -->|"Safety ASK (bypass_immune)"| ASK
        AE4 -->|PASSTHROUGH / other ASK| AE5{Allow Rules?}
        AE5 -->|Match| ALLOW
        AE5 -->|No| ASK
        style DENY fill:#ff6b6b,color:#fff
        style ALLOW fill:#51cf66,color:#fff
        style ASK fill:#ffd43b,color:#333
    ```
  </Tab>

  <Tab title="BYPASS">
    ```mermaid theme={null}
    flowchart TD
        A([Tool Call]) --> B1{Deny Rules?}
        B1 -->|Match| DENY([DENY])
        B1 -->|No| B2{Ask Rules?}
        B2 -->|Match| ASK([ASK])
        B2 -->|No| B3[Tool check_permissions]
        B3 -->|ALLOW| ALLOW([ALLOW])
        B3 -->|DENY| DENY
        B3 -->|"Any ASK (safety ignored) / PASSTHROUGH"| B4{Allow Rules?}
        B4 -->|Match or No| ALLOW
        style DENY fill:#ff6b6b,color:#fff
        style ALLOW fill:#51cf66,color:#fff
        style ASK fill:#ffd43b,color:#333
    ```
  </Tab>

  <Tab title="DONT_ASK">
    ```mermaid theme={null}
    flowchart TD
        A([Tool Call]) --> DA1{Deny Rules?}
        DA1 -->|Match| DENY([DENY])
        DA1 -->|No| DA2{Ask Rules?}
        DA2 -->|Match| DENY
        DA2 -->|No| DA3[Tool check_permissions]
        DA3 -->|ALLOW| ALLOW([ALLOW])
        DA3 -->|DENY| DENY
        DA3 -->|"Any ASK (incl. safety)"| DENY
        DA3 -->|PASSTHROUGH| DA4{Allow Rules?}
        DA4 -->|Match| ALLOW
        DA4 -->|No| DENY
        style DENY fill:#ff6b6b,color:#fff
        style ALLOW fill:#51cf66,color:#fff
    ```
  </Tab>
</Tabs>

<Note>
  **Deny rules** and **explicit ask rules** are always honored, in every mode (including `BYPASS`).

  **Tool-emitted safety ASKs** (`bypass_immune=True`) are honored in `DEFAULT`, `ACCEPT_EDITS`, and `DONT_ASK` — they cannot be silenced by allow rules. In `BYPASS` mode they are skipped on purpose: BYPASS's contract is "the user has opted out of safety prompts; only deny/ask rules remain as guardrails."
</Note>

## Permission Mode

AgentScope supports the following modes, each suited to a different deployment scenario.

| Mode           | Behavior                                                                                                                                                                                                                                                                | Use Case                                     |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| `DEFAULT`      | All ops require explicit rules or user confirmation. The only built-in auto-allow is `Bash` recognizing read-only commands (`ls`, `git status`, `cat`, ...) and returning ALLOW. `Read` / `Glob` / `Grep` return PASSTHROUGH and still ASK unless an allow rule matches | Most secure, recommended default             |
| `ACCEPT_EDITS` | Auto-allow file ops in working directories; auto-allow Bash filesystem commands (`mkdir`/`touch`/`rm`/`cp`/`mv`/`sed`) **only when every target path resolves inside a working directory**                                                                              | Active development with user present         |
| `EXPLORE`      | Read-only: allow read-only tools and read-only Bash commands (`ls`, `git status`, `cat`, ...); deny modifications. User-configured DENY/ASK rules take precedence over the read-only auto-allow                                                                         | Code exploration, planning                   |
| `BYPASS`       | Skip all permission checks **except** deny/ask rules and tool DENY. **Tool safety ASKs are NOT enforced** (`rm -rf /`, writes to `~/.bashrc`, command injection, etc. all pass through). Use deny rules to protect specific paths                                       | Sandboxed environments or fully trusted runs |
| `DONT_ASK`     | Convert every ASK (default, ASK rules, **and** safety ASKs) to DENY; safe-by-default for non-interactive runs                                                                                                                                                           | Unattended / scheduled execution             |

Set the mode via `AgentState.permission_context` when creating the agent, or update it at runtime.

<CodeGroup>
  ```python At initialization theme={null}
  from agentscope.agent import Agent
  from agentscope.state import AgentState
  from agentscope.permission import PermissionContext, PermissionMode

  agent = Agent(
      name="my_agent",
      system_prompt="...",
      model=model,
      state=AgentState(
          permission_context=PermissionContext(
              mode=PermissionMode.DEFAULT,
          )
      ),
  )
  ```

  ```python At runtime theme={null}
  # Switch to read-only mode on the fly
  agent.state.permission_context.mode = PermissionMode.EXPLORE

  # Switch to unattended mode for batch execution
  agent.state.permission_context.mode = PermissionMode.DONT_ASK
  ```

  ```python ACCEPT_EDITS with working directory theme={null}
  from agentscope.permission import AdditionalWorkingDirectory

  agent = Agent(
      name="my_agent",
      system_prompt="...",
      model=model,
      state=AgentState(
          permission_context=PermissionContext(
              mode=PermissionMode.ACCEPT_EDITS,
              working_directories={
                  "/my/project": AdditionalWorkingDirectory(
                      path="/my/project",
                      source="userSettings",
                  )
              },
          )
      ),
  )
  ```
</CodeGroup>

## Permission Rules

A `PermissionRule` maps a specific tool and call pattern to one of three behaviors: `ALLOW`, `DENY`, or `ASK`.

Each rule consists of the following fields. When the permission engine evaluates a rule, it calls the tool's `match_rule()` method with `rule_content` and the actual call input to determine whether the rule applies.

<ParamField path="tool_name" type="str" required>
  Tool this rule applies to: `"Bash"`, `"Read"`, `"Write"`, `"Edit"`, or any custom tool name.
</ParamField>

<ParamField path="rule_content" type="str | None" required>
  Match pattern — semantics depend on `tool_name`:

  * **Bash**: wildcard prefix pattern (`npm run:*` matches `npm run build`, `npm run test`)
  * **Read / Write / Edit**: glob pattern (`src/**/*.py` matches any `.py` under `src/`)
  * **Other tools**: exact JSON-serialized parameter match
</ParamField>

<ParamField path="behavior" type="PermissionBehavior" required>
  `ALLOW`, `DENY`, or `ASK`
</ParamField>

<ParamField path="source" type="str" required>
  Origin of the rule: `"userSettings"`, `"projectSettings"`, `"session"`, etc.
</ParamField>

### Pattern Examples

`rule_content` is consumed by each tool's `match_rule()` method and auto-generated by `ToolBase.generate_suggestions()`. Because both methods are part of the tool interface, each tool can define its own pattern syntax and matching logic independently.

For AgentScope's built-in tools, the patterns are as follows:

<Tabs>
  <Tab title="Bash">
    Matches against the **`command`** parameter. Pattern format is `COMMAND_PREFIX:*` — the prefix is the leading token of the command, and `*` matches any arguments that follow.

    | Pattern        | Matches                         | Does Not Match |
    | -------------- | ------------------------------- | -------------- |
    | `npm run:*`    | `npm run build`, `npm run test` | `npm install`  |
    | `git commit:*` | `git commit -m "fix"`           | `git push`     |
    | `rm:*`         | `rm file.txt`, `rm -rf /tmp/x`  | `ls`           |

    ```python theme={null}
    PermissionRule(
        tool_name="Bash",
        rule_content="npm run:*",
        behavior=PermissionBehavior.ALLOW,
        source="userSettings",
    )
    ```
  </Tab>

  <Tab title="File Tools (Read / Write / Edit)">
    Matches against the **`file_path`** parameter using a glob pattern via `fnmatch`.

    | Pattern       | Matches                   |
    | ------------- | ------------------------- |
    | `src/**`      | Any file under `src/`     |
    | `src/**/*.py` | Python files under `src/` |
    | `config.json` | Exact file match          |

    ```python theme={null}
    PermissionRule(
        tool_name="Write",
        rule_content="src/**",
        behavior=PermissionBehavior.ALLOW,
        source="userSettings",
    )
    ```
  </Tab>
</Tabs>

### Configuring Rules

**At initialization** — pass rules into `PermissionContext` when creating the agent:

```python theme={null}
from agentscope.agent import Agent
from agentscope.state import AgentState
from agentscope.permission import (
    PermissionContext, PermissionMode, PermissionRule, PermissionBehavior
)

agent = Agent(
    name="my_agent",
    system_prompt="...",
    model=model,
    state=AgentState(
        permission_context=PermissionContext(
            mode=PermissionMode.DEFAULT,
            allow_rules={
                "Bash": [PermissionRule(tool_name="Bash", rule_content="npm run:*",
                                        behavior=PermissionBehavior.ALLOW, source="userSettings")],
                "Write": [PermissionRule(tool_name="Write", rule_content="src/**",
                                         behavior=PermissionBehavior.ALLOW, source="userSettings")],
            },
            deny_rules={
                "Bash": [PermissionRule(tool_name="Bash", rule_content="rm:*",
                                        behavior=PermissionBehavior.DENY, source="userSettings")],
            },
        )
    ),
)
```

**At runtime via suggestions** — when the permission system returns ASK, it auto-generates suggested rules from the current call. Pass accepted rules back in `UserConfirmResultEvent.rules`; the agent adds them to the engine automatically:

```python theme={null}
from agentscope.event import UserConfirmResultEvent

# The ASK decision includes suggested_rules generated from the current call.
# To accept a suggestion, include it in the result event:
result = UserConfirmResultEvent(
    confirmed=True,
    rules=[suggested_rule],  # accepted rules are persisted to the engine
)
```

## Built-in Checks

Each tool implements a `check_permissions()` method that runs against the actual call inputs at runtime. AgentScope's built-in tools cover three areas:

* **Dangerous path protection** — `Write`, `Edit`, and `Bash` check whether the target file or command touches sensitive paths. Returns a bypass-immune safety ASK that is honored in `DEFAULT`/`ACCEPT_EDITS`/`DONT_ASK` (allow rules cannot silence it). `BYPASS` mode skips it on purpose.
* **Read-only command detection** — `Bash` parses the command string to detect read-only operations and auto-allows them in **every** mode (including `DEFAULT`). For input-dependent tools like `Bash`, this is exposed via the `check_read_only()` method (see below).
* **ACCEPT\_EDITS mode** — `Write` and `Edit` auto-allow operations on files within configured working directories. `Bash` additionally requires that **every** target path of a filesystem command (`mkdir`/`touch`/`rm`/`cp`/`mv`/`sed`, ...) resolves inside a working directory.

### Custom tools

A custom tool implements `check_permissions()` to add tool-specific permission logic. Tools whose read-only status depends on the input (like `Bash` — `ls` is read-only, `rm` is not) should also override `check_read_only()`.

```python theme={null}
from agentscope.tool import ToolBase
from agentscope.permission import PermissionContext, PermissionDecision, PermissionBehavior

class MyTool(ToolBase):
    name = "MyTool"
    # Static default. For tools whose answer depends on input, leave
    # this as the conservative default and override check_read_only().
    is_read_only = False

    async def check_read_only(self, tool_input: dict) -> bool:
        """Optional: dynamic read-only check.

        Defaults to returning self.is_read_only. Override when whether
        an invocation modifies state depends on the input. The engine
        calls this to decide auto-allow in EXPLORE and ACCEPT_EDITS.
        """
        return tool_input.get("operation") in {"list", "describe", "get"}

    async def check_permissions(
        self,
        tool_input: dict,
        context: PermissionContext,
    ) -> PermissionDecision:
        target = tool_input.get("target")

        # Custom safety check: block operations on production resources.
        # Setting bypass_immune=True makes this ASK survive allow rules
        # in DEFAULT/ACCEPT_EDITS/DONT_ASK; BYPASS still skips it.
        if target and target.startswith("prod-"):
            return PermissionDecision(
                behavior=PermissionBehavior.ASK,
                message=f"Operation targets production resource: {target}",
                decision_reason="Safety check: production resource",
                bypass_immune=True,
            )

        # Return PASSTHROUGH to let the engine continue with rules/mode
        return PermissionDecision(behavior=PermissionBehavior.PASSTHROUGH)
```

### Safety check contract

A **safety check** is a tool-emitted ASK that the tool considers too dangerous to be silently overridden — e.g. `Write` to `~/.bashrc`, `Bash` with `rm -rf /`. Setting `bypass_immune=True` on the decision asks the engine to surface the ASK to the user even when an allow rule matches or the mode would otherwise auto-allow.

Use it whenever a wrong call would cause damage the user almost certainly didn't intend. Example: a custom `DeployTool` returns `bypass_immune=True` when the target is `prod-*`, so a blanket `allow_rules["DeployTool"] = ["*"]` configured for staging cannot accidentally authorize a production deploy.

The exact handling per mode:

| Mode           | `bypass_immune=True` ASK is...                                                                           |
| -------------- | -------------------------------------------------------------------------------------------------------- |
| `DEFAULT`      | honored — allow rules cannot override it                                                                 |
| `ACCEPT_EDITS` | honored — same as `DEFAULT`                                                                              |
| `EXPLORE`      | not applicable (the engine does not call `check_permissions` in EXPLORE; the read-only verdict is final) |
| `BYPASS`       | **ignored** — BYPASS skips all safety ASKs by design                                                     |
| `DONT_ASK`     | converted to DENY (no user available to answer)                                                          |

A regular ASK (`bypass_immune=False`, the default) can be overridden by a matching allow rule in `DEFAULT`/`ACCEPT_EDITS`, and is silently allowed by `BYPASS`'s fallback.

### Read-Only Commands

Common read-only bash commands are auto-allowed without any rules, in **every mode** (including `DEFAULT`). A compound command (`&&`, `||`, `;`, `|`) is read-only only if **all** subcommands are read-only. Output redirections (`>`, `>>`) always make a command non-read-only.

<AccordionGroup>
  <Accordion title="Full read-only command list">
    | Category         | Commands                                                                                                                  |
    | ---------------- | ------------------------------------------------------------------------------------------------------------------------- |
    | Git              | `git status`, `git log`, `git diff`, `git show`, `git branch`, `git blame`, `git grep`, `git reflog`, `git config --list` |
    | Files            | `ls`, `cat`, `head`, `tail`, `grep`, `rg`, `find`, `tree`, `stat`, `wc`, `pwd`, `which`                                   |
    | Docker           | `docker ps`, `docker images`, `docker logs`, `docker inspect`, `docker info`                                              |
    | GitHub CLI       | `gh repo view`, `gh issue list`, `gh pr list`, `gh status`                                                                |
    | Package managers | `npm list`, `pip list`, `pip show`, `node --version`, `python --version`                                                  |
  </Accordion>
</AccordionGroup>

### Dangerous Path Protection

<Warning>
  Operations targeting the following paths trigger a bypass-immune ASK in `DEFAULT`, `ACCEPT_EDITS`, and `DONT_ASK` (converted to DENY in `DONT_ASK`). `BYPASS` mode explicitly skips this check — if you need dangerous-path protection while running in BYPASS, add deny rules for the specific paths.
</Warning>

| Category      | Paths                                                         |
| ------------- | ------------------------------------------------------------- |
| Shell configs | `.bashrc`, `.zshrc`, `.bash_profile`, `.profile`              |
| Git configs   | `.gitconfig`, `.gitmodules`                                   |
| SSH           | `.ssh/config`, `.ssh/authorized_keys`, `id_rsa`, `id_ed25519` |
| Credentials   | `.env`, `.env.local`, `.npmrc`, `.pypirc`, `.aws/credentials` |
| Directories   | `.git/`, `.ssh/`, `.claude/`, `.vscode/`, `.aws/`, `.kube/`   |

## Common Recipes

The following examples show how to configure `AgentState.permission_context` for common deployment scenarios. Each recipe combines a mode with rules to match a specific use case.

<CodeGroup>
  ```python Read-only exploration theme={null}
  # EXPLORE mode: agent can freely use read-only tools (Read, Grep,
  # Glob) and read-only bash commands (`ls`, `git status`, `cat`, ...).
  # Any modification — Write, Edit, or non-read-only bash command — is
  # denied automatically.
  agent = Agent(
      name="explorer",
      system_prompt="...",
      model=model,
      state=AgentState(
          permission_context=PermissionContext(mode=PermissionMode.EXPLORE)
      ),
  )
  ```

  ```python Unattended automation theme={null}
  from agentscope.permission import PermissionRule, PermissionBehavior

  agent = Agent(
      name="ci_agent",
      system_prompt="...",
      model=model,
      state=AgentState(
          permission_context=PermissionContext(
              mode=PermissionMode.DONT_ASK,
              allow_rules={
                  "Bash": [
                      PermissionRule(tool_name="Bash", rule_content="npm run:*",
                                     behavior=PermissionBehavior.ALLOW, source="project"),
                      PermissionRule(tool_name="Bash", rule_content="git commit:*",
                                     behavior=PermissionBehavior.ALLOW, source="project"),
                  ],
              },
          )
      ),
  )
  # Only explicitly allowed commands run; everything else (including
  # safety ASKs like `rm -rf /` or writes to ~/.bashrc) is converted
  # to DENY. Prefer DONT_ASK over BYPASS for unattended runs — it keeps
  # the tools' safety net while still never prompting the user.
  ```

  ```python BYPASS with explicit guardrails theme={null}
  # BYPASS skips tools' safety ASKs by design — deny rules become the
  # only guardrail. Always pair BYPASS with deny rules for the paths
  # and commands you want to protect.
  agent = Agent(
      name="my_agent",
      system_prompt="...",
      model=model,
      state=AgentState(
          permission_context=PermissionContext(
              mode=PermissionMode.BYPASS,
              deny_rules={
                  "Bash": [
                      PermissionRule(tool_name="Bash", rule_content="rm:*",
                                     behavior=PermissionBehavior.DENY, source="userSettings"),
                      PermissionRule(tool_name="Bash", rule_content="git push:*",
                                     behavior=PermissionBehavior.DENY, source="userSettings"),
                  ],
                  "Write": [
                      PermissionRule(tool_name="Write", rule_content="**/.bashrc",
                                     behavior=PermissionBehavior.DENY, source="userSettings"),
                      PermissionRule(tool_name="Write", rule_content="**/.ssh/**",
                                     behavior=PermissionBehavior.DENY, source="userSettings"),
                  ],
              },
          )
      ),
  )
  # Everything allowed except the deny-listed commands and paths.
  # Without these deny rules, BYPASS would let the agent rm anything,
  # git push anywhere, or overwrite ~/.bashrc — by design.
  ```
</CodeGroup>
