跳到正文
dsh-market 浏览插件 GitHub EN

BOWLUNA/dsh-zcode-git

给 DeepSeek Harness 智能体用的结构化 Git 工具:status、diff、log、branch、commit 与 stash。输出由十项 `git -c` 覆盖钉死,因此用户的 pager、颜色或 quotepath 设置无法改变智能体读到的东西;每次调用都是 argv 数组,任何 shell 都不解析参数;写入动作走 harness 的审批服务,没有装审批服务时拒绝执行而不是放行。

Star 数 ★ 0 分类 Git 与代码评审 收录于 2026-09-22

安装

在 DeepSeek Harness 里通过 dsh-market 安装

dsh plugin --profile web add dshmarket

或使用命令行

dsh plugin --profile web add github:BOWLUNA/dsh-zcode-git

装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络。请先审阅源码,并尽量锁定 commit(github:owner/repo#sha)。

README

English | 中文

test

给 DeepSeek Harness 智能体用的六个结构化 Git 工具: status、diff、log、branch、commit、stash —— 输出被钉死、参数不经过任何 shell、 每一次写入都走 harness 的审批服务。

DSH 本身没有任何 git 工具,模型只能通过 bash 手搓命令。这个插件就是来替掉那条路的。

dsh plugin --profile web add dsh-zcode-git

一条提交信息,两条路 —— shell 命令行与 argv 数组之间被实测出来的差异

与 ZCode 的关系

ZCode 是这个插件做法的来源,而且它的 git 层比大多数实现都扎实。 下表刻意写成可核对的:每条 ZCode 的主张都指到文件与行号,每条「多了什么」都指到一条能跑的命令, 而本插件还没追上的地方,那一行会直接写「暂未超过」,不会略去。

ZCode 有什么 本插件取了什么 本插件多了什么(优于在哪) 证据
workflow-git-world-read.ts:12(小节标题)与 :14 —— 「只读是构造出来的,不是检查出来的」:只能构造出五个只读子命令,且没有任何一条路径能拼出 shell 字符串 同一个前提:每次 git 调用走 ctx.subprocess.spawn 的 argv 数组,任何 shell 都看不到参数 写操作那一半,并且关在审批后面。 ZCode 的 git.* 按构造没有写路径;本插件增加了 commit / stash / branch 三类写操作,且 ctx.approval 是 fail-closed —— 没挂审批服务时拒绝执行而不是放行 node tools/verify-comparison.mjs → row 1 · test/exec.test.js · node tools/measure.mjs --only argv
workflow-git-world-read.ts:80 —— GIT_REF_PATTERN,以及 :72 —— 「首字符不许是 - 是这条规则里唯一安全相关的部分」 同一条规则:validateRevision 拒绝以 - 开头的 revision 扩到分支名、提交信息与路径 —— 而且这一行把「为什么只有 revision 需要这条短横线规则」写清楚了:所有用户提供的名字都排在 -- 之后,所以 git branch -- -b 是名字而不是选项。ZCode 在这一点上也是按构造达到同样的效果 node tools/verify-comparison.mjs → row 2(含 -- 位置的断言)· test/validate.test.js
workflow-git-world-read.ts:22 —— -z 的契约,记着 core.quotePath 与「含换行的文件名」;:98 —— GIT_STATUS_ARGV 里的那个 -z;:26 —— 「我们自己的 git log 也已经用 %x00 分隔字段」 同样的 -z,扩到 status 与 stash list 字段分隔符不同,而且我这边是更弱的选择:本插件用 %x1f,ZCode 用 %x00。NUL 在 git 对象里根本不可能出现,所以这一行本插件暂未超过 ZCode node tools/verify-comparison.mjs → row 3
git-snapshot.ts:127 —— execFile(GIT_COMMAND, args, {cwd, maxBuffer, timeout}):走了 argv,但不钉 -c、也不覆盖 env 同样走 argv 数组 钉死十项 git -c 覆盖(color.ui、core.pager、core.quotepath、diff.external、status.relativePaths、log.showSignature、log.date、diff.noprefix、diff.mnemonicPrefix、advice.detachedHead),用户的配置改不了模型读到的字节。ZCode 反而把 git -c 列进了它 bash 通道的 GIT_GLOBAL_DANGEROUS_FLAGS —— 在 shell 里那样做是对的,因为那个 flag 来自用户;而这里它由我们构造,且从不经过 shell node tools/verify-comparison.mjs → row 4(逐名断言十项都真的到了 git)
workflow-git-world-read.ts:28 —— 单一路径基准:线上一律仓库根相对,再用 rev-parse --show-prefix 剥前缀;:36 —— 「工作区就是仓库根时三者恰好相同,所以它会一直不被发现」 —— 暂未超过。 本插件这一层完全没做:会话 cwd 在子目录时交出仓库根相对路径,把那些路径喂回去会得到 ok: true, files: [], message: "" —— 静默地什么都没有。已建缺陷档 node tools/verify-comparison.mjs → row 5(断言该缺口仍然复现 —— 这条检查会在它被修好的那天变红,这正是目的)· node tools/_probe-pathbase.mjs
git-snapshot.ts:168 —— git status 按 2k 字符截断(而不是按文件条目数),以保持 provider-visible prompt 形状稳定 同一个关切:输出要有边界 按条目数与行数分别限,并把截断如实交出去而不是静默截断:maxDiffLines / maxLogEntries / clampInteger,接缝溢出时报 truncated: true 并给出溢出路径(实测:21.6 MiB 的补丁到模型手上是 264 KB) node tools/verify-comparison.mjs → row 6 · node tools/measure.mjs --only spill · test/validate.test.js

怎么自己复核

上面每一行都能归到一条命令上。表里没有任何东西需要「信」:

git clone https://github.com/BOWLUNA/dsh-zcode-git && cd dsh-zcode-git
npm install                       # 链上 harness 的 peer 包;本插件没有运行时依赖
node tools/verify-comparison.mjs  # 逐行核对上面的表 —— 某行说过头就以非零退出
node test/run.mjs                 # 95 项检查
node tools/measure.mjs            # 图里那些数字背后的一次实测,约 18 秒

第 5 行报的是 GAP REPRODUCES 而不是 PASS:它是缺陷、不是「优于」,而这条检查断言的是 这个缺陷仍然存在 —— 这样它被修好之后,README 就没法悄悄继续那么写。

本插件针对 bash 做了什么

用 bash 的问题 本插件的做法
输出形态随用户配置变化(color.ui、core.pager、core.quotepath、diff.external、status.relativePaths、alias) 每次调用都钉死这些配置,并解析 porcelain / NUL / %x1f 等机器可读格式
git commit -m "..." 要同时活过 cmd.exe、PowerShell 与 POSIX sh —— 三者转义规则不同,且都不会「碰巧」保住 %PATH%、$(...) 或换行 构造 argv 数组:参数根本不经过任何 shell
在 bash 里,git status 与 git push --force 权限一样大 读操作与写操作分开,写操作走 harness 的审批服务

环境要求

  • DeepSeek Harness >=0.1.5-rc.2 <0.1.6-0 || >=0.1.6-alpha.1 <0.2.0-0 || >=0.1.7-alpha.1 <0.2.0-0 —— 同一范围也声明在 engines.dsh
  • Node >=20,harness 自带的那份运行时即满足
  • PATH 上有一个 git 二进制

安装

dsh plugin --profile web add dsh-zcode-git

包自带 patch 声明,profile 的 bundles 会自动更新。装完重启 Harness —— bundle 层在启动时读取。

手工挂载:

# cordis.patch.yml
- insert:
    - id: tool-git
      name: dsh-zcode-git

工具清单

工具 类型 作用
git_status 读 分支、upstream、ahead/behind,以及 staged / modified / conflicted / untracked / ignored 五类清单
git_diff 读 逐文件行数统计 + unified diff,可用 paths、staged、contextLines 收窄
git_log 读 提交记录,返回 {hash, shortHash, author, date, subject},limit 限流,paths / revision 过滤
git_branch 读 + 写 list / create / switch / delete;写操作需审批
git_commit 写 可选先 paths 暂存,再提交 message;需审批
git_stash 读 + 写 list / push / pop / apply / drop;写操作需审批

所有工具都接受可选的 path 指定仓库目录,默认取会话工作目录,相对值按会话目录解析。

状态码

git_status 输出 git 自己的两列码 —— 先索引态、后工作区态,所以 .M 是未暂存的改动, M. 是已暂存的改动。同时也会把词形(index / worktree 字段)返回给偏好的调用方。

安全边界

绝不经过 shell。 每次调用都走 ctx.subprocess.spawn 的 argv 数组。Harness 的另一个接缝 ctx.shell 收的是一整行命令字符串,用它就等于把每个参数重新交给 shell 解析 —— 而不存在一套同时对 cmd.exe、PowerShell 和 POSIX sh 正确的转义(cmd.exe 在双引号内部 依然展开 %VAR%)。所以参数干脆不进 shell。含 "、`、$(...)、%PATH%、换行、 中文的提交信息会逐字节原样存入仓库。

输出被钉死。 以下配置在每次调用时强制覆盖(紧跟在 git 之后),因为每一项都会改变本插件 所解析的字节:

color.ui=false              core.pager=cat
core.quotepath=false        status.relativePaths=false
log.showSignature=false     log.date=default
diff.noprefix=false         diff.mnemonicPrefix=false
diff.external=              advice.detachedHead=false

行为类配置则刻意不覆盖:user.name、user.email、core.autocrlf、hooks、 core.sshCommand 是用户的意图,不是噪声。子进程环境额外注入 GIT_TERMINAL_PROMPT=0, 让需要凭据的操作立即失败而不是卡在一个没人能回答的提示上;以及 GIT_OPTIONAL_LOCKS=0, 让读操作不会因为别的窗口占着 index.lock 而失败。

写操作需审批。 git_commit,以及 git_branch / git_stash 的写入动作,都会在 执行前调用 ctx.approval.request()。如果 profile 没有装审批服务,写操作会被拒绝而不是放行 —— 「静默无保护的 git commit」正是本插件要消除的东西。只有在你明确接受这一点时, 才把 requireApprovalForWrites 设为 false。

路径先校验。 绝对路径、.. 穿越、UNC 路径、驱动器相对形式(C:foo)、 NTFS 备用数据流、Windows 保留设备名、控制字符,都在任何进程启动之前被拒绝。 git 自己也会拦大部分,但先校验能给出精确消息,并让明显敌意的输入连进程都不进。

刻意不提供: push、fetch、pull、reset、clean、rebase、remote、config 写入。 在 bash 里这些至少还躺在一行人类能读的命令里;做成工具就变成「一次调用摧毁工作区、 只靠一个提示框把关」。fetch 与 push 还牵涉凭据与主机密钥处理,值得单独设计,而不是在这里加个复选框。

配置

键 默认 含义
timeoutMs 30000 单次调用超时
maxDiffLines 500 默认补丁行数上限(钳制在 10–5000)
maxLogEntries 50 git_log 默认条数(钳制在 1–500)
requireApprovalForWrites true 写操作是否需要审批

patch 条目是整键替换而非深合并,所以要把想保留的键写全:

- insert:
    - id: tool-git
      name: dsh-zcode-git
      config:
        timeoutMs: 60000
        requireApprovalForWrites: true

开发

npm test          # node --test

95 个测试,除 harness 的 peer 包外零依赖,分六层:

  • test/parse.test.js —— 纯解析器,喂录制的 git 输出
  • test/validate.test.js —— 路径、提交信息、分支名、revision 校验
  • test/exec.test.js —— spawn spec,喂记录用的假实现
  • test/tools.test.js —— 工具行为,喂假 subprocess 服务
  • test/e2e.test.js —— 真实 git 二进制 + 真实临时仓库
  • test/schema.test.js —— 与 harness、provider 的 schema 兼容性

给后来写 DSH 工具插件的人

本插件里有四处设计,是因为「显而易见的做法」会失败,而且都不是在出错的地方报错。

① ctx.tools.register() 不编译任何东西。 它只校验 output.schema,然后原样存下定义。 parameters 的作者 DSL 不会被转换 —— 所以直接注册原始定义对象,等于把一个没有顶层 type 的裸属性 map 送给模型 provider,整个请求被拒。必须包一层: ctx.tools.register(defineTool(definition))。而且 provider 只报字母序第一个工具的名字, 于是错误指向一个本身没问题的工具。

② parameters 与 output.schema 用的是两套 DSL。 parameters 是扁平属性 map ({ path: { type: "string" } });output.schema 是裸 JSON Schema ({ type: "object", properties: { ... } })。把后者交给 parameters 会抛 parameters.type must be a value schema object;把前者交给 output.schema 会抛 schema.type must be string/…。

③ schema 关键字对版本敏感。 property 内的 required: true 在 dsh-tools 0.1.5-rc.2 上被接受、在 0.1.6-alpha.2 上被拒绝(UNSUPPORTED_SCHEMA);顶层 required: [...] 数组 两边都拒绝。工具 schema 里出现 enum 会让某个 provider 拒绝整个 function schema,报 must be a JSON Schema of 'type: "object"', got 'type: null'。所以本插件一个都不用, 而是在代码里强制同样的约束 —— 顺带得到了更好的错误消息: unsupported action "x"; expected one of list, create, switch, delete。

④ 返回值必须与 schema 严格一致。 在 schema 声明为 string 的位置放 null, 会让这一轮以 INVALID_TOOL_OUTPUT 失败,而此时 git 已经执行成功了。 没有值就省略字段,不要送 null。

⑤ 渲染器在工作已完成之后运行。 渲染器读了 execute 从未返回的字段,会在 harness 内部抛错, 把一次本来成功的调用变成失败的一轮。test/tools.test.js 之所以要拿每个工具的输出驱动它自己的 渲染器,就是为这个。

许可

MIT

内容来自项目 README(GitHub)↗

评论

评论存放在 GitHub Discussions。用 GitHub 账号登录后可发表评论或点表情。