get-shit-done 工作流名称规范化与路径穿越防护:CJS/SDK 双层校验一致性加固解析
【免费下载链接】get-shit-doneA light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code by TÂCHES.项目地址: https://gitcode.com/GitHub_Trending/getshi/get-shit-done
本文围绕 get-shit-done(TÂCHES 出品的轻量级元提示词 / 上下文工程 / 规范驱动开发系统)的一条Fixed变更集(changeset,关联 PR #3269)展开,深入剖析其中三项底层修复:workstream(工作流)名称在 CJS 与 SDK 两层的统一校验与规范化、model_profile: 'inherit'哨兵值的泄漏修复,以及 SDK 侧writeActiveWorkstream对目标目录存在性的校验。读完本文,你将掌握 workstream 名称命名的合法语法、路径穿越攻击的阻断机制、活动工作流指针的读写与自愈契约,以及这些修复在源码中的真实落地位置。
变更集定位:一次包含三处修复的稳定性改动
本仓库使用 changeset 管理发布说明,.changeset目录下每个文件描述一次面向用户的变更。本文分析的.changeset/nimble-lynx-tumble.md内容如下:
--- type: Fixed pr: 3269 ---其正文共包含三个要点,它们看似独立,实则共同指向「多 workstream 并行的健壮性与安全性」这一主题:
- Workstream 名称规范化:CJS 与 SDK 两层现在对 workstream 名称做一致校验,仅接受字母数字、连字符、下划线与点号(例如
v1.0),并在两层同时阻断通过..序列进行的路径穿越。 inherit哨兵修复:model_profile: 'inherit'哨兵不再作为字面模型 ID 泄漏到 session-runner 中。- 指针写入前校验:SDK 的
writeActiveWorkstream在写入指针前,会确认目标 workstream 目录真实存在。
背景知识:workstream 与活动指针的目录模型
要理解上述修复,需先了解 GSD 的多 workstream 目录布局。从 workstream-utils.ts 可以看到:
- 不带 workstream 时,规划目录为
.planning; - 带 workstream(
--ws <name>)时,规划目录被路由为.planning/workstreams/<name>/,不同 workstream 拥有彼此隔离的计划、阶段状态与产物; - 记录「当前活动 workstream」的指针文件位于
.planning/active-workstream(见 active-workstream-store.ts)。
由于 workstream 名称会被拼接到文件系统路径中(posix.join('.planning', 'workstreams', workstream)),名称本身是否合法就直接决定了路径是否安全——这正是本次修复把「校验」提升到路径构造之前的根本原因。仓库中的 ADR 0004 worktree/workstream seam 模块设计 对相关边界做了更完整的架构阐述。
统一的命名语法:^[a-zA-Z0-9][a-zA-Z0-9._-]*$
修复的核心产物是一套被 CJS 与 SDK 两层共享引用的名称策略模块 workstream-name-policy.ts,其中定义了规范正则:
const ACTIVE_WORKSTREAM_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;据此可整理出明确的合法性与非法性对照:
| 维度 | 规则 | 示例 |
|---|---|---|
| 首字符 | 必须以字母或数字开头 | v1.0、feature-1、2026_alpha均合法 |
| 后续字符 | 允许字母数字、.、_、- | release/1.0、my ws、v1.0/../x均非法 |
| 空值 / 空白 | 空串、纯空白、含空格 | 、`` 非法 |
| 路径穿越 | 禁止..、/、\及裸./.. | ..、../x、a/../b非法 |
实现层面分为两个互补的判定函数:
isValidActiveWorkstreamName(name):先剔除..、../前缀及任何内含..的值,再对剩余字符串执行正则测试(workstream-name-policy.ts);validateWorkstreamName(name):作为isValidActiveWorkstreamName的别名,专门面向 SDK 层调用方导出(workstream-name-policy.ts);hasInvalidPathSegment(name):从「危险面」视角再补一刀——只要包含/、\、裸.、..或点号点序列即判不安全,供 CJS 侧路径拼接逻辑复用(workstream-name-policy.ts)。
这一设计使「点号作为合法字符」与「点号点序列作为非法穿越」能同时成立:v1.0合法,而..在任何位置都被拦截。
路径穿越阻断:fail-closed 的路径构造
..序列一旦混入路径片段,配合.planning/workstreams/<name>的拼接就会产生目录逃逸(例如写出到.planning甚至仓库根目录之外)。为此,路径构造函数relPlanningPath在校验不通过时同步抛出异常,而不是静默忽略或清洗后继续:
export function relPlanningPath(workstream?: string): string { if (!workstream) return '.planning'; if (!validateWorkstreamName(workstream)) { throw new Error( `Invalid workstream name: ${JSON.stringify(workstream)}. ` + `Workstream names must match /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/ and may not contain '..'.`, ); } return posix.join('.planning', 'workstreams', workstream); }(见 workstream-utils.ts)
源码注释明确记载了两个关键决策,均属于本修复要保证的一致性前提:
- 同一接缝上 fail closed:直接调用 SDK、
planningPaths、ContextEngine的所有调用方都会在路径构造前通过共享的validateWorkstreamName策略,杜绝「SDK 层合法、CJS 层非法」或反向的路径分叉(对应问题编号 #3589)。 - 不改变环境变量来源的既有契约:来自环境变量的 workstream 仍由
planningPaths预先过滤为null(对应 #2791 的静默回退契约),因此该校验不会对 env 来源行为造成破坏性变更——这说明本次修复是「在正确的位置加锁」,而不是「改变取值优先级」。
CJS 与 SDK 双层的同源校验机制
变更集强调「CJS 与 SDK 两层一致校验」,其背后是仓库的 shared-module 同步机制。名称策略的权威实现位于 SDK 侧 TypeScript,而 CJS 运行时需要的同名策略由生成流程产出(sdk/scripts/gen-workstream-name-policy.mjs),落地为 workstream-name-policy.generated.cjs;同时,CJS 侧 active-workstream-store 也通过require('./workstream-name-policy.cjs')获得isValidActiveWorkstreamName。脚本/shared-module 的允许清单(scripts/shared-module-handsync-allowlist.json)与lint-shared-module-handsync等约束(见 lint-shared-module-handsync.cjs)确保两个副本不会漂移。
这一点在「活动 workstream 解析」上体现得最直观。CJS 运行时按CLI --ws > GSD_WORKSTREAM 环境变量 > 存储的活动指针三级优先级解析 workstream,并在取到值后统一执行validateWorkstreamName,非法即抛出统一文案的异常:
Invalid workstream name: must be alphanumeric, hyphens, underscores, or dots(见 active-workstream-store.cjs)
指针读写:写入前验证目录、读取时自愈
SDK 侧的 active-workstream-store.ts 用对称的两段逻辑保证指针文件的正确性:
写入端writeActiveWorkstream(projectDir, name)(第 35-49 行)执行三道闸门:
name为空时删除指针文件(清空当前活动状态);name未通过validateWorkstreamName时直接抛错,文案与 CJS 层保持一致;- 本次变更集新增的关键闸门:在
writeFileSync之前先existsSync(workstreamDir(projectDir, name)),若目标目录不存在则抛出Workstream directory does not exist: <name>,从根上避免「指针指向不存在的 workstream」这一悬空状态。
读取端readActiveWorkstream(projectDir)(第 17-33 行)则强调「自愈」:
- 指针内容非法(空或未通过名称校验)时,主动删除指针文件并返回
null; - 指针指向的目录已不存在(例如 workstream 被清理)时,同样删除指针文件并返回
null。
由此,「写入前置校验」与「读取后置自愈」共同保证活动指针在任何时刻要么指向一个真实存在的、命名合法的 workstream,要么不存在——不存在悬空引用,也不存在可供后续拼接利用的脏路径。
model_profile: 'inherit'哨兵不再泄漏为字面模型 ID
第三处修复位于会话执行器。在 session-runner.ts 的resolveModel中,模型解析优先级为「显式options.model> 配置model_profile> SDK 默认」;而当config.model_profile归一化为'inherit'时,函数直接return undefined:
if (config?.model_profile) { const profile = String(config.model_profile).toLowerCase(); if (profile === 'inherit') return undefined; const tier = profile === 'quality' ? 'opus' : (profile === 'budget' || profile === 'speed') ? 'haiku' : (profile === 'balanced' || profile === 'adaptive') ? 'sonnet' : null; if (!tier) return config.model_profile; return resolveRuntimeTierDefault('claude', tier)?.model; }(见 session-runner.ts)
这确保了inherit作为「继承/交给运行时默认」的哨兵语义存在,而不会被当成一个真实模型 ID 传给query()的options.model。同时需要留意其运行环境约束:配置层的 profile→Claude 模型映射仅在运行时为 Claude 时生效;对 Codex、Gemini、OpenCode 等非 Claude 运行时,resolveModel一律返回undefined,交由目标运行时回退到其默认模型(源码注释对应 #2652、#2832)。该哨兵在模型目录层也被显式保留(见 model-catalog.ts 对profile === 'inherit'的透传分支),说明修复目标是「在会话执行边界正确消费哨兵」,而非抹掉哨兵这一配置语义。
如何验证这三处修复
仓库为上述行为提供了多层次回归测试与生成正确性测试,可直接作为验证入口:
- 活动指针读写与解析:
tests/active-workstream-store.test.cjs、tests/bug-2524-sdk-query-ws-flag.test.cjs覆盖指针读取、--ws标志路由与回退行为; - 名称策略生成一致性:
tests/workstream-name-policy-generator.test.cjs与 SDK 侧的sdk/src/bug-3589-planning-paths-validation.test.ts校验命名策略及路径构造的 fail-closed 语义; - 会话执行器模型解析:可结合 session-runner.ts 与
sdk/src/query/config-query.ts中 profile 解析分支(含resolve_model_ids: 'omit'相关逻辑)构造model_profile: 'inherit'的用例进行回归。
本地运行测试的常规方式(仓库已配置 vitest,见 vitest.config.ts)为在sdk/目录执行对应测试文件,或直接查阅根目录 package.json 中声明的测试脚本。
小结
.changeset/nimble-lynx-tumble.md表面是一条精简的 bug-fix 发布说明,实际承载了三项互相咬合的正确性契约:统一的 workstream 名称文法(字母数字 /-/_/.,首字符须为字母数字)从源头定义了「什么是合法的工作流名」;双层的..穿越阻断与目录存在性校验让路径从构造到指针落盘都 fail-closed;inherit哨兵在会话边界的正确消费则消除了配置语义向运行时泄漏的最后一环。理解这些修复的最佳路径,是从 workstream-name-policy.ts 出发,顺藤摸瓜到 workstream-utils.ts、两套 active-workstream-store / SDK 版 与 session-runner.ts,再以对应测试用例收官——这条链路本身就是 GSD「规范驱动、防御式路径处理」工程理念的浓缩样例。
【免费下载链接】get-shit-doneA light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code by TÂCHES.项目地址: https://gitcode.com/GitHub_Trending/getshi/get-shit-done
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考