oh-my-openagent:boulder.json 中 worktree_path 的类型清洗与防御式守卫方案

oh-my-openagent:boulder.json 中 worktree_path 的类型清洗与防御式守卫方案 oh-my-openagentboulder.json 中 worktree_path 的类型清洗与防御式守卫方案【免费下载链接】oh-my-openagentOmO: Drop your tokens. Ultrawork. Done.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent本篇指南围绕 oh-my-openagent 仓库中一份针对 Boulder 状态文件boulder.json的代码变更方案展开当boulder.json中worktree_path字段被手工编辑、外部工具或损坏状态写成null时readBoulderState()的无校验类型断言会让运行时值与 TypeScript 类型契约string | undefined不一致进而可能把非法值泄漏进 Atlas 空闲续跑的提示词。读完后你将掌握这份变更方案中“读取层清洗 调用层守卫 测试锁定”三层防御的完整设计并能结合仓库当前源码定位到每个改动点对应的实际实现位置。一、背景boulder.json 与 BoulderState 类型契约oh-my-openagent 用.omo/boulder.json状态文件追踪进行中的计划plan执行进度包括活动计划路径、会话列表、计划名以及可选的执行代理和 worktree 路径。该文件的核心类型定义在共享包 types.ts 中export interface BoulderState { schema_version?: 2 active_work_id?: string works?: Recordstring, BoulderWorkState active_plan: string started_at: string ended_at?: string elapsed_ms?: number status?: BoulderWorkStatus updated_at?: string session_ids: string[] session_origins?: Recordstring, direct | appended plan_name: string agent?: string worktree_path?: string // ← 本文档关注点类型契约为 string | undefined task_sessions?: Recordstring, TaskSessionState }注意worktree_path?: string类型系统承诺“要么不存在要么是字符串”null不在合法取值范围内。为什么null会混进来关键在于写入路径天然规避了它。从 createBoulderState 的实现看状态构造时对可选字段采用“键省略”策略return { schema_version: 2, active_work_id: workId, works: { [workId]: work }, active_plan: planPath, // ... ...(agent ! undefined ? { agent } : {}), ...(worktreePath ! undefined ? { worktree_path: worktreePath } : {}), }也就是说仓库自身的写入逻辑createBoulderState、addBoulderWork等只会在worktreePath为undefined时省略该键永远不会写出worktree_path: null。但boulder.json是一个落在磁盘上的普通 JSON 文件用户手工编辑、状态文件损坏或第三方工具写入都可能产生完全合法的 JSON{ active_plan: /path/to/plan.md, plan_name: test-plan, worktree_path: null }这正是变更方案要防御的输入。二、问题定位readBoulderState 的无校验类型断言读取入口readBoulderState()位于共享包 read-state.ts在omo-opencode包中经由 storage.ts 再导出。其核心流程是const content readFileSync(filePath, utf-8) const parsed JSON.parse(content) // ... 空对象/数组等顶层结构校验 ... normalizeState(parsed) const state parsed as BoulderState原始方案文档指出的问题在于JSON.parse()的产物被直接as BoulderState断言除顶层“是否为非空对象”之外不对worktree_path等具体字段做运行时校验。当文件里写的是null时state.worktree_path的运行时类型是null而类型签名声称是string | undefined。这个“类型契约与运行时事实脱节”的下游代价体现在两处计划路径解析resolveBoulderPlanPath 会用state.worktree_path?.trim()判断是否需要把计划路径重定位到 worktree 下。可选链对null恰好安全但如果字段是其他非字符串类型例如数字.trim()会直接抛TypeError。Atlas 空闲续跑注入Atlas 钩子在会话空闲时读取 boulder 状态并调用injectContinuation把worktreePath一路传入续跑提示词构造器。提示词中会拼出[Worktree: ${worktreePath}]文本见 boulder-continuation-injector.tsnull/非字符串值一旦泄漏轻则在续跑提示词里渲染出[Worktree: null]之类的噪声上下文重则引发类型误用。三、变更一在 readBoulderState 中清洗 worktree_path方案的第一层防御放在读取层。变更文档给出的 BEFORE/AFTER 对比原方案中对应src/features/boulder-state/storage.ts即当前仓库的 packages/omo-opencode/src/features/boulder-state/storage.ts如下// BEFORE (lines 29-32): if (!Array.isArray(parsed.session_ids)) { parsed.session_ids [] } return parsed as BoulderState // AFTER: if (!Array.isArray(parsed.session_ids)) { parsed.session_ids [] } if (parsed.worktree_path ! undefined typeof parsed.worktree_path ! string) { parsed.worktree_path undefined } return parsed as BoulderState设计动机引自方案文档readBoulderState把JSON.parse()的原始输出直接断言为BoulderState不校验具体字段。当boulder.json里是worktree_path: null合法 JSON来源可能是手工编辑、损坏状态或外部工具时运行时类型是null而 TypeScript 类型是string | undefined。这段清洗确保下游代码拿到的永远符合类型契约。值得注意的边界条件守卫条件是! undefined typeof ! string即“字段存在但不是字符串”时才置为undefined合法的字符串值原样保留、缺失的字段不引入新键。这与session_ids已有的“非数组则重置为空数组”的归一化手法一脉相承——当前仓库的 normalizeState 正是以同样的思路对session_ids、session_origins、task_sessions等字段做逐字段归一化的。四、变更二Atlas 空闲钩子的防御式类型守卫方案的第二层防御是“belt-and-suspenders”双保险。即便读取层已经清洗仓库其他位置的writeBoulderState调用仍可能直接产生非法状态因此在把worktree_path传给续跑函数之前再做一次零成本的typeof检查。变更文档涉及 Atlas 空闲钩子中两处injectContinuation调用点当前仓库对应实现位于 idle-event.ts空闲主路径与 idle-continuation.tsinjectContinuation/scheduleRetry定义处位置 1scheduleRetry 内的续跑注入方案文档标注原文件 lines 83-88// BEFORE: await injectContinuation({ ctx, sessionID, sessionState, options, planName: currentBoulder.plan_name, progress: currentProgress, agent: currentBoulder.agent, worktreePath: currentBoulder.worktree_path, }) // AFTER: await injectContinuation({ ctx, sessionID, sessionState, options, planName: currentBoulder.plan_name, progress: currentProgress, agent: currentBoulder.agent, worktreePath: typeof currentBoulder.worktree_path string ? currentBoulder.worktree_path : undefined, })位置 2handleAtlasSessionIdle 内的续跑注入方案文档标注原文件 lines 184-188// BEFORE: await injectContinuation({ ctx, sessionID, sessionState, options, planName: boulderState.plan_name, progress, agent: boulderState.agent, worktreePath: boulderState.worktree_path, }) // AFTER: await injectContinuation({ ctx, sessionID, sessionState, options, planName: boulderState.plan_name, progress, agent: boulderState.agent, worktreePath: typeof boulderState.worktree_path string ? boulderState.worktree_path : undefined, })设计动机引自方案文档双保险防御。即使readBoulderState已经做了清洗别处的直接writeBoulderState调用仍可能产生非法状态。typeof检查零成本能彻底杜绝null或其他非字符串值泄漏进续跑链路。从当前源码结构看这条链路的完整调用关系是handleAtlasSessionIdleidle-event.ts先经resolveActiveBoulderSession取得boulderState再做完成度、停滞、冷却、后台任务等一连串前置判断最终把boulderState.worktree_path透传给injectContinuation→injectBoulderContinuation在构造续跑提示词时决定是否追加[Worktree: ...]上下文。守卫点选在injectContinuation的入参处恰好卡在“状态值”与“提示词文本”的交界处。五、变更三与四用测试锁定两类损坏状态方案文档同时给出了两组测试用例分别锁定“字段缺失”与“字段为 null”两种场景。5.1 Atlas 钩子层测试index.test.ts在既有session.idle handler的 describe 块中新增两个用例test(should inject continuation when boulder.json has no worktree_path field, async () { // given - boulder state WITHOUT worktree_path const planPath join(TEST_DIR, test-plan.md) writeFileSync(planPath, # Plan\n- [ ] Task 1\n- [x] Task 2) const state: BoulderState { active_plan: planPath, started_at: 2026-01-02T10:00:00Z, session_ids: [MAIN_SESSION_ID], plan_name: test-plan, } writeBoulderState(TEST_DIR, state) const readState readBoulderState(TEST_DIR) expect(readState?.worktree_path).toBeUndefined() const mockInput createMockPluginInput() const hook createAtlasHook(mockInput) // when await hook.handler({ event: { type: session.idle, properties: { sessionID: MAIN_SESSION_ID }, }, }) // then - continuation injected, no worktree context in prompt expect(mockInput._promptMock).toHaveBeenCalled() const callArgs mockInput._promptMock.mock.calls[0][0] expect(callArgs.body.parts[0].text).not.toContain([Worktree:) expect(callArgs.body.parts[0].text).toContain(1 remaining) }) test(should handle boulder.json with worktree_path: null without crashing, async () { // given - manually write boulder.json with worktree_path: null (corrupted state) const planPath join(TEST_DIR, test-plan.md) writeFileSync(planPath, # Plan\n- [ ] Task 1\n- [x] Task 2) const boulderPath join(SISYPHUS_DIR, boulder.json) writeFileSync(boulderPath, JSON.stringify({ active_plan: planPath, started_at: 2026-01-02T10:00:00Z, session_ids: [MAIN_SESSION_ID], plan_name: test-plan, worktree_path: null, }, null, 2)) const mockInput createMockPluginInput() const hook createAtlasHook(mockInput) // when await hook.handler({ event: { type: session.idle, properties: { sessionID: MAIN_SESSION_ID }, }, }) // then - should inject continuation without crash, no [Worktree: null] expect(mockInput._promptMock).toHaveBeenCalled() const callArgs mockInput._promptMock.mock.calls[0][0] expect(callArgs.body.parts[0].text).not.toContain([Worktree: null]) expect(callArgs.body.parts[0].text).not.toContain([Worktree: undefined]) })这两个用例的断言设计各有侧重缺失字段用例验证正常写入路径下readBoulderState读回的worktree_path是undefined续跑提示词中不出现任何[Worktree:上下文同时计划进度1 remaining对应- [ ] Task 1一条未完成任务仍然被正确注入——证明清洗逻辑没有误伤正常流程。null 字段用例绕过writeBoulderState直接手写boulder.json注入worktree_path: null模拟损坏状态验证空闲处理器不崩溃且提示词中既不渲染[Worktree: null]也不渲染[Worktree: undefined]——即null被规范化成了“无 worktree 上下文”。测试文件在仓库中的对应位置是 packages/omo-opencode/src/hooks/atlas/index.test.ts。5.2 存储层测试storage.test.ts 增补describe(#given boulder.json with worktree_path: null, () { test(#then readBoulderState should sanitize null to undefined, () { // given const boulderPath join(TEST_DIR, .sisyphus, boulder.json) writeFileSync(boulderPath, JSON.stringify({ active_plan: /path/to/plan.md, started_at: 2026-01-02T10:00:00Z, session_ids: [session-1], plan_name: test-plan, worktree_path: null, }, null, 2)) // when const state readBoulderState(TEST_DIR) // then expect(state).not.toBeNull() expect(state!.worktree_path).toBeUndefined() }) test(#then readBoulderState should preserve valid worktree_path string, () { // given const boulderPath join(TEST_DIR, .sisyphus, boulder.json) writeFileSync(boulderPath, JSON.stringify({ active_plan: /path/to/plan.md, started_at: 2026-01-02T10:00:00Z, session_ids: [session-1], plan_name: test-plan, worktree_path: /valid/worktree/path, }, null, 2)) // when const state readBoulderState(TEST_DIR) // then expect(state?.worktree_path).toBe(/valid/worktree/path) }) })存储层测试成对出现一个证明null被清洗为undefined另一个证明合法字符串/valid/worktree/path被原样保留防止清洗逻辑写成“一刀切”地把字段直接删掉。该测试增补的落点是 packages/omo-opencode/src/features/boulder-state/storage.test.ts而共享包侧已有的读取测试可参见 packages/boulder-state/src/read-state.test.ts——其中包含了“文件不存在返回 null”“malformed JSON 返回 null”等既有边界场景新增用例与它们共同构成readBoulderState的完整测试面。六、结合当前仓库源码结构的补充说明阅读这份方案时建议对照当前仓库的实际代码组织做三点校准实现位置的演进。方案文档中的路径src/features/boulder-state/storage.ts对应当前仓库的 packages/omo-opencode/src/features/boulder-state/storage.ts该文件目前是一个再导出 shim把readBoulderState、writeBoulderState等函数统一转出自共享包oh-my-opencode/boulder-state实现位于 packages/boulder-state/src/storage/read-state.ts 与 write-state.ts。若在当前仓库落地同类改动清洗逻辑应加在共享包的readBoulderState/normalizeState中而不是 shim 里。状态目录的命名。方案文档的测试夹具使用SISYPHUS_DIR.sisyphus/boulder.json指代状态目录当前仓库测试中状态文件已落在.omo/boulder.json见 read-state.test.ts 中join(directory, .omo)的夹具写法。迁移用例时目录名需同步更新断言逻辑本身不受影响。当前实现的可对照点。当前 read-state.ts 的normalizeState已对session_ids、session_origins、task_sessions等字段做了逐字段归一化但尚未包含worktree_path的字符串类型清洗当前 idle-event.ts 透传给injectContinuation的仍是boulderState.worktree_path原始值。也就是说方案文档所描述的 AFTER 形态与当前仓库实现之间仍存在明确的落地空间这也解释了为何该方案的三层结构读取层清洗、调用层守卫、双向测试锁定值得作为一个整体来参考。七、验证方式与适用前提验证在仓库根目录下运行相关包的测试即可验证改动点。钩子层用 packages/omo-opencode/src/hooks/atlas/index.test.ts 中的session.idle handler用例覆盖端到端行为手写损坏 JSON → 触发session.idle→ 断言提示词文本存储层用readBoulderState的清洗用例覆盖“null 归 undefined、字符串保留”两条契约。适用前提本方案假设boulder.json可能被仓库自身之外的途径写入手工编辑、外部工具因此不信任磁盘上的任何字段类型所有清洗都遵循“非法即置 undefined”的宽松降级策略不做拒绝或抛错保证损坏状态下的可用性优先。限制typeof守卫只保证值要么是字符串要么是undefined不校验路径本身是否合法、是否存在——路径有效性由下游resolveBoulderPlanPathpath.ts通过existsSync回退逻辑兜底。小结这份变更方案的核心价值不在于两行代码本身而在于它演示了处理“外部可写 JSON 状态文件”的标准姿势——在读取边界把运行时值收敛回类型契约在关键消费边界再叠加零成本守卫最后用“缺失字段”和“null 字段”两组测试把契约钉死。对 oh-my-openagent 这类以计划文件 状态文件驱动长任务续跑的系统而言这类防御直接决定了损坏状态下代理是继续安静工作还是把[Worktree: null]之类的脏数据推进模型上下文。【免费下载链接】oh-my-openagentOmO: Drop your tokens. Ultrawork. Done.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考