Skip to content

feat(plugin): 重构安装信任模型,统一官方、本地与第三方市场的权限和更新边界 #1857

Description

@zqchris

使用场景 / Use case

插件既可以从 Cindy 官方市场安装,也可以由用户直接选择本地 .cindy、通过 Git/本地第三方市场安装。我们需要让三种来源共用一条可理解、可验证、可迁移的安装路径:

  • 包体自身的 ghost.json 是版本、权限和宿主兼容性的唯一事实来源;
  • 服务器负责列表、访问授权、不可变 Release 与更新发现,不成为第二个权限裁决者;
  • 用户可明确用本地或第三方版本替换同 ID 的官方插件,但不会因此静默继承官方身份、官方特权或历史凭证;
  • 第三方市场单源损坏不会拖垮官方市场或其它来源;
  • 插件作者能清楚知道普通能力、官方特权、版本兼容和发布契约的边界。

关联事故与历史:

当前问题 / Current limitation

当前官方市场在本地 .cindy 安装器之外又维护了一套权限与来源状态机:

  1. 权限存在多份真相:服务器 Release manifest、下载包 manifest、已安装 canonical/localized manifest、市场 ledger manifestDigest;
  2. 安装前、下载后、提交前多次重新计算来源与权限,产生大量 PRECONDITION 状态和竞态;
  3. 未安装的其它市场条目会预占 ghostId;任一自定义来源暂时不可读,所有官方新安装都 fail-closed;
  4. Renderer/IPC 丢失 declared / accepted / skipped / unreadable 等状态,用户只看到“成功 / 0 个”或通用“安装失败”;
  5. 本地同 ID 包替换官方插件时,现有规则主要依赖 cindy- / filo- / xd- 前缀一刀切,非保留 ID 的官方插件又可被直接覆盖,来源与凭证边界不一致;
  6. 市场更新按 releaseId 而不是可比较版本判断;本地 version 当前只是展示字符串,同版本不同内容和降级都被当作普通更新;
  7. 单仓测试大量 mock 下载与最终安装出口,无法发现 production catalog + 实际包 + 最低支持客户端的跨仓不兼容。

这些复杂度并没有增强最终运行时权限:Desktop 最终仍然必须解析并执行真实 .cindy 包。相反,它制造了平行裁决与升级兼容风险。

目标原则 / Target principles

  1. 包体是唯一权限真相
    安装确认、权限 diff、运行时能力都以本次将要落位的实际 .cindy 中 canonical manifest 为准。服务器可缓存包内元数据用于列表展示,但不提供另一份参与安全裁决的权限 manifest。

  2. 市场是可信下载与更新索引
    服务器只负责目录、访问控制、不可变 Release、包体大小/SHA、兼容版本选择和下载授权。

  3. 一个权威安装器,来源只是窄上下文
    本地文件、官方市场、第三方市场共用 inspect → 权限确认 → 原子落位/回滚。来源上下文只决定 Release SHA、来源记录、官方保留能力是否可用。

  4. 用户选择来源,不让未安装目录预占 ID
    只有磁盘上已经安装的同 ghostId 才构成替换/更新冲突。其它市场存在同名条目只做展示提示,不阻断用户选择。

  5. 允许来源切换,但隔离身份与敏感资产
    用户可以 official ↔ local ↔ third-party 切换;未经官方证明的包不继承官方身份、Host 特权和历史凭证。

  6. 存量升级无感
    已安装、已启用、已批准的插件升级客户端后继续工作;不得要求批量重装、重新授权或重新配置。账本、批准状态和凭证命名空间必须自动迁移。

期望架构 / Proposed architecture

1. 包与兼容性契约

包内 manifest 至少承担:

{
  "id": "example-plugin",
  "version": "1.2.3",
  "minHostApiVersion": 3,
  "slots": [],
  "network": {},
  "node": {}
}
  • id:本地安装身份;
  • version:市场发布要求 SemVer;本地开发包若不可比较则按“替换版本”呈现;
  • minHostApiVersion:声明实际依赖的宿主能力,不用产品版本号间接猜能力;
  • permissions/slots/network/node:实际包的唯一权限声明。

新增 Host 能力必须先落客户端,再允许要求该 Host API 的插件 Release 对兼容客户端可见。

2. 服务器边界

服务器负责:

  • 下发插件目录、介绍、图标和 Release 列表;
  • pluginId + version 保持不可变,同版本不同 SHA 拒绝发布;
  • 上传时计算并保存包体 SHA 与大小;
  • 私有/组织插件下载鉴权;
  • 根据客户端 hostApiVersion 返回该客户端可运行的最新 Release,而非总是返回全市场最新版本;
  • 发布门实际下载包并对最低支持 Host API 做 conformance 检查;
  • CN / Global 保持相同包与兼容性结果。

服务器不负责:

  • 生成另一份安装权限事实;
  • 判断用户是否批准某项权限;
  • 参与本地包的更新/降级授权;
  • 因第三方来源状态决定官方插件能否安装。

