> ## Documentation Index
> Fetch the complete documentation index at: https://java.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 (`io.agentscope.core.permission`) intercepts every tool call the agent makes and produces one of three decisions: **ALLOW**, **DENY**, or **ASK** (request user confirmation).

It combines static configuration with dynamic runtime analysis. Three components together decide the outcome:

* **Rules** — explicit allow / deny / ask patterns per tool and command, with the highest priority. Rules come from two sources: static configuration in `PermissionContextState`, or **suggested rules** added dynamically when the user accepts them at an ASK prompt. Suggested rules are auto-generated from the current invocation — once accepted, identical future calls are auto-handled without prompting.
* **Mode** — a global static policy set at configuration time; decides the default behaviour for calls that match no rule (e.g. `EXPLORE` makes the agent read-only, `DONT_ASK` silently denies anything not matching a rule).
* **Built-in Checks** — runtime analysis performed by the tool itself based on the actual input (implemented in `ToolBase#checkPermissions`). These are runtime checks rather than preconfigured patterns, so they are **non-bypassable** — they are not subject to mode or rules.

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

<Accordion title="Detailed decision flow">
  ```mermaid theme={null}
  flowchart TD
      A([Tool Call]) --> B{Deny Rules?}
      B -->|Match| DENY([DENY])
      B -->|No Match| C{Ask Rules?}
      C -->|Match| ASK1([ASK])
      C -->|No Match| D{Tool-Specific Checks}
      D -->|EXPLORE + write op| DENY
      D -->|Dangerous path| ASK2([ASK])
      D -->|Pass| E{Allow Rules?}
      E -->|Match| ALLOW([ALLOW])
      E -->|No Match| F{"ACCEPT_EDITS + safe file op?"}
      F -->|Yes| ALLOW
      F -->|No| G{"Read-only Bash command?"}
      G -->|Yes| ALLOW
      G -->|No| H{BYPASS mode?}
      H -->|Yes| ALLOW
      H -->|No| I{DONT_ASK mode?}
      I -->|Yes| DENY
      I -->|No| ASK3([ASK])
      ASK1 --> S[Generate Suggestions]
      ASK2 --> S
      ASK3 --> S
      S --> U{User Confirms?}
      U -->|Approve| ALLOW
      U -->|Deny| DENY
      U -->|Apply Rule| R[Update Context] --> ALLOW
      style DENY fill:#ff6b6b,color:#fff
      style ALLOW fill:#51cf66,color:#fff
      style ASK1 fill:#ffd43b,color:#333
      style ASK2 fill:#ffd43b,color:#333
      style ASK3 fill:#ffd43b,color:#333
  ```
</Accordion>

<Note>
  Deny rules and dangerous-path checks are **non-bypassable** — they apply even in `BYPASS` mode.
</Note>

## Permission Mode

The `PermissionMode` enum (`io.agentscope.core.permission.PermissionMode`) supports the following modes:

| Mode           | Behaviour                                                  | Use case                                 |
| -------------- | ---------------------------------------------------------- | ---------------------------------------- |
| `DEFAULT`      | All operations require explicit rules or user confirmation | Safest default, recommended              |
| `ACCEPT_EDITS` | Auto-allow file ops inside the working directory           | Active development with the user present |
| `EXPLORE`      | Read-only: allow reads, deny all writes and commands       | Code exploration, planning               |
| `BYPASS`       | Allow everything (deny / ask rules still apply)            | Fully trusted sandbox                    |
| `DONT_ASK`     | Demote ASK to DENY                                         | Unattended / scheduled runs              |

Set the mode on the agent builder via `permissionContext(...)`:

<Tabs>
  <Tab title="Initial config">
    ```java theme={null}
    import io.agentscope.core.ReActAgent;
    import io.agentscope.core.permission.PermissionContextState;
    import io.agentscope.core.permission.PermissionMode;

    PermissionContextState permCtx =
            PermissionContextState.builder()
                    .mode(PermissionMode.DEFAULT)
                    .build();

    ReActAgent agent =
            ReActAgent.builder()
                    .name("my_agent")
                    .sysPrompt("...")
                    .model(model)
                    .permissionContext(permCtx)
                    .build();
    ```
  </Tab>

  <Tab title="ACCEPT_EDITS with working dir">
    ```java theme={null}
    import io.agentscope.core.permission.AdditionalWorkingDirectory;
    import io.agentscope.core.permission.PermissionContextState;
    import io.agentscope.core.permission.PermissionMode;

    PermissionContextState permCtx =
            PermissionContextState.builder()
                    .mode(PermissionMode.ACCEPT_EDITS)
                    .addWorkingDirectory(
                            "/my/project",
                            new AdditionalWorkingDirectory("/my/project", "userSettings"))
                    .build();
    ```
  </Tab>
