Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ Windows 新增一套契约 v1 声明式插件宿主,与现有 Worker 扩展模
- 安装根为 `app_local_data_dir()/plugins/<pluginID>/<version>/`,`installation.json` 记录版本与启用状态;记录缺失或损坏的包隔离展示,绝不静默加载。
- bundled 包从 `resource_dir()/OfficialPlugins` 只读加载,多版本目录取最高版本;运行时不写安装目录与资源目录,遵守发行基线只读边界。
- 每个插件的语言服务器状态目录在 `app_local_data_dir()/plugin-state/<pluginID>/<workspace 哈希>`,由宿主注入启动计划请求。
- 信任模型 v0:官方 ID 防冒用由 Core 校验兜底;bundled 包随安装器分发;磁盘导入(已落地)与市场下载(M3+)一律走 `plugins.marketplace.installReview` 全量审批(权限 + 完整程序清单),审批界面明示"未认证发布者"。publisher Ed25519 验签明确延后,这是 v0 的已知缺口,Windows 验签方案(Authenticode 还是包内 Ed25519)单独立项评估。
- 信任模型 v1(Windows 插件对齐路线图 M1,见 `docs/development/windows-plugin-parity-plan.md`):发布者 Ed25519 验签已接入 `plugin_host/signature.rs`,与 macOS `PluginPackageSignature` 字节兼容(canonical payload、UTF-8 base64url token、按原始路径排序、每文件 SHA-256 与 contentDigest),采用成熟的 `ed25519-dalek`,固定官方公钥 `lithe-official-plugins-v2`;生产私钥不进入仓库。官方 ID 的信任策略由宿主固定(`php/java-support=sameTeamAsHost`、`go-support=publisherPackage`),覆盖包自声明;`sameTeamAsHost` 只信任宿主安装资源内的 bundled 包,磁盘/市场包不能凭目录名获得该身份。签名文档存在即必须验证,坏签名不会被忽略。磁盘导入与市场下载(M2+)仍走 `plugins.marketplace.installReview` 全量审批(权限 + 完整程序清单),审批界面明示"未认证发布者"。
- 安装记录 schema 2 是安装事务的一部分:`origin`、marketplace/disk provenance(全包内容摘要与批准程序的 canonical JSON)、上一版本的 provenance 都随版本发布一起写入;安装确认、staging 发布前、启动扫描、运行/测试计划入口与回滚都会重新校验完整文件集合和摘要(community 与 marketplace origin 都复检已批准摘要,签名只证明发布者、不豁免字节比对)。版本激活与记录提交是同一事务:记录先写入 temp、激活成功后才提交,提交失败恢复旧字节并保留旧记录,恢复失败如实报错。schema 1 旧记录没有 provenance,扫描直接隔离(`plugin.trust.provenanceMissing`)并要求重新安装,绝不凭 `enabled` 或清单摘要补造发布者身份和已批准字节。bundled 身份只能由可信资源发现器产生:installed 记录声明 `origin`/`previousOrigin=bundled` 一律隔离,`sameTeamAsHost` 在扫描、启动与回滚路径都校验真实资源根包含,记录声明与回滚候选同样拒绝。审批绑定 prepare 时的整包字节:prepare 冻结整包摘要、清单摘要与审查程序,confirm 与 staging 对同一审核身份校验,prepare 之后替换/新增/删除文件都被拒绝。磁盘渠道接受已验签的 `publisherPackage` 包(origin 保持真实 disk/community),声明 `sameTeamAsHost` 的磁盘包即使签名有效也拒绝。
- 磁盘导入的私有快照、staging 重验、内容摘要复查与回滚语义照抄 macOS 侧规则(见声明式语言插件 Note 的磁盘安装节),Windows 不另造审查语义。

### 启动接线(M1 落地形状)
Expand Down Expand Up @@ -83,14 +84,14 @@ WebView2 的 NavigationStarting 可以阻止外站文档提交,但实测外站

### 实施切分

