dshpack

August 27, 2026 · View on GitHub

把一个 dsh 场景——skills、MCP、profile patch、权限默认值——导出成一个可安装、可分享、可审计的 pack,再把 pack 装成标准的 dsh profile。

当前状态:0.5.x 已发布(npm i -g dshpack),仍属预发布。 pack 格式与 CLI 参数都还不是稳定 API。下方命令总表里的 18 个命令全部可用。

它不做什么

  • 不分发、不代理、不 fork @deepseek-ai/dsh
  • 不修改 dsh 上游仓库,也不碰你真实的 ~/.dsh(所有操作都要求显式的 DSH_HOME);
  • 不把第三方 install/build script 当可信代码执行;
  • 不热更新正在运行的 dsh 进程;
  • 不把 preset 合进已有内容的 session,也不替你解决 session 迁移冲突。

安装前你会看到什么

install 在写任何东西之前先给出完整计划。下面是真实输出,对 dsh-packs/web-dev--dry-run 得到:

Will install web-dev@0.1.0 as web-dev
SOURCE: {"kind":"directory","path":"...\dsh-packs\web-dev"}
dsh: current=0.1.0-rc.6 tested=0.1.0-rc.6 mismatch=false
pnpm: current=11.7.0
asset skills/frontend-review -> skills/frontend-review action=create collision=false [热生效]
MCP context7: https://mcp.context7.com/mcp -> profile patch action=configure [重启生效]
default permissionPreset=workspace-write [仅空白会话]
write profiles/web-dev [重启生效]
write profiles/web-dev/cordis.patch.yml [重启生效]
write skills/frontend-review [热生效]
write .dshpack/installed/web-dev.json [热生效]
side-effect profiles/web-dev/cordis.yml: dsh --dump-config(E9)
rollback snapshot: enabled=true state=sha256-bSGmM1FiexZIOjjfdgjsYA14kw8egAI8O58sWTaWVro

每一行都带生效时机标签,因为这是 dsh 最容易踩的坑:skills 热生效,插件与 MCP 要重启新进程,preset 只对空白新 session 有意义。计划里还单列了 side-effect——那一行不是我们写的,是 dsh 自己在 dump 时重写 cordis.yml,我们只是触发方,但仍然要告诉你。

--dry-run 期间对 DSH_HOME 的写入数为 0

Quickstart

corepack enable pnpm
corepack prepare pnpm@11.7.0 --activate
pnpm install --frozen-lockfile
pnpm build

# 只读校验一个本地 pack(不调用 dsh,零写入)
node packages/cli/dist/bin.js validate --strict <pack-dir>

# 看安装计划,不写任何东西
node packages/cli/dist/bin.js install --dry-run --as demo --dsh-home <隔离目> -- <pack-dir>

始终传一个隔离的 --dsh-home,不要拿你真实的 dsh 目录当试验场。

安全模型

这些不是承诺,是有测试锁住、且每条都有能让它转红的 mutant 的行为:

来源必须可复现。 GitHub 源只接受 40 位小写 commit SHA——分支名、tag、短 SHA、甚至 40 位但含大写,一律 exit 20 且在任何子进程启动前就拒绝。HTTPS tarball 必须带 sha512 SRI,URL 不得含 userinfo。

归档中的非普通条目不会被跟随或部署。 symbolic link、hard link、设备与 FIFO 会被逐条跳过;每个被跳过的路径都会作为 warning diagnostic 明确告诉你,路径越界和损坏 header 仍会整档拒绝。

--yes 不能替代危险确认。 它只能省掉那句"确定要装吗"。逐包的 --allow-builddanger-full-access 都仍需各自的显式授权;--replace(覆盖已有 profile)和 --allow-unverified 更是硬门——前者不给就不会发生,后者不给直接失败,都不走"提示一下"这条路。任何交互提示的默认值都是拒绝;非 TTY 环境下缺确认会 exit 21 并打印完整的非交互命令,不会把 CI 卡死。

build script 默认全禁。 依赖的 install/lifecycle script 只有在 pack 的 allowBuilds逐个精确包名列出、且你在安装时逐项授权后才会执行。父包、scope、或某个已授权的依赖,都不会把权限隐式传递出去。这是一份允许名单,不是 sandbox——放行一个 build script 等于允许该依赖以你的用户权限执行代码,所以它应该保持为空。

