chore(core): remove the unused trusted hooks manager and implement unregisterSkillHooks - #11621
Conversation
…registerSkillHooks TrustedHooksManager was exported and tested but never constructed; project hooks are already gated by folder trust at load time and, for project skills, again at fire time. Remove it, its tests, the registry test mock, its legacy-filename allowlist entry and a comment that cited it. unregisterSkillHooks only logged and returned 0. It now removes the session hooks registered by the skill, identified by the skill root, and returns the count. A skill without a root returns 0 so another skill's hooks are never removed. Part of QwenLM#11610
|
✅ Qwen Triage finished — CI landed green on ✅ Qwen Triage 已完成 —— |
|
Thanks for the PR — this is the dead-code item from the #11610 plan, and it holds up under checking. Template ✓ — every section filled in, including the Tested-on table and a complete Chinese translation. Problem — real, and I verified it rather than taking the description's word for it. Worth being plain about what this is, though: neither half changes runtime behaviour today, so this is code-honesty work rather than a user-facing bug fix. That is how #11610 scoped it, so it is fine — it just should not be read as a fix for something users hit. The security framing also checks out, which mattered to me here since this is the hook-trust subsystem. Removing the trust manager removes no enforcement: folder trust still gates project-skill hooks at registration ( Direction — aligned. It is a pre-declared plan item on a maintainer-triaged P1 umbrella ("Remove the unused trusted hooks manager and implement Size — core paths are touched ( Approach — the deletion half is exactly the minimal change, right down to the lint allowlist entry and the now-dangling Risk — no high-risk path match. The revert-correlated set ( Moving on to code review. 🔍 中文说明感谢贡献!这是 #11610 计划里的死代码清理项,核查之后站得住。 模板 ✓ —— 各部分都填了,包括测试平台表格和完整的中文翻译。 问题 —— 真实存在,而且是我自己核过的,不是照抄描述。 不过要说清楚这是什么:两半改动今天都不改变运行时行为,所以这是"让代码诚实"的工作,不是用户会踩到的 bug 修复。#11610 本来就是这么定范围的,因此没问题——只是不要把它当成用户可见的修复来读。 安全层面的说法也成立,这点我很在意,因为这是 hook 信任子系统。删掉这个信任管理器不会削弱任何约束:文件夹信任仍然在注册时( 方向 —— 对齐。它是维护者已定级 P1 的总纲 issue 中预先声明的计划项("Remove the unused trusted hooks manager and implement 规模 —— 触及核心路径( 方案 —— 删除那一半就是最小改动,连 lint 白名单条目和 风险 —— 未命中高风险路径。与回滚相关的那组文件( 进入代码审查 🔍 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
Code reviewMy independent proposal (formed from the title and the "Why it's needed" section, before reading the diff): delete Versus the diff: the deletion half is identical to my proposal, including the two hygiene details I'd have had to remind myself about (the allowlist entry, the dangling What I checked, since this is core and the gate has to name every consumer:
Test quality. The new cases assert the three things that matter — the return count, the identity of the surviving entry ( Suggestions — neither blocks this:
No sequence diagram or changed-files table here — seven files, one deleted class, one filled-in stub, and no new runtime flow to trace. A diagram would be noise. Test evidenceStating plainly what this carries: static verification plus the PR's own CI. I read the base tree at The two claims that decide this PR are statically decidable, and I decided them by reading: "nothing constructs At the time of this fetch: 0 failures, 0 cancellations. Green includes For attribution, the author reports Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 中文说明代码审查我的独立方案(只看标题和"为什么需要"就想好的,没有先看 diff):删掉 与 diff 对比: 删除那一半和我的方案完全一致,连我本来还得提醒自己的两处细节(白名单条目、 因为这是核心路径,门禁必须点名每一个消费者,所以我逐项核过:
测试质量。 新用例断言了三件真正重要的事——返回计数、存活条目的身份( 建议——两条都不阻断本 PR:
这里没有放时序图或改动文件总览——七个文件、一个被删的类、一个被补全的桩函数,没有新的运行时流程需要追踪。放图只会是噪音。 测试证据明确说明本节承载什么:静态核查 + PR 自身的 CI。 我在隔离 worktree 中读取了 决定这个 PR 的两条主张是静态可判定的,我也是靠阅读判定的:"没有任何代码构造 抓取时刻的状态:0 失败、0 取消。 已通过包括 关于出处:作者报告在 Linux 上 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
|
Confidence: 4/5 — clean at every stage; the two open items are a scope question and a missing test, and neither blocks. Going back to my independent proposal: the deletion half is exactly what I would have written, and the implementation half is what I would have written if the function were being kept. The one place I'd have chosen differently is whether to keep it at all, and that is a maintainer's call, not a defect. What I like about this diff is what it did not do. The obvious over-engineered version of The diff is also minimal in the boring sense: no drive-by refactors, no formatting churn, no unrelated files. The two hygiene edits it does carry — the lint allowlist entry and the stale On the "did I verify the problem exists" question: yes, by search rather than by trusting the framing. Both claims reduce to repo-wide reference searches, and both came back empty. That is about as solid as a dead-code claim gets. On volume, since I should say it out loud: this author has 17 open PRs, 8 of them filed today against What I'd still like, non-blocking: a test that unregistering a skill leaves a non-skill rootless session hook alone — a CI status at the time of writing: 中文说明置信度:4/5 —— 各阶段都干净;两个待议项是一个范围问题和一个缺失的测试,都不构成阻断。 回到我的独立方案:删除那一半正是我会写的样子;实现那一半,如果决定保留这个函数,也正是我会写的样子。我唯一会做不同选择的地方是"要不要保留它",而这是维护者的判断,不是缺陷。 这个 diff 让我欣赏的是它没有做什么。 diff 在"无聊"的意义上也是最少的:没有顺手重构,没有格式化噪音,没有无关文件。它确实带上的两处清理——lint 白名单条目,以及跨了两个包、注释里那句失效的 关于"我是否核实了问题真的存在":核实了,靠搜索而不是靠相信 PR 的叙述。两条主张都归结为全仓库引用搜索,结果都是空。死代码类的主张能确证到这个程度,已经接近上限。 关于数量,我应该直说: 这位作者有 17 个开着的 PR,其中 8 个是今天针对 仍然希望补上、但不阻断: 一个测试,验证注销某个 skill 时非 skill 的无根会话 hook 保持不动——现成的例子就是 撰写时的 CI 状态: — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
Test Plan (not a blocker): Tests 90 passed — this review observed 30525, 25020, 2018, 1016, 1984, 535, 7319 passed.
中文说明
已审查——无阻断问题。 建议见行内评论。
Test Plan(非阻断):Tests 90 passed — this review observed 30525, 25020, 2018, 1016, 1984, 535, 7319 passed。
— qwen3.8-max via Qwen Code /review (v0.23.3)
Review follow-up. unregisterSkillHooks no longer requires the passed config to still list hooks, so a skill whose frontmatter lost its hooks block is still cleaned up. It collects matching hook ids before removing them instead of relying on the listing returning a copy. New cases cover a skill with hooks on two events and a config without hooks; each fails under the corresponding mutant. The supervisor store comment no longer names a single credential writer as if it were the only one. Part of QwenLM#11610
yiliang114
left a comment
There was a problem hiding this comment.
Reviewed at head 6bf38952 (7 files, +124/-286). No blocking finding. Approving.
For a PR that deletes a module and implements a previously missing function, the two things that can go wrong are a dangling reference and a mutation-during-iteration bug. I checked both rather than assuming.
The deletion is clean. git grep -n "trustedHooks\|TrustedHooks" at head across packages/, scripts/ and eslint.legacy-filenames.mjs returns zero hits, so removing trustedHooks.ts (-132) and its test (-129) leaves nothing importing, re-exporting or lint-allowlisting it. The eslint.legacy-filenames.mjs entry went with the file (-1), which is the detail usually missed.
unregisterSkillHooks collects before it mutates, and says why. registerSkillHooks.ts:174-196 early-returns 0 when !skill.skillRoot, then materialises the id list first — const hookIds = sessionHooksManager.getAllSessionHooks(sessionId).filter((entry) => entry.skillRoot === skill.skillRoot).map((entry) => entry.hookId); — with the comment "Collect the ids first: removeHook splices the stored per-event arrays." That is the correct order: filtering a live array while removeHook splices it would skip entries and silently leave hooks registered, which for an unregister function is the one failure that matters. It then counts actual removals rather than assumed ones (if (sessionHooksManager.removeHook(sessionId, hookId)) removed++) and returns the count, so a caller can tell "nothing was registered" from "removed N". Matching by skillRoot rather than by skill name is also the right key, since it is what registration is scoped by.
What I did not verify: the +96 test lines' assertions, and whether every caller of the old trusted-hooks manager had already stopped using it before this PR (the zero-grep result makes a dangling reference impossible, but not a caller that was silently relying on behaviour the manager provided). packages/cli/src/agent-view/supervisor-store.ts (+3/-2) is the only non-hooks production file touched and I did not read it. CI at head: green apart from review-pr. No review threads. This touches /packages/core/, so this is the code-owner vote.
中文说明
在 head 6bf38952 上评审(7 个文件,+124/-286)。没有阻塞项。 批准。删除类 PR 会出错的地方就两处:悬空引用,以及边遍历边修改。我都查了。删除是干净的:head 上对 trustedHooks|TrustedHooks 在 packages/、scripts/、eslint.legacy-filenames.mjs 下 grep 零命中,且 eslint.legacy-filenames.mjs 里的条目随文件一起删掉了(-1,这通常是被漏掉的细节)。unregisterSkillHooks 先收集再修改,并写明了原因:registerSkillHooks.ts:174-196 在 !skill.skillRoot 时返回 0,然后先把 id 物化成数组,注释是「先收集 id:removeHook 会对按事件存储的数组做 splice」——顺序是对的,因为一边 splice 一边过滤活数组会跳过条目、静默留下已注册的 hook,而对一个注销函数来说这正是唯一要紧的失败。它随后统计实际移除数而不是假定数,并返回该计数,所以调用方能区分「本来没注册」与「移除了 N 个」。按 skillRoot 而非技能名匹配也是正确的键。未验证:+96 行测试的断言,以及旧 manager 的每个调用方是否在本 PR 之前就已停用(零 grep 排除了悬空引用,但排除不了某个调用方静默依赖该 manager 提供的行为);supervisor-store.ts(+3/-2)是唯一被触碰的非 hooks 生产文件,我没有读。CI 除 review-pr 外全绿,无 review 线程。本 PR 触及 /packages/core/,所以这一票是 code-owner 票。
qwen-code-dev-bot
left a comment
There was a problem hiding this comment.
Approved at head 6bf38952.
Required CI is green — Test (ubuntu-latest, Node 22.x), Lint & Static, Integration Tests (no-AK, No Sandbox) and web-shell E2E Smoke completed successfully; only review-pr (the reviewer pipeline's own job) is outstanding. All three review threads are resolved, none a Critical.
I ran the package locally: packages/core src/hooks/*.test.ts at this head passes 24 files / 790 tests, including the new two-skill case that unregisters one root and checks the other skill's hook survives and the first can be registered again.
The replacement behavior is right for the reason that matters. Removal now keys on skillRoot alone, which is what makes it still work when the SkillConfig handed in no longer lists the hooks it registered earlier — keying on skill.hooks would leave exactly the case a caller needs unremovable. Registration does pass the root (registerSkillHooks.ts:102, { skillRoot, trustGated }), so the filter has real data to match against rather than being a well-written no-op, and a skill with no root returns 0 instead of deleting another skill's hooks. Collecting the ids before the removal loop is necessary rather than tidy: removeHook splices the stored per-event arrays, so filtering and deleting in one pass would skip entries.
The deletion side is clean: git grep -i trustedhooks at this head returns nothing anywhere in the repository, where the base carried it in five places — the module, its 129 lines of tests, a reference in hookRegistry.test.ts, the agent-view comment that cited it as a credential-writer precedent, and its entry in the legacy-filename lint allowlist. It was never re-exported from the hooks barrel, which is the proof that nothing depended on it. The comment that pointed at it now names writers that actually exist (supervisor-store.ts:697), which is the right resolution of the round-one note about that sentence going ungrammatical: the reference is replaced, not dropped.
No new Critical found. Two small things worth an answer rather than a round:
- Both new cases register one hook on one event per skill, so the tests would still pass against an implementation that only scans a single event or stops after the first match. The code is broader than that — it filters every entry from
getAllSessionHooks— but a case with one skill owning two events × two hooks is what pins "all of them", and it is cheap. - The
!skill.skillRootearly return is now the only guard, and its pair — the!skill.hookshalf — is gone rather than merged into the new path. That is intentional per the doc comment, and the no-root case is tested; worth keeping in mind if a caller ever starts passing a root-less skill expecting a no-op that is also reported as a no-op, since the return value is now the only signal either way.
|
两条非阻塞说明的处理:
|
|
Released in v0.23.4. |
What this PR does
Two pieces of hook code that looked functional but were not are fixed.
TrustedHooksManagerand its tests are removed: it was never constructed anywhere, and project hooks are already gated by folder trust when settings load and again when a project skill's hook fires.unregisterSkillHooksnow does what its name says. It removes the session hooks a skill registered, identified by the skill's root directory, and returns how many it removed. It previously logged a message and returned 0, and a skill without a root directory still returns 0 so another skill's hooks are never removed by mistake.Why it's needed
A trust manager that is exported, tested and documented by a comment elsewhere suggests hooks go through a per-hook approval step that does not exist, which misleads anyone auditing how project hooks are trusted.
unregisterSkillHooksis exported from the hooks module, so a caller that unloads a skill and relies on it would keep that skill's hooks firing for the rest of the session.Reviewer Test Plan
How to verify
cd packages/core && npx vitest run src/hooks/registerSkillHooks.test.ts src/hooks/hookRegistry.test.ts. The new case registers two skills, unregisters one, checks that only its hook is gone and the other skill's hook remains, and that the first skill can be registered again. A second new case checks that a skill without a root directory removes nothing.git grep -n -i -E "TrustedHooksManager|trustedHooks" -- .returns nothing; the deleted file's entry in the legacy-filename lint allowlist is removed too.Evidence (Before & After)
Neither change has user-visible behaviour to capture, so the evidence is the tests and the reference search.
The legacy-filename lint allowlist entry for the deleted file is removed as well. With
registerSkillHooks.tsreverted tomain, the new case that unregisters one of two skills fails, and the rootless-skill case passes because the old function also returned 0.Tested on
Environment (optional)
N/A, unit tests only.
Risk & Scope
unregisterSkillHooks.unregisterSkillHookswhen a skill body is unloaded, and theiffield on HTTP hooks, which is declared but not evaluated yet and stays because it is part of the daemon status and SDK types.TrustedHooksManageris no longer exported from its module file; it was not re-exported from the package entry.Linked Issues
Part of #11610
中文说明
这个 PR 做了什么
修正了两处看起来能用、实际不起作用的 hook 代码。删除
TrustedHooksManager及其测试:它从未在任何地方被构造,而项目 hook 已经在加载设置时、以及项目 skill 的 hook 触发时按文件夹信任状态做了门控。unregisterSkillHooks现在名副其实:按 skill 的根目录识别并移除该 skill 注册的会话 hook,返回移除的数量。它以前只打一条日志并返回 0;没有根目录的 skill 仍然返回 0,以免误删其它 skill 的 hook。为什么需要
一个被导出、有测试、还在别处注释里被提到的信任管理器,会让人以为 hook 要经过逐个审批,而这一步并不存在,这会误导审查项目 hook 信任机制的人。
unregisterSkillHooks从 hooks 模块导出,若有调用方在卸载 skill 时依赖它,该 skill 的 hook 会在会话剩余时间里继续触发。评审测试计划
如何验证
cd packages/core && npx vitest run src/hooks/registerSkillHooks.test.ts src/hooks/hookRegistry.test.ts。新用例注册两个 skill、注销其中一个,检查只有它的 hook 被移除、另一个 skill 的 hook 仍在,并且第一个 skill 可以再次注册。另一个新用例检查没有根目录的 skill 不会移除任何 hook。git grep -n -i -E "TrustedHooksManager|trustedHooks" -- .没有结果;已删除文件在旧文件名 lint 白名单里的条目也一并移除。证据(前后对比)
两处改动都没有用户可见的行为可截取,证据是测试和引用搜索:
registerSkillHooks.test.ts、hookRegistry.test.ts、sessionHooksManager.test.ts共 90 个通过;git grep搜不到TrustedHooksManager或trustedHooks。已删除文件在旧文件名 lint 白名单里的条目也一并移除。把registerSkillHooks.ts换回main版本后,注销两个 skill 之一的新用例失败;无根目录 skill 的用例仍通过,因为旧函数本来就返回 0。测试平台
仅在 Linux 上本地验证;macOS 和 Windows 未测试。
环境
不适用,仅单元测试。
风险与范围
unregisterSkillHooks。unregisterSkillHooks,以及 HTTP hook 上已声明但尚未求值的if字段;后者属于 daemon 状态和 SDK 类型的一部分,因此保留。TrustedHooksManager不再从其模块文件导出;它原本也没有从包入口再导出。关联 Issue
Part of #11610