权限与治理
Spark Work 权限治理:会话权限模式与 Profile 规则的两层判定、审批卡四档与超时行为、Rules 作用域、经典 Hooks 与 Hooks V2、用量账本,以及当前真正可查的审计记录入口。
SparkWork 的「权限与治理」不是一个开关,而是两条独立防线加几条旁路记录:会话权限模式决定 SDK 层是否把工具调用送到审批(apps/desktop/src/renderer/design/utils/permission-options.ts);权限 Profile 规则决定到达审批时是自动放行、自动拦截还是弹卡 (packages/agent-runtime/src/services/permission.service.ts)。 规则(Rules)、Hooks、用量账本、本地日志是另外四块独立能力,存储与入口各不相同。 本文只写代码里真实存在的枚举、入口与默认值,并明确标注哪些旧文档提法在当前代码中不存在。
1. 第一层:会话权限模式(SDK 层)
会话权限模式可按执行器(adapter)选择,三种适配器的可选值互斥。真实枚举与界面文案来自permission-options.ts,类型定义在 packages/protocol 的 SessionPermissionMode。
| 执行器 | 模式键名 | 界面文案 | 界面说明 |
|---|---|---|---|
claude-sdk / claude | claude-ask | 请求批准 | 每次工具执行前确认 |
claude-plan | 计划模式 | 先产出计划,再批准执行 | |
claude-auto-edits | 自动编辑 | 自动批准文件编辑 | |
claude-auto | 自动审批 | 使用自动权限策略 | |
claude-bypass | 完全访问 | 完全由 agent 执行 | |
codex | codex-default | 按需批准 | workspace-write;工作区内安全写入自动执行,越界操作请求批准 |
codex-auto-review | 替我批准 | workspace-write;越界操作交由 Codex 自动审查 | |
codex-full-access | 完全访问 | danger-full-access;允许修改 .git 和工作区外文件 | |
spark | spark-default | 手动审批 | 只读工具直接执行;写入与命令逐次确认 |
spark-auto | 自动审批 | 所有工具自动执行(显式 deny 规则仍生效) | |
spark-bypass | 完全访问 | 跳过全部审批与规则,完全由 agent 执行 |
协议里还保留了旧值 spark-accept-edits、spark-plan(只为已存储会话兼容, 界面不再提供)。模式不是「每次都要审批」的同义词,它的实际作用是映射到各内核的原生策略:
- Claude:
claude-ask→ SDKdefault;claude-auto-edits→acceptEdits;claude-auto→auto;claude-bypass→bypassPermissions;claude-plan→plan(只读 enforcement 交给 SDK 原生 plan 模式,不再把 Write/Edit 放进 disallowedTools)。 - Claude 路径还有一组永远拒绝的模式:
Bash(rm -rf /:*)、 fork bomb、Bash(mkfs:*)、Bash(dd if=/dev/zero:*),以及预设自带的Skill工具(SparkWork 用自己的 Skill 体系,调用预设 Skill 只会报 Unknown skill)。 - Codex:
codex-full-access→sandboxMode: danger-full-access+approvalPolicy: never;codex-auto-review→workspace-write+approvalPolicy: on-request+approvalsReviewer: auto_review;codex-default→workspace-write+on-request(无人值守场景降为never)。
三个真实入口:
- 输入框权限下拉(会话内切换):主对话框与 Agent 配置复用同一份
permission-options.ts,切换后会热更新运行中的 Claude 执行器。 - 设置 → 权限策略 → SDK 执行默认策略:可选「默认执行器」(Claude SDK / Codex / Spark)与 「默认权限策略」。这份默认值落在
app_settings的runtime-permissions/defaults,只作用于新会话与无会话输入区; 已有会话保留自己的策略。默认值是claude-sdk+claude-ask(Codex 适配器回落codex-default,Spark 回落spark-default)。 - 协议侧
session:update也带permissionMode,可对已有会话改模式。
2. 第二层:权限 Profile 规则与审批卡
规则层在 设置 → 权限策略(左栏分组「Agent」下的「权限策略」)。它由两部分组成: 选中的 Profile,以及该 Profile 下的动作类目规则。
动作类目是固定的 7 项,界面名称与说明如下(真实取值就是这些 action 字符串):
| action | 界面名称 | 覆盖的工具 |
|---|---|---|
file_read | 读取文件 | Read / Glob / Grep 等读取类工具 |
file_write | 编辑文件 | Write / Edit 等写入类工具 |
command_exec | 执行 shell 命令 | 非破坏性命令 |
command_dangerous | 高风险命令 | rm -rf、git reset --hard、sudo 等 |
network_known | 搜索网络 | WebSearch 等已知安全入口 |
network_unknown | 访问任意网页 | WebFetch 抓取任意 URL |
mcp_tool | 调用 MCP 工具 | 所有 mcp__ 前缀的第三方工具 |
每条规则有四种模式(下拉里是中文):allow 允许、ask 询问、ask-twice 双重确认、deny 拒绝。三个内置 Profile 的语义与默认规则种子:
- strict(一切都问):
file_read/file_write/command_exec/network_known/network_unknown/mcp_tool均为ask,command_dangerous为ask-twice。 - project-standard(工作区写入自动允许,默认 Profile):
file_read/file_write/network_known/mcp_tool为allow,command_exec为ask,command_dangerous为ask-twice,network_unknown为ask。 - trusted(自动允许大多数):除
command_dangerous为ask外, 其余动作类目都是allow。
存储位置:permission_profiles / permission_rules / permission_settings 三张表,当前激活的 Profile 存在 permission_settings 的 permission:active-profile。 IPC 里还有 permission:create-profile / permission:delete-profile, 但设置页目前只用到「列出 Profile / 改规则 / 切换激活 Profile」,没有新建/删除 Profile 的界面入口。
一次审批的完整判定顺序(PermissionService.requestApproval):
- 先把工具名映射成动作类目:
mcp__/mcp:前缀 →mcp_tool;Bash/bash/run_command/git等 →command_exec, 若命令命中危险正则则升级为command_dangerous;无法映射的工具兜底为command_exec,并且拿不到规则时模式默认ask(更安全)。 - 危险命令识别用的是正则,不是关键字列表:
rm -rf、git clean -fx、git reset --hard、sudo、chmod -r、chown -r、dd if=、mkfs、经典 fork bomb。 - 查会话级内存决策(用户点过「会话允许 / 会话拒绝」)→ 查 Profile 规则 → 查
permission_decisions表里的项目/全局记忆(键是action+toolName)。allow直接放行、deny直接拦截。 ask/ask-twice才推审批卡;ask-twice会连弹两次, 两次都放行才通过,任一次拒绝即拒绝。
审批卡长什么样(渲染端真实文案):
- 标题:
允许执行 <toolName>?;副标题显示Session xxxxxxxx · 风险 低/中/高。 - 按钮只有四个:拒绝、会话拒绝、会话允许、允许;
Esc等同拒绝。 - 正文是人话摘要:
修改文件/写入文件/运行命令/访问网络/搜索内容/使用 <toolName>,并列出最多 3 个关键字段 (文件、将要运行、搜索内容、网址、用途、任务); 底部「查看技术详情」折叠原始toolInputJSON。 - 高风险操作不会无限等待:30 分钟无人操作即按拒绝继续,同时推
stream:permission:approval-resolved(reasontimeout)收起卡片, 并往会话时间线写一条agent_error(codePERMISSION_TIMEOUT, 文案如「权限审批「Bash」等待超过 30 分钟已自动拒绝,已跳过该操作」)。会话被取消则写PERMISSION_CANCELLED。 - 卡片已失效后再点按钮,主进程返回
ok:false,界面提示「该权限请求已失效(等待超时或会话已取消), 你的选择未生效」。 - 协议里还存在
allow-project/allow-global(会落permission_decisions表长期记忆),但当前审批卡只暴露上面四个按钮,用户点不到项目/全局授权。 - Computer Use 有硬闸:
computer_direct_action与computer_unknown永远拒绝; 受管的桌面任务 start/resume 等在入口直接放行,不会弹应用内审批。
3. Rules:注入 prompt 的策略片段
Rules 是存在 SQLite(rules 表)里的文本片段,每条包含 name / content / priority / enabled 与一个作用域。 入口是 设置 → 规则。
| 作用域 scope | 设置页分组 | 行为 |
|---|---|---|
system | System(SYS) | 应用内置,只读;只能启用/停用,不能改内容、不能删除。首次会写入两条种子规则「安全约束」「代码风格」 |
team | Team(TEAM) | 团队管理员发布的规则 |
user | User(USER) | 用户全局偏好 |
project | Project(PROJ) | 可带 scopeRef(工作区 id);为空时对所有工作区生效 |
session | Session(SESS) | 会话级临时规则 |
注意设置页 Project 分组下方的说明文案写的是「.spark/rules · 当前工作区」,但代码里没有任何地方读取 .spark/rules/ 目录:项目规则同样存在 rules 表里。新增/编辑面板只有「名称 / 内容 / 优先级」三个字段,没有 scopeRef 选择器, 因此从界面新建的 project 规则 scopeRef 为空、对所有工作区生效。
真实生效链路(每 turn 组装系统提示词时):
- 取所有
enabled的system规则。 - 取
enabled的project规则,且scopeRef为空或等于当前 primary workspace id。 - 取「受管规则」:Agent 配置里勾选的规则(
agent.ruleIds)+ Workflow 节点配置里勾选的规则 (节点config.ruleIds),按priority降序。 - 以上合并去重后拼成
[Runtime Rules]段落注入系统提示词。
三个勾选入口都能在界面上找到:
- 设置 → 规则:分层列表,System 只读、其余可增删改与启停。
- Agent 编辑面板 →「规则」区(描述:约束 Agent 的行为与输出),多选
ruleIds。 - Workflow 节点 Inspector →「规则」字段,多选
ruleIds。
另外有两条容易混淆的链路:
- 项目规则文件是独立链路:
project-context.service.ts会读取工作区里的AGENTS.md/CLAUDE.md(以及.claude/、.codex/、.agents/下的同名文件)作为项目上下文,与 rules 表无关。 rules:compose(RuleCompositionEngine,作用域优先级system < team < user < project < session,支持override/merge)目前只经 IPC 暴露,没有接进 turn 组装链路。 也就是说「按作用域优先级合并」是需要显式调用才成立的能力,不是默认行为。
4. Hooks:两套并存的系统
设置里只有一个「Hooks」入口,但它下面挂着两套互不相同的机制,注意别混用名称。
① 经典 Hooks(通知/提示音)
- 节点只有四个:
permission_request(权限申请)、ask_user_question(Agent 提问)、session_end(Turn 正常结束)、session_fail(运行出错)。 - 每个节点可分别开关「提示音」与「系统通知」,总开关是
enabled,默认全开。 - 配置存在
app_settings的hooks/data(渲染端还同步一份到 localStorage 以即时渲染), 触发走hook:trigger/hook:play-sound/hook:show-notification。 - 界面位置:设置 → Hooks 页面底部的「经典通知」区块,可逐节点「测试」。
② Hooks V2(生命周期事件 + 动作)
- 事件名是点号形式,共 7 个:
turn.started、permission.requested、question.requested、response.committed、turn.completed、turn.failed、turn.cancelled。 - 动作目前三类:
builtin.notification(系统通知,可模板化 title/body)、builtin.sound(提示音)、tool.invoke(调用统一工具目录里的工具, 保存的是稳定引用sourceKind+sourceId+toolName)。 - 作用域是
application/workspace/agent/session; 它们是同一上下文的匹配集合,不是继承树,确定性优先级为session > agent > workspace > application。更高优先级可以显式停用低优先级绑定。 - 绑定安全:绑定时记录授权的
executionHash;定义改动后哈希变化,绑定进入needs_review并不再自动执行,需要重新授权。 - 执行语义:事件先写入
hook_events(outbox),由 dispatcher 派发到hook_runs队列,worker 每 2 秒轮询领取执行。默认超时 15 秒;重试策略默认unsafe(失败不重试), 可改为safe/keyed,最多 10 次、指数退避。 - 运行状态枚举:
queued/running/succeeded/failed/skipped/blocked/cancelled/outcome_unknown。界面可查看运行记录并重试/取消。 - 应用级总开关:关闭 = 停止领取 + 尽力取消运行中的动作;无法确认结果的运行落到
outcome_unknown,它不会回滚已经发生的外部副作用。 - 关键设计:Hook 由宿主确定性地在生命周期事实上触发(
permission.requested由 PermissionService 在真实进入等待时发射),不是让模型在提示词里自己决定要不要调 Hook。 - IPC 前缀是
hookV2::定义 CRUD、绑定 upsert、生效列表、运行记录、系统状态与开关、 预览、工具候选、试运行。
5. 用量账本
用量落在 SQLite 表 usage_ledger,字段为:session_id、provider_id、model_id、input_tokens、output_tokens、reasoning_output_tokens、cache_read_tokens、cache_write_tokens、cost_usd、request_timestamp、created_at。写入点是 per-turn 的增量核算(session/session-usage-ledger.ts), 按 turn 累计基线去重,避免同一份快照重复入账。
界面入口与能看到什么:
| 入口 | 内容 | 数据来源 |
|---|---|---|
| 设置 → 用量统计 | 本月概览(请求数 / 输入 / 输出 / 缓存命中 / 缓存写入)、累计统计、用量排行(模型·渠道两个 tab + 时间区间)、最近请求列表 | usage:get-dashboard、usage:get-by-date-range |
| 设置 → 用量统计 → 数据管理 | 刷新数据;「清理旧记录」删除 90 天以前明细(olderThanDays: 90) | usage:purge |
| 设置 → 通用(页面顶部) | 用量热力图与模型用量趋势卡 | usage:get-by-date-range |
| 同一张「用量排行」卡片的渠道 tab | 渠道维度的用量拆分(由模型分组数据在前端二次聚合) | usage:get-by-date-range 的 modelGroups |
协议里还有 usage:record 与 usage:get-session(会话级汇总)。 需要明确的是:当前没有 CSV/JSON 导出,也没有月度预算提醒—— 旧文档写过这两项,代码里找不到对应实现。
6. 审计与排错:现在真实存在的记录
重要事实:没有「设置 → 审计」这一页,也没有统一的全量审计面板。 可追溯性由下面几块拼起来:
- 本地日志(设置 → 本地日志):读写运行时日志文件,视图可切换
all/canvas(画布任务)/tools(工具运行)三个范围, 按debug/info/warn/error过滤,支持关键词过滤, 单次最多读 500 行;操作有刷新、导出、在文件夹中显示、清空。对应 IPC:log:read、log:clear、log:reveal。 日志级别与「运行时日志」开关同在设置 → 本地日志页。 - 会话时间线:审批超时/会话取消会写
agent_error事件 (PERMISSION_TIMEOUT/PERMISSION_CANCELLED), 这是回答「Agent 为什么跳过了这一步」的权威位置。 - 权限记忆表
permission_decisions:记录 scope(project/global)、action、tool_name、decision(allow/deny)。目前没有列出它的界面, 排查时只能查库。 - Hook 运行记录表
hook_runs:设置 → Hooks 内可查看与重试/取消。 - 插件运行时审计表
plugin_runtime_audit与跨会话引用审计表session_reference_audit:各自模块内部使用。
账号同步(设置 → 账号同步)覆盖的是 customCommands / prompts / memory / assistants / workflows / appearance / promptLibrary 这些类目,不包含权限、用量与审计数据; 这些数据都只在本机 SQLite。
7. 常见坑与排查
- 改了规则却感觉没生效:规则只参与「到达审批时的 allow/ask/deny 判定」, 而工具是否被送到审批回调由会话权限模式(SDK 层)决定。也是历史 bug 的高发点—— 早期审批回调固定
forcePrompt: true,导致设置页改的规则从不生效,现在已改为先过当前 Profile 规则判定。 - 「完全访问」不是更聪明,而是不拦:
claude-bypass/codex-full-access/spark-bypass跳过人工审批:Spark 的spark-bypass界面描述就是「跳过全部审批与规则,完全由 agent 执行」。 设置页在选中 danger 档位时会显示「当前默认策略会跳过人工审批……」的警告。 - 新会话没继承你刚改的默认策略:
runtime-permissions/defaults只作用于新会话与 无会话输入区;已有会话请在会话内切换权限下拉,或通过session:update改。 - 没登记过的第三方工具被当成命令执行类目:动作映射里查不到的工具会兜底为
command_exec,于是「执行 shell 命令」这条规则会意外约束它。别在设置页找 「browser_navigate」这种按工具名的开关——不存在。 - 会话允许/拒绝重启就没了:这两个决策只在内存中生效;跨重启的长期记忆只有 project/global 两档(
permission_decisions表)。 - 长时间离开导致任务被跳过:审批 30 分钟自动按拒绝处理,任务会继续走但跳过该操作, 事后到会话时间线看
PERMISSION_TIMEOUT记录即可确认。 - 以为
.spark/rules/里的文件会被读取:不会。项目规则请用设置 → 规则, 或直接写工作区的AGENTS.md/CLAUDE.md(那是项目上下文链路)。 - Hook 定义改了之后不再执行:这是有意的安全设计——
executionHash变化会让绑定进入needs_review,需要在绑定里重新授权。
8. 最佳实践
- 日常开发用
project-standard+ 对应适配器的「自动编辑 / 按需批准」;command_dangerous保持ask-twice,删除类命令自然会撞上双重确认。 - 团队统一约束不要写在
.spark/rules/:用team作用域规则, 或者把约定写进仓库的AGENTS.md(项目上下文会读取,且随 git 走)。 - 需要「某个 Agent 只额外遵守这几条规则」时,用 Agent 编辑面板的「规则」多选,而不是给全局加规则; Workflow 节点同理,用节点 Inspector 的「规则」字段。
- Hook 的
tool.invoke动作会真实触发外部副作用,且应用级开关关闭时不回滚已发生的动作; 先用界面上的「试运行/预览」验证条件与映射。 - 用量清理是有损操作(删除 90 天以前的明细)。需要长期留存请在清理前用数据库备份(设置 → 存储与备份)。
- 排查顺序建议:会话时间线(是不是被跳过/超时)→ 本地日志(tools 范围看工具调用)→ 权限 Profile 与 会话权限模式(为什么被拦/为什么没弹卡)→
permission_decisions表(是不是被记住放行了)。
速查表
- 权限模式(claude)
- claude-ask 请求批准 / claude-plan 计划模式 / claude-auto-edits 自动编辑 / claude-auto 自动审批 / claude-bypass 完全访问
- 权限模式(codex)
- codex-default 按需批准 / codex-auto-review 替我批准 / codex-full-access 完全访问
- 权限模式(spark)
- spark-default 手动审批 / spark-auto 自动审批 / spark-bypass 完全访问
- 规则模式
- allow 允许 / ask 询问 / ask-twice 双重确认 / deny 拒绝
- 权限 Profile
- strict 一切都问 / project-standard 工作区写入自动允许(默认) / trusted 自动允许大多数
- 动作类目
- file_read · file_write · command_exec · command_dangerous · network_known · network_unknown · mcp_tool
- 审批超时
- 30 分钟,超时按拒绝处理并写 PERMISSION_TIMEOUT
- Rules 作用域
- system / team / user / project / session(规则存 SQLite,不读 .spark/rules/)
- Hook V2 事件
- turn.started · permission.requested · question.requested · response.committed · turn.completed · turn.failed · turn.cancelled
- Hook 作用域
- application / workspace / agent / session,优先级 session > agent > workspace > application
- 用量表
- usage_ledger(tokens / 缓存读写 / cost_usd / request_timestamp)
- 排错入口
- 设置 → 本地日志(all / canvas / tools,最多 500 行)+ 会话时间线 agent_error
常见问题
展开 6 条常见问题
- 「请求批准 / 自动编辑 / 完全访问」和设置里的「允许 / 询问 / 双重确认 / 拒绝」是什么关系?
前者是会话权限模式(SDK 层,按适配器不同,例如 claude-ask / claude-auto / claude-bypass),决定工具调用是否被送进审批回调;后者是权限 Profile 里的动作类目规则,决定到达审批时是自动放行、自动拦截还是弹卡。两层叠加生效。
- 审批卡上的「会话允许」会写进数据库吗?
不会。会话允许/会话拒绝只写内存,重启应用即失效;会落库的是 project/global 两档记忆(permission_decisions 表),但当前审批卡没有提供项目/全局授权按钮。
- 审批一直不点会怎样?
30 分钟后按拒绝继续执行,卡片会自动收起,并在会话时间线写入一条 agent_error(PERMISSION_TIMEOUT),文案说明等待了多少分钟、跳过了哪个工具。
- Rules 的 System / Team / User / Project / Session 是继承关系吗?
不是。Rules 的作用域只是存储与分组维度:每 turn 实际注入的是「启用的 system 规则 + 启用的 project 规则(scopeRef 为空或等于当前工作区)+ Agent 勾选的规则 + Workflow 节点勾选的规则」。带优先级合并语义的 rules:compose 是另一条未接入 turn 链路的 IPC 能力。
- Hooks 里怎么在权限弹卡时发通知?
用 Hooks V2 的 permission.requested 事件配 builtin.notification 动作(经典 Hooks 的 permission_request 节点只能开提示音/系统通知,不能带条件判断)。注意 Hook 定义改动后绑定会进入 needs_review,需要重新授权才继续执行。
- 用量数据能导出吗?能设置月度预算提醒吗?
当前都不能。用量只在设置 → 用量统计与设置 → 通用的热力图/趋势卡里查看,数据管理只有「刷新」与「清理 90 天以前明细」。需要长期留存请用设置 → 存储与备份做数据库备份。