Skip to content

feat(cli): Add workspace permissions rules API#5743

Merged
wenshao merged 11 commits into
QwenLM:mainfrom
doudouOUC:codex/workspace-permissions-rules
Jun 24, 2026
Merged

feat(cli): Add workspace permissions rules API#5743
wenshao merged 11 commits into
QwenLM:mainfrom
doudouOUC:codex/workspace-permissions-rules

Conversation

@doudouOUC

@doudouOUC doudouOUC commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds a minimal remote daemon surface for persistent permission rules: GET /workspace/permissions returns user, workspace, merged, and trust-state views, and POST /workspace/permissions replaces one workspace allow, ask, or deny list. 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 with invalid_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 advertises workspace_permissions when 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. /permissions can 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.ts and confirm the workspace permissions route, ACP permission ext methods, capability registry, and telemetry route registration tests pass. Run cd packages/sdk-typescript && npx vitest run test/unit/DaemonClient.test.ts and confirm the SDK helpers call the expected daemon routes and preserve no-op add/remove semantics. Run npm run build && npm run typecheck and 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

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

Local verification used Node.js v26.0.0 and npm 11.12.1. npm run build && npm run typecheck completed successfully; the build emitted existing non-fatal lint warnings in the VS Code companion package.

Risk & Scope

  • Main risk or tradeoff: The POST route is whole-list replacement for one workspace rule type, matching the minimal persistent settings model; SDK add/remove helpers are read-modify-write conveniences and do not provide concurrency control.
  • Not validated / out of scope: This does not expose the interactive /permissions dialog over ACP, does not add ETag/versioning, does not manage session-only rules, and does not add new UI reducers or React hooks.
  • Breaking changes / migration notes: The new workspace_permissions capability is additive and only advertised when daemon settings persistence is available. New malformed permission rules are now rejected with invalid_rules instead 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 下某一个 allowaskdeny 列表。REST route 与现有 ACP ext method 共享校验和响应结构;read-modify-write 时会保留已存在的非法规则,同时对新提交的非法规则返回 invalid_rules;写入时优先走 live ACP child,以同步活跃的 permission manager;没有 live child 时回退到 daemon 侧 settings 持久化。daemon 会在 settings persistence 可用时广告 workspace_permissions capability,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

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

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

  • Main risk or tradeoff: POST route 对 workspace 下单个 rule type 执行整列替换,符合最小持久 settings 模型;SDK add/remove helper 是 read-modify-write 便利封装,不提供并发控制。
  • Not validated / out of scope: 本 PR 不把交互式 /permissions dialog 暴露给 ACP,不新增 ETag/versioning,不管理 session-only rules,也不新增 UI reducer 或 React hook。
  • Breaking changes / migration notes: 新的 workspace_permissions capability 是 additive,并且只在 daemon settings persistence 可用时广告。新提交的非法 permission rule 现在会以 invalid_rules 被拒绝,而不是被静默存储且永远无法匹配;已存量存在的非法规则会在 read-modify-write 时保留,因此既有 settings 仍可往返。

Linked Issues

Refs #5677

@doudouOUC
doudouOUC marked this pull request as ready for review June 23, 2026 05:07
Copilot AI review requested due to automatic review settings June 23, 2026 05:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

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 permission-settings.ts enables clean reuse between the ACP ext methods and the new REST route. The SDK helpers (get/set/add/remove) with honest TOCTOU caveats are a natural addition. The 773-line route test file is thorough — it covers the live-child path, the persist-fallback path, malformed rule preservation, and error cases. No unrelated changes or drive-by refactors spotted.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

方向:完全对齐——issue #5677 跟踪 slash-command-only 工作流在远程 daemon 和 ACP 上的能力缺口,本 PR 补齐了权限规则的远程读写能力。CHANGELOG 没有直接引用,但 daemon/ACP 对齐方向明确在项目范围内。

方案:范围合理。把权限逻辑抽到 permission-settings.ts 让 ACP ext method 和新 REST route 可以干净地复用。SDK helper(get/set/add/remove)诚实标注了 TOCTOU 风险,是自然的补充。773 行 route 测试覆盖了 live-child 路径、persist-fallback 路径、非法规则保留和各种错误情况。没有发现无关改动或顺手重构。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal before reading the diff: extract the permission rule types/helpers from acpAgent.ts into a shared module, add GET /workspace/permissions and POST /workspace/permissions REST routes mirroring the existing workspace-settings route pattern, register a workspace_permissions capability gated on persistSettingAvailable, and add typed SDK client methods.

