首页
/ 执行前的一道门:learn-claude-code 中 s03 权限管线的三道闸门设计与实现

执行前的一道门:learn-claude-code 中 s03 权限管线的三道闸门设计与实现

2026-09-04 21:38:48作者:秋泉律Samson

本文基于 learn-claude-code 课程的第 s03 章(Permission),完整讲解如何在 Claude Code 风格的 Agent Harness 中,于工具执行之前插入一条"硬拒绝 → 规则匹配 → 用户审批"的三道闸门权限管线。读完后你将理解:权限边界为什么必须由 harness 代码而不是模型自觉来负责、三道闸门各自的命中逻辑与代码实现、管线如何只加一行代码就接入 s02 的 agent loop,以及该模式在仓库后续课程(s04 Hooks、s15 集成 Harness)中的演化方向。

Permission 总览:LLM 返回的工具调用先经过权限管线(deny list / rules / approval)再进入工具分发

Permission 管线细节:三个闸门串联,命中拒绝返回 blocked,命中规则则暂停等待用户确认


一、问题:s02 的 Agent 有工具,却没有 bash 安全边界

回顾 s02_tool_use/code.py:Agent 拥有 bashread_filewrite_fileedit_fileglob 五个工具。其中文件类工具受 safe_path() 保护——任何解析后逃出工作区的路径都会抛出 ValueError

# s02_tool_use/code.py L71-75
def safe_path(p: str) -> Path:
    path = (WORKDIR / p).resolve()
    if not path.is_relative_to(WORKDIR):
        raise ValueError(f"Path escapes workspace: {p}")
    return path

bash 工具不受任何限制:run_bash() 直接以 shell=True 执行模型给出的任意命令。此时如果你让 Agent"清理一下项目",它在理论上是可能执行 rm -rf / 的。

s03 要解决的核心命题是:安全边界由代码负责,且判断必须发生在工具执行之前。模型可能给出危险指令,harness 有责任在执行前拦截或升级确认,而不能依赖系统提示词或模型的"自觉"。

二、解决方案:在工具执行前插入 check_permission()

s02 的循环完全保留,唯一的变动是在工具执行前插入 check_permission()。每个工具调用依次经过三道闸门:硬拒绝优先,软询问次之,都没命中就放行。三道闸门对应三种决策:

闸门 作用 命中后
1. 拒绝列表 永远禁止的操作(rm -rf /sudo 直接拒绝,不执行
2. 规则匹配 取决于上下文的操作(读/写工作区外、rm 文件) 交给闸门 3
3. 用户审批 闸门 2 命中后,暂停等用户确认 用户决定允许或拒绝

三道都没命中 → 直接执行。大部分日常操作(读文件、列目录、写入工作区内文件)走这条路,因此权限管线不会拖慢正常流程。

三、闸门 1:硬拒绝表(Hard Deny List)

第一张表是"永远禁止"的黑名单,先查,命中就返回阻止信息。实现见 s03_permission/code.py

# Gate 1: Hard deny list - always forbidden
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if=", "> /dev/sda"]

def check_deny_list(command: str) -> str | None:
    for pattern in DENY_LIST:
        if pattern in command:
            return f"Blocked: '{pattern}' is on the deny list"
    return None

要点说明:

  • 该表使用简单子串匹配pattern in command),目的是把权限闸门的位置演示清楚;官方文档明确指出"这张表使用简单字符串匹配来说明权限闸门的位置,不能视为完整的安全边界"。子串匹配存在误报的可能(例如 echo "sudo usage" 也会被拦下),这是刻意保留的、便于理解的教学实现。
  • 闸门 1 只对 bash 工具生效——在 check_permission() 内部有 if block.name == "bash" 的前置判断,文件工具不会进入黑名单检查。
  • 命中后返回一段带命中模式的说明(Blocked: 'sudo' is on the deny list),随后管线直接返回 False,工具不会执行。

