Skip to content

Commit 12a19b7

Browse files
committed
docs(serve): mutation control routes protocol section (#4175 Wave 4 PR 17)
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)
1 parent f383ef3 commit 12a19b7

2 files changed

Lines changed: 149 additions & 2 deletions

File tree

docs/developers/qwen-serve-protocol.md

Lines changed: 148 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,11 @@ registry. Clients **must** gate UI off `features`, not off `mode` (per design
9898
'slow_client_warning', 'typed_event_schema',
9999
'session_set_model', 'client_identity', 'client_heartbeat',
100100
'session_permission_vote', 'permission_vote', 'workspace_mcp', 'workspace_skills',
101-
'workspace_providers', 'session_context', 'session_supported_commands',
102-
'session_close', 'session_metadata', 'mcp_guardrails']
101+
'workspace_providers', 'workspace_env', 'workspace_preflight',
102+
'session_context', 'session_supported_commands',
103+
'session_close', 'session_metadata', 'mcp_guardrails',
104+
'session_approval_mode_control', 'workspace_tool_toggle',
105+
'workspace_init', 'workspace_mcp_restart']
103106
```
104107

105108
`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
114117

115118
`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.
116119

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+
117122
`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.
118123

119124
> ⚠️ **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:
906911

907912
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.
908913

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`.
923+
924+
#### `POST /session/:id/approval-mode`
925+
926+
Capability tag: `session_approval_mode_control`. Bridge → ACP extMethod `qwen/control/session/approval_mode`.
927+
928+
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`.
937+
938+
Response (200):
939+
940+
```json
941+
{
942+
"sessionId": "sess:42",
943+
"mode": "auto-edit",
944+
"previous": "default",
945+
"persisted": false
946+
}
947+
```
948+
949+
Errors:
950+
951+
- `400 {code: 'invalid_approval_mode', allowed: [...]}` — unknown mode literal.
952+
- `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.
967+
968+
Request:
969+
970+
```json
971+
{ "enabled": false }
972+
```
973+
974+
Response (200):
975+
976+
```json
977+
{ "toolName": "Bash", "enabled": false }
978+
```
979+
980+
Errors:
981+
982+
- `400 {code: 'invalid_tool_name'}` — empty path parameter.
983+
- `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).
994+
995+
Request:
996+
997+
```json
998+
{ "force": false }
999+
```
1000+
1001+
Response (200):
1002+
1003+
```json
1004+
{ "path": "/work/bound/QWEN.md", "action": "created" }
1005+
```
1006+
1007+
`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?}`.
1015+
1016+
#### `POST /workspace/mcp/:server/restart`
1017+
1018+
Capability tag: `workspace_mcp_restart`. Bridge → ACP extMethod `qwen/control/workspace/mcp/restart`.
1019+
1020+
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`:
1025+
1026+
```json
1027+
{ "serverName": "docs", "restarted": true, "durationMs": 1234 }
1028+
```
1029+
1030+
```json
1031+
{
1032+
"serverName": "docs",
1033+
"restarted": false,
1034+
"skipped": true,
1035+
"reason": "budget_would_exceed"
1036+
}
1037+
```
1038+
1039+
Soft skip reasons (all return 200):
1040+
1041+
| `reason` | Meaning |
1042+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1043+
| `'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. |
1046+
1047+
Errors (non-2xx):
1048+
1049+
- `400 {code: 'invalid_server_name'}` — empty path parameter.
1050+
- `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.
1054+
9091055
### `GET /session/:id/events` (SSE)
9101056

9111057
Subscribe to the session's event stream.

docs/users/qwen-serve.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Run Qwen Code as a local HTTP daemon so multiple clients (IDE plugins, web UIs,
1212
- **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).
1313
- **First-responder permissions** — when the agent asks for permission to run a tool, every connected client sees the request; whichever client answers first wins.
1414
- **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.
1516

1617
## Quickstart
1718

0 commit comments

Comments
 (0)