首页
/ learn-claude-code s03 深度解析:用三道闸门权限管线为 Agent 工具执行加一道安全门

learn-claude-code s03 深度解析:用三道闸门权限管线为 Agent 工具执行加一道安全门

2026-09-04 21:51:49作者:胡唯隽

本篇基于 learn-claude-code(Bash is all you need——从 0 到 1 构建 nano claude-code 风格 agent harness 的教学仓库)第 3 章文档 s03_permission/README.md 与配套源码 s03_permission/code.py 展开。文章将完整覆盖「执行前权限判断(Check Permissions Before Execution)」这一 Harness 层的实现:拒绝列表、规则匹配、用户审批三道闸门的定义、串接方式与插入点,并结合源码逐行印证每道闸门的边界与局限。读完你能掌握如何在自己的 agent loop 中用最小改动(一行插入)建立「硬拒绝 → 软询问 → 放行」的权限决策管线,并理解字符串匹配式 deny list 为什么不能当作完整安全边界。

Permission Overview: s03 在 LLM 与工具分发之间插入 check_permission() 权限管线

1. 问题:s02 的 Agent 有 5 个工具,但 bash 没有任何限制

回顾 s02_tool_use 的成果:Agent 拥有 bashread_filewrite_fileedit_fileglob 五个工具。其中文件类工具受 safe_path 保护——s02 源码 s02_tool_use/code.py 中的实现会先对路径做 resolve(),再用 is_relative_to(WORKDIR) 校验,越界即抛 ValueError

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 工具是放开的:任意 shell 命令经 subprocess.run(command, shell=True, ...) 直接执行。文档给出的反例很直接:让 Agent「清理一下项目」,它可能执行 rm -rf /

s03 给出的原则是:安全边界不能靠信任模型,必须由代码负责,且判断发生在每次工具执行之前

2. 解决方案:保留 s02 循环,只插入一行 check_permission()

s03 没有重写任何已有逻辑,s02 的 agent loop 被完整保留。唯一改动是在工具执行前插入 check_permission()——每个 tool_use 块依次经过三道闸门,顺序固定:硬拒绝优先,软询问次之,都没命中就放行

三道闸门对应三种决策:

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

三道都没命中则直接执行——大部分日常操作走这条路。

Permission Pipeline: 每个工具调用依次经过 deny list、规则匹配、用户审批三道闸门

3. 三道闸门的逐层实现

3.1 闸门 1:硬拒绝列表(DENY_LIST)

先查、命中即阻止。列表与判定函数位于 code.py#L141-L147

