Files
soft_quay/docs/api.md
T

32 KiB
Raw Blame History

协议合约(Catalog / 软件包 / 许可证 / 事件 / CLI)

本项目没有自建在线 API;盒子只消费静态签名文件。本文定义所有跨边界数据结构和交互合约的目标形状。 实现前可细化,但不要在代码里另起一套不兼容结构。字段变化必须同步更新本文、04-architecture.md 和 schemas/ 下的 JSON Schema。

通用约定

  • 传输:HTTPS;所有 JSON 使用 UTF-8。
  • 时间格式:ISO 8601(UTC)。
  • 签名:Ed25519;客户端只内置公钥;示例中的 "signature": "..." 均为占位符。
  • 版本号:语义化版本(SemVer)。
  • 软件 ID:^[a-z0-9-]+$,永久稳定,发布后不得更改。
  • 软件包控制的路径统一使用 UTF-8 与 / 分隔,并由客户端共享的 Windows 安全相对路径策略校验。JSON Schema 只表达基础形状;运行时还逐段拒绝空段、./..、首尾 ASCII 空格、尾随句点、控制字符、Windows 禁止字符和 DOS 设备名及其兼容变体。

1. Catalog 清单(远端 → 盒子)

现代版与 Win7 版使用不同 channel 文件:manifest-modern.json / manifest-win7.json。更新器必须校验 channel,禁止 Win7 版下载现代版包。

{
  "schema_version": 1,
  "channel": "modern",
  "generated_at": "2026-07-16T00:00:00Z",
  "min_box_version": "1.0.0",
  "apps": [
    {
      "id": "json-parser",
      "name": "JSON解析工具",
      "description": "示例简介",
      "version": "1.2.0",
      "channel": "stable",
      "status": "active",
      "category": "开发工具",
      "tags": ["工具", "JSON"],
      "icon": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
      "homepage": "https://example.com",
      "tutorial": "https://example.com/tutorial",
      "min_os": "windows-7-sp1",
      "architectures": ["386", "amd64"],
      "entry_exe": "JsonParser.exe",
      "requires_admin": false,
      "packages": {
        "amd64": {
          "url": "https://download.example.com/json-parser-1.2.0-amd64.zip",
          "size": 12345678,
          "sha256": "...",
          "signature": "..."
        }
      }
    }
  ],
  "signature": "..."
}

行为要求:

  • 网络成功:按“HTTPS 获取 → 验签 → Schema/字段/目标 channel 校验 → 替换缓存”处理;任一步失败都拒绝新内容,继续用最后一次验证且客户端可消费的缓存。
  • 网络失败:用缓存;清单过期给提示,但保留已安装软件的启动能力。
  • 下架:显式 status: deprecated | hidden,不用名称前缀。
  • 分类:category 是单一稳定分类,tags 是搜索/多标签展示数据;两者都不得为空。
  • 过滤:先校验 manifest channel,再按 min_os、architectures 和 packages 过滤;不兼容软件可见说明但不可下载。
  • 状态:active 可安装;deprecated 可见但不可新装/更新;hidden 不进入目录结果,但本地已安装记录仍由本地状态模块保留。
  • URL:Catalog、package、homepage、tutorial 只接受无用户信息、无 fragment 的绝对 HTTPS URL。
  • 架构:MVP Schema 接受 386 / amd64;一个 app 的 architectures 必须与 packages 键一一对应。当前两个客户端发布目标仍是 amd64。
  • 系统版本:min_os v1 只允许 windows-7-sp1、windows-10、windows-11;更高系统可消费更低最低版本的软件。
  • Schema:客户端协议文件为 schemas/manifest.schema.json;标准包元数据为 schemas/app.schema.json。运行时还会执行 JSON Schema 难以表达的重复 ID、架构映射和 channel 目标一致性检查。

1.1 Catalog 签名域