M1 包存储 + 启动校验 + 语言注册 + LSP 启动(已落地);M2 磁盘安装链路 + 管理界面 v1 + 死链处置(已落地);M3 声明式设置 + 运行/测试计划 + PHP 契约包(已落地,Worker 版保留见上文);M4 Web 工具窗口(已落地,桥 v0 只含 state/openFile/主题,build.* 与 functions.* 等 Windows 构建系统能力接入后开放)。每个里程碑独立 PR,矩阵条目随代码同 PR 更新。总体方向遵循[公共扩展契约与 Provider 接入方向](../../proposed/architecture/2026-10-13-public-extension-contract-and-provider-api.md),本 Note 只落地其中 Windows 契约宿主的第一阶段。
M1 包存储 + 启动校验 + 语言注册 + LSP 启动(已落地);M2 磁盘安装链路 + 管理界面 v1 + 死链处置(已落地);M3 声明式设置 + 运行/测试计划 + PHP 契约包(已落地,Worker 版保留见上文);M4 Web 工具窗口(已落地,桥 v0 只含 state/openFile/主题,build.* 与 functions.* 等 Windows 构建系统能力接入后开放)。路线图 M1(发布者签名 + 全包完整性 + 安装记录事务,`signature.rs` 与 schema 2 安装记录)已落地;本 Note 早期的 M1–M4 编号是宿主 PR 的内部切分,与路线图的 M0–M10 不是同一序列。每个里程碑独立 PR,矩阵条目随代码同 PR 更新。总体方向遵循[公共扩展契约与 Provider 接入方向](../../proposed/architecture/2026-10-13-public-extension-contract-and-provider-api.md),本 Note 只落地其中 Windows 契约宿主的第一阶段。

## 考虑过的备选方案

- 把 Worker 扩展模型整体改造成契约宿主:契约没有主题、AI provider 这些概念,改造成本高且无收益,不采用。
- 在 Tauri 侧另写一套清单校验:违反"校验只在 Core 实现一次",共享 fixtures 无法复用,双端语义必然漂移,不采用。
- 把 Worker RPC 桥接成 litheFunctions:两者协议语义不同,会同时存在两套函数协议,等有真实插件需求时再按[插件函数契约](2026-10-20-plugin-function-contract.md)接入。
- 信任模型一步到位 publisher 签名:Windows 验签载体需要单独评估,v0 以 bundled 加全量审批兜底,签名延后是显式记录的缺口而不是遗漏。
- 信任模型一步到位 publisher 签名:v0 曾以 bundled 加全量审批兜底,签名延后是当时显式记录的缺口;路线图 M1 已选择与 macOS 相同的包内 Ed25519 文档签名(而非 Authenticode),因为它跨平台字节兼容、验证在安装事务内完成,且不依赖系统证书存储。
- 在 `lsp_start` 一类原生命令里拉起插件服务器:现有 LSP 会话全部由 Core 统一管理(会话、取消、事件泵都在 Core),前端只解析可执行路径与参数;另建原生拉起路径会分裂会话所有权,不采用。

## 后果
Expand All @@ -111,6 +112,7 @@ M1 包存储 + 启动校验 + 语言注册 + LSP 启动(已落地);M2 磁
- 网页隔离与上下文回归:`web.rs` 覆盖同包和外站 URL 的导航/桥接共用检查、真实路径与内部/外部目录链接;Windows 用无需提权的 junction,Unix 用目录符号链接。`plugin-web-appearance.test.ts` 执行原生宿主桥脚本,验证语言、主题、根变量、主题事件与外站不暴露桥;前端文件测试验证原生真实目标传递与拒绝传播。
- 版本探测回归:`runtime_probe_collects_output_and_rejects_overflow`、`runtime_probe_reclaims_inherited_pipes_after_parent_exit` 使用当前测试二进制作为固定夹具;后者让父进程退出、后代保留管道,断言调用有界结束且后代已退出。测试环境变量仅附加到子进程,不改测试运行器的全局环境。运行 WindowsRust 与 Frontend 计时 harness,检查 `.artifacts/test-stability/index.html`;真实 WebView2 验证还要由本机 HTTP 计数器确认外站 GET 为零,同时检查导航、弹窗/下载、动态主题、包文件和发行目录哈希不变;单凭页面 URL 没变不能证明请求未外发。