# 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),命中任意一个模式就返回阻止信息;
  • 文档明确声明:这张表使用简单字符串匹配来「说明权限闸门的位置」,不是完整的安全边界。例如 rm -rf /*rm -rf . --no-preserve-root 之类变形可以绕过子串匹配——生产环境应替换为命令解析(AST/tokenize)或更强的沙箱机制;
  • 闸门 1 只对 bash 工具生效(见 3.4 中的 if block.name == "bash" 条件),文件类工具不查这张表。

3.2 闸门 2:规则匹配(PERMISSION_RULES)

闸门 2 回答的是「什么时候需要问用户」。每条规则由三部分组成:tools(作用于哪些工具)、check(检查条件)、message(呈现给用户的理由)。源码位于 code.py#L151-L164

# 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. 文件工具越界规则:把 path 参数与 WORKDIR 拼接后 resolve(),再用 is_relative_to(WORKDIR) 判断——这复用了 s02 中 safe_path 的同款校验逻辑(见 s02_tool_use/code.py#L71-L75),但用途从「直接抛错」变成了「触发询问」。绝对路径(如 /etc/something)或含 .. 的越界相对路径都会命中;
  2. 破坏性 bash 命令规则:用关键词("rm ""> /etc/""chmod 777")做粗筛。注意关键词带尾部空格("rm "),ls -l | grep rm 这类不含独立 rm 的命令不会误报,而 rm -i test.txt 会命中。

从源码结构看,一个值得注意的设计变化是:s03 的文件工具实现(run_read/run_write/run_edit,见 code.py#L73-L102)不再调用 s02 的 safe_path,而是直接 (WORKDIR / path).resolve() 后读写。也就是说,工作区边界在 s03 里从「工具内部的硬校验」上移成了「执行前闸门里的人工确认点」——用户若回复 y,文件工具确实可以把数据写到工作区外。这符合本章「把危险操作交给用户决策」的教学目标,但也说明三道闸门是审批流而非沙箱:真正的隔离仍依赖闸门 1 的 deny list 与后续章节的机制。

3.3 闸门 3:用户审批(ask_user)

规则命中后暂停整个循环,等待标准输入。实现位于 code.py#L168-L172

# 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"

三个行为细节:

  • 呈现内容包含命中规则的理由(reason)和完整工具入参(args),保证用户在做决定前能看清「将被执行什么」;
  • 默认拒绝:只有输入 yyes 才放行,任何其他输入(包括直接回车)都返回 "deny"——审批失败时的默认值是安全的;
  • 返回的 "allow"/"deny" 字符串由上层管线消费,ask_user 本身不产生副作用。

3.4 管线串接与插入点:check_permission()

三道闸门由 check_permission(block) -> bool 串成固定顺序的管线,位于 code.py#L176-L187

# 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

顺序语义:deny list 只对 bash 生效且优先级最高;规则匹配对所有工具生效;只有规则命中才进入用户审批;返回 False 即终止本次调用,返回 True 才允许分发执行。

在 agent loop 中的插入点只有一行,位于 code.py#L204-L219

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}"
    ...
    results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})

注意拒绝路径的处理:被拒的调用不会从对话中消失,而是以 tool_result + "Permission denied." 的形式写回消息历史(results 列表最终在 code.py#L221{"role": "user", "content": results} 追加进 messages)。模型因此能感知「这个操作没被批准」,可以选择换一种方式或向用户解释——这是权限系统与模型协同的关键闭环,而不只是打印一条错误。

4. 相对 s02 的变更与周边实现细节

组件 之前 (s02) 之后 (s03)
安全模型 无(信任模型,仅文件工具有 safe_path) 三道闸门权限管线
新增函数 check_deny_listcheck_rulesask_usercheck_permission
循环 直接执行所有工具 执行前插入 check_permission()

源码里还有几处与权限行为直接相关的设定,值得在复用时留意:

  • 系统提示参与安全约定SYSTEM = f"You are a coding agent at {WORKDIR}. All destructive operations require user approval."code.py#L58)。提示词层面的声明与代码层面的闸门互为补充:前者引导模型主动避免危险操作,后者保证即使模型越界也会被拦截。
  • 工作区基准是进程启动目录WORKDIR = Path.cwd()code.py#L54),因此所有 is_relative_to(WORKDIR) 校验的基准随启动位置变化。
  • bash 执行有硬超时subprocess.run(..., timeout=120),超时返回 Error: Timeout (120s)code.py#L63-L70)——权限闸门之外,执行层本身也限制了单次调用的爆炸半径。
  • API 配置:脚本读取 .envload_dotenv(override=True)),支持 ANTHROPIC_BASE_URL 指向兼容端点,模型名由环境变量 MODEL_ID 指定,max_tokens=8000code.py#L50-L56code.py#L194-L197)。

5. 亲手试一下

运行前提:Python 3,安装 requirements.txt 中的 anthropicpython-dotenvpip install anthropic python-dotenv),并在 .env 中配置 ANTHROPIC_API_KEY(如需自定义网关可设 ANTHROPIC_BASE_URLMODEL_ID)。

cd learn-claude-code
python s03_permission/code.py

文档给出的四条验证 prompt 分别覆盖管线的三种结果:

  1. Create a file called test.txt in the current directory —— 工作区内写入,不命中任何规则,直接通过
  2. Delete the file test.txt —— bash 命令含 rm ,命中闸门 2,暂停等待 y/N 确认
  3. What files are in the current directory? —— 只读操作(glob/read_file),全部通过
  4. Try to write a file to /etc/something —— 路径越出工作区,命中闸门 2 的越界规则,暂停等待确认

观察重点:哪些操作直接通过?哪些需要你确认?哪些被直接拒绝(比如让 Agent 执行 sudorm -rf /,应看到红色的 [blocked] 输出而非审批提示)。

6. 延伸:硬编码检查为什么只是过渡方案

s03 已经把权限检查就位,但 check_permission() 是硬编码在 agent_loop 里的一次调用。文档在结尾提出了下一步的动机:想在每次工具执行前后加日志?想在某些写操作后自动 git commit?这些扩展逻辑继续往 loop 里塞,循环很快就会膨胀。

这正是 s04_hooks 要解决的问题:把 check_permission() 从循环体内移到一个 PreToolUse 钩子上,循环只调用 trigger_hooks("PreToolUse", block),由注册表决定执行什么。仓库后续阶段的代码也印证了这条演进路径——例如 tests/test_task_system.py#L81 中断言 lesson.permission_hook in lesson.HOOKS["PreToolUse"],即权限检查最终成为可注册的 hook 而非固定语句。

小结:s03 用约 50 行代码(DENY_LIST + PERMISSION_RULES + 四个函数 + 循环中一行插入)演示了 agent harness 权限层的最小形态——硬拒绝、上下文规则、人工审批三级决策,默认值全部偏向「不执行」。它诚实地标注了字符串匹配的局限,把「审批流 ≠ 沙箱」的边界讲清楚,并自然地把扩展点留给 hooks 机制。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384