凭据不外泄,也不"悄悄删掉"。 export 在收集前、写入前、写入后各扫一次,命中直接 exit 31 失败——不会替你删掉再给一个看起来干净、实则不可审计的 pack。诊断只给 path:line:column绝不回显命中的值。扫描分五层:敏感键名、已知 token 形状(含 GitHub / npm / AWS / Google / Stripe / Slack / OpenAI 前缀)、Bearer|Basic、URL userinfo,以及基于 Shannon 熵的未知格式兜底。

一个刻意的取舍:32/40 位十六进制串与 UUID 被判为凭据。它们与 pack 强制要求的 40 位 commit SHA、校验和、以及各种合法 id 完全同形,纳入检测会让每个 pinned source 都误报。所以残余暴露面可以精确表述为:凭据要同时"存放在非敏感键名下""形状与合法标识符碰撞"才可能漏。

装坏了能退回去。 install 是一个带 journal 的事务。任一步失败都会回滚:新 profile 移进 $DSH_HOME/.dshpack/backups/<txid>/不删除),--replace 场景把原 profile 原样 rename 回去,skills/presets 只清理本次事务创建的项,settings 用保存的原文原子恢复。

退出码

含义
0成功
2用法 / schema
10环境(Node / pnpm / dsh 不可用)
20source、网络、完整性
21用户拒绝,或非交互环境缺少确认
22profile 冲突或锁
23dsh 子进程失败
24装后验证失败,但已干净回滚 —— 机器状态等同安装前,重试是安全的
25需人工恢复 —— 机器停在中间态,不要盲目重试,先按打印的恢复路径处理
30契约(patch / skill / settings / profile)
31安全(路径 / 凭据)
70内部错误

24 和 25 必须分开:自动化拿到退出码就要决定要不要重试,而这两种结局要求的响应恰好相反。JSON 输出里的 status 也区分(rolled-back / rollback-failed),但不该要求调用方解析 stdout 才知道能不能重试。

命令

装东西进来

命令作用副作用
validate <source>校验 pack 格式、来源、完整性、凭据零写入,且不调用 dsh
install <source>按计划以可回滚事务安装见上方计划输出
switch <profile>校验并打印启动命令默认不 spawn、不改 session;只有 --run 才前台启动 dsh

管住已经装进来的

命令作用副作用
list列出 tracked / untracked / reserved / broken profiles只读
status汇总所有 profile 的受跟踪状态只读;默认不联网--check-updates 才查上游
diff <profile>对比本地漂移与可选的上游差异只读
update <profile>更新到已验证的目标 pack三路合并;你后来改过的内容不会被静默覆盖
restore <profile>还原到某一代不丢弃你在那之后做的修改
uninstall <profile>卸载一个 tracked profile归属无法证明的内容一律保留,宁可留下也不误删
gc回收无引用的 CAS block 与过期代际只删确认无人引用的块
migrate把 legacy metadata 重建为 v1就地升级元数据
doctor诊断 dsh 环境与配置边界会写,见下
ui启动本机 Pack 管理服务(总览 / diff / 诊断 / pack 详情 / 自由组合 / Skill 编辑)仅监听 127.0.0.1;stdout URL 含本次启动 token,默认随机端口

status 每行给三个数,含义是钉死的:drift 是这个 profile 里被本地改过的资产数;update 只有加了 --check-updates 才不是 not-checkedshared 是"有多少资产还被别的 profile 用着"——按 profile 去重计数,同一个 profile 里两份内容相同的资产不算共享。这条区分是有意义的:shared 存在的意义就是回答"卸掉它会不会动到别人还要的字节",而自己的两份副本会随它一起被删。想看引用计数意义上的共享看 gc,那是另一个问题。任一项无法计算时输出 "unavailable" 而不是 0——读不出来和真的是零,对自动化不是一回事。

本机 UI:自由组合与 Skill 编辑

「自由组合」页可添加多个本地目录、github:tarball: 来源;先预览每个来源可选的 skill、provenance 与冲突,再对每个冲突明确选择 preferrename。预览不会写入 DSH_HOME;「组装并安装」仍走既有的 plan → 逐项授权 → apply 和 install 事务,未预览或尚有未决冲突时不能执行。

「Skill 内容编辑器」从 profile 的 skill 列表进入,并标出已有本地 drift。浏览器只提交 profileskillId 与文本,绝不提交文件路径;服务端将 id 限为安全字符并自行闭合到 skills/<skillId>/SKILL.md。单次内容上限为 256 KiB。保存是用户本地改动,不会重写该 profile 的 pack 基线,因此保存后 diff 会立即显示 drift。