3. Desktop 权威安装器

收敛为类似下列单一入口:

installCindyPackage(filePath, {
  source: "local" | "official-market" | "third-party-market",
  sourceKey,
  expectedReleaseSha256,
  releaseId,
  officialTrust,
})

公共管线:

  1. 限量读取包体并计算 SHA;
  2. 解包、校验 manifest、签名/trust、zip-slip、zip bomb、skill/资源一致性;
  3. 与当前已安装实际 manifest 比较权限;
  4. 用户只确认一次真实包权限;
  5. 将确认绑定到同一包 SHA;
  6. ghostId 加锁,staging → backup → 原子替换,失败回滚;
  7. 保存最小来源记录并重启运行时。

保留现有本地安装器的包体 SHA 钉死、原子换目录、运行时权限和安装锁。删除市场专属的第二套权限复核。

4. SHA 与来源判断

场景 行为
官方市场下载,实际 SHA 与 Release 不同 硬失败;不能静默降级成“非官方”
本地包 SHA 命中已知官方 Release 可视为该官方包的离线重装/更新
本地包与官方同 ID / 同版本但 SHA 不同 提示“与官方发布内容不同”,用户确认后可切换为本地版本
无法联网或没有官方 SHA 记录 视为来源未经验证,允许本地安装
第三方市场 SHA 匹配 只证明与该第三方市场登记的 Release 一致,不等于 Cindy 官方背书

未来如需离线证明官方身份,可增加包签名;不能用“有一个 SHA”本身冒充签名。

5. ID、版本与更新

  • 市场同一 id + version 不可变;
  • 客户端比较“已安装包版本”与“当前 Host API 下最新兼容版本”;
  • 相同 SHA:显示已是当前包,不重复替换;
  • 同版本不同 SHA:明确提示本地/第三方自定义替换;
  • 更高版本:普通更新;
  • 更低版本:明确标注降级,用户确认后允许;
  • 非 SemVer:不假装判断高低,显示“替换版本”;
  • 市场 releaseId 只做溯源,不代替版本比较。

6. 来源切换、凭证与官方特权

来源切换必须是一等状态,而不是依赖 ID 前缀和残留 ledger 推断。

建议将敏感资产至少绑定到:

owner + ghostId + trustDomain/sourceKey

规则:

  • 同一可信来源的正常更新可延续凭证和 OAuth 连接;
  • official → local / third-party:必须明确提示来源变化;
  • 未经官方证明的新包不得自动继承官方 Token、OAuth、Connection 或其它敏感资产;
  • 普通 UI 偏好、布局等非敏感状态可以保留;
  • 用户可另行明确选择迁移/重新授权凭证;
  • local / third-party 不得声明或使用只对官方 trustDomain 开放的能力,例如受控 tokenBroker、组织身份、宿主登录令牌等;
  • exact official SHA 或未来可信包签名可恢复 official trustDomain;
  • 切回官方版本时同样走明确来源切换,并恢复/重新连接对应官方凭证域。

需要替换当前仅靠 cindy- / filo- / xd- 前缀的一刀切规则;前缀仍可保留为命名规范,但不能独自承担来源真实性。

7. 第三方市场边界

  • 第三方市场可以提供目录、版本、SHA 和下载地址;
  • 其 SHA 只建立“包与该市场记录一致”的来源证明,不授予官方身份;
  • 不得预占尚未安装的 ghostId
  • 一个来源不可读、部分条目非法或同步失败,不阻断其它来源安装;
  • 来源 UI 必须展示 declared / accepted / skipped / unreadable 及稳定 reason;
  • 正常空市场、全部条目被拒绝、暂时不可读是三种不同状态;
  • 同 ID 多来源由用户选择;已安装后 provenance 决定默认从哪个来源检查更新;
  • 切换来源使用第 6 节的凭证与特权边界。

8. 第三方插件开发边界

作者文档与 Forge 需要明确:

  • 包内 manifest 是权限和兼容性的权威契约;
  • 市场插件版本必须 SemVer,同版本不可重新发布不同内容;
  • 普通第三方能力与官方 Host 特权的完整清单;
  • 未支持 Host API 时如何开发、测试和发布;
  • 本地开发允许同版本不同内容、替换和降级,但会显示非官方/自定义来源;
  • 第三方不得通过抢注官方 ID、复用 secret key 或来源切换继承官方资产;
  • Forge 检查应与 Desktop 实际 validator 共用同一契约,不再维护平行枚举。

建议删除/收缩的现有机制

  • 跨全部市场的潜在 ghostId 所有权计算;
  • assertSourceOwnsGhostId 对不完整来源的全局安装阻断;
  • 服务器 manifest 与真实包 manifest 的市场专属双重权限裁决;
  • reviewedManifest / previouslyInstalledManifest / reviewedBaseline 等为平行 manifest 补洞的状态;
  • ledger manifestDigest 作为权限批准或潜在来源所有权的事实;如其它安全用途仍需要,须重新命名并收窄职责;
  • releaseId 变化直接等同版本更新;
  • 非稳定错误统一折叠为无法诊断的“安装失败”。