四、闸门 2:规则匹配(Context-Dependent Rules)

黑名单解决"永远不行",但还有一类操作是"看情况"的:写工作区外的路径、执行 rm、改系统文件权限……这些操作不该一刀切禁止,而应触发用户确认。PERMISSION_RULES 用数据驱动的方式描述"什么时候需要问用户",每条规则指定适用工具列表检查条件(lambda)和触发原因,见 s03_permission/code.py

# Gate 2: Rule matching - context-dependent checks
PERMISSION_RULES = [
    {"tools": ["read_file", "write_file", "edit_file"],
     "check": lambda args: not (WORKDIR / args.get("path", "")).resolve().is_relative_to(WORKDIR),
     "message": "Writing outside workspace"},
    {"tools": ["bash"],
     "check": lambda args: any(kw in args.get("command", "") for kw in ["rm ", "> /etc/", "chmod 777"]),
     "message": "Potentially destructive command"},
]

def check_rules(tool_name: str, args: dict) -> str | None:
    for rule in PERMISSION_RULES:
        if tool_name in rule["tools"] and rule"check":
            return rule["message"]
    return None

两条内置规则逐条拆解:

  1. 工作区外访问规则read_file / write_file / edit_file):把工具参数中的 path 拼到 WORKDIR 下并 resolve() 解引用符号链接,再用 is_relative_to(WORKDIR) 判断是否仍在工作区内;只要"不相对"(即逃出去了),就命中。这条规则等价于把 s02 中 safe_path() 的"硬抛异常"改造成了"软询问"——同一个 resolve() + is_relative_to() 判定,在 s02 里直接让工具报错,在 s03 里则升级为用户可见的审批提示。
  2. 破坏性 bash 命令规则:命令中若包含 rm > /etc/chmod 777 任一片段,判定为"潜在破坏性命令"。

check_rules() 按序遍历规则表,返回第一条命中规则message 作为原因字符串;没有任何规则命中则返回 None(放行)。这种"工具 + lambda 条件 + 原因文案"的规则结构,后续扩展新检查只需追加字典项,无需改动管线代码。

五、闸门 3:用户审批(Pause & Ask)

规则命中后,管线暂停执行,把"原因 + 具体工具 + 完整参数"打印出来,等待用户在终端输入确认,见 s03_permission/code.py

# Gate 3: User approval - wait for confirmation after rule match
def ask_user(tool_name: str, args: dict, reason: str) -> str:
    print(f"\n\033[33m[permission] {reason}\033[0m")
    print(f"   Tool: {tool_name}({args})")
    choice = input("   Allow? [y/N] ").strip().lower()
    return "allow" if choice in ("y", "yes") else "deny"

实现细节:

  • 输出使用 ANSI 黄色高亮 [permission] {reason},并原样展示工具名与完整参数字典——用户审批时必须看到"Agent 到底想对什么路径/什么命令动手"。
  • 默认拒绝:input() 提示为 [y/N],只有输入 y / yes 才返回 allow,其余任何输入(包括直接回车)都返回 deny。这是审批类交互的标准安全默认值。
  • 返回的是字符串 "allow" / "deny",由上层 check_permission() 决定是否继续执行。

六、管线组装:check_permission() 与 agent_loop 的一行接入

三道闸门串成一个纯函数式的判断入口,见 s03_permission/code.py

# Pipeline: all three gates chained
def check_permission(block) -> bool:
    if block.name == "bash":
        reason = check_deny_list(block.input.get("command", ""))
        if reason:
            print(f"\n\033[31m[blocked] {reason}\033[0m")
            return False
    reason = check_rules(block.name, block.input)
    if reason:
        decision = ask_user(block.name, block.input, reason)
        if decision == "deny":
            return False
    return True

执行顺序是严格的短路链:

  1. 仅当工具为 bash 时先查闸门 1,命中即打印红色 [blocked]return False
  2. 所有工具都查闸门 2,命中后进入闸门 3 等待审批;用户拒绝则 return False
  3. 全部未命中或用户批准 → return True,工具正常执行。

