diff --git a/docs/users/_meta.ts b/docs/users/_meta.ts index 06587e1e735..3691406b61c 100644 --- a/docs/users/_meta.ts +++ b/docs/users/_meta.ts @@ -15,6 +15,7 @@ export default { 'integration-jetbrains': 'JetBrains IDEs', 'integration-github-action': 'GitHub Actions', 'qwen-serve': 'Daemon mode (qwen serve)', + 'qwen-serve-deploy-local': 'Daemon mode — local launch templates', 'Code with Qwen Code': { type: 'separator', title: 'Code with Qwen Code', // Title is optional diff --git a/docs/users/qwen-serve-deploy-local.md b/docs/users/qwen-serve-deploy-local.md new file mode 100644 index 00000000000..5746296b9a2 --- /dev/null +++ b/docs/users/qwen-serve-deploy-local.md @@ -0,0 +1,221 @@ +# Local launch templates for `qwen serve` (v0.16-alpha) + +Reference templates for running `qwen serve` as a long-lived background process on a developer workstation. Pairs with the [v0.16-alpha known limits](./qwen-serve.md#v016-alpha-known-limits) — local-only, single-user, BYO bearer token. Containerized / multi-host / TLS-fronted deployments defer to v0.16.x. + +> **Audience**: dogfooding developers who want the daemon up across reboots, with logs going somewhere durable, and a clean `restart-on-failure` story. If you only need the daemon for the duration of a single shell session, plain `qwen serve` (foreground, Ctrl-C to stop) is fine. + +## Generate a bearer token (once) + +```bash +openssl rand -hex 32 > ~/.qwen-serve-token # user-managed, NOT a built-in path +chmod 600 ~/.qwen-serve-token +export QWEN_SERVER_TOKEN="$(cat ~/.qwen-serve-token)" +``` + +The path / filename is yours to choose; v0.16-alpha does not auto-generate or auto-locate a token file (deferred to v0.16.x). See the [Authentication](./qwen-serve.md#authentication) section of the user guide for the canonical BYO setup. + +> **Scope this `export` to the current shell session only.** Don't add it to `~/.bashrc` / `~/.zshrc` — a profile-level export exposes the bearer token to every process spawned from that shell (IDE subprocesses, browser debuggers, `npm` scripts from unrelated projects). For long-running setups, use the systemd `EnvironmentFile=` / launchd `EnvironmentVariables` mechanisms below — both scope the token to just the daemon process. + +The daemon reads the bearer token from either `--token ` on the CLI or the `QWEN_SERVER_TOKEN` env var (whitespace stripped from both). The TypeScript SDK's `DaemonClient` constructor falls back to `QWEN_SERVER_TOKEN` when no `token` option is passed (PR 27 fallback — clients with the env var set never need to thread the value through their script). + +One shell-level `export` covers both server boot and SDK client construction (just keep it scoped to the session, per the note above). + +## Linux: systemd user unit + +> **Find your `qwen` binary first.** The unit file's `ExecStart=` must hold an **absolute path** — service managers don't read your shell's `PATH`. Run `which qwen` to discover it. Common locations: `/usr/local/bin/qwen` (Linuxbrew, manual installs), `~/.nvm/versions/node/vX.Y.Z/bin/qwen` (nvm), `~/.fnm/aliases/default/bin/qwen` (fnm), `~/.volta/bin/qwen` (Volta). Substitute the actual path everywhere the templates below show `/PATH/TO/qwen`. + +`~/.config/systemd/user/qwen-serve.service`: + +```ini +[Unit] +Description=Qwen Code daemon (loopback HTTP + SSE) +After=network.target + +[Service] +Type=simple +# Replace with your project; %h expands to $HOME under user units. +WorkingDirectory=%h/your-project +# Run `which qwen` to find the absolute path. systemd does NOT read $PATH. +ExecStart=/PATH/TO/qwen serve --hostname 127.0.0.1 --port 4170 +# Read the bearer token from a chmod 600 file rather than inlining it +# in the unit. `Environment=` would expose the token in the unit file +# (typically 644 = world-readable). EnvironmentFile keeps the token in +# the user-owned secret file you already created with `chmod 600`. +EnvironmentFile=%h/.qwen-serve-token-env +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=default.target +``` + +Build the env file once (the token file from the setup step holds the raw value; this wraps it in `KEY=value` form so systemd reads it as an env assignment): + +```bash +echo "QWEN_SERVER_TOKEN=$(cat ~/.qwen-serve-token)" > ~/.qwen-serve-token-env +chmod 600 ~/.qwen-serve-token-env +``` + +Manage: + +```bash +systemctl --user daemon-reload +systemctl --user enable --now qwen-serve.service +loginctl enable-linger "$(whoami)" # keep the user manager running after logout / across reboot +journalctl --user -u qwen-serve -f # tail logs +systemctl --user restart qwen-serve.service # after token rotation +systemctl --user disable --now qwen-serve.service +``` + +Without `loginctl enable-linger`, the user-level systemd instance shuts down when the user logs out and only restarts on next login — on a headless dev box the daemon would not survive an SSH session ending. `enable-linger` is what makes "across reboots" actually work. + +**System-wide alternative** (shared dev hosts, less common): drop the unit at `/etc/systemd/system/qwen-serve@.service` with `User=%i`, manage via `sudo systemctl enable --now qwen-serve@.service`. Same `[Service]` body otherwise — but world-readable `Environment=` exposure is even more problematic at this level, so always use `EnvironmentFile=` pointing at the user's `chmod 600` file. Pick user-level + linger for single-user workstations. + +## macOS: launchd user agent + +> **Find your `qwen` binary first.** Same constraint as systemd — `ProgramArguments` must hold an **absolute path**. Run `which qwen` to discover it. Common locations on macOS: `/opt/homebrew/bin/qwen` (Homebrew on Apple Silicon), `/usr/local/bin/qwen` (Homebrew on Intel, manual installs), `~/.nvm/versions/node/vX.Y.Z/bin/qwen` (nvm), `~/.volta/bin/qwen` (Volta). Substitute below where the template shows `/PATH/TO/qwen`. + +`~/Library/LaunchAgents/com.qwenlm.qwen-serve.plist`: + +```xml + + + + + Label + com.qwenlm.qwen-serve + ProgramArguments + + + /PATH/TO/qwen + serve + --hostname + 127.0.0.1 + --port + 4170 + + + WorkingDirectory + /Users/YOUR-USERNAME/your-project + EnvironmentVariables + + + QWEN_SERVER_TOKEN + PASTE-YOUR-TOKEN-HERE + + RunAtLoad + + + KeepAlive + + SuccessfulExit + + + + ThrottleInterval + 10 + + StandardOutPath + /Users/YOUR-USERNAME/Library/Logs/qwen-serve/out.log + StandardErrorPath + /Users/YOUR-USERNAME/Library/Logs/qwen-serve/err.log + + +``` + +Manage: + +```bash +mkdir -p ~/Library/Logs/qwen-serve # first time only +chmod 600 ~/Library/LaunchAgents/com.qwenlm.qwen-serve.plist # plist holds the inline token +launchctl load ~/Library/LaunchAgents/com.qwenlm.qwen-serve.plist +launchctl unload ~/Library/LaunchAgents/com.qwenlm.qwen-serve.plist # to stop +tail -f ~/Library/Logs/qwen-serve/out.log ~/Library/Logs/qwen-serve/err.log +``` + +After editing the plist (e.g., rotating the token) you must `unload` then `load` again — `launchctl` does not auto-reload on plist changes the way `systemd daemon-reload` does. Note: each `load` truncates the log files, so save them off if you're investigating an incident before rotating. + +## tmux session (interactive supervision) + +Assumes `QWEN_SERVER_TOKEN` is already exported in your shell (see the setup section above): + +```bash +tmux new -d -s qwen-serve "cd ~/your-project && qwen serve --hostname 127.0.0.1" +tmux attach -t qwen-serve # see live logs; Ctrl-b d to detach +tmux kill-session -t qwen-serve +``` + +`tmux new -d` inherits the parent shell's environment, so `QWEN_SERVER_TOKEN` flows through automatically. Best when you want to occasionally watch the daemon's stdout (auth warnings, MCP discovery progress, slow-client warnings) without committing to a service unit. Survives terminal close but not host reboot. + +## nohup one-liner (quick + dirty) + +Assumes `QWEN_SERVER_TOKEN` is already exported in your shell: + +```bash +nohup bash -c 'cd ~/your-project && qwen serve --hostname 127.0.0.1' > qwen-serve.log 2>&1 & +echo $! # daemon PID; capture if you want to `kill` cleanly later +``` + +The wrapping `bash -c '...'` ensures the daemon binds to `~/your-project` rather than wherever you happened to run the command. Without that `cd`, `qwen serve` defaults to `process.cwd()` and a `POST /session` from a client expecting your project workspace returns `400 workspace_mismatch` — silent foot-gun. + +OK for one-off "let me run this in the background while I poke at the API" workflows. **Not recommended** for anything beyond a single session — no restart-on-crash, log file grows unbounded, no clean way to find the daemon if you forget the PID. Prefer tmux for interactive supervision or systemd / launchd for anything you want to outlast a reboot. + +## Verifying the daemon is up + +```bash +curl http://127.0.0.1:4170/health # → {"status":"ok"} +curl -H "Authorization: Bearer $QWEN_SERVER_TOKEN" \ + http://127.0.0.1:4170/capabilities | jq .protocolVersions # daemon's feature set +``` + +When auth is configured (i.e., the daemon was started with `--token` / `QWEN_SERVER_TOKEN` set, OR `--require-auth=true`), every route except `/health` on loopback binds requires `Authorization: Bearer `. If you started the daemon without a token on the loopback default (the `qwen serve` zero-config path), neither call requires a header. The templates above all configure a token, so the `Authorization` header is needed in practice. If `/capabilities` returns `401`, the unit / plist token doesn't match the env-exported token your `curl` is using. + +## Token rotation + +1. Generate a new token + write the env file the unit references: + ```bash + openssl rand -hex 32 > ~/.qwen-serve-token + chmod 600 ~/.qwen-serve-token + echo "QWEN_SERVER_TOKEN=$(cat ~/.qwen-serve-token)" > ~/.qwen-serve-token-env + chmod 600 ~/.qwen-serve-token-env + ``` + (For the launchd / nohup / tmux templates: edit the plist's `` value or re-`export QWEN_SERVER_TOKEN`. Don't forget `chmod 600` on the plist if you regenerate it.) +2. Restart the daemon: + - **systemd**: `systemctl --user restart qwen-serve.service` + - **launchd**: `launchctl unload ~/Library/LaunchAgents/com.qwenlm.qwen-serve.plist && launchctl load ~/Library/LaunchAgents/com.qwenlm.qwen-serve.plist` + - **tmux / nohup**: `kill ` then re-run with the new token in env +3. Update any client SDKs / scripts. The TypeScript SDK's `DaemonClient` reads `QWEN_SERVER_TOKEN` automatically (PR 27 fallback) — re-`export` the new value in any client shell and reconstruct the client. + +## Restart and crash behavior + +Service-manager restart semantics differ across the templates: + +- **systemd `Restart=on-failure`** — restart only on non-zero exit / signal. A clean SIGTERM (`systemctl stop`) does **not** trigger a restart loop. +- **launchd `KeepAlive` with `SuccessfulExit=false`** (the template above) — matches systemd behavior. A bare `` would have respawned even after a clean exit. `ThrottleInterval=10` rate-limits restart storms on persistent failures, mirroring systemd's `RestartSec=5`. +- **tmux / nohup** — no automatic restart. A daemon crash leaves you with a dead PID until you re-run. + +Within a **single daemon process lifetime**, client disconnects recover via SSE `Last-Event-ID` resume per the [Durability model](./qwen-serve.md#durability-model) section of the user guide — the replay ring is in-memory. + +A daemon **restart** drops all in-memory sessions; clients reconnect and start fresh. Cross-restart durability of session content (prompts, tool calls, conversation history) is **NOT** in v0.16-alpha. + +## Out of scope (defers to v0.16.x or later) + +- **Containerized deployment** — Dockerfile, docker-compose, Kubernetes manifests, nginx + TLS reverse proxy, multi-instance token isolation. Defers to v0.16.x once an enterprise pilot is committed; the doc would otherwise rot from no-one-validating. +- **Cross-host federation / multi-daemon coordination on one host** — `1 daemon = 1 workspace × N sessions` is enforced. Instance-path token keying + stale-token cleanup defer to v0.16.x. +- **Auto-generated daemon tokens** — alpha is BYO-token. Auto-gen + token-store infrastructure defers to v0.16.x. +- **Windows native service** (`nssm`, Service Control Manager wrapper) — for now use [WSL2](https://learn.microsoft.com/en-us/windows/wsl/) and follow the systemd section above. + +See the [v0.16-alpha known limits](./qwen-serve.md#v016-alpha-known-limits) callout in the main user guide for the full deferred-features list, and [#4175](https://github.com/QwenLM/qwen-code/issues/4175) for the v0.16-alpha rollout tracking issue. diff --git a/docs/users/qwen-serve.md b/docs/users/qwen-serve.md index ec2306b7744..0bc91349302 100644 --- a/docs/users/qwen-serve.md +++ b/docs/users/qwen-serve.md @@ -29,7 +29,7 @@ The first npm release of `qwen serve` (v0.16-alpha) is intentionally narrow — **Deployment surface — local-only:** - ✅ Loopback (`127.0.0.1`, default) — no auth required, suitable for dev workstations -- ✅ Local launch via `systemd` / `launchd` / `nohup &` / `tmux` (templates land in PR 30a) +- ✅ Local launch via `systemd` / `launchd` / `nohup &` / `tmux` — see [Local launch templates](./qwen-serve-deploy-local.md) - ✅ Bring-your-own bearer token via `QWEN_SERVER_TOKEN` env var ([Authentication](#authentication) for setup) - ❌ **Containerized deployment** — Docker / Compose / Kubernetes / nginx reverse-proxy with TLS termination NOT in v0.16-alpha. Defers to v0.16.x once an enterprise pilot is committed (would otherwise rot from no-one-validating). - ❌ **Multi-daemon coordination on one host** — `1 daemon = 1 workspace × N sessions` is enforced. Cross-host federation, instance-path token keying, and stale-token cleanup defer to v0.16.x. @@ -458,6 +458,7 @@ const result = await flow.awaitCompletion({ signal: abortCtrl.signal }); ## What's next +- **Setting up a long-running daemon?** [Local launch templates (systemd / launchd / nohup / tmux)](./qwen-serve-deploy-local.md) for v0.16-alpha (local-only). - **Build a client?** See the [DaemonClient TypeScript quickstart](../developers/examples/daemon-client-quickstart.md) and the [HTTP protocol reference](../developers/qwen-serve-protocol.md). - **Reading the source?** Bridge code lives at `packages/cli/src/serve/`; SDK client at `packages/sdk-typescript/src/daemon/`. - **Tracking the roadmap?** Stage 1.5 / Stage 2 progress is tracked on issue [#3803](https://github.com/QwenLM/qwen-code/issues/3803).