迁移要求

这是插件基座与凭证边界改造,迁移是 P0:

  • 现有官方市场安装记录自动映射到 official trustDomain;
  • 现有 Git/本地市场记录自动映射到对应 sourceKey;
  • 现有纯本地插件映射到 local;
  • 现有凭证不得删除或丢失;
  • 能确定来源的凭证自动迁移到对应 trustDomain;
  • 来源不明确时保持插件可用并给出一次性恢复路径,不能静默把凭证交给不同来源;
  • 升级后不要求用户重新安装、重新批准或重新配置所有插件;
  • 迁移可重复执行、失败可恢复,并覆盖 Windows 文件锁/中断场景。

分阶段实施建议

Phase 0:事故止血与观测

Phase 1:抽取统一安装核心

  • 将本地与市场共同的 inspect、SHA、权限 diff、原子更新抽成单一 Main API;
  • 增加明确 source context 和 trustDomain;
  • 行为先保持兼容,建立全链路 fixture。

Phase 2:官方市场变薄

  • 权限确认改为只看实际包;
  • 删除二次 market manifest 复核状态机;
  • ledger 收缩为 provenance;
  • 服务器按 Host API 返回最新兼容 Release。

Phase 3:第三方市场与来源切换

  • 删除潜在 ID 预占;
  • 实现来源切换确认、敏感资产隔离和第三方特权限制;
  • 补齐来源状态 UI 和第三方作者文档。

Phase 4:迁移与发布门收口

  • 完成旧 ledger/vault 自动迁移;
  • CN / Global、Windows / macOS、最低支持客户端矩阵实测;
  • 删除旧兼容分支和不再使用的平行 manifest 状态。

验收标准 / Acceptance criteria

  • 官方市场安装只下载一次,用户确认和最终落位是同一 SHA 的实际包;
  • 权限确认只来自包内 canonical manifest;
  • 官方下载 SHA 不符硬失败;本地 SHA 不匹配只警告并允许用户明确替换;
  • 本地同 ID 同 SHA no-op;同版本不同 SHA可明确替换;低版本可明确降级;
  • 旧 Host API 获得最新兼容 Release,不会看到或安装无法运行的新能力包;
  • 一个第三方市场不可读不影响官方市场和其它第三方市场;
  • 未安装的同 ID 市场条目不预占所有权;
  • official → local/third-party 不静默继承官方凭证和 Host 特权;
  • 同来源正常更新保留启停态、布局、配置和应保留的凭证;
  • 切回官方版本有清晰来源转换语义;
  • 现有插件升级客户端后无需批量重装、重新授权或重新配置;
  • 生产 CN / Global 全部公开插件可通过真实包安装巡检;
  • 覆盖 Windows 10 冷安装、文件锁、更新中断与迁移恢复;
  • server、cindy-protocol、Desktop validator、Forge 和 official-plugins 使用同一契约或共享 conformance fixtures。

已考虑的替代方案 / Alternatives considered

  1. 继续修补现有市场状态机
    已由 chore(plugins): 市场发现跳过插件条目时记录原因与位置 #1524fix(plugin): 支持真实包权限变化后重新确认 #1648fix(desktop): preserve legacy plugin permission approvals #1657 等证明只能修局部症状;多份 manifest 与来源所有权仍会继续产生竞态和兼容事故。

  2. 完全取消 SHA
    本地文件可以没有官方 SHA 证明,但官方市场下载仍必须用 Release SHA 发现分发错误或内容替换。

  3. 禁止本地包覆盖官方 ID
    简单但限制用户开发、fork、回滚和离线安装;更合理的是允许来源切换,同时隔离官方身份、特权和敏感资产。

  4. 直接用 Cindy 产品版本做兼容门
    可行但把能力与发版号耦合。优先使用单调的 Host API version;产品版本只用于展示和灰度。

  5. 让服务器继续裁决权限
    会继续产生服务器投影 manifest 与实际包不一致。服务器可以在发布时校验,但客户端安装授权只能以实际包为准。

跨仓范围与门禁

可能涉及:

  • makecindy/cindy:Desktop installer、market service、ledger/vault migration、Renderer;
  • makecindy/cindy-protocol:manifest / Host API / conformance 契约;
  • Cindy server:Release 不可变性、兼容版本选择、下载与发布门;
  • makecindy/cindy-official-plugins:官方插件版本与能力声明、发布 CI。

本议题触及插件运行时、批准状态、凭证边界、manifest 契约和存量迁移,后续实现 PR 均需按插件基座白名单确认门审查;不得通过放宽运行时沙箱或丢弃存量凭证换取简化。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions