feat: add preview-first OPC host adapters#28
Conversation
Independent QA acceptance
No raw home paths, credentials, prompts, requests, tokens, or session identifiers are retained in this public evidence. |
coconilu
left a comment
There was a problem hiding this comment.
Review(Kimi Code CLI 代为审查)
总体评价:架构与 issue #26 契约高度对齐,但有 4 个 major 问题,建议修复后再合并。
已核实属实的部分:plan 零写、120s 过期、一次性确认、重启失效、写入时状态指纹(覆盖 source/probe/capability/ownership/target,常量时间比较)均在代码中真实强制且有注入测试;verify 走新进程官方发现机制而非文件存在;Claude 不用 marketplace remove、卸载 --keep-data;所有权 fail-closed 有充分测试;adapter 路由全部经过 Host+Origin+CSRF 防护;manifest 不含绝对路径/凭据,privacy_scan PASS。
本地实测:validate_repo.py PASS;隔离环境下 python -m unittest discover -s tests → 415 tests, OK (skipped=6),与 PR 声明一致。
Major(建议修复后合并)
-
Kimi 更新可静默置换用户自有目录 —
opc_adapters.py:1143-1146
守卫条件是if target.exists() and manifest is None: raise UNKNOWN_TARGET_CONFLICT,只在全新安装时 fail closed。场景:OPC 已装 v1(skills A/B),canonical v2 新增 skill C,而用户自己已有~/.kimi-code/skills/C(非 OPC 创建)。更新时os.replace直接把用户目录挪进备份并覆盖,plan diff 只显示replace kimi:skill/C,无任何冲突提示。违反 issue「更新只处理 manifest 能证明由 OPC 创建的内容;未知文件必须保留并转为冲突/人工处理」及 ADR-0018 的 fail-closed 原则。建议:manifest 存在时,对已存在但不在managed_targets中的 target 同样抛UNKNOWN_TARGET_CONFLICT。 -
Kimi update 的回滚不校验当前哈希即
shutil.rmtree—opc_adapters.py:1275-1292
backup 恢复路径中if target.exists(): if current is None: raise ROLLBACK_CONFLICT; shutil.rmtree(target)—— 有 manifest 时不校验哈希直接删除,update 之后用户做过的编辑会被 rollback 销毁。同文件 install 回滚路径(1266-1273)恰恰有_is_link+tree_digest校验并抛ROLLBACK_CONFLICT,两条路径自相矛盾,也违反 ADR-0018「manifest 证明所有权且当前哈希仍匹配才可改动/移除」。建议:rmtree 前按 current manifest 校验链接与哈希,不匹配则ROLLBACK_CONFLICT。 -
前端没有任何恢复入口 —
assets/app/dashboard.js
apply 返回的rollback_id被直接丢弃,UI 无回滚按钮,/api/adapters/rollback只能从带外脚本调用(前端仅展示 plan 的 rollback 描述文本,adapter-plan-rollback)。issue 明确要求「OPC App 提供每个宿主的…执行进度、验证结果和恢复入口」。建议:apply 成功后展示本次rollback_id并提供「回滚本次操作」按钮(配确认对话)。 -
apply 中途失败不留结构化恢复入口 —
opc_adapters.py(write_operation仅在成功路径执行)
失败时遗留的 backup 目录无任何记录指向它。具体地,Claude 部分安装清理失败(ROLLBACK_FAILED)后:插件已装、manifest 不存在 → probe 报drifted→ 任何 plan 都抛OWNERSHIP_CONFLICT,用户被卡死只能手工修。测试test_claude_partial_install_cleanup_failure_is_explicit_and_recoverable(名含 "recoverable")实际固定的正是这个无恢复路径的状态。违反 issue「部分失败有结构化结果、恢复入口」。建议:失败时也持久化 operation 记录(含 backup_ref 与错误码),或对 OPC 刚写入的内容允许带指纹的修复计划。
Minor
opc_adapters.py:466-469vs529-530:CodexAdapter.target_fingerprint读取的投影目录名是{version}-{hash16},而_codex_source实际创建的是{version},指纹永远返回projection_exists=False, hash=None—— 写入前指纹对 Codex 投影状态失明(apply 时_codex_source的tree_digest校验兜底,非安全洞,但属死代码且削弱PLAN_STATE_CHANGED覆盖)。建议统一命名。opc_adapters.py:1137-1158+1428-1434:Kimi update 不清理 canonical 包中已删除的 skill,manifest 被重写为当前集合后旧 skill 目录成为孤儿,之后卸载也不再清除,违反卸载完整性。AdapterManager.apply只在 pop plan 时持锁(opc_adapters.py:1382-1383),而服务是ThreadingHTTPServer:同一 host 的两个不同 plan 可并发 apply,状态指纹校验与首次写入之间存在 TOCTOU 窗口。建议 apply/rollback 全程持 per-host 锁。- nit:
opc_adapters.py:530-534Codex 投影缓存仅按source_version键控,同版本内容变化会在 apply 时抛PROJECTION_CONFLICT阻断合法更新。 - nit:
opc_adapters.py:722-725Claude_is_installed的name兜底匹配可能把其他 marketplace 的同名插件误判为已安装(仅影响状态显示,无实际破坏)。
值得确认的疑问
/api/adapters/rollback无独立确认令牌(仅 Origin+CSRF+rollback_id)。rollback 是恢复性写入且有 CSRF/Origin 防护,可辩护,但前端补回滚按钮时应配确认对话,请明确设计意图。- Codex/Claude 的漂移检测只覆盖安装态、不回读宿主缓存内容哈希(manifest 的
content_hash是 canonical 源哈希),与 Kimi 不对称,建议文档写明。
未做的事:三宿主真实安装态验收需要一次性环境与真实 CLI,本次未执行;官方 Plugin Validator 未复跑。
Summary
Safety boundaries
plugin marketplace remove; a stable App-owned marketplace is atomically swapped and refreshed, and uninstall/partial-install cleanup uses--keep-data.opc, Git history, manager preferences/rules/experience, optional Mem0 data, plugin data where supported, and other hostsCI cleanup-race repair
Integration with merged #27
origin/mainnormally; merge commit parents are Adapter head4903612and feat(app): 独立本地 OPC App 控制平面 #27 squash mergee88118eorigin/mainand the former feat(app): 独立本地 OPC App 控制平面 #27 branch had identical trees before resolving the nine same-content conflicts485c05326dafc4bf278d96cd55d06cda0106eab5exactly equals the pre-merge Adapter tree; no product content was dropped or rewrittenValidation
python scripts/validate_repo.py- PASSpython -m unittest discover -s tests -p test_*.py -v- 422 PASS, 6 expected skips at6bcf6f730161595116: Ubuntu/Python 3.10+3.12, Windows/Python 3.10+3.12, and both optional-Mem0 jobs PASS at6bcf6f7git statusunchanged and all 7 environment roots isolatedpython scripts/privacy_scan.py- PASS30154167815- all 4 core and both optional-Mem0 jobs PASS30161595066: synthetic Ubuntu/Windows PASS; fixed-ref release job expected SKIP for a PRDisposable real-host implementation evidence
This is Developer evidence, not independent QA:
2162841dd793d21671eccb7fe76fe9c3da6816adf447ba3890a4871b7e5f4e69; local marketplace install, real update, fresh discovery, uninstall, rollback, reinstall, config/plugin-data sentinels - PASSKIMI_CODE_HOMEwith loopback-only OpenAI-compatible stub; real/v1/chat/completionsrequest contained all seven exact canonical Skill identities; install, discovery, no-op update, uninstall, rollback, reinstall, sentinel preservation - PASSIndependent Reviewer installed-state evidence
Reviewed against exact commit
221fb4205e4e3ed821d9012dd92c42732428931e; Developer self-report was not used as QA evidence:APPDATA,LOCALAPPDATA, andPSModuleAnalysisCachePath, exact repository status remained clean. No raw paths, prompts, requests, credentials, tokens, or session identifiers are retained here.Prior independent Reviewer verdict at
221fb42: approve, with no findings. Merge-only head6bcf6f7preserves the exact tree of review-fix head4903612, adds merged #27 as its second parent, and has fresh green CI; the PR remains unmerged pending the responsible owner's decision. The reproducible privacy-safe commands are indocs/host-adapters.md.Depends on #27.
Closes #26