客户端采用以下签名域,供发布器实现对齐:

  1. 输入必须是单个 UTF-8 JSON object;重复字段、尾随 JSON、浮点/指数数字、-0、前导零和非法 Unicode surrogate 直接拒绝。JSON 字符串中的 \u high surrogate 必须立刻与一个 low surrogate 配对;孤立/不匹配 surrogate 不得替换为 U+FFFD 后继续处理。
  2. 读取顶层 signature(标准 Base64 编码的 64 字节 Ed25519 签名),然后从对象中移除该字段。
  3. 对剩余值递归规范化:对象键按 Unicode 字符串升序排列;数组保持原顺序;字符串按 JSON 转义;数字只接受 0、正整数或负的非零整数,并保持其原始合法十进制 token(包括大于 IEEE-754 安全整数的值);不保留无意义空白。只删除顶层 signature,任何嵌套 package signature 仍属于 signed payload。
  4. Ed25519 直接签名/验证上述规范 JSON 字节。

signature 文本必须是唯一的标准 padded Base64 表示:严格解码为 64 字节后重新编码必须逐字节等于输入,因此 CR/LF、其他空白、缺失/额外 padding 都拒绝。固定跨实现 corpus 位于 testdata/catalog/canonical-vectors.json;客户端与 softbox-catalog 发布端必须读取其中的静态 canonical bytes、测试公钥和签名,不得以自身 canonicalizer 重新生成期望值。密钥 ID/轮换字段尚未定稿,在单公钥协议升级前不得另造签名域。

1.2 图标内容引用与本地缓存

Catalog icon v1 是 sha256:<64 hex> 内容引用,不是可直接请求的 URL。最终分发字段仍由发布端定稿;客户端通过注入的 IconFetcher 把引用解析为图标字节,不得在 UI 中拼接或猜测 URL。

客户端缓存合约:

  1. 请求键为 (icon digest, DPI);DPI 接受 48~768 的整数值。
  2. 加载顺序为有界 memory LRU → 磁盘 → 注入的流式 Fetcher;磁盘文件名为 <digest>-<dpi>.icon。memory 默认同时限制为 32 MiB 与 256 个 key,命中提升 LRU recency。
  3. 同一 key 的并发 miss 由一个 in-flight leader 执行 disk/fetch/validate/store,等待者复用结果;不同 key 不互相串行。等待者取消只结束自身等待,leader 失败/取消后必须释放 key 供后续重试。
  4. Fetcher 返回与请求 context 绑定的 io.ReadCloser 和可选声明长度。声明长度超过 2 MiB 默认上限时不读 body;未知或伪造长度仍只读 maxBytes+1,所有成功/失败路径都关闭 body。
  5. 远端和磁盘字节都必须复核 SHA-256,并通过图片完整解码、2 MiB 默认字节上限与 2048×2048 默认尺寸上限。
  6. 只有验证成功的远端字节可用同目录临时文件原子写入磁盘;损坏的普通缓存文件删除后可重新获取,symlink/非普通文件按不安全布局拒绝。
  7. 新进程断网时可读取再次验证成功的磁盘缓存;缓存损坏且远端不可用时返回 no valid icon available,UI 使用稳定占位图。
  8. IconEventDelivery 在调用方拥有的后台 context 中完成 IconCache.Load 与 DecodeIcon,成功发布 IconReady,失败只发布稳定分类的 IconFailed;取消直接结束且不发布迟到失败。事件身份为 request_id + app_id + icon_ref + DPI,不得把原始 URL/query 或 Gio 类型放进 payload。
  9. application event relay 是有界 FIFO,队列满时执行可取消的 lossless backpressure,不静默丢图标结果。后台 pump 成功入队后只调用并发安全的 Window.Invalidate;Gio Frame/UI goroutine 在 Layout 前 drain 并执行 ApplyEvent/ApplyIcon。
  10. shell 只接受当前 app 最新且 icon_ref/DPI 匹配的 request_id;删除 app、替换 IconRef、DPI 变化或取消请求后丢弃迟到 ready/failed。同一 AppID 更换 IconRef 时先清除旧 ImageOp,Layout 始终只复用内存 paint.ImageOp。
  11. symlink、目录和其他非普通 cache entry 必须返回 ErrIconCacheUnsafe 并立即停止;不得读取/跟随、删除、改写或重命名该 entry,不得回退 Fetcher。该错误由 delivery 映射成唯一稳定码 unsafe_cache,后台返回链仍保留 ErrIconCacheUnsafe 供调用方分类。
  12. shell 只为当前最新身份保存 IconEventIdentity + IconFailureCode;仅 unsafe_cache 在选中详情显示安全警告、稳定 code、app ID 与 <digest>-<dpi>.icon locator。不得显示绝对 cache root、原始 error、URL/query、token 或 link target;人工处理遵循 故障排查,不提供自动清理/隔离/重试操作。