</Tabs>

## Permission Rule

`PermissionRule` (a record) maps a tool plus a specific call pattern to one of three behaviours: `ALLOW`, `DENY`, `ASK`.

Each rule has the fields below. When the engine evaluates a rule, it calls the tool's `matchRule()` with the `ruleContent` and the actual input to decide whether the rule fires.

* **`toolName` · `String` · *required*** — The tool name the rule applies to: `todo_write` (built-in) or any custom tool name.

* **`ruleContent` · `String | null` · *optional*** — Match pattern — semantics depend on the tool, interpreted by the tool's `matchRule()`. `null` means the rule matches every invocation of that tool.

* **`behavior` · `PermissionBehavior` · *required*** — `ALLOW`, `DENY`, `ASK`, or `PASSTHROUGH`

* **`source` · `String` · *required*** — Origin of the rule: `"userSettings"`, `"projectSettings"`, `"session"`, `"suggested"`, …

### Configuring rules

**At init time** — pass rules through `PermissionContextState.builder()`:

```java theme={null}
import io.agentscope.core.permission.PermissionBehavior;
import io.agentscope.core.permission.PermissionContextState;
import io.agentscope.core.permission.PermissionMode;
import io.agentscope.core.permission.PermissionRule;

PermissionContextState permCtx =
        PermissionContextState.builder()
                .mode(PermissionMode.DEFAULT)
                .addAllowRule(
                        "safe_read",
                        new PermissionRule(
                                "safe_read", null, PermissionBehavior.ALLOW, "userSettings"))
                .addAskRule(
                        "dangerous_delete",
                        new PermissionRule(
                                "dangerous_delete",
                                null,
                                PermissionBehavior.ASK,
                                "userSettings"))
                .addDenyRule(
                        "drop_table",
                        new PermissionRule(
                                "drop_table", null, PermissionBehavior.DENY, "userSettings"))
                .build();
```

**At runtime via suggested rules** — when the permission system returns ASK, it auto-generates suggested rules based on the current invocation. Pass the accepted rules in `ConfirmResult` and the agent will write them into the engine:

```java theme={null}
import io.agentscope.core.event.ConfirmResult;

// ASK decisions carry suggestedRules on the ToolUseBlock.
// Accept them by attaching to the result:
ConfirmResult result =
        new ConfirmResult(
                /* confirmed = */ true,
                /* toolCall  = */ toolCall,
                /* rules     = */ toolCall.getSuggestedRules());
```

Runnable examples: `agentscope-examples/documentation/.../tool/PermissionContextExample.java`, `hitl/PermissionHITLExample.java`.

## Built-in checks

Every tool implements `checkPermissions(toolInput, context)` (on `ToolBase`) — a runtime check on the actual input that returns `Mono<PermissionDecision>`. These checks cannot be bypassed: they apply regardless of mode or rules.

`PermissionDecision` provides four static factories: `allow(message)` / `deny(message)` / `ask(message)` / `passthrough(message)`. Returning `PASSTHROUGH` means "I'm not deciding — let the engine evaluate rules and mode."

A custom tool can override `checkPermissions()` for its own logic:

```java theme={null}
import io.agentscope.core.permission.PermissionDecision;
import io.agentscope.core.tool.ToolBase;
import io.agentscope.core.tool.ToolExecutionContext;
import java.util.Map;
import reactor.core.publisher.Mono;

public class MyTool extends ToolBase {

    public MyTool() {
        super(
                ToolBase.builder()
                        .name("MyTool")
                        .description("...")
                        .readOnly(false));
    }

    @Override
    public Mono<PermissionDecision> checkPermissions(
            Map<String, Object> toolInput, ToolExecutionContext context) {
        Object target = toolInput.get("target");

        // Custom safety check: block production resources.
        if (target instanceof String s && s.startsWith("prod-")) {
            return Mono.just(
                    PermissionDecision.ask("Operation targets production resource: " + s));
        }

        // Return PASSTHROUGH to let the engine continue evaluating rules / mode.
        return Mono.just(PermissionDecision.passthrough("default"));
    }
}
```

### Dangerous-path protection

The `ToolBase` dangerous-path list is maintained in `ToolDangerousPathConstants`. A custom tool can append more paths via the `dangerousFiles` / `dangerousDirectories` attributes on `@Tool`. Once matched, the path triggers ASK even in `BYPASS` mode.

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

## HITL integration

