Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4069eaa
fix(web-shell): allow session refresh with daemon auth
Aug 3, 2026
dea7b29
test(web-shell): cover SPA fallback shell branch for non-session navi…
Aug 3, 2026
0ac2f01
Merge branch 'main' into agent/fix-web-shell-session-refresh-auth
qwen-code-dev-bot Aug 3, 2026
516dca1
Merge branch 'main' into agent/fix-web-shell-session-refresh-auth
wenshao Aug 3, 2026
572c9e9
test(web-shell): retarget sec-fetch SPA fallback test to non-session …
qwen-code-dev-bot Aug 4, 2026
af38a40
fix(web-shell): serve pre-auth navigations during the deferred runtim…
qwen-code-dev-bot Aug 4, 2026
56e620e
Merge branch 'main' into agent/fix-web-shell-session-refresh-auth
qwen-code-dev-bot Aug 4, 2026
91c3f0f
fix(cli): align deferred pre-auth web shell gate with warm routing (#…
qwen-code-dev-bot Aug 4, 2026
c257d65
fix(cli): report daemon startup failure to pre-auth web shell navigat…
qwen-code-dev-bot Aug 4, 2026
451ac12
Merge branch 'main' into agent/fix-web-shell-session-refresh-auth
qwen-code-dev-bot Aug 4, 2026
3f27d53
fix(cli): exempt bare /assets from the deferred pre-auth gate (#8445)
qwen-code-dev-bot Aug 5, 2026
31b8f09
refactor(cli): dedupe runtime failure envelopes and deferred-window t…
qwen-code-dev-bot Aug 5, 2026
729af2d
refactor(cli): rename startup envelope helper and pin query-string de…
qwen-code-dev-bot Aug 5, 2026
2a68383
fix(cli): serve the // root alias pre-auth and fail-close the deferre…
qwen-code-dev-bot Aug 5, 2026
2e90b9e
docs(cli): record the pre-auth %2F session deep-link invariant (#8445)
qwen-code-dev-bot Aug 6, 2026
3440313
Merge branch 'main' into agent/fix-web-shell-session-refresh-auth
qwen-code-dev-bot Aug 6, 2026
d796703
fix(test): isolate serve streaming suite from stray workspace setting…
qwen-code-dev-bot Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/users/qwen-serve.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ Notes:
| `--http-bridge` | `true` | Stage 1 mode: production attempts to preheat one primary `qwen --acp` child for compatibility and retries on first use after failure, while each trusted secondary can start one child on demand. Sessions targeting a runtime multiplex onto its child via ACP `newSession()`; untrusted secondaries cannot start ACP. Stage 2 native in-process becomes available later. |
| `--initialize-timeout-ms <n>` | `10000` | ACP child request timeout, including the `initialize` handshake (ms). Must be a positive integer up to `2147483647`. Values above the JS timer ceiling (`2^31-1`) are rejected at boot because Node silently compresses them to 1 ms. Cold-container deployments that need extra headroom for child startup can raise this; the same value governs `newSession`, workspace-status polls, and other ACP ext-method deadlines. |
| `--allow-origin <pat>` | — | T2.4 ([#4514](https://github.com/QwenLM/qwen-code/issues/4514)). Cross-origin allowlist for browser webui clients. Repeatable. Each value is `*` (any origin — boot refuses if no bearer token is configured; `--require-auth` on loopback is recommended so `/health` and `/demo` are also bearer-gated, since both are pre-auth on loopback by default) or a canonical URL origin (`<scheme>://<host>[:<port>]`, no trailing slash / path / userinfo / query). **Subdomain wildcards (`https://*.example.com`) are intentionally unsupported** — list each subdomain explicitly, or use `*` with a configured token (and `--require-auth` for full hardening). Matched origins receive CORS response headers (`Access-Control-Allow-Origin`, `Vary: Origin`, methods, headers, max-age, and exposed `Retry-After`); unmatched origins still get a 403 with the same envelope as today's wall. `Origin: null` (sandboxed iframes, file:// docs) is always rejected, even under `*`. Pre-flight via `caps.features.allow_origin`. Loopback self-origin hits are unaffected. |
| `--web` / `--no-web` | `true` | Serve the built Web Shell SPA at the daemon root (`GET /`, `/assets/*`, and SPA deep-link fallback). The static shell is registered **before** the bearer-auth gate — a browser can't attach a token to a `<script>` subresource or an address-bar navigation, the shell carries no secrets, and every API route stays token-gated regardless. On non-loopback binds a one-line stderr warning notes the UI is reachable without auth. Use `--no-web` for an API-only daemon. No effect when the build omits the Web Shell assets (the daemon logs a breadcrumb and runs API-only). |
| `--web` / `--no-web` | `true` | Serve the built Web Shell SPA at the daemon root (`GET /`, `/assets/*`, and `GET /session/<id>` document navigations). These entry points are registered **before** the bearer-auth gate — a browser can't attach a token to a `<script>` subresource or an address-bar navigation, and the shell carries no secrets. Every API route stays token-gated regardless, and the SPA deep-link fallback for all other paths sits behind the bearer gate too. On non-loopback binds a one-line stderr warning notes the UI is reachable without auth. Use `--no-web` for an API-only daemon. No effect when the build omits the Web Shell assets (the daemon logs a breadcrumb and runs API-only). |
| `--open` | `false` | After the listener is up, open the Web Shell in your default browser at the daemon URL (with `#token=` appended as a URL fragment when a token is configured — a fragment is never sent to the server, keeping the token out of access logs and Referer headers). No-op with `--no-web`, or in headless / CI / SSH environments where no browser is available. |

> **Memory project scope caveats.**
Expand Down
39 changes: 23 additions & 16 deletions integration-tests/cli/qwen-serve-streaming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ const CLI_BIN =
process.env['TEST_CLI_PATH'] ??
path.resolve(__dirname, '../../packages/cli/dist/index.js');
const TOKEN = 'streaming-integ-secret';
const REPO_ROOT = path.resolve(__dirname, '../..');

// Windows: this suite shells out to `pgrep` / `kill -KILL` to simulate
// child-process crashes for the SIGKILL → `session_died` test, and those
Expand Down Expand Up @@ -78,6 +77,7 @@ let base = '';
let client: DaemonClient;
let fakeServer: FakeOpenAIServer;
let homeDir = '';
let workspaceDir = '';
let pendingWritePath = '';

beforeAll(async () => {
Expand Down Expand Up @@ -131,6 +131,7 @@ beforeAll(async () => {
ui: { enableFollowupSuggestions: false },
}),
);
workspaceDir = mkdtempSync(path.join(tmpdir(), 'qwen-serve-streaming-ws-'));
daemon = spawn(
process.execPath,
[
Expand All @@ -143,16 +144,19 @@ beforeAll(async () => {
'--hostname',
'127.0.0.1',
// Per #3803 §02 (1 daemon = 1 workspace), pin the bound
// workspace so every `createOrAttachSession({ workspaceCwd:
// REPO_ROOT })` below matches. Without this the daemon inherits
// the test runner's cwd (CI / IDE-launcher / direct vitest
// invocations all differ) and every session create returns
// 400 workspace_mismatch — the SSE / permission / Last-Event-ID
// tests below would all silently 404. Same fix the sibling routes test
// received earlier in this PR — missed in this file in the original §02
// pass.
// workspace so every `createOrAttachSession({ workspaceCwd })`
// below matches. Without this the daemon inherits the test
// runner's cwd (CI / IDE-launcher / direct vitest invocations
// all differ) and every session create returns 400
// workspace_mismatch — the SSE / permission / Last-Event-ID
// tests below would all silently 404. A scratch workspace (not
// the checkout) also keeps sessions hermetic: the daemon merges
// the workspace's `.qwen/settings.json` into every session, and
// a stray one on a shared runner (e.g. a `tools.sandbox` mode or
// a `tools.core` allowlist missing `todo_write`) silently breaks
// the Stop Guard flow below.
'--workspace',
REPO_ROOT,
workspaceDir,
],
{
stdio: ['ignore', 'pipe', 'pipe'],
Expand Down Expand Up @@ -211,6 +215,9 @@ afterAll(async () => {
if (homeDir) {
rmSync(homeDir, { recursive: true, force: true });
}
if (workspaceDir) {
rmSync(workspaceDir, { recursive: true, force: true });
}
}, 15_000);

/** Open an authenticated SSE stream and yield parsed frames. */
Expand Down Expand Up @@ -241,7 +248,7 @@ async function* sseFrames(
describePOSIX('qwen serve — child-crash recovery (real SIGKILL)', () => {
it('publishes session_died after the qwen --acp child is SIGKILL-ed', async () => {
const session = await client.createOrAttachSession({
workspaceCwd: REPO_ROOT,
workspaceCwd: workspaceDir,
});

// Find the daemon's direct `--acp` child PID.
Expand Down Expand Up @@ -295,7 +302,7 @@ describePOSIX('qwen serve — child-crash recovery (real SIGKILL)', () => {
);

// Listing must NOT show the dead session.
const remaining = await client.listWorkspaceSessions(REPO_ROOT);
const remaining = await client.listWorkspaceSessions(workspaceDir);
// Explicit `s` type for resilience against a stale dist .d.ts
// in the reviewer's tsc env (see same note in routes.test.ts).
expect(
Expand All @@ -306,7 +313,7 @@ describePOSIX('qwen serve — child-crash recovery (real SIGKILL)', () => {

// Retry must spawn fresh, not reuse the corpse.
const fresh = await client.createOrAttachSession({
workspaceCwd: REPO_ROOT,
workspaceCwd: workspaceDir,
});
expect(fresh.sessionId).not.toBe(session.sessionId);
expect(fresh.attached).toBe(false);
Expand All @@ -316,7 +323,7 @@ describePOSIX('qwen serve — child-crash recovery (real SIGKILL)', () => {
describePOSIX('qwen serve — multi-client first-responder permission', () => {
it('fans out permission_request to both subscribers; only one vote wins', async () => {
const session = await client.createOrAttachSession({
workspaceCwd: REPO_ROOT,
workspaceCwd: workspaceDir,
});

// Pin the session to `default` approval mode. The ACP child
Expand Down Expand Up @@ -448,7 +455,7 @@ describePOSIX('qwen serve — multi-client first-responder permission', () => {
describePOSIX('qwen serve — Last-Event-ID resume', () => {
it('reconnect with Last-Event-ID:N yields events with id > N', async () => {
const session = await client.createOrAttachSession({
workspaceCwd: REPO_ROOT,
workspaceCwd: workspaceDir,
});

// Fire a short prompt to populate the bus.
Expand Down Expand Up @@ -494,7 +501,7 @@ describePOSIX('qwen serve — Last-Event-ID resume', () => {
describePOSIX('qwen serve — daemon Todo Stop Guard replay', () => {
it('continues after prompt admission without an SSE client and replays the bounded attempts', async () => {
const session = await client.createOrAttachSession({
workspaceCwd: REPO_ROOT,
workspaceCwd: workspaceDir,
});
const requestStart = fakeServer.requests.length;
const guardMarker = `todo-guard-e2e-${requestStart}`;
Expand Down
Loading
Loading