做一个 pack 出来

命令作用副作用
init [dir]四档模板起草一个 pack收尾自动 lock + validate,任一道不过就整目录回滚
export把现有 profile 导出成 packdsh dump 会写 profile/cordis.yml
compose [compose.yml]按清单从多个来源组装一个 pack冲突必须显式解决;产出前过三次凭据扫描
lock [dir]生成/更新 pack.lock.yml只写该目录,产物确定且幂等
pack [dir]打成可复现且带 SRI 的 tarball产出 tarball + .sha512 + manifest.json

doctor 的副作用要说清楚,因为它容易被误以为只读:走 --dump-* 的检查项会让 dsh 重写 profile/cordis.yml(不是我们写的,但由我们触发),而 dshpack 自己会在 $DSH_HOME/.dshpack/logs/ 写审计日志。--jsonsideEffects 字段把两者都列出来并标注归属:

[
  { "owner": "dsh",     "path": "profile/cordis.yml" },
  { "owner": "dshpack", "path": ".dshpack/logs/<file>" }
]

全命令支持 --dsh-home--no-color--quiet--json。JSON 模式下 stdout 只有一个 object,进度走 stderr。

自由组装:从别人的 pack 取材

做一个 pack 有三条路:从零手写(init)、把自己现有的 profile 导出来(export)、或者从多个来源取材组装(compose)。第三条是 compose 存在的理由——你想要 A 包里的两个 skill 和 B 包里的一个,但不想 fork 任何一个。

# compose.yml
composeVersion: 0
name: my-research-kit
version: 0.1.0
description: 我自己攒的科研写作套装
author: your-name
license: MIT

include:
  # 来源一:本机某个 profile(内部走 export,需要 --dsh-home)
  - from: profile:research-writing
    skills: [paper-outline, citation-verification]

  # 来源二:别人发布的 pack —— 必须是 40 位小写 SHA,与 install 同一条纪律
  - from: github:dsh-packs/web-dev#3414f1af3fd674998cea81716586f4716a538f50
    skills: [commit-convention]

  # 来源三:本地目录;"*" 表示该来源全部 skill
  - from: ./my-skills
    skills: ["*"]

# 同名冲突必须在这里显式解决,只能二选一
resolve:
  - id: commit-convention
    rename: web-commit-convention   # 或 prefer: <上面某个 from>

defaults:
  permissionPreset: workspace-write
# 只报告将取什么、有哪些冲突,零写入
dshpack compose compose.yml --dry-run

# 真的组装(默认产出目录 = 与 compose.yml 同级的 <name>/)
dshpack --dsh-home <隔离目> compose compose.yml

冲突绝不静默。 两个来源给出同一个 skill id 时 exit 30,并列出全部冲突而不是只报第一个;你必须在 resolve 里选 renameprefer。"后来的覆盖先来的"是最容易写出来的行为,也是这里明确不做的——它意味着你的 pack 内容取决于 include 的书写顺序,而你不会注意到。rename 本身也不能制造新冲突:改名撞上另一个来源已有的 id 同样报错。

取不到就失败。 你点名的 skill 在来源里不存在时报错并列出该来源实际有什么,不会安静地少给你一个。

每个素材都记来源。 产出的 pack.yml 里有 provenance,逐条记 from / originalId / licensegithub: 记的是完整 40 位 SHA。来源 license 不明时需要显式 --allow-unknown-license;与你声明的 license 冲突时如实列出,绝不自动改写别人的许可声明

产出前过三次凭据扫描(写入前 / 写入后 / lock 之后),任一次命中即 exit 31零产出。收尾自动跑 lockvalidate,任一道不过就整体回滚——不会留下半个目录。

上面这段冲突行为有一个可以直接跑的最小示例在 examples/compose/:两个本地来源都提供 note-taking,用 prefer 裁决。CI 会把它组装一遍,并且验证删掉 resolve 后确实以 30 被拒——所以它不会退化成一个没有冲突的示例还继续绿着。

pack 目录长什么样

pack 目录里的文件分三类,各有各的待遇:

类别成员布局校验会被部署进 lock过凭据扫描
pack 语义文件pack.ymlpack.lock.ymlskills/patch/settings/未知项拒绝
仓库常规物README*LICENSE*.gitignore.github/CHANGELOG*允许
完全忽略.git/node_modules/不遍历

