跳转到内容

11. 工具、权限与安全边界

Agent 系统的安全边界不在“模型有没有被提醒小心”,而在工具执行前的确定性检查。Craft 将 Claude hook 与 Pi JSONL request 都收敛到 runPreToolUseChecks(),再把需要人决策的部分返回成结构化 prompt。

来源 例子 执行位置
provider built-in Read/Write/Edit/Bash/Glob/Grep Claude SDK 或 Pi subprocess
session-scoped submit plan、browser、spawn、call_llm、set status in-process callback / MCP bridge
source proxy mcp__linear__create_issue 父进程 McpClientPool
mini/background title、summary、task helpers backend mini completion/host

无论来源,涉及文件、shell、网络 mutation 或系统权限时,都应在同一产品 policy 下运行。

持久枚举是:

type PermissionMode = 'safe' | 'ask' | 'allow-all'

UI 可能显示 Explore / Ask to Edit / Auto;代码注释偶尔保留旧称 explore/execute,但序列化值不可变化。

  • 只读 Bash pattern;
  • Read/Glob/Grep 等读取;
  • 禁止普通 Write/Edit/mutation;
  • 特例允许写当前 session 的精确 plansFolderPathdataFolderPath,用于提交计划与安全数据转换;
  • 不用弹窗,因为 safe 语义是“阻止”,不是“问”。
  • 读操作直接通过;
  • Bash、文件写、MCP/API mutation、admin 变成 permission prompt;
  • 可有 session-scoped always allow command/domain;
  • 高风险命令不会因宽松自定义规则静默放行。
  • 权限层自动通过写/命令;
  • 仍经过输入规范化、配置 schema、source activation、prerequisite 和某些结构安全 guard;
  • “allow all”不等于关闭整个 pipeline。

Pre-tool input 带一个 permissionMode,但中央管道会再次从 session mode manager 读取 canonical effective mode。若二者不同,只用后者决策并记录:

  • incoming/effective;
  • modeVersion
  • lastChangedBy/At

源码:pre-tool-use.ts

这防止 provider hook 捕获旧闭包:用户刚 Shift+Tab 改模式,旧 query 的 callback 参数仍可能是上一版。

PreToolUseCheckResult

type Result =
| { type: 'allow' }
| { type: 'modify'; input }
| { type: 'block'; reason; source? }
| { type: 'prompt'; promptType; description; ... }
| { type: 'source_activation_needed'; sourceSlug; sourceExists }
| { type: 'call_llm_intercept'; input }
| { type: 'spawn_session_intercept'; input }

为什么需要 modify?路径展开、skill qualification、metadata stripping、RTK command rewrite 都会改变真正执行的 input,但模型仍应看到与自己意图一致的 tool result。

为什么 intercept call_llm/spawn_session?它们需要 host 级 auth、session 创建与预算控制,不能当普通 MCP call 直接进 provider。

代码注释列出主顺序:runPreToolUseChecks

flowchart TD
I["normalized tool + input"] --> M["1. permission mode"]
M -->|block| B["block with diagnostics"]
M --> S["2. inactive source check"]
S -->|inactive| A["source activation needed"]
S --> P["3. prerequisite"]
P -->|missing guide/skill| B
P --> X["4. call_llm/spawn intercept"]
X --> T["5. input transforms + config guards"]
T --> Q["6. ask-mode prompt decision"]
Q -->|read/safe| OK["allow/modify"]
Q -->|mutation| U["permission prompt"]

顺序不是随意的。例如 inactive source 要在 tool execute 前变成 activation flow;prerequisite 要在 source mutation prompt 之前阻止,确保模型先读 guide;ask prompt 应基于原始 command 做安全判断,再把 RTK rewrite 作为执行 input。

shouldAllowToolInMode() 解析 tool 类别、Bash command、文件目标、API method/path 和 permissions.json 合并规则。

Safe 模式的 plans/data 例外必须比较规范化绝对路径;仅检查字符串前缀会被 ../ 或相似目录名绕过。路径处理章节与 sandbox 再做二次保护。

若阻止,reason 附 effective mode 与最后变更信息,帮助调试“明明切了 Auto 为什么还拦”。

对于 mcp__<slug>__<tool>

  • sessioncraft-agents-docs 是 built-in,不按用户 source 拦;
  • <slug> 不在 activeSourceSlugs:返回 activation needed;
  • 同时标记 source 是否存在,区分“已配置但未启用”和“根本没有配置”。

Agent adapter 调用 SessionManager 的 activation callback;成功后工具集热更新,并可触发原用户消息一次自动重试。

PrerequisiteManager 确保:

  • skill mention 后先读 SKILL.md;
  • source tool 前先读 guide.md;
  • compaction 后重新读;
  • Bash 中用于读取 pending skill 的命令可被识别并解除前置条件。

若没满足,返回 blocksource:'prerequisite'。这不是安全拒绝,而是可恢复的教学反馈;Agent 读文件后再次调用即可。

~、相对路径和特定 tool 字段规范成执行环境可理解的绝对路径。远程模式必须以 server working directory 为准。

写 workspace config/source/status/permission/automation 等文件前,解析候选内容并跑 schema;无效写在落盘前被阻止。

开启 feature flag 时,直接用 Bash/Write 操作 labels/automations 等受管目录会被阻止,并提示使用 craft-agent CLI/专用 tool。这样验证、migration 和事件不会被绕开。

把简短 skill name 补为 workspace/global 作用域下的明确身份,避免同名歧义。

Pi 可能在 input 里附 metadata.intent/displayName。这些用于产品 UI,不应传给真实 MCP/API schema,因此在执行前剥离,同时父进程另存显示元数据。

