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

12 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() を挿入すること — 各ツール呼び出しは 3 つのゲートを固定順序で通過する:ハード拒否が最優先、次にソフト確認、どちらも一致しなければ許可。

3 つのゲートは 3 つの決定に対応する:

ゲート 役割 一致時
1. 拒否リスト 常に禁止される操作(rm -rf /sudo 即座に拒否、実行しない
2. ルールマッチング コンテキスト依存の操作(作業ディレクトリ外への書き込み、rm ファイル) ゲート 3 へ
3. ユーザー承認 ゲート 2 が一致した場合、ユーザー確認を待機 ユーザーが許可または拒否を決定

3 つのゲートのどれにも一致しない → 直接実行。日常の操作の大部分はこの経路を通る。


仕組み

Permission Pipeline

ゲート 1:ハード拒否リスト。最初に確認し、一致すればブロックメッセージを返す。(教育デモ:単純な文字列マッチングは信頼できるセキュリティ機構ではない — コマンドの変種やシェル展開で回避される可能性がある。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"

3 つのゲートを直列に接続、ツール実行前に挿入する:

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 のループに 1 行追加するだけ:
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)
セキュリティモデル なし(モデルを信頼) 3 ゲート権限パイプライン
新規関数 check_deny_list, check_rules, ask_user, check_permission
ループ すべてのツールを直接実行 実行前に check_permission() を挿入

試してみよう

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

以下のプロンプトを試してみよう:

  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 をトリガーしたい場合は? このような拡張ロジックがループ内に散らばると、ループはすぐに膨張する。

→ s04 Hooksループにフックを追加する。拡張ロジックはフックにぶら下げ、ループはクリーンに保つ。

CC ソースコードを深掘り

以下は CC ソースコード types/permissions.tsutils/permissions/permissions.tstoolExecution.tsutils/permissions/yoloClassifier.tstools/AgentTool/forkSubagent.ts の検証に基づく。

一、PermissionResult3 種ではなく、4 種

教育版の 3 つのゲートdeny → ask → allowは CC と完全には対応しない。CC の PermissionResult には 4 つの behavior がある(types/permissions.ts:241-266

behavior 意味 教育版の対応
allow 直接許可 ゲート 3 通過
deny 直接拒否 ゲート 1 一致
ask ユーザーにダイアログを表示 ゲート 2 一致
passthrough ツールが意見を表明せず、汎用パイプラインに委ねる 教育版にはなし

二、本番環境の検証段階

CC のツール呼び出しは 3 つのゲートを通るのではなく、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 に変換

三、拒否リスト1 つのファイルではなく、8 つのソース

CC には単一の deny list はない。権限ルールは 8 つのソースから来る(types/permissions.ts:54-62

ソース 設定場所
userSettings ~/.claude/settings.json
projectSettings .claude/settings.json
localSettings settings.local.json
flagSettings フィーチャーフラグ
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)、最後に分類器を呼び出す。分類器が連続して拒否しすぎた場合 → 手動承認にフォールバック。

六、権限バブリング

サブ AgentAgentTool 経由でフォークされたもの)の permissionMode'bubble' に設定される(forkSubagent.ts:50)。これは権限ダイアログが親 Agent のターミナルにバブルアップすることを意味する。サブ Agent で黙って拒否されるのではない。Bash 分類器はこの過程で引き続き実行され — 権限ダイアログを表示しつつ、バックグラウンドで自動承認可能か判断する。

教育版の単純化は意図的

  • 多段階パイプライン → 3 ゲート:理解のハードルが大幅に下がる
  • 8 ルールソース → 1 つのローカル DENY_LIST概念量を制御可能
  • isDestructive → 省略(教育版には UI レイヤーがなく、CC でも権限決定には参加しない)
  • YoloClassifier → 省略(追加の LLM 呼び出しとテレメトリに依存)
  • 権限バブリング → 省略s15 でマルチ Agent を扱う)