feat(channels): add pairing approval management API - #8045
Conversation
|
Thanks for the PR! Template looks good ✓ Problem: this is a feature addition, not a bug fix — the Channel management UI can approve pairing requests but has no backend contract for listing or revoking previously approved senders. Part of #7209. The gap is real and the follow-up UI work depends on this slice. Direction: aligned. Additive workspace-scoped API for the daemon's channel management surface, consistent with the existing pairing-request routes. CHANGELOG has no direct reference but the channel management area is actively developed. Size: cross-package (channels, cli, sdk-typescript) but only 176 production logic lines — 14 in PairingStore, 86 in service + routes, 76 in SDK. Test lines: 178. Well within bounds. Approach: scope feels right. Each layer (store → service → routes → SDK → docs) gets exactly the additions it needs. No unrelated changes, no drive-by refactors. The Risk: no elevated risk signals. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:这是功能新增,不是 bug 修复——频道管理 UI 已能批准配对请求,但缺少列出或撤销已授权 sender 的后端契约。属于 #7209 的一部分,后续 UI 工作依赖此切片。 方向:对齐。为 daemon 频道管理面增加 workspace-scoped 的增量 API,与现有配对请求路由一致。 规模:跨包(channels、cli、sdk-typescript)但仅 176 行生产逻辑代码。测试 178 行。远在限制之内。 方案:范围合理。每一层(store → service → routes → SDK → docs)都恰好只加了所需的增量。无无关改动。 风险:无升级风险信号。 进入代码审查 🔍 — Qwen Code · qwen3.8-max-preview Reviewed at |
Code reviewIndependent proposal first: given "add workspace-scoped APIs for listing and revoking pairing approvals," I would add a The implementation follows existing patterns at every layer — no surprises:
No correctness issues, no security concerns, no convention violations. Testing
macOS and Windows tests are skipped (fork PR limitation). The Ubuntu suite and the Serve A/B job both pass. The Serve A/B result is particularly relevant — it exercises the daemon routes this PR adds. A maintainer also verified locally against a real daemon (see comments below). 中文说明代码审查独立方案:给定"添加 workspace-scoped 的配对授权管理 API",我的方案与 PR 完全一致——在 PairingStore 加 实现逐层遵循现有模式,无意外:
无正确性问题、无安全隐患、无规范违反。 测试Ubuntu 测试套件和 Serve A/B 均通过。macOS/Windows 因 fork PR 限制跳过。维护者已在本地对真实 daemon 进行了验证(见下方评论)。 — Qwen Code · qwen3.8-max-preview Reviewed at |
|
Confidence: 5/5 — clean, convention-following additive API; every layer does exactly what it needs to and nothing more. This is what a well-scoped backend slice looks like. The PR adds two endpoints, threads them through three layers, and stops. No abstractions that aren't already there, no scope creep, no "while I'm here" edits. The workspace isolation semantics are correct and tested — revoking in one workspace leaves the other (and the legacy baseline) untouched, which is the whole point of the scoping work in #7017. The maintainer's local verification against a real daemon (comments below) confirms the contract works end-to-end, and the Serve A/B CI job exercises the new routes on the base-vs-head comparison. Ubuntu suite is green; macOS/Windows skips are a fork CI limitation, not a PR concern. Would merge without hesitation. 中文说明置信度:5/5 — 干净、遵循规范的增量 API;每一层恰好只做所需的事。 这是一个范围良好的后端切片。PR 新增两个端点,贯穿三层,然后停止。没有多余的抽象,没有范围蔓延。Workspace 隔离语义正确且经过测试——在一个工作区撤销不影响另一个(也不影响 legacy 基线),这正是 #7017 scoping 工作的核心。 维护者已在本地对真实 daemon 验证了端到端契约,Serve A/B CI 在 base-vs-head 比较中执行了新路由。Ubuntu 套件通过;macOS/Windows 跳过是 fork CI 限制。 毫不犹豫可以合并。 — Qwen Code · qwen3.8-max-preview Reviewed at |
🩺 serve daemon A/BBuilt the PR base vs this PR head ✅ No response changes against the PR base across 4 scenario(s). — Qwen Code · serve A/B |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
中文说明
已审查。 建议见行内评论。 未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
— qwen3.8-max-preview via Qwen Code /review
| [ | ||
| 'DELETE', | ||
| 'http://daemon/workspaces/%2Ftmp%2Fwork%20space/channels/bot/pairing-approvals', | ||
| ], |
There was a problem hiding this comment.
[Suggestion] The WorkspaceDaemonClient test asserts the HTTP method and URL for the new revokeWorkspaceChannelPairingApproval call but never asserts the request body — unlike the primary DaemonClient test, which pins JSON.parse(calls[11]!.body!) to { senderId: 'sender/1' }. — Concrete cost: if body: request were accidentally dropped from the channelRequest call in WorkspaceDaemonClient.revokeWorkspaceChannelPairingApproval, the workspace-scoped DELETE would be sent with no body, the server's parsePairingSenderId would read body['senderId'] as undefined and return 400 invalid_channel_pairing_sender_id for every workspace-scoped revocation, and this test would still pass because it only checks calls.map(({ method, url }) => ...). Add a body assertion after the header checks (confirm the workspace DELETE call index), e.g.:
expect(JSON.parse(calls[6]!.body!)).toEqual({ senderId: 'sender-1' });中文说明
WorkspaceDaemonClient 的测试只断言了新 revokeWorkspaceChannelPairingApproval 调用的 HTTP 方法和 URL,却没有断言请求体——而主 DaemonClient 测试用 JSON.parse(calls[11]!.body!) 固定为 { senderId: 'sender/1' }。具体代价:如果 WorkspaceDaemonClient.revokeWorkspaceChannelPairingApproval 的 channelRequest 调用里意外漏掉 body: request,workspace-scoped 的 DELETE 将不带请求体发出,服务端 parsePairingSenderId 会把 body['senderId'] 读成 undefined,并对每一次 workspace-scoped 撤销返回 400 invalid_channel_pairing_sender_id,而本测试仍会通过,因为它只检查 calls.map(({ method, url }) => ...)。建议在 header 断言之后补一条 body 断言(确认 workspace DELETE 的调用索引):
expect(JSON.parse(calls[6]!.body!)).toEqual({ senderId: 'sender-1' });— qwen3.8-max-preview via Qwen Code /review
There was a problem hiding this comment.
已修复:commit dbe6d73121 固定了 qualified workspace 撤销请求的 DELETE body。验证通过:SDK public-surface 与 DaemonClient focused tests 314/314、仓库 build、SDK typecheck、Prettier、git diff --check。
| DaemonChannelPairingApprovalsSnapshot, | ||
| DaemonChannelPairingRevocationRequest, | ||
| DaemonChannelPairingRevocationResult, |
There was a problem hiding this comment.
[Suggestion] These three new types are exported from daemon/index.ts but are not re-exported from the top-level barrel packages/sdk-typescript/src/index.ts, breaking the pattern of the four pre-existing sibling pairing types (DaemonChannelPairingRequest, DaemonChannelPairingRequestsSnapshot, DaemonChannelPairingApprovalRequest, DaemonChannelPairingApprovalResult), which are re-exported there. — Concrete cost: a consumer of @qwen-code/sdk-typescript who calls client.workspaceChannelPairingApprovals('bot') and writes import type { DaemonChannelPairingApprovalsSnapshot } from '@qwen-code/sdk-typescript' gets a compile error, even though the four sibling types resolve from the same path. Add the three types to the re-export block in packages/sdk-typescript/src/index.ts:
type DaemonChannelPairingApprovalResult,
type DaemonChannelPairingApprovalsSnapshot,
type DaemonChannelPairingRevocationRequest,
type DaemonChannelPairingRevocationResult,中文说明
这三个新类型已从 daemon/index.ts 导出,但没有从顶层 barrel packages/sdk-typescript/src/index.ts 再导出,打破了四个既有同级 pairing 类型(DaemonChannelPairingRequest、DaemonChannelPairingRequestsSnapshot、DaemonChannelPairingApprovalRequest、DaemonChannelPairingApprovalResult)的模式——它们都在那里被再导出。具体代价:使用 @qwen-code/sdk-typescript 的消费者调用 client.workspaceChannelPairingApprovals('bot') 并写 import type { DaemonChannelPairingApprovalsSnapshot } from '@qwen-code/sdk-typescript' 时会编译报错,尽管四个同级类型都能从同一路径解析。建议在 packages/sdk-typescript/src/index.ts 的再导出块中补上这三个类型:
type DaemonChannelPairingApprovalResult,
type DaemonChannelPairingApprovalsSnapshot,
type DaemonChannelPairingRevocationRequest,
type DaemonChannelPairingRevocationResult,— qwen3.8-max-preview via Qwen Code /review
There was a problem hiding this comment.
已修复:commit dbe6d73121 从顶层 SDK entry 再导出 3 个 pairing approval/revocation 类型,并在 public-surface 测试中固定该契约。验证通过:SDK public-surface 与 DaemonClient focused tests 314/314、仓库 build、SDK typecheck、Prettier、git diff --check。
Local verification against a real daemon — merge referenceI built this locally and ran it end to end against a live Verdict: behaves as the description claims. 88/88 end-to-end checks pass on the PR head, and the driver demonstrably discriminates the change (14/60 at merge-base, 40/60 against a deliberately broken build). Three non-blocking notes below for the follow-up UI PR — none of them are defects introduced here. How I ran itThe daemon runs straight from this branch's TypeScript source (a loader that maps every workspace The important part is the oracle. Every pairing request is produced by the same production code an inbound DingTalk/WeCom/Feishu message runs through — What passed
Does the harness actually discriminate the change?
Design decisions I checked and agree with
Three non-blocking notes for the follow-up UI PR
Minor: the SDK surfaces the daemon error code only via EnvironmentLinux (Debian 13, kernel 6.12), Node 22.22. Daemon run from source via tsx; three daemons used (PR head with token, merge-base with token, PR head token-less). No live IM provider involved — the pairing/allowlist path is entirely local, and the platform adapters are not on it. 中文说明基于真实 daemon 的本地验证 —— 合并参考我在本地把这个 PR 完整跑起来了,不只是跑单元测试,而是对着一个真实运行的 结论:行为与 PR 描述一致。 PR head 上 88/88 项端到端检查全部通过,并且这套 driver 确实能区分该改动(merge-base 上 14/60,故意打坏的构建上 40/60)。下面有三条不阻塞合并的说明,供后续 UI PR 参考——都不是本 PR 引入的缺陷。 怎么跑的daemon 直接从这个分支的 TypeScript 源码启动(用一个 loader 把每个 workspace 包的 关键在于 oracle。每一个配对请求都由真实入站消息(钉钉/企业微信/飞书)所走的同一份生产代码产生—— 通过了哪些
这套验证真的能区分改动吗
我确认并认可的几个设计取舍
三条不阻塞合并的说明(供后续 UI PR)
小点:SDK 只通过 环境Linux(Debian 13,内核 6.12),Node 22.22。daemon 通过 tsx 从源码运行;共用到三个 daemon(带 token 的 PR head、带 token 的 merge-base、无 token 的 PR head)。全程没有涉及真实 IM 服务——配对/allowlist 路径完全是本地的,平台适配器不在这条链路上。 🤖 Generated with Claude Code — Claude Opus 5 (1M context) |
Re-export the new approval and revocation types from the public SDK entry, and pin the qualified workspace DELETE request body in regression coverage.
Local verification — behavior confirmed on a real daemon, recommend mergeI built this PR locally and verified it against a live Result: the contract behaves exactly as documented. 50/50 live checks pass, plus 4,092 automated tests, and a 5-mutant matrix confirms the harness is not vacuous. No blocking issues found. Verified at head Why a live run mattered hereThe PR's own tests assert that the allowlist file changes. What they cannot assert is the thing that actually matters: the channel worker is a separate OS process that enforces the pairing gate. A revocation issued through the daemon API is only real if that worker starts refusing the sender afterwards. So the harness drives real inbound messages through a real mock IM server and uses the worker's own reply as the oracle — a pairing-code prompt means refused, the agent's answer means admitted. That end-to-end loop is confirmed: approve → sender reaches the agent → revoke via Live E2E — 50/50Topology: one daemon registering three workspaces serves the API under test; a second daemon hosts the enforcing worker. They share
Point-by-point against your Reviewer Test Plan:
Mutation matrix — 5/5 killedEach mutant was applied to the built artifact the live daemon actually loads, then the full 50-check E2E was re-run. No survivors, so no check is vacuous. M3 is the most informative: reverting Automated suites
One caveat on test-plan step 3 — not a defect in this PRWhile building the harness I hit two pre-existing daemon constraints that make the "same channel name in two workspaces" scenario unreachable with two simultaneously-running workers:
This does not weaken the PR. The pairing routes only require the channel to be configured, so both workspaces' approvals remain fully addressable from one daemon — verified live (C17, C20, C45) — and M3 proves the scoping is load-bearing for exactly that path, which is what the follow-up UI PR will use. It only means the scenario has to be verified as I did (sequential worker handover, C47–C49) rather than with two live workers. Worth knowing when writing the UI PR's test plan. Minor observations (non-blocking, no change requested)
中文说明本地验证 —— 已在真实 daemon 上确认行为,建议合并我在本地构建了本 PR,并针对运行真实 channel worker 的真实 结论:契约行为与文档完全一致。50/50 项实时检查通过,另有 4,092 个自动化测试通过,5 个变异体全部被杀死,证明验证harness 非空洞。未发现阻塞问题。 验证于 head 为什么必须做实时验证本 PR 自带的测试断言的是 allowlist文件发生了变化。但它们无法断言真正关键的一点:channel worker 是一个独立的操作系统进程,配对门禁由它执行。通过 daemon API 发出的撤销,只有在该 worker 随后真的开始拒绝该 sender 时才算真实生效。因此本 harness 通过真实的 mock IM server 推送真实入站消息,并以 worker 自己的回复作为判据 —— 回复配对码提示即为拒绝,回复 agent 答案即为放行。 该端到端闭环已确认:批准 → sender 可达 agent → 通过 实时 E2E —— 50/50拓扑:一个注册了三个 workspace 的 daemon 提供被测 API;第二个 daemon 承载执行门禁的 worker。二者共享
逐条对照你的 Reviewer 测试计划:
变异矩阵 —— 5/5 被杀死每个变异体都施加于实时 daemon 真正加载的构建产物,然后重跑完整的 50 项 E2E。无幸存者,说明没有任何一项检查是空洞的。 M3 信息量最大:将 自动化测试
关于测试计划第 3 条的一点说明 —— 并非本 PR 的缺陷在搭建 harness 时我遇到了两个既有的 daemon 约束,它们使得"两个 workspace 同名频道"这一场景无法以两个同时运行的 worker 复现:
这并不削弱本 PR。配对路由只要求频道处于已配置状态,因此两个 workspace 的授权都可以从同一个 daemon 完整访问 —— 已实时验证(C17、C20、C45)—— 且 M3 证明 scoping 对这条路径确实承载语义,而后续 UI PR 走的正是这条路径。它只意味着该场景需要像我这样验证(顺序交接 worker,C47–C49),而非用两个同时运行的 worker。写 UI PR 的测试计划时值得留意。 次要观察(不阻塞,无需修改)
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
中文说明
已审查。 未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
— qwen3.8-max-preview via Qwen Code /review
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
Released in v0.21.2. |






What this PR does
This PR adds workspace-scoped APIs for listing senders approved through pairing mode and revoking one approval. It exposes the same contract for the primary workspace and an explicitly selected workspace, and adds matching TypeScript SDK helpers.
Revocation updates only the selected workspace's pairing allowlist. It never falls back to another runtime or mutates the legacy process-global allowlist.
Why it's needed
The Channel management UI can already review and approve pending pairing requests, but it has no supported backend contract for showing previously approved senders or removing their access. This API is the backend slice required for a follow-up UI PR.
Reviewer Test Plan
How to verify
404 channel_pairing_approval_not_found.Evidence (Before & After)
N/A — this is a daemon and SDK contract with no UI changes.
Automated verification passed locally: 23 pairing-store tests, 43 daemon service/route tests, 2 focused SDK route tests, full build, full typecheck, and full lint.
Tested on
Environment (optional)
Node.js 22 workspace install; daemon route tests used Supertest. No live IM provider was required for this backend contract.
Risk & Scope
Linked Issues
Part of #7209.
中文说明
本 PR 做了什么
本 PR 新增了 workspace-scoped 的配对授权接口,用于列出通过配对模式获准的 sender,以及撤销某个 sender 的授权。主工作区和显式指定的工作区使用同一套契约,同时补齐对应的 TypeScript SDK helper。
撤销只更新所选工作区的配对 allowlist,不会回退到其他 runtime,也不会修改进程级 legacy 全局 allowlist。
为什么需要
频道管理 UI 已经能够查看并批准待处理的配对请求,但还没有受支持的后端契约来展示历史已授权 sender 或移除其访问权限。这个 API 是后续 UI PR 所需的后端切片。
Reviewer 测试计划
如何验证
404 channel_pairing_approval_not_found。前后证据
N/A——这是 daemon 和 SDK 契约变更,没有 UI 改动。
本地自动化验证已通过:23 个 pairing-store 测试、43 个 daemon service/route 测试、2 个聚焦 SDK route 测试,以及完整 build、typecheck 和 lint。
测试平台
环境
Node.js 22 workspace 安装;daemon route 测试使用 Supertest。本后端契约不需要真实 IM provider。
风险与范围
关联 Issue
#7209 的一部分。