Files
gui-yue 1baf1aca5a Follow up PR #265: refine chapters, diagrams, and add S20 (#283)
* feat: s01-s14 docs quality overhaul — tool pipeline, single-agent, knowledge & resilience

Rewrite code.py and README (zh/en/ja) for s01-s14, each chapter building
incrementally on the previous. Key fixes across chapters:

- s01-s04: agent loop, tool dispatch, permission pipeline, hooks
- s05-s08: todo write, subagent, skill loading, context compact
- s09-s11: memory system, system prompt assembly, error recovery
- s12-s14: task graph, background tasks, cron scheduler

All chapters CC source-verified. Code inherits fixes forward (PROMPT_SECTIONS,
json.dumps cache, real-state context, can_start dep protection, etc.).

* feat: s15-s19 docs quality overhaul — multi-agent platform: teams, protocols, autonomy, worktree, MCP tools

Rewrite code.py and README (zh/en/ja) for s15-s19, the multi-agent platform
chapters. Each chapter inherits all previous fixes and adds one mechanism:

- s15: agent teams (TeamCreate, teammate threads, shared task list)
- s16: team protocols (plan approval, shutdown handshake, consume_inbox)
- s17: autonomous agents (idle polling, auto-claim, consume_lead_inbox)
- s18: worktree isolation (git worktree, bind_task, cwd switching, safety)
- s19: MCP tools (MCPClient, normalize_mcp_name, assemble_tool_pool, no cache)

All appendix source code references verified against CC source. Config priority
corrected: claude.ai < plugin < user < project < local.

* fix: 5 regressions across s05-s19 — glob safety, todo validation, memory extraction, protocol types, dep crash

- s05-s09: glob results now filter with is_relative_to(WORKDIR) (inherited from s02)
- s06-s08: todo_write validates content/status required fields (inherited from s05)
- s09: extract_memories uses pre-compression snapshot instead of compacted messages
- s16: submit_plan docstring clarifies protocol-only (not code-level gate)
- s17-s19: match_response restores type mismatch validation (from s16)
- s17-s19: claim_task deps list handles missing dep files without crashing

* fix: s12 Todo V2 logic reversal, s14/s15 cron range validation, s18/s19 worktree name validation

- s12 README (zh/en/ja): fix Todo V2 direction — interactive defaults to Task,
  non-interactive/SDK defaults to TodoWrite. Fix env var name to
  CLAUDE_CODE_ENABLE_TASKS (not TODO_V2).
- s14/s15: add _validate_cron_field with per-field range checks (minute 0-59,
  hour 0-23, dom 1-31, month 1-12, dow 0-6), step > 0, range lo <= hi.
  Replace old try/except validation that only caught exceptions.
- s18/s19: add validate_worktree_name() to remove_worktree and keep_worktree,
  not just create_worktree.

* fix: align s16-s19 teaching tool consistency

* fix pr265 chapter diagrams

* Add comprehensive s20 harness chapter

* Fix chapter smoke test regressions

* Clarify README tutorial track transition

---------

Co-authored-by: Haoran <bill-billion@outlook.com>
2026-05-20 21:45:38 +08:00

9.5 KiB
Raw Permalink Blame History

s03: Permission — 执行前做权限判断

中文 · English · 日本語

s01 → s02 → s03s04 → s05 → ... → s20

"工具执行前先做权限判断" — 权限管线决定哪些操作需要审批。

Harness 层: 权限 — 在工具执行前加一道门。


问题

s02 的 Agent 有 5 个工具。file tools 受 safe_path 保护,但 bash 不受限制。让它"清理一下项目",可能执行 rm -rf /

安全不能靠信任模型,要靠代码——在工具执行之前做判断。


解决方案

Permission Overview

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

三道闸门对应三种决策:

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

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


工作原理

Permission Pipeline

闸门 1:一张硬拒绝表,先查,命中就返回阻止信息。(教学示意:简单字符串匹配不是可靠安全机制,命令变体和 shell 展开可能绕过。CC 的做法见附录。)

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

闸门 2:规则匹配——描述"什么时候需要问用户"。每条规则指定工具和检查条件。

