feat(cli): Add workspace permissions rules API#5743
Conversation
Co-authored-by: Qwen-Coder <[email protected]>
|
Thanks for the PR! Template looks good ✓ On direction: directly aligned — issue #5677 tracks remote daemon and ACP parity gaps for slash-command-only workflows, and this PR closes the permission rules read/write gap for remote clients. CHANGELOG: no direct reference but the daemon/ACP parity area is clearly within scope. On approach: scope feels right. The extraction of permission logic into Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 方向:完全对齐——issue #5677 跟踪 slash-command-only 工作流在远程 daemon 和 ACP 上的能力缺口,本 PR 补齐了权限规则的远程读写能力。CHANGELOG 没有直接引用,但 daemon/ACP 对齐方向明确在项目范围内。 方案:范围合理。把权限逻辑抽到 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
Co-authored-by: Qwen-Coder <[email protected]>
Code ReviewIndependent proposal before reading the diff: extract the permission rule types/helpers from The PR follows this approach exactly. The extraction into No critical blockers found. No AGENTS.md violations. Minor observation (not a blocker): the Unit TestsBuild & TypecheckTmux TestingN/A — this is a daemon/ACP/SDK API change with no TUI or visual UI surface, as noted in the PR's Reviewer Test Plan. 中文说明代码审查独立方案:把权限规则类型和 helper 从 PR 完全按照这个方案执行。 没有发现关键阻断问题,没有 AGENTS.md 违规。 单元测试全部通过:workspace-permissions 18/18、acpAgent 139/139、server 513/513、DaemonClient 175/175。 构建与类型检查build 0 errors(15 个 pre-existing warning),typecheck 全部通过。 Tmux 测试N/A——这是 daemon/ACP/SDK API 变更,没有 TUI 表面。 — Qwen Code · qwen3.7-max |
|
Stepping back: this PR does exactly what I'd do if tasked with closing the permission rules parity gap. The extraction of The SDK helpers are a natural addition. The honest JSDoc caveat about TOCTOU on All 845 relevant tests pass. Build is clean. Typecheck is clean. The diff is focused — every changed file contributes to the stated goal, no drive-by refactors, no scope creep. The LGTM, ready to ship. ✅ 中文说明退一步看:这个 PR 完全符合补齐权限规则能力缺口的做法。把 SDK helper 是自然的补充。对 add/remove 的 TOCTOU 风险做了诚实的 JSDoc 说明——read-modify-write 不是原子操作就不该装作是。 845 个相关测试全部通过。构建干净,类型检查干净。diff 聚焦——每个改动文件都服务于目标,没有顺手重构,没有范围膨胀。 可以合并 ✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
— qwen3.7-max via Qwen Code /review
✅ Local runtime verification (real daemon, not just unit tests)I built the real esbuild bundle from this PR's head ( Environment
Results
Live examples:
|
| 层级 | 范围 | 结果 |
|---|---|---|
| 单元 | workspace-permissions.test.ts |
✅ 5/5 |
| 单元 | DaemonClient.test.ts |
✅ 169/169 |
| 单元 | acpAgent.test.ts(权限用例) |
✅ 5/5 |
| 集成(本 PR 自带 E2E) | 在 bundle 上跑 qwen-serve-routes.test.ts |
✅ 26/26 |
| 真实 daemon | GET /capabilities 的 features 中广告了 workspace_permissions |
✅ |
| 真实 daemon | 鉴权门:无 token 的 POST |
✅ 401 |
| 真实 daemon | GET 结构(v:1、user / workspace / merged / isTrusted、path) |
✅ |
| 真实 daemon | POST 对 workspace 和 user scope → trim/去重 + 落盘到 settings.json |
✅ |
| 真实 daemon | 校验:invalid_scope / invalid_rule_type / invalid_rules / invalid_rule |
✅ 各返回 400 |
| 真实 daemon | fallback(无 child)与 live‑ACP‑child 两条写入路径 | ✅ 均验证 |
真实示例:
POST /workspace/permissions {scope:workspace, ruleType:deny,
rules:[" Read(.env) ","Read(.env)","Bash(rm *)"]}
→ 200, workspace.deny = ["Read(.env)","Bash(rm *)"] # 已 trim + 去重
settings.json → {"permissions":{"deny":["Read(.env)","Bash(rm *)"]}}
# user(全局)scope 同样落盘到 ~/.qwen/settings.json ✅
# 当存在 live ACP child 时,写入会走 child(优先路径) ✅
⚠️ 运行时确认的一个健壮性问题
当 settings.json 中已存在一条非法规则时,read‑modify‑write 会被阻断。GET 会原样返回规则字符串(readPermissionRuleSet 不过滤非法项),但每次写入都会对整个列表重新校验,遇到第一条非法项就返回 400 invalid_rule。因此任何「读取 → 修改 → 写回」的客户端——包括本 PR 自己的 addWorkspacePermissionRule / removeWorkspacePermissionRule——一旦列表里有一条非法规则(例如括号不闭合;而本 PR 之前的 setRules 和交互式 /permissions 都未经 parseRule 校验就接受了这类规则),就再也无法新增/删除任何规则。
在 daemon 上的真实复现:
# settings.json 中已有 allow: ["Bash(git log)", "Bash(rm "] ("Bash(rm " 非法)
GET /workspace/permissions → workspace.allow = ["Bash(git log)","Bash(rm "] # 原样返回
POST /workspace/permissions {scope:workspace, ruleType:allow,
rules:["Bash(git log)","Bash(rm ","Bash(ls)"]} # 新增一条合法规则 "Bash(ls)"
→ 400 {"error":"Malformed permission rule: Bash(rm","code":"invalid_rule"}
GET → "Bash(ls)" 未被持久化
直接通过 SDK helper(真实 daemon):
addWorkspacePermissionRule("workspace","allow","Bash(pwd)") → 抛出 invalid_rule (被阻断)
addWorkspacePermissionRule("workspace","ask", "Bash(pwd)") → OK (干净列表——对照组)
修复建议:只校验本次传入的增量,或像 core 的 parseRules 那样对已存在的非法规则告警跳过、而不是整个请求拒绝。同时为 add/remove 的真正「追加/替换」路径补一个 SDK 测试(现有用例只覆盖了 no‑op 分支)即可拦住此问题。
结论
功能符合设计——capability 门控、鉴权、校验、持久化,以及 fallback 与 live‑ACP‑child 两条写入路径均正确。上面的 round‑trip 行为是合并前值得处理(或明确判定为本 PR 范围之外)的一个健壮性缺口。
- normalizePermissionRules: skip malformed rules instead of rejecting the entire request, fixing read-modify-write bricking (review QwenLM#1 & QwenLM#4) - Add tests for addWorkspacePermissionRule/removeWorkspacePermissionRule covering the actual POST path (review QwenLM#2) - Add JSDoc documenting non-atomic read-modify-write and TOCTOU risk on add/remove helpers (review QwenLM#3)
…ath tests - Add try/catch around buildPermissionSettings in persist-fallback POST path, matching GET handler error handling - Add tests for ACP non-SessionNotFoundError, persistSetting failure, and unknown client id rejection
The normalizePermissionRules change to skip (instead of reject) malformed rules requires updating the acpAgent test to expect successful resolution with the malformed rule filtered out.
🔬 Local build & verification report — PR #5743Verified at PR head TL;DR🔴 One blocking issue: the PR's own Reviewer Test Plan fails. What I ran (exactly the Reviewer Test Plan, plus a real-daemon e2e)
1) The failing test (will turn CI red)Root cause — const rule = item.trim();
if (parseRule(rule).invalid) {
continue; // ← silently skips. The test expects a throw + no setValue().
}The string 2) Real-daemon e2e — the user-facing impactI booted the actual built daemon (
So a client — including one persisting a security-sensitive 3) Suggested fix (verified — 657/657 pass)Reject malformed rules instead of skipping them: if (parseRule(rule).invalid) {
throw new PermissionRulesValidationError(
`Malformed permission rule: ${rule}`,
'invalid_rules',
);
}With this change all 657 CLI tests pass with no regressions: the REST route already maps Alternatively, if silent-drop is intended, then Everything else looks solidClean refactor (extracting Reproduction commands# isolated worktree at PR head + clean install
git worktree add --detach /tmp/pr5743 639834fcf6de4e97ca46d00ca923c7a7782b5e46
cd /tmp/pr5743 && npm ci
# Reviewer Test Plan
cd packages/cli && npx vitest run \
src/serve/routes/workspace-permissions.test.ts \
src/acp-integration/acpAgent.test.ts \
src/serve/server.test.ts # → 1 failed / 656 passed
cd ../sdk-typescript && npx vitest run test/unit/DaemonClient.test.ts # → 171 passed
cd ../.. && npm run build && npm run typecheck # → both pass
# real daemon
HOME=/tmp/h QWEN_HOME=/tmp/h/.qwen OPENAI_API_KEY=fake OPENAI_BASE_URL=http://127.0.0.1:9/v1 \
OPENAI_MODEL=fake QWEN_MODEL=fake \
node packages/cli/dist/index.js serve --port 0 --token T --hostname 127.0.0.1 --workspace /tmp/ws
# then: curl -H 'Authorization: Bearer T' http://127.0.0.1:<port>/workspace/permissions
# curl -X POST … -d '{"scope":"user","ruleType":"deny","rules":["ShellTool(git status"]}' → 200, rule dropped🇨🇳 中文版(完整对应)🔬 PR #5743 本地构建与验证报告验证环境:PR head 结论速览🔴 一个阻塞性问题:PR 自己的 Reviewer Test Plan 跑不过。 我跑了什么(严格按 Reviewer Test Plan,外加真实 daemon e2e)
1)失败的测试(会让 CI 变红)根因 —— const rule = item.trim();
if (parseRule(rule).invalid) {
continue; // ← 静默跳过;但测试期望「抛错 + 不调用 setValue」。
}字符串 2)真实 daemon e2e —— 对用户的实际影响我启动了真正构建出来的 daemon(
也就是说,一个客户端 —— 哪怕是在持久化一条安全敏感的 3)建议修复(已验证 —— 657/657 通过)把「跳过」改成「拒绝」: if (parseRule(rule).invalid) {
throw new PermissionRulesValidationError(
`Malformed permission rule: ${rule}`,
'invalid_rules',
);
}改完后 全部 657 个 CLI 测试通过、无回归:REST route 已经把 或者,如果静默丢弃是有意为之,那就必须改 其余部分都很扎实重构干净(把 Verification performed locally by the maintainer against a real build; CI checks were still pending at the time of writing. |
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
21bf8bb to
b4572c6
Compare
| res.status(200).json(liveResponse as QwenPermissionSettings); | ||
| return; | ||
| } | ||
|
|
There was a problem hiding this comment.
[Suggestion] The persist-fallback path (no live ACP child) writes permission rules to disk via persistSetting but never calls syncLivePermissionManagers. While there's no live ACP child for this specific workspace, other active sessions in the same daemon process may continue enforcing stale permission rules until a separate reload event fires.
Two separate concerns in this fallback path:
- After
persistSettingsucceeds,buildPermissionSettings(loadSettings(boundWorkspace))at line ~206 can throw and return 500internal_erroreven though the mutation the caller asked for actually landed on disk. Consider using a distinct error code (e.g.,response_build_error) since the write succeeded. broadcastSettingsChangedfires before the response is built — if the response-build throws, connected SSE clients receive asettings_changedevent while the HTTP caller sees a 500, creating a split-brain.
— qwen3.7-max via Qwen Code /review
| rule: string, | ||
| opts?: { clientId?: string }, | ||
| ): Promise<DaemonWorkspacePermissionsStatus> { | ||
| const normalizedRule = rule.trim(); |
There was a problem hiding this comment.
[Suggestion] Two concerns with the SDK add/remove helpers:
-
No client-side rule validation:
addWorkspacePermissionRuleandremoveWorkspacePermissionRuleonly callrule.trim()before making a GET+POST round-trip. A malformed rule (e.g.,"Bash(git *"with unbalanced paren) passes local checks, triggers two HTTP calls, and is only rejected server-side. Adding aparseRulecheck client-side would catch the most common mistake before any I/O. -
String equality for dedup:
currentRules.includes(normalizedRule)uses strict string comparison, butparseRuleresolves tool name aliases (e.g.,Bash→run_shell_command). Adding"Bash(git *)"when"run_shell_command(git *)"already exists won't be detected as a semantic duplicate, potentially accumulating equivalent rules in settings.
— qwen3.7-max via Qwen Code /review
|
Qwen Code review did not complete successfully: Qwen review timed out after 85 minutes. See workflow logs. |
wenshao
left a comment
There was a problem hiding this comment.
No new high-confidence issues found at this head. The earlier blockers in this thread are resolved here:
- Malformed-rule round-trip:
normalizePermissionRulesnow preserves pre-existing malformed rules (matched againstexistingRules) while still rejecting genuinely new malformed input. Read-modify-write through the SDKadd/removehelpers works again, and theacpAgent"rejects malformed permission rules" test passes. - Write boundary / exposure / limits: POST is restricted to
scope: "workspace", the REST response strips the scopepathfields, and rule limits are enforced (MAX_PERMISSION_RULES_COUNT=500,MAX_PERMISSION_RULE_LENGTH=512).
The live-ACP-child vs. persist-fallback split is consistent: the fallback broadcasts its already-normalized rules (equal to the reloaded value), while the live path reloads from disk precisely because its local list is not yet ACP-normalized — loadSettings reads fresh from disk each call, so the reload is correct cross-process. Capability gating (workspace_permissions) and route registration are both behind persistSettingAvailable, so they stay in sync.
Local verification on bddf771 (isolated worktree, fresh npm ci): npm run typecheck ✅, ESLint on changed files ✅, CLI route/ACP/server vitest ✅ 668/668, SDK DaemonClient vitest ✅ 173/173.
— claude-opus-4-8 via Qwen Code /qreview
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
🔬 Local runtime verification — real
|
| # | Check | Result |
|---|---|---|
| A1 | workspace_permissions advertised in GET /capabilities |
✅ |
| A2 | POST without bearer token → 401 |
✅ |
| A3 | GET shape (v:1, user/workspace/merged/isTrusted) and no path field leaked |
✅ |
| A4 | Validation: invalid_scope (user/system), invalid_rule_type, invalid_rules (non-array / too-many >500 / too-long >512) → 400 |
✅ |
| A5 | POST trim + dedup, persisted to settings.json ([" Read(.env) ","Read(.env)","Bash(rm *)"] → ["Read(.env)","Bash(rm *)"]) |
✅ |
| B1 | Round-trip (former blocker): pre-existing malformed rule returned verbatim by GET, preserved on write while a new valid rule is added |
✅ |
| C1–C5 | SDK workspacePermissions / setWorkspacePermissionRules / add / remove (POST paths + no-op short-circuits) against the live daemon |
✅ |
| C6 | SDK add succeeds despite a pre-existing malformed rule (the exact case that was blocked before) |
✅ |
The two earlier blockers (read-modify-write through add/remove, and the PR's own acpAgent malformed-rule test) are genuinely resolved at this head. 👍
🔴 The one bug: new malformed rule → 500 instead of 400 (message lost)
Repro (pristine PR code), no session running:
POST /workspace/permissions {"scope":"workspace","ruleType":"allow","rules":["Bash(git *"]}
→ HTTP 500 {"error":"Failed to update permission rules","code":"permission_update_failed"}The daemon writes the workspace through the live ACP child (the -32602 below comes from the child, proving that path is the active one — the 400-correct persist-fallback only runs when no child is reachable). Daemon stderr shows the message is already gone:
qwen serve: POST /workspace/permissions ACP error (key=permissions.allow, scope=workspace, …): [object Object]
Instrumented the caught error to get its exact shape:
typeof=object isErr=false ctor=Object code=-32602
json={"code":-32602,"message":"Invalid params: Malformed permission rule: Bash(git *"}
So the bridge delivers a plain object that does carry code:-32602 and the real message — but workspace-permissions.ts:25-31 gates on err instanceof Error:
if (code === -32602 && err instanceof Error) return err.message; // plain object ⇒ false ⇒ message droppedBecause the object is not an Error instance, getInvalidParamsMessage returns undefined, the handler falls through to the generic 500 permission_update_failed, and String(err) logs [object Object].
Why the unit test misses it — workspace-permissions.test.ts:497 ("POST maps live ACP invalid params to invalid_rules") mocks the child error as Object.assign(new Error(...), { code: -32602 }), i.e. a real Error instance, which passes the instanceof guard. The real cross-process JSON-RPC error is a plain object, so the test is green while the daemon is not.
Impact
- A client sending a malformed rule gets a
5xx(server fault / “retry”) for what is a4xxclient-input error — breaks client error-handling and retry logic. - The actionable
Malformed permission rule: <rule>message is replaced by a generic string — the client can’t tell the user which rule is bad. - The daemon logs
[object Object], hampering debugging. - Deterministic and on the primary (live-child) write path — not an edge case.
Verified fix (one line) — drop the instanceof Error requirement and read message off the object:
function getInvalidParamsMessage(err: unknown): string | undefined {
if (err && typeof err === 'object' && 'code' in err) {
const code = (err as { code?: unknown }).code;
- if (code === -32602 && err instanceof Error) return err.message;
+ const message = (err as { message?: unknown }).message;
+ if (code === -32602 && typeof message === 'string') return message;
}
return undefined;
}With this change the same request returns the intended result and the full E2E matrix is 19/19:
POST … {"rules":["Bash(git *"]}
→ HTTP 400 {"error":"Invalid params: Malformed permission rule: Bash(git *","code":"invalid_rules"}(Optional polish: the Invalid params: prefix from RequestError.invalidParams leaks into the HTTP body — worth stripping for a clean client message. A regression test should mock the child error as a plain {code:-32602, message} object, not an Error, so it actually covers this path.)
Verdict
Everything except the malformed-new-rule error mapping is verified working on a real daemon, including the round-trip that was the prior blocker. I’d apply the one-line fix above (and tighten the test) before merge; nothing else blocking.
中文版(合并参考)
我基于本 PR 最新 head(bddf771)构建了真实 bundle,并对 真实运行的 daemon 做了端到端验证——原生 HTTP(fetch + curl)以及构建出的 @qwen-code/sdk DaemonClient,在单元测试之外补充真实链路。
结论速览
功能整体扎实:capability 门控、鉴权、校验、持久化、trim/去重,以及之前作为 blocker 的非法规则 read-modify-write 往返都通过(自动化 E2E 17/19)。但真实 daemon 暴露了一个被单测掩盖的 bug:
🔴 提交一条全新的非法规则时返回
500 permission_update_failed,而不是预期的400 invalid_rules,且可定位问题的Malformed permission rule: …信息被丢弃。根因:getInvalidParamsMessage要求err instanceof Error,但 live ACP child 把 JSON-RPC 错误以普通对象{code:-32602, message}返回,于是该判断永远不成立。一行修复即可恢复 400 + 信息——已验证,全部 19/19 通过。
环境
- Head
bddf771,Linux,Nodev22.22.2,隔离 git worktree 中全新npm ci+npm run bundle(真实dist/cli.js+dist/chunks/serve-*.js,已确认workspace/permissions、permission_update_failed、registerWorkspacePermissionsRoutes等被编译进去)。 qwen serve --hostname 127.0.0.1 --port 41743 --workspace <tmp> --no-web,配置QWEN_SERVER_TOKEN与隔离QWEN_HOME,运行于 tmux;通过 HTTP 以及构建出的 SDKindex.mjs的new DaemonClient({ baseUrl, token })驱动。
验证通过的部分(真实运行)
| # | 检查 | 结果 |
|---|---|---|
| A1 | GET /capabilities 广告 workspace_permissions |
✅ |
| A2 | 无 token 的 POST → 401 |
✅ |
| A3 | GET 结构(v:1、user/workspace/merged/isTrusted),未泄漏 path 字段 |
✅ |
| A4 | 校验:invalid_scope(user/system)、invalid_rule_type、invalid_rules(非数组 / 超过 500 条 / 超过 512 字符)→ 400 |
✅ |
| A5 | POST trim + 去重并落盘到 settings.json |
✅ |
| B1 | 往返(此前的 blocker):GET 原样返回已存在的非法规则,写入时保留该非法规则并新增合法规则 |
✅ |
| C1–C5 | SDK workspacePermissions / setWorkspacePermissionRules / add / remove(POST 路径 + no-op 短路)对真实 daemon |
✅ |
| C6 | 存在已有非法规则时 SDK add 仍成功(正是此前被阻断的场景) |
✅ |
之前的两个 blocker(add/remove 的 read-modify-write、PR 自带的 acpAgent 非法规则测试)在当前 head 确实已修复。👍
🔴 唯一的 bug:新非法规则 → 500(且信息丢失),应为 400
复现(PR 原始代码,无 session):
POST /workspace/permissions {"scope":"workspace","ruleType":"allow","rules":["Bash(git *"]}
→ HTTP 500 {"error":"Failed to update permission rules","code":"permission_update_failed"}daemon 通过 live ACP child 写入工作区(下面的 -32602 正是来自 child,说明这条路径才是默认生效路径;返回 400 的 persist-fallback 仅在没有可用 child 时才走)。daemon stderr 显示信息已丢失:
qwen serve: POST /workspace/permissions ACP error (…): [object Object]
对捕获到的错误做埋点,得到其真实形态:
typeof=object isErr=false ctor=Object code=-32602
json={"code":-32602,"message":"Invalid params: Malformed permission rule: Bash(git *"}
即 bridge 返回的是普通对象,本身带有 code:-32602 和真实 message——但 workspace-permissions.ts:25-31 用 err instanceof Error 作门槛:
if (code === -32602 && err instanceof Error) return err.message; // 普通对象 ⇒ false ⇒ message 被丢弃由于该对象不是 Error 实例,getInvalidParamsMessage 返回 undefined,handler 落入通用的 500 permission_update_failed,String(err) 打出 [object Object]。
单测为何漏掉 —— workspace-permissions.test.ts:497("POST maps live ACP invalid params to invalid_rules")把 child 错误 mock 成 Object.assign(new Error(...), { code: -32602 }),是真实 Error 实例,能通过 instanceof。而真实跨进程 JSON-RPC 错误是普通对象,于是测试是绿的、daemon 却不对。
影响
- 客户端提交非法规则时拿到
5xx(服务器故障/“重试”),而这本应是4xx客户端输入错误——破坏客户端错误处理与重试逻辑。 - 可定位的
Malformed permission rule: <rule>信息被通用串替换——客户端无法告诉用户哪条规则有问题。 - daemon 日志打成
[object Object],不利排查。 - 确定性问题,且发生在主(live-child)写入路径,并非边角情况。
已验证的修复(一行) —— 去掉 instanceof Error 要求,直接从对象上读 message:
function getInvalidParamsMessage(err: unknown): string | undefined {
if (err && typeof err === 'object' && 'code' in err) {
const code = (err as { code?: unknown }).code;
- if (code === -32602 && err instanceof Error) return err.message;
+ const message = (err as { message?: unknown }).message;
+ if (code === -32602 && typeof message === 'string') return message;
}
return undefined;
}改完后同一请求返回预期结果,完整 E2E 矩阵 19/19:
POST … {"rules":["Bash(git *"]}
→ HTTP 400 {"error":"Invalid params: Malformed permission rule: Bash(git *","code":"invalid_rules"}(可选优化:RequestError.invalidParams 的 Invalid params: 前缀会泄漏到 HTTP body,建议去掉以得到干净的客户端信息。回归测试应把 child 错误 mock 成普通 {code:-32602, message} 对象而非 Error,才能真正覆盖该路径。)
结论
除“新非法规则的错误映射”外,其余在真实 daemon 上均验证通过,包括此前作为 blocker 的往返行为。建议合并前应用上面的一行修复(并加强测试),其余无阻断问题。
| persistSetting: async (...args) => { | ||
| await persistSetting(...args); | ||
| }, | ||
| broadcastSettingsChanged: (key, value, scope, clientId) => { |
There was a problem hiding this comment.
[Suggestion] The broadcastSettingsChanged closure (lines 2533–2539 here, lines 2550–2556 below) and parseAndValidateClientId (lines 2540–2541, 2558–2559) are duplicated verbatim between registerWorkspaceSettingsRoutes and registerWorkspacePermissionsRoutes. If one copy is modified without updating the other, the two routes silently diverge.
Consider extracting shared closures before both registrations:
const broadcastSettingsChanged = (key, value, scope, clientId) => {
bridge.publishWorkspaceEvent({ ... });
};
const parseAndValidateClientId = (req, res) =>
parseAndValidateWorkspaceClientId(req, res, bridge);— qwen3.7-max via Qwen Code /review
Co-authored-by: Qwen-Coder <[email protected]>
45b408a
qqqys
left a comment
There was a problem hiding this comment.
Critical permission-rule issues look resolved at the current head. I rechecked the live ACP invalid-params mapping, workspace-only write boundary, malformed-rule round trip, REST path exposure, and response/broadcast paths; no new critical blocker found.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No new high-confidence issues found at this head. Build passes, all 841 tests pass (668 CLI + 173 SDK), tsc and eslint clean. The prior review comments from earlier commits are stale at this SHA. Low-confidence observations (broadcast data source asymmetry in fallback path, duplicated adapter closures in server.ts, missing audit logging on success path) are left in the terminal review for human consideration.
— qwen3.7-max via Qwen Code /review
|
Qwen Code review did not complete successfully: Qwen review timed out after 85 minutes. See workflow logs. |
Co-authored-by: Qwen-Coder <[email protected]>
wenshao
left a comment
There was a problem hiding this comment.
R4 review — commit 45b408a2a (fix(cli): handle ACP invalid params errors)
No new high-confidence findings in this incremental commit. The three R3 suggestions addressed here are correctly fixed:
getInvalidParamsMessageinstanceof Error→ now duck-types{code, message}with atypeof message === 'string'guard, and strips the"Invalid params: "prefix. Handles the plain-objectErrorResponsethat the ACP SDK actually delivers.DaemonPermissionScopetype → narrowed from'user' | 'workspace'to'workspace', consistent with the route'sscope !== 'workspace'rejection.- Test gaps (response_build_error branches, non-string/empty rule validation, SDK POST failure propagation) → all covered with correct assertions.
Build passes, typecheck clean, 18/18 workspace-permissions tests pass, 175/175 DaemonClient tests pass.
— qwen3.7-max via Qwen Code /review
Co-authored-by: Qwen-Coder <[email protected]>
|
@qwen-code /triage |
✅ Local build + real-daemon verification — PR #5743Verified at commit Results
Real-daemon E2E (built
|
| 检查项 | 结果 |
|---|---|
npm run build(所有包,含 SDK 浏览器 bundle) |
✅ exit 0 —— 仅有 15 条既有的 VS Code companion lint warning,0 error |
npm run typecheck(6 个 workspace) |
✅ 干净,无 error |
CLI 套件:workspace-permissions.test + acpAgent.test + server.test |
✅ 670 通过 |
SDK 套件:DaemonClient.test |
✅ 175 通过 |
| 真实 daemon E2E(真实二进制) | ✅ 19 / 19 条断言 |
| 变异测试(破坏“拒绝非法规则”逻辑) | ✅ 测试套件能抓到 —— 有 1 条用例失败,随后已还原 |
真实 daemon E2E(构建出的 packages/cli/dist/index.js serve)
启动真实 daemon 并绑定到临时 workspace,端到端驱动 HTTP 接口:
- Capability ——
/capabilities正确广告workspace_permissions。 - GET —— 返回
{ v:1, user, workspace, merged, isTrusted }。 - POST
allow—— 响应体与磁盘上的.qwen/settings.json同时更新;merged正确反映规则。 - 按列替换 —— POST
deny不会覆盖已存的allow列表。 appliedVia(333c2424新增)—— 没有 live ACP child 时,写入正确报告appliedVia: "persist-fallback"。- 校验 ——
scope!=workspace→invalid_scope;非法ruleType→invalid_rule_type;非数组 / 非法规则 →invalid_rules(均为 HTTP 400)。 - 拒绝即原子 —— 被拒绝的非法 POST 不会破坏已存规则。
- 非法规则保留 —— 已存量的非法规则(
Bash(rm)可成功往返(以 trim 后的Bash(rm保留),而全新的非法规则仍会被invalid_rules拒绝。
说明(属确认行为,非缺陷)
- 写接口在 loopback 上也需要 bearer token。 loopback 上
GET免认证,但POST /workspace/permissions在未配置--token/QWEN_SERVER_TOKEN时返回401 token_required。这是预期的 mutate 写保护 —— 建议在 SDK/文档中点明,方便远程客户端知道写操作需要 token。 - 规则在往返时会被规范化(trim),与 route 测试自身的预期一致(
Bash(rm→Bash(rm)。
结论
在 333c2424 上,构建、typecheck、完整测试套件、变异测试以及真实端到端 daemon 行为全部通过。从构建与行为角度看,可以合并。(这是功能验证,不是设计/安全代码评审。)PR 目前显示 BLOCKED,看起来是 merge-queue / CI / 审批门禁,而非代码问题。
— verified with claude-opus-4-8 · local build + real-daemon E2E (Node v22.22.2, macOS)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
What this PR does
Adds a minimal remote daemon surface for persistent permission rules:
GET /workspace/permissionsreturns user, workspace, merged, and trust-state views, andPOST /workspace/permissionsreplaces one workspaceallow,ask, ordenylist. The REST route shares validation and response shaping with the existing ACP extension methods, preserves pre-existing malformed rules during read-modify-write, rejects newly submitted malformed rules withinvalid_rules, prefers a live ACP child for writes so active permission managers are synchronized, and falls back to daemon-side settings persistence when no child is running. The daemon now advertisesworkspace_permissionswhen settings persistence is available, and the TypeScript SDK exposes typed status/rule shapes plus get, set, add, and remove helpers.Why it's needed
Issue #5677 tracks remote daemon and ACP parity gaps for slash-command-only workflows.
/permissionscan currently manage persistent allow/ask/deny rules interactively, but remote clients cannot read or update the same settings because the generic workspace settings route intentionally does not expose permission arrays or live permission-manager synchronization. This gives ACP and daemon clients the same persistent rule management primitive without exposing the interactive dialog itself.Reviewer Test Plan
How to verify
Run
cd packages/cli && npx vitest run src/serve/routes/workspace-permissions.test.ts src/acp-integration/acpAgent.test.ts src/serve/server.test.tsand confirm the workspace permissions route, ACP permission ext methods, capability registry, and telemetry route registration tests pass. Runcd packages/sdk-typescript && npx vitest run test/unit/DaemonClient.test.tsand confirm the SDK helpers call the expected daemon routes and preserve no-op add/remove semantics. Runnpm run build && npm run typecheckand confirm the full workspace build, SDK browser bundle budget, and TypeScript checks pass.Evidence (Before & After)
N/A. This is a daemon, ACP, and SDK API change with no TUI or visual UI surface.
Tested on
Environment (optional)
Local verification used Node.js v26.0.0 and npm 11.12.1.
npm run build && npm run typecheckcompleted successfully; the build emitted existing non-fatal lint warnings in the VS Code companion package.Risk & Scope
/permissionsdialog over ACP, does not add ETag/versioning, does not manage session-only rules, and does not add new UI reducers or React hooks.workspace_permissionscapability is additive and only advertised when daemon settings persistence is available. New malformed permission rules are now rejected withinvalid_rulesinstead of being silently stored and never matching; already-stored malformed rules are preserved on read-modify-write so existing settings can still round-trip.Linked Issues
Refs #5677
中文说明
What this PR does
新增一个最小的远程 daemon 持久权限规则接口:
GET /workspace/permissions返回 user、workspace、merged 和 trust-state 视图,POST /workspace/permissions替换 workspace scope 下某一个allow、ask、deny列表。REST route 与现有 ACP ext method 共享校验和响应结构;read-modify-write 时会保留已存在的非法规则,同时对新提交的非法规则返回invalid_rules;写入时优先走 live ACP child,以同步活跃的 permission manager;没有 live child 时回退到 daemon 侧 settings 持久化。daemon 会在 settings persistence 可用时广告workspace_permissionscapability,TypeScript SDK 也新增了类型化状态、规则类型以及 get、set、add、remove helper。Why it's needed
Issue #5677 跟踪 slash-command-only 工作流在远程 daemon 和 ACP 上的能力缺口。当前
/permissions可以交互式管理持久 allow/ask/deny 规则,但远程客户端无法读取或更新同一批 settings,因为通用 workspace settings route 有意不暴露 permission arrays,也不负责 live permission-manager 同步。这个 PR 给 ACP 和 daemon 客户端提供同等的持久规则管理原语,但不暴露交互式 dialog 本身。Reviewer Test Plan
How to verify
运行
cd packages/cli && npx vitest run src/serve/routes/workspace-permissions.test.ts src/acp-integration/acpAgent.test.ts src/serve/server.test.ts,确认 workspace permissions route、ACP permission ext methods、capability registry 和 telemetry route registration 测试通过。运行cd packages/sdk-typescript && npx vitest run test/unit/DaemonClient.test.ts,确认 SDK helper 调用了预期 daemon route,并保留 add/remove no-op 语义。运行npm run build && npm run typecheck,确认完整 workspace build、SDK browser bundle budget 和 TypeScript 检查通过。Evidence (Before & After)
N/A。这是 daemon、ACP 和 SDK API 变更,没有 TUI 或可视 UI 表面。
Tested on
Environment (optional)
本地验证使用 Node.js v26.0.0 和 npm 11.12.1。
npm run build && npm run typecheck已成功完成;构建过程中 VS Code companion package 输出了既有的非阻塞 lint warning。Risk & Scope
/permissionsdialog 暴露给 ACP,不新增 ETag/versioning,不管理 session-only rules,也不新增 UI reducer 或 React hook。workspace_permissionscapability 是 additive,并且只在 daemon settings persistence 可用时广告。新提交的非法 permission rule 现在会以invalid_rules被拒绝,而不是被静默存储且永远无法匹配;已存量存在的非法规则会在 read-modify-write 时保留,因此既有 settings 仍可往返。Linked Issues
Refs #5677