When the permission engine returns an ASK decision for a tool call, the agent pauses instead of executing and returns a response with `GenerateReason.PERMISSION_ASKING`. The returned `Msg` contains the `ToolUseBlock`s in `ASKING` state. The caller extracts them, presents the pending operation to the user, and resumes the agent with `ConfirmResult` objects.

### Interaction flow

1. Configure ASK rules for tools that require human confirmation
2. Agent pauses on ASK tools, returning `PERMISSION_ASKING`
3. Extract `ToolUseBlock`s (with `ASKING` state) from the returned `Msg` and show them to the user
4. Build `ConfirmResult` objects and attach them to the resume message via metadata

```java theme={null}
import io.agentscope.core.event.ConfirmResult;
import io.agentscope.core.message.GenerateReason;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.MsgRole;
import io.agentscope.core.message.ToolCallState;
import io.agentscope.core.message.ToolUseBlock;
import io.agentscope.core.message.UserMessage;
import io.agentscope.core.permission.PermissionBehavior;
import io.agentscope.core.permission.PermissionContextState;
import io.agentscope.core.permission.PermissionMode;
import io.agentscope.core.permission.PermissionRule;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

// 1. Configure permissions: safe_read auto-allowed, dangerous_delete requires confirmation
PermissionContextState permCtx =
        PermissionContextState.builder()
                .mode(PermissionMode.DEFAULT)
                .addAllowRule(
                        "safe_read",
                        new PermissionRule(
                                "safe_read", null, PermissionBehavior.ALLOW, "policy"))
                .addAskRule(
                        "dangerous_delete",
                        new PermissionRule(
                                "dangerous_delete", null, PermissionBehavior.ASK, "policy"))
                .build();

ReActAgent agent =
        ReActAgent.builder()
                .name("GuardedAgent")
                .sysPrompt("...")
                .model(model)
                .toolkit(toolkit)
                .permissionContext(permCtx)
                .build();

// 2. Call the agent
Msg result = agent.call(new UserMessage("Delete /tmp/important.txt")).block();

// 3. Check whether user confirmation is needed
if (result != null && result.getGenerateReason() == GenerateReason.PERMISSION_ASKING) {
    // Extract the ASKING ToolUseBlocks from the returned Msg
    List<ToolUseBlock> askingTools =
            result.getContent().stream()
                    .filter(b -> b instanceof ToolUseBlock)
                    .map(ToolUseBlock.class::cast)
                    .filter(t -> t.getState() == ToolCallState.ASKING)
                    .toList();

    // Show pending operations to the user
    askingTools.forEach(t -> System.out.println("Pending: " + t.getName() + " " + t.getInput()));

    // 4. Collect the user's decision, build ConfirmResult, and resume
    boolean approved = askUser();
    List<ConfirmResult> confirmResults =
            askingTools.stream()
                    .map(t -> new ConfirmResult(approved, t))
                    .toList();

    Map<String, Object> meta = new HashMap<>();
    meta.put(Msg.METADATA_CONFIRM_RESULTS, confirmResults);
    Msg resumeMsg =
            Msg.builder()
                    .name("user")
                    .role(MsgRole.USER)
                    .textContent(approved ? "approved" : "denied")
                    .metadata(meta)
                    .build();

    Msg finalResult = agent.call(List.of(resumeMsg)).block();
}
```

### All tools denied

When the user denies **all** tool calls from a reasoning step in the confirmation UI, the agent continues to the next reasoning iteration by default — the model only sees "Permission denied by user" tool results, which often leads to unhelpful reasoning.

To stop the agent in this scenario, wire up an `onActing` middleware that observes `AllToolsDeniedEvent` and emits a `RequestStopEvent`. After stopping, `Msg.getGenerateReason()` returns `ALL_TOOLS_DENIED`.

