|
| 1 | +# Deep Code Permission System (设计文档) |
| 2 | + |
| 3 | +scopes是枚举值,列表如下: |
| 4 | + |
| 5 | +``` |
| 6 | +# PermissionScope |
| 7 | +read-in-cwd |
| 8 | +read-out-cwd |
| 9 | +write-in-cwd |
| 10 | +write-out-cwd |
| 11 | +delete-in-cwd |
| 12 | +delete-out-cwd |
| 13 | +query-git-log |
| 14 | +mutate-git-log |
| 15 | +network |
| 16 | +mcp |
| 17 | +``` |
| 18 | + |
| 19 | +settings.json的配置项(例子): |
| 20 | + |
| 21 | +``` |
| 22 | +{ |
| 23 | + "permissions": { |
| 24 | + "allow": [ |
| 25 | + "write-in-cwd" |
| 26 | + ], |
| 27 | + "deny": [ |
| 28 | + "write-out-cwd" |
| 29 | + ], |
| 30 | + "ask": [ |
| 31 | + "read-out-cwd" |
| 32 | + ], |
| 33 | + "defaultMode": "allowAll|askAll" // 默认是allowAll |
| 34 | + } |
| 35 | +} |
| 36 | +``` |
| 37 | + |
| 38 | +工具和PermissionScope可能的对应关系: |
| 39 | + |
| 40 | +- read: read-in-cwd, read-out-cwd |
| 41 | +- write: write-in-cwd, write-out-cwd |
| 42 | +- edit: write-in-cwd, write-out-cwd |
| 43 | +- WebSearch: network |
| 44 | +- mcp__*: mcp |
| 45 | +- bash: 每一次bash命令需要的scope在sideEffects字段中。如果sideEffects字段为undefined|null,或者sideEffects包含了"unknown"则总是ask |
| 46 | +- 其他: 无权限要求,总是允许 |
| 47 | + |
| 48 | +## bash tool的参数schema新增sideEffects字段 |
| 49 | + |
| 50 | +目标:让LLM在每一次调用`bash`时显式声明该命令可能需要的权限范围,后端只信任这个结构化字段,不从自然语言`description`中推断权限。 |
| 51 | + |
| 52 | +需要同步修改两处schema: |
| 53 | + |
| 54 | +1. `src/prompt.ts`里的`getTools()`内置`bash`工具定义。 |
| 55 | +2. `templates/tools/bash.md`里的`bash`工具说明和JSON schema示例。 |
| 56 | + |
| 57 | +新增字段: |
| 58 | + |
| 59 | +``` |
| 60 | +sideEffects: PermissionScope[] | ["unknown"] |
| 61 | +``` |
| 62 | + |
| 63 | +`bash`可声明的scope只包含文件系统、Git历史和网络权限,不包含`mcp`: |
| 64 | + |
| 65 | +``` |
| 66 | +read-in-cwd |
| 67 | +read-out-cwd |
| 68 | +write-in-cwd |
| 69 | +write-out-cwd |
| 70 | +delete-in-cwd |
| 71 | +delete-out-cwd |
| 72 | +query-git-log |
| 73 | +mutate-git-log |
| 74 | +network |
| 75 | +unknown |
| 76 | +``` |
| 77 | + |
| 78 | +建议schema如下: |
| 79 | + |
| 80 | +```json |
| 81 | +{ |
| 82 | + "type": "object", |
| 83 | + "properties": { |
| 84 | + "command": { |
| 85 | + "description": "The command to execute", |
| 86 | + "type": "string" |
| 87 | + }, |
| 88 | + "description": { |
| 89 | + "description": "Clear, concise description of what this command does in active voice.", |
| 90 | + "type": "string" |
| 91 | + }, |
| 92 | + "sideEffects": { |
| 93 | + "description": "Permission scopes required by this bash command. Use [] only for commands that do not read, write, delete, or access the network. Use [\"unknown\"] when the effects cannot be classified safely.", |
| 94 | + "type": "array", |
| 95 | + "items": { |
| 96 | + "type": "string", |
| 97 | + "enum": [ |
| 98 | + "read-in-cwd", |
| 99 | + "read-out-cwd", |
| 100 | + "write-in-cwd", |
| 101 | + "write-out-cwd", |
| 102 | + "delete-in-cwd", |
| 103 | + "delete-out-cwd", |
| 104 | + "query-git-log", |
| 105 | + "mutate-git-log", |
| 106 | + "network", |
| 107 | + "unknown" |
| 108 | + ] |
| 109 | + }, |
| 110 | + "uniqueItems": true |
| 111 | + } |
| 112 | + }, |
| 113 | + "required": [ |
| 114 | + "command", |
| 115 | + "sideEffects" |
| 116 | + ], |
| 117 | + "additionalProperties": false |
| 118 | +} |
| 119 | +``` |
| 120 | + |
| 121 | +字段语义: |
| 122 | + |
| 123 | +- `sideEffects: []`表示命令不需要权限,例如`date`、`node --version`这类只读取进程环境或输出版本信息的命令。 |
| 124 | +- `sideEffects`必须按最小必要权限填写;例如`rg foo src`是`["read-in-cwd"]`,`npm install`通常是`["write-in-cwd", "network"]`。 |
| 125 | +- 如果命令访问项目目录之外的路径,需要使用`*-out-cwd`;例如`cat /etc/hosts`是`["read-out-cwd"]`。 |
| 126 | +- 删除类操作使用`delete-*`;如果同一条命令还会写入其他文件,再同时声明对应的`write-*`。 |
| 127 | +- 查询Git历史使用`query-git-log`;例如`git log`、`git show HEAD`、`git blame`、`git diff HEAD~1..HEAD`这类读取提交历史、提交对象或历史diff的命令。 |
| 128 | +- 修改Git历史或引用使用`mutate-git-log`;例如`git commit`、`git reset`、`git rebase`、`git merge`、`git cherry-pick`、`git tag`这类会创建提交、移动引用或改写提交图的命令。 |
| 129 | +- Git命令如果同时读写工作区文件,也需要同时声明文件系统scope;例如`git checkout -- src/foo.ts`需要`["write-in-cwd"]`,`git reset --hard HEAD~1`需要`["write-in-cwd", "mutate-git-log"]`。 |
| 130 | +- `unknown`只能单独出现为`["unknown"]`,不能和其他scope混用。 |
| 131 | + |
| 132 | +示例: |
| 133 | + |
| 134 | +```json |
| 135 | +{ "command": "date", "description": "Show current date", "sideEffects": [] } |
| 136 | +{ "command": "rg \"TODO\" src", "description": "Search TODO markers in source files", "sideEffects": ["read-in-cwd"] } |
| 137 | +{ "command": "npm install", "description": "Install package dependencies", "sideEffects": ["write-in-cwd", "network"] } |
| 138 | +{ "command": "rm -rf dist", "description": "Delete build output directory", "sideEffects": ["delete-in-cwd"] } |
| 139 | +{ "command": "curl -s https://example.com", "description": "Fetch example.com response", "sideEffects": ["network"] } |
| 140 | +{ "command": "git show --stat HEAD", "description": "Show file statistics for HEAD", "sideEffects": ["query-git-log"] } |
| 141 | +{ "command": "git blame src/prompt.ts", "description": "Show line authorship for prompt source", "sideEffects": ["read-in-cwd", "query-git-log"] } |
| 142 | +{ "command": "git reset --hard HEAD~1", "description": "Reset branch and worktree to previous commit", "sideEffects": ["write-in-cwd", "mutate-git-log"] } |
| 143 | +``` |
| 144 | + |
| 145 | +## 核心数据结构设计 |
| 146 | + |
| 147 | +``` |
| 148 | +export type UserPromptContent = { |
| 149 | + text?: string; |
| 150 | + imageUrls?: string[]; |
| 151 | + skills?: SkillInfo[]; |
| 152 | ++ permissions?: [{toolCallId: "...", permission: "allow|deny"}]; |
| 153 | ++ alwaysAllows?: ["<PermissionScope>"]; |
| 154 | +}; |
| 155 | +
|
| 156 | +export type SessionEntry = { |
| 157 | + id: string; |
| 158 | + ... |
| 159 | + toolCalls: unknown[] | null; // 例如:[{"id":"...","function":{"name":"bash","arguments":"{\"command\": \"...\", \"description\": \"...\"}"}}] |
| 160 | + status: SessionStatus; |
| 161 | ++ askPermissions?: [{toolCallId: "...", scopes: ["<PermissionScope>"], name: "...", command: "...", description?: "..."}]; |
| 162 | +}; |
| 163 | +
|
| 164 | +export type SessionStatus = "... | "completed" | "interrupted" | "ask_permission"; // 新增 ask_permission 状态 |
| 165 | +
|
| 166 | +export type SessionMessage = { |
| 167 | + ... |
| 168 | + meta?: MessageMeta; |
| 169 | + ... |
| 170 | +}; |
| 171 | +
|
| 172 | +export type MessageMeta = { |
| 173 | + ... |
| 174 | ++ permissions?: [{toolCallId: "...", permission: "allow|deny|ask"}]; |
| 175 | ++ userPrompt?: UserPromptContent; //对于role为user的消息,持久化userPrompt可方便后续排查问题 |
| 176 | +}; |
| 177 | +``` |
| 178 | + |
| 179 | +## 前端流程 |
| 180 | + |
| 181 | +如果当前会话状态不是ask_permission,则保持现状。会话状态是ask_permission时: |
| 182 | + |
| 183 | +对SessionEntry.askPermissions中每一个toolCallId的每一个scope,显示权限弹窗(示例): |
| 184 | + |
| 185 | +``` |
| 186 | +<toolName> |
| 187 | +
|
| 188 | + <command> |
| 189 | + <description> |
| 190 | +
|
| 191 | + Do you want to proceed? |
| 192 | + ❯ 1. Yes |
| 193 | + 2. Yes, and always allow <explain-of-the-scope> |
| 194 | + 3. No |
| 195 | +``` |
| 196 | + |
| 197 | +注意对于read/write/edit的`<command>`,格式可以是"工具名称+相对或绝对文件路径",例如:`read ~/dev/main.c` |
| 198 | + |
| 199 | +如果在权限弹窗过程中,用户按Esc,则走现有的interrupt流程(会话状态也应该变成"interrupted")。 |
| 200 | + |
| 201 | +提醒注意一种情况:例如askPermissions里面有好几个item的scopes是`["write-in-cwd"]`,如果用户已经在第一个权限弹窗选了"always allow write in CWD `~/dev/qrcode_test/`",则后面的几个scopes是`["write-in-cwd"]`的item就不用显示权限弹窗了。 |
| 202 | + |
| 203 | +如果用户完成了所有权限弹窗的选择,则判断: |
| 204 | + |
| 205 | +1. 如果用户提交的结果中包含deny,则需要用户输入user prompt,按回车手动提交replySession()。 |
| 206 | + - 如果用户没有输入user prompt就退出了,或者切换到了其他会话。则重新开始这个会话时,由于会话状态还是ask_permission,则会重新显示权限弹窗,要求用户选择。 |
| 207 | +2. 如果用户提交的结果中不包含deny,则以`/continue`作为UserPromptContent.text内容,前端自动提交replySession()。 |
| 208 | + |
| 209 | + |
| 210 | +## 后端流程 |
| 211 | + |
| 212 | +后端主要是对replySession()和activateSession()进行升级: |
| 213 | + |
| 214 | +1. 支持传入UserPromptContent.permissions和alwaysAllows |
| 215 | +2. 如果UserPromptContent.alwaysAllows非空,将其中的scopes追加写入项目级别的settings.json配置文件(`permissions.allow`字段),避免重复写入已存在的项。 |
| 216 | +3. 检查当前会话消息列表末尾是否存在连续的role为assistant的有tool_calls的消息,也就是"待执行消息"。如果没有,则走现有流程。 |
| 217 | +4. 对于每一条待执行消息,先检查UserPromptContent.permissions中对应的toolCallId的用户授权是allow还是deny |
| 218 | + - 如果是allow,则正常执行这个toolCall |
| 219 | + - 如果是deny,则直接返回失败结果,报错信息提示LLM用户禁用相关权限。例如: |
| 220 | + ``` |
| 221 | + { |
| 222 | + "ok": false, |
| 223 | + "name": "edit", |
| 224 | + "error": "用户已禁用了在项目目录之外修改文件的权限,请不要尝试用任何方式修改目录之外的文件" |
| 225 | + } |
| 226 | + ``` |
| 227 | +5. 如果对于某条待执行消息,在UserPromptContent.permissions没有出现对应的toolCallId的用户授权,则检查它的 SessionMessage.meta.permissions[].permission 是allow还是deny还是ask |
| 228 | + - 如果是allow,则正常执行这个toolCall |
| 229 | + - 如果是deny,则直接返回失败结果,报错信息提示LLM用户禁用相关权限 |
| 230 | + - 如果是ask,则直接返回失败结果,报错信息提示LLM用户未授权执行。例如: |
| 231 | + ``` |
| 232 | + { |
| 233 | + "ok": false, |
| 234 | + "name": "edit", |
| 235 | + "error": "用户暂未授权执行,如果有必要,可重新尝试执行" |
| 236 | + } |
| 237 | + ``` |
| 238 | + - 如果不存在,则正常执行这个toolCall(兼容老版本会话数据) |
| 239 | +6. 当LLM返回了新的待执行消息时,不要立即执行,而是: |
| 240 | + 1. 根据配置的permissions和defaultMode,计算出SessionMessage.meta.permissions字段 |
| 241 | + 2. 如果存在一个待执行消息的SessionMessage.meta.permissions[].permission是ask,则把SessionEntry.status设置为"ask_permission",并设置好SessionEntry.askPermissions,然后退出activateSession,这样就回到了上面的前端流程。 |
0 commit comments