The PR follows this approach exactly. The extraction into permission-settings.ts is clean — readPermissionRuleSet, normalizePermissionRules, buildPermissionSettings are reused by both the ACP ext methods and the new REST route. The normalizePermissionRules function correctly handles the malformed-rule preservation requirement (existing malformed rules round-trip, new malformed rules are rejected). The REST route properly prefers a live ACP child for writes and falls back to daemon-side persistence when no child is running.

No critical blockers found. No AGENTS.md violations.

Minor observation (not a blocker): the persistSetting type in ServeAppDeps was broadened from Promise<void> to Promise<void | LoadedSettings> to support the permissions route's response optimization. The existing settings route wraps it to discard the return — a clean compatibility approach.

Unit Tests

$ cd packages/cli && npx vitest run src/serve/routes/workspace-permissions.test.ts
 ✓ src/serve/routes/workspace-permissions.test.ts (18 tests) 150ms
 Test Files  1 passed (1)
      Tests  18 passed (18)

$ cd packages/cli && npx vitest run src/acp-integration/acpAgent.test.ts
 ✓ src/acp-integration/acpAgent.test.ts (139 tests) 6474ms
 Test Files  1 passed (1)
      Tests  139 passed (139)

$ cd packages/cli && npx vitest run src/serve/server.test.ts
 ✓ src/serve/server.test.ts (513 tests) 10526ms
 Test Files  1 passed (1)
      Tests  513 passed (513)

$ cd packages/sdk-typescript && npx vitest run test/unit/DaemonClient.test.ts
 ✓ test/unit/DaemonClient.test.ts (175 tests) 1828ms
 Test Files  1 passed (1)
      Tests  175 passed (175)

Build & Typecheck

$ npm run build
✖ 15 problems (0 errors, 15 warnings)  [pre-existing vscode-ide-companion lint warnings]

$ npm run typecheck
> tsc --noEmit  [all packages pass]

Tmux Testing

N/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 从 acpAgent.ts 抽到共享模块,新增 GET/POST /workspace/permissions REST route,复用 workspace-settings 的路由模式,注册一个受 persistSettingAvailable 控制的 workspace_permissions capability,并新增类型化的 SDK client 方法。

PR 完全按照这个方案执行。permission-settings.ts 的抽取很干净——readPermissionRuleSetnormalizePermissionRulesbuildPermissionSettings 被 ACP ext method 和新 REST route 共同复用。REST route 优先通过 live ACP child 写入,没有 child 时回退到 daemon 侧持久化。

没有发现关键阻断问题,没有 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

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Stepping back: this PR does exactly what I'd do if tasked with closing the permission rules parity gap. The extraction of permission-settings.ts from the 65-line inline block in acpAgent.ts is the right move — it enables the REST route to share validation and response shaping without duplicating logic. The REST route's dual-path write strategy (live ACP child first, persist fallback second) is well-thought-out and matches how the daemon already handles settings mutations.

The SDK helpers are a natural addition. The honest JSDoc caveat about TOCTOU on add/remove is the right call — no point pretending a read-modify-write is atomic when it isn't.

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 persistSetting return-type broadening in ServeAppDeps is a minor interface change, but it's handled cleanly: the existing settings route wraps it to discard the return, and the new permissions route uses it to avoid a redundant loadSettings call after persisting.

LGTM, ready to ship. ✅

中文说明

退一步看:这个 PR 完全符合补齐权限规则能力缺口的做法。把 permission-settings.tsacpAgent.ts 的内联代码中抽出来是正确的——让 REST route 可以共享校验和响应结构,不重复逻辑。REST route 的双路径写入策略(优先 live ACP child,回退 persist)经过深思熟虑,和 daemon 已有的 settings mutation 模式一致。

SDK helper 是自然的补充。对 add/remove 的 TOCTOU 风险做了诚实的 JSDoc 说明——read-modify-write 不是原子操作就不该装作是。

