Skip to content

feat(cli): Add workspace-qualified core REST routes - #6567

Merged
doudouOUC merged 16 commits into
QwenLM:mainfrom
doudouOUC:codex/phase3-workspace-qualified-rest
Jul 9, 2026
Merged

feat(cli): Add workspace-qualified core REST routes#6567
doudouOUC merged 16 commits into
QwenLM:mainfrom
doudouOUC:codex/phase3-workspace-qualified-rest

Conversation

@doudouOUC

Copy link
Copy Markdown
Collaborator

What this PR does

This PR adds the Phase 3 workspace-qualified core REST contract under /workspaces/:workspace/.... The selector resolves workspace id first and then a URL-encoded absolute cwd, including POSIX, Windows drive, and UNC-style absolute paths. The implementation keeps all legacy /workspace/... and workspace-less session routes on their existing primary-workspace semantics while adding plural routes for core file access, status surfaces, settings, permissions, trust, lifecycle, MCP control, tool toggles, memory, project-agent CRUD, and persisted session organization/batch operations.

It also advertises the new workspace_qualified_rest_core capability, makes plural route telemetry resolve the selected workspace when possible, adds SDK workspaceById() and workspaceByCwd() helpers through WorkspaceDaemonClient, and documents the selector, trust, scope, and out-of-scope boundaries.

Why it's needed

This is the Phase 3 follow-up for issue #6378 on top of #6558. After persisted non-primary workspace session data exists, clients need a stable REST surface for operating on a registered secondary workspace directly instead of tunneling through primary-only daemon state or special-casing individual routes.

The trust policy is explicit: trust status/request and file reads remain available according to their existing policies, file writes rely on the selected workspace filesystem write guard, and other plural core routes require a trusted selected runtime and return 403 untrusted_workspace when the workspace is registered but untrusted.

Reviewer Test Plan

How to verify

Confirm /capabilities includes workspace_qualified_rest_core, then use a registered secondary workspace id and encoded cwd against representative plural routes such as /workspaces/:workspace/file, /workspaces/:workspace/mcp, /workspaces/:workspace/agents, and /workspaces/:workspace/sessions. Expected behavior is that id and cwd selectors route to the selected runtime, unknown selectors return 400 workspace_mismatch, untrusted file reads follow the filesystem read policy, and trusted-only plural routes return 403 untrusted_workspace for untrusted workspaces.

Confirm plural memory, agents, and permissions mutation routes reject global or user scope with 400 global_scope_not_supported_for_workspace_route, while legacy primary /workspace/... routes keep their existing global/user semantics.

Local verification run: cd packages/cli && npx vitest run src/serve/workspace-qualified-rest.test.ts src/serve/server/telemetry.test.ts; cd packages/sdk-typescript && npx vitest run test/unit/DaemonClient.test.ts; npm run build && npm run typecheck && npm run lint.

Evidence (Before & After)

N/A. This is REST, SDK, telemetry, and documentation work with no TUI-visible change.

Tested on

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

Environment (optional)

Node v22.22.3, npm 10.9.8. Local verification used targeted Vitest files plus root npm run build && npm run typecheck && npm run lint.

Risk & Scope

  • Main risk or tradeoff: this is a broad daemon route-surface expansion, so the main risk is route parity drift between legacy primary routes and plural selected-runtime routes; the implementation keeps shared resolver/trust helpers and focused tests for selector, trust, scope, telemetry, and SDK URL construction to reduce that risk.
  • Not validated / out of scope: auth, voice, extensions, ACP/WebSocket transport, channel-worker routing, and agent generation are intentionally not workspace-qualified in this PR. This draft is based on feat(cli): List persisted sessions for trusted workspaces #6558's branch and should be reviewed with that dependency in mind.
  • Breaking changes / migration notes: no intended breaking changes. Existing primary workspace routes remain backward compatible. The SDK daemon browser bundle budget is raised for the new workspace-qualified helper surface.

Linked Issues

Refs #6378. Depends on #6558.

中文说明

What this PR does

本 PR 添加 Phase 3 的 workspace-qualified core REST 契约,路径为 /workspaces/:workspace/...。选择器先按 workspace id 解析,再按 URL 编码后的绝对 cwd 解析,并覆盖 POSIX、Windows drive、UNC 风格绝对路径。实现保持所有旧的 /workspace/... 和不带 workspace 的 session 路由沿用 primary workspace 语义,同时为核心文件访问、状态面、设置、权限、信任、生命周期、MCP 控制、工具开关、memory、project-agent CRUD、持久化 session 组织和批量操作添加 plural 路由。

它还新增 workspace_qualified_rest_core capability,让 plural 路由 telemetry 在可解析时归因到选中的 workspace,添加 SDK workspaceById()workspaceByCwd() helper 并通过 WorkspaceDaemonClient 暴露,同时补充 selector、trust、scope 和排除范围的协议文档。

Why it's needed

这是 issue #6378 基于 #6558 的 Phase 3 后续工作。非 primary workspace 的持久化 session 数据可用之后,客户端需要一个稳定的 REST 面直接操作已注册的 secondary workspace,而不是通过 primary-only daemon 状态绕行或为单个路由做特殊处理。

信任策略是显式的:trust status/request 和文件读取沿用现有策略,文件写入依赖选中 workspace 的 filesystem write guard,其他 plural core 路由要求选中的 runtime 已受信任,并在 workspace 已注册但未受信任时返回 403 untrusted_workspace

Reviewer Test Plan

How to verify

确认 /capabilities 包含 workspace_qualified_rest_core,然后使用已注册 secondary workspace 的 id 和编码 cwd 请求代表性 plural 路由,例如 /workspaces/:workspace/file/workspaces/:workspace/mcp/workspaces/:workspace/agents/workspaces/:workspace/sessions。预期行为是 id 和 cwd selector 都会路由到选中的 runtime,未知 selector 返回 400 workspace_mismatch,未受信任 workspace 的文件读取遵循 filesystem read policy,而 trust-only plural 路由对未受信任 workspace 返回 403 untrusted_workspace

确认 plural memory、agents 和 permissions mutation 路由在请求 globaluser scope 时返回 400 global_scope_not_supported_for_workspace_route,同时 legacy primary /workspace/... 路由保留现有 global/user 语义。

本地验证命令:cd packages/cli && npx vitest run src/serve/workspace-qualified-rest.test.ts src/serve/server/telemetry.test.tscd packages/sdk-typescript && npx vitest run test/unit/DaemonClient.test.tsnpm run build && npm run typecheck && npm run lint

Evidence (Before & After)

N/A。本 PR 是 REST、SDK、telemetry 和文档工作,没有 TUI 可见变化。

Tested on

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

Environment (optional)

Node v22.22.3,npm 10.9.8。本地验证使用了目标 Vitest 文件以及根目录 npm run build && npm run typecheck && npm run lint

Risk & Scope

  • Main risk or tradeoff: 这是一次较宽的 daemon route surface 扩展,主要风险是 legacy primary 路由与 plural selected-runtime 路由之间出现语义漂移;实现通过共享 resolver/trust helper,并对 selector、trust、scope、telemetry 和 SDK URL 构造添加聚焦测试来降低该风险。
  • Not validated / out of scope: auth、voice、extensions、ACP/WebSocket transport、channel-worker routing 和 agent generation 有意不在本 PR 中 workspace-qualified。本 draft 基于 feat(cli): List persisted sessions for trusted workspaces #6558 分支,评审时需要考虑该依赖。
  • Breaking changes / migration notes: 没有预期破坏性变更。现有 primary workspace 路由保持向后兼容。SDK daemon browser bundle 预算因新增 workspace-qualified helper surface 而上调。

Linked Issues

Refs #6378. Depends on #6558.

@wenshao
wenshao marked this pull request as ready for review July 9, 2026 03:47
Copilot AI review requested due to automatic review settings July 9, 2026 03:56
@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Re-triage (updated 2026-07-09) — head 0c3cbe229 (merged main into branch).

Thanks for the continued work on this!

Template looks good ✓ — all required sections present with bilingual body.

Problem: observed and tracked. This is Phase 3 of the multi-workspace daemon initiative (RFC #6378), building on Phase 1 (#6394), Phase 2a (#6410), and Phase 2b (#6511, #6558 — both merged). The dependency PR #6558 landed today. Clients need workspace-qualified REST routes to operate on secondary workspaces directly instead of tunneling through primary-only daemon state.

Direction: clearly aligned. The CHANGELOG shows a systematic phase-by-phase rollout of multi-workspace support. This PR continues the established pattern — each legacy route gets a plural counterpart under /workspaces/:workspace/... with explicit trust policy. No direction concerns.

Size: ~3,028 production lines + 135 deletions = ~3,163 production logic lines across 23 production files. ~1,479 test lines across 6 test files. Cross-package (cli + sdk-typescript). The 1000+ advisory applies — this is a large PR, but the scope is inherent to the surface area being covered (file, status, settings, permissions, trust, lifecycle, MCP, tools, memory, agents, sessions). Each route module is focused and under 250 production lines. Maintainer awareness has already been demonstrated through multiple runtime verification reports on this PR.

Approach: the shared workspace-route-runtime.ts helper (148 lines) centralizes selector resolution, trust gate, and error responses — reused by every plural route. This is the right level of abstraction for the pattern. The SDK WorkspaceDaemonClient follows the same URL-construction pattern as existing DaemonClient methods. No scope creep or unrelated changes detected.

Moving on to code review and testing. 🔍

中文说明

感谢持续的工作!

模板完整 ✓ — 所有章节齐全,含中文对照。

问题: 已观测且有跟踪。这是多 workspace daemon 计划的 Phase 3(RFC #6378),建立在 Phase 1(#6394)、Phase 2a(#6410)和 Phase 2b(#6511#6558——均已合并)之上。依赖 PR #6558 今天已合并。客户端需要 workspace-qualified REST 路由直接操作 secondary workspace,而不是通过 primary-only daemon 状态绕行。

方向: 明确对齐。CHANGELOG 展示了系统性的分阶段多 workspace 支持推进。本 PR 延续既有模式——每个 legacy 路由在 /workspaces/:workspace/... 下获得对应 plural 路由,并配有显式信任策略。无方向性问题。

规模: ~3,028 行生产代码 + 135 行删除 = ~3,163 行生产逻辑,跨 23 个生产文件。~1,479 行测试代码,跨 6 个测试文件。跨包(cli + sdk-typescript)。1000+ 大 PR 建议适用——但范围是所覆盖面(文件、状态、设置、权限、信任、生命周期、MCP、工具、memory、agents、sessions)的固有需求。每个路由模块专注且低于 250 行生产代码。维护者已通过多次运行时验证报告表明知晓。

方案: 共享 workspace-route-runtime.ts helper(148 行)集中了选择器解析、信任门控和错误响应——被每个 plural 路由复用。这是该模式的正确抽象层级。SDK WorkspaceDaemonClient 遵循与既有 DaemonClient 方法相同的 URL 构造模式。未检测到范围蔓延或无关改动。

进入代码审查和测试 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Re-triage (updated 2026-07-09) — fresh verification after main merge into branch.

Code Review

Independent proposal: before reading the diff, I'd approach this by (1) creating a shared workspace selector middleware that resolves :workspace param → WorkspaceRuntime with trust checks, (2) mounting plural route handlers that mirror each legacy route but use the middleware, and (3) adding an SDK client wrapper that URL-encodes selectors. That's essentially what this PR does.

Comparison: the PR's approach matches my independent proposal. The workspace-route-runtime.ts module provides resolveWorkspaceRuntimeFromParam (selector resolution) and requireTrustedWorkspaceRuntime (trust gate) — shared by all plural routes. Each route file (settings, permissions, trust, lifecycle, MCP, tools, agents, memory, sessions) has a registerWorkspaceQualified*Routes function that mirrors its legacy counterpart. The SDK WorkspaceDaemonClient wraps DaemonClient with URL-encoded selectors via workspaceById() and workspaceByCwd().

No critical blockers found. The implementation is consistent:

  • Selector resolution handles id, absolute POSIX path, Windows drive, and UNC paths through isPortableAbsolutePath + normalizePortableAbsolutePath
  • Trust gate is stricter on plural routes (all require trusted workspace, vs legacy primary-only trust)
  • global/user scope rejection on plural memory/agents/permissions returns 400 global_scope_not_supported_for_workspace_route
  • File system factory resolved from WorkspaceRouteContext for plural routes, from app.locals for legacy
  • Telemetry middleware resolves workspace for plural routes when possible
  • Error responses normalized through sendUntrustedWorkspaceResponse / sendWorkspaceMismatch helpers

One observation (not a blocker): session.ts gains ~227 lines for plural session batch routes (delete/archive/unarchive) and session group CRUD. These handlers duplicate the structure of the legacy routes they mirror — but this is intentional since they need different workspace resolution and trust policy than the legacy primary-workspace routes.

Real-Scenario Testing

Build:npm run build — all packages compile cleanly.

Typecheck:npm run typecheck — no errors.

Lint:npm run lint — no errors.

Unit tests: ✅ 312 tests pass:

  • packages/cli: 4 test files, 92 tests (workspace-qualified-rest, telemetry, multi-workspace-sessions, workspace-file-read)
  • packages/sdk-typescript: 1 test file, 220 tests (DaemonClient)

Daemon runtime verification (started from worktree build, port 4179):

qwen serve listening on http://127.0.0.1:4179 (mode=http-bridge, workspace=/home/github-runner/actions-runner-11/_work/qwen-code/qwen-code/.qwen/worktrees/triage)
qwen serve: bound to workspace "/home/github-runner/actions-runner-11/_work/qwen-code/qwen-code/.qwen/worktrees/triage"
qwen serve: startup timing: processToListenMs=123 runQwenServeToListenMs=34
qwen serve: bearer auth disabled (loopback default). Set QWEN_SERVER_TOKEN to enable.

Capability advertised:

$ curl -sf http://127.0.0.1:4179/capabilities | grep workspace_qualified_rest_core
workspace_qualified_rest_core  ✅

Plural routes verified:

GET /workspaces/:workspace/trust → 200 ✅
{
    "v": 1,
    "workspaceCwd": "/home/github-runner/.../triage",
    "effective": { "state": "trusted", "source": "disabled" }
}

GET /workspaces/:workspace/settings → 200 ✅
{ "v": 1, "settings": [...] }

GET /workspaces/:workspace/agents → 200 ✅
{ "v": 1, "workspaceCwd": "...", "agents": [{ "name": "test-engineer", ... }] }

GET /workspaces/:workspace/sessions → 200 ✅
{ "sessions": [] }

GET /workspaces/nonexistent-id/sessions → 400 workspace_mismatch ✅
{ "error": "`:workspace` must decode to a workspace id or absolute path", "code": "workspace_mismatch" }

Daemon telemetry confirms correct routing:

route=GET /workspaces/.../trust durationMs=3 status=200
route=GET /workspaces/.../settings durationMs=2 status=200
route=GET /workspaces/.../agents durationMs=9 status=200
route=GET /workspaces/nonexistent-id/sessions durationMs=2 status=400
route=GET /workspaces/.../sessions durationMs=1 status=200

All plural routes resolve the workspace selector correctly and return expected responses. The workspace_mismatch error fires for invalid selectors. The daemon handles the single-workspace case (no multi_workspace_sessions advertised) properly — plural routes still work against the primary workspace via cwd selector.

中文说明

代码审查

独立方案: 在阅读 diff 之前,我会这样做:(1) 创建共享的 workspace selector middleware,将 :workspace 参数解析为 WorkspaceRuntime 并附带信任检查,(2) 挂载与 legacy 路由镜像对应的 plural 路由处理器,(3) 添加 URL 编码选择器的 SDK 客户端包装。这基本就是本 PR 的做法。

对比: PR 方案与我的独立方案一致。workspace-route-runtime.ts 提供 resolveWorkspaceRuntimeFromParam(选择器解析)和 requireTrustedWorkspaceRuntime(信任门控)——被所有 plural 路由共享。每个路由文件(settings、permissions、trust、lifecycle、MCP、tools、agents、memory、sessions)都有 registerWorkspaceQualified*Routes 函数镜像其 legacy 对应。SDK WorkspaceDaemonClient 通过 workspaceById()workspaceByCwd() 包装 URL 编码选择器。

未发现关键阻塞问题。 实现一致:选择器解析处理 id、POSIX 绝对路径、Windows 盘符和 UNC 路径;plural 路由上的信任门控比 legacy 更严格;plural memory/agents/permissions 拒绝 global/user scope;文件系统在 plural 路由上从 WorkspaceRouteContext 解析。

一个观察(非阻塞):session.ts 增加约 227 行用于 plural session 批量操作和 group CRUD。这些处理器复制了 legacy 路由的结构——但这是有意为之,因为它们需要不同的 workspace 解析和信任策略。

实际场景测试

构建: ✅ 全部编译通过
类型检查: ✅ 无错误
Lint: ✅ 无错误
单元测试: ✅ 312 个测试通过(CLI 92 + SDK 220)

Daemon 运行时验证: daemon 启动正常,workspace_qualified_rest_core capability 已广播。plural trust、settings、agents、sessions 路由均返回预期 200 响应。无效选择器返回 400 workspace_mismatch。telemetry 确认所有路由正确分发。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Re-triage (updated 2026-07-09) — final assessment after all verification.

Stepping back: this is the third phase of a carefully sequenced multi-workspace daemon initiative. The RFC (#6378) laid out the architecture, Phases 1–2 built the foundation (registry, sessions, persistence), and this PR adds the REST surface that clients actually need to operate on secondary workspaces.

The implementation matches what I'd have designed independently — shared selector resolution + trust middleware, mirrored plural route handlers, SDK wrapper with URL encoding. Each route module is focused (under 250 lines), the shared runtime helper is 148 lines, and the test coverage is thorough (92 CLI + 220 SDK tests, plus the maintainer's own multi-round runtime verification).

The code is straightforward — no clever tricks, no premature abstractions, no speculative features beyond what the phase plan calls for. The WorkspaceDaemonClient follows the exact same patterns as the existing DaemonClient methods. The telemetry integration is minimal (workspace resolution when possible). The bundle budget bump from 141KB to 150KB is reasonable for the new surface area.

312 tests pass, build/typecheck/lint all clean, daemon starts and serves all plural routes correctly. The maintainer has independently verified this multiple times with full runtime suites.

This is a well-scoped feature addition in a tracked initiative. LGTM.

中文说明

退一步看:这是多 workspace daemon 计划的第三阶段。RFC(#6378)设计了架构,Phase 1–2 构建了基础(注册表、sessions、持久化),本 PR 添加了客户端实际需要的 secondary workspace REST 操作面。

实现与我的独立设计一致——共享选择器解析 + 信任中间件,镜像 plural 路由处理器,SDK 包装器带 URL 编码。每个路由模块专注(低于 250 行),共享运行时 helper 148 行,测试覆盖充分(CLI 92 + SDK 220 测试,加上维护者自己多轮运行时验证)。

代码直截了当——没有花哨技巧,没有过早抽象,没有超出阶段计划的投机功能。WorkspaceDaemonClient 遵循与既有 DaemonClient 方法完全相同的模式。telemetry 集成最小化(尽可能解析 workspace)。bundle 预算从 141KB 提升到 150KB 对新覆盖面来说合理。

312 个测试通过,build/typecheck/lint 全部干净,daemon 启动并正确服务所有 plural 路由。维护者已独立多次验证完整运行时套件。

这是一个跟踪计划中范围明确的功能添加。LGTM。

Qwen Code · qwen3.7-max

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.

Pull request overview

This PR adds a Phase 3 REST surface for selecting a non-primary workspace via /workspaces/:workspace/... (by workspace id or URL-encoded absolute cwd), and wires that selection through core “workspace” features (file/status/settings/permissions/trust/lifecycle/MCP/tools/memory/agents/sessions). It also extends the TypeScript SDK with WorkspaceDaemonClient helpers and updates telemetry normalization and protocol documentation to reflect the new capability.

Changes:

  • Add workspace selector + trust-gating helpers and mount new workspace-qualified REST routes across the CLI serve layer.
  • Extend the TypeScript SDK with workspaceById() / workspaceByCwd() and a new WorkspaceDaemonClient facade, plus unit tests.
  • Normalize telemetry for plural workspace routes, advertise the workspace_qualified_rest_core capability, and document the new contract.

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/sdk-typescript/test/unit/DaemonClient.test.ts Adds unit coverage for SDK workspace-qualified agents route URL construction + headers.
packages/sdk-typescript/src/index.ts Re-exports WorkspaceDaemonClient from the package entrypoint.
packages/sdk-typescript/src/daemon/index.ts Re-exports WorkspaceDaemonClient from the daemon barrel.
packages/sdk-typescript/src/daemon/DaemonClient.ts Introduces workspace-qualified request helpers and the WorkspaceDaemonClient API surface.
packages/sdk-typescript/scripts/build.js Raises the daemon browser bundle size budget for the new SDK surface area.
packages/cli/src/serve/workspace-route-runtime.ts Adds workspace selector resolution + trust helper utilities used by plural routes.
packages/cli/src/serve/workspace-qualified-rest.test.ts Adds integration-style tests for selector behavior, trust gating, scope rejection, and capability advertisement.
packages/cli/src/serve/workspace-memory.ts Mounts workspace-qualified memory status/mutation routes with trust + scope enforcement.
packages/cli/src/serve/workspace-agents.ts Mounts workspace-qualified agents CRUD routes with trust + scope enforcement.
packages/cli/src/serve/server/telemetry.ts Normalizes telemetry route labels for plural /workspaces/:workspace/... routes.
packages/cli/src/serve/server/telemetry.test.ts Adds coverage ensuring plural workspace agent routes map to stable telemetry labels.
packages/cli/src/serve/server.ts Mounts new plural routes and updates telemetry workspace attribution to resolve selected workspace when possible.
packages/cli/src/serve/routes/workspace-trust.ts Adds workspace-qualified trust status + trust request routes (including untrusted workspaces).
packages/cli/src/serve/routes/workspace-tools.ts Adds workspace-qualified tool enable/disable route with trusted-runtime gating.
packages/cli/src/serve/routes/workspace-status.ts Adds workspace-qualified status/diagnostic status routes with trusted-runtime gating.
packages/cli/src/serve/routes/workspace-settings.ts Adds workspace-qualified settings read/write route with trusted-runtime gating.
packages/cli/src/serve/routes/workspace-permissions.ts Adds workspace-qualified permissions read/write routes with trusted-runtime + scope enforcement.
packages/cli/src/serve/routes/workspace-mcp-control.ts Adds workspace-qualified MCP mutation routes with trusted-runtime gating + client id validation.
packages/cli/src/serve/routes/workspace-lifecycle.ts Adds workspace-qualified init/reload lifecycle routes with trusted-runtime gating + client id validation.
packages/cli/src/serve/routes/workspace-file-write.ts Adds workspace-qualified file write/edit routes by injecting selected workspace context into existing handlers.
packages/cli/src/serve/routes/workspace-file-read.ts Adds workspace-qualified file read/stat/list/glob routes and adjusts relative-path projection for selected workspaces.
packages/cli/src/serve/routes/session.ts Adds workspace-qualified session batch operations and session group CRUD.
packages/cli/src/serve/capabilities.ts Advertises the new workspace_qualified_rest_core capability.
docs/developers/qwen-serve-protocol.md Documents selector semantics, trust policy, and the new plural route surfaces.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/cli/src/serve/routes/session.ts Outdated
Comment thread packages/cli/src/serve/workspace-route-runtime.ts Outdated
@wenshao

wenshao commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Suggestions — commit 0c3cbe229b5c73bad7a45d40f1bf6dc4fcffe0b4

The author's fixes in bb0eba482 addressed the session-route portion of the previous Critical findings. The following Suggestion-level items remain from prior reviews and were accepted by the maintainer:

File Issue Suggested fix
workspace-route-runtime.ts:137 Shared sendWorkspaceMismatch omits boundWorkspace/requestedWorkspace (session.ts local version now includes both) Expand shared helper to match session.ts pattern
workspace-route-runtime.ts:105 requireTrustedWorkspaceRuntime doesn't pass workspaceCwd/workspaceId to sendUntrustedWorkspaceResponse Pass runtime.workspaceCwd and runtime.workspaceId
workspace-qualified-rest.test.ts file/edit, MCP disable/authenticate/clear-auth, file write mode:replace lack test coverage Add tests for untested route categories
workspace-route-runtime.ts:53 Bare catch {} around canonicalizeWorkspace() silently swallows EACCES/EIO/ELOOP Narrow to ENOENT or log swallowed errors
workspace-file-read.ts File read routes lack trust gate (unlike all other workspace-qualified routes) Add requireTrustedWorkspaceRuntime or document the exemption

— qwen3.7-max via Qwen Code /review

@doudouOUC
doudouOUC force-pushed the codex/phase3-workspace-qualified-rest branch from a348596 to b1e473d Compare July 9, 2026 06:25
@doudouOUC
doudouOUC changed the base branch from codex/phase2b-persisted-session-list to main July 9, 2026 06:25
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration.

中文

请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。

doudouOUC added a commit to doudouOUC/qwen-code that referenced this pull request Jul 9, 2026
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
doudouOUC added a commit to doudouOUC/qwen-code that referenced this pull request Jul 9, 2026
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Comment thread packages/cli/src/serve/routes/session.ts Outdated
Comment thread packages/cli/src/serve/workspace-route-runtime.ts Outdated
Comment thread packages/cli/src/serve/workspace-qualified-rest.test.ts
doudouOUC added a commit to doudouOUC/qwen-code that referenced this pull request Jul 9, 2026
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC doudouOUC self-assigned this Jul 9, 2026
@doudouOUC
doudouOUC enabled auto-merge July 9, 2026 07:47
Comment thread packages/cli/src/serve/server.ts Outdated
Comment thread packages/sdk-typescript/src/daemon/DaemonClient.ts
Comment thread packages/cli/src/serve/capabilities.ts

@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.

⚠️ Downgraded from Approve to Comment: CI failing: review-pr, Test (ubuntu-latest, Node 22.x).

Reviewed — no blockers. Suggestion-level recommendations are in the Suggestion summary comment below.

— qwen3.7-max via Qwen Code /review

doudouOUC added a commit to doudouOUC/qwen-code that referenced this pull request Jul 9, 2026
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
doudouOUC added a commit to doudouOUC/qwen-code that referenced this pull request Jul 9, 2026
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Comment thread packages/cli/src/serve/routes/workspace-file-write.ts
doudouOUC added a commit to doudouOUC/qwen-code that referenced this pull request Jul 9, 2026
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC
doudouOUC requested a review from wenshao July 9, 2026 09:03
doudouOUC and others added 3 commits July 9, 2026 17:25
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Comment thread packages/cli/src/serve/workspace-route-runtime.ts Outdated
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC
doudouOUC requested a review from wenshao July 9, 2026 11:55
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Comment thread packages/cli/src/serve/workspace-route-runtime.ts
Comment thread packages/cli/src/serve/workspace-route-runtime.ts
Comment thread packages/cli/src/serve/workspace-route-runtime.ts
});
}

