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

# 权限控制

Qoder Agent SDK 的权限控制能力用于管理模型在一次 `query()` 会话里能做什么。它可以限制模型可见的工具，设置默认授权策略，在工具执行前交给宿主应用审批，也可以在用户授权后把新规则应用到当前会话。

权限控制不是一个单独的 API，而是一组放在 `QoderAgentOptions` 里的配置。通常你会先决定本次会话允许模型使用哪些工具，再决定这些工具在什么情况下可以执行，最后按需要接入运行时审批、动态规则更新、settings 或 hooks。

```python theme={null}
from qoder_agent_sdk import QoderAgentOptions, query


async for message in query(
    prompt="Inspect the repository and summarize risky changes.",
    options=QoderAgentOptions(
        cwd="/path/to/project",
        tools=["Read", "Grep", "Bash"],
        allowed_tools=["Read", "Grep"],
        disallowed_tools=["Bash"],
        permission_mode="default",
    ),
):
    print(message)
```

上面这个例子表达了一个常见策略：模型可以看到 `Read`、`Grep`、`Bash`，其中 `Read` 和 `Grep` 预授权，`Bash` 禁止执行。实际项目里，你可以继续加上 `can_use_tool`，把未预授权的操作交给自己的产品 UI、审批系统或风控服务判断。

<div id="能力总览" />

## 能力总览

`query()` 的权限相关 options 大致分成四类。第一类决定默认策略，例如计划模式、自动允许编辑、禁止交互询问。第二类决定工具范围和工具规则。第三类让宿主应用参与运行时审批。第四类用于更高级的 settings、hooks 和 MCP 工具策略。

| 想解决的问题              | 推荐入口                                 | 说明                                    |
| ------------------- | ------------------------------------ | ------------------------------------- |
| 设置会话默认权限行为          | `permission_mode`                    | 决定未命中显式规则时如何处理工具调用                    |
| 明确确认跳过权限检查          | `allow_dangerously_skip_permissions` | 仅配合 `bypassPermissions` 或 `yolo` 使用   |
| 限制本次会话可见工具          | `tools`                              | 未包含的工具不会作为可用工具提供给模型                   |
| 预授权某些工具             | `allowed_tools`                      | 命中后通常不再进入授权询问                         |
| 禁止某些工具              | `disallowed_tools`                   | 命中后拒绝，优先级高于 allow                     |
| 由宿主应用审批工具调用         | `can_use_tool`                       | SDK host 在运行时返回 allow 或 deny          |
| 把审批交给外部 prompt tool | `permission_prompt_tool_name`        | 适合运行环境已经提供 permission prompt tool 的场景 |
| 授权后更新当前会话规则         | `PermissionUpdate`                   | 常用于“本次允许”或“本会话始终允许”                   |
| 允许访问 cwd 外目录        | `add_dirs`                           | 扩展本次会话可访问的目录范围                        |
| 从 settings 配置权限规则   | `settings`                           | 适合启动会话时提供静态权限配置                       |
| 在生命周期里拦截或审计         | `hooks`                              | 适合高级拦截、审计和告警                          |
| 给 MCP server 声明工具策略 | MCP tool policy                      | 在 MCP server 配置里声明工具级 allow/ask/deny  |

常见选择方式：

* 只想让模型读代码，不允许修改：设置 `tools`，再用 `allowed_tools` 预授权 `Read`、`Grep`，用 `disallowed_tools` 禁止 `Write`、`Edit`、`Bash`。
* 想让模型先给计划，不做实际变更：使用 `permission_mode="plan"`。
* 想让安全的编辑自动通过：使用 `permission_mode="acceptEdits"` 或 `"auto"`。
* 想让自己的 UI 弹出审批框：使用 `can_use_tool`。
* 想让用户选择“始终允许本会话”：在 `can_use_tool` 的 allow 结果里返回 `updated_permissions`。
* 想完全不弹交互确认，未预授权直接拒绝：使用 `permission_mode="dontAsk"`。
* 想访问 monorepo 之外的共享目录：使用 `add_dirs`。

后续示例为了聚焦权限配置，会省略遍历消息的代码；真实使用时仍需要消费 `query()` 返回的异步消息流。Python SDK 默认可以使用本地 `qodercli` 登录态；如果你的应用需要显式认证，可以在 `QoderAgentOptions.auth` 中传入自己的认证配置。

<div id="快速开始让宿主应用审批工具调用" />

## 快速开始：让宿主应用审批工具调用