845 个相关测试全部通过。构建干净,类型检查干净。diff 聚焦——每个改动文件都服务于目标,没有顺手重构,没有范围膨胀。

可以合并 ✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

Comment thread packages/cli/src/config/permission-settings.ts
Comment thread packages/sdk-typescript/test/unit/DaemonClient.test.ts

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Downgraded from Approve to Comment: CI failing: Integration Tests (No-AK Smoke).

— qwen3.7-max via Qwen Code /review

Comment thread packages/sdk-typescript/src/daemon/DaemonClient.ts
Comment thread packages/cli/src/config/permission-settings.ts
Comment thread packages/sdk-typescript/test/unit/DaemonClient.test.ts
@doudouOUC doudouOUC self-assigned this Jun 23, 2026
@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

✅ Local runtime verification (real daemon, not just unit tests)

I built the real esbuild bundle from this PR's head (695aa4c) and exercised the daemon end‑to‑end — a live qwen serve driven over HTTP with curl and the TypeScript SDK — in addition to the unit/integration suites.

Environment

  • Bundle: node esbuild.config.jsdist/cli.js (≈6.3 MB, API‑only). Confirmed the new route code is actually compiled in (string markers permission_update_failed, invalid_rule_type, workspace_permissions, Malformed permission rule).
  • Daemon: qwen serve --hostname 127.0.0.1 --port 4178 --workspace <tmp> with QWEN_SERVER_TOKEN, isolated QWEN_HOME, running in tmux. qwenCodeVersion=0.19.0, mode=http-bridge.
  • Node v22.

Results