function resolveTrustedRuntime(

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] This resolveTrustedRuntime function is identical to resolveTrustedMcpRuntime in workspace-mcp-control.ts:231 — same 6-line composition of resolveWorkspaceRuntimeFromParam + requireTrustedWorkspaceRuntime. If the trust-gate semantics change, both copies must update in lockstep.

Consider exporting a single shared resolveTrustedWorkspaceRuntime from workspace-route-runtime.ts.

— qwen3.7-max via Qwen Code /review

const selector = decodeURIComponent(rawSelector);
const byId = workspaceRegistry.getByWorkspaceId(selector);
if (byId) return byId.workspaceCwd;
if (isPortableAbsolutePath(selector)) {

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] Double workspace resolution per request: the telemetry middleware calls resolveRegisteredWorkspaceRuntimeByPathSelector here to extract workspaceCwd for hashing, then the matched route handler calls resolveWorkspaceRuntimeFromParam again (which invokes the same resolution internally). For path-based selectors, this doubles the canonicalizeWorkspace filesystem I/O (realpathSync).

Consider caching the resolved runtime on the Express req object during this middleware pass (via setWorkspaceRouteContext), or moving telemetry to run after Express route matching.

— qwen3.7-max via Qwen Code /review

if (req.method === 'GET' && /^\/workspaces\/[^/]+\/sessions$/.test(path)) {
return { route: 'GET /workspace/:id/sessions' };
}
const pluralWorkspacePrefix = /^\/workspaces\/[^/]+/;

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] Telemetry route mapping maintenance trap: ~100 lines of hardcoded suffix-to-route mappings for workspace-qualified URLs. Every new route must be manually added to this list with no compile-time or runtime enforcement — a missing entry silently degrades telemetry.

