diff --git a/docs/design/serve-server-final-split.md b/docs/design/serve-server-final-split.md
index b79e42bc8cf..bf06b2b351d 100644
--- a/docs/design/serve-server-final-split.md
+++ b/docs/design/serve-server-final-split.md
@@ -10,13 +10,13 @@ The assembly order is part of the daemon contract and must stay visually auditab
1. same-origin `Origin` stripping
2. CORS and host allowlist
-3. pre-auth `/health` and `/demo` on allowed loopback setups
+3. pre-auth `/health` on allowed loopback setups
4. access logging
5. Web Shell static assets
6. bearer auth
7. rate limit
-8. JSON body parser and JSON parser error mapper
-9. post-auth `/health` and `/demo` when required
+8. post-auth `/health` when required
+9. JSON body parser and JSON parser error mapper
10. daemon telemetry
11. REST route groups
12. ACP HTTP and WebSocket routes
diff --git a/docs/design/serve-server-split.md b/docs/design/serve-server-split.md
index 69a055b9377..6910e8f7625 100644
--- a/docs/design/serve-server-split.md
+++ b/docs/design/serve-server-split.md
@@ -10,13 +10,13 @@ The app assembly order is part of the public behavior and must stay stable:
1. same-origin `Origin` stripping
2. CORS and host allowlist
-3. pre-auth `/health` and `/demo` on allowed loopback setups
+3. pre-auth `/health` on allowed loopback setups
4. access logging
5. Web Shell static assets
6. bearer auth
7. rate limit
-8. JSON body parser and JSON parser error mapper
-9. post-auth `/health` and `/demo` when required
+8. post-auth `/health` when required
+9. JSON body parser and JSON parser error mapper
10. daemon telemetry
11. REST route groups
12. ACP HTTP and WebSocket routes
diff --git a/docs/developers/daemon/00-index.md b/docs/developers/daemon/00-index.md
index 5e81e230d43..4c9f5eefb72 100644
--- a/docs/developers/daemon/00-index.md
+++ b/docs/developers/daemon/00-index.md
@@ -86,7 +86,7 @@ Use these anchors when moving from the docs into the latest `main` code:
| Surface | Implementation anchors | Primary docs |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
-| Bootstrap and HTTP assembly | `packages/cli/src/serve/run-qwen-serve.ts`, `packages/cli/src/serve/server.ts`, `packages/cli/src/serve/routes/health-demo.ts`, `/demo` | [`02`](./02-serve-runtime.md), [`20`](./20-quickstart-operations.md) |
+| Bootstrap and HTTP assembly | `packages/cli/src/serve/run-qwen-serve.ts`, `packages/cli/src/serve/server.ts`, `packages/cli/src/serve/routes/health.ts`, `packages/cli/src/serve/web-shell-static.ts` | [`02`](./02-serve-runtime.md), [`20`](./20-quickstart-operations.md) |
| ACP bridge and session multiplexing | `packages/acp-bridge/src/bridge.ts`, `packages/acp-bridge/src/bridgeTypes.ts`, `@qwen-code/acp-bridge` | [`03`](./03-acp-bridge.md), [`08`](./08-session-lifecycle.md) |
| Permission mediation | `packages/acp-bridge/src/permissionMediator.ts`, `fromLoopback: boolean`, `policy.*` | [`04`](./04-permission-mediation.md), [`12`](./12-auth-security.md) |
| MCP transport pool | `packages/core/src/tools/mcp-transport-pool.ts`, `mcp-pool-key.ts`, `pid-descendants.ts`, `session-mcp-view.ts`, `/mcp refresh`, `MCPCallInterruptedError` | [`05`](./05-mcp-transport-pool.md), [`06`](./06-mcp-budget-guardrails.md) |
@@ -144,7 +144,7 @@ Use these anchors when moving from the docs into the latest `main` code:
| Area | Current state | Primary docs |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
| Configuration | Full `qwen serve` flags, env vars, `settings.json`, `ServeOptions`, `BridgeOptions`, and important constants are collected in one page. | [`17`](./17-configuration.md) |
-| Quickstart / operations | Shortest startup path, launch recipes, curl checks, demo page auth behavior, route split, shutdown behavior, and embedded invocation recipes are covered. | [`20`](./20-quickstart-operations.md) |
+| Quickstart / operations | Shortest startup path, launch recipes, curl checks, Web Shell auth behavior, route split, shutdown behavior, and embedded invocation recipes are covered. | [`20`](./20-quickstart-operations.md) |
| Errors | Boot-time explicit failures, route errors, bridge errors, EventBus errors, filesystem errors, and mediator errors are summarized with remediation. | [`18`](./18-error-taxonomy.md) |
| Observability | `QWEN_SERVE_DEBUG`, curl recipes, useful events, telemetry gaps, and investigation checklists are documented. | [`19`](./19-observability.md) |
diff --git a/docs/developers/daemon/02-serve-runtime.md b/docs/developers/daemon/02-serve-runtime.md
index 7fbee49bc1e..34d69450e4e 100644
--- a/docs/developers/daemon/02-serve-runtime.md
+++ b/docs/developers/daemon/02-serve-runtime.md
@@ -37,19 +37,19 @@
**Subsystems**:
-| Path | Role |
-| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| `serve/fs/` | `WorkspaceFileSystem` factory plus `policy.ts` (size/trust/binary checks), `paths.ts` (canonicalize, resolveWithin, symlink rejection), `audit.ts`, and typed `FsError` values. |
-| `serve/routes/workspace-file-read.ts`, `workspace-file-write.ts` | HTTP handlers for `GET /file`, `GET /file/bytes`, `POST /file/write`, and `POST /file/edit`. |
-| `serve/workspace-memory.ts` | `GET/POST /workspace/memory` (QWEN.md CRUD). |
-| `serve/workspace-agents.ts` | `GET/POST/DELETE /workspace/agents` (subagent CRUD). |
-| `serve/daemon-status-provider.ts` | Env snapshot plus daemon-host preflight cells: Node version, CLI entry, workspace stat, ripgrep, git, npm. |
-| `serve/permission-audit.ts` | `PermissionAuditRing` (512-entry FIFO) and `createPermissionAuditPublisher`. |
-| `serve/auth/device-flow.ts`, `qwen-device-flow-provider.ts` | Device-flow OAuth routes. See [`12-auth-security.md`](./12-auth-security.md). |
-| `serve/daemon-logger.ts` | `DaemonLogger` structured file logs. See [`19-observability.md`](./19-observability.md). |
-| `serve/debug-mode.ts` | Shared `isServeDebugMode()` predicate controlling verbose error context in HTTP responses. |
-| `serve/acp-http/` | ACP Streamable HTTP transport (RFD #721), mounted at `/acp`. Seven files implement JSON-RPC POST, SSE GET, DELETE teardown, and shared bridge usage in parallel with the REST surface. |
-| `serve/demo.ts` | Self-contained inline HTML for `GET /demo`: browser debug console with chat UI, event log, and workspace inspector. On loopback without `--require-auth`, it is registered **before** `bearerAuth`; on non-loopback or with `--require-auth`, it is registered **after** `bearerAuth`. Served with CSP `default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'` plus `X-Frame-Options: DENY`. |
+| Path | Role |
+| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `serve/fs/` | `WorkspaceFileSystem` factory plus `policy.ts` (size/trust/binary checks), `paths.ts` (canonicalize, resolveWithin, symlink rejection), `audit.ts`, and typed `FsError` values. |
+| `serve/routes/workspace-file-read.ts`, `workspace-file-write.ts` | HTTP handlers for `GET /file`, `GET /file/bytes`, `POST /file/write`, and `POST /file/edit`. |
+| `serve/workspace-memory.ts` | `GET/POST /workspace/memory` (QWEN.md CRUD). |
+| `serve/workspace-agents.ts` | `GET/POST/DELETE /workspace/agents` (subagent CRUD). |
+| `serve/daemon-status-provider.ts` | Env snapshot plus daemon-host preflight cells: Node version, CLI entry, workspace stat, ripgrep, git, npm. |
+| `serve/permission-audit.ts` | `PermissionAuditRing` (512-entry FIFO) and `createPermissionAuditPublisher`. |
+| `serve/auth/device-flow.ts`, `qwen-device-flow-provider.ts` | Device-flow OAuth routes. See [`12-auth-security.md`](./12-auth-security.md). |
+| `serve/daemon-logger.ts` | `DaemonLogger` structured file logs. See [`19-observability.md`](./19-observability.md). |
+| `serve/debug-mode.ts` | Shared `isServeDebugMode()` predicate controlling verbose error context in HTTP responses. |
+| `serve/acp-http/` | ACP Streamable HTTP transport (RFD #721), mounted at `/acp`. Seven files implement JSON-RPC POST, SSE GET, DELETE teardown, and shared bridge usage in parallel with the REST surface. |
+| `serve/web-shell-static.ts`, `serve/web-shell-resolver.ts` | Locate and mount the built Web Shell assets (the daemon's browser UI) at `/`, `/assets`, and `/session/:id`, plus the SPA deep-link fallback registered after all API routes. Mounted **before** `bearerAuth` in every launch mode — a browser cannot attach `Authorization` to a navigation or subresource — while every API route it calls stays token-gated. Degrades to API-only when the assets are absent; `--no-web` opts out. |
**ACP bridge package imports**:
@@ -113,32 +113,33 @@ Calling `createServeApp` directly returns only an `Application`; the embedder ow
## Configuration
-| Source | Key | Effect |
-| --------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
-| Env | `QWEN_SERVER_TOKEN` | Bearer token after trim. |
-| Env | `QWEN_SERVE_NO_MCP_POOL=1` | Forces `mcpPoolActive=false`. |
-| ACP child env | `QWEN_SERVE_MCP_CLIENT_BUDGET` / `QWEN_SERVE_MCP_BUDGET_MODE` | Generated from `--mcp-client-budget` / `--mcp-budget-mode` and forwarded through `childEnvOverrides`. |
-| Env | `QWEN_SERVE_PROMPT_DEADLINE_MS` / `QWEN_SERVE_WRITER_IDLE_TIMEOUT_MS` | Default prompt / SSE idle timeouts. |
-| Env | `QWEN_SERVE_RATE_LIMIT*` | Rate-limit switch, prompt / mutation / read caps, and window default. |
-| Env | `QWEN_SERVE_DEBUG=1` | Verbose stderr logs. See [`19-observability.md`](./19-observability.md). |
-| Flags | `--hostname`, `--port` | Listen binding. |
-| Flags | `--token`, `--require-auth`, `--enable-session-shell` | Bearer token, loopback auth hardening, and explicit shell execution switch. |
-| Flag | `--workspace` | Overrides `process.cwd()`; repeat to register additional isolated workspace runtimes. |
-| Flags | `--max-sessions`, `--max-pending-prompts-per-session`, `--max-connections`, `--event-ring-size` | Bridge / Express caps. |
-| Flags | `--mcp-client-budget=N`, `--mcp-budget-mode={off,warn,enforce}` | Forwarded to the ACP child. |
-| Flags | `--allow-origin`, `--allow-private-auth-base-url` | Browser CORS allowlist and localhost/private auth provider installation switch. |
-| Flags | `--prompt-deadline-ms`, `--writer-idle-timeout-ms`, `--channel-idle-timeout-ms`, `--initialize-timeout-ms` | Prompt, SSE writer, ACP child idle lifecycle, and ACP child request timeout control. |
-| Flags | `--session-reap-interval-ms`, `--session-idle-timeout-ms` | Disconnected-session reaping control. |
-| Flags | `--rate-limit*` | Per-tier HTTP rate limit. |
-| `settings.json` | `policy.permissionStrategy`, `policy.consensusQuorum` | `MultiClientPermissionMediator` policy and quorum. |
-| `settings.json` | `context.fileName` | `getCurrentGeminiMdFilename` override for the bridge. |
+| Source | Key | Effect |
+| --------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
+| Env | `QWEN_SERVER_TOKEN` | Bearer token after trim. |
+| Env | `QWEN_SERVE_NO_MCP_POOL=1` | Forces `mcpPoolActive=false`. |
+| ACP child env | `QWEN_SERVE_MCP_CLIENT_BUDGET` / `QWEN_SERVE_MCP_BUDGET_MODE` | Generated from `--mcp-client-budget` / `--mcp-budget-mode` and forwarded through `childEnvOverrides`. |
+| Env | `QWEN_SERVE_PROMPT_DEADLINE_MS` / `QWEN_SERVE_WRITER_IDLE_TIMEOUT_MS` | Default prompt / SSE idle timeouts. |
+| Env | `QWEN_SERVE_RATE_LIMIT*` | Rate-limit switch, prompt / mutation / read caps, and window default. |
+| Env | `QWEN_SERVE_DEBUG=1` | Verbose stderr logs. See [`19-observability.md`](./19-observability.md). |
+| Flags | `--hostname`, `--port` | Listen binding. |
+| Flags | `--token`, `--require-auth`, `--enable-session-shell` | Bearer token, loopback auth hardening, and explicit shell execution switch. |
+| Flag | `--workspace` | Overrides `process.cwd()`; repeat to register additional isolated workspace runtimes. |
+| Flags | `--max-sessions`, `--max-pending-prompts-per-session`, `--max-connections`, `--event-ring-size` | Bridge / Express caps. |
+| Flags | `--mcp-client-budget=N`, `--mcp-budget-mode={off,warn,enforce}` | Forwarded to the ACP child. |
+| Flags | `--allow-origin`, `--allow-private-auth-base-url` | Browser CORS allowlist and localhost/private auth provider installation switch. |
+| Flag | `--web` / `--no-web` | Serve or skip the Web Shell UI at the daemon root (default serves). `--no-web` leaves the daemon API-only. |
+| Flags | `--prompt-deadline-ms`, `--writer-idle-timeout-ms`, `--channel-idle-timeout-ms`, `--initialize-timeout-ms` | Prompt, SSE writer, ACP child idle lifecycle, and ACP child request timeout control. |
+| Flags | `--session-reap-interval-ms`, `--session-idle-timeout-ms` | Disconnected-session reaping control. |
+| Flags | `--rate-limit*` | Per-tier HTTP rate limit. |
+| `settings.json` | `policy.permissionStrategy`, `policy.consensusQuorum` | `MultiClientPermissionMediator` policy and quorum. |
+| `settings.json` | `context.fileName` | `getCurrentGeminiMdFilename` override for the bridge. |
See [`17-configuration.md`](./17-configuration.md) for the merged reference.
## Caveats and known limits
- Direct `createServeApp` without `deps.fsFactory` or `deps.bridge` defaults to `trusted: false`; agent-side ACP `writeTextFile` rejects as `untrusted_workspace`. The warning is printed once.
-- `denyBrowserOriginCors` rejects **all** requests carrying `Origin`; the demo page works because another middleware strips matching same-origin values first.
+- `denyBrowserOriginCors` rejects **all** requests carrying `Origin`; the **loopback** Web Shell works because another middleware strips matching loopback same-origin values first — non-loopback binds require `--allow-origin` for the shell's XHRs.
- Body-parser ordering: routes using `mutate({ strict: true })` return 401 only after `express.json()`. The worst case is `--max-connections × express.json({limit: '10mb'})`, up to about 2.5 GB of transient memory on a saturated loopback listener; this tradeoff is intentional.
- Multiple daemons in one process must use per-handle `childEnvOverrides`; mutating `process.env` races because `defaultSpawnChannelFactory` snapshots env at spawn time.
diff --git a/docs/developers/daemon/12-auth-security.md b/docs/developers/daemon/12-auth-security.md
index 1acb23d1db2..66e08a62a84 100644
--- a/docs/developers/daemon/12-auth-security.md
+++ b/docs/developers/daemon/12-auth-security.md
@@ -56,7 +56,7 @@ daemon bind beyond loopback in the open.
```mermaid
flowchart LR
- REQ[Request] --> SO["strip same-origin Origin
(demo page support)"]
+ REQ[Request] --> SO["strip same-origin Origin
(Web Shell support)"]
SO --> CORS{"--allow-origin?"}
CORS -->|yes| AO["allowOriginCors
(allowlist match)"]
CORS -->|no| DC["denyBrowserOriginCors
(reject all Origin)"]
@@ -100,7 +100,7 @@ Non-loopback binds bypass this middleware (operator chose the surface area; bear
Reject any request with an `Origin` header. CLI/SDK never set Origin; only browsers do. Returns deterministic `403 { error: 'Request denied by CORS policy' }` rather than the 500 HTML the `cors` package's error-callback would produce.
-Exception: the demo page's same-origin XHRs are handled by a separate middleware (in `server.ts`) that strips `Origin` when it matches the daemon's own address.
+Exception: the Web Shell's same-origin XHRs on a **loopback** bind are handled by a separate middleware (in `server/self-origin.ts`) that strips `Origin` when it matches one of the loopback self-origins (`127.0.0.1`, `localhost`, `[::1]`, `host.docker.internal`). On non-loopback binds the shell's XHRs carry an unmatched `Origin` and need `--allow-origin` for the daemon origin.
### `allowOriginCors` (`--allow-origin` mode)
@@ -298,7 +298,7 @@ sequenceDiagram
- **`--require-auth` shadows feature preflight.** Unauthenticated clients cannot discover the `require_auth` tag; their discovery surface is the 401 body itself.
- **Mutation gate body-parser ordering**: `mutationGate({strict: true})` 401 responses fire **after** `express.json()` parses the body. Worst case on a saturated loopback listener: `--max-connections × express.json({limit: '10mb'})` ≈ 2.5 GB transient. Loopback-only attack surface, intentionally accepted.
-- **Same-origin Origin stripping** in `server.ts` happens _before_ `denyBrowserOriginCors`. If a future change moves the strip elsewhere, the demo page breaks.
+- **Same-origin Origin stripping** in `server.ts` happens _before_ `denyBrowserOriginCors`. If a future change moves the strip elsewhere, the Web Shell breaks.
- **Token comparison is over the SHA-256 digest**, not the raw token. Reduces timing leakage by collapsing variable-length token compares to a fixed-size digest compare.
- The daemon does **not** carry mTLS, request signing, or pair-token proof-of-possession today. `--rate-limit` provides HTTP rate limiting by client-id / IP key; it is not client identity authentication.
diff --git a/docs/developers/daemon/17-configuration.md b/docs/developers/daemon/17-configuration.md
index d42ca257c3a..6cb7f512bc9 100644
--- a/docs/developers/daemon/17-configuration.md
+++ b/docs/developers/daemon/17-configuration.md
@@ -6,45 +6,46 @@ This page collects every setting that affects the `qwen serve` daemon and its ad
## CLI flags (`qwen serve`)
-| Flag | Type | Default | Effect |
-| --------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `--hostname ` | string | `127.0.0.1` | Bind address. Loopback values: `127.0.0.1`, `localhost`, `::1`, `[::1]`. Non-loopback requires a bearer token at boot. `host:port` input is rejected with guidance to use `--port`. |
-| `--port ` | number | `4170` | Listen port; `0` means ephemeral. |
-| `--token ` | string | env | Bearer token. Overrides `QWEN_SERVER_TOKEN` and is trimmed at boot. It appears in the process command line, so prefer env in deployments. |
-| `--require-auth` | boolean | `false` | Extends bearer auth to loopback and `/health`; boot refuses to start without a token. |
-| `--workspace ` | absolute path / repeatable | `process.cwd()` | Startup workspace runtime; repeat to register additional isolated runtimes. The first is primary. Every value must be absolute and a directory; canonicalized at boot. |
-| `--memory-project-scope ` | `git-root` / `workspace` | `git-root` | Project-memory partitioning. `git-root` shares memory among workspaces at the same Git root; `workspace` isolates by exact workspace directory. Overrides `QWEN_CODE_MEMORY_PROJECT_SCOPE`. |
-| `--max-sessions ` | number | `32` | Per-workspace active session cap. `0` / `Infinity` means unlimited; `NaN` / negative values throw. |
-| `--max-total-sessions ` | number | derived for multiple startup/restored workspaces | Daemon-wide active session cap. When omitted, a finite default is derived once from the per-workspace cap and startup/restored workspace count. `0` / `Infinity` means unlimited. |
-| `--max-pending-prompts-per-session ` | number | `5` | Accepted but pending/running prompt cap per session. Excess prompt returns 503. `0` / `Infinity` means unlimited; negative or non-integer values throw. |
-| `--max-connections ` | number | `256` | HTTP listener `server.maxConnections`; `0` / `Infinity` means unlimited. |
-| `--enable-session-shell` | boolean | `false` | Enables direct `POST /session/:id/shell` execution. Requires bearer token, and every call must carry a session-bound `X-Qwen-Client-Id`. |
-| `--event-ring-size ` | number | `8000` | Per-session SSE replay ring; soft cap is `1_000_000`. |
-| `--compacted-replay-max-bytes ` | positive integer | `4194304` | Byte cap for the bounded in-memory replay snapshot returned by `POST /session/:id/load`; hard cap is `268435456`. |
-| `--memory-budget-mb ` | integer in `[1024, 1048576]` | 50% of cgroup-constrained or host memory, capped at the flag maximum (1048576 MB) | Total memory budget for the daemon process tree, capped at resolved available memory. Observed and reported under `limits.memory` in daemon status; it does not size any child process. Boot rejects out-of-range values. |
-| `--memory-pressure-mode ` | `off` \| `observe` | `observe` | Whether the daemon derives a memory-pressure level from its own RSS and V8 heap. Both modes report `runtime.memory.pressure`; only `observe` raises `daemon_memory_pressure`. Root process only; no remediation. |
-| `--child-heap-mode ` | `off` \| `observe` | `observe` | Whether the daemon models a per-child heap partition of the budget. `observe` reports it and counts spawns past it; nothing is applied. `off` publishes no partition at all — `maxConcurrentChildren` and `perChildCeilingMb` are both `null`. |
-| `--http-bridge` | boolean | `true` | Stage 1 bridge mode. `--no-http-bridge` still falls back to http-bridge and prints to stderr. |
-| `--mcp-client-budget ` | positive integer | unset | Sets `WorkspaceMcpBudget.clientBudget` and forwards it to the ACP child through `childEnvOverrides`. |
-| `--mcp-budget-mode ` | `off` / `warn` / `enforce` | `warn` when budget is set, otherwise `off` | Sets `WorkspaceMcpBudget.mode`; `enforce` requires `--mcp-client-budget`. |
-| `--external-tool-guard-mode ` | `off` / `required` | `off` | Enables the managed ACP external pre-execution Guard. `required` fails startup unless its loopback provider completes the v1 handshake. |
-| `--external-tool-guard-endpoint ` | loopback HTTP(S) origin | unset | Provider origin used only in `required` mode. It must be origin-only and use `127.0.0.1`, `localhost`, or `::1`; paths, credentials, redirects, and proxy routing are rejected. |
-| `--external-tool-guard-timeout-ms ` | integer `100..30000` | `3000` | Per-handshake and per-prepare deadline. A timeout fails startup during the handshake or fails the invocation closed during a turn. |
-| `--allow-origin ` | repeatable string | unset | Cross-origin allowlist that replaces the default CORS denial. `*` allows any origin but requires a token. |
-| `--allow-private-auth-base-url` | boolean | `false` | Allows `/workspace/auth/provider` to install localhost / private-network auth provider `baseUrl`; use only in trusted local development. |
-| `--prompt-deadline-ms ` | positive integer | unset | Server-side prompt wallclock limit in ms. Timeout aborts and returns an error. |
-| `--writer-idle-timeout-ms ` | positive integer | unset | Per-SSE-connection idle timeout in ms. The daemon closes the SSE connection when no event is sent for this duration. |
-| `--channel-idle-timeout-ms ` | non-negative integer | `0` | How long to keep the ACP child alive after the last session closes. `0` means reclaim immediately. |
-| `--initialize-timeout-ms ` | positive integer | `10000` | ACP child request timeout, including the initialize handshake (ms). |
-| `--session-restore-timeout-ms ` | positive integer | `60000` | ACP session load/resume timeout (ms). When this flag is omitted, an explicitly supplied initialize timeout raises the budget but never lowers it below the default. |
-| `--session-reap-interval-ms ` | non-negative integer | `60000` | Session reaper scan interval; `0` disables it. |
-| `--session-idle-timeout-ms ` | non-negative integer | `1800000` | Disconnected-session idle reaping time; `0` disables it. |
-| `--rate-limit` / `--no-rate-limit` | boolean | env / off | Enables per-tier HTTP rate limiting for prompt, mutation, and read routes. |
-| `--rate-limit-prompt ` | positive integer | `10` | Prompt request limit per window; requires rate limiting to be enabled. |
-| `--rate-limit-mutation ` | positive integer | `30` | Mutation request limit per window; requires rate limiting to be enabled. |
-| `--rate-limit-read ` | positive integer | `120` | Read request limit per window; requires rate limiting to be enabled. |
-| `--rate-limit-window-ms ` | integer `>= 1000` | `60000` | Rate limit window length; requires rate limiting to be enabled. |
-| no flag | - | - | `QWEN_SERVE_NO_MCP_POOL=1` fully disables the pool. |
+| Flag | Type | Default | Effect |
+| --------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `--hostname ` | string | `127.0.0.1` | Bind address. Loopback values: `127.0.0.1`, `localhost`, `::1`, `[::1]`. Non-loopback requires a bearer token at boot. `host:port` input is rejected with guidance to use `--port`. |
+| `--port ` | number | `4170` | Listen port; `0` means ephemeral. |
+| `--token ` | string | env | Bearer token. Overrides `QWEN_SERVER_TOKEN` and is trimmed at boot. It appears in the process command line, so prefer env in deployments. |
+| `--require-auth` | boolean | `false` | Extends bearer auth to loopback and `/health`; boot refuses to start without a token. |
+| `--workspace ` | absolute path / repeatable | `process.cwd()` | Startup workspace runtime; repeat to register additional isolated runtimes. The first is primary. Every value must be absolute and a directory; canonicalized at boot. |
+| `--memory-project-scope ` | `git-root` / `workspace` | `git-root` | Project-memory partitioning. `git-root` shares memory among workspaces at the same Git root; `workspace` isolates by exact workspace directory. Overrides `QWEN_CODE_MEMORY_PROJECT_SCOPE`. |
+| `--max-sessions ` | number | `32` | Per-workspace active session cap. `0` / `Infinity` means unlimited; `NaN` / negative values throw. |
+| `--max-total-sessions ` | number | derived for multiple startup/restored workspaces | Daemon-wide active session cap. When omitted, a finite default is derived once from the per-workspace cap and startup/restored workspace count. `0` / `Infinity` means unlimited. |
+| `--max-pending-prompts-per-session ` | number | `5` | Accepted but pending/running prompt cap per session. Excess prompt returns 503. `0` / `Infinity` means unlimited; negative or non-integer values throw. |
+| `--max-connections ` | number | `256` | HTTP listener `server.maxConnections`; `0` / `Infinity` means unlimited. |
+| `--enable-session-shell` | boolean | `false` | Enables direct `POST /session/:id/shell` execution. Requires bearer token, and every call must carry a session-bound `X-Qwen-Client-Id`. |
+| `--event-ring-size ` | number | `8000` | Per-session SSE replay ring; soft cap is `1_000_000`. |
+| `--compacted-replay-max-bytes ` | positive integer | `4194304` | Byte cap for the bounded in-memory replay snapshot returned by `POST /session/:id/load`; hard cap is `268435456`. |
+| `--memory-budget-mb ` | integer in `[1024, 1048576]` | 50% of cgroup-constrained or host memory, capped at the flag maximum (1048576 MB) | Total memory budget for the daemon process tree, capped at resolved available memory. Observed and reported under `limits.memory` in daemon status; it does not size any child process. Boot rejects out-of-range values. |
+| `--memory-pressure-mode ` | `off` \| `observe` | `observe` | Whether the daemon derives a memory-pressure level from its own RSS and V8 heap. Both modes report `runtime.memory.pressure`; only `observe` raises `daemon_memory_pressure`. Root process only; no remediation. |
+| `--child-heap-mode ` | `off` \| `observe` | `observe` | Whether the daemon models a per-child heap partition of the budget. `observe` reports it and counts spawns past it; nothing is applied. `off` publishes no partition at all — `maxConcurrentChildren` and `perChildCeilingMb` are both `null`. |
+| `--http-bridge` | boolean | `true` | Stage 1 bridge mode. `--no-http-bridge` still falls back to http-bridge and prints to stderr. |
+| `--mcp-client-budget ` | positive integer | unset | Sets `WorkspaceMcpBudget.clientBudget` and forwards it to the ACP child through `childEnvOverrides`. |
+| `--mcp-budget-mode ` | `off` / `warn` / `enforce` | `warn` when budget is set, otherwise `off` | Sets `WorkspaceMcpBudget.mode`; `enforce` requires `--mcp-client-budget`. |
+| `--external-tool-guard-mode ` | `off` / `required` | `off` | Enables the managed ACP external pre-execution Guard. `required` fails startup unless its loopback provider completes the v1 handshake. |
+| `--external-tool-guard-endpoint ` | loopback HTTP(S) origin | unset | Provider origin used only in `required` mode. It must be origin-only and use `127.0.0.1`, `localhost`, or `::1`; paths, credentials, redirects, and proxy routing are rejected. |
+| `--external-tool-guard-timeout-ms ` | integer `100..30000` | `3000` | Per-handshake and per-prepare deadline. A timeout fails startup during the handshake or fails the invocation closed during a turn. |
+| `--allow-origin ` | repeatable string | unset | Cross-origin allowlist that replaces the default CORS denial. `*` allows any origin but requires a token. |
+| `--allow-private-auth-base-url` | boolean | `false` | Allows `/workspace/auth/provider` to install localhost / private-network auth provider `baseUrl`; use only in trusted local development. |
+| `--web` / `--no-web` | boolean | `true` | Serve the built Web Shell SPA at the daemon root (`GET /`, `/assets/*`, and `/session/:id` document navigations). These entry points are mounted before `bearerAuth`; every API route stays token-gated. `--no-web` leaves the daemon API-only. |
+| `--prompt-deadline-ms ` | positive integer | unset | Server-side prompt wallclock limit in ms. Timeout aborts and returns an error. |
+| `--writer-idle-timeout-ms ` | positive integer | unset | Per-SSE-connection idle timeout in ms. The daemon closes the SSE connection when no event is sent for this duration. |
+| `--channel-idle-timeout-ms ` | non-negative integer | `0` | How long to keep the ACP child alive after the last session closes. `0` means reclaim immediately. |
+| `--initialize-timeout-ms ` | positive integer | `10000` | ACP child request timeout, including the initialize handshake (ms). |
+| `--session-restore-timeout-ms ` | positive integer | `60000` | ACP session load/resume timeout (ms). When this flag is omitted, an explicitly supplied initialize timeout raises the budget but never lowers it below the default. |
+| `--session-reap-interval-ms ` | non-negative integer | `60000` | Session reaper scan interval; `0` disables it. |
+| `--session-idle-timeout-ms ` | non-negative integer | `1800000` | Disconnected-session idle reaping time; `0` disables it. |
+| `--rate-limit` / `--no-rate-limit` | boolean | env / off | Enables per-tier HTTP rate limiting for prompt, mutation, and read routes. |
+| `--rate-limit-prompt ` | positive integer | `10` | Prompt request limit per window; requires rate limiting to be enabled. |
+| `--rate-limit-mutation ` | positive integer | `30` | Mutation request limit per window; requires rate limiting to be enabled. |
+| `--rate-limit-read ` | positive integer | `120` | Read request limit per window; requires rate limiting to be enabled. |
+| `--rate-limit-window-ms ` | integer `>= 1000` | `60000` | Rate limit window length; requires rate limiting to be enabled. |
+| no flag | - | - | `QWEN_SERVE_NO_MCP_POOL=1` fully disables the pool. |
## Environment variables
@@ -106,24 +107,25 @@ The daemon constructs each workspace runtime from that workspace's merged settin
`packages/cli/src/serve/types.ts` defines the typed options object accepted by both `runQwenServe` and `createServeApp`. It mirrors the CLI flags above and adds:
-| Field | Effect |
-| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `eventRingSize` | Overrides the default per-session ring size. |
-| `memoryProjectScope` | `'git-root' \| 'workspace'` project-memory partitioning; falls back to `QWEN_CODE_MEMORY_PROJECT_SCOPE`. |
-| `maxPendingPromptsPerSession` | Pending prompt cap per session; `0` / `Infinity` means unlimited. |
-| `mcpPoolActive` | Programmatic switch, defaulting from `QWEN_SERVE_NO_MCP_POOL`. |
-| `externalToolGuard` | Optional `{mode:'required', endpoint, token, timeoutMs?}`. Omission is fully off; required mode performs the provider handshake before listening. |
-| `allowOrigins` | Cross-origin allowlist (`string[]`), corresponding to `--allow-origin`. |
-| `allowPrivateAuthBaseUrl` | Allows private / localhost auth provider `baseUrl` installation. |
-| `enableSessionShell` | Enables session shell execution; bearer token and session-bound client id are still required. |
-| `promptDeadlineMs` | Prompt wallclock limit. |
-| `writerIdleTimeoutMs` | SSE writer idle timeout. |
-| `channelIdleTimeoutMs` | How long to keep the ACP child warm after the last session closes. |
-| `initializeTimeoutMs` | ACP child request timeout, including the initialize handshake. |
-| `sessionRestoreTimeoutMs` | ACP session load/resume timeout. Precedence: explicit restore value; otherwise an explicit initialize value raises the 60000 default but never lowers it; otherwise 60000. |
-| `sessionReapIntervalMs` | Session reaper scan interval. |
-| `sessionIdleTimeoutMs` | Disconnected-session idle reaping time. |
-| `rateLimit*` | Per-tier HTTP rate limit switch, thresholds, and window. |
+| Field | Effect |
+| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `eventRingSize` | Overrides the default per-session ring size. |
+| `memoryProjectScope` | `'git-root' \| 'workspace'` project-memory partitioning; falls back to `QWEN_CODE_MEMORY_PROJECT_SCOPE`. |
+| `maxPendingPromptsPerSession` | Pending prompt cap per session; `0` / `Infinity` means unlimited. |
+| `mcpPoolActive` | Programmatic switch, defaulting from `QWEN_SERVE_NO_MCP_POOL`. |
+| `externalToolGuard` | Optional `{mode:'required', endpoint, token, timeoutMs?}`. Omission is fully off; required mode performs the provider handshake before listening. |
+| `allowOrigins` | Cross-origin allowlist (`string[]`), corresponding to `--allow-origin`. |
+| `allowPrivateAuthBaseUrl` | Allows private / localhost auth provider `baseUrl` installation. |
+| `serveWebShell` | Serve the built Web Shell SPA at the daemon root (default `true`); `false` (the CLI's `--no-web`) leaves the daemon API-only. No effect when the build omits the shell assets. |
+| `enableSessionShell` | Enables session shell execution; bearer token and session-bound client id are still required. |
+| `promptDeadlineMs` | Prompt wallclock limit. |
+| `writerIdleTimeoutMs` | SSE writer idle timeout. |
+| `channelIdleTimeoutMs` | How long to keep the ACP child warm after the last session closes. |
+| `initializeTimeoutMs` | ACP child request timeout, including the initialize handshake. |
+| `sessionRestoreTimeoutMs` | ACP session load/resume timeout. Precedence: explicit restore value; otherwise an explicit initialize value raises the 60000 default but never lowers it; otherwise 60000. |
+| `sessionReapIntervalMs` | Session reaper scan interval. |
+| `sessionIdleTimeoutMs` | Disconnected-session idle reaping time. |
+| `rateLimit*` | Per-tier HTTP rate limit switch, thresholds, and window. |
## `BridgeOptions` (programmatic bridge embedding)
diff --git a/docs/developers/daemon/19-observability.md b/docs/developers/daemon/19-observability.md
index 99a4f892b02..e384cce9cbd 100644
--- a/docs/developers/daemon/19-observability.md
+++ b/docs/developers/daemon/19-observability.md
@@ -6,23 +6,23 @@
## What exists today
-| Surface | Location | Purpose |
-| ------------------------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `QWEN_SERVE_DEBUG` stderr logs | `bridge.ts` and call sites | Env values `1` / `true` / `on` / `yes` (case-insensitive) print `qwen serve debug: ...` lines to stderr. |
-| OpenTelemetry span instrumentation | `server.ts` `daemonTelemetryMiddleware` | Classified daemon API requests that reach the telemetry middleware are wrapped in `withDaemonRequestSpan`; attributes include canonical route, workspace hash when resolved, sessionId, clientId, and status code. Permission routes have dedicated spans. Prompt lifecycle is traced end-to-end. Configuration lives in `settings.json` `telemetry`. |
-| OpenTelemetry daemon perf metrics | `telemetry/*event-loop-lag*`, `daemon-metrics` | Event loop lag gauges for daemon and ACP child processes, plus daemon-child pipe message byte histograms. |
-| `DaemonLogger` structured file logs | `serve/daemon-logger.ts` | Appends to a stable, size-rotated `daemon.log`. File records include `runId` and PID. Boot prints the selected stable/fallback path; full status exposes health, issues, and file-copy loss counters. |
-| Per-request access-log middleware | `server/access-log.ts` | Logs method/path, status, duration, session, and first raw client ID after each request. A 60-token burst / 2-per-second bucket aggregates excess traffic into five fixed status counters. Health, heartbeat, and successful SSE exclusions remain. |
-| `/health` | `server.ts` route | Liveness probe; `?deep=1` returns extended details. |
-| `/capabilities` | `server.ts` route | Preflight feature discovery. See [`11-capabilities-versioning.md`](./11-capabilities-versioning.md). |
-| `/workspace/preflight` | Route -> `DaemonStatusProvider` | Structured readiness cells: Node version, CLI entry, ripgrep, git, npm, plus ACP-level cells once a child is alive. |
-| `/workspace/env` | Route -> `DaemonStatusProvider` | Daemon process env snapshot. Secret env vars report only presence; proxy URL credentials are stripped. |
-| `/workspace/mcp` | Route -> bridge extMethod | Pool, budget, and refusal snapshot. |
-| `/workspace/skills`, `/workspace/providers` | Routes | ACP-side live snapshots; return empty idle data when no session exists. |
-| Per-session SSE | `GET /session/:id/events` | Real-time event stream. |
-| `/demo` debug console | `GET /demo` (`packages/cli/src/serve/demo.ts`) | Browser-accessible single-page console: chat, event log, workspace inspector, and permission UX. On loopback, `http://127.0.0.1:4170/demo` is the quickest end-to-end validation path without writing SDK code. Registration rules are in [`02-serve-runtime.md`](./02-serve-runtime.md). |
-| `PermissionAuditRing` | `permission-audit.ts` | In-memory FIFO of 512 permission decisions. |
-| Mediator `decisionReason` audit | `permissionMediator.ts` | Internal structured record explaining why a permission request resolved the way it did. |
+| Surface | Location | Purpose |
+| ------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `QWEN_SERVE_DEBUG` stderr logs | `bridge.ts` and call sites | Env values `1` / `true` / `on` / `yes` (case-insensitive) print `qwen serve debug: ...` lines to stderr. |
+| OpenTelemetry span instrumentation | `server.ts` `daemonTelemetryMiddleware` | Classified daemon API requests that reach the telemetry middleware are wrapped in `withDaemonRequestSpan`; attributes include canonical route, workspace hash when resolved, sessionId, clientId, and status code. Permission routes have dedicated spans. Prompt lifecycle is traced end-to-end. Configuration lives in `settings.json` `telemetry`. |
+| OpenTelemetry daemon perf metrics | `telemetry/*event-loop-lag*`, `daemon-metrics` | Event loop lag gauges for daemon and ACP child processes, plus daemon-child pipe message byte histograms. |
+| `DaemonLogger` structured file logs | `serve/daemon-logger.ts` | Appends to a stable, size-rotated `daemon.log`. File records include `runId` and PID. Boot prints the selected stable/fallback path; full status exposes health, issues, and file-copy loss counters. |
+| Per-request access-log middleware | `server/access-log.ts` | Logs method/path, status, duration, session, and first raw client ID after each request. A 60-token burst / 2-per-second bucket aggregates excess traffic into five fixed status counters. Health, heartbeat, and successful SSE exclusions remain. |
+| `/health` | `server.ts` route | Liveness probe; `?deep=1` returns extended details. |
+| `/capabilities` | `server.ts` route | Preflight feature discovery. See [`11-capabilities-versioning.md`](./11-capabilities-versioning.md). |
+| `/workspace/preflight` | Route -> `DaemonStatusProvider` | Structured readiness cells: Node version, CLI entry, ripgrep, git, npm, plus ACP-level cells once a child is alive. |
+| `/workspace/env` | Route -> `DaemonStatusProvider` | Daemon process env snapshot. Secret env vars report only presence; proxy URL credentials are stripped. |
+| `/workspace/mcp` | Route -> bridge extMethod | Pool, budget, and refusal snapshot. |
+| `/workspace/skills`, `/workspace/providers` | Routes | ACP-side live snapshots; return empty idle data when no session exists. |
+| Per-session SSE | `GET /session/:id/events` | Real-time event stream. |
+| Web Shell UI | `GET /` (`packages/cli/src/serve/web-shell-static.ts`) | Browser UI served from the bundled Web Shell assets: chat, session list, workspace inspector, and permission UX. On loopback, `http://127.0.0.1:4170/` is the quickest end-to-end validation path without writing SDK code. Registration rules are in [`02-serve-runtime.md`](./02-serve-runtime.md). |
+| `PermissionAuditRing` | `permission-audit.ts` | In-memory FIFO of 512 permission decisions. |
+| Mediator `decisionReason` audit | `permissionMediator.ts` | Internal structured record explaining why a permission request resolved the way it did. |
## What does not exist today
diff --git a/docs/developers/daemon/20-quickstart-operations.md b/docs/developers/daemon/20-quickstart-operations.md
index 2db30e65b59..a8c935e8447 100644
--- a/docs/developers/daemon/20-quickstart-operations.md
+++ b/docs/developers/daemon/20-quickstart-operations.md
@@ -16,7 +16,7 @@ qwen serve: bound to workspace "/your/cwd"
qwen serve: bearer auth disabled (loopback default). Set QWEN_SERVER_TOKEN to enable.
```
-Open `http://127.0.0.1:4170/demo` in a browser to see the debug console: chat UI, event stream, and workspace inspection. In the default loopback dev mode, `createServeApp()` mounts the `/demo` route from `packages/cli/src/serve/routes/health-demo.ts` **before** `bearerAuth`, so no token is required.
+Open `http://127.0.0.1:4170/` in a browser to get the Web Shell UI: chat, session list, and workspace inspection. `createServeApp()` mounts the bundled Web Shell assets (`packages/cli/src/serve/web-shell-static.ts`) **before** `bearerAuth`, so the shell itself loads without a token; its own API calls carry the bearer when one is configured — start the daemon with `--open` (which puts the token in the URL fragment, never sent to the server) or append `#token=…` manually when auth is enabled. `--no-web` opts out and leaves the daemon API-only.
## 2. Launch recipes
@@ -67,7 +67,7 @@ qwen serve --channel-idle-timeout-ms 60000
QWEN_SERVE_RATE_LIMIT=1 qwen serve
```
-With the hardened loopback recipe (3), `/demo` is registered after `bearerAuth`. A normal browser navigation needs an auth header, so use curl or an SDK script instead.
+With the hardened loopback recipe (3), `/health` is registered after `bearerAuth`, so probes must carry the token like every other API route (the Web Shell static surface stays pre-auth by design; pass `--no-web` for an API-only daemon).
## 3. Full startup flags
@@ -198,23 +198,21 @@ curl -N \
-H 'Last-Event-ID: 0' \
'http://127.0.0.1:4170/session//events'
-# 8. Demo page
-open http://127.0.0.1:4170/demo
+# 8. Web Shell UI
+open http://127.0.0.1:4170/
```
When bearer auth is enabled, add `-H "Authorization: Bearer $QWEN_SERVER_TOKEN"` to every request.
-## 8. Can the demo page be used?
+## 8. Is there a browser UI?
-**Yes.** It is implemented by `getDemoHtml(port)` in `packages/cli/src/serve/demo.ts` as self-contained HTML with no external dependency.
+**Yes — the Web Shell.** `resolveWebShellDir()` finds the built assets (bundled next to the CLI bundle in a release, `packages/web-shell/dist` in a checkout) and `mountWebShellAssets()` serves them at `/`, `/assets`, and `/session/:id` document navigations (browser deep links — a plain `curl /session/` gets the API's 401/404, not the shell). When the assets are missing the daemon degrades to API-only instead of crashing; `--no-web` opts out explicitly.
-| Launch mode | Where `/demo` is registered | Direct browser navigation |
-| --------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------ |
-| Loopback without `--require-auth` | `routes/health-demo.ts`, mounted by `createServeApp()` **before** `bearerAuth` | Works without token |
-| Loopback with `--require-auth` | `routes/health-demo.ts`, mounted by `createServeApp()` **after** `bearerAuth` | Difficult to use from a plain browser; use curl or SDK |
-| Non-loopback bind | `routes/health-demo.ts`, mounted by `createServeApp()` **after** `bearerAuth` | Same as above |
+The static shell is mounted **before** `bearerAuth` in every launch mode — a browser cannot attach an `Authorization` header to an address-bar navigation or a `
-