若启用,原 Bash 可重写成 RTK 包装命令以改善输出/工具体验。权限决策仍针对原命令,只有 SDK 执行拿 rewrite 后输入,避免包装器名字掩盖危险原命令。

Prompt 类型:

  • bash
  • file_write
  • mcp_mutation
  • api_mutation
  • admin_approval

结构化请求包含 description、command、appName、reason、impact、是否预期 OS prompt、remember window、command hash、approval TTL。UI/消息渠道不必解析字符串来决定按钮和风险说明。

PermissionManager 提供:

  • read-only pattern 判断;
  • base command extraction;
  • dangerous command 集;
  • network domain extraction/whitelist;
  • session-scoped alwaysAllowedCommands/domains。

危险集合包含删除/权限/磁盘/进程/关机、curl/wget/ssh/scp/rsync,以及 git push/reset/rebase/checkout 等。仅看第一个 token 不够:管道、&&、重定向、shell wrapper 都可能隐藏后续 mutation,所以 mode-manager 还有更完整的 shell 分析与 config rules。

安全结论:正则/allowlist 是降低风险,不是 shell 语义证明。高保证场景仍需 OS sandbox 或容器。

文件工具要同时考虑:

  • permission mode;
  • canonical path/符号链接;
  • workspace/session allowed roots;
  • safe plans/data 例外;
  • config domain schema;
  • session script sandbox 的 input/output root;
  • 跨机器/远程路径不存在。

session-tools-core/runtime/path-security.ts 与 filesystem isolation tests 覆盖 traversal/symlink 等,防止仅靠 prompt/PreToolUse 字段检查。

API wrapper 知道 HTTP method/path,GET 等读取可自动通过;POST/PATCH/DELETE 等在 ask 模式形成 api_mutation。MCP 没有统一 read/write 标志,只能结合 tool annotations、命名/配置与 source guide 分类。

风险:第三方 MCP 可能把删除动作命名成 get_status。权限系统不能完全信任名字,应优先使用 MCP readOnlyHint/destructiveHint 等标准 annotations,未知工具在 ask/safe 下保守处理。

少数操作(如 Homebrew 受保护目录步骤)需要原生管理员权限。分类器返回:

  • plain-language reason/impact;
  • requiresSystemPrompt
  • rememberForMinutes
  • commandHash
  • TTL。

PrivilegedExecutionBroker 在执行前比较 expected hash 与待执行 command hash,并检查过期/一次性状态。这样审批 UI显示的命令不能在批准后被替换。

仅传 boolean “approved”是不够的;批准必须绑定具体 command、session、时间窗。

Session-scoped whitelist 允许减少重复弹窗:

  • command 按规范化/base command 记录;
  • network command 可按 domain;
  • 有时间或 session 生命周期边界;
  • 不跨 workspace/global 默默永久化;
  • dangerous/admin 类可有更严格限制。

如果 command input 在审批后被 transform,完整性校验要明确 hash 针对原命令还是最终执行命令。Craft 的 RTK 策略是决策针对原命令,admin broker则绑定最终 privileged command。

session-tools-core/tool-defs.ts 用 Zod 定义 canonical schema/description/handler registry,再派生:

  • Claude SDK Zod shape;
  • Pi/MCP JSON Schema(移除 $schema 兼容 AJV);
  • 验证后的 typed handler input。

新增工具不应分别复制 schema。否则两 backend 可能接受不同字段、安全描述不同。

script_sandboxtransform_data 执行 Python/Node/Bun,但有:

  • runtime resolution;
  • input file allowlist;
  • output 必须在 session data;
  • traversal/symlink 校验;
  • 环境变量最小化;
  • network isolation(平台能力允许时);
  • timeout,script_sandbox 最大 15 秒;
  • stdout/result 限制。

这比给模型普通 Bash 更适合处理大 source response:数据落文件,脚本在受限目录转换,只把小结果回上下文。

Ask 模式需要人,但 headless server 可能只有 WebUI/Telegram。Permission request 是 SessionEvent,所以可以由不同渠道承接;消息 binding 的 approvalChannel 决定在 chat 还是 app 审批,但“是否要审批”仍由 session mode 决定。

若没有任何可达审批 surface,ask 会卡住。自动 TaskRunner 因此不会默认 ask;部署时健康检查应暴露 pending request,而不是让 session静默等待。

flowchart LR
MODEL["Untrusted model output"] --> SCHEMA["Tool schema parse"]
SCHEMA --> PRE["Central PreToolUse"]
PRE --> HUMAN["Optional human approval"]
HUMAN --> HOST["Host handler / SDK tool"]
HOST --> OS["Filesystem / process / network"]
HOST --> OUT["Result guard + binary/large externalization"]
OUT --> MODEL

每层负责不同问题:schema 防形状错误,PreToolUse 防策略越权,人审批给意图授权,host/sandbox 防 OS 越界,result guard 防返回内容撑爆上下文。

  • allow-all 允许高影响行为,仍需用户明确选择;
  • shell 静态分析无法覆盖所有语言/编码/间接执行;
  • MCP mutation 分类依赖第三方 annotations/配置;
  • 机器绑定凭据加密保护静态文件,但同用户下恶意进程仍可能调用系统/读取内存;
  • prompt injection 可能诱导合法工具组合造成危害,需 source trust 与人审批共同缓解;
  • remote capability 还需客户端侧校验,服务端允许不代表本机应无条件执行。

Craft 安全设计的核心不是三档按钮,而是让所有 backend 在执行前进入同一个判定管道,并把“阻止、修改、激活、前置读取、拦截、人工审批”建模成不同结果。安全语义因此不随 provider 切换。

下一章沿 source proxy tool 继续往下:一个 source 如何从文件配置、凭据/OAuth、连接池变成模型可调用的动态工具。