当你需要把工具调用接入自己的审批逻辑时，使用 `can_use_tool`。SDK 会在运行时把工具名、工具入参和一组可展示的审批信息交给你的 callback。callback 返回 `PermissionResultAllow` 时工具继续执行，返回 `PermissionResultDeny` 时工具被拒绝。

```python theme={null}
from typing import Any

from qoder_agent_sdk import create_sdk_mcp_server, query, tool
from qoder_agent_sdk.types import (
    PermissionResultAllow,
    PermissionResultDeny,
    QoderAgentOptions,
    ToolPermissionContext,
)


@tool("read_order", "Read an order by ID.", {"order_id": str})
async def read_order(args: dict[str, Any]) -> dict[str, Any]:
    return {"content": [{"type": "text", "text": f"order:{args['order_id']}"}]}


server = create_sdk_mcp_server(name="orders", tools=[read_order])


async def can_use_tool(
    tool_name: str,
    input_data: dict[str, Any],
    context: ToolPermissionContext,
):
    if tool_name != "mcp__orders__read_order":
        return PermissionResultDeny(
            message="Only order reads are allowed in this workflow."
        )

    return PermissionResultAllow(updated_input=input_data)


async for _ in query(
    prompt="Read order 1001.",
    options=QoderAgentOptions(
        mcp_servers={"orders": server},
        permission_mode="default",
        can_use_tool=can_use_tool,
    ),
):
    pass
```

这个例子里，`read_order` 是一个 SDK MCP 工具。模型调用它时，完整工具名会是 `mcp__orders__read_order`。`can_use_tool` 只允许这个工具执行，并把原始入参作为 `updated_input` 返回。

<div id="控制默认策略permission_mode" />

## 控制默认策略：permission\_mode

`permission_mode` 决定会话的默认权限策略。它适合表达“这次会话整体处于什么模式”，例如先计划、自动接受编辑、不询问直接拒绝，或者在受控环境里跳过权限检查。

```python theme={null}
QoderAgentOptions(
    cwd="/path/to/project",
    permission_mode="plan",
)
```

`plan` 模式适合让模型先产出计划，默认不执行实际变更。

| Mode                | 行为                                                                          |
| ------------------- | --------------------------------------------------------------------------- |
| `default`           | 标准权限行为。工具调用按 tools、allow/deny 规则、动态审批或运行时策略处理                               |
| `acceptEdits`       | 自动接受文件编辑类操作，适合已确认可以修改工作区的场景                                                 |
| `bypassPermissions` | 跳过权限检查，必须同时设置 `allow_dangerously_skip_permissions=True`                     |
| `yolo`              | `bypassPermissions` 的兼容别名，也必须同时设置 `allow_dangerously_skip_permissions=True` |
| `plan`              | 计划模式，适合先产出执行计划，默认不执行实际变更                                                    |
| `dontAsk`           | 不进行交互询问。未预授权、未被规则允许的操作会被拒绝                                                  |
| `auto`              | 由运行时能力自动判断 allow 或 deny。安全的工作区内文件编辑可能自动放行                                   |

需要在同一个会话里切换模式时，可以使用 `QoderSDKClient`：

```python theme={null}
from qoder_agent_sdk import QoderSDKClient


async with QoderSDKClient(
    options=QoderAgentOptions(
        cwd="/path/to/project",
        permission_mode="plan",
    )
) as client:
    await client.set_permission_mode("default")
```

`bypassPermissions` 和 `yolo` 都是高风险模式。SDK 要求显式传入 `allow_dangerously_skip_permissions=True`，避免调用方误把普通会话变成跳过权限检查的会话。

```python theme={null}
QoderAgentOptions(
    cwd="/path/to/project",
    permission_mode="bypassPermissions",
    allow_dangerously_skip_permissions=True,
)
```

<div id="控制工具范围toolsallowed_toolsdisallowed_tools" />

## 控制工具范围：tools、allowed\_tools、disallowed\_tools

工具控制回答的是“模型能看到哪些工具，以及哪些工具默认允许或禁止”。这三个字段经常一起出现，但语义不同。

```python theme={null}
QoderAgentOptions(
    cwd="/path/to/project",
    tools=["Read", "Grep", "Bash"],
    allowed_tools=["Read", "Grep"],
    disallowed_tools=["Bash"],
)
```

这段配置表示：本次会话只提供 `Read`、`Grep`、`Bash` 三个工具；其中 `Read` 和 `Grep` 预授权；`Bash` 被禁止，即使模型想调用也不会执行。

