mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-06-20 20:23:36 +08:00
* 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>
This commit is contained in:
232
s03_permission/README.en.md
Normal file
232
s03_permission/README.en.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# s03: Permission — Check Permissions Before Execution
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → s02 → `s03` → [s04](../s04_hooks/) → s05 → ... → s20
|
||||
> *"Check permissions before executing"* — The permission pipeline decides which operations need approval.
|
||||
>
|
||||
> **Harness Layer**: Permission — a gate before tool execution.
|
||||
|
||||
---
|
||||
|
||||
## The Problem
|
||||
|
||||
s02's Agent has 5 tools. File tools are protected by `safe_path`, but bash is unrestricted. Ask it to "clean up the project," and it might run `rm -rf /`.
|
||||
|
||||
Safety can't rely on trusting the model — it needs code: a check before every tool execution.
|
||||
|
||||
---
|
||||
|
||||
## The Solution
|
||||
|
||||

|
||||
|
||||
s02's loop is fully preserved. The only change is inserting `check_permission()` before tool execution — each tool call passes through three gates in a fixed order: hard deny first, then soft ask, and if neither matches, allow.
|
||||
|
||||
The three gates correspond to three decisions:
|
||||
|
||||
| Gate | Purpose | On Match |
|
||||
|------|---------|----------|
|
||||
| 1. Deny List | Permanently forbidden operations (`rm -rf /`, `sudo`) | Denied immediately, not executed |
|
||||
| 2. Rule Matching | Context-dependent operations (writing outside workspace, `rm` files) | Passed to Gate 3 |
|
||||
| 3. User Approval | After Gate 2 matches, pauses for user confirmation | User decides allow or deny |
|
||||
|
||||
None of the three gates match → execute directly. Most routine operations take this path.
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||

