diff --git a/docs/00-ai-start-here.md b/docs/00-ai-start-here.md index 275f0ce..d651b40 100644 --- a/docs/00-ai-start-here.md +++ b/docs/00-ai-start-here.md @@ -47,7 +47,7 @@ SoftBox 软件盒子是一个使用 Go + Gio 开发的 Windows 桌面客户端, ## 当前阶段 -当前项目已完成 Phase 0~2、T-301 与审核整改 `T-604`~`T-612`。Windows 安全路径阻断项、图标缓存资源边界、后台结果回 UI 线程的事件接线、双适配器交互契约、`VisibleItems` 快照生命周期、双端 Gio shell 职责拆分、unsafe cache 安全诊断/runbook 以及 ZIP 中央目录/EOCD(含 ZIP64)预扫描均已关闭;下一步按 Phase 1 审核顺序落成断电耐久性整改,T-302 暂后置。 +当前项目已完成 Phase 0~2、T-301 与审核整改 `T-604`~`T-612`。Windows 安全路径阻断项、图标缓存资源边界、后台结果回 UI 线程的事件接线、双适配器交互契约、`VisibleItems` 快照生命周期、双端 Gio shell 职责拆分、unsafe cache 安全诊断/runbook 以及 ZIP 中央目录/EOCD(含 ZIP64)预扫描均已关闭;`T-613` 已落成为安装文件、目录与 journal 耐久顺序任务并是当前唯一待领取项,T-302 暂后置。 优先路径: @@ -55,7 +55,7 @@ SoftBox 软件盒子是一个使用 Go + Gio 开发的 Windows 桌面客户端, 2. 已完成 Phase 1:清单验签、ZIP 安全解压、原子切换回滚原型。 3. 已完成 Phase 2 与 T-301:清单/列表/详情/图标缓存 + 可恢复下载队列。 4. 已完成 T-604:modern/Win7 workspace 与 Gio 版本解析彻底隔离。 -5. 已完成 T-606~T-612:图标缓存资源边界、UI 线程事件接线、双 Gio 适配器交互契约、`VisibleItems` generation 生命周期、双端 `shell.go` 同 package 镜像职责拆分、unsafe cache 诊断/人工恢复指引和 ZIP 中央目录/EOCD 预扫描;下一步串行处理 Phase 1 断电耐久性整改,再继续 T-302/T-303 与 Phase 4-6。 +5. 已完成 T-606~T-612:图标缓存资源边界、UI 线程事件接线、双 Gio 适配器交互契约、`VisibleItems` generation 生命周期、双端 `shell.go` 同 package 镜像职责拆分、unsafe cache 诊断/人工恢复指引和 ZIP 中央目录/EOCD 预扫描;下一步执行 T-613 的 Phase 1 文件/目录/journal 耐久整改,再继续 T-302/T-303 与 Phase 4-6。 ## 领取任务规则 diff --git a/docs/06-tasks.md b/docs/06-tasks.md index c188fb7..8d8f8a7 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -42,12 +42,13 @@ #### Phase 1 交叉审核加固 -Phase 1 安全整改按 `docs/review/phase1-security-review.md` 的交叉复核定稿顺序串行落成。T-605 与 T-612 已关闭;后续断电耐久和签名向量任务在前一整改完成并提交后再正式编号,T-302 继续后置。 +Phase 1 安全整改按 `docs/review/phase1-security-review.md` 的交叉复核定稿顺序串行落成。T-605 与 T-612 已关闭;T-613 处理文件、目录和 journal 的断电耐久顺序。签名向量任务在前一整改完成并提交后再正式编号,T-302 继续后置。 | ID | 任务 | 依赖 | 验收要点 | | --- | --- | --- | --- | | T-605 | 统一 Windows 安全路径校验并封堵 ZIP 逃逸 | T-102, T-201, T-202, T-604 | Catalog/ZIP/installed-app 共用逐段 Windows 安全相对路径策略;拒绝尾随空格/点与 DOS 设备名;输出路径增加 destination 包含性兜底;原生 Windows 用例证明不写出 staging | | T-612 | 在 ZIP 打开前限制包大小与中央目录元数据 | T-605 | 已验签 Catalog size、已完成普通下载文件长度与同句柄 EOCD/ZIP64 预扫描一致;在 `zip.NewReader` 前限制原始包、中央目录与声明条目数 | +| T-613 | 建立安装文件与目录事务耐久顺序 | T-612 | payload Sync、staging tree/journal/rename/remove 的目录栅栏;Windows `FlushFileBuffers` fail-closed;物理断电故障注入仍后置到 T-302/T-601 | ### Phase 2 · 清单与软件列表 diff --git a/docs/current-state.md b/docs/current-state.md index 8555732..61f5bd9 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -13,7 +13,7 @@ ## 当前快照 - 日期:2026-07-18 -- 阶段:Phase 2 已完成(T-201~T-204);Phase 3 的 T-301 可恢复下载队列已完成;审核整改 T-604~T-612 已完成,T-302 继续暂后置 +- 阶段:Phase 2 已完成(T-201~T-204);Phase 3 的 T-301 可恢复下载队列已完成;审核整改 T-604~T-612 已完成,T-613 已落成为唯一 TODO,T-302 继续暂后置 - 技术栈:根 Go 1.25 workspace 只纳入 core/app-modern,`app-win7/go.work` 独立纳入 core/app-win7;版本闸门证明 modern Gio v0.10.1 与 win7 Gio v0.6.0 不交叉解析 - 生产代码:core 已有 Catalog/本地状态/存储、共享 Windows 安全相对路径策略、安全 ZIP 解压/回滚原型(Extractor 在同一普通文件句柄上核对 expected Catalog size 后有界预扫 EOCD/ZIP64、中央目录与条目数)、发布稳定只读 generation 的无 IO 软件列表模型、按 key in-flight + 流式有界读取 + 32 MiB/256-key LRU 的可信图标缓存、图标 Load/Decode 事件发布用例、有界 application event relay,以及默认并发 2 的持久可恢复下载队列;modern/win7 主循环已接 relay/Invalidate,AppShell 已实现搜索/分类/视图、惰性列表、详情右栏、完整图标失败 identity 生命周期与仅 `unsafe_cache` 可见的安全 locator/人工恢复提示,并按 root/header/catalog/detail/style 同 package 镜像职责拆文件 - 测试:core 覆盖 Catalog、列表快照 generation/零复制、SemVer/12 状态、本地安装记录、Windows dot-space/设备名/Unicode 折叠路径攻击、ZIP destination 包含性与 EOCD/ZIP64 原始包/中央目录/条目数预扫描、图标并发/取消/读取边界/LRU、真实目录/symlink fail-closed 与 cache→`unsafe_cache` event、relay 背压与关闭、下载并发/暂停/取消/重试/Range/断连/恢复/事件失败与文件身份替换;两个 app 覆盖 Editor/视图/分类/行/恢复/关闭接线、500 项 viewport、AppID 控件与分类控件生命周期、详情上下文、空状态语义、UI drain 前后、图标失败身份生命周期与 `unsafe_cache` 详情语义;安装恢复矩阵保持通过 @@ -21,14 +21,14 @@ - 标准启动路径:`./init.sh` / `./init.ps1`(同步依赖、执行完整 Phase 0 闸门、打印双目标构建命令) - 标准验证路径:`bash scripts/verify_phase0.sh` / `./scripts/verify_phase0.ps1` - 版本管理:git 已初始化,main 分支,远端 origin 为 Gitea `opc/soft_quay`;harness 文档已提交 -- 当前 blocker:无;T-612 已闭合 ZIP reader 前的 Catalog size/完成文件长度/中央目录 EOCD(含 ZIP64)预扫描边界。下一步按 `docs/review/phase1-security-review.md` 最终处理顺序第 3 项落成断电耐久性整改(预计 T-613);T-302 继续后置到该阻断项关闭 +- 当前 blocker:无;T-612 已闭合 ZIP reader 前的 Catalog size/完成文件长度/中央目录 EOCD(含 ZIP64)预扫描边界。T-613 已落成,将为 payload、staging tree、journal 与目录 rename/remove 建立耐久顺序和 Windows Flush 栅栏;T-302 继续后置到该阻断项关闭 ## 当前目录要点 | 路径 | 状态 | 说明 | | --- | --- | --- | | `docs/` | 已有 | harness coding 文档集(本次初始化完成) | -| `docs/tasks/` | 已有 | Phase 0~2、T-301 与 T-604~T-612 已完成;下一项 Phase 1 断电耐久性整改尚未编号,T-302 暂后置 | +| `docs/tasks/` | 已有 | Phase 0~2、T-301 与 T-604~T-612 已完成;T-613 是当前唯一 TODO 的 Phase 1 文件/目录/journal 耐久整改,T-302 暂后置 | | `scripts/` | 已有 | harness 治理、core 边界、Go 版本检查与 Phase 0 双平台验证入口 | | `core/` | 已建 | Go 1.20 兼容;已有正式 Catalog、本地状态/存储、共享 Windows safepath、列表模型、有界并发图标缓存、图标事件/relay、可恢复下载队列与 Phase 1 安装安全原型 | | `app-modern/` | 已建 | Go 1.25.0 + Gio v0.10.1;Modern AppShell 已接入虚拟列表、详情、图标事件 drain/过期拒绝和内存 ImageOp,并拆为五类 shell 职责文件 | @@ -42,7 +42,7 @@ - 已完成:Phase 0 的 `T-001`~`T-004`;Phase 1 的 `T-101`、`T-102`、`T-103`;Phase 2 的 `T-201`~`T-204`;Phase 3 的 `T-301`;审核整改 `T-604`~`T-612`。 - 正在进行:无。 -- 下一个可领取任务:暂无已落成 TODO;下一步按 Phase 1 最终处理顺序第 3 项落成断电耐久性整改(预计 `T-613`)。 +- 下一个可领取任务:`T-613` — 为 payload、staging tree、journal 与目录 rename/remove 建立可测试的耐久顺序和 Windows Flush 栅栏。 ## 当前可运行内容 diff --git a/docs/tasks/T-613.md b/docs/tasks/T-613.md new file mode 100644 index 0000000..27d9edf --- /dev/null +++ b/docs/tasks/T-613.md @@ -0,0 +1,79 @@ +--- +id: T-613 +title: 建立安装文件与目录事务耐久顺序 +phase: 1 +deps: [T-612] +status: TODO +created: 2026-07-18 +issue: null +context_ref: 0f69fa3bb4d00e6c79938c0a7bc66f5e0faec09e +claim_branch: null +work_branch: agent/codex/T-613 +write_paths: + - docs/tasks/T-613.md + - core/installer/ + - docs/api.md + - docs/04-architecture.md + - docs/05-coding-rules.md + - docs/review/phase1-security-review.md + - docs/00-ai-start-here.md + - docs/06-tasks.md + - docs/current-state.md +--- + +## 问题 / 背景 + +T-103 已建立 `staging → current → backup` 的进程崩溃恢复状态机,但它只证明了关键步骤之间的进程退出可恢复,没有建立真实断电时的数据与目录项耐久顺序。当前 Extractor 在 `io.Copy` 后直接关闭 payload 文件;`transaction.go` 虽会同步临时 journal 内容,但 journal rename、journal 删除、目录 rename 以及 `RemoveAll` 后都没有父目录持久化栅栏。发生掉电时,`committed` journal、payload 数据、`current`/`backup` 目录名和清理动作可能以不安全的相对顺序落盘。 + +根据 `docs/review/phase1-security-review.md` 最终处理顺序第 3 项,这是 T-302 的独立阻断项。Windows 不存在与 POSIX 完全等价的目录 fsync;本任务必须把可用的 Windows 目录句柄 `FlushFileBuffers` 策略落实为可测试的实现,且在无法建立所需栅栏时 fail closed。单元测试能够证明调用顺序和错误传播,不能声称替代 Windows VM/真机的物理断电、文件锁或杀毒软件故障注入。 + +## 方案 + +1. 在 `core/installer` 建立单一、可测试的内部耐久接口/栅栏,由 Extractor、transaction、Switcher、Recovery 和受控删除路径复用: + - 普通文件在写入、CRC/长度校验成功后必须 `Sync` 再关闭;Sync/Close 任一失败使本次 staging 删除并返回错误,不得报告可安装结果。 + - staging 完整提取后,按子目录到根目录的顺序同步目录树,保证已创建文件、目录和 metadata 在 Switcher 写 `prepared` journal 前已经经过目录项栅栏。 + - 写 journal 继续采用临时文件→内容 Sync→Close→rename;目标/backup rename、backup 删除和最终 journal 删除后都要同步 app root。`writeTransaction` 成功的语义必须是对应 phase 已经完成文件与目录栅栏。 + - 每次 `current ↔ backup ↔ staging` 目录 rename、rollback/recovery rename 和 `removeManagedDirectory` 完成后,先同步 app root,再运行测试钩子或写下一 phase journal。已 `committed` 并持久化前不得删除 backup;删除 backup 与 transaction 后也要完成 root 栅栏。 +2. 用 build tag 分离目录栅栏实现,不引入第三方依赖: + - 非 Windows 使用打开目录后的 `File.Sync`。 + - Windows 使用 `syscall.CreateFile` 的 `FILE_FLAG_BACKUP_SEMANTICS` 打开目录句柄并调用 `FlushFileBuffers`;只使用 Win7 已存在的 API,不让现代 API 进入导入表。目录打开、flush 或 close 失败必须返回稳定 installer 耐久错误,不得静默降级为 no-op。 + - 所有真实文件系统操作保持在 installer 内,不把平台 API 泄露给 domain/application 或 Gio。测试用可注入 fake fence 精确断言顺序和故障传播;Windows 原生测试必须验证真实目录 fence 可执行。 +3. 固化顺序与恢复语义: + 1. 每个 payload 的内容已验证、Sync、Close; + 2. staging 子目录至根目录已 Sync; + 3. `prepared` journal 以临时文件内容 Sync + root 栅栏持久化; + 4. current→backup rename + root 栅栏 → `current_backed_up` journal + root 栅栏; + 5. staging→current rename + root 栅栏 → `staging_activated` journal + root 栅栏; + 6. health 成功后 `committed` journal + root 栅栏,才清理 backup 和 transaction;health 失败/Recovery 的反向 rename 与清理也遵循同一栅栏。 +4. 新增耐久错误、单元与原生测试:覆盖 payload Sync/Close、staging tree、journal replace/remove、每个切换/rollback/recovery rename、目录清理的调用顺序;注入每一栅栏失败并断言不越过下一个 phase、现有恢复不变式仍成立、无误报成功。Windows 用例在真实目录上验证目录句柄 Flush;不支持/失败的文件系统必须显式失败。保留既有阶段崩溃、路径、ZIP、CRC 与恢复矩阵。 +5. 同步 API、架构、编码规则和审核记录,明确“耐久栅栏”相对于“物理掉电证明”的边界。T-302/T-601 仍需在目标 NTFS/Windows VM 或真机执行断电、杀毒/文件锁等故障注入;本任务不把单元测试描述为完整硬件级持久化担保。 + +## 验收要点 + +- 每个成功提取的 payload 文件在 CRC/大小校验后、返回 `ExtractResult` 前已 Sync 并正确 Close;Sync/Close 失败删除 staging,不返回成功,不留下可被 Switcher 使用的半持久 payload。 +- staging 目录树在 Extractor 成功返回前按子→父顺序经过目录栅栏;失败 fail closed。`prepared` journal 只可在该栅栏之后写入。 +- 所有 journal replace/remove、`current/backup/staging` rename 和受控目录删除都使用单一耐久路径;每次可见目录变更先有 root 栅栏,再可写后续 phase/运行 afterStep。`committed` journal 经过 root 栅栏后才允许清理 backup 和 transaction。 +- Windows 目录栅栏通过 `FILE_FLAG_BACKUP_SEMANTICS` 目录句柄 + `FlushFileBuffers` 实现且可在原生测试运行;不支持或 flush 失败返回稳定耐久错误,绝不静默跳过。非 Windows 无头测试继续可运行。 +- fake fence 覆盖成功顺序与每一类栅栏故障:payload、staging tree、journal、rename、rollback/recovery、cleanup;故障不推进 phase,既有 `Recover` 状态机仍恢复或 fail closed。 +- 文档准确区分“代码已建立并测试耐久顺序”与“尚需 T-302/T-601 在 Windows VM/真机完成物理断电、文件锁/杀毒干扰故障注入”;不再把目前单元测试写成硬件掉电证明。 +- Go 1.20.14 + `GOWORK=off` 下 `go vet ./installer`、`go test -count=10 ./installer` 通过;当前 Windows 主机上的 installer 原生测试、完整 `./scripts/verify_phase0.ps1`、`python scripts/validate_agent_context.py`、`python scripts/validate_harness_governance.py` 与提交前差异检查通过。 + +## 边界(不改什么) + +- 不实现 T-302 的下载、SHA-256、Catalog identity、app.json/files.json 校验、安装事件或生产 `cmd` 编排;不把现有 installer 原型宣称为完整用户安装链。 +- 不修改 ZIP 协议、Catalog/下载队列、路径安全、中央目录预扫描、health-check 定义、UI、Schema、Go/Gio 版本或 workspace。 +- 不引入第三方 durability 库,不使用 Win10+ API;Windows 只用 Win7 已有的 `CreateFile`/`FlushFileBuffers` 路径。 +- 不声称单元测试、`Sync` 或 `FlushFileBuffers` 覆盖电源切断、硬件缓存、网络文件系统、文件锁或杀毒软件的所有行为;这类目标环境故障注入仍须 T-302/T-601 单独验证。 +- 不修改、提交或删除用户的 `soft_quay.code-workspace`,不启动子 Agent,不在本任务外落成 T-614 或恢复 T-302。 + +## 协作约束 + +- 按仓库当前规则由单 Agent 串行执行,不启动子 Agent。 +- 本任务只允许修改 frontmatter 中的 `write_paths`;若需要新的安装协议、下载链接线、外部 VM/真机基础设施或第三方依赖,必须停止并另立任务,不得在 T-613 内扩边。 +- T-613 完成、完整验证并提交前,不落成或领取签名向量整改、T-302 或其他后续任务。 + +## 执行记录 + +- 2026-07-18:根据 `docs/review/phase1-security-review.md` 最终处理顺序第 3 项落成任务;现有全局最大任务为 T-612,因此取 T-613,依赖已完成的 T-612。 +- 2026-07-18:本地代码检索确认 Extractor 在 `io.Copy` 后仅 Close payload,`replaceFileWithBackup` 只 Sync 临时 journal 内容,Switcher/Recovery 的目录 rename 与 `removeManagedDirectory` 后均没有父目录栅栏。任务因此覆盖 payload、目录树、journal、切换/恢复 rename 与清理的统一顺序,不只修单个 `Sync()`。 +- 2026-07-18:Windows 策略限定为 Win7 已有的目录句柄 `CreateFile(FILE_FLAG_BACKUP_SEMANTICS)` + `FlushFileBuffers`,失败 fail closed;物理掉电/锁/杀毒故障注入的实测证据仍明确后置到 T-302/T-601。