PERMISSION_RULES = [
    {
        "tools": ["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"](args):
            return rule["message"]
    return None

闸门 3:规则命中后,暂停等用户输入。

def ask_user(tool_name: str, args: dict, reason: str) -> str:
    print(f"\n{reason}")
    print(f"   Tool: {tool_name}({args})")
    choice = input("   Allow? [y/N] ").strip().lower()
    return "allow" if choice in ("y", "yes") else "deny"

三道闸门串在一起,插在工具执行之前:

def check_permission(block) -> bool:
    # 闸门 1: 硬拒绝
    if block.name == "bash":
        reason = check_deny_list(block.input.get("command", ""))
        if reason:
            print(f"\n{reason}")
            return False

    # 闸门 2 + 3: 规则匹配 → 用户审批
    reason = check_rules(block.name, block.input)
    if reason:
        decision = ask_user(block.name, block.input, reason)
        if decision == "deny":
            return False

    return True

# 在 agent_loop 中——s02 的循环只加了一行:
for block in response.content:
    if block.type == "tool_use":
        if not check_permission(block):           # ← 新增
            results.append({... "content": "Permission denied."})
            continue
        output = TOOL_HANDLERS[block.name](**block.input)  # s02 原有
        results.append(...)

相对 s02 的变更

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

试一下

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

试试这些 prompt

  1. Create a file called test.txt in the current directory(应该直接通过)
  2. Delete all temporary files in /tmpbash + rm 会触发闸门 2
  3. What files are in the current directory?(只读,全部通过)
  4. Try to write a file to /etc/something(写工作区外,触发闸门 2

观察重点:哪些操作直接通过?哪些需要你确认?哪些被直接拒绝?


接下来

权限检查做了——但每次都在循环里硬编码 check_permission()。如果我想在每次工具执行前后加日志?如果想在某些操作后自动触发 git commit这些扩展逻辑散落在 loop 里,循环很快就会膨胀。

s04 Hooks → 给循环加钩子,扩展逻辑挂在钩子上,循环保持干净。

深入 CC 源码

以下基于 CC 源码 types/permissions.tsutils/permissions/permissions.tstoolExecution.tsutils/permissions/yoloClassifier.tstools/AgentTool/forkSubagent.ts 的核查。

一、PermissionResult不是 3 种,是 4 种

教学版的三道闸门deny → ask → allow和 CC 不完全对应。CC 的 PermissionResult 有 4 个 behaviortypes/permissions.ts:241-266

behavior 含义 教学版对应
allow 直接允许 闸门 3 通过
deny 直接拒绝 闸门 1 命中
ask 弹出对话框问用户 闸门 2 命中
passthrough 工具不表态,交给通用管线决定 教学版无

二、生产版的验证阶段

CC 的工具调用不是经过三道闸门,而是经过多个阶段,分布在 checkPermissionsAndCallTool()toolExecution.ts:599-1745、hooks、hasPermissionsToUseToolInner()utils/permissions/permissions.ts:1158-1310)和 classifier 逻辑里:

  1. Zod schema 验证toolExecution.ts:614-680)— 参数类型检查
  2. validateInput()toolExecution.ts:682-733)— 工具级语义验证
  3. backfillObservableInput()toolExecution.ts:784)— 补全遗留字段
  4. PreToolUse hookstoolExecution.ts:800-862)— 钩子可以返回 allow/deny/ask
  5. resolveHookPermissionDecision()toolExecution.ts:921-931)— 协调钩子+管线决策
  6. hasPermissionsToUseToolInner()permissions.ts:1158-1310)— 多层规则检查:
    • 整个工具被 deny rule 禁用 → deny
    • 整个工具被 ask rule 标记 → ask
    • tool.checkPermissions() 工具自己的判断
    • 工具自己返回 deny → deny
    • requiresUserInteraction()ask
    • 内容相关的 ask 规则 → ask(不可绕过)
    • 安全检查违规 → ask(不可绕过)
    • bypassPermissions 模式 → allow
    • 整个工具被 allow rule 放行 → allow
    • passthrough → 转为 ask

三、拒绝列表:不是一个文件,是 8 个来源

CC 没有单一的 deny list。权限规则来自 8 个来源(types/permissions.ts:54-62

来源 配置位置
userSettings ~/.claude/settings.json
projectSettings .claude/settings.json
localSettings settings.local.json
flagSettings Feature flags
policySettings 企业管理策略
cliArg --allowedTools / --deniedTools
command 内联命令
session 会话内临时授权

每条规则格式:{ toolName: "Bash", ruleBehavior: "deny", ruleContent: "npm publish:*" }。多个来源的规则合并高优先级来源覆盖低优先级从低到高user < project < local < flag < policy加上 cliArg、command、session

四、isDestructive() 是什么

CC 中 isDestructiveTool.ts:405-406纯粹是 UI 展示用的——在工具列表里显示 [destructive] 标签。它不参与权限决策。默认所有工具都返回 false。只有 ExitWorktreeremove 时)和 MCP 工具(依赖 annotations.destructiveHint)覆写了它。

五、YoloClassifier自动审批

CC 的 auto 模式下,不会每次都弹对话框。classifyYoloActionutils/permissions/yoloClassifier.ts:1012)把工具调用 + 对话上下文发给一个分类器 LLM 判断是否安全。先尝试 acceptEdits 模式模拟(permissions.ts:620-656,如果 acceptEdits 允许 → 直接批准),再查安全工具白名单(permissions.ts:658-686),最后才调分类器。分类器连续拒绝太多次 → 回退到人工审批。

六、权限冒泡

子 Agent通过 AgentTool fork 出来的)的 permissionMode 设为 'bubble'forkSubagent.ts:50)。意思是权限弹窗冒泡到父 Agent 的终端,而不是在子 Agent 里静默拒绝。Bash 分类器在这个过程中继续跑——给权限对话框显示的同时在后台判断是否可以自动批准。

教学版的简化是刻意的

  • 多阶段管线 → 3 道闸门:理解门槛大幅降低
  • 8 个规则来源 → 1 个本地 DENY_LIST概念量可控
  • isDestructive → 忽略(教学版没有 UI 层CC 里它也不参与权限决策)
  • YoloClassifier → 省略(依赖于额外的 LLM 调用和遥测系统)
  • 权限冒泡 → 省略s15 才涉及多 Agent