当前生产 cmd 尚未装配 NewIconCache、真实 IconFetcher 或 IconEventDelivery.LoadAndPublish 调用方。以上是已由 core/双端适配器测试冻结的安全契约,不是生产网络图标链已经启用的声明。

2. 标准软件包协议 v1(ZIP)

json-parser_1.4.2_windows_amd64.zip
├─ app.json      # 身份、版本、入口、兼容性
├─ files.json    # 解压后文件清单(v1.1 强制,v1 推荐)
└─ payload/      # 实际安装到 current/ 的程序文件

2.1 app.json

{
  "schema_version": 1,
  "id": "json-parser",
  "name": "JSON解析工具",
  "vendor": "MyCompany",
  "version": "1.4.2",
  "channel": "stable",
  "min_os": "windows-7-sp1",
  "architecture": "amd64",
  "entrypoint": "JsonParser.exe",
  "working_directory": ".",
  "product_id": "product-json-parser",
  "supports_trial": true,
  "requires_admin": false,
  "data_policy": "local-app-data",
  "update_policy": "managed-by-softbox"
}

校验规则:entrypoint/working_directory 必须通过共享 Windows 安全相对路径策略(working_directory 额外允许 .),并位于 payload 内;id、version、channel、architecture 必须与 Catalog 记录一致;schema_version 高于盒子支持范围时拒绝安装并提示升级盒子(盒子始终支持当前与前一个 Schema)。

2.2 files.json

{
  "schema_version": 1,
  "files": [
    { "path": "JsonParser.exe", "size": 3456789, "sha256": "..." }
  ]
}

完整 ZIP 的 SHA-256 由签名 Catalog 保存(不写入包内部);files.json 用于解压后复核关键文件与修复功能。

2.3 安全限制(必须拒绝)

绝对路径;../ 与 Windows ".. " 等归一化穿越;首尾 ASCII 空格/尾随句点造成的路径别名;DOS 设备名;符号链接/重解析点逃出 staging;写入其他软件或盒子目录;覆盖 data/ 与 licenses/;包内自动执行脚本(install.bat/PowerShell 钩子);未验证 SHA-256/签名的包被执行;解压文件数、总体积或压缩比无上限;entrypoint 指向 payload 之外。

