Git 隔离与安全护栏

工作区信任、隔离子会话的 git 拦截、主工作树与合并冲突提示、删除二次确认、先读后改、SSRF 防护,以及权限求值顺序与询问超时。

作者 David更新于 第 27 篇(共 33 篇)

MiMo Code 常让多个 agent 同时动一个仓库:Actor 子智能体、Orchestrator 用 --isolate 派出的子会话、Workflow 给子任务开的 worktree。本篇把分散在各处的护栏放在一起讲。它们大多是“把关”或“提示”,不是沙箱,源码注释对此也写得很直白。上游的权限模型见 权限系统,本篇只讲 MiMo 的改动。

怎么用

开关位置作用
--trustmimo 命令行跳过工作区信任提示
auto_worktree配置设为 true 时,主会话第一次改动 git 主工作树后注入一次提醒,默认关闭
/permission-timeoutTUI权限询问超时:不超时、30 秒、1、2、5、10 分钟
MIMOCODE_SKIP_ALL_FORCED_ASK_TIMEOUT_MS环境变量上面那个超时的初始毫秒数,沿用的旧名字
/skip-permissionsTUI运行期自动放行普通询问,删除确认除外
MIMOCODE_AUTO_APPROVE_DELETE环境变量设为 true 后不再单独确认删除类命令
--dangerously-skip-permissions(别名 --yolo)命令行TUI 在用户配置之下垫一条全放行规则,mimo run 则自动回复每个询问;两者都放行删除确认,显式 deny 仍然生效。TUI 在交互终端里会先要求确认
权限键 bash_delete配置 permission只有 deny 有效,allow 放不过删除确认

一条 bash 命令会依次经过下面几道关卡:

图表加载中…

工作区信任

mimo 启动 TUI 前检查当前目录(packages/opencode/src/cli/cmd/tui/thread.ts:240)。checkTrust(packages/opencode/src/project/workspace-trust.ts:23)分三档:目录正好是主目录(依次比较 HOME、USERPROFILE、HOMEDRIVE 拼 HOMEPATH 与 os.homedir())或文件系统根时为 dangerous;等于或位于 <data>/trusted-workspaces.json 所记路径之下时为 trusted;其余为 untrusted。

不是 trusted 就弹出选择框,取消或选否直接退出。untrusted 目录选择信任后写入列表(带文件锁),dangerous 目录每次都要确认,不会被记住。提示里有一句专门讲插件风险:

If malicious plugins exist in this directory, they can execute arbitrary code, read, modify, or exfiltrate your files.

(packages/opencode/src/cli/cmd/tui/i18n/en.ts:585)

--trust 跳过整个检查(thread.ts:199)。代码里只有 TUI 入口做这项检查,mimo run 不做。

隔离子会话:拦下跨分支的 git 操作

应用管理的 worktree 都建在 <data>/worktree/<项目 ID>/ 下(packages/opencode/src/worktree/index.ts:239)。同一仓库的所有 worktree 共用一个引用存储,子会话在自己的目录里执行 git update-ref refs/heads/main HEAD,一样能挪动主检出正在用的分支。packages/opencode/src/tool/isolated-git-guard.ts 的文件头注释记下了实测结果,并说明它要把 orchestrator 提示词里的叮嘱变成机制。

bash 工具在权限询问之前调用它(packages/opencode/src/tool/bash.ts:1282):只要会话自己的目录位于 <data>/worktree 之下,就检查 tree-sitter 解析出的每个命令节点,管道、&& 链与子 shell 里的 git 命令都不例外。依据的是会话目录而不是命令的 cwd,先 cd 进主检出再 rebase 同样会被拦。violates(isolated-git-guard.ts:158)先跳过 -C、-c、--git-dir 等全局选项找出子命令;下表的“自己的分支”指从 worktree 的 .git 文件读出的当前分支,读不到时一律按“不拥有”处理:

子命令被拦的情形
rebase、merge除 --abort、--continue、--skip、--quit 等处理进行中操作的旗标外一律拦
checkout、switch切到别的分支、--detach、用 -B 或 -C 强建别人的分支;带 -- 或多个位置参数(恢复文件)、目标是路径、新建分支都放行
branch带删除、强制移动或改名旗标,且对象不全是自己的分支
push强推或删除,且目标不是自己的分支;不写 refspec 视为推自己的分支,普通推送不拦
worktreeadd、remove、move、prune、repair、lock、unlock
update-ref目标不是自己的分支
symbolic-ref两个位置参数(写入)或删除
tag-f、-d 及其长写法;新建标签放行

reset 与 pull 故意不拦,它们只动当前 worktree 自己的分支;reset --hard 另有下文的删除确认。命中时直接抛错,信息里写明这个 worktree 与分支,要求只提交、推送自己的分支,把 rebase、merge 交给 orchestrator。注释也交代了能力边界(isolated-git-guard.ts:46):

This is a string/argv-level check over the already-parsed command AST. A determined command can evade it (shell aliases, sh -c "$(printf …)", eval, a wrapper script, a git alias configured in the repo). It is not a sandbox and does not try to be one.

主工作树:只提示,不拦截

bash 工具解析命令时还算出两样东西(bash.ts:1272):

  • isFileWrite(bash.ts:293):有写文件的重定向(2>&1 这类描述符复制不算),或命令头是 tee、cp、mv、mkdir、带 -i 的 sed、PowerShell 的 set-content 等。
  • mainWorktreeHits(bash.ts:519):跟踪 cd、pushd、popd 推算实际目录,收集重定向目标、写命令的路径参数(cp、ln、install 只算目标)、git -C 指向的目录,以及会改工作树的 git 子命令,再逐级向上找 .git 为目录的主工作树。

结果写进工具元数据。配置 auto_worktree 为 true 时(packages/opencode/src/config/config.ts:126),insertReminders 在主 agent 的根会话里扫描历史(packages/opencode/src/session/prompt.ts:1189):write、edit、apply_patch、multiedit、notebook_edit 成功写入的文件,或退出码为 0 的 bash 调用记下的命中,只要落在某个 git 主工作树里(packages/opencode/src/tool/auto-worktree-hint.ts:118),就在 user 消息上注入一段 Auto-Worktree Notice 提醒,并把会话的 auto_worktree_hint_sent 置位,每个会话只提醒一次。文案分两种(auto-worktree-hint.ts:190):仓库 .git/worktrees 下已有链接工作树,就让模型直接新建 worktree 切过去、不必先问;否则明确要求不要自作主张,先问用户要不要隔离。

同目录下的 conflict-detection.ts 是另一半。checkConflict(packages/opencode/src/tool/conflict-detection.ts:91)依次看三个信号:同目录有 5 分钟内更新过且未归档的会话;.git/index.lock 存在;有名为 claude、codex 或 cursor 的进程(Linux、macOS 上用 lsof 加 ps 查打开了该目录文件的进程,Windows 上用 wmic 只比进程名,命令不可用时视为无冲突)。它只被实验接口 POST /experimental/worktree/auto 调用,有冲突就新建 worktree(packages/opencode/src/server/routes/instance/experimental.ts:359),仓库里没有客户端调用这个接口。

对照设计文档 docs/compose/specs/auto-worktree.md:文档设想在创建会话时检测冲突、自动建 worktree,由 write、edit、apply_patch 工具各自注入提示,外部进程名单还有 Kilo Code;代码里会话创建路由没有接入冲突检测,提示统一在 insertReminders 注入,进程名单只有三项。docs/compose/spec/auto-worktree-config-toggle.md 描述的默认关闭与作用范围与代码一致。

合并冲突:说明冲突归谁

packages/opencode/src/tool/merge-conflict-notice.ts 的长注释交代了起因:orchestrator 提示词已经写明冲突应交还分支的主人,可在 mimo-v2.5 上 3 次实测都没照做,模型合并出冲突后自己读文件、改冲突标记、提交,有一次还删掉了作者的分支。注释认为系统提示词离当前决策太远,而工具结果在下一次工具调用之前必然被读到,于是改为在产生冲突的那次 bash 结果末尾追加说明(bash.ts:1218)。

annotate(merge-conflict-notice.ts:204)分两层判断。先做便宜的文本测试,决定值不值得起 git 进程:输出里有大写的 CONFLICT,或命令里 git 后面跟着 merge、rebase、cherry-pick、revert、pull、am 之一(merge-conflict-notice.ts:132)。再要求两个事实同时成立:git ls-files --unmerged 有输出,且 git 目录里存在 MERGE_HEAD、CHERRY_PICK_HEAD、REVERT_HEAD、rebase-merge、rebase-apply 之一。退出码不参与判断,git merge x || true 照样识别;只是打印了 CONFLICT 字样的 echo 则会在第二层被排除。

追加的文字列出未合并的文件、对应的中止命令(如 git merge --abort、git rebase --abort)和一条 session send <owning-session-id> "..." 模板,并声明这是内部上下文、不要复述给用户。这项检查对所有会话生效;隔离子会话里的 git merge 已被上一节的护栏拦下,两者不会打架。

worktree 服务的改动

与 v1.14.19 的 packages/opencode/src/worktree/index.ts 相比:

  • 分支前缀从 opencode/ 改为 mimocode/(worktree/index.ts:219)。
  • 按父仓库加信号量(worktree/index.ts:187),避免并发的 git worktree add 争用 .git/index.lock。
  • 原先先 worktree add --no-checkout -b、启动时再 reset --hard,现在一步 worktree add -b 直接检出,随后用 symbolic-ref 断言 HEAD 挂在新分支上(worktree/index.ts:253)。
  • 把父仓库解析出的 user.name、user.email 写进新 worktree 的本地配置(worktree/index.ts:295);bash 工具也会在项目仓库能解析出身份时补上 GIT_AUTHOR_NAME、GIT_AUTHOR_EMAIL、GIT_COMMITTER_NAME、GIT_COMMITTER_EMAIL,不覆盖用户已经设置的值(bash.ts:979)。
  • 删除 worktree 前先销毁该目录上的实例(worktree/index.ts:439);新增 head 与 isPristine(worktree/index.ts:648),Workflow 运行时据此删掉没有产生改动的 worktree(packages/opencode/src/workflow/runtime.ts:1027,见 Workflow 运行时)。

删除二次确认

bash 工具把删除类命令单独拎出来(bash.ts:117、bash.ts:135):rm、rmdir、unlink、shred,PowerShell 的 remove-item、del、rd 等,以及 git reset --hard、git clean -f、git branch -D、git tag -d、git worktree remove、git push --force、git stash drop 或 clear。命中时发起权限名为 bash_delete 的询问,always 为空,并代替普通的 bash 询问(bash.ts:748)。

bash_delete 在权限服务里属于 FORCED_ASK(packages/opencode/src/permission/index.ts:222):配置里的 allow 规则和“总是允许”的记录都放不过它,只有显式 deny 能提前拒绝;即使回复“总是允许”也按一次处理(permission/index.ts:573);/skip-permissions 同样不放行。例外有三种:

  • 每个删除目标都能解析出来,且都在系统临时目录之内(不含临时目录本身,也不含项目内的路径)时,不做二次确认,只走普通询问(bash.ts:917)。带通配符、变量或命令替换的参数一律算无法确认;git 类删除操作的是仓库状态,从不享受这个例外。
  • MIMOCODE_AUTO_APPROVE_DELETE=true 或 TUI 以 --dangerously-skip-permissions 启动时,实例的 autoApproveDelete 默认打开,bash_delete 自动通过,但还要再过一次普通 bash 询问(permission/index.ts:240、bash.ts:1308)。
  • mimo run --dangerously-skip-permissions 在运行期间通过接口临时打开这个开关,结束后复原(packages/opencode/src/cli/cmd/run.ts:38)。

识别也有漏网的写法:isDelete 直接把第二个词当作 git 子命令(bash.ts:547),git -C 目录 reset --hard 这种带全局选项的形式不会触发删除确认。

先读后改与可恢复错误

edit(替换已有内容时)与 notebook_edit 要求目标文件在对话里被 read 工具成功读过,否则抛出 RecoverableError,让模型先读再重试(packages/opencode/src/tool/read-state.ts:26、packages/opencode/src/tool/edit.ts:87)。路径相对会话工作目录解析,Windows 上不区分大小写。检查的是本轮请求实际带着的历史,压缩边界之前的读取不算数;multiedit 内部调用 edit,一并受约束;write 与 apply_patch 不检查。v1.14.19 只在 edit.txt 里写了“编辑前必须先读”的用法说明,代码并不强制。

RecoverableError(packages/opencode/src/tool/recoverable.ts:15)标记模型下一轮就能自己纠正的失败:参数校验不通过(工具包装层把 zod 错误统一转成它,packages/opencode/src/tool/tool.ts:121)、未读先改、MCP 工具本轮不可用、任务或 actor ID 不存在等。处理器给失败的工具部件加上 metadata.recoverable(packages/opencode/src/session/processor.ts:381),TUI 看到这个标记就把该行画成删除线,不显示红色错误块(packages/opencode/src/cli/cmd/tui/routes/session/index.tsx:3089);交给模型的错误文本不变。判断时先看类型,再退而检查结构上的 recoverable === true,免得经过 orDie 和序列化后 instanceof 失效(recoverable.ts:32)。

