【免费下载链接】gsd-core
Git. Ship. Done - Core
导读
本篇技术指南围绕 gsd-core 一次真实的健壮性修复展开:config-set <key>(只传键、不传值)曾以{ updated: true }加退出码 0 的方式"成功",却因为undefined值被JSON.stringify静默丢弃而悄悄破坏.planning/config.json。文章将以该修复为主线,讲解 gsd-core 配置子系统(config-set/config-get)的完整参数校验链路、ERROR_REASON.USAGE类型化错误机制,以及驱动这次修复的 CLI 对抗性输入矩阵(negative matrix)测试工具的设计。读完你将掌握 gsd-core 命令行参数校验的防守思路、类型化错误断言方法,以及如何用无 shell 的 spawnSync 测试工具系统化地验证 CLI 的异常输入安全。
一、问题背景:一次"成功"的失败
修复记录见仓库归档的 changeset 文档 .changeset/archived/3593-cli-negative-matrix-harness.md,其描述非常直白:
修复前,执行
config-set model_profile(只有 key、没有 value)会返回{ updated: true }且退出码为 0——但 value 会以undefined传入,随后被JSON.stringify在写盘时静默丢弃。这既不会报错,也不会留下任何可追踪的失败痕迹,属于典型的"静默配置损坏"(silent config corruption)。
问题链条有三环:
- 参数缺口不设防:
config-set只校验了 key 的存在性,未校验 value 是否存在; - 值传递为
undefined:缺值调用时 value 参数为undefined,而解析分支(布尔/数字/JSON)全部落空,原值原样进入写盘流程; - JSON 序列化静默丢弃:
JSON.stringify对undefined属性会直接跳过,导致"设置成功"但键实际上被删掉或从未写入。
用一句话概括:CLI 报告成功、磁盘悄然变化、调用方毫无察觉——这正是对抗性输入测试要消灭的失败模式。
二、修复落地:写盘之前的双重 Usage 守卫
修复在配置命令的核心实现 src/config.cts 的cmdConfigSet中完成。该函数在解析与写盘之前先做两道参数守卫:
function cmdConfigSet(cwd: string, keyPath: string | undefined, value: string | undefined, raw: boolean, options: ConfigSetOptions = {}): void { const dryRun = options.dryRun === true; if (!keyPath) { error('Usage: config-set <key.path> <value>', ERROR_REASON.USAGE); } // #3593: reject the "key without value" form (e.g. `config-set // model_profile` with args[2] === undefined). Without this guard the // value passes through as undefined, the number/boolean/json branches // all fall through, and the write either silently strips the key // (JSON.stringify drops undefined values) or writes a corrupt entry. // Typed reason so the negative-matrix test can assert on it instead // of greppinng prose. if (value === undefined) { error('Usage: config-set <key.path> <value>', ERROR_REASON.USAGE); } // ... }要点有三:
- 两道守卫缺一不可:第一道拦截"完全没传 key"(
config-set裸调用),第二道拦截"只传 key 没传 value"(config-set model_profile)——后者正是本次修复的靶点; - 发生在任何写操作之前:守卫在
loadConfigJson、withPlanningLock、platformWriteSync之前执行,确保失败路径不触碰磁盘; - 携带类型化原因码:
error(..., ERROR_REASON.USAGE)不只是打印文案,还会附带结构化的错误原因,让测试可以断言result.reason === 'usage'而非靠正则匹配散文文本(源码注释原文:"Typed reason so the negative-matrix test can assert on it instead of greppinng prose")。
类型化错误机制:ERROR_REASON 冻结枚举
ERROR_REASON.USAGE来自 I/O 原语模块 src/io.cts。该模块以Object.freeze定义了一组错误原因枚举,其中USAGE: 'usage'与CONFIG_INVALID_KEY、CONFIG_KEY_NOT_FOUND、CONFIG_NO_FILE、CONFIG_PARSE_FAILED等一起构成 CLI 可编程化的失败契约。配合--json-errors模式,error()会在 stderr 输出单行 JSON 载荷(形如{ ok: false, reason: 'usage', message: ... }),随后抛出ExitError以非零码退出(ADR-3889 起error()由直接process.exit改为抛错,便于在进程内测试中捕获断言)。
从源码结构看,这一机制的价值在于:错误从"给人看的话"升级为"给程序判的码",测试与上层 Agent 都能用reason精确区分失败类别,而不是解析文本。
三、驱动修复的测试基建:CLI 负向输入矩阵(#3593)
这次修复并非偶然发现,而是由新引入的CLI adversarial-input matrix(对抗性输入矩阵,#3593)系统化地"逼"出来的。测试工具位于 tests/helpers/cli-negative.cjs,其设计值得单独拆解。
3.1 无 shell 的进程调用
工具核心是spawnSync包装gsd-core/bin/gsd-tools.cjs,关键约束是:敌对值一律作为 argv 元素传递,绝不拼进 shell 字符串(源码注释:"Hostile values are passed as argv elements — never composed into a shell string")。这样测试输入里的;、&&、$()、反引号、引号、换行、空字节等元字符到达 CLI 时只是不透明的数据,而非 shell 语法——从源头杜绝了"测试工具自身引入假阳性注入"的可能。
3.2 类型化中间表示(IR)
parseSpawnResult把原始spawnSync结果整理成结构化 IR:
status/signal:退出码与终止信号;ok/reason/message:从--json-errors的 stderr JSON 载荷解析而来;hasStackTrace:用正则/\n\s{2,}at\s+/检测 stderr 是否泄漏了 V8 栈帧——只要出现at帧行,就说明 CLI 在未包裹的代码路径上抛了异常,测试即可据此失败;jsonErrorsRequested:标记本次是否走了 JSON 错误模式,供测试区分"缺 reason 是缺陷"还是"故意关闭 JSON 模式"。
工具文档明确写道:"The harness does NOT decide what the test asserts — it just shapes the data so the assertion is mechanical and prose-free",即测试只断言reason码与退出状态,永不断言散文文本。
3.3 针对 config-set 缺值的回归测试
该矩阵针对config命令族共枚举了 12 类对抗输入(源自 CONTRIBUTING.md 的 "QA Matrix Requirements / CLI and command routing"),并在 tests/config-get-default.test.cjs 中以折叠区块(folded fromfeat-3593-cli-negative-config.test.cjs)的形式落地。本次修复的核心回归测试如下:
test('config-set with key but no value fails with a typed reason', (t) => { const projectDir = createTempProject('cli-neg-config-3-'); t.after(() => cleanup(projectDir)); const result = runCli(['config-set', 'model_profile'], { cwd: projectDir }); assertSafeFailure(result, 'config-set missing value'); });assertSafeFailure是一组普适不变量:
status非 0(必须失败);signal为 null(必须干净退出,而非被信号杀死);hasStackTrace为 false(不得泄漏 V8 栈帧);- JSON 载荷
ok === false,且reason是非空字符串(类型化原因,可精确断言)。
矩阵中的其他用例还覆盖了:空串/纯空白 key、重复--cwd、未知全局 flag、未知子命令、以--开头的值(应作为值而非 flag 处理)等,并配合snapshotInventory快照断言——失败的读操作不得改动文件系统。这些共同把"config 命令族在任何畸形输入下都必须安全失败"固化成了可回归的契约。
四、纵深:config-set 的完整入参校验体系
cmdConfigSet远不止两道 Usage 守卫。从 src/config.cts 的实现看,它在解析阶段之后、写盘之前还串联了一整套按 key 分派的类型化校验,值得完整列出:
值解析规则
'true'/'false'解析为布尔;'null'解析为 null(且语义化为"清除该键",见下);- 数字走
Number.isFinite(Number(val))而非!isNaN——'Infinity'/'-Infinity'不再被强制转成非有限数(JSON.stringify会把非有限数渲染成null,造成磁盘与回显不一致); - 以
[或{开头的字符串尝试JSON.parse为数组/对象,失败则保留为字符串。
清除语义(#2046)
- 显式传入
null等价于"清除":调用unsetConfigValue删除该键(而非持久化 JSON null),并支持--dry-run预览。源码注释指出:持久化的 null 仍是"存在且接近 truthy"的值,消费者必须特判,对 secret 键尤其危险——遗留值可能被当作真实凭据使用。
按 key 的强类型校验(摘录)
| Key 类别 | 校验规则 |
|---|---|
context/phase_id_convention/workflow.drift_action/workflow.human_verify_mode/workflow.context_guard_mode等 | 枚举白名单,assertEnumValue要求typeof parsedValue === 'string'且属于合法集合(堵住String(["val"]) === "val"的数组强转旁路) |
context_window/workflow.smart_zone_tokens/workflow.drift_threshold | 有限正整数;smart_zone_tokens额外要求Number.isSafeInteger(读侧只接受安全整数,接受与执行必须一致) |
workflow.post_planning_gaps/workflow.compact_content/workflow.agent_hint_routing/planner.stall_detection_enabled/git.create_tag/statusline.*等 | 严格布尔,字符串"false"不能绕过(#4570:只有真布尔可改变默认开启的策略) |
git.protected_branches | 非空字符串数组,且逐元素校验(用索引遍历而非.every(),避免稀疏数组空洞被跳过) |
hooks.context_warning_threshold/hooks.context_critical_threshold | 0–100 的百分比;且拒绝"警告=0"与"临界=100"这类配对后永不可用的端点值 |
ship.pr_body_sections | 段对象的字段白名单、source选择器正则、模板 token 白名单 |
review.default_reviewers/review.reviewer_instances.<name>.<field> | 实例名正则、内建 slug 冲突、cli字段必须为已知适配器 |
| 能力注册表键(#1628) | 按getCapabilityConfigSchema(cwd)声明的 type(enum/boolean/number/string)动态校验 |
写路径的通用防御
_setNestedValue/_unsetNestedValue对每个路径段(含中间段)做内联字面量比较,拦截__proto__/prototype/constructor,防原型污染(CodeQLjs/prototype-pollution-utility查询能识别的屏障形态);- secret 键(
isSecretKey)在 CLI 输出前一律maskSecret掩码——明文只存在于磁盘config.json,stdout/stderr 永不回显(见 src/secrets.cts 的SECRET_CONFIG_KEYS与相关测试中对brave_search掩码的断言); - 写入统一走
withPlanningLock加锁 + 单次platformWriteSync原子落盘;批写setConfigValues在单次锁内完成多键写入。
此外config-get(读侧)也承担了对称职责:config-get <key> [--default <value>]对缺失键依次回退 root 配置继承(#2702)、--default标志、schema 级默认值(SCHEMA_DEFAULTS与能力注册表 configSchema,见 src/config-loader.cts 的CONFIG_DEFAULTS清单),且遍历全程用hasOwnProperty门控防止原型链解析——相关断言见 tests/config-get-default.test.cjs 中折叠的 #2256 区块与原型遍历属性测试。
五、实操验证:如何在本仓库复现修复前后行为
以下操作均在当前仓库内可执行(仓库为只读,请勿修改任何文件):
1. 查看修复源码与守卫注释
- 修复主体:src/config.cts 中
cmdConfigSet的#3593注释与两道 Usage 守卫(约 824–838 行); - 类型化错误枚举:src/io.cts 中
ERROR_REASON(USAGE: 'usage'等)与error()的 JSON 模式。
2. 阅读并理解负向矩阵工具
- 工具实现:tests/helpers/cli-negative.cjs;
- 回归测试(
config-set with key but no value等 12 类用例):tests/config-get-default.test.cjs 中folded:feat-3593-cli-negative-config区块。
3. 运行相关测试验证契约
node --test tests/config-get-default.test.cjs该文件同时覆盖config-get --default(#1893)、能力注册表 schema 默认值(#2256)、context_window合法键(#2798)与 schema 默认返回(#2943),是理解 config 命令族契约的最短路径。
4. 手动观察当前行为(修复后)
在一个临时项目目录中(例如mkdir /tmp/gsd-demo && cd /tmp/gsd-demo),执行:
node /data/web/disk1/git_repo/gh_mirrors/ge/gsd-core/gsd-core/bin/gsd-tools.cjs config-set model_profile --json-errors修复后的预期结果:非零退出,stderr 输出单行 JSON(ok: false,reason: "usage"),不产生任何栈帧;且目录中不会出现被部分写入的config.json——这正是"失败在写盘之前"的直观体现。
六、结语:从一次 bug 到一类防御
回看 #3593 的完整闭环,可以看到 gsd-core 的一条质量主线:
- 问题暴露:CLI 负向矩阵以系统化对抗输入(缺参、空串、元字符、重复 flag、未知子命令等 12 类)代替随机手测;
- 缺陷定位:
config-set缺值调用落入JSON.stringify静默丢弃undefined的陷阱; - 修复加固:写盘前增设类型化 Usage 守卫,让失败可编程化、可断言;
- 回归锁定:用
reason === 'usage'+ 无栈帧 + 零文件突变的三重断言,把"必须安全失败"固化为永久契约。
对任何命令行工具的维护者而言,这条链路的可迁移价值在于:不要信任参数个数,不要在写盘时依赖序列化器兜底,永远给失败一个类型化原因。gsd-core 的 config 命令族还提供了完整的枚举/布尔/数字/原型污染/密钥掩码校验体系,可以作为配置类 CLI 入参治理的参照实现。
关联资源
- 修复记录:.changeset/archived/3593-cli-negative-matrix-harness.md
- 核心实现:src/config.cts、src/io.cts、src/config-loader.cts
- 测试基建与用例:tests/helpers/cli-negative.cjs、tests/config-get-default.test.cjs
- 交互式配置命令文档:commands/gsd/config.md、docs/CONFIGURATION.md
【免费下载链接】gsd-core
Git. Ship. Done - Core
相关推荐
gsd-core 配置写入链路修复剖析:config-set 如何解锁 model_overrides.<agent-id> 动态键
gsd core 配置写入链路修复剖析:config set 如何解锁 model_overrides.<agent id 动态键 本文围绕 gsd core(
gsd-core 代码审查自动修复调度修复:`/gsd-code-review --fix` 标志从静默丢弃到完整链路
gsd core 代码审查自动修复调度修复: /gsd code review fix 标志从静默丢弃到完整链路 本篇技术文章以 gsd core 仓库中的 c
get-shit-done 缺陷修复实录:3593 让 `config-set <key>` 缺值调用在写入前干净失败
get shit done 缺陷修复实录: 3593 让 config set <key 缺值调用在写入前干净失败 导读 .planning/config.js
人工智能AI 应用提示工程开发工具工作流自动化AI Agent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考