T-102 Phase 1 原型进一步固定:

  • ZIP 名称只接受 UTF-8 / 分隔的规范 Windows 安全相对路径;逐段拒绝反斜杠、盘符、冒号/NTFS ADS、NUL/控制字符、Windows 禁止字符、./..、首尾 ASCII 空格、尾随句点、DOS 设备名及大小写折叠后的重复输出路径。
  • 顶层只允许必需的 app.json、可选 files.json 与 payload/;只把 payload/ 内容写入全新的 staging。
  • 拒绝符号链接、设备/管道等特殊文件和加密条目。
  • T-302 的 core/application/install.InstallService 只接收已验签、严格解析并按目标过滤后的 Catalog Entry + architecture 与 .download 候选路径。它必须确认 entry/package 与 App.Packages[architecture] 精确对应;不得从 T-301 task、DownloadCompleted payload 或本地 metadata 取得 app/version/size/hash。外层 Catalog Ed25519 签名覆盖嵌套 package 的 size、sha256 与 signature 文本;当前协议未定义 package signature 的独立待签名字节/公钥域,客户端不得臆造第二套包级验签。
  • Extractor.ExtractVerifiedFile 必须接收该 Catalog package 的 size、sha256 与 app identity expectation。它以一次 Lstat → open → fstat → Lstat 取得普通 .download 文件,并在同一打开句柄上先精确核对 expectedPackageSize、再计算/常量时间比较 SHA-256;任一失败时不得构造 ZIP reader、读取 app.json 或创建 staging。T-301 的 known-total 完成文件已经以 Catalog size 限长,但 T-302 仍须执行上述重新对账,不能信任可篡改的下载 metadata。
  • 构造 zip.Reader 前只读取文件尾部至多 65,557 字节以定位 EOCD,并按需读取固定的 ZIP64 locator/EOCD 记录;校验单磁盘、中央目录 offset/size/entries 的边界及 entries/中央目录大小硬上限。当前默认上限为:原始包 4 GiB、中央目录 64 MiB、10,000 个条目、总展开 4 GiB、单条及总体压缩比 200:1。大小不一致、原始包超限、中央目录超限、声明条目超限分别保留 ErrArchiveSizeMismatch、ErrArchiveTooLarge、ErrCentralDirectoryTooLarge、ErrTooManyEntries 错误链;格式、截断、跨盘或不一致 ZIP64 归入 ErrInvalidArchive。
  • SHA-256 通过后,预扫描继续使用同一文件句柄和已核对的长度创建 zip.NewReader;完整中央目录/路径/entrypoint/类型/CRC/展开量预检仍是第二道防线。合法 ZIP64 被支持,不因 32 位 EOCD 哨兵值误拒绝。T-302 按真实包体分布复核上述暂定限额后再冻结。
  • entrypoint 使用 payload 内相对路径表示,不得自带 payload/ 前缀,且必须精确对应 ZIP 中的普通文件。
  • 所有输出路径在创建 staging 前完成规划,并在逐段名称校验后再次验证 native filepath.Join 结果仍位于 destination 内;包含性检查是纵深防御,不能替代 Windows 名称规则。
  • app.json 必须是 ZIP 根目录唯一普通文件,读取上限 1 MiB;严格 JSON 解析拒绝未知字段和尾随值,完整 v1 字段/常量必须通过运行时校验。id、version、channel、min_os、architecture、entrypoint、requires_admin 必须与可信 Catalog selection 一致;只有这一比对成功后才可创建 staging 并提取 payload。
  • T-615 将 ZIP 输入与 staging 输出错误分开:ZIP entry 的 open/read/CRC/close 失败保留为 ErrArchiveCorrupt;创建目录/文件、write、sync、close 与 staging-tree sync 失败保留 ErrStagingOutput 和底层错误链,不能误报为 ZIP 损坏。提取失败后的本次 staging 清理必须可观察;主失败和清理失败同时发生时,两者都可由 errors.Is 识别。

2.4 安装记录 installed-app.json(本地)

记录实际安装的软件 ID、版本、架构、channel、已验证的启动元数据和文件清单;与 current/、staging/、backup/ 同级存放于 apps/<id>/。

{
  "schema_version": 1,
  "id": "json-parser",
  "version": "1.4.2",
  "architecture": "amd64",
  "channel": "stable",
  "entrypoint": "JsonParser.exe",
  "working_directory": ".",
  "min_os": "windows-7-sp1",
  "requires_admin": false,
  "files": [
    {
      "path": "JsonParser.exe",
      "size": 3456789,
      "sha256": "0000000000000000000000000000000000000000000000000000000000000000"
    }
  ]
}

规则:

  • Schema 位于 schemas/installed-app.schema.json;未知字段、非法 SemVer、ID/目录不匹配、非 386|amd64 架构、非 stable channel、共享 Windows 安全相对路径以外的文件名、大小写折叠重复路径和非法 SHA-256 均拒绝。
  • files 必须是数组;v1 可以为空。T-302 从实际已验证、CRC/长度检查后写入 staging 的每个 payload 文件计算路径、size 和 SHA-256 并写入完整清单;可选 files.json 不是新的信任根,仍保留给后续修复功能。
  • T-401 新安装必须写入 entrypoint、working_directory、min_os、requires_admin,其值来自已与可信 Catalog 精确比对的 app.json。前 3 个字段沿用共享安全相对路径/已知系统版本规则(working directory 额外允许 .);entrypoint 必须也是 files 内的普通文件。为保持 v1 可读,历史记录可缺少这些可选字段,但启动用例不得猜测默认 EXE、工作目录或最低系统,必须 fail closed。
  • 写入使用 app 目录内临时文件 + installed-app.json.backup 原子替换;主文件缺失时可读取中断遗留 backup,但所有读取都重新严格校验。
  • SemVer 比较遵循 2.0.0:major/minor/patch 与 prerelease 参与 precedence,build metadata 不影响更新判断。

