feat(tokenless): bundle native qoder plugin - #3201
kongche-jbw wants to merge 1 commit into
Conversation
Package shared hooks and pinned platform binaries together so a detached Qoder cache can run without an ANOLISA installation. Keep existing adapter fallback behavior unchanged. Disable shell-dependent recovery in standalone mode because hook-local PATH does not reach the agent shell. Full model-session acceptance remains pending. Supplements: 1381793 ("fix(tokenless): support native qoder plugins")
Forrest-ly
left a comment
There was a problem hiding this comment.
总体评价
独立 Qoder 插件包的设计是合理的:bin/ 启动器 + native/<target>/ 平台二进制 + 自带 common/hooks,让插件缓存不再依赖 ANOLISA 安装;对既有安装路径零侵入(本地跑 tests/test-qoder-adapter-install.sh 17/17 通过)。用 TOKENLESS_DISABLE_SHELL_RECOVERY 主动降级"原文恢复能力"而不是让 Core 发出 Agent Shell 永远解析不到的 tokenless retrieve 标记,这个取舍是对的。主要问题集中在测试可移植性、Python 版本下限与项目既有约定不一致、共享 hook 契约变更未同步到既有测试与文档,以及打包脚本的失败处理;均不阻塞,但建议在解除 draft 前处理。
审查结论
approve(无阻塞项;本 PR 仍是 draft,作者自述的 model-session acceptance / Linux runtime checks / marketplace publication 尚未完成,此结论仅为代码审查意见,不代表可以合并)
详细意见
🔴 必须修改(阻塞合并)
无。
🟡 建议修改(不阻塞但推荐)
1. src/tokenless/tests/test_qoder_bundle.py 缺少 Python 版本守卫,在 python3 < 3.10 的机器上是 FAIL 而不是 SKIP(已复现)
run-hook.sh:30 探测的是环境里的 python3,而测试用 sys.executable 驱动打包(self.package()),两者可以不同,测试对前者没有任何控制。在本机(Anolis 8 系,python3 = 3.8.17)实测:
test_detached_bundle_uses_own_binaries_and_disables_recovery ... FAIL
AssertionError: {} != {'binary': 'bundled', 'recovery': '1'}
Ran 5 tests in 0.461s — FAILED (failures=1)
失败信息是 hook 的 fail-open 输出 {},与真实原因(宿主 python3 太旧)完全无关,排查成本很高;而 Makefile:292 已把这个测试挂进 make test-adapters。同目录的兄弟测试早有约定:tests/test_compress_response_hook.py:235 的 _needs_py39 = sys.version_info < (3, 9) + @unittest.skipIf(...)。建议:(a) 按同样约定加版本守卫;(b) 更彻底的做法是让 hook 子进程使用测试自己解析出的 ≥3.10 解释器(把其所在目录前置到子进程 PATH),使测试与宿主 python3 解耦。CI 侧无风险(tokenless-prebuilt-ci.yaml:42-44 固定 3.11)。
2. run-hook.sh:30-32 的 Python 3.10 下限与项目既有约定不一致,会让独立包在 3.9 宿主上整体静默失效
- 项目对共享 hook 声明的下限是 3.9:
tests/test_compress_response_hook.py:235、tests/test_compress_schema_hook.py:163(skip 文案即 "hook_utils requires Python 3.9+")。 - 实际代码更宽松:
hook_utils.py/compress_response_hook.py/rewrite_hook.py/compress_schema_hook.py都有from __future__ import annotations,也没有match、removeprefix、zip(strict=)等 3.9+/3.10+ 用法,本机 3.8.17 下四个模块ast.parse与 import 全部通过。 - 既有 ANOLISA 安装路径(同文件
:61-75的 CANDIDATES 分支、scripts/install.sh:43)只检查command -v python3,没有版本下限。
结果:同一份 hook 代码,在 python3 为 3.8/3.9 的宿主上,ANOLISA 安装照常压缩,独立包却每次 fail_open(所有 hook 只输出 {}),而 3.10+ 这个要求目前只出现在本 PR 新增的 packaging/qoder/README*.md、src/tokenless/README*.md 尾段和 framework-integration.md 里。建议要么把下限对齐到 3.9,要么在组件 README 的"运行依赖"章节明确说明独立包为何更严,并考虑同样约束安装路径,避免两条分发渠道行为分叉。附带一点::30-32 把"找不到 python3"和"python3 太旧"合并成同一句 Python 3.10+ is required,诊断信息不准确,建议区分。
3. hook_utils.py:211-216 改了共享契约,但既有测试 tests/test_compress_response_hook.py:421-428 没跟着更新
test_recovery_requires_bare_tokenless_on_path 在 shutil.which 返回路径时断言 assertTrue(...),而该函数现在多了一个环境变量条件,测试没有固定 TOKENLESS_DISABLE_SHELL_RECOVERY。一旦这个变量被导出(例如开发者正在调试独立包,或在装了独立包的 Qoder 会话里跑测试),这个既有测试就会莫名失败。建议把两个分支都包在 mock.patch.dict(os.environ, {"TOKENLESS_DISABLE_SHELL_RECOVERY": "0"}) 里,并补一个 "1" → False 的用例;新行为的覆盖目前只在 qoder 专属测试里(test_qoder_bundle.py:112-123),但改动落在所有 adapter 共用的模块上。
4. TOKENLESS_DISABLE_SHELL_RECOVERY 是跨 adapter 契约,却只写在 qoder 打包文档里
它在共享的 hook_utils.py:214 生效,因此凡是 import hook_utils 的 adapter 都会响应,包括 adapters/tokenless/hermes/__init__.py:514(Hermes 走的是自己的插件入口,不是 qoder 的 run-hook.sh)。项目已有权威的环境变量清单:docs/user-guide/{en,zh}/token-saving/tokenless/configuration-and-privacy.md 的 ## Environment variables → ### Adapter and diagnostic variables(该表已收录 TOKENLESS_AGENT_ID、TOKENLESS_TOOL_READY_SPEC、TOKENLESS_ENV_FIX_SCRIPT 等)。建议把这个变量补进中英两份表格,否则其他 adapter 维护者无从得知它的存在与语义。
5. run-hook.sh:23-47 给每次工具调用增加了可测量的固定开销,其中最大一笔花在一个硬关闭的空 hook 上
本机实测:bash bin/tokenless --check ≈ 6.9 ms/次(bash + 2 个 uname fork),python3 -c 版本探测 ≈ 24.3 ms/次,合计 ≈ 31 ms/次 hook 调用。按 hooks/hooks.json 的注册情况,非 Bash 工具调用触发 2 次(tool_ready_hook.sh + compress_response_hook.py)≈ 62 ms,Bash 工具调用触发 3 次 ≈ 93 ms。而 common/hooks/tool_ready_hook.sh:24-25 是硬关闭的(printf '%s\n' '{}'; exit 0),它不需要二进制也不需要 Python,却要付全额探测成本,且它在 PreToolUse 上 matcher 为空(匹配所有工具)。建议:在进入探测前对 tool_ready_hook.sh 直接短路返回 {};并把 --check 与版本探测合并成一个 bash 进程(版本探测本质只需要一次 python3 启动)。另外 --check 只验证了 native/<target>/{tokenless,rtk} 的存在与可执行位,这与启动器真正 exec 时 packaging/qoder/tokenless:12-17 的检查完全重复,信息增量为零;README 里"Missing runtime dependencies warn"实际语义是"缺文件时告警",措辞可以更准确。
6. run-hook.sh:25 用目录存在性判定 standalone 模式,判据偏弱
[ -d "${SCRIPT_DIR}/../bin" ] 命中的是"hooks 旁边有个 bin 目录",而不是"存在我们自己的启动器"。当前 ANOLISA 安装布局(.qoder-plugin/ commands/ hooks/ scripts/,见 tests/test-qoder-adapter-install.sh 对缓存布局的断言)不含 bin/,所以现在不会误判;但一旦插件缓存里出现任何空的或无关的 bin/,:27 会因启动器缺失而失败,进而所有 hook 永久静默 fail-open(压缩全关,只留一行 stderr),这类故障非常难定位。建议改成 [ -x "${PLUGIN_ROOT}/bin/tokenless" ],或以 bundle.json 作为 standalone 标记(它本来就是打包产物的一部分)。
7. run-hook.sh:43-46 内层 case 没有 *) 分支,注释声明的不变量只是"恰好成立"
:23-24 的注释写着独立包"must not fall back to another installed version when an asset is missing",但内层 case 只处理 *.py / *.sh,两个模式都不匹配时控制流会穿出 if 块,落到 :49-59 的 ANOLISA FHS 候选(含 /usr/local/share/anolisa/... 与 $HOME/.local/share/anolisa/...),正好违背该不变量。今天不会发生,仅因为 :18-21 的 basename 白名单保证了后缀;但白名单是将来最容易被扩的地方。建议补 *) echo "[tokenless] Unsupported bundled hook: ${SCRIPT}" >&2; fail_open ;;,让不变量由代码结构保证。
8. package.py:31 版本提取既不锚定 [workspace.package],也没有失败保护
re.search(r'^version = "([^"]+)"', ..., re.M)[1] 取的是 Cargo.toml 中第一个行首 version = "..."。今天它正好命中 [workspace.package](Cargo.toml:26-27),因为 [workspace.dependencies] 用的是内联写法;但只要将来在它之前出现任何一个行首 version = 条目,插件 manifest 就会被静默盖上错误版本,而且 version.txt 比对(:36)也会跟着用错的基准放行不匹配的二进制。同仓的 packaging/raw/verify-release.py:12-14 已经有锚定到 ^\[workspace\.package\] 的 WORKSPACE_VERSION,建议直接复用该模式(verify-release.py 整体依赖 gitignore 掉的 .anolisa/component.toml 和生成态 plugin.json,干净检出下跑不了,所以复用正则而非调用脚本是合理的)。另外 [1] 无保护:模式不匹配时抛的是 TypeError: 'NoneType' object is not subscriptable,而 verify-release.py:62 给的是明确的 ERROR: ... has no workspace package version。顺带一提,仓内现在同时存在三套版本提取规则(Makefile:42 的 grep '^version' | head -1、verify-release.py 的锚定式、以及这里),值得收敛。
9. package.py 的失败路径对发布方不友好:裸 traceback + 半成品目录会被 :32 的守卫永久卡住
:36 在 version.txt 缺失时抛 FileNotFoundError,:39-51 在架构校验失败时抛 CalledProcessError,都是原始 traceback;对照 packaging/raw/package.sh:50-52 的 require_file → ERROR: missing packaging input: ...,这里的可用性明显更差。更重要的是::53 的 copytree 一旦创建了输出目录,之后任何一步失败(:56 模板缺失、:58 共享 hooks 缺失、:80-81 LICENSE/NOTICE 缺失、磁盘写满)都会留下半成品目录,而重跑必然被 :32-33 的 "output already exists" 拒绝,发布方得手动删目录——这与 :25 的 docstring "Validate all inputs before creating a fresh plugin output directory" 不符(LICENSE/NOTICE/模板/共享 hooks 都没有前置校验)。建议:把这些输入按 package.sh 的方式在创建目录前逐项 require,并用"临时目录构建 + os.replace 落位"或 try/except → shutil.rmtree(args.output) 保证失败即无残留。
10. package.py:20 的 darwin-x64 与 packaging/raw/package.sh:45 的显式禁止相冲突
raw 打包对 macos-x86_64 是直接 die "Tokenless raw packages do not support macOS x86_64",而这里把它列为合法 target,README 只说"accepted for separately built assets; this does not imply a published Intel Mac release"。等于开了一条没有受支持构建来源的通道:verify-binaries.py 只校验 Mach-O CPU 类型,任何来路不明的 Intel 二进制都能通过。建议在官方 Intel 产物存在之前先从 TARGETS 去掉 darwin-x64,或在 README 明确写出这类二进制的来源与校验方式,并让两个打包入口的 target 矩阵保持一致(现在它们已经分叉,将来加分平台时很容易只改一边)。
11. 小问题(可一并处理)
run-hook.sh:26的PLUGIN_ROOT="$(cd ... && pwd -P)"少了CDPATH='',而同文件:12和packaging/qoder/tokenless:4都加了。当前因为路径是绝对的、CDPATH 不会被查询所以无实害,但与本文件既有防御性写法不一致,建议补齐。Makefile:292的python3 tests/test_qoder_bundle.py是test-adapters里唯一没有@echo "==> ..."前导行的步骤,建议加一行(例如@echo "==> Testing standalone qoder bundle...")保持输出可读性。packaging/qoder/tokenless:18的--check是启动器私有暗号(真实 CLI 没有该 flag,crates/tokenless-cli/src/main.rs的Commands里只有EnvCheck),目前无冲突;但一个"透明转发"的包装器私吞一个通用名字的 flag,将来真实 CLI 若新增--check就会被静默吞掉,建议换个不可能撞车的名字(如--tokenless-bundle-check)或用环境变量探测。
🟢 值得肯定
- fail-open 纪律贯彻到位:新增分支里每一处失败(启动器缺失、平台不支持、Python 过旧、bundled hook 缺失)都走
fail_open,不会因为打包问题阻塞 Qoder 的工具调用。 - 对既有安装路径零侵入:本地实跑
tests/test-qoder-adapter-install.sh→ 17 passed / 0 failed;hook_utils.py的改动在变量未设置时逐字节等价于原行为(None != "1"),所有现存 adapter 不受影响。 - 先校验后落盘:
package.py:32-51把版本与架构校验全部放在创建输出目录之前,测试也用assertFalse(self.output.exists())精确锁住了这个语义;self.output = self.root / "detached cache"用带空格的路径顺带覆盖了引号处理,很到位。 - 复用而非复制:架构校验直接调
packaging/raw/verify-binaries.py,没有另写一份 Mach-O/ELF 解析。 - 产物可复现:
bundle.json只含版本与实际落盘文件的 SHA-256,没有时间戳;shutil.copy2保留 mtime。 - 文档诚实:中英双语,明确写出
version.txt只是调用方提供的来源信息而非二进制身份证明、表格采样会被绕过、tokenless-stats斜杠命令缺失、不要同时启用两份副本、结构校验+mock 契约测试不能证明端到端节省——这些正是发布方最容易踩的坑。本地跑scripts/docs-lint.sh(命名 + en/zh 树 parity)与scripts/docs-link-check.py(相对链接)均通过。
ikunkun-sys
left a comment
There was a problem hiding this comment.
🤖 AI review by codex-runner · Agent: Reviewer · 判定:REQUEST_CHANGES
1. 变更概述
新增 Qoder 独立插件打包器,包含共享 hooks、平台二进制、启动器及校验清单;关闭依赖 Shell 原文恢复的压缩,移除依赖全局命令的 stats 入口,并补充测试和双语文档。审查 head:086ee07f。
2. 正确性
[P2] 独立包仍可能调用包外 RTK,违反运行时隔离约定。
启动器第 12–20 行仅检查执行权限并前置 PATH。若包内 RTK 存在,但因运行依赖缺失而无法成功执行 --version,Core 的解析逻辑会继续选择 ANOLISA 安装目录中的 RTK,并将其路径写入重写后的命令。
因此,故障时可能使用另一个版本执行工具,而非按声明保留原命令。需要确保独立模式禁止该回退,并覆盖“包内 RTK 不可运行、全局 RTK 可用”的场景。此项由源码调用链确认,未做真实二进制复现。
另已复现已有 review 的测试隔离问题:导出 TOKENLESS_DISABLE_SHELL_RECOVERY=1 后,既有恢复可用性测试断言失败,建议固定测试环境。
3. 风格与一致性
整体改动集中,复用了既有架构校验器。非阻塞建议:两份组件 README 将“独立插件分发”放在许可证章节下,应调整到安装或分发章节。
4. 风险与验证
主要风险是上述包内、包外运行时混用。关闭恢复能力的传递逻辑正确;命令重写使用带引号的 RTK 绝对路径。
本地通过:diff 空白检查、Bash 语法、Python AST、3 个恢复命令测试及恢复开关断言。完整打包测试需要写临时目录,本次只读环境未运行。CI 的 Tokenless 检查成功;真实模型会话及 Linux 运行验收仍待完成。未修改文件或提交 GitHub 反馈。
5. 结论
结论:REQUEST_CHANGES — 暂不建议合并;先修复独立模式的 RTK 回退隔离并补充针对性测试,再完成正文承诺的端到端验收。
Forrest-ly
left a comment
There was a problem hiding this comment.
Re-review · head 仍为
086ee07f(无新 commit)· 本轮为意见复核,不重读全量 diff。
上一轮我方结论 approve(2026-09-10),其后 ikunkun-sys 提交 CHANGES_REQUESTED(2026-09-11)。本轮任务:复核该外部意见是否成立、是否推翻上一轮结论。
按去重约定,本文不重复上一轮 5 条 🟡 与 6 条小问题,也不重复外部 review 已述内容;只给出对 [P2] 的实质性补充与结论裁定。
总体评价
外部审查者提出的 [P2]「独立包仍可能调用包外 RTK」成立,且我在本地做了真实复现(外部审查者自述仅由源码调用链确认、未做二进制复现)。复现同时暴露了第二个未被报告的触发条件,以及「现有测试在结构上不可能发现该问题」的原因。
上一轮我给出 approve,是因为没有把 rewrite_hook.py → main.rs → env_check.rs → entry.rs 这条 RTK 解析链走完;本轮走完后,新证据(尤其是 env_check_tests.rs:421 明确把该回退锁定为意图行为)改变了结论:bundle 不能依赖 PATH 前置来获得运行时隔离。因此结论由 approve 调整为 request changes,与 ikunkun-sys 一致。
需要强调的是:这不是本 PR 引入的 Rust 回归(PR 未改任何 Rust),而是本 PR 新增的隔离承诺与 Core 既有 RTK 解析策略之间的缺口 —— 因此修复应落在 bundle 层,正好在本 PR 的文件范围内,不需要扩大 scope。
审查结论
request changes(由上一轮 approve 调整;唯一阻塞项为下述 🔴 1 条)
详细意见
🔴 必须修改(阻塞合并)
1. [补充 ikunkun-sys review] 包内 RTK 不可用时,独立包会静默改用包外 RTK,并把其绝对路径写进 agent 实际执行的命令
调用链(逐跳确认,全部为已存在代码)
| 跳 | 位置 | 行为 |
|---|---|---|
| 1 | adapters/tokenless/qoder/hooks/run-hook.sh:34 |
前置 PATH="${PLUGIN_ROOT}/bin:${PATH}",exec rewrite_hook.py |
| 2 | packaging/qoder/tokenless:19-20 |
启动器前置 native/<target>,绝对路径 exec 包内 tokenless |
| 3 | crates/tokenless-cli/src/main.rs:547-550 |
PreTool 请求 → env_check::resolve_rtk_path() |
| 4 | crates/tokenless-cli/src/env_check.rs:610-619 |
command -v rtk → 命中包内 rtk,作为 path_candidate |
| 5 | env_check.rs:623-645 |
candidates = [包内 rtk] + binary_fallback_paths("rtk", home),find_map 跳过不满足条件的候选后继续往后找 |
| 6 | env_check.rs:521-567 |
后备列表含 ~/.local/bin/rtk、/usr/local/bin/rtk、/usr/bin/rtk、/usr/libexec/anolisa/tokenless/rtk、/usr/lib/anolisa/tokenless/rtk 等包外路径 |
| 7 | crates/tokenless-runtime/src/entry.rs:347 |
Command::new(rtk_path).arg("rewrite") —— 由包外 RTK 实际执行重写 |
| 8 | entry.rs:403 → entry.rs:589-604 |
shell_quote(&rtk_path...) —— 包外 RTK 的绝对路径被写入重写后的命令字符串,交给 Qoder 的 shell 执行 |
第 5 跳的跳过条件有三个(env_check.rs:627-643):is_executable_file 失败(含 is_trusted_path 拒绝)、--version 执行失败或非零退出、版本号无法匹配 VERSION_RE 或 < 0.35.0。三者任一成立都会回退到包外,且全程没有任何 stderr 诊断。
本地复现(外部审查未做的一步)
我在 env_check.rs:623 的 select_rtk_path 上加了一个临时单测(已 revert,未提交、未推送),模拟独立包目录结构 <plugin>/native/linux-x64/rtk + 包外 ~/.local/bin/rtk(0.43.0):
[EXPERIMENT-1 broken bundled rtk] -> Some("<home>/.local/bin/rtk")
[EXPERIMENT-2 old bundled rtk] -> Some("<home>/.local/bin/rtk")
test result: ok. 1 passed
-
触发条件 A(外部审查者提出的):包内 rtk 存在、有可执行位,但
--version非零退出(复现中模拟GLIBC_2.34 not found)→ 解析结果落到包外~/.local/bin/rtk。 -
触发条件 B(本轮新增,外部 review 未提及,且更容易发生):包内 rtk 能正常运行但版本
< 0.35.0→ 同样落到包外。而env_check.rs:643硬性要求>= 0.35.0,打包侧对此没有任何校验:packaging/qoder/package.py:36-37只把version.txt与Cargo.toml的 version 比对 —— 那是 tokenless 的版本,不是 rtk 的;且 README:15 自己说明version.txt是「调用方提供的来源信息,不是二进制身份证明」。package.py:39-51调用的packaging/raw/verify-binaries.py只校验 ELF/Mach-O 架构(其 docstring:63 明确「without executing cross-target code」),从不校验版本。package.py:69-73把 rtk 原样copy2进native/<target>/,只chmod 0o755。
也就是说:发布方投喂一个偏旧的 rtk,打包会成功、
bundle.json会记录它的 SHA-256、--check会通过,但运行时每次 PreTool 重写都静默改用别人家的 rtk。条件 B 不依赖任何宿主异常,纯粹是打包校验缺口。
为什么现有测试结构上不可能发现
tests/test_qoder_bundle.py:62-91 是本 PR 唯一涉及「包内二进制」的测试,但:
:67写入了 rtk 的 fixture(#!/bin/sh\necho rtk\n),却从未被任何断言使用 —— 该测试只check_output(['tokenless']),完全没有触及 RTK 解析。:74把HOME指向一个空目录empty-home。binary_fallback_paths(env_check.rs:531-543, 558-562)的 user 级候选全部由此派生,空 HOME ⇒ 包外竞争者被人为清空。- 结果:隔离性是在「不存在任何竞争 RTK」的环境里被断言的,而这恰恰与真正危险的配置相反 —— README:56 自己警告的「不要同时启用独立副本与 ANOLISA 副本」,正是包外 RTK 一定存在的场景。
顺带一提,:67 那个 fixture 输出 rtk 而不带版本号,按 VERSION_RE 规则本身就会被第 5 跳跳过 —— 测试里的包内 rtk 其实是个「不兼容候选」,只是因为 HOME 为空且没有真正调用 Core,才没暴露问题。
与本 PR 自己引入的契约冲突(3 处,均在本 PR 新增文件里)
run-hook.sh:23-24注释即设计不变量:"Standalone bundles own their dependencies and must not fall back to another installed version when an asset is missing." —— RTK 路径直接违反。packaging/qoder/README.md:42-43/README_zh.md:39:"Missing dependencies emit a diagnostic to stderr and pass through the original tool result." —— RTK 缺失依赖时既无诊断,也不保留原命令,而是改写后执行。packaging/qoder/README.md:44-45/README_zh.md:40-41:"It uses its own binary version even when another Tokenless is on PATH." —— 这句对 tokenless 本身成立(见下方 🟢),但读者会自然理解为「整个运行时都用包内版本」,而 rtk 不成立;且第 8 跳会把包外绝对路径显式写进命令,用户可在会话记录里直接看到陌生路径。
严重性校准(为什么是 P2,不是 P0/P1)
- 不构成提权面:包外候选必须通过
is_executable_file→is_trusted_path(env_check.rs:569-584+:57-138),要求文件与父目录属当前 uid 或 root、且非 world-writable,路径列表本身也是硬编码的。 - 真实危害是正确性与可诊断性:包内 core 与包外 rtk 版本错配导致行为不可预期;故障时无任何日志线索;而 README:56 描述的误配置会让它更易发生、更难定位。
- 残留的低概率风险:
rtk是个短名字,若宿主上存在同名但无关的程序且恰好输出 ≥ 0.35.0 的版本号,第 7-8 跳会让 agent 的 shell 以env ... /path/to/无关程序 <原命令>形式执行。概率很低(需同时满足 trusted path + 版本正则 + ≥0.35.0),但非零,且属于「静默」类故障。
修复方向:这是 pre-existing 且被测试锁定的意图行为,不要在 Rust 侧改
crates/tokenless-cli/src/tests/env_check_tests.rs:421-442 的 rtk_resolution_skips_incompatible_path_candidate 明确断言了「PATH 候选不兼容时回退到另一份已安装 RTK」,我本地跑过该测试(1 passed)。对 ANOLISA 安装路径而言这是正确的容错设计,不该动。所以修复必须落在 bundle 层 —— 也就是本 PR 新增的文件,scope 无需扩大。
推荐(按性价比排序):
- (a) 最小改动、纯 shell、完全在本 PR 范围内 —— 推荐:扩展启动器已有的
--check(packaging/qoder/tokenless:12-18)。它现在已经校验 rtk 的存在性与可执行位,只差可用性。把:18的早退改为顺带实跑一次版本探测,例如仅当版本 ≥ 0.35.0 才exit 0,否则输出诊断并exit 1。这样run-hook.sh:27-29现成的 fail-open 分支就会接管:输出{}、保留原命令 —— 正好等于 README:42-43 承诺的行为,同时也堵住触发条件 A 与 B。
注意配合上一轮 🟡 第 5 条的延迟问题:--check每次工具调用都会跑,建议只对rewrite_hook.py做 rtk 探测(它是唯一触发 RTK 解析的 hook,compress_response_hook.py/tool_ready_hook.sh不需要),避免给所有 hook 再加一次 exec 开销。 - (b) 打包侧补版本校验(Python,同样在 scope 内):让
package.py对 rtk 版本做校验。由于verify-binaries.py:63刻意不跨目标执行代码、README:16 又要求「不要在 macOS 上打包」,宿主 == 目标时可直接执行rtk --version断言 ≥ 0.35.0;跨目标时要求发布方额外提供rtk-version.txt(沿用现有version.txt的 provenance 模式),并同样断言。这样条件 B 在构建期就被拦住,而不是等到用户机器上静默降级。 - (c) 完整修复(需要 Rust 改动 + 版本 bump,超出本 PR 声明范围,建议单开 follow-up issue):给
select_rtk_path增加一个「严格/独立模式」开关(如TOKENLESS_RTK_NO_FALLBACK=1),由启动器导出,命中时把候选集限制为 PATH 解析结果、不回退。只有这个方案能同时覆盖第三个触发条件(is_trusted_path拒绝包内路径,例如插件缓存被解包成 world-writable 或以其他 uid 安装)。建议 (a)+(b) 先落地解阻塞,(c) 记为后续项。
需要补的测试(外部审查者的要求合理,且现有测试无法替代):
在 tests/test_qoder_bundle.py 增加「包内 RTK 不可运行 / 版本过低,且包外存在可用 RTK」的用例。关键是要真实布置包外竞争者(不要再复用 :74 的空 HOME),并断言 hook 走 fail-open 而非产出带包外路径的重写命令。若 (a) 落地,断言 --check 非零 + run-hook.sh 输出 {} 即可;若 (c) 落地,则应在 env_check_tests.rs 补 select_rtk_path 的严格模式用例。
🟡 建议修改(不阻塞但推荐)
上一轮 5 条 🟡 + 6 条小问题全部仍未处理(head 未变,符合预期),按去重约定不在此重复,请见 2026-09-10 那条 review。本轮只对其中一条做优先级说明:
- 上一轮 🟡 第 3 条(
hook_utils.py:211-216共享契约变更未同步tests/test_compress_response_hook.py:421-428)优先级上调,建议与本轮 🔴 一并修掉。
理由:(1) 已被两位审查者独立复现(我方 Anolis 8 / python3.8 环境,ikunkun-sys 导出TOKENLESS_DISABLE_SHELL_RECOVERY=1后断言失败),不再是推测;(2) 我在本轮再次确认:421-428仍只 mockshutil.which、未固定该环境变量,而:423-428的assertFalse/assertTrue两个分支现在都受:214的新条件支配;(3) 修复成本约 3 行(mock.patch.dict固定两个分支 + 补一个"1"→ False 的用例),却落在所有 adapter 共用的模块上,拖着不修的收益为零。
严重性仍属测试健壮性而非产品正确性,故不上升为 🔴,但建议不要单独留到下一轮。 - 另外补充一条本轮顺带确认的事实,供处理 🟡 第 1 条时参考:本机
python3为 3.8.17,python3 tests/test_qoder_bundle.py仍是 FAIL 而非 SKIP(AssertionError: {} != {'binary': 'bundled', 'recovery': '1'},Ran 5 tests, failures=1),与上一轮描述一致;而Makefile:292已把它挂进make test-adapters,CI 侧因固定 3.11 不受影响。
确认外部 review 的非阻塞项(不重复计为新发现)
- 「两份组件 README 把『独立插件分发』放在许可证章节下」—— 核实为真:
src/tokenless/README.md:993的## License下挂了:997的### Standalone Qoder distribution;README_zh.md:581的## 许可证下挂了:585的### Qoder 独立插件分发。分发说明作为许可证的子章节确实是结构错误,同意移到安装/分发章节。 - 「导出
TOKENLESS_DISABLE_SHELL_RECOVERY=1后既有测试失败」—— 与上一轮 🟡 第 3 条同一问题,见上。
🟢 值得肯定(本轮复核新增)
tokenless自身的隔离承诺是成立的,README:44-45 那句话对 tokenless 准确:本轮专门验证了这条链 ——hook_utils.resolve_binary(:392-415)优先shutil.which(name),仅在 PATH 查不到时才回退到 ANOLISA FHS 候选;而run-hook.sh:34已前置${PLUGIN_ROOT}/bin、run-hook.sh:27的--check又保证了bin/tokenless可用,启动器:20再以绝对路径 exec。所以隔离缺口仅限 rtk,不涉及主二进制 —— 这也说明修复面比看起来小。--check这个扩展点选得好:启动器:12-18已经在做「包内二进制存在性 + 可执行位」校验,run-hook.sh:27-29已经有对应的 fail-open 分支。补上「可用性 + 版本」只是把既有机制走完最后一步,不需要新增任何控制流 —— 上面 🔴 的方案 (a) 之所以是最小改动,正是因为这个骨架已经就位。- fail-open 主干在 rtk 完全缺失时是对的:
entry.rs:344的rtk_path.ok_or(RuntimeError::RtkUnavailable)?保证了「哪儿都找不到 rtk」时 PreTool 失败 →rewrite_hook.py:78-79skip()→ 原命令保留。缺口只在「包内不可用 + 包外可用」这个中间态,问题定位因此很清晰。 - 上一轮已肯定过的各项(fail-open 纪律、对既有安装路径零侵入、先校验后落盘、复用
verify-binaries.py、产物可复现、文档诚实)本轮复核仍然成立,不再重复。
Why
Installing the Qoder adapter directory alone loads its hooks but leaves the shared scripts and
native runtime outside the plugin cache. A clean machine can therefore install the plugin
successfully without obtaining compression.
What changed
Add a standalone bundle builder that packages shared hooks and caller-verified, version-matched
platform binaries. A plugin-local launcher selects the platform and keeps runtime resolution
inside the bundle; existing ANOLISA adapter installations keep their fallback behavior.
Standalone hooks disable shell-dependent Marker recovery because their private PATH is not
inherited by Qoder's shell. Table sampling that requires recovery is consequently bypassed.
The bundle omits the stats slash command that requires a global executable.
Related issue
no-issue: standalone distribution for the existing native Qoder adapter
User / Agent impact
Publishers can produce a plugin requiring Bash and Python 3.10+, without npm lifecycle scripts
or an installation Skill. Missing runtime dependencies warn and preserve original tool output.
Risk and compatibility
The builder checks architecture and supplied version metadata, but publishers must independently
verify binary provenance and checksums. Do not enable standalone and ANOLISA copies together.
No Rust implementation changes or version bump.
Validation
passed in an isolated HOME.
build logs and large JSON; plain text and recovery-dependent table sampling passed through.
Documentation and rollback
Update bilingual packaging instructions, component READMEs, quick starts and framework integration.
Disable or uninstall through Qoder's native plugin manager to roll back. This draft is reviewable;
keep it unmerged until end-to-end acceptance is complete.