webfetch 的 SSRF 防护

v1.14.19 的 webfetch 不校验目标地址。MiMo 在询问权限之前调用 assertSafeUrl(packages/opencode/src/tool/webfetch.ts:38,实现在 packages/opencode/src/util/ssrf.ts:83):

  • 拒绝主机名 metadata.google.internal、metadata.goog、kubernetes.default.svc;
  • IPv4 拒绝以 10.、0. 开头的地址,以及 172.16 至 172.31、192.168、169.254 链路本地、100.64 至 100.127 共享地址段,外加阿里云元数据地址 100.100.100.200;
  • IPv6 拒绝 fe80: 链路本地、fc 与 fd 开头的唯一本地地址,以及映射到上述 IPv4 的地址;
  • 域名先做一次 DNS 解析再检查结果,解析失败也按拒绝处理。

响应回来后,若最终 URL 与请求 URL 不同,再校验一次(webfetch.ts:99)。几处边界:回环地址 127.0.0.0/8、localhost 与 ::1 不在拒绝范围内;DNS 检查用的是 lookup 返回的一个地址,真正发请求时 HTTP 客户端会重新解析;重定向由 HTTP 客户端自动跟随,事后校验只能拦住内容,请求已经发出。同文件里逐跳手动校验重定向的 safeFetch(ssrf.ts:58)在 src 里没有调用方。

权限:求值顺序、跳过与超时

Permission.ask(permission/index.ts:257)在上游基础上有这些改动:

  1. deny 不可被覆盖:v1.14.19 把规则集和运行期批准记录拼在一起取最后一条匹配,“总是允许”的记录可以压过配置里的 deny;MiMo 先单独用规则集求值,命中 deny 立即失败,批准记录只能把 ask 升级为 allow(permission/index.ts:272)。
  2. 强制询问:即上一节的 bash_delete。
  3. 运行期跳过:/skip-permissions 打开实例级的 skipAll,此后会阻塞的普通询问直接放行;打开的同时还会放行已经挂起的普通询问(permission/index.ts:310、permission/index.ts:618)。
  4. 非交互调用:系统派生的 agent 以 interactive: false 询问,需要人工确认时直接以 DeniedError 失败,不会挂起(permission/index.ts:370)。后台子智能体继承父会话已有授权、Orchestrator 子会话的审批转发,分别见 Actor 与 Orchestrator。

第 5 处是询问超时(packages/opencode/src/permission/index.ts:503):

      const timeoutMs = s.permissionAskTimeoutMs
      if (timeoutMs != null) {
        guarded = Effect.timeoutOrElse(guarded, {
          duration: `${timeoutMs} millis`,
          orElse: () =>
            bus
              .publish(Event.Replied, {
                sessionID: info.sessionID,
                requestID: info.id,
                reply: "reject",
              })
              .pipe(
                Effect.andThen(() =>
                  Effect.fail(
                    new CorrectedError({
                      feedback: `No user response within ${Math.round(timeoutMs / 1000)}s. This action was auto-rejected as a safety measure — NOT an explicit user denial. Skip this operation and continue with the rest of the task.`,
                    }),
                  ),
                ),
              ),
        })
      }

超时默认为 null,即一直等。设置途径有三:TUI 的 /permission-timeout 选择框,档位写在 packages/opencode/src/cli/cmd/tui/component/dialog-permission-timeout.tsx:8;接口 GET 与 POST /permission/ask-timeout(packages/opencode/src/server/routes/instance/permission.ts:176);旧环境变量 MIMOCODE_SKIP_ALL_FORCED_ASK_TIMEOUT_MS 给出新实例的初始值(permission/index.ts:30)。它对普通询问和强制询问都生效,与 skipAll 互不影响。超时失败用的是 CorrectedError:处理器只在 RejectedError 时把本轮标记为受阻(processor.ts:403),所以模型拿到反馈后会继续,反馈里也写明这不是用户的拒绝。

设计文档 docs/compose/spec/permission-skip-and-timeout-decouple.md 与代码基本一致,只是把接口写成了 /instance/permission/ask-timeout,实际路由挂在 /permission 下(packages/opencode/src/server/routes/instance/index.ts:61)。

下一篇:MCP:采样与工具检索——MCP 服务器反过来请模型补全时怎么选模型、怎么审批,以及工具太多时的按需检索。

本页目录