2.4.1 本地可见状态推导

状态事实由后台/存储层预先收集,UI Layout 不扫描磁盘。单一可见状态按以下优先级推导:

  1. 存在 install-transaction.json 或其 backup → rollback_pending。
  2. 存在活跃任务状态 → queued|downloading|verifying|extracting|installing|failed。
  3. 已检测到进程运行 → running。
  4. Catalog 判定不可兼容 → incompatible。
  5. 无 installed-app.json → not_installed。
  6. 本地 SemVer 低于 Catalog → update_available;否则 → installed。Catalog 中已隐藏/下架且无可比较版本时,保留 installed。

2.5 安装切换事务(Phase 1 原型)

apps/<id>/install-transaction.json 用于断电恢复:

{
  "schema_version": 1,
  "phase": "staging_activated",
  "had_current": true
}

phase 只允许:prepared、current_backed_up、staging_activated、rollback_required、committed。日志使用临时文件 + 同目录 backup 原子替换;恢复时同时检查日志与 current/staging/backup 实际状态。未完成健康检查的 current 不视为可信:有旧版时恢复 backup,首次安装则撤销 current。

T-613 为该原型建立了 fail-closed 的耐久顺序:每个 payload 先完成 CRC/长度检查、Sync、Close;staging 目录按子目录→staging 根→父目录同步。每次 journal 写入均为临时文件内容 Sync/Close 后 rename,并在 transaction/backup rename、删除后同步 app root。current → backup、staging → current、rollback/recovery rename 和受控目录删除也必须先同步 app root,才可写下一 phase 或触发测试步骤;只有 committed journal 完成该栅栏后才清理 backup 和 journal。非 Windows 打开目录后 File.Sync;Windows 以 CreateFile(FILE_FLAG_BACKUP_SEMANTICS) 打开读写目录句柄并调用 FlushFileBuffers,任一打开、flush 或 close 失败均返回安装耐久错误,不得静默降级。

T-302 在 Switcher 的 health 阶段先运行必需的注入 health check,再原子写入新 installed-app.json;health 或记录写失败都必须触发既有 rollback,使旧 current/记录保持可用。只有 health 与记录均成功后才写 committed 并清理 backup/journal。

若无 transaction 但存在残留 staging/,Recover 只会在 app root 已通过真实目录布局校验后删除该受控同级目录并返回 aborted;staging 不是目录或布局不安全时 fail closed。此恢复不得覆盖 current/、installed-app.json、data/ 或 licenses/。

这些栅栏与注入失败测试只证明代码层面的调用顺序和 fail-closed 行为,不证明断电后硬件/驱动缓存、网络文件系统、文件锁或杀毒软件的物理表现。T-302 已完成代码整合;T-601 仍须在目标 Windows VM/真机执行断电与干扰故障注入。

2.6 安装预检与失败码

在同句柄 size/SHA、ZIP 预扫描和严格 app.json 身份比对均通过后,提取器会在创建 staging/ 前提供已规划 payload 的准确展开字节数、普通文件数和安全 entrypoint。安装 use case 必须以 payload_bytes + 64 MiB 查询 app root 所在卷的可用空间;可用空间不足时不创建 staging。预检不能替代写入、同步、切换或回滚阶段的 fail-closed I/O 错误处理。

安装 use case 同时在上述位置检查当前 current/<entrypoint> 是否正在运行。容量、storage failure classifier 与运行状态均通过 core 接口注入,且均为必需依赖;classifier 只对已带 ErrStagingOutput 的原始 I/O 链识别磁盘满,core 不导入 Windows API。检查失败按不可安全继续处理。T-401 又在存在旧 current 的更新中、current → backup rename 紧邻前复查同一目标;明确运行中清理本次 staging 并返回 app_running,检测错误返回 target_state_unavailable,其他 switch/rename 错误仍为原有安装失败归因。首次安装不做第二次检查。v1 不强杀、不等待进程退出。

安装结果面向调用方的错误码为下表的稳定英文枚举;UI 负责本地化,原始错误只保留给 errors.Is、日志和诊断,不得进入 UI payload。