|
||||
|
||||
**Gate 1**: A hard deny list. Check first; if matched, return a block message. (Teaching demo: simple string matching is not a reliable security mechanism — command variants and shell expansion can bypass it. CC's approach is in the appendix.)
|
||||
|
||||
```python
|
||||
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
|
||||
```
|
||||
|
||||
**Gate 2**: Rule matching — describes "when to ask the user." Each rule specifies a tool and a check condition.
|
||||
|
||||
```python
|
||||
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
|
||||
```
|
||||
|
||||
**Gate 3**: After a rule matches, pause for user input.
|
||||
|
||||
```python
|
||||
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"
|
||||
```
|
||||
|
||||
**All three gates chained together**, inserted before tool execution:
|
||||
|
||||
```python
|
||||
def check_permission(block) -> bool:
|
||||
# Gate 1: Hard deny
|
||||
if block.name == "bash":
|
||||
reason = check_deny_list(block.input.get("command", ""))
|
||||
if reason:
|
||||
print(f"\n⛔ {reason}")
|
||||
return False
|
||||
|
||||
# Gate 2 + 3: Rule matching → User approval
|
||||
reason = check_rules(block.name, block.input)
|
||||
if reason:
|
||||
decision = ask_user(block.name, block.input, reason)
|
||||
if decision == "deny":
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# In agent_loop — s02's loop with just one line added:
|
||||
for block in response.content:
|
||||
if block.type == "tool_use":
|
||||
if not check_permission(block): # ← NEW
|
||||
results.append({... "content": "Permission denied."})
|
||||
continue
|
||||
output = TOOL_HANDLERS[block.name](**block.input) # s02 original
|
||||
results.append(...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Changes from s02
|
||||
|
||||
| Component | Before (s02) | After (s03) |
|
||||
|-----------|-------------|-------------|
|
||||
| Security model | None (trust the model) | Three-gate permission pipeline |
|
||||
| New functions | — | check_deny_list, check_rules, ask_user, check_permission |
|
||||
| Loop | Executes all tools directly | Inserts check_permission() before execution |
|
||||
|
||||
---
|
||||
|
||||
## Try It
|
||||
|
||||
```sh
|
||||
cd learn-claude-code
|
||||
python s03_permission/code.py
|
||||
```
|
||||
|
||||
Try these prompts:
|
||||
|
||||
1. `Create a file called test.txt in the current directory` (should pass through)
|
||||
2. `Delete all temporary files in /tmp` (bash + rm triggers Gate 2)
|
||||
3. `What files are in the current directory?` (read-only, all pass)
|
||||
4. `Try to write a file to /etc/something` (writing outside workspace triggers Gate 2)
|
||||
|
||||
What to watch for: Which operations pass through? Which need your confirmation? Which are denied outright?
|
||||
|
||||
---
|
||||
|
||||
## What's Next
|
||||
|
||||
Permission checks are in place — but every check is hardcoded as `check_permission()` inside the loop. What if you want to add logging before and after each tool execution? What if you want to auto-trigger a git commit after certain operations? Scattering this extension logic throughout the loop makes it bloat.
|
||||
|
||||
→ s04 Hooks: Add hooks to the loop. Extension logic hangs on hooks; the loop stays clean.
|
||||
|
||||
<details>
|
||||
<summary>Dive into CC Source Code</summary>
|
||||
|
||||
> The following is based on a review of CC source code `types/permissions.ts`, `utils/permissions/permissions.ts`, `toolExecution.ts`, `utils/permissions/yoloClassifier.ts`, `tools/AgentTool/forkSubagent.ts`.
|
||||
|
||||
### 1. PermissionResult: Not 3, but 4
|
||||
|
||||
The teaching version's three gates (deny → ask → allow) don't fully correspond to CC. CC's `PermissionResult` has 4 behaviors (`types/permissions.ts:241-266`):
|
||||
|
||||
| behavior | Meaning | Teaching Version Equivalent |
|
||||
|----------|---------|---------------------------|
|
||||
| `allow` | Allow directly | Gate 3 passes |
|
||||
| `deny` | Deny directly | Gate 1 matches |
|
||||
| `ask` | Show dialog to user | Gate 2 matches |
|
||||
| `passthrough` | Tool doesn't express opinion, passes to generic pipeline | Not in teaching version |
|
||||
|
||||
### 2. Production Verification Stages
|
||||
|
||||
CC's tool calls don't go through three gates — they go through multiple stages distributed across `checkPermissionsAndCallTool()` (`toolExecution.ts:599-1745`), hooks, `hasPermissionsToUseToolInner()` (`utils/permissions/permissions.ts:1158-1310`), and classifier logic:
|
||||
|
||||
1. **Zod schema validation** (`toolExecution.ts:614-680`) — parameter type checking
|
||||
2. **validateInput()** (`toolExecution.ts:682-733`) — tool-level semantic validation
|
||||
3. **backfillObservableInput()** (`toolExecution.ts:784`) — backfill legacy fields
|
||||
4. **PreToolUse hooks** (`toolExecution.ts:800-862`) — hooks can return allow/deny/ask
|
||||
5. **resolveHookPermissionDecision()** (`toolExecution.ts:921-931`) — coordinate hook + pipeline decisions
|
||||
6. **hasPermissionsToUseToolInner()** (`permissions.ts:1158-1310`) — multi-layer rule check:
|
||||
- Entire tool disabled by deny rule → `deny`
|
||||
- Entire tool flagged by ask rule → `ask`
|
||||
- `tool.checkPermissions()` tool's own judgment
|
||||
- Tool itself returns deny → `deny`
|
||||
- `requiresUserInteraction()` → `ask`
|
||||
- Content-related ask rules → `ask` (not bypassable)
|
||||
- Security check violation → `ask` (not bypassable)
|
||||
- bypassPermissions mode → `allow`
|
||||
- Entire tool allowed by allow rule → `allow`
|
||||
- passthrough → converted to `ask`
|
||||
|
||||
### 3. Deny List: Not One File, but 8 Sources
|
||||
|
||||
CC doesn't have a single deny list. Permission rules come from 8 sources (`types/permissions.ts:54-62`):
|
||||
|
||||
| Source | Configuration Location |
|
||||
|--------|----------------------|
|
||||
| `userSettings` | `~/.claude/settings.json` |
|
||||
| `projectSettings` | `.claude/settings.json` |
|
||||
| `localSettings` | `settings.local.json` |
|
||||
| `flagSettings` | Feature flags |
|
||||
| `policySettings` | Enterprise management policy |
|
||||
| `cliArg` | `--allowedTools` / `--deniedTools` |
|
||||
| `command` | Inline command |
|
||||
| `session` | In-session temporary authorization |
|
||||
|
||||
Each rule format: `{ toolName: "Bash", ruleBehavior: "deny", ruleContent: "npm publish:*" }`. Rules from multiple sources are merged, with higher-priority sources overriding lower ones (low to high: user < project < local < flag < policy, plus cliArg, command, session).
|
||||
|
||||
### 4. What is isDestructive()
|
||||
|
||||
In CC, `isDestructive` (`Tool.ts:405-406`) is **purely for UI display** — showing a `[destructive]` label in the tool list. It doesn't participate in permission decisions. All tools return `false` by default. Only ExitWorktree (on remove) and MCP tools (depending on `annotations.destructiveHint`) override it.
|
||||
|
||||
### 5. YoloClassifier (Auto-Approval)
|
||||
|
||||
In CC's auto mode, it doesn't pop a dialog every time. `classifyYoloAction` (`utils/permissions/yoloClassifier.ts:1012`) sends the tool call + conversation context to a classifier LLM to judge safety. It first tries acceptEdits mode simulation (`permissions.ts:620-656`, if acceptEdits allows → auto-approve), then checks the safe tool whitelist (`permissions.ts:658-686`), and finally calls the classifier. If the classifier rejects too many times in a row → falls back to manual approval.
|
||||
|
||||
### 6. Permission Bubbling
|
||||
|
||||
A sub-Agent's (forked via AgentTool) `permissionMode` is set to `'bubble'` (`forkSubagent.ts:50`). This means permission dialogs **bubble up to the parent Agent's terminal**, rather than being silently denied in the sub-Agent. The Bash classifier continues running during this process — displaying the permission dialog while judging in the background whether auto-approval is possible.
|
||||
|
||||
### The Teaching Version's Simplification Is Intentional
|
||||
|
||||
- Multi-stage pipeline → 3 gates: dramatically lower barrier to understanding
|
||||
- 8 rule sources → 1 local DENY_LIST: manageable concept count
|
||||
- isDestructive → omitted (teaching version has no UI layer, and it doesn't participate in permission decisions in CC either)
|
||||
- YoloClassifier → omitted (depends on additional LLM calls and telemetry)
|
||||
- Permission bubbling → omitted (s15 covers multi-Agent)
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
232
s03_permission/README.ja.md
Normal file
232
s03_permission/README.ja.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# s03: Permission — 実行前に権限を判断する
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → s02 → `s03` → [s04](../s04_hooks/) → s05 → ... → s20
|
||||
> *"ツール実行前に権限を判断"* — 権限パイプラインは、どの操作に承認が必要かを決める。
|
||||
>
|
||||
> **Harness レイヤー**: 権限 — ツール実行前に一つのゲートを追加。
|
||||
|
||||
---
|
||||
|
||||
## 課題
|
||||
|
||||
s02 の Agent は 5 つのツールを持つ。file tools は `safe_path` で保護されるが、bash は制限なし。「プロジェクトを掃除して」と頼むと、`rm -rf /` を実行しかねない。
|
||||
|
||||
安全性はモデルを信頼することではなく、コードに頼る — ツール実行前に判断を挟む。
|
||||
|
||||
---
|
||||
|
||||
## ソリューション
|
||||
|
||||

|
||||
|
||||
s02 のループは完全に維持される。唯一の変更は、ツール実行前に `check_permission()` を挿入すること — 各ツール呼び出しは 3 つのゲートを固定順序で通過する:ハード拒否が最優先、次にソフト確認、どちらも一致しなければ許可。
|
||||
|
||||
3 つのゲートは 3 つの決定に対応する:
|
||||
|
||||
| ゲート | 役割 | 一致時 |
|
||||
|--------|------|--------|
|
||||
| 1. 拒否リスト | 常に禁止される操作(`rm -rf /`、`sudo`) | 即座に拒否、実行しない |
|
||||
| 2. ルールマッチング | コンテキスト依存の操作(作業ディレクトリ外への書き込み、`rm` ファイル) | ゲート 3 へ |
|
||||
| 3. ユーザー承認 | ゲート 2 が一致した場合、ユーザー確認を待機 | ユーザーが許可または拒否を決定 |
|
||||
|
||||
3 つのゲートのどれにも一致しない → 直接実行。日常の操作の大部分はこの経路を通る。
|
||||
|
||||
---
|
||||
|
||||
## 仕組み
|
||||
|
||||

|
||||
|
||||
**ゲート 1**:ハード拒否リスト。最初に確認し、一致すればブロックメッセージを返す。(教育デモ:単純な文字列マッチングは信頼できるセキュリティ機構ではない — コマンドの変種やシェル展開で回避される可能性がある。CC のアプローチは付録を参照。)
|
||||
|
||||
```python
|
||||
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**:ルールマッチング — 「いつユーザーに聞くべきか」を記述する。各ルールはツールとチェック条件を指定する。
|
||||
|
||||
```python
|
||||
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**:ルールが一致した後、ユーザー入力を待機。
|
||||
|
||||
```python
|
||||
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 つのゲートを直列に接続**、ツール実行前に挿入する:
|
||||
|
||||
```python
|
||||
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() を挿入 |
|
||||
|
||||
---
|
||||
|
||||
## 試してみよう
|
||||
|
||||
```sh
|
||||
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 /tmp`(bash + 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:ループにフックを追加する。拡張ロジックはフックにぶら下げ、ループはクリーンに保つ。
|
||||
|
||||
<details>
|
||||
<summary>CC ソースコードを深掘り</summary>
|
||||
|
||||
> 以下は CC ソースコード `types/permissions.ts`、`utils/permissions/permissions.ts`、`toolExecution.ts`、`utils/permissions/yoloClassifier.ts`、`tools/AgentTool/forkSubagent.ts` の検証に基づく。
|
||||
|
||||
### 一、PermissionResult:3 種ではなく、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 hooks**(`toolExecution.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 では `isDestructive`(`Tool.ts:405-406`)は**純粋に UI 表示用** — ツール一覧に `[destructive]` ラベルを表示するだけ。権限決定には参加しない。デフォルトではすべてのツールが `false` を返す。ExitWorktree(remove 時)と MCP ツール(`annotations.destructiveHint` に依存)のみがオーバーライドする。
|
||||
|
||||
### 五、YoloClassifier(自動承認)
|
||||
|
||||
CC の auto モードでは、毎回ダイアログを表示するわけではない。`classifyYoloAction`(`utils/permissions/yoloClassifier.ts:1012`)はツール呼び出し + 会話コンテキストを分類器 LLM に送って安全性を判断する。まず acceptEdits モードのシミュレーションを試み(`permissions.ts:620-656`、acceptEdits が許可すれば → 自動承認)、次にセーフツールホワイトリストを確認し(`permissions.ts:658-686`)、最後に分類器を呼び出す。分類器が連続して拒否しすぎた場合 → 手動承認にフォールバック。
|
||||
|
||||
### 六、権限バブリング
|
||||
|
||||
サブ Agent(AgentTool 経由でフォークされたもの)の `permissionMode` は `'bubble'` に設定される(`forkSubagent.ts:50`)。これは権限ダイアログが**親 Agent のターミナルにバブルアップ**することを意味する。サブ Agent で黙って拒否されるのではない。Bash 分類器はこの過程で引き続き実行され — 権限ダイアログを表示しつつ、バックグラウンドで自動承認可能か判断する。
|
||||
|
||||
### 教育版の単純化は意図的
|
||||
|
||||
- 多段階パイプライン → 3 ゲート:理解のハードルが大幅に下がる
|
||||
- 8 ルールソース → 1 つのローカル DENY_LIST:概念量を制御可能
|
||||
- isDestructive → 省略(教育版には UI レイヤーがなく、CC でも権限決定には参加しない)
|
||||
- YoloClassifier → 省略(追加の LLM 呼び出しとテレメトリに依存)
|
||||
- 権限バブリング → 省略(s15 でマルチ Agent を扱う)
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
232
s03_permission/README.md
Normal file
232
s03_permission/README.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# s03: Permission — 执行前做权限判断
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → s02 → `s03` → [s04](../s04_hooks/) → s05 → ... → s20
|
||||
> *"工具执行前先做权限判断"* — 权限管线决定哪些操作需要审批。
|
||||
>
|
||||
> **Harness 层**: 权限 — 在工具执行前加一道门。
|
||||
|
||||
---
|
||||
|
||||
## 问题
|
||||
|
||||
s02 的 Agent 有 5 个工具。file tools 受 `safe_path` 保护,但 bash 不受限制。让它"清理一下项目",可能执行 `rm -rf /`。
|
||||
|
||||
安全不能靠信任模型,要靠代码——在工具执行之前做判断。
|
||||
|
||||
---
|
||||
|
||||
## 解决方案
|
||||
|
||||

|
||||
|
||||
s02 的循环完全保留。唯一的变动在工具执行前插入 `check_permission()`——每个工具调用经过三道闸门,顺序固定:硬拒绝优先,软询问次之,都没命中就放行。
|
||||
|
||||
三道闸门对应三种决策:
|
||||
|
||||
| 闸门 | 作用 | 命中后 |
|
||||
|------|------|--------|
|
||||
| 1. 拒绝列表 | 永远禁止的操作(`rm -rf /`、`sudo`) | 直接拒绝,不执行 |
|
||||
| 2. 规则匹配 | 取决于上下文的操作(写工作区外、`rm` 文件) | 交给闸门 3 |
|
||||
| 3. 用户审批 | 闸门 2 命中后,暂停等用户确认 | 用户决定允许或拒绝 |
|
||||
|
||||
三道都没命中 → 直接执行。大部分日常操作走这条路。
|
||||
|
||||
---
|
||||
|
||||
## 工作原理
|
||||
|
||||

|
||||
|
||||
**闸门 1**:一张硬拒绝表,先查,命中就返回阻止信息。(教学示意:简单字符串匹配不是可靠安全机制,命令变体和 shell 展开可能绕过。CC 的做法见附录。)
|
||||
|
||||
```python
|
||||
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**:规则匹配——描述"什么时候需要问用户"。每条规则指定工具和检查条件。
|
||||
|
||||
```python
|
||||
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**:规则命中后,暂停等用户输入。
|
||||
|
||||
```python
|
||||
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"
|
||||
```
|
||||
|
||||
**三道闸门串在一起**,插在工具执行之前:
|
||||
|
||||
```python
|
||||
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() |
|
||||
|
||||
---
|
||||
|
||||
## 试一下
|
||||
|
||||
```sh
|
||||
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 /tmp`(bash + 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 → 给循环加钩子,扩展逻辑挂在钩子上,循环保持干净。
|
||||
|
||||
<details>
|
||||
<summary>深入 CC 源码</summary>
|
||||
|
||||
> 以下基于 CC 源码 `types/permissions.ts`、`utils/permissions/permissions.ts`、`toolExecution.ts`、`utils/permissions/yoloClassifier.ts`、`tools/AgentTool/forkSubagent.ts` 的核查。
|
||||
|
||||
### 一、PermissionResult:不是 3 种,是 4 种
|
||||
|
||||
教学版的三道闸门(deny → ask → allow)和 CC 不完全对应。CC 的 `PermissionResult` 有 4 个 behavior(`types/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 hooks**(`toolExecution.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 中 `isDestructive`(`Tool.ts:405-406`)**纯粹是 UI 展示用的**——在工具列表里显示 `[destructive]` 标签。它不参与权限决策。默认所有工具都返回 `false`。只有 ExitWorktree(remove 时)和 MCP 工具(依赖 `annotations.destructiveHint`)覆写了它。
|
||||
|
||||
### 五、YoloClassifier(自动审批)
|
||||
|
||||
CC 的 auto 模式下,不会每次都弹对话框。`classifyYoloAction`(`utils/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)
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
251
s03_permission/code.py
Normal file
251
s03_permission/code.py
Normal file
@@ -0,0 +1,251 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
s03_permission.py - Permission System
|
||||
|
||||
Three gates inserted before tool execution:
|
||||
|
||||
Gate 1: Hard deny list (rm -rf /, sudo, ...)
|
||||
Gate 2: Rule matching (write outside workspace? destructive cmd?)
|
||||
Gate 3: User approval (pause and wait for confirmation)
|
||||
|
||||
+-------+ +--------+ +--------+ +--------+ +------+
|
||||
| Tool | -> | Gate 1 | -> | Gate 2 | -> | Gate 3 | -> | Exec |
|
||||
| call | | deny? | | match? | | allow? | | |
|
||||
+-------+ +--------+ +--------+ +--------+ +------+
|
||||
| | | |
|
||||
v v v v
|
||||
(normal) (blocked) (ask user) (user says no?)
|
||||
|
||||
Only one line added to the agent loop:
|
||||
|
||||
if not check_permission(block):
|
||||
continue
|
||||
|
||||
Builds on s02 (multi-tool). Usage:
|
||||
|
||||
python s03_permission/code.py
|
||||
Needs: pip install anthropic python-dotenv + ANTHROPIC_API_KEY in .env
|
||||
"""
|
||||
|
||||
import os, subprocess
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import readline
|
||||
readline.parse_and_bind('set bind-tty-special-chars off')
|
||||
readline.parse_and_bind('set input-meta on')
|
||||
readline.parse_and_bind('set output-meta on')
|
||||
readline.parse_and_bind('set convert-meta off')
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from anthropic import Anthropic
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv(override=True)
|
||||
if os.getenv("ANTHROPIC_BASE_URL"):
|
||||
os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
|
||||
|
||||
WORKDIR = Path.cwd()
|
||||
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
|
||||
MODEL = os.environ["MODEL_ID"]
|
||||
|
||||
SYSTEM = f"You are a coding agent at {WORKDIR}. All destructive operations require user approval."
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# FROM s02 (unchanged): Tool Implementations
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
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
|
||||
|
||||
|
||||
def run_bash(command: str) -> str:
|
||||
try:
|
||||
r = subprocess.run(command, shell=True, cwd=WORKDIR,
|
||||
capture_output=True, text=True, timeout=120)
|
||||
out = (r.stdout + r.stderr).strip()
|
||||
return out[:50000] if out else "(no output)"
|
||||
except subprocess.TimeoutExpired:
|
||||
return "Error: Timeout (120s)"
|
||||
|
||||
|
||||
def run_read(path: str, limit: int | None = None) -> str:
|
||||
try:
|
||||
lines = safe_path(path).read_text().splitlines()
|
||||
if limit and limit < len(lines):
|
||||
lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"]
|
||||
return "\n".join(lines)
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
def run_write(path: str, content: str) -> str:
|
||||
try:
|
||||
file_path = safe_path(path)
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_path.write_text(content)
|
||||
return f"Wrote {len(content)} bytes to {path}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
def run_edit(path: str, old_text: str, new_text: str) -> str:
|
||||
try:
|
||||
file_path = safe_path(path)
|
||||
text = file_path.read_text()
|
||||
if old_text not in text:
|
||||
return f"Error: text not found in {path}"
|
||||
file_path.write_text(text.replace(old_text, new_text, 1))
|
||||
return f"Edited {path}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
def run_glob(pattern: str) -> str:
|
||||
import glob as g
|
||||
try:
|
||||
results = []
|
||||
for match in g.glob(pattern, root_dir=WORKDIR):
|
||||
if (WORKDIR / match).resolve().is_relative_to(WORKDIR):
|
||||
results.append(match)
|
||||
return "\n".join(results) if results else "(no matches)"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# FROM s02 (unchanged): Tool Definitions & Dispatch
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
TOOLS = [
|
||||
{"name": "bash", "description": "Run a shell command.",
|
||||
"input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
|
||||
{"name": "read_file", "description": "Read file contents.",
|
||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}},
|
||||
{"name": "write_file", "description": "Write content to a file.",
|
||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
|
||||
{"name": "edit_file", "description": "Replace exact text in a file once.",
|
||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
|
||||
{"name": "glob", "description": "Find files matching a glob pattern.",
|
||||
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
|
||||
]
|
||||
|
||||
TOOL_HANDLERS = {
|
||||
"bash": run_bash, "read_file": run_read, "write_file": run_write,
|
||||
"edit_file": run_edit, "glob": run_glob,
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# NEW in s03: Three-Gate Permission Pipeline
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
# Gate 2: Rule matching — context-dependent checks
|
||||
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
|
||||
|
||||
|
||||
# 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⚠ {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"
|
||||
|
||||
|
||||
# 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⛔ {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
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# agent_loop — same as s02, with check_permission() inserted
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
def agent_loop(messages: list):
|
||||
while True:
|
||||
response = client.messages.create(
|
||||
model=MODEL, system=SYSTEM, messages=messages,
|
||||
tools=TOOLS, max_tokens=8000,
|
||||
)
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
|
||||
if response.stop_reason != "tool_use":
|
||||
return
|
||||
|
||||
results = []
|
||||
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})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("s03: Permission")
|
||||
print("输入问题,回车发送。输入 q 退出。\n")
|
||||
|
||||
history = []
|
||||
while True:
|
||||
try:
|
||||
query = input("\033[36ms03 >> \033[0m")
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
break
|
||||
if query.strip().lower() in ("q", "exit", ""):
|
||||
break
|
||||
history.append({"role": "user", "content": query})
|
||||
agent_loop(history)
|
||||
for block in history[-1]["content"]:
|
||||
if getattr(block, "type", None) == "text":
|
||||
print(block.text)
|
||||
print()
|
||||
97
s03_permission/images/permission-overview.en.svg
Normal file
97
s03_permission/images/permission-overview.en.svg
Normal file
@@ -0,0 +1,97 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 320" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
<marker id="arrow-blue" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#2563eb"/>
|
||||
</marker>
|
||||
<marker id="arrow-red" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/>
|
||||
<stop offset="100%" stop-color="#2563eb"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="720" height="320" fill="#fafbfc" rx="8"/>
|
||||
|
||||
<!-- Title -->
|
||||
<rect x="0" y="0" width="720" height="48" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="40" width="720" height="8" fill="url(#header)"/>
|
||||
<text x="360" y="31" fill="#fff" font-size="16" font-weight="700" text-anchor="middle">Permission — Loop unchanged, a gate before tool execution</text>
|
||||
|
||||
<!-- ===== s02 preserved (gray) ===== -->
|
||||
<text x="50" y="76" fill="#94a3b8" font-size="11" font-weight="600">s02 preserved</text>
|
||||
|
||||
<!-- User input -->
|
||||
<rect x="60" y="88" width="120" height="40" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="120" y="113" fill="#1e3a5f" font-size="12" font-weight="600" text-anchor="middle">messages[]</text>
|
||||
|
||||
<!-- Arrow → LLM -->
|
||||
<line x1="180" y1="108" x2="228" y2="108" stroke="#2563eb" stroke-width="1.5" marker-end="url(#arrow-blue)"/>
|
||||
|
||||
<!-- LLM -->
|
||||
<rect x="230" y="84" width="130" height="48" rx="8" fill="#fff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="295" y="104" fill="#1e3a5f" font-size="13" font-weight="700" text-anchor="middle">LLM</text>
|
||||
<text x="295" y="122" fill="#64748b" font-size="10" text-anchor="middle">stop_reason?</text>
|
||||
|
||||
<!-- No → return -->
|
||||
<line x1="295" y1="132" x2="295" y2="156" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="308" y="150" fill="#16a34a" font-size="9" font-weight="600">No</text>
|
||||
|
||||
<rect x="240" y="158" width="110" height="32" rx="16" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="295" y="178" fill="#166534" font-size="11" font-weight="600" text-anchor="middle">Return result</text>
|
||||
|
||||
<!-- Yes → next step -->
|
||||
<line x1="360" y1="108" x2="400" y2="108" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="380" y="100" fill="#d97706" font-size="9" font-weight="600">Yes</text>
|
||||
|
||||
<!-- ===== s03 new: Permission check ===== -->
|
||||
<text x="482" y="72" fill="#dc2626" font-size="11" font-weight="600" text-anchor="middle">s03 new</text>
|
||||
|
||||
<!-- Permission check box -->
|
||||
<rect x="402" y="78" width="160" height="120" rx="10" fill="#fef2f2" stroke="#dc2626" stroke-width="2" stroke-dasharray="6,3"/>
|
||||
<text x="482" y="100" fill="#991b1b" font-size="11" font-weight="700" text-anchor="middle">check_permission()</text>
|
||||
|
||||
<!-- Gate 1 -->
|
||||
<rect x="416" y="110" width="132" height="24" rx="4" fill="#fee2e2" stroke="#dc2626" stroke-width="1"/>
|
||||
<text x="482" y="126" fill="#991b1b" font-size="9" font-weight="600" text-anchor="middle">Gate 1: Deny List</text>
|
||||
|
||||
<!-- Gate 2 -->
|
||||
<rect x="416" y="140" width="132" height="24" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
|
||||
<text x="482" y="156" fill="#92400e" font-size="9" font-weight="600" text-anchor="middle">Gate 2: Rule Matching</text>
|
||||
|
||||
<!-- Gate 3 -->
|
||||
<rect x="416" y="170" width="132" height="24" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
|
||||
<text x="482" y="186" fill="#92400e" font-size="9" font-weight="600" text-anchor="middle">Gate 3: User Approval</text>
|
||||
|
||||
<!-- Deny → return deny message -->
|
||||
<path d="M 402 188 L 376 188 L 376 174 L 350 174" fill="none" stroke="#dc2626" stroke-width="1.5" marker-end="url(#arrow-red)"/>
|
||||
<text x="378" y="184" fill="#dc2626" font-size="8" font-weight="600">Deny</text>
|
||||
|
||||
<!-- Pass → tool execution -->
|
||||
<line x1="562" y1="138" x2="598" y2="138" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="575" y="132" fill="#16a34a" font-size="8" font-weight="600">Pass</text>
|
||||
|
||||
<!-- ===== s02 preserved: Tool execution ===== -->
|
||||
<text x="608" y="124" fill="#94a3b8" font-size="9">s02</text>
|
||||
|
||||
<!-- TOOL_HANDLERS -->
|
||||
<rect x="600" y="130" width="100" height="64" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="650" y="152" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL_</text>
|
||||
<text x="650" y="166" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">HANDLERS</text>
|
||||
<text x="650" y="184" fill="#64748b" font-size="8" text-anchor="middle">bash/read/write/...</text>
|
||||
|
||||
<!-- Arrow: tool results → back to messages -->
|
||||
<path d="M 700 162 L 710 162 L 710 230 L 120 230 L 120 128" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
|
||||
|
||||
<!-- ===== Legend ===== -->
|
||||
<rect x="60" y="260" width="600" height="44" rx="6" fill="#f1f5f9"/>
|
||||
<rect x="80" y="276" width="12" height="12" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="100" y="286" fill="#334155" font-size="10">s02 preserved (loop, LLM, dispatch — unchanged)</text>
|
||||
<rect x="400" y="276" width="12" height="12" rx="2" fill="#fef2f2" stroke="#dc2626" stroke-width="1"/>
|
||||
<text x="420" y="286" fill="#334155" font-size="10">s03 new (three-gate permission pipeline)</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
97
s03_permission/images/permission-overview.ja.svg
Normal file
97
s03_permission/images/permission-overview.ja.svg
Normal file
@@ -0,0 +1,97 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 320" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
<marker id="arrow-blue" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#2563eb"/>
|
||||
</marker>
|
||||
<marker id="arrow-red" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/>
|
||||
<stop offset="100%" stop-color="#2563eb"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- 背景 -->
|
||||
<rect width="720" height="320" fill="#fafbfc" rx="8"/>
|
||||
|
||||
<!-- タイトル -->
|
||||
<rect x="0" y="0" width="720" height="48" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="40" width="720" height="8" fill="url(#header)"/>
|
||||
<text x="360" y="31" fill="#fff" font-size="16" font-weight="700" text-anchor="middle">Permission — ループは変更なし、ツール実行前にゲートを追加</text>
|
||||
|
||||
<!-- ===== s02 維持(灰色) ===== -->
|
||||
<text x="50" y="76" fill="#94a3b8" font-size="11" font-weight="600">s02 維持</text>
|
||||
|
||||
<!-- ユーザー入力 -->
|
||||
<rect x="60" y="88" width="120" height="40" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="120" y="113" fill="#1e3a5f" font-size="12" font-weight="600" text-anchor="middle">messages[]</text>
|
||||
|
||||
<!-- 矢印 → LLM -->
|
||||
<line x1="180" y1="108" x2="228" y2="108" stroke="#2563eb" stroke-width="1.5" marker-end="url(#arrow-blue)"/>
|
||||
|
||||
<!-- LLM -->
|
||||
<rect x="230" y="84" width="130" height="48" rx="8" fill="#fff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="295" y="104" fill="#1e3a5f" font-size="13" font-weight="700" text-anchor="middle">LLM</text>
|
||||
<text x="295" y="122" fill="#64748b" font-size="10" text-anchor="middle">stop_reason?</text>
|
||||
|
||||
<!-- No → 戻る -->
|
||||
<line x1="295" y1="132" x2="295" y2="156" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="308" y="150" fill="#16a34a" font-size="9" font-weight="600">No</text>
|
||||
|
||||
<rect x="240" y="158" width="110" height="32" rx="16" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="295" y="178" fill="#166534" font-size="11" font-weight="600" text-anchor="middle">結果を返す</text>
|
||||
|
||||
<!-- Yes → 次へ -->
|
||||
<line x1="360" y1="108" x2="400" y2="108" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="380" y="100" fill="#d97706" font-size="9" font-weight="600">Yes</text>
|
||||
|
||||
<!-- ===== s03 新規:権限チェック ===== -->
|
||||
<text x="482" y="72" fill="#dc2626" font-size="11" font-weight="600" text-anchor="middle">s03 新規</text>
|
||||
|
||||
<!-- 権限チェック枠 -->
|
||||
<rect x="402" y="78" width="160" height="120" rx="10" fill="#fef2f2" stroke="#dc2626" stroke-width="2" stroke-dasharray="6,3"/>
|
||||
<text x="482" y="100" fill="#991b1b" font-size="11" font-weight="700" text-anchor="middle">check_permission()</text>
|
||||
|
||||
<!-- ゲート 1 -->
|
||||
<rect x="416" y="110" width="132" height="24" rx="4" fill="#fee2e2" stroke="#dc2626" stroke-width="1"/>
|
||||
<text x="482" y="126" fill="#991b1b" font-size="9" font-weight="600" text-anchor="middle">ゲート 1: 拒否リスト</text>
|
||||
|
||||
<!-- ゲート 2 -->
|
||||
<rect x="416" y="140" width="132" height="24" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
|
||||
<text x="482" y="156" fill="#92400e" font-size="9" font-weight="600" text-anchor="middle">ゲート 2: ルール照合</text>
|
||||
|
||||
<!-- ゲート 3 -->
|
||||
<rect x="416" y="170" width="132" height="24" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
|
||||
<text x="482" y="186" fill="#92400e" font-size="9" font-weight="600" text-anchor="middle">ゲート 3: ユーザー承認</text>
|
||||
|
||||
<!-- 拒否 → 拒否メッセージを返す -->
|
||||
<path d="M 402 188 L 376 188 L 376 174 L 350 174" fill="none" stroke="#dc2626" stroke-width="1.5" marker-end="url(#arrow-red)"/>
|
||||
<text x="378" y="184" fill="#dc2626" font-size="8" font-weight="600">拒否</text>
|
||||
|
||||
<!-- 通過 → ツール実行 -->
|
||||
<line x1="562" y1="138" x2="598" y2="138" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="575" y="132" fill="#16a34a" font-size="8" font-weight="600">通過</text>
|
||||
|
||||
<!-- ===== s02 維持:ツール実行 ===== -->
|
||||
<text x="608" y="124" fill="#94a3b8" font-size="9">s02</text>
|
||||
|
||||
<!-- TOOL_HANDLERS -->
|
||||
<rect x="600" y="130" width="100" height="64" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="650" y="152" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL_</text>
|
||||
<text x="650" y="166" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">HANDLERS</text>
|
||||
<text x="650" y="184" fill="#64748b" font-size="8" text-anchor="middle">bash/read/write/...</text>
|
||||
|
||||
<!-- 矢印:ツール結果 → メッセージリストに戻る -->
|
||||
<path d="M 700 162 L 710 162 L 710 230 L 120 230 L 120 128" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
|
||||
|
||||
<!-- ===== 凡例 ===== -->
|
||||
<rect x="60" y="260" width="600" height="44" rx="6" fill="#f1f5f9"/>
|
||||
<rect x="80" y="276" width="12" height="12" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="100" y="286" fill="#334155" font-size="10">s02 維持(ループ、LLM、ディスパッチ — 変更なし)</text>
|
||||
<rect x="400" y="276" width="12" height="12" rx="2" fill="#fef2f2" stroke="#dc2626" stroke-width="1"/>
|
||||
<text x="420" y="286" fill="#334155" font-size="10">s03 新規(3 ゲート権限パイプライン)</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
97
s03_permission/images/permission-overview.svg
Normal file
97
s03_permission/images/permission-overview.svg
Normal file
@@ -0,0 +1,97 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 320" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
<marker id="arrow-blue" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#2563eb"/>
|
||||
</marker>
|
||||
<marker id="arrow-red" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#dc2626"/>
|
||||
</marker>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/>
|
||||
<stop offset="100%" stop-color="#2563eb"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- 背景 -->
|
||||
<rect width="720" height="320" fill="#fafbfc" rx="8"/>
|
||||
|
||||
<!-- 标题 -->
|
||||
<rect x="0" y="0" width="720" height="48" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="40" width="720" height="8" fill="url(#header)"/>
|
||||
<text x="360" y="31" fill="#fff" font-size="16" font-weight="700" text-anchor="middle">Permission — 循环不变,工具执行前加一道门</text>
|
||||
|
||||
<!-- ===== s02 保留(灰色) ===== -->
|
||||
<text x="50" y="76" fill="#94a3b8" font-size="11" font-weight="600">s02 保留</text>
|
||||
|
||||
<!-- 用户输入 -->
|
||||
<rect x="60" y="88" width="120" height="40" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="120" y="113" fill="#1e3a5f" font-size="12" font-weight="600" text-anchor="middle">messages[]</text>
|
||||
|
||||
<!-- 箭头 → LLM -->
|
||||
<line x1="180" y1="108" x2="228" y2="108" stroke="#2563eb" stroke-width="1.5" marker-end="url(#arrow-blue)"/>
|
||||
|
||||
<!-- LLM -->
|
||||
<rect x="230" y="84" width="130" height="48" rx="8" fill="#fff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="295" y="104" fill="#1e3a5f" font-size="13" font-weight="700" text-anchor="middle">LLM</text>
|
||||
<text x="295" y="122" fill="#64748b" font-size="10" text-anchor="middle">stop_reason?</text>
|
||||
|
||||
<!-- 否 → 返回 -->
|
||||
<line x1="295" y1="132" x2="295" y2="156" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="308" y="150" fill="#16a34a" font-size="9" font-weight="600">否</text>
|
||||
|
||||
<rect x="240" y="158" width="110" height="32" rx="16" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="295" y="178" fill="#166534" font-size="11" font-weight="600" text-anchor="middle">返回结果</text>
|
||||
|
||||
<!-- 是 → 下一步 -->
|
||||
<line x1="360" y1="108" x2="400" y2="108" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="380" y="100" fill="#d97706" font-size="9" font-weight="600">是</text>
|
||||
|
||||
<!-- ===== s03 新增:权限检查 ===== -->
|
||||
<text x="482" y="72" fill="#dc2626" font-size="11" font-weight="600" text-anchor="middle">s03 新增</text>
|
||||
|
||||
<!-- 权限检查框 -->
|
||||
<rect x="402" y="78" width="160" height="120" rx="10" fill="#fef2f2" stroke="#dc2626" stroke-width="2" stroke-dasharray="6,3"/>
|
||||
<text x="482" y="100" fill="#991b1b" font-size="11" font-weight="700" text-anchor="middle">check_permission()</text>
|
||||
|
||||
<!-- 闸门 1 -->
|
||||
<rect x="416" y="110" width="132" height="24" rx="4" fill="#fee2e2" stroke="#dc2626" stroke-width="1"/>
|
||||
<text x="482" y="126" fill="#991b1b" font-size="9" font-weight="600" text-anchor="middle">闸门 1: 拒绝列表</text>
|
||||
|
||||
<!-- 闸门 2 -->
|
||||
<rect x="416" y="140" width="132" height="24" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
|
||||
<text x="482" y="156" fill="#92400e" font-size="9" font-weight="600" text-anchor="middle">闸门 2: 规则匹配</text>
|
||||
|
||||
<!-- 闸门 3 -->
|
||||
<rect x="416" y="170" width="132" height="24" rx="4" fill="#fef3c7" stroke="#d97706" stroke-width="1"/>
|
||||
<text x="482" y="186" fill="#92400e" font-size="9" font-weight="600" text-anchor="middle">闸门 3: 用户审批</text>
|
||||
|
||||
<!-- 拒绝 → 返回拒绝信息 -->
|
||||
<path d="M 402 188 L 376 188 L 376 174 L 350 174" fill="none" stroke="#dc2626" stroke-width="1.5" marker-end="url(#arrow-red)"/>
|
||||
<text x="378" y="184" fill="#dc2626" font-size="8" font-weight="600">拒绝</text>
|
||||
|
||||
<!-- 通过 → 工具执行 -->
|
||||
<line x1="562" y1="138" x2="598" y2="138" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="575" y="132" fill="#16a34a" font-size="8" font-weight="600">通过</text>
|
||||
|
||||
<!-- ===== s02 保留:工具执行 ===== -->
|
||||
<text x="608" y="124" fill="#94a3b8" font-size="9">s02</text>
|
||||
|
||||
<!-- TOOL_HANDLERS -->
|
||||
<rect x="600" y="130" width="100" height="64" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="650" y="152" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL_</text>
|
||||
<text x="650" y="166" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">HANDLERS</text>
|
||||
<text x="650" y="184" fill="#64748b" font-size="8" text-anchor="middle">bash/read/write/...</text>
|
||||
|
||||
<!-- 箭头:工具结果 → 回到消息列表 -->
|
||||
<path d="M 700 162 L 710 162 L 710 230 L 120 230 L 120 128" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
|
||||
|
||||
<!-- ===== 图例 ===== -->
|
||||
<rect x="60" y="260" width="600" height="44" rx="6" fill="#f1f5f9"/>
|
||||
<rect x="80" y="276" width="12" height="12" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="100" y="286" fill="#334155" font-size="10">s02 保留(循环、LLM、分发——完全不变)</text>
|
||||
<rect x="400" y="276" width="12" height="12" rx="2" fill="#fef2f2" stroke="#dc2626" stroke-width="1"/>
|
||||
<text x="420" y="286" fill="#334155" font-size="10">s03 新增(三道闸门权限管线)</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
61
s03_permission/images/permission-pipeline.en.svg
Normal file
61
s03_permission/images/permission-pipeline.en.svg
Normal file
@@ -0,0 +1,61 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 280" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#2563eb"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="720" height="280" fill="#fafbfc" rx="8"/>
|
||||
<rect x="0" y="0" width="720" height="38" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="30" width="720" height="8" fill="url(#header)"/>
|
||||
<text x="360" y="25" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Permission Pipeline — Three Gates</text>
|
||||
|
||||
<!-- Tool call enters -->
|
||||
<rect x="40" y="62" width="120" height="36" rx="6" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="100" y="84" fill="#1e40af" font-size="12" font-weight="600" text-anchor="middle">Tool call enters</text>
|
||||
|
||||
<line x1="160" y1="80" x2="210" y2="80" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Gate 1: Deny list -->
|
||||
<rect x="214" y="56" width="145" height="48" rx="6" fill="#fee2e2" stroke="#dc2626" stroke-width="2"/>
|
||||
<text x="286" y="76" fill="#991b1b" font-size="11" font-weight="700" text-anchor="middle">Gate 1: Deny List</text>
|
||||
<text x="286" y="94" fill="#991b1b" font-size="9" text-anchor="middle">rm -rf /, sudo, shutdown</text>
|
||||
|
||||
<line x1="359" y1="80" x2="409" y2="80" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Gate 2: Rule check -->
|
||||
<rect x="413" y="56" width="145" height="48" rx="6" fill="#fef3c7" stroke="#d97706" stroke-width="2"/>
|
||||
<text x="485" y="76" fill="#92400e" font-size="11" font-weight="700" text-anchor="middle">Gate 2: Rule Matching</text>
|
||||
<text x="485" y="94" fill="#92400e" font-size="9" text-anchor="middle">Write outside ws? Destructive?</text>
|
||||
<text x="485" y="116" fill="#166534" font-size="8" font-weight="600" text-anchor="middle">no match → allow</text>
|
||||
|
||||
<line x1="558" y1="80" x2="608" y2="80" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="583" y="72" fill="#92400e" font-size="8" font-weight="600" text-anchor="middle">match</text>
|
||||
|
||||
<!-- Gate 3: User approval -->
|
||||
<rect x="612" y="56" width="90" height="48" rx="6" fill="#fef3c7" stroke="#d97706" stroke-width="2"/>
|
||||
<text x="657" y="76" fill="#92400e" font-size="11" font-weight="700" text-anchor="middle">Gate 3</text>
|
||||
<text x="657" y="94" fill="#92400e" font-size="9" text-anchor="middle">User approval</text>
|
||||
<text x="657" y="116" fill="#64748b" font-size="8" font-weight="600" text-anchor="middle">allow / deny</text>
|
||||
|
||||
<!-- Results area -->
|
||||
<rect x="40" y="130" width="662" height="130" rx="6" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="60" y="152" fill="#1e3a5f" font-size="12" font-weight="600">Three Decisions</text>
|
||||
|
||||
<rect x="60" y="166" width="200" height="42" rx="4" fill="#fee2e2" stroke="#fca5a5" stroke-width="0.5"/>
|
||||
<text x="160" y="184" fill="#991b1b" font-size="11" font-weight="600" text-anchor="middle">Deny</text>
|
||||
<text x="160" y="200" fill="#991b1b" font-size="9" text-anchor="middle">Gate 1 hit, or user denied</text>
|
||||
|
||||
<rect x="280" y="166" width="200" height="42" rx="4" fill="#fef3c7" stroke="#fbbf24" stroke-width="0.5"/>
|
||||
<text x="380" y="184" fill="#92400e" font-size="11" font-weight="600" text-anchor="middle">Ask</text>
|
||||
<text x="380" y="200" fill="#92400e" font-size="9" text-anchor="middle">Gate 2 matched, enter Gate 3</text>
|
||||
|
||||
<rect x="500" y="166" width="182" height="42" rx="4" fill="#dcfce7" stroke="#86efac" stroke-width="0.5"/>
|
||||
<text x="591" y="184" fill="#166534" font-size="11" font-weight="600" text-anchor="middle">Allow</text>
|
||||
<text x="591" y="200" fill="#166534" font-size="9" text-anchor="middle">No rule hit, or user approved</text>
|
||||
|
||||
<text x="371" y="248" fill="#64748b" font-size="10" text-anchor="middle">Priority: hard deny → rule matching → if matched ask user; if unmatched allow by default</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
61
s03_permission/images/permission-pipeline.ja.svg
Normal file
61
s03_permission/images/permission-pipeline.ja.svg
Normal file
@@ -0,0 +1,61 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 280" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#2563eb"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="720" height="280" fill="#fafbfc" rx="8"/>
|
||||
<rect x="0" y="0" width="720" height="38" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="30" width="720" height="8" fill="url(#header)"/>
|
||||
<text x="360" y="25" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Permission Pipeline — 3 つのゲート</text>
|
||||
|
||||
<!-- Tool call enters -->
|
||||
<rect x="40" y="62" width="120" height="36" rx="6" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="100" y="84" fill="#1e40af" font-size="12" font-weight="600" text-anchor="middle">ツール呼び出し</text>
|
||||
|
||||
<line x1="160" y1="80" x2="210" y2="80" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Gate 1: Deny list -->
|
||||
<rect x="214" y="56" width="145" height="48" rx="6" fill="#fee2e2" stroke="#dc2626" stroke-width="2"/>
|
||||
<text x="286" y="76" fill="#991b1b" font-size="11" font-weight="700" text-anchor="middle">ゲート 1: 拒否リスト</text>
|
||||
<text x="286" y="94" fill="#991b1b" font-size="9" text-anchor="middle">rm -rf /, sudo, shutdown</text>
|
||||
|
||||
<line x1="359" y1="80" x2="409" y2="80" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Gate 2: Rule check -->
|
||||
<rect x="413" y="56" width="145" height="48" rx="6" fill="#fef3c7" stroke="#d97706" stroke-width="2"/>
|
||||
<text x="485" y="76" fill="#92400e" font-size="11" font-weight="700" text-anchor="middle">ゲート 2: ルール照合</text>
|
||||
<text x="485" y="94" fill="#92400e" font-size="9" text-anchor="middle">ws 外への書き込み?破壊的?</text>
|
||||
<text x="485" y="116" fill="#166534" font-size="8" font-weight="600" text-anchor="middle">不一致 → allow</text>
|
||||
|
||||
<line x1="558" y1="80" x2="608" y2="80" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="583" y="72" fill="#92400e" font-size="8" font-weight="600" text-anchor="middle">一致</text>
|
||||
|
||||
<!-- Gate 3: User approval -->
|
||||
<rect x="612" y="56" width="90" height="48" rx="6" fill="#fef3c7" stroke="#d97706" stroke-width="2"/>
|
||||
<text x="657" y="76" fill="#92400e" font-size="11" font-weight="700" text-anchor="middle">ゲート 3</text>
|
||||
<text x="657" y="94" fill="#92400e" font-size="9" text-anchor="middle">ユーザー承認</text>
|
||||
<text x="657" y="116" fill="#64748b" font-size="8" font-weight="600" text-anchor="middle">allow / deny</text>
|
||||
|
||||
<!-- Results area -->
|
||||
<rect x="40" y="130" width="662" height="130" rx="6" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="60" y="152" fill="#1e3a5f" font-size="12" font-weight="600">3 つの決定</text>
|
||||
|
||||
<rect x="60" y="166" width="200" height="42" rx="4" fill="#fee2e2" stroke="#fca5a5" stroke-width="0.5"/>
|
||||
<text x="160" y="184" fill="#991b1b" font-size="11" font-weight="600" text-anchor="middle">拒否 (deny)</text>
|
||||
<text x="160" y="200" fill="#991b1b" font-size="9" text-anchor="middle">ゲート 1 一致、またはユーザー拒否</text>
|
||||
|
||||
<rect x="280" y="166" width="200" height="42" rx="4" fill="#fef3c7" stroke="#fbbf24" stroke-width="0.5"/>
|
||||
<text x="380" y="184" fill="#92400e" font-size="11" font-weight="600" text-anchor="middle">確認 (ask)</text>
|
||||
<text x="380" y="200" fill="#92400e" font-size="9" text-anchor="middle">ゲート 2 一致、ゲート 3 へ</text>
|
||||
|
||||
<rect x="500" y="166" width="182" height="42" rx="4" fill="#dcfce7" stroke="#86efac" stroke-width="0.5"/>
|
||||
<text x="591" y="184" fill="#166534" font-size="11" font-weight="600" text-anchor="middle">許可 (allow)</text>
|
||||
<text x="591" y="200" fill="#166534" font-size="9" text-anchor="middle">ルール不一致、またはユーザー許可</text>
|
||||
|
||||
<text x="371" y="248" fill="#64748b" font-size="10" text-anchor="middle">優先順位:ハード拒否 → ルール照合 → 一致ならユーザー承認、不一致ならデフォルト許可</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
61
s03_permission/images/permission-pipeline.svg
Normal file
61
s03_permission/images/permission-pipeline.svg
Normal file
@@ -0,0 +1,61 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 280" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#2563eb"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="720" height="280" fill="#fafbfc" rx="8"/>
|
||||
<rect x="0" y="0" width="720" height="38" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="30" width="720" height="8" fill="url(#header)"/>
|
||||
<text x="360" y="25" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Permission Pipeline — 三道闸门</text>
|
||||
|
||||
<!-- Tool call enters -->
|
||||
<rect x="40" y="62" width="120" height="36" rx="6" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="100" y="84" fill="#1e40af" font-size="12" font-weight="600" text-anchor="middle">工具调用进入</text>
|
||||
|
||||
<line x1="160" y1="80" x2="210" y2="80" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Gate 1: Deny list -->
|
||||
<rect x="214" y="56" width="145" height="48" rx="6" fill="#fee2e2" stroke="#dc2626" stroke-width="2"/>
|
||||
<text x="286" y="76" fill="#991b1b" font-size="11" font-weight="700" text-anchor="middle">闸门 1: 拒绝列表</text>
|
||||
<text x="286" y="94" fill="#991b1b" font-size="9" text-anchor="middle">rm -rf /, sudo, shutdown</text>
|
||||
|
||||
<line x1="359" y1="80" x2="409" y2="80" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Gate 2: Rule check -->
|
||||
<rect x="413" y="56" width="145" height="48" rx="6" fill="#fef3c7" stroke="#d97706" stroke-width="2"/>
|
||||
<text x="485" y="76" fill="#92400e" font-size="11" font-weight="700" text-anchor="middle">闸门 2: 规则匹配</text>
|
||||
<text x="485" y="94" fill="#92400e" font-size="9" text-anchor="middle">写工作区外?读敏感路径?</text>
|
||||
<text x="485" y="116" fill="#166534" font-size="8" font-weight="600" text-anchor="middle">未命中 → allow</text>
|
||||
|
||||
<line x1="558" y1="80" x2="608" y2="80" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="583" y="72" fill="#92400e" font-size="8" font-weight="600" text-anchor="middle">命中</text>
|
||||
|
||||
<!-- Gate 3: User approval -->
|
||||
<rect x="612" y="56" width="90" height="48" rx="6" fill="#fef3c7" stroke="#d97706" stroke-width="2"/>
|
||||
<text x="657" y="76" fill="#92400e" font-size="11" font-weight="700" text-anchor="middle">闸门 3</text>
|
||||
<text x="657" y="94" fill="#92400e" font-size="9" text-anchor="middle">用户审批</text>
|
||||
<text x="657" y="116" fill="#64748b" font-size="8" font-weight="600" text-anchor="middle">允许 / 拒绝</text>
|
||||
|
||||
<!-- Results area -->
|
||||
<rect x="40" y="130" width="662" height="130" rx="6" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="60" y="152" fill="#1e3a5f" font-size="12" font-weight="600">三种决策</text>
|
||||
|
||||
<rect x="60" y="166" width="200" height="42" rx="4" fill="#fee2e2" stroke="#fca5a5" stroke-width="0.5"/>
|
||||
<text x="160" y="184" fill="#991b1b" font-size="11" font-weight="600" text-anchor="middle">阻止 (deny)</text>
|
||||
<text x="160" y="200" fill="#991b1b" font-size="9" text-anchor="middle">闸门 1 命中,或用户拒绝</text>
|
||||
|
||||
<rect x="280" y="166" width="200" height="42" rx="4" fill="#fef3c7" stroke="#fbbf24" stroke-width="0.5"/>
|
||||
<text x="380" y="184" fill="#92400e" font-size="11" font-weight="600" text-anchor="middle">询问 (ask)</text>
|
||||
<text x="380" y="200" fill="#92400e" font-size="9" text-anchor="middle">闸门 2 命中,进入闸门 3</text>
|
||||
|
||||
<rect x="500" y="166" width="182" height="42" rx="4" fill="#dcfce7" stroke="#86efac" stroke-width="0.5"/>
|
||||
<text x="591" y="184" fill="#166534" font-size="11" font-weight="600" text-anchor="middle">允许 (allow)</text>
|
||||
<text x="591" y="200" fill="#166534" font-size="9" text-anchor="middle">规则未命中,或用户允许</text>
|
||||
|
||||
<text x="371" y="248" fill="#64748b" font-size="10" text-anchor="middle">规则优先:闸门 1 硬拒绝 → 闸门 2 规则匹配 → 命中则用户审批,未命中默认允许</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
Reference in New Issue
Block a user