☰
gsd-core `detect-custom-files` 修复:`/gsd-update` 不再静默销毁用户自定义 Skills
2026/9/28 2:53:36 网站建设 项目流程

【免费下载链接】gsd-core

Git. Ship. Done - Core

项目地址:https://gitcode.com/gh_mirrors/ge/gsd-core
点击查看免费下载

导读

本篇技术文章围绕 gsd-core 仓库中的变更集.changeset/archived/wise-rams-gather.md(对应 PR #3318 / issue #3317)展开,深入解析一次影响数据安全的缺陷修复:SDK 移植版本中的detect-custom-files工具遗漏了GSD_MANAGED_DIRS中的skills/目录,导致用户在<config-dir>/skills/<name>/下自定义的技能文件在/gsd-update更新时被静默删除,且不会进入gsd-user-files-backup/备份。读完本文,你将完整理解 gsd-core 的用户自定义文件检测、备份与恢复链路,掌握该修复的实现原理、测试覆盖与边界防护,并能在自己的部署中验证该行为。

一、变更集原文与问题本质

变更集记录如下:

detect-custom-filesnow scansskills/— SDK port omittedskillsfromGSD_MANAGED_DIRS, so user-added skills under<config-dir>/skills/<name>/were never detected and got silently destroyed during/gsd-update(no entry written togsd-user-files-backup/). One-line parity withbin/gsd-tools.cjs. (#3317)

它揭示了两个关键事实:

  1. 缺陷成因:SDK 移植(SDK port)在维护受管目录清单GSD_MANAGED_DIRS时遗漏了skills,使得detect-custom-files根本不会遍历skills/目录;
  2. 后果:/gsd-update的"干净安装"(clean-install)阶段会整体擦除受管目录,未被检测到的用户自定义技能会随目录被静默销毁,且不产生任何备份记录。

这一行修复的本质是"对齐":让 SDK 移植版本的受管目录清单与bin/gsd-tools.cjs(及其安装树中的gsd-core/bin/gsd-tools.cjs)保持一致。

二、背景:更新流程如何保护用户自定义文件

要理解这个 bug 的严重性,先要理解 gsd-core 更新链路中"受管目录 / 清单 / 检测 / 备份"四个环节的协作关系。

2.1 受管目录与文件清单

gsd-core 安装器(bin/install.js)在安装完成后会写入gsd-file-manifest.json(见writeManifest,bin/install.js)。该清单以相对路径为键、以文件内容的 SHA256 哈希为值,记录"这次安装由 GSD 官方交付了哪些文件"(generateManifest)。

与此同时,以下目录被定义为 GSD 受管目录,更新时会被整体擦除并重写:

  • 整目录接管(whole-managed):gsd-core/、commands/gsd/—— 递归扫描其中所有文件;
  • 前缀接管(prefix-managed):agents/、共享 hooks 目录(默认hooks/,pi 等运行时为gsd-hooks/)、skills/—— 只扫描以gsd-开头的顶层条目。

2.2detect-custom-files的职责

detect-custom-files是gsd-tools的一个子命令,位于 gsd-core/bin/gsd-tools.cjs(安装树内的实际实现)与 bin/gsd-tools.cjs(仓库根目录的 SDK 源)。它的判定规则很简单:磁盘上存在、但不在gsd-file-manifest.json中的文件,即用户自行添加、安装器不了解的文件——这些文件会在下一次干净安装时被删除,因此必须先被发现并备份。

2.3 更新工作流中的调用点

/gsd-update工作流(gsd-core/workflows/update.md)在backup_custom_files步骤中调用该子命令(gsd-core/workflows/update.md):

CUSTOM_JSON='' if [ -f "$GSD_TOOLS" ] && [ -n "$RUNTIME_DIR" ]; then CUSTOM_JSON=$(node "$GSD_TOOLS" detect-custom-files --config-dir "$RUNTIME_DIR" 2>/dev/null) fi if [ -z "$CUSTOM_JSON" ]; then CUSTOM_JSON='{"custom_files":[],"custom_count":0}' fi
  • RUNTIME_DIR是解析出的配置目录(例如~/.config/opencode、~/.gemini/antigravity等运行时各自的配置根);
  • 检测结果以 JSON 输出,随后被解析出custom_count;
  • 当CUSTOM_COUNT > 0时,每个自定义文件被复制到$RUNTIME_DIR/gsd-user-files-backup/(gsd-core/workflows/update.md),并提示用户:"Found N custom file(s) inside GSD-managed directories. These have been backed up to gsd-user-files-backup/ before the update. You'll be offered a restore once the new version is installed."

工作流文档还特别强调了一个实践教训(bug #1997):不要用 bash 路径截取(${filepath#$RUNTIME_DIR/})或内联node -e require()来解析相对路径,因为当$RUNTIME_DIR未设置时这些写法会静默失败,造成CUSTOM_COUNT=0的假阴性——这正是本变更集所修复 bug 的同类风险:检测盲区等于静默数据丢失。

三、缺陷分析:为什么漏掉skills/会导致静默销毁

在修复之前,SDK 移植版本的受管目录清单缺少skills。后果链如下:

  1. /gsd-update触发干净安装,安装器按受管目录递归擦除skills/下的全部内容;
  2. 由于detect-custom-files从不扫描skills/,用户添加的skills/gsd-<name>/SKILL.md不会被列为自定义文件;
  3. 因此不会写入gsd-user-files-backup/备份条目;
  4. 擦除完成后,用户技能永久丢失,且无任何恢复入口。

这是典型的"静默数据丢失"(silent data loss):错误不报错、不告警、不留备份,只有在用户下次需要该技能时才会发现。

需要说明的细节是:skills/目录本身是 gsd-core 自 v1.39.0 技能整合(skill consolidation,#2790)后成为受管根的。测试文件 tests/update-custom-backup.test.cjs 中对此有明确注释:"After v1.39.0 skill consolidation (#2790), skills/ became a GSD-managed root. GSD_MANAGED_DIRS was missing 'skills', so user-added GSD-prefixed skill directories like skills/gsd-custom-skill/SKILL.md were never walked and got wiped"(tests/update-custom-backup.test.cjs)。也就是说,这个 bug 是"目录升格为受管根"与"受管目录清单未同步更新"错位产生的回归。

四、修复实现:routeDetectCustomFiles与受管目录清单

4.1 受管目录清单的源码形态

修复后的清单位于 gsd-core/bin/gsd-tools.cjs:

// GSD-managed directories to scan for user-added files. Whole-owned // roots are wiped recursively; shared runtime roots are pruned by the // same gsd-* top-level prefix used by install.js _removeGsdEntries. const GSD_WHOLE_MANAGED_DIRS = [ 'gsd-core', path.join('commands', 'gsd'), ]; const GSD_PREFIX_MANAGED_DIRS = [ 'agents', ...resolveSharedHooksDirCandidates(resolvedConfigDir), 'skills', ];

skills被归入前缀管理(prefix-managed)一类,与agents/、hooks 目录同级,这与安装器_removeGsdEntries使用相同的gsd-*顶层前缀剪枝策略(见源码注释)。

4.2 前缀选择性扫描

对应的扫描逻辑(gsd-core/bin/gsd-tools.cjs):

for (const managedDir of GSD_PREFIX_MANAGED_DIRS) { const absDir = path.join(resolvedConfigDir, managedDir); if (!fs.existsSync(absDir)) continue; for (const entry of fs.readdirSync(absDir, { withFileTypes: true })) { if (!entry.name.startsWith('gsd-')) continue; collectCustomFiles(path.join(absDir, entry.name), resolvedConfigDir, manifestKeys, customFiles); } }

这带来一个重要的语义细节:不是skills/下所有文件都算自定义。

  • skills/gsd-planner/、skills/gsd-...(GSD 官方技能,以gsd-前缀开头且已登记在 manifest 中)——不会出现在custom_files中;
  • skills/gsd-my-custom-skill/SKILL.md(用户新增、不在 manifest 中)——会被检测并备份;
  • skills/gstack-one/等非gsd-前缀的技能——不会被扫描,因为安装器也不会删除它们(shared skills 由安装器保留,见测试注释 #1325)。

这种"前缀选择性"是修复正确性的核心:detect-custom-files必须与安装器的删除行为精确对偶——只备份那些即将被擦除的文件,既不漏报(数据丢失)也不误报(把官方技能当作自定义文件造成永久误报,这一点在 bin/install.js 附近有专门注释提及)。

4.3 输出契约

命令成功时输出(gsd-core/bin/gsd-tools.cjs):

{ "custom_files": ["skills/gsd-my-custom-skill/SKILL.md", "..."], "custom_count": 1, "manifest_found": true, "manifest_version": "1.40.0" }

边界行为:

  • --config-dir缺失或目录不存在:报用法错误并退出非零;
  • 无gsd-file-manifest.json:返回{ custom_files: [], custom_count: 0, manifest_found: false }——与 install.jssaveLocalPatches在无清单时的行为一致;
  • manifest 解析失败:返回manifest_found: false并带error: 'manifest parse error'。

五、测试验证:skills/扫描的行为契约

该修复的回归测试集中在 tests/update-custom-backup.test.cjs,与主题直接相关的用例包括:

测试验证点
scans skills/ directory and detects custom gsd-prefixed skills not in manifest (#2942, #1325)(tests/update-custom-backup.test.cjs)skills/gsd-my-custom-skill/SKILL.md被列为自定义文件;manifest 中已有的skills/gsd-planner/SKILL.md不被误报
does not report non-gsd shared skills, hooks, or prior backups (#1325)(tests/update-custom-backup.test.cjs)非gsd-前缀技能、既有备份目录gsd-user-files-backup/skills/均不参与检测
折叠自 bug-2942 的skills/ directory missing from GSD_MANAGED_DIRS系列(tests/update-custom-backup.test.cjs)检测自定义gsd-前缀技能、不检测共享技能、custom_count与custom_files.length一致、manifest_found语义
agents/ and skills/ scanning is unaffected by the hooks-dir resolution change(tests/update-custom-backup.test.cjs)#3023 的 hooks 目录名解析改造不影响agents/与skills/扫描

测试文件头部注释(tests/update-custom-backup.test.cjs)说明该测试面向detect-custom-files子命令的更新工作流备份检测(#1997),因此这批测试同时覆盖了备份、恢复(restore-custom-files,#1854)、兼容性扫描与对抗性输入等更广的契约。

六、纵深:修复背后的完整保护链路

本变更集只修了"检测"一环,但围绕它的完整机制值得一并理解,因为它们共同决定了"用户文件在更新中不丢失":

6.1 恢复:restore-custom-files

检测与备份只是第一步,恢复由 gsd-core/bin/gsd-tools.cjs 中的restore-custom-files完成(对应 issue #1854)。它支持两种模式:

  • plan(默认):遍历gsd-user-files-backup/,执行兼容性检查,不写任何文件;
  • --apply:在 plan 的基础上把合格条目复制回原位置。

关键不变量(源码注释明确列出):备份永不删除、已交付文件不被覆盖、已存在的不同文件不被覆盖、单个失败不中断整体、绝不写出配置目录之外。

6.2 兼容性扫描

恢复前会对每个备份文件执行"对新版本的兼容性预检"(scanRestoreCompatibility,gsd-core/bin/gsd-tools.cjs),因为自定义技能最常见的两种 GSD 引用在版本升级中可能改名:

  • @gsd-core/workflows/foo.md这类对交付文件的引用(正则gsd-core/...\.(md|cjs|js|json|sh));
  • /gsd:plan-phase这类 slash 命令引用。

两者缺失都会产生 advisory 警告(不阻断恢复,仅随报告输出)。此外,SKILL.md、agents/、commands/属于 frontmatter 驱动的表面,缺少name/description字段的备份文件会被标记为"恢复后不可用"。扫描文件上限为 1 MiB(RESTORE_SCAN_MAX_BYTES),超限文件仍可恢复、只是跳过内容扫描。

6.3 安全防护

恢复路径对符号链接采取比常规assertWithinRoot更严格的策略:collectBackupEntries拒绝遍历符号链接,isSymlinkPath/hasSymlinkedAncestor双重检查确保写入不会穿透链接落到配置目录之外(gsd-core/bin/gsd-tools.cjs)。测试中甚至覆盖了skills/gsd-$(touch pwned);&&id``/SKILL.md这类恶意路径注入与ATTACKER CONTENT` 覆盖攻击场景(tests/update-custom-backup.test.cjs)。

6.4 hooks 目录名的动态解析

清单中 hooks 目录不是硬编码的:resolveSharedHooksDirCandidates(gsd-core/bin/gsd-tools.cjs)会读取安装器写入的<configDir>/gsd-core/.gsd-runtime标记(#2297),再查已交付的 capability registry(./lib/capability-registry.cjs)中该运行时的hostBehaviors.sharedHooksDirName(默认hooks,pi 运行时为gsd-hooks)。这是 #3023 对抗性评审发现的同类 bug 修复:硬编码hooks会让重命名后的共享 hooks 树对检测完全不可见。当运行时无法判定时,采用"返回所有已知候选名"的非对称降级——多扫是安全的(不存在的目录会被跳过、manifest 中的文件不会被误报),少扫才是真正的 bug。

七、如何在本地复现与验证

detect-custom-files是独立 CLI 子命令,无需触发完整更新即可验证。以仓库内的安装树实现为例:

# 1. 构造一个最小配置目录:包含 manifest 与一个"用户自定义"技能 mkdir -p /tmp/gsd-demo/skills/gsd-my-custom-skill printf '# Demo Skill\n' > /tmp/gsd-demo/skills/gsd-my-custom-skill/SKILL.md printf '{"files": {"skills/gsd-planner/SKILL.md": "abc"}}' > /tmp/gsd-demo/gsd-file-manifest.json # 2. 运行检测(使用仓库内实现) node gsd-core/bin/gsd-tools.cjs detect-custom-files --config-dir /tmp/gsd-demo

预期输出应包含"custom_files": ["skills/gsd-my-custom-skill/SKILL.md"]且custom_count: 1,同时不会误报 manifest 中已有的skills/gsd-planner/SKILL.md。若将自定义技能改为非gsd-前缀目录(如skills/gstack-one/),则不会被报告——因为安装器同样不会删除它。

若需走完整链路,可参考/gsd-update工作流的backup_custom_files步骤实现(gsd-core/workflows/update.md)与配套的restore-custom-files恢复步骤。

八、小结

wise-rams-gather这一行变更集修复了一个典型的"清单漂移"类回归:当skills/升格为受管根后,SDK 移植版本的GSD_MANAGED_DIRS未能同步,导致/gsd-update静默销毁用户自定义技能且不留备份。修复通过将skills纳入前缀管理的受管目录扫描清单,使detect-custom-files与安装器的删除行为精确对偶——既覆盖了用户新增的gsd-*技能,又不会误报官方技能或非gsd-前缀的共享技能。配合gsd-user-files-backup/备份、restore-custom-files恢复、兼容性预检与符号链接防护,gsd-core 在版本升级场景下对用户自定义文件的保护形成了"检测—备份—恢复—校验"的完整闭环。

【免费下载链接】gsd-core

Git. Ship. Done - Core

项目地址:https://gitcode.com/gh_mirrors/ge/gsd-core
点击查看免费下载

相关推荐

上一篇:分布式多级缓存框架layering-cache:为高并发场景设计的监控友好型解决方案
下一篇:轻量级对话界面:Ollama Web UI Lite 技术概述

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询