code 含义
hash_mismatch 完成文件长度或 SHA-256 与已验签 Catalog selection 不符
zip_path_escape ZIP/app manifest 路径违反共享 Windows 安全相对路径规则
zip_corrupt ZIP 结构、CRC 或受限读取/提取不完整,不能作为有效包
package_invalid package/app manifest 身份或协议不符合可信 selection
disk_full 可用空间小于 payload_bytes + 64 MiB,或平台 classifier 识别 staging write/sync/close 的底层 I/O 为磁盘满
disk_check_failed 无法可靠取得可用空间或得到非法容量值
app_running 当前目标程序仍在运行,更新不能替换
target_state_unavailable 无法可靠取得目标运行状态
install_failed 其他未细分的安装、健康、记录、切换、回滚或非磁盘满 staging 输出 I/O 失败

上述任一预检或验证失败都发生在 switch 前;已有版本的 current 和 installed-app.json 必须保持可用,首次安装不得留下 executable current。

2.6.1 受控启动与失败码

core/application/launch 的请求仅接受 app ID。它从本地记录取得启动元数据,确认 current 与工作目录是真实受控目录、entrypoint 是 current 内列入 files 的普通非 symlink 文件,再依次检查最低系统、授权和该精确 entrypoint 的运行状态,最后调用平台启动器。请求或 UI 不得提供 EXE、路径、参数、URL 或 shell command;授权 checker 是必需注入边界,许可证策略仍由 T-501~T-503 实现。

Windows 平台用 Toolhelp32 快照枚举,并以 QueryFullProcessImageName 的规范绝对路径作最终身份匹配;同名 EXE 只可作为查询优化,不能成为运行结论。快照/枚举/候选路径查询失败必须返回错误,不能当作“不在运行”。启动器只接收已验证的绝对 entrypoint 和工作目录:普通启动不经命令 shell 或 PATH 搜索;requires_admin 使用固定 runas Windows 动词且不传参数。非 Windows stub 对兼容、进程检测和启动均返回明确不支持错误。

code 含义
not_installed 没有可读取的安装记录
launch_metadata_invalid 历史记录缺少完整启动元数据,或元数据不安全/不一致
launch_target_unsafe current、工作目录或 entrypoint 布局不是受控真实对象
entrypoint_missing 记录中的 entrypoint 文件不存在
compatibility_unavailable / app_incompatible 无法可靠判定系统,或系统不满足最低版本
authorization_unavailable / not_authorized 授权边界不可用,或当前 app 未获授权
app_running / target_state_unavailable 精确 entrypoint 已运行,或不能可靠判断其状态
launch_failed 平台拒绝或未能创建受控进程

2.7 下载任务元数据 download-task.json(本地)

每个任务以稳定 request_id 为主键,元数据位于 downloads/tasks/<request_id>.json,字节文件位于 downloads/files/<request_id>.part|.download。本地路径只由客户端从 request_id 派生,不接受 URL 或 Content-Disposition 提供的文件名。

{
  "schema_version": 1,
  "request_id": "download-json-parser-001",
  "app_id": "json-parser",
  "url": "https://download.example.com/json-parser.zip",
  "status": "paused",
  "attempt": 2,
  "done": 1048576,
  "total_known": true,
  "total": 12345678,
  "validator": {
    "etag": "\"release-1.4.2\""
  },
  "error_code": "",
  "created_at": "2026-07-16T00:00:00Z"
}