Layer Scope Result
Unit workspace-permissions.test.ts ✅ 5/5
Unit DaemonClient.test.ts ✅ 169/169
Unit acpAgent.test.ts (permission cases) ✅ 5/5
Integration (this PR's own E2E) qwen-serve-routes.test.ts on the bundle ✅ 26/26
Live daemon workspace_permissions advertised in GET /capabilities features
Live daemon auth gate — POST without token ✅ 401
Live daemon GET shape (v:1, user / workspace / merged / isTrusted, paths)
Live daemon POST workspace and user scope → trim/dedup + persisted to settings.json
Live daemon validation: invalid_scope / invalid_rule_type / invalid_rules / invalid_rule ✅ 400 each
Live daemon fallback (no child) and live‑ACP‑child write paths ✅ both

Live examples:

POST /workspace/permissions {scope:workspace, ruleType:deny,
     rules:[" Read(.env) ","Read(.env)","Bash(rm *)"]}
  → 200, workspace.deny = ["Read(.env)","Bash(rm *)"]      # trimmed + deduped
  settings.json → {"permissions":{"deny":["Read(.env)","Bash(rm *)"]}}

# user (global) scope also persists to ~/.qwen/settings.json  ✅
# with a live ACP child, the write is routed through the child (preferred path)  ✅

⚠️ One robustness issue confirmed at runtime

A read‑modify‑write cycle is blocked by a pre‑existing malformed rule. GET returns rule strings verbatim (readPermissionRuleSet keeps them), but every write re‑validates the entire list and returns 400 invalid_rule on the first malformed entry. So a client that reads → edits → writes — including this PR's own addWorkspacePermissionRule / removeWorkspacePermissionRule — cannot add or remove any rule once a malformed entry exists (e.g. unbalanced parens, which the pre‑PR setRules and the interactive /permissions flow accepted without parseRule validation).

Reproduced live against the daemon:

# settings.json already contains  allow: ["Bash(git log)", "Bash(rm "]   ("Bash(rm " is malformed)
GET  /workspace/permissions  → workspace.allow = ["Bash(git log)","Bash(rm "]   # returned verbatim
POST /workspace/permissions {scope:workspace, ruleType:allow,
     rules:["Bash(git log)","Bash(rm ","Bash(ls)"]}          # adding a VALID rule "Bash(ls)"
  → 400 {"error":"Malformed permission rule: Bash(rm","code":"invalid_rule"}
GET  → "Bash(ls)" was NOT persisted

And through the SDK helper directly (live daemon):

addWorkspacePermissionRule("workspace","allow","Bash(pwd)")  → THROWS invalid_rule   (blocked)
addWorkspacePermissionRule("workspace","ask",  "Bash(pwd)")  → OK                     (clean list — control)

Suggested fix: validate only the incoming delta, or drop invalid pre‑existing rules with a warning (as core's parseRules already does) rather than rejecting the whole request. Adding an SDK test for the actual append/replace path (the current add/remove tests only assert the no‑op branch) would catch this.

Verdict

The feature works as designed — capability gating, auth, validation, persistence, and both the fallback and live‑ACP‑child write paths are all correct. The round‑trip behavior above is the one robustness gap worth addressing (or explicitly accepting as out of scope) before merge.

中文版(合并参考)

✅ 本地真实运行时验证(真实 daemon,而非仅单元测试)

我基于本 PR 的最新 head(695aa4c)构建了真实的 esbuild bundle,并对 daemon 做了端到端验证——启动真实的 qwen serve,用 curl 和 TypeScript SDK 通过 HTTP 实际驱动,同时跑了单元/集成测试。

环境

  • Bundle:node esbuild.config.jsdist/cli.js(≈6.3 MB,API‑only)。已确认新路由代码确实被编译进 bundle(字符串标记 permission_update_failedinvalid_rule_typeworkspace_permissionsMalformed permission rule)。
  • Daemon:qwen serve --hostname 127.0.0.1 --port 4178 --workspace <tmp>,配置 QWEN_SERVER_TOKEN、隔离的 QWEN_HOME,在 tmux 中运行。qwenCodeVersion=0.19.0mode=http-bridge
  • Node v22。

结果

层级 范围 结果
单元 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)
wenshao
wenshao previously approved these changes Jun 23, 2026
Comment thread packages/cli/src/serve/routes/workspace-permissions.ts
Comment thread packages/cli/src/serve/routes/workspace-permissions.test.ts
…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.
@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

🔬 Local build & verification report — PR #5743

Verified at PR head 639834f · macOS · Node v22.22.2 · fresh npm ci in an isolated git worktree (not a symlinked node_modules).

TL;DR

🔴 One blocking issue: the PR's own Reviewer Test Plan fails. acpAgent.test.ts › qwen/permissions/setRules rejects malformed permission rules fails because normalizePermissionRules() silently skips malformed rules (continue) instead of rejecting them. Everything else — route tests, SDK tests, build, typecheck, lint, and the real-daemon happy path — is green. A one-line fix makes all 657 CLI tests pass (verified).

What I ran (exactly the Reviewer Test Plan, plus a real-daemon e2e)

Check Result
vitest run workspace-permissions.test.ts acpAgent.test.ts server.test.ts 🔴 1 failed / 656 passed
vitest run sdk-typescript/test/unit/DaemonClient.test.ts ✅ 171 passed
npm run build ✅ pass
npm run typecheck ✅ pass
eslint (all changed files) ✅ clean
Real daemon (qwen serve) + curl ⚠️ happy path ✅ / malformed-rule footgun 🔴

1) The failing test (will turn CI red)

FAIL  src/acp-integration/acpAgent.test.ts
  > qwen/permissions/setRules rejects malformed permission rules
  AssertionError: promise resolved "{ v: 1, user: { … } }" instead of rejecting
    .rejects.toThrowError(/Malformed permission rule/)   ← never thrown
    expect(settings.setValue).not.toHaveBeenCalled()     ← setValue IS called with []

Root cause — packages/cli/src/config/permission-settings.ts:

const rule = item.trim();
if (parseRule(rule).invalid) {
  continue;          // ← silently skips. The test expects a throw + no setValue().
}

The string "Malformed permission rule" appears only in the test, nowhere in the source — implementation and test have diverged. This test is new in this PR, and the CI Test (macos/ubuntu/windows) jobs are still pending, so they will go red on this.

2) Real-daemon e2e — the user-facing impact

I booted the actual built daemon (node packages/cli/dist/index.js serve --port 0 --token … --workspace …) and exercised the HTTP API directly:

  • GET /capabilities advertises workspace_permissions
  • GET /workspace/permissions returns correct scoped + merged + trust state
  • POST trims & dedups — [" ShellTool(git status) ", "ShellTool(git status)"]["ShellTool(git status)"], written to settings.json
  • POST {"scope":"system"}400 invalid_scope
  • 🔴 POST {"scope":"user","ruleType":"deny","rules":["ShellTool(git status"]} (malformed) → HTTP 200, and the rule is silently dropped (deny stays []).

