You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adds a "Mutation: approval, tools, init, MCP restart" section to the
developer protocol doc covering all four PR 17 routes:
- POST /session/:id/approval-mode — `{mode, persist?}` request, four
closed-enum modes, trust-gate 403 with `errorKind: 'auth_env_error'`,
`approval_mode_changed` SSE event (session-scoped)
- POST /workspace/tools/:name/enable — `{enabled}` request, unknown
names accepted, "next-spawn semantics" call-out, `tool_toggled`
SSE event (workspace-scoped fan-out)
- POST /workspace/init — `{force?}` request, scaffold-only contract
(no LLM call), 409 with `path` + `existingSize` body when the
target exists with non-whitespace content, `workspace_initialized`
SSE event (workspace-scoped)
- POST /workspace/mcp/:server/restart — empty body, soft-skip
decision table (in_flight / disabled / budget_would_exceed),
`mcp_server_restarted` and `mcp_server_restart_refused` SSE events
Capability list at the top of the file updated with the four new
tags (and a missing-from-PR-13 fix for `workspace_env` /
`workspace_preflight`).
User-facing `qwen-serve.md` gains a one-line "Remote runtime control"
bullet under "What it gives you" pointing to the four routes and
clarifying that `/workspace/init` is mechanical only.
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
`session_scope_override` is the negotiation handle for the per-request `sessionScope` field on `POST /session` (see below). Older daemons silently ignore the field, so SDK clients should pre-flight `caps.features` for this tag before sending it.
@@ -114,6 +117,8 @@ registry. Clients **must** gate UI off `features`, not off `mode` (per design
114
117
115
118
`session_close` and `session_metadata` advertise `DELETE /session/:id` and `PATCH /session/:id/metadata`. Older daemons return `404`; pre-flight these tags before exposing close or rename affordances.
116
119
120
+
`session_approval_mode_control`, `workspace_tool_toggle`, `workspace_init`, and `workspace_mcp_restart` (issue [#4175](https://github.com/QwenLM/qwen-code/issues/4175) PR 17) advertise the four mutation control routes documented under "Mutation: approval, tools, init, MCP restart" below. All four are strict-gated by the PR 15 mutation gate (a daemon configured without a bearer token rejects them with 401 `token_required`). Older daemons return `404`; pre-flight each tag before exposing the corresponding affordance.
121
+
117
122
`mcp_guardrails` (issue [#4175](https://github.com/QwenLM/qwen-code/issues/4175) PR 14) covers the MCP budget surface: the `clientCount` / `clientBudget` / `budgetMode` / `budgets[]` fields on `GET /workspace/mcp`, the `disabledReason` field on per-server cells, and the `--mcp-client-budget` / `--mcp-budget-mode` CLI flags. Older daemons omit the new fields entirely; SDK clients pre-flight this tag before relying on `budgets[]` semantics. The registry descriptor also carries `modes: ['warn', 'enforce']` for future feature-modes exposure — for now, clients infer mode from the snapshot's `budgetMode` field. Server refusal under `enforce` mode is deterministic by `Object.entries(mcpServers)` declaration order; a future scope-precedence layer (if qwen-code adopts one) would shift this to "lowest-precedence first" to mirror claude-code's `plugin < user < project < local` convention.
118
123
119
124
> ⚠️ **PR 14 v1 scope: per-session, not per-workspace.** Each ACP session inside the daemon constructs its own `Config` + `McpClientManager` (via `acpAgent.newSessionConfig`). The budget caps live MCP clients **per session**; each session independently reads `QWEN_SERVE_MCP_CLIENT_BUDGET` from the forwarded env. With `--mcp-client-budget=10` and 5 concurrent ACP sessions, the actual live MCP client count can reach 5 × 10 = 50 across the daemon. The `GET /workspace/mcp` snapshot reads the **bootstrap session's**`McpClientManager` accounting only — the `budgets[0].scope: 'session'` value is the honest signal that this is per-session, not aggregated. **Wave 5 PR 23 (shared MCP pool)** will introduce a workspace-scoped manager and add a `scope: 'workspace'` cell alongside the per-session cell for true cross-session aggregation. v1 is the in-process counter + soft enforcement foundation that PR 23 builds on.
@@ -906,6 +911,147 @@ Response:
906
911
907
912
On success, publishes `model_switched` to the SSE stream. On failure, publishes `model_switch_failed` (so passive subscribers see the failure, not just the caller). Races against the agent channel exit so a wedged child can't block the HTTP handler.
908
913
914
+
### Mutation: approval, tools, init, MCP restart
915
+
916
+
Issue [#4175](https://github.com/QwenLM/qwen-code/issues/4175) Wave 4 PR 17 adds four mutation control routes that let remote clients change runtime posture without touching the daemon host's CLI. All four:
917
+
918
+
- Are gated by the **strict** mutation gate from PR 15. A daemon configured without a bearer token rejects them with `401 {code: 'token_required'}`. Configure `--token` (or `QWEN_SERVER_TOKEN`) before opting in.
919
+
- Accept and stamp the `X-Qwen-Client-Id` header (PR 7 audit chain). When the header carries a trusted id, the daemon emits `originatorClientId` on the corresponding SSE event so cross-client UIs can suppress echoes of their own mutations.
920
+
- Pre-flight each per-tag capability before exposing the affordance. Older daemons return `404` for the route.
921
+
922
+
Three of the four routes (`tools/:name/enable`, `init`, `mcp/:server/restart`) emit **workspace-scoped** events: every active session SSE bus receives the event, regardless of which session was attached when the mutation was triggered. `approval-mode` emits a **session-scoped** event because the change is local to one session's `Config`.
Change the approval mode of a live session. The new mode lands inside the ACP child's per-session `Config` immediately. Settings are NOT written to disk by default — pass `persist: true` to also write `tools.approvalMode` to workspace settings.
929
+
930
+
Request:
931
+
932
+
```json
933
+
{ "mode": "auto-edit", "persist": false }
934
+
```
935
+
936
+
`mode` must be one of `'plan' | 'default' | 'auto-edit' | 'yolo'` (mirror of core's `ApprovalMode` enum; the SDK exports `DAEMON_APPROVAL_MODES` for runtime validation). `persist` defaults to `false`.
-`400 {code: 'invalid_persist_flag'}` — `persist` is non-boolean.
953
+
-`403 {code: 'trust_gate', errorKind: 'auth_env_error'}` — the requested mode requires a trusted folder (privileged modes in untrusted workspaces are rejected by core's `Config.setApprovalMode`).
954
+
-`404` — session unknown.
955
+
956
+
SSE event (session-scoped): `approval_mode_changed` with `{sessionId, previous, next, persisted, originatorClientId?}`.
957
+
958
+
#### `POST /workspace/tools/:name/enable`
959
+
960
+
Capability tag: `workspace_tool_toggle`. Pure file IO — no ACP roundtrip.
961
+
962
+
Toggle a tool name in the workspace's `tools.disabled` settings list. Tools listed there are **not registered** at all (distinct from `permissions.deny`, which keeps the tool registered and rejects invocation). Both built-in tools (`Bash`, `Read`, `Write`) and MCP-discovered tools (`mcp__github__create_issue`) flow through `ToolRegistry.registerTool`, which consults the disabled set.
963
+
964
+
Live ACP children retain already-registered tools — the toggle takes effect on the **next** ACP child spawn. Combine with `POST /workspace/mcp/:server/restart` (for MCP-sourced tools) or new-session creation to make the change effective in the current daemon.
965
+
966
+
Unknown tool names are accepted: pre-disabling a not-yet-installed MCP tool is a legitimate use case.
-`400 {code: 'invalid_enabled_flag'}` — `enabled` missing or non-boolean.
984
+
985
+
SSE event (workspace-scoped): `tool_toggled` with `{toolName, enabled, originatorClientId?}`.
986
+
987
+
#### `POST /workspace/init`
988
+
989
+
Capability tag: `workspace_init`. Pure file IO — no ACP roundtrip, **no LLM invocation**.
990
+
991
+
Scaffold an empty `QWEN.md` (or whatever `getCurrentGeminiMdFilename()` returns under `--memory-file-name` overrides) at the daemon's bound workspace root. Mechanical only — for AI-driven content fill, follow up with `POST /session/:id/prompt`.
992
+
993
+
Default refuses to overwrite when the target file exists with non-whitespace content. Whitespace-only files are treated as absent (matches the local `/init` slash command).
`action` is `'created'` for fresh creates and whitespace-only overrides; `'overwrote'` when `force: true` replaced non-empty content.
1008
+
1009
+
Errors:
1010
+
1011
+
-`400 {code: 'invalid_force_flag'}` — `force` is non-boolean.
1012
+
-`409 {code: 'workspace_init_conflict', path, existingSize}` — file exists with non-whitespace content and `force` is omitted/false. Body carries the absolute path and size (bytes) so SDK clients can render an "overwrite N bytes?" prompt without re-stat'ing.
1013
+
1014
+
SSE event (workspace-scoped): `workspace_initialized` with `{path, action, originatorClientId?}`.
Restart a configured MCP server through the ACP child's `McpClientManager.discoverMcpToolsForServer` (disconnect + reconnect + rediscover). Pre-checks the live budget snapshot from PR 14 v1's accounting so a restart on a budget-saturated workspace returns a soft refusal rather than triggering a `BudgetExhaustedError` cascade.
1021
+
1022
+
Request body is empty (`{}`). The path parameter is the URL-encoded server name as it appears in `mcpServers` config.
1023
+
1024
+
Response (200) — discriminated union on `restarted`:
|`'in_flight'`| Another discovery / restart for this server is already in progress. The route returns immediately rather than awaiting the original promise. Caller should retry after a short delay. |
1044
+
|`'disabled'`| Server is configured but listed in `excludedMcpServers`. Re-enable before restart. |
1045
+
|`'budget_would_exceed'`| Daemon is `--mcp-budget-mode=enforce`, the target server is not currently in `reservedSlots`, and the live total has reached `clientBudget`. Caller should free a slot first. |
-`404` — server name not in `mcpServers` config, or no live ACP channel exists (restart inherently requires a live `McpClientManager` instance).
1051
+
-`500` — internal error (e.g. `ToolRegistry` not initialized).
1052
+
1053
+
SSE events (workspace-scoped): `mcp_server_restarted` with `{serverName, durationMs, originatorClientId?}` on success; `mcp_server_restart_refused` with `{serverName, reason, originatorClientId?}` on soft skip.
Copy file name to clipboardExpand all lines: docs/users/qwen-serve.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -12,6 +12,7 @@ Run Qwen Code as a local HTTP daemon so multiple clients (IDE plugins, web UIs,
12
12
-**Reconnect-safe streaming** — SSE with `Last-Event-ID` reconnect lets a client drop and pick up exactly where it left off (within the ring's replay window).
13
13
-**First-responder permissions** — when the agent asks for permission to run a tool, every connected client sees the request; whichever client answers first wins.
14
14
-**One daemon, one workspace** — each `qwen serve` process binds to exactly one workspace at boot (per [#3803](https://github.com/QwenLM/qwen-code/issues/3803) §02). Multi-workspace deployments run one daemon per workspace on separate ports (or behind an orchestrator).
15
+
-**Remote runtime control** ([#4175](https://github.com/QwenLM/qwen-code/issues/4175) PR 17) — change a session's approval mode (`POST /session/:id/approval-mode`), toggle a tool per workspace (`POST /workspace/tools/:name/enable`), scaffold an empty `QWEN.md` (`POST /workspace/init`, mechanical only — does NOT call the model; for AI-fill, follow up with `POST /session/:id/prompt`), or restart a single MCP server with a budget pre-check (`POST /workspace/mcp/:server/restart`). All four are strict-gated — configure `--token` first.
0 commit comments