规则:

  • Schema 为 schemas/download-task.schema.json;内部状态只允许 queued/downloading/paused/failed/completed。paused 对 UI 映射为 queued + DownloadPaused 事件,不增加第 13 个 AppStatus。
  • 默认最多 2 个 downloading;同一 request_id 幂等,身份/URL/size 不同则冲突;同一 app 同时只允许一个非 completed 任务。
  • 暂停保留 .part 和元数据;失败 retry 保留 .part;取消固定删除该 request_id 精确派生的 part/final/metadata,不使用递归删除。取消已线性化后,body close/sync 等非致命错误只记录日志,不能把取消反转成 failed 或跳过清理。
  • 续传只在存在 strong ETag 或合法 Last-Modified 时发送 Range + If-Range;无可靠 validator 时从 0 重下。请求固定 Accept-Encoding: identity,所有 redirect 重新校验 HTTPS。
  • 206 必须严格匹配 Content-Range 起点、长度、total 与期望 size;已有 part 但服务端返回 200 时截断并创建新 attempt,不能 old+new 拼接。
  • known total 使用已验签 Catalog package.size 作为精确传输长度;unknown total 使用显式 total_known: false 并受 4 GiB 默认硬上限保护。
  • known total 的 .part 已达到精确 total 时,同进程 resume/retry 与重启恢复都直接 sync、核对身份并 finalize,不得再请求 Range: bytes=<total>-。
  • 完成顺序为 part sync/close → 核对实际写入文件身份 → rename .download → metadata completed → DownloadCompleted。rename 前后必须仍是同一普通文件;恢复时以普通文件实际长度对账,不信任 metadata.done。
  • 事件投递是 best-effort,失败不得反向把已完成/已取消的持久任务改成失败。队列必须把投递错误交给 OnObserverError 记录;application/UI 在启动或事件通道重连后必须用 Queue.Tasks() 对账持久状态,不能只依赖某一次终态事件。
  • .download 仍是不可信隔离字节。T-302 必须重新从已验签/过滤 Catalog 取得并交叉核对 app/version/arch/size/SHA-256(及已由 Catalog 解析器校验格式、被外层签名覆盖的 package signature 文本),在同一普通文件句柄完成 size+SHA-256 后才可读取 app.json 或解压。

3. 许可证(服务端签发 → 本地离线验证)

{
  "schema_version": 1,
  "license_id": "lic-...",
  "machine_hash": "...",
  "products": ["product-json-parser"],
  "issued_at": "2026-07-16T00:00:00Z",
  "perpetual": true,
  "update_policy": "updates-until-2027-12-31",
  "rebind_policy": "self-service-1-per-90d",
  "signature": "..."
}
  • machine_hash 由平台层多个稳定硬件标识清洗生成;许可证中不保存原始序列号和 MAC。
  • 客户端用内置 Ed25519 公钥离线验签;许可证保存于 licenses/,与程序文件、用户配置分离;更新不得覆盖。
  • 撤销名单同样签名并缓存,网络失败保留宽限期。
  • 盒子负责导入/展示/管理;子软件必须用 sdk 的 licensing 逻辑独立再验证(签名 + machine_hash + product_id),决定正式版/试用版/授权错误。

4. application 事件合约(core → UI)

后台任务不直接修改 Gio 控件,只发布事件;UI 按 request_id + app_id 更新 ViewModel 并 Invalidate。

事件 触发时机 负载 结果
CatalogRefreshed 清单验签并缓存成功 catalog 摘要、generated_at 列表刷新
CatalogRejected 清单验签失败 原因码 提示 + 继续用缓存
DownloadStarted 一个下载 attempt 开始/因安全重下而重置 request_id, app_id, attempt, done, total_known, total 状态 → downloading
DownloadProgress 当前 attempt 进度更新 request_id, app_id, attempt, done, total_known, total, speed 进度条刷新;done 在同 attempt 内单调
DownloadPaused 用户暂停且旧 worker 已完全退出 request_id, app_id, attempt, done 状态 → queued(暂停态)
DownloadCompleted 字节完整落盘,尚未通过 SHA/签名 request_id, app_id, attempt, done, path 状态 → verifying;T-302 接管信任校验
DownloadFailed 传输/Range/存储失败 request_id, app_id, attempt, done, error_code 状态 → failed + 可重试
DownloadCanceled 取消清理完成且旧 attempt 不再发事件 request_id, app_id, attempt 从任务视图移除并回到本地基础状态
InstallCompleted 原子切换成功 + 健康检查通过 app_id, version 状态 → installed
InstallRolledBack 切换失败恢复 backup app_id, error_code 状态 → rollback 完成提示
AppStarted / AppExited 进程启动/退出检测 app_id, pid 状态 → running / installed
LicenseChanged 许可证导入/撤销 products 授权视图刷新
IconReady 图标已完成可信加载与后台解码 request_id, app_id, icon_ref, DPI, image.Image 有界 relay 唤醒窗口;UI Frame 验证仍为最新请求后创建 ImageOp
IconFailed 图标加载、校验或解码失败(取消不发布) request_id, app_id, icon_ref, DPI, error_code UI 记录诊断;仅保留同 icon_ref/DPI 的既有可信图标,否则继续占位