| 字段                 | 作用           | 适合场景         |
| ------------------ | ------------ | ------------ |
| `tools`            | 限制本次会话可用工具集合 | 缩小模型能力边界     |
| `allowed_tools`    | 添加允许规则       | 让低风险工具无需反复审批 |
| `disallowed_tools` | 添加拒绝规则       | 明确禁止高风险工具    |

当同一个工具同时匹配 allow 和 deny 时，deny 优先。这能确保禁止规则不会被更宽泛的允许规则绕过。

MCP 工具也使用完整工具名匹配。例如 SDK MCP server 名为 `orders`，工具名为 `read_order`，完整工具名就是 `mcp__orders__read_order`。

```python theme={null}
QoderAgentOptions(
    mcp_servers={"orders": server},
    allowed_tools=["mcp__orders__read_order"],
)
```

<div id="运行时审批can_use_tool" />

<div id="canusetool" />

<div id="can-use-tool" />

<div id="can_use_tool" />

## 运行时审批：can\_use\_tool

`can_use_tool` 适合宿主应用需要参与审批的场景。比如你要把权限请求展示在自己的 UI 里，让用户点“允许一次”“始终允许本会话”或“拒绝”；或者你要调用企业风控服务判断某个命令是否可执行。

```python theme={null}
async def can_use_tool(
    tool_name: str,
    input_data: dict[str, Any],
    context: ToolPermissionContext,
):
    show_approval_dialog(
        title=context.title or tool_name,
        description=context.description,
        input_data=input_data,
    )

    approved = await wait_for_user_approval(context.signal)

    if not approved:
        return PermissionResultDeny(message="Rejected by user.")

    return PermissionResultAllow(updated_input=input_data)


QoderAgentOptions(
    cwd="/path/to/project",
    permission_mode="default",
    can_use_tool=can_use_tool,
)
```

`can_use_tool` 的签名如下：

```python theme={null}
CanUseTool = Callable[
    [str, dict[str, Any], ToolPermissionContext],
    Awaitable[PermissionResult],
]
```

关键字段解释：

| 字段                                               | 说明                                               |
| ------------------------------------------------ | ------------------------------------------------ |
| `tool_name`                                      | 完整工具名，例如 `Read`、`Bash`、`mcp__orders__read_order` |
| `input_data`                                     | 本次工具调用的原始参数                                      |
| `context.tool_use_id`                            | 本次工具调用 ID                                        |
| `context.signal`                                 | 授权请求被取消时会被设置，UI 或远程审批应监听它                        |
| `context.title` / `display_name` / `description` | 运行时生成的人类可读文案，适合直接用于审批 UI                         |
| `context.suggestions`                            | 运行时建议的权限更新，可用于“始终允许本会话”                          |
| `context.blocked_path`                           | 路径相关授权场景中的受限路径                                   |
| `context.decision_reason`                        | 运行时提供的审批原因说明，可用于展示或审计                            |
| `context.agent_id`                               | 子 Agent 发起工具调用时的 Agent ID                        |

返回 `PermissionResultAllow` 表示继续执行工具：

```python theme={null}
return PermissionResultAllow(updated_input=input_data)
```

`updated_input` 是工具最终收到的参数。你可以原样返回，也可以在审批通过后修改参数。例如给查询增加租户 ID、把路径改写到安全目录，或者去掉不允许的字段。

返回 `PermissionResultDeny` 表示拒绝工具：

```python theme={null}
return PermissionResultDeny(
    message="This command is not allowed in the current workspace."
)
```

`deny.message` 必填，它会成为拒绝原因的一部分，供模型、日志或宿主应用展示。SDK 收到 CLI 的授权请求但没有配置 `can_use_tool` 时，会返回错误，不会默认放行。

当权限系统直接拒绝工具调用时，消息流中可能出现结构化的权限拒绝消息：

```python theme={null}
from qoder_agent_sdk import SDKPermissionDeniedMessage


if isinstance(message, SDKPermissionDeniedMessage):
    print(
        message.tool_name,
        message.tool_use_id,
        message.message,
        message.decision_reason,
        message.decision_reason_type,
    )
```

这类消息常见于 `permission_mode="dontAsk"`、自动拒绝或规则拒绝等场景。宿主应用可以用它更新 UI 状态或写审计日志。

<div id="会话内更新权限permissionupdate" />

## 会话内更新权限：PermissionUpdate

`PermissionUpdate` 用于在一次审批之后更新当前会话里的权限规则。最常见的场景是用户在审批 UI 里选择“始终允许本会话”。这时你可以把运行时给出的 `suggestions` 原样返回，也可以自己构造明确的规则。

```python theme={null}
async def can_use_tool(
    tool_name: str,
    input_data: dict[str, Any],
    context: ToolPermissionContext,
):
    decision = await show_approval_dialog(
        tool_name=tool_name,
        suggestions=context.suggestions,
    )

    if decision == "always-allow-this-session":
        return PermissionResultAllow(
            updated_input=input_data,
            updated_permissions=context.suggestions,
        )

    if decision == "allow-once":
        return PermissionResultAllow(updated_input=input_data)

    return PermissionResultDeny(message="Rejected by user.")
```

也可以直接构造规则：

```python theme={null}
from qoder_agent_sdk.types import PermissionRuleValue, PermissionUpdate


return PermissionResultAllow(
    updated_input=input_data,
    updated_permissions=[
        PermissionUpdate(
            type="addRules",
            behavior="allow",
            destination="session",
            rules=[PermissionRuleValue(tool_name="mcp__orders__read_order")],
        )
    ],
)
```

支持的更新类型：

| 类型                  | 作用                     |
| ------------------- | ---------------------- |
| `addRules`          | 追加 allow、ask 或 deny 规则 |
| `replaceRules`      | 替换规则                   |
| `removeRules`       | 删除规则                   |
| `setMode`           | 切换权限模式                 |
| `addDirectories`    | 追加允许访问的目录              |
| `removeDirectories` | 移除目录授权                 |

推荐把动态权限更新写入当前会话：

```python theme={null}
destination = "session"
```

`session` 只影响当前 query 会话后续的权限检查。需要持久化到本地、项目或用户级配置时，优先使用 settings 管理流程，而不是依赖单次工具审批回调里的动态更新。

<div id="访问额外目录add_dirs" />

## 访问额外目录：add\_dirs

默认情况下，会话以 `cwd` 作为主要工作目录。模型需要读取或修改 `cwd` 之外的目录时，应显式传入 `add_dirs`。

```python theme={null}
QoderAgentOptions(
    cwd="/repo/app",
    add_dirs=["/repo/packages/shared"],
)
```

这个配置表示会话主工作目录是 `/repo/app`，同时允许模型访问 `/repo/packages/shared`。这适合 monorepo、跨仓库调试、共享库排查等场景。

运行过程中也可以通过 `PermissionUpdate` 调整目录授权：

```python theme={null}
return PermissionResultAllow(
    updated_input=input_data,
    updated_permissions=[
        PermissionUpdate(
            type="addDirectories",
            destination="session",
            directories=["/repo/packages/shared"],
        )
    ],
)
```

目录授权属于权限边界的一部分。不要把大范围目录加入 `add_dirs` 作为通用默认值；更稳妥的做法是按任务需要添加最小目录集合。

<div id="外部授权工具permission_prompt_tool_name" />

## 外部授权工具：permission\_prompt\_tool\_name

`permission_prompt_tool_name` 用于把权限请求交给运行环境中的 permission prompt tool，而不是在 SDK host 中实现 `can_use_tool`。它适合已有外部审批工具、远程运行环境或统一权限网关的场景。

```python theme={null}
QoderAgentOptions(
    permission_prompt_tool_name="mcp__permission_server__approve",
)
```

使用时要注意三点：

* `permission_prompt_tool_name` 必须是当前运行环境可以识别的 prompt tool 名称。
* `permission_prompt_tool_name` 与 `can_use_tool` 互斥，不能同时传入。
* SDK host 自己要决定审批时，优先使用 `can_use_tool`。

permission prompt tool 会收到下面的输入：

```python theme={null}
{
    "tool_name": "mcp__orders__read_order",
    "input": {"order_id": "1001"},
    "tool_use_id": "toolu_...",
}
```

它需要返回权限结果：

```python theme={null}
{
    "behavior": "allow",
    "updatedInput": {"order_id": "1001"},
    "updatedPermissions": [],
    "toolUseID": "toolu_...",
}

{
    "behavior": "deny",
    "message": "Denied by policy.",
    "interrupt": True,
    "toolUseID": "toolu_...",
}
```

`allow.updatedInput` 是最终执行工具时使用的入参。如果希望保持原始入参，应该原样返回收到的 `input`。`deny.message` 必填。`interrupt=True` 表示拒绝后中断当前 Agent 流程。

<div id="用-settings-提供权限规则" />

<div id="用settings提供权限规则" />

## 用 settings 提供权限规则

`settings` 适合在会话启动前提供静态权限配置。它比 `can_use_tool` 更适合表达“这个项目默认允许什么、拒绝什么、额外目录有哪些”。

```python theme={null}
QoderAgentOptions(
    cwd="/path/to/project",
    settings={
        "permissions": {
            "allow": ["Read", "Grep"],
            "deny": ["Bash"],
            "ask": ["Write"],
            "defaultMode": "default",
            "additionalDirectories": ["/path/to/shared-lib"],
        }
    },
)
```

字段说明：

| 字段                                         | 说明                          |
| ------------------------------------------ | --------------------------- |
| `permissions.allow`                        | 允许规则                        |
| `permissions.deny`                         | 拒绝规则                        |
| `permissions.ask`                          | 始终询问规则                      |
| `permissions.defaultMode`                  | 默认权限模式                      |
| `permissions.disableBypassPermissionsMode` | 设置为 `"disable"` 时禁用跳过权限检查模式 |
| `permissions.additionalDirectories`        | 额外可访问目录                     |

如果你的应用会读取并应用 settings 中的默认权限模式，建议在执行高风险模式前做自己的产品级确认。`bypassPermissions`、`yolo` 这类模式应该只出现在明确受信任的环境中。

<div id="用-hooks-做高级拦截和审计" />

<div id="用hooks做高级拦截和审计" />

## 用 hooks 做高级拦截和审计

Hooks 适合已经接入 SDK hooks 体系，并希望在工具生命周期里做更细粒度控制的场景。和 `can_use_tool` 相比，hooks 更适合横切逻辑，例如审计、告警、统一拦截、记录拒绝原因。

```python theme={null}
from qoder_agent_sdk import HookMatcher


async def pre_tool_use_hook(inp, tool_use_id, context):
    return {
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",
            "permissionDecisionReason": "Shell commands are disabled here.",
        }
    }


QoderAgentOptions(
    cwd="/path/to/project",
    hooks={
        "PreToolUse": [
            HookMatcher(matcher="Bash", hooks=[pre_tool_use_hook]),
        ],
    },
)
```

权限相关 hook 主要有三类：

| Hook                | 触发时机    | 常见用途                  |
| ------------------- | ------- | --------------------- |
| `PreToolUse`        | 工具调用前   | 提前允许、拒绝、要求询问或交给后续流程   |
| `PermissionRequest` | 进入权限请求时 | 在正常 prompt 前直接返回允许或拒绝 |
| `PermissionDenied`  | 权限被拒绝后  | 审计、告警、记录拒绝原因          |

`PreToolUse` 可以返回：

```python theme={null}
{
    "hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": "deny",
        "permissionDecisionReason": "...",
        "updatedInput": {},
    }
}
```

其中 `permissionDecision` 可以是 `"allow"`、`"deny"`、`"ask"` 或 `"defer"`。

`PermissionRequest` 可以返回类似工具审批的权限结果：

```python theme={null}
{
    "hookSpecificOutput": {
        "hookEventName": "PermissionRequest",
        "decision": {
            "behavior": "deny",
            "message": "Denied by policy.",
        },
    }
}
```

`PermissionDenied` 通常用于观察结果，不负责放行工具。它的输入会包含被拒绝的工具名、工具入参、工具调用 ID，以及拒绝原因。

<div id="mcp-tool-policy" />

<div id="mcptoolpolicy" />

## MCP Tool Policy

如果权限策略天然属于某个 MCP server，也可以直接在 MCP server config 里声明 tool-level permission policy。这样策略跟随 MCP server 配置，而不是散落在全局 `allowed_tools` 或 `disallowed_tools` 中。

```python theme={null}
import os


QoderAgentOptions(
    mcp_servers={
        "repo_tools": {
            "type": "http",
            "url": os.environ["REPO_TOOLS_MCP_URL"],
            "tools": [
                {"name": "search", "permission_policy": "always_allow"},
                {"name": "write_file", "permission_policy": "always_ask"},
                {"name": "delete_file", "permission_policy": "always_deny"},
            ],
        }
    },
)
```

策略含义：

| 策略             | 行为         |
| -------------- | ---------- |
| `always_allow` | 匹配工具直接允许   |
| `always_ask`   | 匹配工具进入授权流程 |
| `always_deny`  | 匹配工具直接拒绝   |

`name` 可以是 MCP tool 的原始名称，也可以是完整工具名，例如 `mcp__repo_tools__search`。实际匹配时，运行时会把策略名称映射到当前 MCP 工具调用。