第二类允许但不部署,然而照样要过凭据扫描——README 里贴了 token 是真实且常见的事故。第三类是根本不进去,不是"进去了再忽略"。

pack.lock.yml 是必需的,且应当提交进 git。手写的 pack 用 dshpack lock 生成。

JSON Schema 随 @dshpack/core 一起发布,可以直接 resolve:

import schema from '@dshpack/core/schemas/pack.schema.json' with { type: 'json' };

它由 TypeBox 真源生成,发布前会解包 tarball 逐字节比对——schema 必须在包里,且必须与真源一致,两个方向都有断言。

GitHub 仓库网址与 fake-IP 代理

installcompose 的来源可以直接写 github:owner/repohttps://github.com/owner/repo(可带尾随 /.git)。dshpack 会先查询该仓库的默认 分支 HEAD,再把它转换为 github:owner/repo#<40位小写SHA>;计划、lock 和 provenance 只记录这个固定 SHA,不会把裸网址作为可落盘来源。

若你的代理使用 fake-IP 或透明代理,让 codeload.github.com 等主机在本地 DNS 中解析为保留 地址,请显式设置 DSHPACK_TRUST_LOCAL_DNS=1

DSHPACK_TRUST_LOCAL_DNS=1 dshpack install --dry-run --as demo --dsh-home <isolated-home> -- https://github.com/owner/repo

该开关只信任用户自己的 DNS/路由对主机名给出的地址;localhost*.localhost 和 IP 字面量仍然被拒绝。未设置时保留默认 SSRF DNS 预检。

Starter packs

两个示例 pack 在 dsh-packs org:web-dev(四个前端 skills + 零凭据的 context7 文档检索 MCP)与 research-writing(五个研究写作 skills,刻意不连任何 MCP)。

两者都在 Windows 原生WSL2 Ubuntu 24.04 原生 ext4 上做过端到端认证:install → 新 dsh 进程 --dump-config 行全命中 → doctor --strict 全部 exit 0。矩阵与全部原始 stdout/stderr/exit code 见 docs/starter-pack-certification.md。两个 pack 都不声明 allowBuilds——即没有任何 build script 被授权。安装它们时若出现要求授权 build script 的提示,说明来源不对,请中止。

research-writing 不连 MCP 是设计决定,不是没做完:研究场景处理的是未发表稿件与他人版权材料,任何 MCP 都意味着内容离开你的机器。真实 dump 里已验证它零 MCP 配置。

你需要知道的限制

  1. 插件与 MCP 变更需要新的 dsh 进程,不会热加载。skills 是热生效的。
  2. agent preset 只对空白新 session 生效,不会补写或合并已有 session。M0 的两个 starter pack 都不带 preset。
  3. skills 是提示词内容,不是校验器citation-verification 告诉你怎么核实引用,但不会替你去核。
  4. pack 格式在 M0 不是稳定 API

平台

组件版本状态
Node.js>=22.19.0 <25CI 用 22.19.0;真机实测覆盖 22.19.0 与 24.13.1
pnpm11.7.0Corepack 固定
dsh0.1.0-rc.6契约 smoke 使用;非产品依赖承诺
Windows原生阻塞 CI + 真机认证
UbuntuGitHub runner + WSL2 24.04阻塞 CI + 真机认证

Windows 上开发命令应在 PowerShell 里直接可用,不依赖 Bash 专属语法、符号链接权限或大小写敏感路径。升级固定版本须走独立 PR,同步更新配置、lockfile、CI、本表与 ADR-0002

故障排查

  • exit 70 INTERNAL:内部错误,不是你的用法问题。带上脱敏后的命令与版本报 issue。
  • exit 10 E_PROBEdshpnpm 不在 PATH 上。注意 Windows 上可能同时存在无扩展名的 dshdsh.CMD
  • exit 25:不要重试。先按输出里的人工恢复路径处理,机器处于中间态。
  • 装完插件但 dsh 没变化:插件变更不热加载,必须完全退出并启动新的 dsh 进程。
  • pnpm install --frozen-lockfile 失败:不要手改 lockfile,确认 Node/pnpm 固定版本后在依赖变更 PR 中重新生成。

报 issue 请给脱敏后的命令、版本、OS 和最小复现;不要上传 token、真实 .dsh 或会话内容

贡献与安全

开发流程与验证命令见 CONTRIBUTING.md。漏洞请走 SECURITY.md 的私密渠道,不要开公开 issue。

MIT License