错误码为稳定英文枚举(如 hash_mismatch, zip_path_escape, disk_full, app_running, signature_invalid),UI 负责本地化文案。图标事件只使用 unavailable、invalid_content、unsafe_cache,原始网络错误只返回后台调用方/日志,不得进入 UI payload。unsafe_cache 详情只使用 event 中已经验证的 app ID、icon_ref 与 DPI 生成 <digest>-<dpi>.icon locator;不得把 cache root、原始错误或 link target 补进 event/UI。

5. CLI 参数合约

5.1 SoftBox.exe

SoftBox.exe                          # 正常启动
SoftBox.exe --open-app <id>          # 打开并定位到指定软件
SoftBox.exe --update-app <id>        # 触发指定软件更新流程
SoftBox.exe --repair-app <id>        # 按 files.json 修复安装(V1.1)

子软件的「检查更新」按钮调用以上参数,不自建下载器;盒子未运行时子软件可启动它。

5.2 SoftBoxUpdater.exe(盒子自更新助手)

SoftBoxUpdater.exe --pid <主程序PID> --staging <暂存目录> --target <目标目录>

等待主进程退出 → 备份旧版 → 切换新版 → 启动新版 SoftBox → 失败时恢复备份。退出码:0 成功;非 0 失败并写日志。

5.3 子软件推荐参数(v1 推荐,非强制)

<App>.exe --softbox-info      # 输出软件 ID、版本、架构、协议版本 JSON,退出码 0
<App>.exe --softbox-health    # 基本环境自检,成功退出码 0

产品自定义参数不得占用 --softbox- 前缀。

6. 进程退出协议(更新前)

v1:进程快照判断运行 → 提示用户保存关闭 → 等待正常退出 → 超时取消更新,不默认强杀。 V1.1:命名管道 \\.\pipe\softbox.<app-id>,盒子发送 {"command": "prepare_update", "request_id": "..."},子软件保存数据回复 ready 后自行退出。

6.1 子软件更新编排 v1

core/application/update 只接受外层已经从验证/过滤 Catalog 取得的 install.InstallRequest,并再次要求 package 与 architecture 精确匹配。它先读取受控本地 installed-app.json + current,要求旧记录有完整且安全的启动元数据,并且 Catalog 目标版本严格高于本地 SemVer;不得由 UI 或下载事件提供 EXE、路径、参数、URL、版本或 hash。

若旧 entrypoint 未运行,编排直接委托 InstallService;若运行,则顺序固定为:后台取得关闭确认 → 按配置的 1 秒至 10 分钟上限等待自然退出 → 委托 InstallService。等待器仅轮询 T-401 的完整 entrypoint 身份,支持 context 取消;不能调用 kill/TerminateProcess、shell、脚本或 IPC。等待完成后安装器仍在 staging 前和 current → backup 紧邻前复查运行状态,因此用户重新启动旧版本会安全返回既有 app_running,不覆盖 current。

code 含义
not_installed 没有受控安装记录
update_metadata_invalid / update_target_unsafe 历史记录缺启动元数据,或旧 entrypoint/current 布局不安全
update_not_available Catalog 版本与本地相同或更低
target_state_unavailable 无法可靠判定旧 entrypoint 是否运行
close_confirmation_unavailable / close_declined 无法取得关闭确认,或用户拒绝关闭
exit_wait_canceled / exit_wait_timeout / exit_wait_unavailable 等待被取消、达到上限,或进程快照等待不可用
install_failed 已委托安装器但安装失败;原始 InstallError 链仍保留其 stage/code

更新编排自身不写 current、backup、transaction、data/ 或 licenses/。production cmd 目前没有可信 Catalog、许可证和 completed-download 消费来源,也没有关闭确认 UI,因此尚未装配该用例;不得用 allow-all 授权或测试 fake 宣称更新闭环已经启用。

待实现时确认

  • 清单密钥 ID/轮换字段定稿后同步 schemas/。
  • 错误码完整枚举表。
  • 图标资源从内容哈希到下载位置/分辨率变体的发布端映射格式。
  • 撤销名单的结构与宽限期时长。
  • machine_hash 的标识来源清单与加权算法(平台层内部文档)。