Consider deriving the telemetry route name from Express's req.route.path (available after route matching), or adding a test that asserts every registered workspace-qualified route has a corresponding telemetry mapping.

— qwen3.7-max via Qwen Code /review

@@ -290,3 +321,39 @@ export function registerWorkspaceFileWriteRoutes(
handlePostFileEdit(req, res, deps),

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] Test coverage gaps across new workspace-qualified routes:

  • POST /workspaces/:workspace/file/edit is registered here but has no test (only file/write is tested)
  • MCP actions disable, authenticate, clear-auth in workspace-mcp-control.ts are untested (only enable has a test)
  • File-read sub-routes /file/bytes, /stat, /list, /glob have no workspace-qualified test
  • ~15+ WorkspaceDaemonClient SDK methods have no test coverage
  • Telemetry route patterns are tested only for the agent route family (~5 of ~25 patterns)

A miswired route (e.g., wrong handler mapping in the MCP action loop) or a URL-encoding bug in the SDK would ship undetected.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Re-verification report — head 1c9ee5731

I rebuilt from source and re-ran the full runtime suite from my previous report against the updated head, plus new probes for the changes this round introduced.

Three of my four substantive findings are fixed, and the fixes are backed by real tests. But the round also introduced a regression that has turned CI red, on routes outside this PR's scope.


✅ Fixed and verified

Finding 1 — symlinked cwd selectors. resolveRegisteredWorkspaceRuntimeByPathSelector now falls back to canonicalizeWorkspace(). Re-run against a live 4-workspace daemon with ws/secondary-link → ws/secondary:

Request before now
GET /workspaces/<symlink>/file 400 200
GET /workspaces/<symlink>/sessions 400 200
GET /workspaces/<symlink>/settings / /mcp 400 200
POST /session {cwd:<symlink>} (control) 200 200

Telemetry attribution followed the fix — the symlinked selector now resolves to the secondary workspace instead of falling back to primary:

[TELEMETRY] GET /workspaces/%2F…%2Fws%2Fsecondary-link/file  wsHash=20c04f1684ce94f2   ← was 5692ffac (primary)

Single-decode, proven positively. The error body no longer echoes the selector, so I registered a fourth workspace whose cwd literally contains %20: /var/tmp/…/ws/pct%20name. A double-decoding resolver would turn the encoded selector into …/pct name and miss it.

GET /workspaces/29ed285b908d6e1d/file                       → 200   (by id)
GET /workspaces/%2F…%2Fws%2Fpct%2520name/file               → 200   (by encoded cwd, %25 survives)
GET /workspaces/%2F…%2Fws%2Fpct%20name/file                 → 400   (what a double decode would produce)

Finding 2 — untrusted-primary asymmetry. Behavior is unchanged (still 10/12 read-only routes diverge on a DO_NOT_TRUST primary), but it is now an explicit, documented contract: "This plural trust gate is intentionally stricter than some legacy primary-workspace read routes, which keep their existing compatibility behavior and are not drop-in replacements." That's exactly what I asked for. Accepted.

Finding 6 — doc drift. All three items corrected: the selector really does canonicalize now; the file-write trust policy text matches the 403 untrusted_workspace short-circuit; and the single-workspace discoverability gap is documented (clients use capabilities.workspaceCwd as the cwd selector). Verified live: on a single-workspace daemon capabilities.workspaces[] is absent, workspaceCwd is present, and the cwd selector returns 200.

Finding 5 — mutation testing now has teeth. This is the important one. Re-running my mutations against the grown suites (workspace-qualified-rest.test.ts + multi-workspace-sessions.test.ts, 50 tests baseline):

Mutation last round now
remove the canonicalizeWorkspace fallback (regress the symlink fix) n/a 1 test fails
drop the plural memory scope gate 0 fail ❌ 1 test fails
point plural sessions/archive + unarchive at boundWorkspace 0 fail ❌ 1 test fails
point plural sessions/delete at boundWorkspace 0 fail ❌ 1 test fails
point plural session-groups CRUD at boundWorkspace 0 fail ❌ 1 test fails
requireTrustedWorkspaceRuntime → always true 7 fail ✅ still covered ✅

The destructive persisted-session routes are now regression-protected against silently targeting the wrong workspace. That was my main merge blocker and it's resolved.

Unchanged and still correct: trust gate matrix (all 40 plural routes against the untrusted workspace — 0 ungated), FS confinement (../primary/p.txt, /etc/passwd400 path_outside_workspace on trusted and untrusted runtimes), per-workspace session isolation, route/SDK parity (out-of-scope surfaces still 404).


🔴 New regression: CI is red, and the cause is a breaking change to pre-existing routes

Test (ubuntu-latest, Node 22.x) fails. I reproduced it locally — 3 failures in src/serve/server.test.ts, all the same cause:

× createServeApp > POST /session/:id/load and /resume > 400s unknown explicit cwd before touching the bridge
× createServeApp > POST /session/:id/load and /resume > 400 workspace_mismatch before touching the bridge for non-primary cwd
× createServeApp > GET /workspace/:id/sessions > 400 workspace_mismatch when querying a cross-workspace path (#3803 §02)

expect(res.body.boundWorkspace).toBe(WS_BOUND)   →   received: undefined

This round rewrote sendWorkspaceMismatch and the untrusted-403 body to strip boundWorkspace, requestedWorkspace, workspaceCwd, and workspaceId — but it applied that to the shared helper and to session.ts's local closure, which serve pre-existing routes. Measured on a live daemon:

Route (exists on main today) main response this PR
POST /session (unregistered cwd) {error, code, boundWorkspace, requestedWorkspace} {error, code, workspaceCount}
POST /session (registered, untrusted cwd) {error, code, workspaceCwd} {error, code}
GET /workspace/<unknown>/sessions {…, boundWorkspace, requestedWorkspace} {error, code, workspaceCount}

Three consequences:

  1. CI is red. Those 3 tests are unmodified by this PR — they come straight from main. The only change the PR makes to server.test.ts is adding workspace_qualified_rest_core to the capability baseline.

  2. A documented contract is now wrong. docs/developers/qwen-serve-protocol.md still specifies the WorkspaceMismatchError body for POST /session as containing boundWorkspace and requestedWorkspace, and tells clients to "route the request to a daemon bound to requestedWorkspace." That section wasn't updated.

  3. It contradicts "no intended breaking changes" in the PR description. POST /session and GET /workspace/:id/sessions are outside this PR's stated scope.

On the motivation: I asked for this strip in an inline comment, framing it as an anti-enumeration bypass. Having now measured it, I don't think that framing holds, and I'd rather correct myself than have you carry a broken contract for it. /capabilities sits behind the same bearer token as the mismatch route:

no token:  GET /capabilities                        → 401
no token:  GET /workspaces/%2Fno%2Fsuch/file        → 401
token:     GET /capabilities → workspaceCwd: /…/ws/primary
                               workspaces[].cwd: primary | secondary | untrusted | pct%20name

Any caller who can trigger a workspace_mismatch can already read every workspace path from /capabilities. So stripping the fields closes nothing, while costing three tests and a documented, useful field. requestedWorkspace in particular is just the caller's own input echoed back.

Suggested resolution — pick one:

  • (a) Preferred: keep the minimal bodies on the new plural routes only, and restore boundWorkspace / requestedWorkspace / workspaceCwd on POST /session, POST /session/:id/load|resume, and GET /workspace/:id/sessions. Zero behavior change outside this PR's scope, CI green, docs stay true.
  • (b) Keep the strip everywhere, but then update the 3 tests, rewrite the WorkspaceMismatchError section of qwen-serve-protocol.md, and move the change out of a "no breaking changes" PR into its own so clients see it in the changelog.

🟡 Still open from last round (unchanged code)

Finding 3 — telemetry labels name routes that don't exist. server/telemetry.ts is byte-identical to last round. Re-traced from the running daemon:

POST /workspaces/:ws/file/write        → label "POST /workspace/file/write"     (no such route)
POST /workspaces/:ws/sessions/archive  → label "POST /workspace/sessions/archive" (real: POST /sessions/archive)
GET  /workspaces/:ws/file              → label "GET /file"                       (legacy GET /file emits nothing)

The GET /file metric series still counts only workspace-qualified traffic.

Finding 4 — scope-rejection vocabulary. The memory scope gate is now tested, but the vocabulary is still inconsistent. Re-measured:

scope plural /memory plural /agents plural /permissions
project 400 global_scope_not_supported… accepted 400 global_scope_not_supported…
wrokspace (typo) 400 global_scope_not_supported… 400 invalid_scope 400 global_scope_not_supported…

workspace-memory.ts:346 still reports a typo'd or omitted scope as "global scope not supported."

Both remain follow-up material, not blockers.


Note on workspace_qualified_rest_core gating

This round removed the persistSettingAvailable condition and instead documented that workspace-qualified settings also require the workspace_settings tag. That's a fine resolution — verified that qwen serve always sets persistSetting, so there's no observable regression; it only matters for embedders calling createServeApp directly, and the capability descriptor now says so explicitly.


Verdict

The substance of this PR is in good shape. Selector resolution, trust gating, per-workspace routing, and now the test coverage of the destructive session routes are all correct under real traffic — I mutated each one and the suite caught it.

The only thing standing between this and merge is the error-body regression. Fix it per (a) or (b), get CI green, and I'm happy to approve. Findings 3 and 4 can land as a follow-up.

中文版

复验报告 —— head 1c9ee5731

我重新从源码构建,并针对更新后的 head 重跑了上一份报告里的完整运行时套件,同时针对本轮新引入的改动补了新的探测。

我提出的四个实质性问题中,三个已修复,而且修复都有真实测试兜底。 但本轮也引入了一个回归,它让 CI 变红,且影响的是本 PR 范围之外的既有路由。


✅ 已修复并验证

发现 1 —— 符号链接 cwd selector。 resolveRegisteredWorkspaceRuntimeByPathSelector 现在会回退到 canonicalizeWorkspace()。在真实的 4-workspace daemon 上重跑(ws/secondary-link → ws/secondary):

请求 之前 现在
GET /workspaces/<symlink>/file 400 200
GET /workspaces/<symlink>/sessions 400 200
GET /workspaces/<symlink>/settings / /mcp 400 200
POST /session {cwd:<symlink>}(对照组) 200 200

telemetry 归属也随之修正——符号链接 selector 现在归属到 secondary,而不是回退到 primary:

[TELEMETRY] GET /workspaces/%2F…%2Fws%2Fsecondary-link/file  wsHash=20c04f1684ce94f2   ← 之前是 5692ffac(primary)

单次解码,这次是正面证明。 错误响应体不再回显 selector,所以我注册了第四个 workspace,其 cwd 路径中真的含有 %20/var/tmp/…/ws/pct%20name。如果解析器解码两次,编码后的 selector 会变成 …/pct name,从而匹配不到它。

GET /workspaces/29ed285b908d6e1d/file                       → 200   (按 id)
GET /workspaces/%2F…%2Fws%2Fpct%2520name/file               → 200   (按编码 cwd,%25 被保留)
GET /workspaces/%2F…%2Fws%2Fpct%20name/file                 → 400   (这正是双重解码会产生的结果)

发现 2 —— 未受信 primary 的不对称。 行为没有变(在 DO_NOT_TRUST 的 primary 上仍有 12 条只读路由中的 10 条不一致),但现在它是一个明确的、写进文档的契约:"This plural trust gate is intentionally stricter than some legacy primary-workspace read routes, which keep their existing compatibility behavior and are not drop-in replacements." 这正是我要求的。接受。

发现 6 —— 文档偏差。 三处全部修正:selector 现在确实做 canonicalization;file-write 的信任策略描述与 403 untrusted_workspace 短路一致;单 workspace 下的可发现性缺口也写进了文档(clients use capabilities.workspaceCwd as the cwd selector)。实测确认:单 workspace daemon 上 capabilities.workspaces[] 缺席,workspaceCwd 存在,cwd selector 返回 200

发现 5 —— 变异测试现在有牙齿了。 这是最重要的一点。针对扩充后的测试套件(workspace-qualified-rest.test.ts + multi-workspace-sessions.test.ts,基线 50 个测试)重跑我的变异:

变异 上一轮 现在
删掉 canonicalizeWorkspace 回退(回退符号链接修复) 1 个测试失败
删掉 plural memory 的 scope 门禁 0 失败 ❌ 1 个测试失败
把 plural sessions/archive + unarchive 指向 boundWorkspace 0 失败 ❌ 1 个测试失败
把 plural sessions/delete 指向 boundWorkspace 0 失败 ❌ 1 个测试失败
把 plural session-groups CRUD 指向 boundWorkspace 0 失败 ❌ 1 个测试失败
requireTrustedWorkspaceRuntime → 恒 true 7 失败 ✅ 仍被覆盖 ✅

破坏性的持久化 session 路由现在对"静默打到错误 workspace"有了回归保护。这是我上次的主要合并阻塞项,已经解决。

保持不变且依然正确的部分: 信任门禁矩阵(全部 40 条 plural 路由对未受信 workspace —— 零条未受门禁)、文件系统边界(../primary/p.txt/etc/passwd → 在受信与未受信 runtime 上都是 400 path_outside_workspace)、按 workspace 的 session 隔离、路由与 SDK 的对齐(范围外的面仍然 404)。


🔴 新回归:CI 变红,根因是对既有路由的破坏性改动

Test (ubuntu-latest, Node 22.x) 失败。我在本地复现了——src/serve/server.test.ts 中 3 个失败,根因相同:

× createServeApp > POST /session/:id/load and /resume > 400s unknown explicit cwd before touching the bridge
× createServeApp > POST /session/:id/load and /resume > 400 workspace_mismatch before touching the bridge for non-primary cwd
× createServeApp > GET /workspace/:id/sessions > 400 workspace_mismatch when querying a cross-workspace path (#3803 §02)

expect(res.body.boundWorkspace).toBe(WS_BOUND)   →   received: undefined

本轮重写了 sendWorkspaceMismatch 和未受信 403 的响应体,剥离了 boundWorkspacerequestedWorkspaceworkspaceCwdworkspaceId——但改动同时落在了共享 helpersession.ts 里的局部闭包上,而后者服务的是既有路由。在真实 daemon 上实测:

路由(main 上今天就有) main 的响应 本 PR
POST /session(未注册 cwd) {error, code, boundWorkspace, requestedWorkspace} {error, code, workspaceCount}
POST /session(已注册但未受信 cwd) {error, code, workspaceCwd} {error, code}
GET /workspace/<unknown>/sessions {…, boundWorkspace, requestedWorkspace} {error, code, workspaceCount}

三个后果:

  1. CI 红了。 那 3 个测试并未被本 PR 修改——它们直接来自 main。本 PR 对 server.test.ts 的唯一改动是把 workspace_qualified_rest_core 加进 capability 基线。

  2. 一份写进文档的契约现在是错的。 docs/developers/qwen-serve-protocol.md 仍然把 POST /sessionWorkspaceMismatchError 响应体规定为包含 boundWorkspacerequestedWorkspace,并告诉客户端 "route the request to a daemon bound to requestedWorkspace." 这一节没有被更新。

  3. 它与 PR 描述里的 "no intended breaking changes" 相矛盾。 POST /sessionGET /workspace/:id/sessions 都在本 PR 声明的范围之外。

关于动机: 这个剥离是我在一条 inline comment 里要求的,当时我把它定性为"绕过 403 反枚举"。现在实测之后,我认为那个定性站不住脚,与其让你为它背上一个被破坏的契约,不如我先纠正自己。/capabilities 和 mismatch 路由处在同一道 bearer token 之后:

无 token:  GET /capabilities                        → 401
无 token:  GET /workspaces/%2Fno%2Fsuch/file        → 401
有 token:  GET /capabilities → workspaceCwd: /…/ws/primary
                               workspaces[].cwd: primary | secondary | untrusted | pct%20name

任何能触发 workspace_mismatch 的调用方,本来就能从 /capabilities 读到全部 workspace 路径。所以剥离这些字段并没有关掉任何缺口,代价却是三个测试和一个有文档、有用处的字段。尤其 requestedWorkspace 只是把调用方自己的输入回显一遍。

建议的处理方式——二选一:

  • (a) 推荐: 只在新的 plural 路由上保留精简响应体,并在 POST /sessionPOST /session/:id/load|resumeGET /workspace/:id/sessions 上恢复 boundWorkspace / requestedWorkspace / workspaceCwd。本 PR 范围之外零行为变更,CI 变绿,文档保持正确。
  • (b) 全面保留剥离,但相应地更新那 3 个测试、重写 qwen-serve-protocol.mdWorkspaceMismatchError 一节,并把这个改动从"无破坏性变更"的 PR 里拆出去单独提,好让客户端能在 changelog 里看到。

🟡 上一轮遗留、代码未变

发现 3 —— telemetry 标签指向不存在的路由。 server/telemetry.ts 与上一轮逐字节相同。从运行中的 daemon 重新抓取:

POST /workspaces/:ws/file/write        → 标签 "POST /workspace/file/write"       (不存在此路由)
POST /workspaces/:ws/sessions/archive  → 标签 "POST /workspace/sessions/archive" (真实的是 POST /sessions/archive)
GET  /workspaces/:ws/file              → 标签 "GET /file"                        (legacy GET /file 不发 telemetry)

名为 GET /file 的指标序列仍然只统计 workspace-qualified 的流量。

发现 4 —— scope 拒绝措辞。 memory 的 scope 门禁现在有测试了,但措辞依旧不一致。重新实测:

scope plural /memory plural /agents plural /permissions
project 400 global_scope_not_supported… 接受 400 global_scope_not_supported…
wrokspace(拼写错误) 400 global_scope_not_supported… 400 invalid_scope 400 global_scope_not_supported…

workspace-memory.ts:346 仍然把拼错或省略的 scope 报成 "global scope not supported"

这两项都属于后续改进,不是阻塞项。


关于 workspace_qualified_rest_core 的 gating

本轮移除了 persistSettingAvailable 条件,改为在文档中说明 workspace-qualified settings 还需要 workspace_settings tag。这是一个合理的处理方式——我确认了 qwen serve 总会设置 persistSetting,所以没有可观察到的回归;它只影响直接调用 createServeApp 的嵌入方,而 capability 描述里现在已经明确写了这一点。


结论

这个 PR 的实质内容状态良好。selector 解析、信任门禁、按 workspace 路由,以及现在破坏性 session 路由的测试覆盖,在真实流量下都是正确的——我对每一条都做了变异,测试套件都抓到了。

距离合并只差那个错误响应体的回归。 按 (a) 或 (b) 处理,把 CI 弄绿,我就可以 approve。发现 3 和 4 可以作为后续 PR。

const canonicalMatch = registry.getByWorkspaceCwd(canonicalSelector);
if (canonicalMatch) return canonicalMatch;
for (const runtime of registry.list()) {
if (canonicalizeWorkspace(runtime.workspaceCwd) === canonicalSelector) {

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] Redundant canonicalizeWorkspace() on already-canonical registry entries. The workspace registry stores CWDs that were canonicalized at boot time (via canonicalizeWorkspace() before insertion into byCwd), so calling canonicalizeWorkspace(runtime.workspaceCwd) here re-runs realpathSync.native — a synchronous filesystem syscall — on paths that are already canonical.

For N registered workspaces, this performs N unnecessary syscalls on every path-selector miss, blocking the event loop each time.

Suggested fix: Compare canonicalSelector directly against runtime.workspaceCwd:

for (const runtime of registry.list()) {
  if (runtime.workspaceCwd === canonicalSelector) {
    return runtime;
  }
}

— incremental review since 718e80a

if (!isPortableAbsolutePath(selector)) {
res.status(400).json({
error: `\`:${paramName}\` must decode to a workspace id or absolute path`,
code: 'workspace_mismatch',

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] Differentiated error responses enable workspace enumeration. The resolver returns three distinguishable response classes:

Selector input Status Body
Not a valid format 400 {code: "workspace_mismatch"} (no workspaceCount)
Valid absolute path, unregistered 400 {code: "workspace_mismatch", workspaceCount: N}
Registered, untrusted 403 {code: "untrusted_workspace"}

An attacker probing selectors can distinguish all three states to enumerate registered workspace paths, learn trust status, and learn the total workspace count.

Consider returning the same opaque 400 response for both invalid-format and unregistered selectors, and removing workspaceCount from the error body.

— incremental review since 718e80a

});
res.status(200).json({
key,
scope,

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] Route-parity maintenance risk: this plural settings POST inlines deps.invalidateServeFeaturesCache() + runtime.bridge.publishWorkspaceEvent(...) directly, while the legacy route delegates to the shared broadcastSettingsChanged helper (defined in server.ts:973).

The inlining is intentional here (different bridge target: runtime.bridge vs primaryBridge), but if broadcastSettingsChanged ever gains additional side effects (logging, metrics, etc.), this plural route will not pick them up automatically.

Consider extracting a shared helper that accepts the bridge as a parameter:

broadcastSettingsChanged(key, value, scope, clientId, bridge);

— incremental review since 718e80a

);
}

fileStat(filePath: string): Promise<unknown> {

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] fileStat, dirList, and glob are the only WorkspaceDaemonClient methods that return Promise<unknown>. Every other method uses typed interfaces (DaemonWorkspaceFile, DaemonToolToggleResult, etc.).

Consumers get no type safety and must cast locally. Consider defining DaemonFileStat, DaemonDirList, and DaemonGlobResult interfaces matching the server response shapes.

— incremental review since 718e80a

@wenshao

wenshao commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

/review — commit 1c9ee573172d734408175b0d797c531429c36765

This is an incremental review since commit 718e80ad. Many Critical findings from the previous review have been addressed: sendWorkspaceMismatch no longer leaks boundWorkspace/requestedWorkspace, sendUntrustedWorkspaceResponse correctly omits workspaceCwd/workspaceId, and the 400 response body no longer exposes the primary workspace path.

Build ✅ · Typecheck ✅ · Lint ✅ · SDK tests 217/217 ✅ · Telemetry tests 10/10 ✅

(workspace-qualified-rest.test.ts fails in this worktree due to a missing @aspect-build/shell native module — environment issue, not a code defect.)

New findings (not covered by previous review)

# File Severity Issue
1 workspace-route-runtime.ts:52-54 Suggestion Redundant canonicalizeWorkspace() on already-canonical registry entries in the fallback loop — unnecessary realpathSync syscalls
2 DaemonClient.ts:3615-3631 Suggestion fileStat/dirList/glob return Promise<unknown> — only untyped methods on the class
3 workspace-route-runtime.ts:82-98 Suggestion Differentiated error responses (400 without workspaceCount for invalid format vs 400 with workspaceCount for unregistered path vs 403 for untrusted) enable workspace enumeration
4 workspace-settings.ts:449-453 Suggestion Plural settings POST inlines cache invalidation + bridge event instead of shared helper — route-parity maintenance risk
5 Test coverage Suggestion Agents routes missing untrusted-workspace rejection test; settings validation error branches, permissions invalid_rule_type, and trust error branches untested

Verdict

Comment. The PR is in significantly better shape than the previous revision. The core architecture (shared resolver, trust helpers, SDK client) is well-designed. The remaining findings are Suggestion-level improvements that can be addressed in follow-up commits.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@wenshao

wenshao commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts, but the head branch changed while resolving, so the update was not pushed. Re-run /resolve. The resolved diff is attached as the qwen-resolve-pr-6567 artifact on the workflow run.

Merge Conflict Resolution Summary — PR #6567

Base branch

main (fd613ea)

Conflicts

1. packages/cli/src/serve/server.ts — imports (line ~155)

PR (HEAD): Added workspace-qualified route imports:

  • isPortableAbsolutePath, resolveRegisteredWorkspaceRuntimeByPathSelector from ./workspace-route-runtime.js
  • registerWorkspaceQualifiedLifecycleRoutes from ./routes/workspace-lifecycle.js
  • registerWorkspaceQualifiedMcpControlRoutes from ./routes/workspace-mcp-control.js
  • registerWorkspaceQualifiedToolsRoutes from ./routes/workspace-tools.js

Main (origin/main): Added:

  • registerWorkspaceChannelControlRoutes from ./routes/workspace-channel-control.js

Resolution: Kept all PR imports plus the new channel control import from main. The two sides add independent route registrations — no semantic conflict.

2. packages/cli/src/serve/server.ts — route registration (line ~1112)

PR (HEAD): Registered registerWorkspaceQualifiedMcpControlRoutes(app, { workspaceRegistry, ... }).

Main (origin/main): Added a conditional block that registers registerWorkspaceChannelControlRoutes when deps.getChannelWorkerSnapshot and deps.reloadChannelWorker are present.

Resolution: Kept both blocks. The qualified MCP control routes and the channel control routes are independent features and do not overlap.

3. packages/sdk-typescript/scripts/build.js — bundle size budget (line ~50)

PR (HEAD): Bumped MAX_DAEMON_BROWSER_BUNDLE_BYTES from 139KB to 150KB (workspace-qualified REST helpers).

Main (origin/main): Bumped from 139KB to 141KB (ACP status/preheat APIs).

Resolution: Set the limit to 153KB (150KB + 3KB for main's additions). Added a comment line documenting the incremental bump from merging main.

Commit

chore: merge origin/main into PR branch, resolve conflicts in server.ts and build.js

@doudouOUC
doudouOUC requested a review from wenshao July 9, 2026 13:29
@wenshao

wenshao commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Re-verification report — head 0c3cbe229

Rebuilt from source and re-ran the full runtime suite plus every mutation from my two earlier reports (#1, #2) against the new head, which merges current main.

The blocker is resolved, and resolved the right way. Every fix from the previous rounds survived the merge. From my side this is ready to merge once CI reports green — two low-severity follow-ups remain, neither blocking.


✅ The error-body regression is fixed (option (a))

bb0eba482 restores the fields on the pre-existing routes while leaving the new plural surface minimal — exactly the split I asked for. Measured on a live 4-workspace daemon:

Route body now
POST /session (unregistered cwd) {error, code, boundWorkspace, workspaceCount, requestedWorkspace}
POST /session (registered, untrusted) {error, code, workspaceCwd, workspaceId}
POST /session/:id/load / /resume (bad cwd) {…, boundWorkspace, requestedWorkspace}
GET /workspace/<unknown>/sessions {…, boundWorkspace, requestedWorkspace}
GET /workspaces/<unknown>/settings (new) {error, code, workspaceCount} — stays minimal ✅
GET /workspaces/<untrusted>/settings (new) {error, code} — stays minimal ✅

On a single-workspace daemon the POST /session mismatch body matches the WorkspaceMismatchError example in qwen-serve-protocol.md verbatim, including the boundWorkspace / requestedWorkspace fields the doc tells clients to use. No doc change needed.

The 3 tests that were red are green: npx vitest run src/serve/server.test.ts -t "workspace_mismatch"3 passed. All five directly-affected suites → 728 passed, 0 failed.

And the restored fields have teeth. I checked that the intermediate commit d6c619b26 ("fix CI failure") had made CI pass by weakening the assertions to expect(res.body.boundWorkspace).toBeUndefined(); bb0eba482 reverts that and asserts the real values. So I re-applied the strip as a mutation:

Mutation tests failed
re-strip boundWorkspace / requestedWorkspace from the legacy sendWorkspaceMismatch 6
re-strip workspaceCwd / workspaceId from the legacy untrusted 403 2

A future regression here can't slip through silently again.


✅ Everything from the earlier rounds survived the main merge

The merge pulled in a lot (session-artifact persistence #6557, channel worker reload #6598, MCP prompt fix #6571, …), so I re-ran the whole matrix rather than assuming.

Runtime, on real daemons:

  • Selector matrix: id / encoded cwd / trailing slash / .. segments / symlink alias → all 200; unknown id, unknown path, relative, Windows drive, UNC → clean 400 workspace_mismatch, no crash.
  • Single-decode still proven positively against the workspace whose cwd literally contains %20: by-id 200, by-encoded-cwd 200, and the path a double-decode would produce → 400.
  • Trust gate across all 40 plural routes on the untrusted workspace: 0 ungated.
  • FS confinement per runtime: ../primary/p.txt and /etc/passwd400 path_outside_workspace from both trusted and untrusted runtimes.
  • Session isolation: GET /workspaces/<primary>/sessions excludes the secondary's session; POST /workspaces/<primary>/sessions/archive with the secondary's id returns notFound.
  • Untrusted-primary asymmetry unchanged (10/12 read-only routes diverge) — now an explicitly documented, intentional contract. Accepted.

Mutation, against the grown suites (50 tests baseline):

Mutation tests failed
remove the canonicalizeWorkspace fallback (regress the symlink fix) 1 ✅
point plural sessions/archive + unarchive at boundWorkspace 1 ✅
point plural sessions/delete at boundWorkspace 1 ✅

On the 12 failures you may see locally

A full src/serve run in my sandbox shows 12 failures across fast-path.test.ts, workspace-memory.test.ts, workspace-agents.test.ts, and fs/workspace-file-system.test.ts. These are not yours. I swapped in origin/main's src/serve as a control and got the identical 12 failures — they're environment artifacts (root user, no real npm install), not a regression.


🟡 Two follow-ups, unchanged and non-blocking

Both files are byte-identical to the previous head, so my earlier evidence stands.

Telemetry route labels (server/telemetry.ts) still name routes that don't exist:

POST /workspaces/:ws/file/write        → label "POST /workspace/file/write"       (no such route)
POST /workspaces/:ws/sessions/archive  → label "POST /workspace/sessions/archive"  (real: POST /sessions/archive)
GET  /workspaces/:ws/file              → label "GET /file"                         (legacy GET /file emits nothing,
                                                                                    so that series counts only plural traffic)

Scope-rejection vocabulary (workspace-memory.ts:346) still reports a typo'd or omitted scope as "global scope not supported", and scope: "project" is accepted by plural /agents but rejected as a "global scope" by plural /memory and /permissions.

Nit: the untrusted 403 message is now the generic "Workspace is not trusted." on pre-existing routes too, where main embedded the path (Workspace "<cwd>" is not trusted.). The structured workspaceCwd / workspaceId fields carry that information, so only a client string-matching the message would notice. Fine as-is; mentioning it for the changelog.


Verdict

I'm satisfied. Selector resolution, trust gating, per-workspace routing, FS confinement, session isolation, and the error contract are all correct under real traffic, and I mutated each load-bearing behavior to confirm the tests would catch a regression. The main merge didn't erode anything.

LGTM once Test (ubuntu-latest) reports green — it was still running when I posted this, but the three tests it failed on now pass locally and the only other failures reproduce on main. The two follow-ups above are worth a separate PR.

中文版

复验报告 —— head 0c3cbe229

重新从源码构建,并针对合并了当前 main 的新 head,重跑了完整运行时套件,以及我前两份报告(#1#2)中的全部变异测试。

阻塞项已解决,而且是以正确的方式解决的。 前两轮的每一个修复都在这次 merge 后存活了下来。就我这边而言,只要 CI 变绿即可合并——剩下两个低严重度的后续项,都不构成阻塞。


✅ 错误响应体的回归已修复(方案 (a))

bb0eba482既有路由上恢复了字段,同时让新的 plural 路由面保持精简——正是我建议的切分方式。在真实的 4-workspace daemon 上实测:

路由 现在的响应体
POST /session(未注册 cwd) {error, code, boundWorkspace, workspaceCount, requestedWorkspace}
POST /session(已注册但未受信) {error, code, workspaceCwd, workspaceId}
POST /session/:id/load / /resume(错误 cwd) {…, boundWorkspace, requestedWorkspace}
GET /workspace/<unknown>/sessions {…, boundWorkspace, requestedWorkspace}
GET /workspaces/<unknown>/settings(新) {error, code, workspaceCount} —— 保持精简 ✅
GET /workspaces/<untrusted>/settings(新) {error, code} —— 保持精简 ✅

在单 workspace daemon 上,POST /session 的 mismatch 响应体与 qwen-serve-protocol.mdWorkspaceMismatchError 的示例逐字一致,包括文档让客户端使用的 boundWorkspace / requestedWorkspace 字段。文档无需改动。

之前红掉的 3 个测试已经变绿:npx vitest run src/serve/server.test.ts -t "workspace_mismatch"3 passed。五个直接受影响的测试文件 → 728 passed,0 failed

而且恢复的字段是有牙齿的。 我查了一下,中间那个 commit d6c619b26("fix CI failure")是通过弱化断言(改成 expect(res.body.boundWorkspace).toBeUndefined())让 CI 通过的;bb0eba482 把它改了回来,并断言了真实值。于是我把剥离作为变异重新施加了一次:

变异 失败测试数
在 legacy sendWorkspaceMismatch 上重新剥离 boundWorkspace / requestedWorkspace 6
在 legacy 未受信 403 上重新剥离 workspaceCwd / workspaceId 2

将来这里的回归不会再被静默放过。


✅ 前几轮的修复全部在 main merge 后存活

这次 merge 带进来的东西不少(session artifact 持久化 #6557、channel worker reload #6598、MCP prompt 修复 #6571 等),所以我没有想当然,而是把整个矩阵重跑了一遍。

运行时,真实 daemon 上:

  • selector 矩阵:id / 编码 cwd / 结尾斜杠 / .. 段 / 符号链接别名 → 全部 200;未知 id、未知路径、相对路径、Windows 盘符、UNC → 干净的 400 workspace_mismatch,不崩溃。
  • 单次解码仍以那个 cwd 中真的含有 %20 的 workspace 正面证明:按 id 200,按编码 cwd 200,而双重解码会产生的那个路径 → 400
  • 未受信 workspace 上全部 40 条 plural 路由的信任门禁:零条未受门禁
  • 按 runtime 的文件系统边界:../primary/p.txt/etc/passwd 在受信与未受信 runtime 上都返回 400 path_outside_workspace
  • session 隔离:GET /workspaces/<primary>/sessions 不含 secondary 的 session;POST /workspaces/<primary>/sessions/archive 传 secondary 的 id 返回 notFound
  • 未受信 primary 的不对称保持不变(12 条只读路由中 10 条不一致)—— 现在是明确写进文档的、有意的契约。接受。

变异,针对扩充后的套件(基线 50 个测试):

变异 失败测试数
移除 canonicalizeWorkspace 回退(回退符号链接修复) 1 ✅
把 plural sessions/archive + unarchive 指向 boundWorkspace 1 ✅
把 plural sessions/delete 指向 boundWorkspace 1 ✅

关于你本地可能看到的 12 个失败

在我的沙箱里跑完整的 src/serve 会看到 12 个失败,分布在 fast-path.test.tsworkspace-memory.test.tsworkspace-agents.test.tsfs/workspace-file-system.test.ts这些不是你造成的。 我把 origin/mainsrc/serve 换进来作为对照组,得到完全相同的 12 个失败——它们是环境导致的(root 用户、没有真正的 npm install),不是回归。


🟡 两个后续项,未变更,不阻塞

这两个文件与上一个 head 逐字节相同,所以我之前的证据依然成立。

telemetry 路由标签server/telemetry.ts)仍然指向不存在的路由:

POST /workspaces/:ws/file/write        → 标签 "POST /workspace/file/write"        (不存在此路由)
POST /workspaces/:ws/sessions/archive  → 标签 "POST /workspace/sessions/archive"  (真实的是 POST /sessions/archive)
GET  /workspaces/:ws/file              → 标签 "GET /file"                         (legacy GET /file 不发 telemetry,
                                                                                   所以该序列只统计 plural 流量)

scope 拒绝措辞workspace-memory.ts:346)仍把拼错或省略的 scope 报成 "global scope not supported";并且 scope: "project" 被 plural /agents 接受,却被 plural /memory/permissions 当作 "global scope" 拒绝。

小点: 未受信 403 的消息在既有路由上现在也是通用的 "Workspace is not trusted.",而 main 会把路径嵌进去(Workspace "<cwd>" is not trusted.)。结构化的 workspaceCwd / workspaceId 字段已经承载了这个信息,所以只有对消息做字符串匹配的客户端才会察觉。保持现状即可;提一句是为了 changelog。


结论

我满意了。selector 解析、信任门禁、按 workspace 路由、文件系统边界、session 隔离,以及错误契约,在真实流量下都是正确的;我对每一个承重行为都做了变异,确认测试能抓到回归。main 的 merge 没有侵蚀任何东西。

只要 Test (ubuntu-latest) 变绿就 LGTM —— 我发这条时它还在跑,但它之前失败的那三个测试在本地已经通过,而其余失败在 main 上同样复现。上面两个后续项值得单独开一个 PR。

@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.

⚠️ Downgraded from Approve to Comment: CI pending (Test ubuntu-latest).

Incremental review since commit 1c9ee5731. The author's fixes in bb0eba482 correctly address the session-route portion of the previous Critical findings — sendWorkspaceMismatch now includes boundWorkspace and requestedWorkspace, and sendUntrustedWorkspaceResponse now passes workspaceCwd/workspaceId in session routes. Tests are updated to match.

The shared helper sendWorkspaceMismatch in workspace-route-runtime.ts and requireTrustedWorkspaceRuntime still omit these fields for workspace-qualified routes (12+ callers). This inconsistency was discussed in the prior review and approved — no new inline comments needed.

Build passes. 230/230 collectable tests pass. The workspace-qualified-rest.test.ts collection failure is a pre-existing @xterm/headless environment issue, not PR-specific.

— qwen3.7-max via Qwen Code /review

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@yiliang114

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@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. ✅

@doudouOUC
doudouOUC added this pull request to the merge queue Jul 9, 2026
Merged via the queue into QwenLM:main with commit f5d36aa Jul 9, 2026
47 of 48 checks passed
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.

6 participants