- 路线图 M1 信任链:`plugin_host/signature.rs` 用 RFC 8032 测试密钥锚定 Ed25519 原语;共享公开向量 `shared/contracts/plugins/v1/fixtures/signature-vectors.json`(含非 ASCII 文件名、canonical payload 字节、确定性签名与 contentDigest)由 `shared_fixture_verifies_end_to_end` 断言,Swift 端 `PluginSignatureVectorTests` 消费同一文件(macOS 实机运行待 CI)。`install_tests` 走真实 prepare→review→confirm→publish→scan 路径(测试密钥经 cfg(test) 注入,生产不可达):记录提交失败恢复旧字节与旧记录并可重试、首次安装记录失败不留字节、staged 字节绑定批准摘要、同版本替换拒绝改动字节、已签名磁盘包安装与加载、未知 key/坏签名/sameTeam 磁盘包拒绝、prepare 后替换/增删文件拒绝、回滚候选按其自身 provenance 验证且 bundled 声明拒绝;`tests.rs` 覆盖重启扫描隔离篡改/增删文件、installed 记录伪造 bundled 来源隔离、官方 ID 拒绝无签名社区安装(seal 错误)、社区包中存在的签名文档必须验证;`signature.rs` 单测覆盖真实资源根包含检查。`declarative_plugins_confirm_disk_install` 与回滚保持 #1424 的逐窗口停止屏障,失败时旧版本与旧记录保持可用。确认编排里,会话身份只解析一次且位于统一清理块内,其值既作为真实 `lifecycle::begin` 的屏障键、也显式传入共享编排作为发布前身份校验的期望值——helper 不重读身份,发布前拒绝身份漂移(屏障期间会话换成其他插件时,任何字节都不在错误屏障下发布);安装记录的读取与回滚链构造同样在屏障内完成。屏障拒绝与校验拒绝(含身份解析失败)都释放快照与会话记录。事务中断遗留的 `*.previous-tmp` 恢复备份(完整版本名命名,相邻补丁版本不碰撞)在下一次发布前按 durable record 当前批准的摘要判定:备份即批准字节则恢复、批准字节已在他处完整存在则清理备份、无法证明则拒绝歧义状态并完整保留数据,跨版本的孤儿备份在其批准字节可证时先归位自身版本目录,恢复失败保留备份并如实报错,绝不盲删。恢复前还校验记录的插件身份与单段版本路径,损坏记录不能把备份移到其他插件或用户目录。清理失败的回归从真实发布事务留下备份后再触发下一次记录提交失败,避免只手工创建预期路径而漏掉实际备份命名错误;测试文件锁与临时目录均用作用域清理。
- M4 Web 工具窗口:`plugin_host/web.rs` 测试覆盖包路径规范化(`..`、编码分隔符、空段拒绝)、CSP 内容断言(default-src none、无 wasm-unsafe-eval、frame-ancestors none)、桥脚本冻结与作用域、状态键隔离;web-panel fixture 随 `ui:[1,2]` 声明提升转为被宿主接受(能力门控测试同步)。桥 v0 的 build.*/functions.* 返回 `plugin.web.unknownMethod`,等构建系统能力接入。


Expand Down Expand Up @@ -142,6 +144,8 @@ M1 包存储 + 启动校验 + 语言注册 + LSP 启动(已落地);M2 磁
- `windows/tauri/src/features/plugins/declarative-plugin-settings-editor.tsx`(设置编辑)
- `windows/tauri/src/features/plugins/declarative-plugin-run.ts`(契约插件运行入口)
- `windows/tauri/src-tauri/src/plugin_host/web.rs`(Web 工具窗口 scheme、桥与状态)
- `windows/tauri/src-tauri/src/plugin_host/signature.rs`(发布者验签、contentDigest、官方 ID 固定信任策略;依赖 `ed25519-dalek`)
- `shared/contracts/plugins/v1/fixtures/signature-vectors.json`(双端共享签名测试向量;macOS 消费方为 `macos/Tests/LitheTests/PluginSignatureVectorTests.swift`)
- `Plugins/win/Official/PhpSupportContract/`(契约版 PHP bundled 包)
- `windows/tauri/src/features/plugins/declarative-plugins-section.tsx`(设置内管理页)
- `windows/tauri/src/features/editor/lsp/resolve-editor-lsp-launch.ts`(契约插件回落来源)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Agent 笔记:插件 fixture 覆盖门禁