So a client — including one persisting a security-sensitive deny rule — gets a 200 OK while its rule silently disappears, with no error to act on. The REST route and the ACP method share the same normalizePermissionRules, so both swallow malformed rules.

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 PermissionRulesValidationError400 invalid_rules, and the ACP handler maps it → RequestError.invalidParams, so REST and ACP reject malformed rules consistently.

Alternatively, if silent-drop is intended, then acpAgent.test.ts must be updated to assert the 200/skip semantics — but that keeps the footgun of silently discarding a deny rule behind a 200.

Everything else looks solid

Clean refactor (extracting permission-settings.ts out of acpAgent.ts), good test coverage, documented non-atomic read-modify-write (TOCTOU) on the SDK add/remove helpers, and correct trust handling (untrusted workspace rules excluded from merged). Once the malformed-rule handling is reconciled, this is good to merge.

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 639834f · macOS · Node v22.22.2 · 在独立 git worktree 内全新 npm ci(不是符号链接 node_modules)。

结论速览

🔴 一个阻塞性问题:PR 自己的 Reviewer Test Plan 跑不过。 acpAgent.test.ts › qwen/permissions/setRules rejects malformed permission rules 失败,因为 normalizePermissionRules() 对畸形规则是静默跳过continue)而不是拒绝。其余全部为绿:route 测试、SDK 测试、build、typecheck、lint、真实 daemon 的正常路径。一行修复即可让全部 657 个 CLI 测试通过(已验证)。

我跑了什么(严格按 Reviewer Test Plan,外加真实 daemon e2e)

检查项 结果
vitest run workspace-permissions.test.ts acpAgent.test.ts server.test.ts 🔴 1 failed / 656 passed
vitest run sdk-typescript/test/unit/DaemonClient.test.ts ✅ 171 passed
npm run build ✅ 通过
npm run typecheck ✅ 通过
eslint(所有改动文件) ✅ 干净
真实 daemon(qwen serve)+ curl ⚠️ 正常路径 ✅ / 畸形规则有坑 🔴

1)失败的测试(会让 CI 变红)

FAIL  src/acp-integration/acpAgent.test.ts
  > qwen/permissions/setRules rejects malformed permission rules
  AssertionError: promise 被 resolve 了,而不是 reject
    .rejects.toThrowError(/Malformed permission rule/)   ← 从未抛出
    expect(settings.setValue).not.toHaveBeenCalled()     ← setValue 实际被以 [] 调用了

根因 —— packages/cli/src/config/permission-settings.ts

const rule = item.trim();
if (parseRule(rule).invalid) {
  continue;          // ← 静默跳过;但测试期望「抛错 + 不调用 setValue」。
}

字符串 "Malformed permission rule" 只存在于测试里,源码里完全没有 —— 实现和测试已经分叉。该测试是本 PR 新增的,而 CI 的 Test (macos/ubuntu/windows) 任务还在 pending,所以它们会因此变红。

2)真实 daemon e2e —— 对用户的实际影响

我启动了真正构建出来的 daemon(node packages/cli/dist/index.js serve --port 0 --token … --workspace …),直接打 HTTP 接口:

  • GET /capabilities 广告了 workspace_permissions
  • GET /workspace/permissions 返回正确的 scoped + merged + trust 状态
  • POST 会 trim + 去重 —— [" ShellTool(git status) ", "ShellTool(git status)"]["ShellTool(git status)"],并写入 settings.json
  • POST {"scope":"system"}400 invalid_scope
  • 🔴 POST {"scope":"user","ruleType":"deny","rules":["ShellTool(git status"]}(畸形)→ HTTP 200,且规则被静默丢弃deny 仍是 [])。

也就是说,一个客户端 —— 哪怕是在持久化一条安全敏感的 deny 规则 —— 会拿到 200 OK,但它的规则悄无声息地消失了,没有任何错误可供它处理。REST route 与 ACP method 共用同一个 normalizePermissionRules,所以两条路径都会吞掉畸形规则。

3)建议修复(已验证 —— 657/657 通过)

把「跳过」改成「拒绝」:

if (parseRule(rule).invalid) {
  throw new PermissionRulesValidationError(
    `Malformed permission rule: ${rule}`,
    'invalid_rules',
  );
}