See [Middleware — Stop agent when all tools are denied](/v2/en/docs/building-blocks/middleware#stop-agent-when-all-tools-are-denied) for the implementation.

### Streaming mode

When using `streamEvents()`, you don't need to extract `ToolUseBlock`s from the returned `Msg` — the event stream delivers a `RequireUserConfirmEvent` that carries the pending tool calls directly:

```java theme={null}
import io.agentscope.core.event.AgentEvent;
import io.agentscope.core.event.ConfirmResult;
import io.agentscope.core.event.RequireUserConfirmEvent;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.MsgRole;
import io.agentscope.core.message.ToolUseBlock;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

// Subscribe to the event stream
agent.streamEvents(List.of(new UserMessage("Delete /tmp/important.txt")))
        .doOnNext(event -> {
            if (event instanceof RequireUserConfirmEvent confirmEvent) {
                // Get pending ToolUseBlocks directly from the event
                List<ToolUseBlock> pending = confirmEvent.getToolCalls();
                pending.forEach(t ->
                        System.out.println("Pending: " + t.getName() + " " + t.getInput()));

                // Collect user decision, store pending list for the resume call
            }
        })
        .blockLast();

// Resume is the same as with the blocking API: build ConfirmResult in metadata
List<ConfirmResult> confirmResults =
        pendingTools.stream()
                .map(t -> new ConfirmResult(true, t))
                .toList();
Map<String, Object> meta = new HashMap<>();
meta.put(Msg.METADATA_CONFIRM_RESULTS, confirmResults);
Msg resumeMsg =
        Msg.builder()
                .name("user")
                .role(MsgRole.USER)
                .textContent("approved")
                .metadata(meta)
                .build();
agent.call(List.of(resumeMsg)).block();
```

If the resume is sent with `streamEvents(List.of(resumeMsg))`, the stream includes a
`UserConfirmResultEvent` before the resumed tool execution. Use its `replyId` to associate
the accepted results with the earlier `RequireUserConfirmEvent`; the event contains only
the confirmations included in that resume call.

Comparison of the two modes:

|                       | Blocking `call()`                                                | Streaming `streamEvents()`                                 |
| --------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------- |
| Getting pending tools | Filter `ToolUseBlock`s (state `ASKING`) from `Msg.getContent()`  | Get directly from `RequireUserConfirmEvent.getToolCalls()` |
| Resuming              | Same: build `ConfirmResult` in metadata and issue a new `call()` | Same                                                       |
| Use case              | REST APIs, simple synchronous services                           | WebSocket, SSE, real-time UIs                              |

### Unattended mode

In CI or cron-job scenarios with no human operator, set the mode to `DONT_ASK` so that all ASK decisions degrade to DENY automatically:

```java theme={null}
PermissionContextState headless =
        PermissionContextState.builder()
                .mode(PermissionMode.DONT_ASK)
                .addAllowRule(
                        "safe_read",
                        new PermissionRule(
                                "safe_read", null, PermissionBehavior.ALLOW, "policy"))
                .build();
// ASK-rule hits are auto-denied — no blocking wait
```

Full runnable example: `agentscope-examples/documentation/.../hitl/PermissionHITLExample.java`.

## Common recipes

The examples below show how to configure `permissionContext` for typical deployment scenarios. Each recipe combines a mode with a rule set tuned for one use case.

<Tabs>
  <Tab title="Read-only exploration">
    ```java theme={null}
    // EXPLORE mode: agent freely calls read-only tools; all writes are auto-denied.
    PermissionContextState explore =
            PermissionContextState.builder()
                    .mode(PermissionMode.EXPLORE)
                    .build();

    ReActAgent explorer =
            ReActAgent.builder()
                    .name("explorer")
                    .sysPrompt("...")
                    .model(model)
                    .permissionContext(explore)
                    .build();
    ```
  </Tab>

  <Tab title="Unattended automation">
    ```java theme={null}
    import io.agentscope.core.permission.PermissionBehavior;
    import io.agentscope.core.permission.PermissionRule;

    PermissionContextState ci =
            PermissionContextState.builder()
                    .mode(PermissionMode.DONT_ASK)
                    .addAllowRule(
                            "deploy",
                            new PermissionRule(
                                    "deploy", "staging", PermissionBehavior.ALLOW, "project"))
                    .addAllowRule(
                            "git_commit",
                            new PermissionRule(
                                    "git_commit", null, PermissionBehavior.ALLOW, "project"))
                    .build();

    ReActAgent ciAgent =
            ReActAgent.builder()
                    .name("ci_agent")
                    .sysPrompt("...")
                    .model(model)
                    .permissionContext(ci)
                    .build();
    // Only explicitly allowed commands run; everything else is silently denied.
    ```
  </Tab>

  <Tab title="Block dangerous commands">
    ```java theme={null}
    PermissionContextState bypassWithDeny =
            PermissionContextState.builder()
                    .mode(PermissionMode.BYPASS)
                    .addDenyRule(
                            "drop_table",
                            new PermissionRule(
                                    "drop_table", null, PermissionBehavior.DENY, "userSettings"))
                    .addDenyRule(
                            "force_push",
                            new PermissionRule(
                                    "force_push", null, PermissionBehavior.DENY, "userSettings"))
                    .build();
    // Everything except the explicitly denied tools runs (deny rules can't be bypassed).
    ```
  </Tab>
</Tabs>