状态:已实现

## 先说结论

`shared/contracts/plugins/v1/fixtures` 的覆盖检测器(`scripts/check-plugin-fixture-coverage.mjs` + `plugin-fixture-coverage.json` + 单测)已接入 `ci-macos.yml` 的 `changes` job——该 job 在 ubuntu runner 上对每个 pull request(含 Draft)运行,不新增 workflow、不占 macOS/Windows runner。任何人新增 fixture 而不在清单登记,真实 PR 检查会以 `unknown-fixture` 失败。门禁的防伪造立场是"证据与诚实分类",不是"永远保持 partial":允许全部 fixture 真实 wired 后零缺口通过,但没有证据的 wired、没有阶段归属的缺口一律拒绝。

## 问题

- 覆盖清单(M0 交付)靠显式登记维护。若只在本地运行检测器,"新增 fixture 忘了登记"只会安静漏过,`unknown-fixture` 不会让任何 PR 失败,清单会重新漂移。
- 第一版真实仓库守护测试断言 `knownGaps > 0`:把 2026-10-25 当天的真实缺口数量固化成了永久约束。未来某天缺口真实补齐时这个断言必然失败,反而诱导后人删掉守护测试。
- 缺口阶段(gapStage)的注释说 M0–M10,正则却排除 M0,语义与说明不一致。

## 决策

### CI 入口复用现有 Linux lane,不旁建 workflow

`ci-macos.yml` 的 `changes` job 已有一个"scripts 测试 + JUnit/HTML 计时报告 + artifact 上传"的成熟 lane(Sparkle baseline 模式):`node --test` 双 reporter(spec 到 stdout、junit 到 `.artifacts/test-stability/<stem>.junit.xml`),再用 `parse-junit-cases.mjs` + `writeTestReportArtifacts` 生成逐项计时的 HTML/JUnit 报告,最后由同一 job 的 upload 步骤上传整个 `.artifacts/test-stability/`。插件 fixture 门禁镜像同一模式插入三步:检测器 CLI(真正的清单 vs 目录比较)、单测(`--test-timeout=15000` 本地期限)、计时报告生成。该 job 在 ubuntu 上对所有 PR 运行(含 Draft),因此 #1476 这类新增 fixture 的 PR 在合入前就会撞上 `unknown-fixture`。

门禁与测试的接线由测试自身守护:`check-plugin-fixture-coverage.test.mjs` 里有一条 CI 接线守卫测试,读取 `ci-macos.yml` 断言两个入口(CLI 步骤、测试文件)与期限/报告参数存在——workflow 改名或移除调用时,测试先红。

### 防伪造 = 诚实分类,不是永久 partial

- 正例:一个全部 wired 且证据文件真实存在的清单,检测器必须通过且 `knownGaps = 0`(有专门单测锁住这条路)。未来覆盖补齐时门禁自然放行。
- 负例:wired 却没有 Windows 证据、known gap 缺阶段或说明、证据引用文件不存在,全部拒绝(既有单测覆盖)。
- 真实仓库守护测试只断言"清单与目录一致 + 每个非 wired 条目带合法阶段与说明",不再断言缺口数量。

### gapStage 语义:M0–M10 全部合法

路线图 M0 的退出条件把基线问题路由进聚焦修复 PR,所以"由 M0 拥有的缺口"是合法归属;M11 及以后不存在。正则为 `^M(10|[0-9])$`,注释与实现一致,并有单测同时覆盖 M0 接受与 M11 拒绝。

### M1 交接(#1476)

`fixtures/signature-vectors.json` 在 #1476 中新增;其 PR 合入 preview 前(或同时)必须在清单登记,否则撞 `unknown-fixture`。最小登记条目(检测器已预置 `signature-vectors` 类别):

```json
{
"path": "signature-vectors.json",
"category": "signature-vectors",
"outcome": "success",
"coreEvidence": [],
"windowsEvidence": ["windows/tauri/src-tauri/src/plugin_host/signature.rs:663 shared_fixture_verifies_end_to_end"],
"windowsStatus": "<按 #1476 实际证据填 wired 或 validated-only>",
"gapStage": null,
"gapDetail": null
}
```

该类别是唯一允许 `coreEvidence` 为空的类别:签名向量由双端宿主各自的签名测试消费(Windows `signature.rs`、macOS `PluginSignatureVectorTests.swift` 的 RFC 8032 锚点、确定性签名、内容摘要、签名文档回读四组 @Test),没有 Core 校验面;豁免只对 `signature-vectors` 生效,其他类别仍必须有 Core 侧证据(有单测锁定)。`backend.plugin.json` 已由本 PR 随 preview 同步(`83017afa`/8471b998)登记为 `unwired` + `followup`——`followup` 是为"冻结路线图(M0–M10)之后落地、按计划 §6 单列后续阶段"的新能力保留的缺口阶段,是 M 编号之外的唯一合法值。登记条目的最终行号以 #1476 合并时的代码为准。合并顺序优先 #1474(门禁)先合、#1476 随后带登记合入;若 #1476 先合,它必须自带登记条目,否则其 PR 门禁失败。

## 考虑过的备选方案

- 新建独立 `ci-fixture-coverage.yml`:多一个 workflow、多一份维护与触发面,且与"优先复用现有门禁"的仓库惯例冲突,不采用。
- 只在本地可选运行检测器:登记纪律依赖人的记忆,已知会漂移,不采用。
- 保留 `knownGaps > 0` 作为防伪造断言:把"今天有缺口"固化成"永远有缺口",未来真实补齐反而红,防伪造应针对"没有证据的 wired",不采用。
- 在 `verify-shared-contracts.sh`(Ruby 内联)里实现枚举比较:该脚本是契约语义校验(schema/拒绝码),把 Node 已实现的检测器复制一份 Ruby 只会分叉逻辑,不采用。

## 后果

- 收益:新增 fixture 未登记会直接打红 PR(含 Draft);单测有 15 秒本地期限与逐项计时报告;覆盖补齐路径(全 wired)有正例保证畅通。
- 代价:`ci-macos.yml` 的 `changes` job 多两步(约几秒);维护清单成为改 fixture 的同 PR 义务;workflow 与测试之间由守卫测试双向绑定,改 CI 入口措辞时守卫测试需要同步。

## 验证

```bash
node scripts/check-plugin-fixture-coverage.mjs
node --test --test-timeout=15000 scripts/check-plugin-fixture-coverage.test.mjs
node --test --test-reporter=junit --test-reporter-destination=.artifacts/test-stability/plugin-fixture-coverage.junit.xml scripts/check-plugin-fixture-coverage.test.mjs
./.agents/skills/write-stable-tests/scripts/verify-test-stability.ps1 # 或 .sh
node scripts/verify-agent-notes.mjs
```

CI 等效路径:`ci-macos.yml` changes job(ubuntu)的 "Check plugin fixture coverage manifest" / "Run plugin fixture coverage tests" / "Generate plugin fixture coverage timing report" 三步,artifact 随既有 upload 步骤上传。

## 适用范围

- `scripts/check-plugin-fixture-coverage.mjs`、`scripts/plugin-fixture-coverage.json`、`scripts/check-plugin-fixture-coverage.test.mjs`
- `.github/workflows/ci-macos.yml` 的 changes job 中 "plugin fixture coverage" 三步
- `docs/development/windows-plugin-fixture-coverage.md`(审计快照与维护规则)

不覆盖:M1 拥有的签名/安装信任链 Note(#1476)、共享契约 fixtures 本体、平台功能矩阵。
Loading
Loading