改完后 全部 657 个 CLI 测试通过、无回归:REST route 已经把 PermissionRulesValidationError 映射成 400 invalid_rules,ACP handler 则映射成 RequestError.invalidParams,于是 REST 与 ACP 对畸形规则的拒绝行为保持一致。

或者,如果静默丢弃是有意为之,那就必须改 acpAgent.test.ts 去断言 200/跳过 的语义 —— 但这会保留「在 200 背后悄悄丢弃 deny 规则」这个坑。

其余部分都很扎实

重构干净(把 permission-settings.tsacpAgent.ts 抽出来)、测试覆盖良好、SDK add/remove helper 的非原子 read-modify-write(TOCTOU)有注释说明、trust 处理正确(untrusted workspace 规则不并入 merged)。把畸形规则的处理对齐之后,即可合并。

Verification performed locally by the maintainer against a real build; CI checks were still pending at the time of writing.

Comment thread packages/cli/src/serve/routes/workspace-permissions.ts
Comment thread packages/cli/src/config/permission-settings.ts Outdated
Comment thread packages/cli/src/serve/routes/workspace-permissions.ts
Comment thread packages/cli/src/serve/routes/workspace-permissions.test.ts Outdated
@doudouOUC
doudouOUC force-pushed the codex/workspace-permissions-rules branch from 21bf8bb to b4572c6 Compare June 23, 2026 17:15
Comment thread packages/cli/src/config/permission-settings.ts
Comment thread packages/cli/src/config/permission-settings.ts
Comment thread packages/cli/src/serve/routes/workspace-permissions.ts Outdated
Comment thread packages/cli/src/serve/routes/workspace-permissions.ts Outdated
res.status(200).json(liveResponse as QwenPermissionSettings);
return;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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:

  1. After persistSetting succeeds, buildPermissionSettings(loadSettings(boundWorkspace)) at line ~206 can throw and return 500 internal_error even 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.
  2. broadcastSettingsChanged fires before the response is built — if the response-build throws, connected SSE clients receive a settings_changed event while the HTTP caller sees a 500, creating a split-brain.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/serve/routes/workspace-permissions.test.ts