接入点只在 agent_loop() 的工具分发循环里加了一个判断,见 s03_permission/code.py

for block in response.content:
    if block.type != "tool_use":
        continue

    print(f"\033[36m> {block.name}\033[0m")

    # s03 change: run through permission pipeline before executing
    if not check_permission(block):
        results.append({"type": "tool_result", "tool_use_id": block.id,
                        "content": "Permission denied."})
        continue

    handler = TOOL_HANDLERS.get(block.name)
    output = handler(**block.input) if handler else f"Unknown: {block.name}"
    print(str(output)[:200])
    results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})

messages.append({"role": "user", "content": results})

这里有两个容易被忽略但很关键的设计:

  • 拒绝不是终止,而是回传给模型。被拒的工具调用会以 tool_result: "Permission denied." 的形式写回消息历史,模型在下一轮会"看到"自己被拒了哪个操作、原因语境如何,从而自行换一种合规做法或向用户解释——权限结果成为 agent loop 里正常的一等公民消息,与工具正常输出走同一条通道。
  • s02 的循环形态零改动:仍是 client.messages.create(...) → 追加 assistant 消息 → 判断 stop_reason != "tool_use" 则返回 → 收集 results 追加为 user 消息。权限系统是一个"可插拔的前置过滤器",这为 s04 把它抽成 Hook 埋下了伏笔。

此外,系统提示词也从"软"层面同步了这条边界(s03_permission/code.py#L58):

SYSTEM = f"You are a coding agent at {WORKDIR}. All destructive operations require user approval."

即:提示词负责让模型"知道"审批的存在(减少无谓的危险尝试),代码闸门负责在模型犯错时"拦住"它。软硬两层各司其职。

七、从源码结构看:s02 到 s03 的安全模型迁移

对比 s02_tool_use/code.pys03_permission/code.py,可以观察到一次安全模型的位置迁移:

组件 之前 (s02) 之后 (s03)
安全模型 无统一模型(文件工具靠 safe_path 硬抛异常,bash 无限制) 三道闸门权限管线
新函数 check_deny_listcheck_rulesask_usercheck_permission
循环 直接执行所有工具 执行前插入 check_permission()
越界路径 工具内部 raise ValueError,模型只能看到报错文本 闸门 2 先命中 → 用户显式审批,路径仍可被允许
bash 完全不受限 deny list + 破坏性命令关键词 + 审批三重防护

从源码结构看,s03 的文件工具实现(如 run_readrun_write)已不再调用 s02 的 safe_path(),而是直接 (WORKDIR / path).resolve() 读写——路径越界的判定统一上移到了闸门 2。这意味着同一类越界行为从"工具直接失败"变成了"先问人",给合法跨目录操作留了口子,同时把判断逻辑收敛到一处(PERMISSION_RULES),不再散落在各工具内部。

八、运行与实测:四个能看出三道闸门差异的 prompt

8.1 环境准备

前置依赖见 s03_permission/code.py 文件头 docstring:

cd learn-claude-code
pip install anthropic python-dotenv
# .env 中配置:
#   ANTHROPIC_API_KEY=<你的密钥>
#   MODEL_ID=<模型名>
#   ANTHROPIC_BASE_URL=<可选,自定义网关地址>

代码入口还会处理两个细节:load_dotenv(override=True) 加载 .env;若设置了 ANTHROPIC_BASE_URL 则移除 ANTHROPIC_AUTH_TOKEN(见 s03_permission/code.py#L50-L56)。bash 工具有 120 秒超时,输出截断到 50000 字符;每次 API 调用 max_tokens=8000s03_permission/code.py#L63-L70)。

8.2 启动

python s03_permission/code.py

进入交互式终端(提示符 s03 >>,输入 qexit 退出)。

8.3 建议依次尝试的 prompt 与预期闸门行为

  1. Create a file called test.txt in the current directory —— 写入工作区内,三道闸门全部未命中,直接通过
  2. Delete the file test.txt —— 模型会调用 bash 执行 rm test.txtrm 关键词命中闸门 2,弹出 [permission] Potentially destructive command 等待 y/N
  3. What files are in the current directory? —— 只读 glob/bash ls全部放行
  4. Try to write a file to /etc/something —— write_filepath 解析后逃出 WORKDIR,命中闸门 2 的工作区外规则,进入用户审批。

观察重点:哪些操作直接通过?哪些需要你确认?哪些被直接拒绝(如让 Agent 执行 sudo reboot,会被闸门 1 以红色 [blocked] 直接拦下)?把实际交互对照 s03_permission/README.zh.md 中的三道闸门表验证一遍,就建立了完整的权限心智模型。

九、局限性与边界说明

  • 字符串黑名单不是安全边界:闸门 1 的子串匹配只用于教学定位,真实系统需要命令解析(tokenize/AST 级)才能识别 $(...)、反引号、变量展开等绕过形态。
  • 规则覆盖是白名单式的枚举chmod 777> /etc/ 等关键词只覆盖了常见破坏动作,新攻击面需要往 PERMISSION_RULES 里加条目。
  • 审批交互依赖终端ask_user() 直接读 stdin,在无人值守(后台任务、定时任务)场景不可用——这正是仓库后续章节要解决的问题。

十、后续演化:从内联判断到 Hook 与运行时策略

s03 的权限检查目前每次都硬编码在循环里。如果在每次工具执行前后加日志?如果想在某些操作后自动触发 git commit?扩展逻辑散落在 loop 里,循环很快就会膨胀。s04_hooks/README.zh.md 的解法是给循环加钩子:check_permission 这类逻辑从循环体迁出,挂到 PreToolUse 钩子上,循环保持干净。

仓库的测试与集成代码印证了这条演化链:

  • tests/test_task_system.pyassert lesson.permission_hook in lesson.HOOKS["PreToolUse"]——确认权限检查已注册为 PreToolUse 钩子;
  • tests/test_background_tasks.pytest_background_bash_passes_permission_before_dispatch——后台 bash 任务在分发前同样要过权限关;
  • tests/test_cron_scheduler.pytest_scheduled_turn_never_reads_interactive_permission_input——定时任务轮次绝不能读交互式审批输入,说明非交互场景需要替代的权限策略;
  • s15_integrated_harness/code.py 中,DENY_LISTpermission_hook(block) 被重新定义并通过 register_hook("PreToolUse", permission_hook) 挂入集成 Harness,MCP 工具也复用同一权限策略(参见 tests/test_agent_teams_runtime.pytest_mcp_permission_uses_host_policytest_integrated_permission_requires_approval_for_every_shell_command)。

十一、核心要点回顾

  1. 权限判断必须发生在工具执行之前,由 harness 代码负责,不依赖模型自觉;提示词只做软层提醒。
  2. 三道闸门是短路链:deny list(硬拒绝)→ 规则匹配(上下文判断)→ 用户审批(默认拒绝),全部未命中才放行。
  3. 管线以函数形式插入 agent loop 只需一行,拒绝结果以 tool_result: "Permission denied." 回传模型,让模型在循环内自我修正。
  4. 安全机制的位置可以迁移:s02 的工具内硬校验(safe_path)在 s03 中上移为规则层的软询问,判断逻辑从散落的工具内部收敛到统一的 PERMISSION_RULES
  5. 内联判断是过渡形态:s04 之后权限检查演化为 PreToolUse 钩子,并贯穿后台任务、定时任务与 MCP 工具的统一策略。

相关文档:s03_permission/README.zh.md(本课中文原文)、s02_tool_use/README.zh.md(前置章节:多工具与分发)、s04_hooks/README.zh.md(后续章节:钩子机制)。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
docsdocs
暂无描述
Markdown
889
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341