rule: string,
opts?: { clientId?: string },
): Promise<DaemonWorkspacePermissionsStatus> {
const normalizedRule = rule.trim();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Two concerns with the SDK add/remove helpers:

  1. No client-side rule validation: addWorkspacePermissionRule and removeWorkspacePermissionRule only call rule.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 a parseRule check client-side would catch the most common mistake before any I/O.

  2. String equality for dedup: currentRules.includes(normalizedRule) uses strict string comparison, but parseRule resolves tool name aliases (e.g., Bashrun_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-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully: Qwen review timed out after 85 minutes. See workflow logs.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new high-confidence issues found at this head. The earlier blockers in this thread are resolved here:

  • Malformed-rule round-trip: normalizePermissionRules now preserves pre-existing malformed rules (matched against existingRules) while still rejecting genuinely new malformed input. Read-modify-write through the SDK add/remove helpers works again, and the acpAgent "rejects malformed permission rules" test passes.
  • Write boundary / exposure / limits: POST is restricted to scope: "workspace", the REST response strips the scope path fields, 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.

⚠️ Downgraded from Approve to Comment: CI failing: review-pr. Note this is the automated Qwen review workflow timing out (~85 min), not a code/test failure — the platform Test jobs (macOS/ubuntu/windows, Node 22.x) and CodeQL are green.

— claude-opus-4-8 via Qwen Code /qreview

@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

wenshao
wenshao previously approved these changes Jun 23, 2026

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

🔬 Local runtime verification — real qwen serve daemon (PR #5743 @ bddf771)

I built the real bundle from this PR's head and drove a live daemon end-to-end — raw HTTP (fetch + curl) and the built @qwen-code/sdk DaemonClient — on top of the unit suites.

TL;DR

The feature is solid: capability gating, auth, validation, persistence, trim/dedup, and the malformed-rule round-trip that was the earlier blocker are all green (17/19 automated E2E checks). But the real daemon surfaced one bug the unit tests mask:

🔴 Submitting a brand-new malformed rule returns 500 permission_update_failed instead of the intended 400 invalid_rules, and the actionable Malformed permission rule: … message is dropped. Root cause: getInvalidParamsMessage requires err instanceof Error, but the live ACP child returns the JSON-RPC error as a plain {code:-32602, message} object, so the guard never matches. A one-line fix restores the 400 + message — verified, all 19/19 green.

Environment

  • Head bddf771fb836b31a972c0768f95a2cd36876b071, Linux, Node v22.22.2, fresh npm ci + npm run bundle in an isolated git worktree (real dist/cli.js + dist/chunks/serve-*.js; markers workspace/permissions, permission_update_failed, registerWorkspacePermissionsRoutes confirmed compiled in).
  • qwen serve --hostname 127.0.0.1 --port 41743 --workspace <tmp> --no-web with QWEN_SERVER_TOKEN and an isolated QWEN_HOME, in tmux. Driven over HTTP and via new DaemonClient({ baseUrl, token }) from the built SDK index.mjs.

What works (verified live)

# 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 dropped

Because 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 itworkspace-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

  1. A client sending a malformed rule gets a 5xx (server fault / “retry”) for what is a 4xx client-input error — breaks client error-handling and retry logic.
  2. The actionable Malformed permission rule: <rule> message is replaced by a generic string — the client can’t tell the user which rule is bad.
  3. The daemon logs [object Object], hampering debugging.
  4. 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,Node v22.22.2,隔离 git worktree 中全新 npm ci + npm run bundle(真实 dist/cli.js + dist/chunks/serve-*.js,已确认 workspace/permissionspermission_update_failedregisterWorkspacePermissionsRoutes 等被编译进去)。
  • qwen serve --hostname 127.0.0.1 --port 41743 --workspace <tmp> --no-web,配置 QWEN_SERVER_TOKEN 与隔离 QWEN_HOME,运行于 tmux;通过 HTTP 以及构建出的 SDK index.mjsnew DaemonClient({ baseUrl, token }) 驱动。

验证通过的部分(真实运行)

# 检查 结果
A1 GET /capabilities 广告 workspace_permissions
A2 无 token 的 POST401
A3 GET 结构(v:1user/workspace/merged/isTrusted),未泄漏 path 字段
A4 校验:invalid_scope(user/system)、invalid_rule_typeinvalid_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-31err instanceof Error 作门槛:

if (code === -32602 && err instanceof Error) return err.message; // 普通对象 ⇒ false ⇒ message 被丢弃

由于该对象不是 Error 实例,getInvalidParamsMessage 返回 undefined,handler 落入通用的 500 permission_update_failedString(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 却不对。

影响

  1. 客户端提交非法规则时拿到 5xx(服务器故障/“重试”),而这本应是 4xx 客户端输入错误——破坏客户端错误处理与重试逻辑。
  2. 可定位的 Malformed permission rule: <rule> 信息被通用串替换——客户端无法告诉用户哪条规则有问题。
  3. daemon 日志打成 [object Object],不利排查。
  4. 确定性问题,且发生在(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.invalidParamsInvalid params: 前缀会泄漏到 HTTP body,建议去掉以得到干净的客户端信息。回归测试应把 child 错误 mock 成普通 {code:-32602, message} 对象而非 Error,才能真正覆盖该路径。)

结论

除“新非法规则的错误映射”外,其余在真实 daemon 上均验证通过,包括此前作为 blocker 的往返行为。建议合并前应用上面的一行修复(并加强测试),其余无阻断问题。

Comment thread packages/cli/src/serve/routes/workspace-permissions.ts Outdated
Comment thread packages/cli/src/config/permission-settings.ts
Comment thread packages/cli/src/serve/routes/workspace-permissions.ts
Comment thread packages/cli/src/config/permission-settings.ts
Comment thread packages/sdk-typescript/test/unit/DaemonClient.test.ts
persistSetting: async (...args) => {
await persistSetting(...args);
},
broadcastSettingsChanged: (key, value, scope, clientId) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

Comment thread packages/sdk-typescript/src/daemon/types.ts Outdated
@doudouOUC
doudouOUC dismissed stale reviews from qwen-code-ci-bot, wenshao, and qqqys via 45b408a June 23, 2026 22:41
qqqys
qqqys previously approved these changes Jun 23, 2026

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/cli/src/serve/routes/workspace-permissions.ts
Comment thread packages/cli/src/serve/routes/workspace-permissions.ts

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully: Qwen review timed out after 85 minutes. See workflow logs.

@doudouOUC
doudouOUC dismissed stale reviews from qwen-code-ci-bot and qqqys via 9eec079 June 23, 2026 23:26

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  • getInvalidParamsMessage instanceof Error → now duck-types {code, message} with a typeof message === 'string' guard, and strips the "Invalid params: " prefix. Handles the plain-object ErrorResponse that the ACP SDK actually delivers.
  • DaemonPermissionScope type → narrowed from 'user' | 'workspace' to 'workspace', consistent with the route's scope !== '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.

⚠️ Downgraded from Approve to Comment: CI failing: review-pr, delay-automatic-review.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao

wenshao commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

✅ Local build + real-daemon verification — PR #5743

Verified at commit 333c2424 (fix(cli): report workspace permission write backend) on macOS (Darwin 25.5.0), Node v22.22.2 / npm 10.9.7. I built the whole workspace from source and exercised the new /workspace/permissions API against a real qwen serve daemon (the built binary, not mocks).

Results

Check Result
npm run build (all packages incl. SDK browser bundle) ✅ exit 0 — only the 15 pre-existing VS Code-companion lint warnings, 0 errors
npm run typecheck (6 workspaces) ✅ clean, no errors
CLI suites: workspace-permissions.test + acpAgent.test + server.test 670 passed
SDK suite: DaemonClient.test 175 passed
Real-daemon E2E (built binary) 19 / 19 assertions
Mutation test (break malformed-rule rejection) ✅ suite catches it — 1 test fails, then reverted

Real-daemon E2E (built packages/cli/dist/index.js serve)

Spawned the actual daemon bound to throwaway workspaces and drove the HTTP API end-to-end:

  • Capability/capabilities advertises workspace_permissions.
  • GET — returns { v:1, user, workspace, merged, isTrusted }.
  • POST allow — response and on-disk .qwen/settings.json both updated; merged reflects the rule.
  • Per-list replace — POST deny does not clobber the stored allow list.
  • appliedVia (new in 333c2424) — a write with no live ACP child correctly reports appliedVia: "persist-fallback".
  • Validationscope!=workspaceinvalid_scope; bad ruleTypeinvalid_rule_type; non-array / malformed rule → invalid_rules (all HTTP 400).
  • Atomic reject — a rejected malformed POST leaves the stored rules intact.
  • Malformed-rule preservation — a pre-existing malformed rule (Bash(rm ) round-trips successfully (preserved as the trimmed Bash(rm), while a brand-new malformed rule is still rejected with invalid_rules.

Notes (confirmed behavior, not defects)

  • Write routes require a bearer token even on loopback. GET is auth-free on loopback, but POST /workspace/permissions returns 401 token_required unless the daemon is started with --token / QWEN_SERVER_TOKEN. This is the intended mutate-guard — worth surfacing in the SDK/docs so remote clients know writes need a token.
  • Rules are normalized (trimmed) on round-trip, matching the route test's own expectation (Bash(rm Bash(rm).

Verdict

Build, typecheck, the full test suites, the mutation check, and real end-to-end daemon behavior all pass at 333c2424. From a build-and-behavior standpoint this looks ready to merge. (This is a functional verification, not a design/security code review.) The PR currently shows BLOCKED — that appears to be merge-queue / CI / approval gating rather than a code problem.

🇨🇳 中文版(完整对应)

✅ 本地构建 + 真实 daemon 验证 — PR #5743

验证 commit:333c2424fix(cli): report workspace permission write backend,环境:macOS(Darwin 25.5.0),Node v22.22.2 / npm 10.9.7。我从源码完整构建了整个 workspace,并用真实的 qwen serve daemon(构建出的真实二进制,而非 mock)端到端跑通了新增的 /workspace/permissions 接口。

结果

检查项 结果
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 列表。
  • appliedVia333c2424 新增)—— 没有 live ACP child 时,写入正确报告 appliedVia: "persist-fallback"
  • 校验 —— scope!=workspaceinvalid_scope;非法 ruleTypeinvalid_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 qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants