Режим демона (qwen serve)
Запускайте Qwen Code как локальный HTTP-демон, чтобы несколько клиентов (плагины для IDE, веб-интерфейсы, скрипты CI, пользовательские CLI) использовали одну сессию агента через HTTP + Server-Sent Events вместо того, чтобы каждый из них запускал свой собственный подпроцесс.
🚧 v0.16-alpha:
qwen serveвпервые публикуется в npm в версии v0.16-alpha как текстовый чат / кодинг с только локальным развертыванием. Поддержка вложений изображений / файлов в пути промпта, контейнеризированное развертывание (Docker / k8s / nginx reverse-proxy) и защита удаленного / мультидемонного режима будут добавлены в последующем патче, когда будет подтвержден пилотный проект для предприятий. Полный список отложенных функций см. в разделе Известные ограничения v0.16-alpha.
Статус: Этап 1 (экспериментальный). Протокольная поверхность зафиксирована в таблице маршрутов §04 из issue #3803 . Этап 1.5 (флаг
qwen --serve— TUI размещает тот же HTTP-сервер) и Этап 2 (рефакторинг внутри процесса + полировкаmDNS/OpenAPI/WebSocket/Prometheus) следуют непосредственно за ним.Честность в отношении охвата: Этап 1 рассчитан на разработчиков, создающих прототипы клиентов для протокольной поверхности, и на локальное сотрудничество одного пользователя / небольшой команды. Рабочие нагрузки производственного уровня с множеством клиентов / длительным выполнением / нестабильной сетью (мобильные приложения, IM-боты с 1000+ чатами) требуют гарантий Этапа 1.5+, которых нет в этом релизе. Полный список пробелов см. в разделе Гарантии времени выполнения Этапа 1.5+, а дорожную карту сближения — в #3803.
Что это дает
- Встроенный веб-интерфейс Web Shell —
qwen serve«из коробки» обслуживает браузерный Web Shell в своем корне (http://127.0.0.1:4170/); запуститеqwen serve --open, чтобы автоматически открыть его в браузере. Он обслуживается на том же источнике (origin), что и API, поэтому второй порт или обратный прокси не нужны. Передайте--no-webдля демона только с API. - До одного основного дочернего процесса ACP плюс один по требованию для каждого доверенного вторичного, много клиентов — продакшен пытается предварительно прогреть основной бридж и повторяет попытку при первом использовании после сбоя; доверенные вторичные среды выполнения запускают свой дочерний процесс по требованию, а недоверенные вторичные никогда его не запускают. При стандартном
sessionScope: 'single'клиенты, нацеленные на одно рабочее пространство, используют одну сессию ACP и совместно работают над одним разговором, diff-ами файлов и запросами разрешений. - Потоковая передача с безопасным переподключением — SSE с переподключением через
Last-Event-IDпозволяет клиенту отключиться и возобновить работу ровно с того места, где он остановился (в пределах окна воспроизведения ring buffer). - Постраничные сохраненные транскрипты —
GET /session/:id/transcriptвозвращает полный активный сохраненный на диске транскрипт в виде страниц воспроизведения без подключения клиента и без изменения живого окна воспроизведения SSE. - Разрешения для первого ответившего — когда агент запрашивает разрешение на запуск инструмента, запрос видят все подключенные клиенты; побеждает тот клиент, который ответит первым.
- Один демон, одно или несколько рабочих пространств — повторите
--workspaceдля регистрации изолированных сред выполнения рабочих пространств под одним слушателем. Первое рабочее пространство является основным и остается по умолчанию для запросов, не указывающихcwd. - Экспериментальные каналы под управлением демона — запустите с
qwen serve --channel <name>или запустите без канала и выберите его позже командойqwen channel set. Воркеры — это отдельные процессы, принадлежащие жизненному циклу демона. Их выбор можно запрашивать, заменять, перезагружать и останавливать без перезапуска демона. - Удаленное управление средой выполнения — измените режим одобрения сессии (
POST /session/:id/approval-mode), включите/выключите инструмент (POST /workspace/tools/:name/enable) или загруженный навык (POST /workspace/skills/:name/enable) для рабочего пространства, создайте пустойQWEN.md(POST /workspace/init, только механическое действие — НЕ вызывает модель; для заполнения ИИ выполнитеPOST /session/:id/prompt), перезапустите один MCP-сервер с предварительной проверкой бюджета (POST /workspace/mcp/:server/restart) или добавьте/удалите MCP-серверы во время выполнения без перезапуска демона (POST /workspace/mcp/servers,DELETE /workspace/mcp/servers/:name). Все строго ограничено — сначала настройте--token. - Краткое содержание сессии (#4175 follow-up) — получите краткое резюме из одного предложения «на чем я остановился» для активной сессии (
POST /session/:id/recap). ОбертываетgenerateSessionRecapиз ядра как побочный запрос к быстрой модели; не засоряет ни основную историю чата, ни поток SSE. Не строгий шлюз (та же политика, что и для/prompt); хелпер SDKclient.recapSession(sessionId).- Известное ограничение — усиление затрат токенов: маршрут является конечной точкой с чистыми затратами (каждый вызов — это побочный запрос LLM, без выгоды от состояния), а у демона нет ограничения скорости (rate limit) для каждого маршрута в v1. При стандартной loopback-конфигурации без токена сбойный или вредоносный локальный клиент может спамить этот маршрут, чтобы сжечь токены. Настройте
--token(и опционально--require-auth) на общих хостах для разработки перед открытием демона. - Безопасность параллельного получения резюме: два одновременных вызова
/recapдля одной сессии запускают два независимых побочных запроса.generateSessionRecapчитает снимок истории чата черезGeminiClient.getChat().getHistory()и передает его в отдельный вызовBaseLlmClient.generateText(черезrunSideQuery); он никогда не добавляет и не изменяетGeminiChatсессии. Безопасно вызывать из нескольких клиентов без координации.
- Известное ограничение — усиление затрат токенов: маршрут является конечной точкой с чистыми затратами (каждый вызов — это побочный запрос LLM, без выгоды от состояния), а у демона нет ограничения скорости (rate limit) для каждого маршрута в v1. При стандартной loopback-конфигурации без токена сбойный или вредоносный локальный клиент может спамить этот маршрут, чтобы сжечь токены. Настройте
Известные ограничения v0.16-alpha
Первый релиз qwen serve в npm (v0.16-alpha) намеренно ограничен — это текстовый чат / кодинг для разработчиков, запускающих демон на своих машинах. Список ниже явно описывает отложенную функциональность, чтобы внедряющие могли это учесть; всё здесь входит в дорожную карту патчей v0.16.x или в ближайший последующий релиз.
Поверхность продукта — только текст:
- ✅ Текстовые промпты и текстовые ответы (чат, кодинг, вызовы инструментов, интеграция MCP)
- ❌ Вложения изображений / файлов в пути промпта —
MessageEmitterв настоящее время отображает только текст; мультимодальное эхо появится, когда будет подтверждена альфа-цель с потребностями в изображениях (#4175 chiga0 #27 P0 item) - ❌ Потоковые загрузки — те же ограничения, что и для мультимодальности
Поверхность развертывания — только локально:
- ✅ Loopback (
127.0.0.1, по умолчанию) — аутентификация не требуется, подходит для рабочих станций разработчиков - ✅ Локальный запуск через
systemd/launchd/nohup &/tmux— см. Шаблоны локального запуска - ✅ Использование своего bearer-токена через переменную окружения
QWEN_SERVER_TOKEN(настройка в разделе Аутентификация) - ❌ Контейнеризированное развертывание — Docker / Compose / Kubernetes / nginx reverse-proxy с терминацией TLS НЕ входят в v0.16-alpha. Отложено до v0.16.x после подтверждения пилотного проекта для предприятий (иначе устареет из-за отсутствия валидации).
- ❌ Координация нескольких демонов на одном хосте — один демон может размещать несколько явно зарегистрированных рабочих пространств, но демоны не координируются друг с другом. Кросс-хостовая федерация, привязка токенов к путям экземпляров и очистка устаревших токенов отложены до v0.16.x.
- ✅ Отзываемые токены сопряжения Local Control —
--local-controlгенерирует отдельный LAN-токен сопряжения, принадлежащий демону. Общее хранилище токенов демона остаётся BYO-token.
Защита — минимум для локального использования одним пользователем:
- ✅ Проверка безопасности при запуске (отказывает в привязке не к loopback без токена, PR 15 / #4236 )
- ✅ Шлюз аутентификации для маршрутов мутации, маршрутизация разрешений с областью действия сессии (PR из Wave 4)
- ✅ Ограничения MCP + координация разрешений для нескольких клиентов (F2 / F3)
- ✅ Абсолютный дедлайн промпта + таймаут простоя SSE-писателя — включается через
--prompt-deadline-msи--writer-idle-timeout-ms; анонсируется черезprompt_absolute_deadlineиwriter_idle_timeoutпри включении. - ✅ Ограничение скорости HTTP — включается через
--rate-limitи пороги для каждого уровня; анонсируется черезrate_limitпри включении. - ⏸️ Метрики Prometheus + нагрузочное тестирование — отложено до v0.17 F4 Phase-1 масштабной инструментации, когда 30-50 активных сессий станут реальной целью.
- ⏸️ CLI-флаг
--max-body-size— демон по умолчанию применяетexpress.json({ limit: '10mb' }), чего с запасом хватает для текстовых промптов (окна контекста модели значительно меньше 10 МиБ символов). Настройка через флаг в v0.16.x.
Более подробный перечень «что мы не будем исправлять на Этапе 1» (модель мутации состояния сессии на одном хосте + N параллельных сессий, использующих один дочерний процесс ACP) см. в разделе Границы области Этапа 1 — что мы не будем исправлять на Этапе 1.5 ниже.
Быстрый старт
1. Запуск демона (loopback, без аутентификации)
cd your-project/
qwen serve
# → qwen serve listening on http://127.0.0.1:4170 (mode=http-bridge, workspace=/path/to/your-project)
# → qwen serve: bearer auth disabled (loopback default). Set QWEN_SERVER_TOKEN to enable.Привязка по умолчанию — 127.0.0.1:4170. Bearer-аутентификация отключена для loopback, чтобы локальная разработка «просто работала». Демон регистрирует текущий рабочий каталог как основное рабочее пространство; используйте абсолютный путь --workspace /path/to/dir, чтобы переопределить его, и повторите флаг для регистрации дополнительных изолированных сред выполнения.
Откройте веб-интерфейс Web Shell. Перейдите по адресу http://127.0.0.1:4170/ (или запустите демон с qwen serve --open, чтобы открыть его автоматически) для доступа к полноценному браузерному терминалу — чат, diff-ы, история коммитов, вызовы инструментов и запросы разрешений. Интерфейс обслуживается в корне демона на том же источнике, что и API. В остальной части этого руководства используется сырой HTTP, чтобы вы могли писать скрипты для работы с API напрямую.
Для аутентифицированного запуска для одного пользователя без ручного создания токена включите явно:
qwen serve --open-with-authЭтот режим только для loopback генерирует 256-битный bearer-токен, если ни --token, ни QWEN_SERVER_TOKEN не предоставляют его, затем передаёт его открытому Web Shell как фрагмент URL #token=. Оболочка удаляет фрагмент и хранит учётные данные в sessionStorage этой вкладки; обновление работает, но закрытие вкладки или перезапуск демона теряет учётные данные. В CI, SSH или другой среде, где автооткрытие недоступно, демон запускается и печатает URL с фрагментом для ручного открытия. Напечатанный URL содержит секрет.
Флаг по умолчанию выключен, включает поведение открытия браузера и требует Web Shell, собранных ассетов Web Shell и привязки к loopback. Обычный qwen serve --open остаётся без токена на loopback. В режиме аутентифицированного открытия обычные API-маршруты отклоняют других локальных клиентов без bearer; статические ассеты Web Shell и loopback /health сохраняют своё существующее поведение до аутентификации, если также не установлен --require-auth. Для нескольких клиентов или повторно открываемого Web Shell используйте стабильный общий токен:
export QWEN_SERVER_TOKEN="$(openssl rand -hex 32)"
qwen serve --open2. Проверка работоспособности
curl http://127.0.0.1:4170/health
# → {"status":"ok"}
curl http://127.0.0.1:4170/capabilities
# → {"v":1,"mode":"http-bridge","features":["health","daemon_status","capabilities","session_create",...],"workspaceCwd":"/path/to/your-project"}
curl http://127.0.0.1:4170/daemon/status
# → {"v":1,"detail":"summary","status":"ok","runtime":{...}}Поле workspaceCwd показывает основное рабочее пространство совместимости, чтобы клиенты могли намеренно опустить cwd в POST /session. Текущие клиенты должны выбирать доверенную запись из workspaces[] и отправлять cwd этой записи при явном нацеливании на среду выполнения.
Поле limits.maxPendingPromptsPerSession анонсирует активный лимит приёма промптов на сессию; null означает, что лимит отключен. limits.maxTotalSessions анонсирует опциональный общедемонный лимит новых сессий; null означает без ограничений.
Запуск каналов из демона
# Start one configured channel under qwen serve
qwen serve --channel telegram
# Start several configured channels under daemon-owned workspace workers
qwen serve --channel telegram --channel feishu
# Start all configured channels
qwen serve --channel all
# Or start a token-protected daemon with no channel worker
QWEN_SERVER_TOKEN=secret qwen serve
# Enable or replace its runtime selection later
qwen channel set telegram --token secret
qwen channel set telegram feishu --token secret
qwen channel set all --token secret
# Inspect or stop daemon-managed channels
qwen channel status --daemon-url http://127.0.0.1:4170 --token secret
qwen channel stop --daemon-url http://127.0.0.1:4170 --token secretЭтот режим является экспериментальным и управляется демоном. Он не заменяет автономную команду qwen channel start: без --daemon-url существующие команды qwen channel start, stop и status остаются автономными. При qwen serve --channel демон резервирует аренду службы каналов перед прослушиванием и завершает запуск, если начальный воркер не может стать готовым. Без --channel он не загружает среду выполнения каналов и не резервирует аренду службы каналов до первого PUT среды выполнения. Если готовый воркер позже завершится с ошибкой, демон продолжает работу, перезапускает его в рамках политики ограниченных перезапусков и сообщает его состояние (включая предупреждения channel_worker_exited) в GET /daemon/status.
Управление средой выполнения открыто через GET, PUT и DELETE /workspace/channel; хелперы SDK: getChannelWorkerControl(), setChannelWorkerSelection() и stopChannelWorker(). PUT/DELETE/перезагрузка используют строгий шлюз мутации, поэтому у демона должен быть настроен bearer-токен. Выборы среды выполнения намеренно эфемерны: PUT не редактирует настройки или параметры загрузки, и перезапуск возвращает выбор qwen serve --channel (или отключен, если флаг пропущен). Именованные выборы обрезаются и дедуплицируются в порядке первого вхождения; порядок сохраняется, потому что первый канал может влиять на общий выбор модели.
qwen channel set и qwen channel reload через демон, а также status и stop с --daemon-url, не могут обнаружить токен, сгенерированный --open-with-auth. Используйте QWEN_SERVER_TOKEN и передайте то же значение с --token этим клиентам, или пропустите режим аутентифицированного открытия.
Демон читает настройки каждого канала (токены, proxy, model для каждого канала) при запуске его воркера. Для повторного чтения настроек без изменения зафиксированного выбора вызовите POST /workspace/channel/reload (SDK client.reloadChannelWorker(), или qwen channel reload). Перезагрузка повторно определяет принадлежность рабочему пространству и перезапускает выбранные воркеры через тот же безопасный путь согласования с откатом. Возможность channel_control присутствует, когда управление средой выполнения подключено; channel_reload присутствует только пока менеджер включен. Сохраненные потоки восстанавливаются с диска.
cwd каждого выбранного канала должен разрешаться в зарегистрированное рабочее пространство, и каналы группируются по этому рабочему пространству-владельцу: демон с одним рабочим пространством запускает один воркер (без изменений); демон с несколькими рабочими пространствами (--workspace повторенный) запускает один воркер для каждого рабочего пространства, которому принадлежит выбранный канал, каждый привязан к cwd этого рабочего пространства, QWEN_DAEMON_WORKSPACE и наложению env. Для размещения канала в неосновном рабочем пространстве определите его в собственном .qwen/settings.json этого рабочего пространства (без cwd) или установите явный cwd, равный пути рабочего пространства; канал, определенный только в пользовательской/системной области без cwd, неоднозначен для рабочих пространств и вызывает ошибку загрузки. --channel all остается только для основного рабочего пространства (он размещает каналы основного рабочего пространства) и не может комбинироваться с именованными каналами.
Замена выбора выполняет предварительную проверку конфигурации, принадлежности и доверия перед остановкой чего-либо. Она сохраняет воркеры рабочего пространства, чей упорядоченный выбор не изменился. Если измененный воркер не может запуститься, демон останавливает новые воркеры и восстанавливает старый выбор. Если демон не может подтвердить, что старый дочерний процесс завершился даже после SIGKILL, он сохраняет аренду PID и отказывается создавать дубликат воркера. Воркер считается готовым, когда хотя бы один запрошенный адаптер подключается; PUT затем возвращает partial: true, а /daemon/status сообщает channel_worker_partial_connect для отсутствующих адаптеров.
Когда адаптер отклоняет connect(), текущие снимки воркера могут включать записи startupFailures с каналом, phase: "connect", опциональным кодом адаптера и сообщением с удаленными учетными данными. qwen channel set, qwen channel reload и удаленный qwen channel status --daemon-url … выводят эти причины. Если каждый адаптер завершается сбоем при динамическом set или reload, команда получает 502 channel_worker_start_failed; причины в ответе описывают эту попытку, а его state описывает результат после отката. Неудачная попытка не сохраняется последующими запросами статуса. На один запуск воркера сохраняется не более 64 причин, и коды адаптеров следует рассматривать как диагностические, а не стабильные категории. Начальный запуск qwen serve --channel … по-прежнему завершается, когда ни один адаптер не подключается.
Демон также предоставляет снимки среды выполнения только для чтения для клиентских интерфейсов и
операторов: GET /daemon/status, GET /workspace/mcp,
GET /workspace/skills, GET /workspace/providers, GET /workspace/env,
GET /workspace/preflight,
GET /workspace/:id/session-info,
GET /session/:id/status, GET /session/:id/context,
GET /session/:id/supported-commands, и
GET /session/:id/tasks, GET /session/:id/lsp, и
GET /session/:id/transcript.
GET /workspace/:id/session-info (и парный
GET /workspaces/:workspace/session-info) возвращает агрегированные
количества сессий для рабочего пространства: сохраненные active / archived / total, плюс
текущее количество live в памяти, когда живое состояние доступно. Зарегистрированные
недоверенные вторичные рабочие пространства опускают live, потому что их чтения каталога не
запрашивают живой бридж. Постраничный список GET /workspace/:id/sessions не
включает общее количество, поэтому это выделенная поверхность для вопроса «сколько сессий
существует?» — полезна, когда запланированные или периодические задачи оставляют большое локальное хранилище.
⚠️ Сканирование диска — не опрашивайте часто. Эта конечная точка обходит локальные JSONL-файлы сессий в каталоге чатов рабочего пространства. Ответы всегда включают
expensive: trueиcost: "disk_scan". Вызывайте её нечасто (ручное обновление, инструментарий оператора, периодическая загрузка UI) — никогда по таймеру или при каждом рендере боковой панели. ПредпочитайтеGET /workspace/:id/sessionsдля просмотра страниц иGET /daemon/statusдля живых количеств сессий в памяти. Ответ сtruncated: trueозначает, что сканирование достигло предела безопасности или не смогло классифицировать каждый файл-кандидат, поэтому сохраненные количества являются нижними границами.
curl http://127.0.0.1:4170/workspace/$(python3 -c "import urllib.parse,os; print(urllib.parse.quote(os.getcwd(), safe=''))")/session-info
# → {"active":450,"archived":30,"total":480,"live":2,"expensive":true,"cost":"disk_scan"}GET /session/:id/status возвращает сводку live-моста для одной сессии:
sessionId, workspaceCwd, createdAt, опциональный displayName, clientCount,
и hasActivePrompt. Он возвращает 200 со сводкой, если демон хранит live-сессию с этим id, и 404 (тело { "error": …, "sessionId": … })
в противном случае. Используйте его для опроса, работает ли одна известная сессия
(hasActivePrompt) или сколько клиентов подключено (clientCount) без
получения и сканирования всего постраничного списка сессий:
curl http://127.0.0.1:4170/session/$SESSION_ID/status
# → {"sessionId":"…","workspaceCwd":"…","createdAt":"…","clientCount":1,"hasActivePrompt":false}Это необработанное представление live-сессии, поэтому clientCount и hasActivePrompt совпадают
с соответствующей записью в GET /workspace/:id/sessions — но эти два маршрута
не идентичны побайтово. Конечная точка списка обогащает каждый элемент сохраненными
данными из хранилища сессий: его createdAt — это сохраненное время первого промпта, и он
добавляет updatedAt и displayName, полученный из сохраненного заголовка или первого
промпта. /status вместо этого сообщает собственный createdAt live-сессии, опускает
updatedAt и возвращает displayName только если он установлен в live-сессии.
GET /session/:id/lsp возвращает структурированный статус LSP для каждой сессии. Запустите
демон с --experimental-lsp, чтобы включить LSP в порожденных сессиях агента;
иначе маршрут возвращает enabled: false без серверов.
GET /daemon/status — это консолидированный снимок для устранения неполадок. Стандартный
detail=summary читает только состояние демона в памяти (сессии, разрешения,
счетчики транспортов SSE/ACP, отклонения из-за ограничения скорости, память процесса, разрешенные лимиты)
и не запускает дочерний процесс ACP. Используйте GET /daemon/status?detail=full для
диагностики по каждой сессии, деталей подключения ACP, счетчиков потока устройств аутентификации и
разделов статуса рабочего пространства, когда вы активно исследуете проблему.
GET /workspace/mcp, GET /workspace/skills и GET /workspace/providers
сообщают о live-среде выполнения ACP и не запускают дочерний процесс ACP в режиме простоя;
неактивный демон возвращает initialized: false с пустым снимком. Как только
сессия становится активной, они переключаются на initialized: true и показывают реальное
состояние.
Для удаленного зеркалирования панели /skills из CLI вызовите POST /workspace/skills/:name/enable с { "enabled": true | false }, проверив возможность workspace_skill_toggle. Для изменения нескольких навыков проверьте workspace_skill_batch_toggle и вызовите POST /workspace/skills/enable с { "skillNames": ["review", "deploy"], "enabled": false }; его ответ разделяет успешные results и errors для каждой цели, сохраняет валидные цели вместе и обновляет активные сессии ACP один раз. Маршруты обновляют skills.disabled и skills.enabled рабочего пространства по мере необходимости и отклоняют неизвестные, скрытые, неактивные из расширения, заблокированные в области с более высоким приоритетом и недоверенные цели. Включение навыка skills.defaultDisabled записывает канонический opt-in в skills.enabled; жесткая запись skills.disabled, унаследованная из области с более высоким приоритетом, по-прежнему не может быть переопределена. Ячейки статуса навыков предоставляют disabledReason (hard, default или inactive_extension) и опциональный lockedScope. Ответ deferred означает, что настройка была сохранена, пока дочерний процесс ACP не запущен; она применится при запуске дочернего процесса. skills.disabled отключает как ручное использование, так и использование моделью, в отличие от disable-model-invocation: true, который оставляет доступным прямой вызов /skill-name. Для пакетов расширений V2 проверьте extension_batch_activation_v2: PUT /extensions/activation изменяет глобальные значения по умолчанию, а PUT /workspaces/:workspace/extensions/activation изменяет точные переопределения для выбранного рабочего пространства и принимает "inherit" для их очистки. Оба принимают имена в extensionNames; enabled и disabled могут быть объявлены до установки, а inherit для неизвестного имени является пустой операцией. Каждый запрос возвращает одну операцию для опроса.
GET /workspace/env и GET /workspace/preflight всегда отвечают
initialized: true независимо от состояния ACP. env никогда не обращается к ACP
(только информация о процессе демона); preflight отвечает ячейками уровня демона из
process.* и выдает заполнители status: 'not_started' для ячеек уровня ACP, когда дочерний процесс неактивен.
GET /workspace/env сообщает о среде выполнения, платформе, песочнице,
прокси и наличии (никогда о значении) переменных окружения с секретами из белого списка,
таких как OPENAI_API_KEY. URL-адреса прокси очищаются от учетных данных и сводятся
к host:port перед отправкой по сети. Маршрут всегда отвечает непосредственно от
процесса демона и никогда не порождает дочерний процесс ACP.
GET /workspace/preflight возвращает список проверок готовности. Ячейки уровня демона
(версия Node, точка входа CLI, каталог рабочего пространства, ripgrep, git, npm)
отображаются всегда. Ячейки уровня ACP (аутентификация, обнаружение MCP, навыки, провайдеры,
реестр инструментов, исходящий трафик) требуют активного дочернего процесса ACP — когда демон неактивен
они выдают заполнители status: 'not_started' вместо запуска ACP только
для их заполнения. Сбои сопоставляются с закрытым перечислением errorKind (missing_binary,
auth_env_error, init_timeout, restore_timeout, protocol_error, missing_file,
parse_error, blocked_egress), чтобы клиентские интерфейсы могли отображать структурированные
инструкции по устранению.
Демон также предоставляет хелперы для работы с файлами рабочего пространства:
GET /fileчитает текстовые файлы. Ответы полного снимка возвращают хэшsha256:<hex>сырых байтов; окна с конечным числом строк из файлов более 256 КиБ опускают его.GET /file/bytesчитает ограниченные окна сырых байтов и возвращает содержимое в base64.POST /file/writeсоздает или заменяет текстовые файлы.POST /file/editприменяет одну точную текстовую замену.
Запись/редактирование — это строгие маршруты мутации: даже для loopback они требуют
настроенного bearer-токена, иначе они возвращают token_required. Для замен
и редактирования требуется последний expectedHash из полноразмерного GET /file
(или полноразмерного GET /file/bytes). Частичное окно большого файла не может
использоваться как токен оптимистичной конкурентности. create никогда не перезаписывает. Явные записи в игнорируемые пути
разрешены, но аудируются. Бинарная запись, удаление/перемещение/mkdir и рекурсивное создание родительских
каталогов не входят в эту поверхность.
3. Открытие сессии
curl -X POST http://127.0.0.1:4170/session \
-H 'Content-Type: application/json' \
-d '{}'
# → {"sessionId":"<uuid>","workspaceCwd":"…","attached":false}cwd можно опустить — маршрут использует основное рабочее пространство демона. Отправка cwd, который не каноникализируется ни в одно зарегистрированное рабочее пространство, возвращает 400 workspace_mismatch.
Второй клиент, отправляющий запрос на /session для той же разрешенной среды выполнения рабочего пространства, получает "attached": true при стандартном sessionScope: 'single' — теперь он использует общую сессию агента этой среды выполнения. Опущенный cwd разрешается в основное рабочее пространство; выбор другого зарегистрированного рабочего пространства создает или подключается к отдельной стандартной сессии этой среды выполнения.
4. Подписка на поток событий (сначала в другом терминале)
SESSION_ID="<from step 3>"
curl -N http://127.0.0.1:4170/session/$SESSION_ID/events
# → id: 1
# event: session_update
# data: {"id":1,"v":1,"type":"session_update","data":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"…"}}}Строка data: — это полная оболочка события — {id?, v, type, data, originatorClientId?} — JSON-строка в одну строку. Полезная нагрузка ACP (блок sessionUpdate в этом примере) находится под data внутри этой оболочки. Строки id: / event: уровня SSE — это удобство для клиентов EventSource; те же значения появляются внутри JSON-оболочки, поэтому потребители с сырым fetch тоже их получают.
Откройте это до отправки промпта — буфер воспроизведения SSE хранит
последние 8000 событий, чтобы запоздалый подписчик мог догнать через Last-Event-ID,
но для простого случая «наблюдения за одним промптом» проще всего подписаться
сначала и позволить ему передавать данные в реальном времени.
Поток отправляет session_update (фрагменты LLM, вызовы инструментов, использование),
permission_request (инструменту требуется одобрение), permission_resolved
(кто-то проголосовал), model_switched, model_switch_failed и терминальные
фреймы session_died (дочерний процесс агента упал — SSE затем закрывается) и
client_evicted (ваша очередь переполнена — SSE затем закрывается).
5. Отправка промпта (вернитесь в исходный терминал)
curl -X POST http://127.0.0.1:4170/session/$SESSION_ID/prompt \
-H 'Content-Type: application/json' \
-d '{"prompt":[{"type":"text","text":"What does src/main.ts do?"}]}'
# → {"stopReason":"end_turn"}curl -N из шага 4 будет печатать фреймы по мере их поступления.
Опциональный страж остановки Todo
Клиенты с длительным выполнением могут включить ограниченное продолжение, когда
текущая рабочая цепочка успешно записывает верхнеуровневый список Todo и затем останавливается
с элементами все еще ожидающими выполнения или в процессе. Добавьте это в settings.json и
перезапустите демон:
{
"experimental": {
"todoStopGuard": true
}
}Страж добавляет не более двух последовательных вызовов основной модели без нового ввода
пользователя. Сообщение пользователя в середине хода выполняется первым и запускает новый этап с двумя попытками;
retry/continue и связанные фоновые результаты сохраняют бюджет текущего этапа. Каждый вызов и состояние окончательного исчерпания появляются как воспроизводимые
события session_update с _meta.source: "todo_stop_guard"; метаданные
включают попытку и количество незавершенных, но никогда не включают текст Todo. Очередной полный
промпт также выполняется первым, а существующие правила разрешений/отмены остаются
без изменений.
Пока активная цепочка ожидает связанную фоновую работу, несвязанные срабатывания cron/loop и уведомления о старых задачах откладываются. Периодическая работа ограничена и коалесцируется на каждую задачу, пока цепочка не уступит.
Опция по умолчанию false, требует перезапуска и принудительно отключена в безопасном
режиме, bare-режиме и режиме Approval plan. Она хранится только в памяти: загрузка состояния Todo
с диска или перезапуск демона не активируют её. Новый обычный промпт
должен успешно выполнить собственный верхнеуровневый todo_write; retry/continue и переподключение живого
клиента сохраняют текущую рабочую цепочку в памяти. Успешное изменение
рабочего каталога сессии очищает его, чтобы старый Todo не мог возобновиться в новом
рабочем пространстве.
Аутентификация
Для всего, что выходит за рамки loopback, вы обязаны передать bearer-токен:
export QWEN_SERVER_TOKEN="$(openssl rand -hex 32)"
qwen serve --hostname 0.0.0.0 --port 4170
# → boot refuses without QWEN_SERVER_TOKENЗатем клиенты отправляют Authorization: Bearer $QWEN_SERVER_TOKEN в каждом запросе. /health освобожден от этого только при привязке к loopback, чтобы проверки жизнеспособности k8s/Compose внутри пода (где демон слушает 127.0.0.1) не требовали учетных данных. При привязке не к loopback (--hostname 0.0.0.0 и т.д.) /health требует токен, как и любой другой маршрут — в противном случае злоумышленник может зондировать произвольные адреса, чтобы подтвердить существование демона. Используйте /capabilities, чтобы проверить правильность вашего токена сквозным способом (он всегда требует аутентификации):
Защищенный loopback (
--require-auth). Стандартное поведение loopback без токена подходит для ноутбука одного пользователя, но небезопасно на общих хостах для разработки, CI-раннерах или многопользовательских рабочих станциях, где любой локальный пользователь может выполнитьcurl 127.0.0.1:4170. Передайте--require-auth, чтобы сделать bearer-токен обязательным для каждого маршрута — включая/healthи/capabilities— даже при привязке к127.0.0.1. Запуск завершится ошибкой без токена. При включенном флаге неаутентифицированный клиент не может прочитать/capabilities, чтобы узнать, что требуется аутентификация; поверхностью обнаружения является само тело ответа 401. После аутентификации тегcaps.features.require_authявляется постаутентификационным подтверждением того, что развертывание защищено (полезно для интерфейсов аудита / соответствия):qwen serve --require-auth --token "$(openssl rand -hex 32)" # → /health, /capabilities, /session, … all require Authorization: Bearer … curl http://127.0.0.1:4170/health # → 401 curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:4170/capabilities | jq '.features | index("require_auth")' # → 13 (or whatever index — non-null after authenticating means the tag is present)
curl -H "Authorization: Bearer $QWEN_SERVER_TOKEN" http://your-host:4170/capabilities
# → {"v":1,"mode":"http-bridge","features":[...],"modelServices":[],"workspaceCwd":"/path/to/your-project"}
# Wrong token → 401Сравнение токенов выполняется за константное время (SHA-256 + crypto.timingSafeEqual); ответы 401 одинаковы для «отсутствующий заголовок», «неверная схема» и «неверный токен», поэтому side-channel не может их различить.
--open-with-auth — это удобство, принадлежащее CLI, а не ещё один источник токенов демона: он выбирает --token, когда эта опция определена (даже если пустая), иначе QWEN_SERVER_TOKEN, затем обрезает выбранное значение и генерирует только если результат пуст. Демон не сохраняет сгенерированное значение и не экспортирует его как QWEN_SERVER_TOKEN; существующая внутренняя передача аутентифицированному дочернему процессу остаётся неизменной. Web Shell хранит свою браузерную копию только в sessionStorage получающей вкладки; этот режим не добавляет механизма обнаружения учётных данных между вкладками или внешними клиентами. Токен не может быть отозван независимо и не привязан к идентичности клиента. Владение предоставляет те же полномочия демона, что и любой другой bearer-токен. См. дизайн аутентифицированного запуска Web Shell и связанные будущие работы в #4514 .
HTTPS / TLS (для мобильного / кросс-девайс доступа)
По умолчанию демон обслуживает простой HTTP. Это нормально для localhost, но телефон или планшет, обращающийся к IP-адресу локальной сети (https://192.168.x.x:4170), не является безопасным контекстом по http:// — поэтому браузеры блокируют getUserMedia (голосовой ввод), WebRTC и другие API, доступные только в безопасном контексте. Передайте --tls-cert + --tls-key, чтобы обслуживать Web Shell по HTTPS и разблокировать их:
# 1. Установите локальный CA и добавьте его в доверенные (однократно). Мобильное устройство
# также должно доверять этому CA — mkcert выведет путь к корневому сертификату.
mkcert -install
# 2. Сгенерируйте сертификат для LAN IP вашей машины. Добавьте localhost / 127.0.0.1 в
# SAN: при использовании `--open` демон переписывает URL браузера на
# 127.0.0.1, поэтому сертификат, выпущенный только для LAN IP, будет отклонен с ошибкой
# ERR_CERT_COMMON_NAME_INVALID. (mkcert именует выходные файлы по всем хостам.)
mkcert 192.168.1.100 localhost 127.0.0.1
# 3. Запустите демон по HTTPS. Привязка к не-loopback интерфейсам по-прежнему требует токен,
# а Origin браузера должен быть разрешен через CORS.
qwen serve \
--hostname 0.0.0.0 \
--token "$(openssl rand -hex 32)" \
--tls-cert "./192.168.1.100+2.pem" \
--tls-key "./192.168.1.100+2-key.pem" \
--allow-origin "https://192.168.1.100:4170"
# → qwen serve слушает на https://0.0.0.0:4170Примечания:
- Оба флага или ни одного — запуск завершится ошибкой, если указан только один из них (сертификат без ключа не может запустить HTTPS-слушатель).
- TLS не зависит от аутентификации — HTTPS шифрует транспорт; bearer-токен по-прежнему защищает каждый API-маршрут. Привязка к не-loopback интерфейсам требует токен как с TLS, так и без него.
- Только терминация TLS — без автогенерации, без ACME / Let’s Encrypt. Это удобство для LAN / разработки; для публичных развертываний терминацию TLS следует выполнять на reverse proxy (см. модель угроз ниже).
Флаги CLI
| Flag | Default | Purpose |
|---|---|---|
--port <n> | 4170 | TCP-порт. 0 = эфемерный порт, назначенный ОС. |
--hostname <addr> | 127.0.0.1 | Интерфейс привязки. Всё, что выходит за пределы loopback, требует токен. |
--local-control | false | Предоставить Web Shell на одном выбранном приватном IPv4-интерфейсе с принадлежащим демону отзываемым токеном сопряжения, QR-кодом терминала, точным browser origin и best-effort подавлением спящего режима. Комбинируется с --token, --allow-origin и --port 0; конфликтует с --no-web и нестандартным --hostname. Используйте --local-control-address, если доступно несколько кандидатов LAN, и добавьте --tls-cert + --tls-key для browser API безопасного контекста, таких как голосовой ввод. |
--local-control-address <ip> | — | Какой LAN IPv4-адрес предоставлять, если у хоста более одного кандидата. Необходим только если --local-control сообщает о неоднозначном выборе. |
--token <str> | — | Bearer-токен. Если не указан, используется переменная окружения QWEN_SERVER_TOKEN (с удалением начальных и конечных пробелов — удобно для $(cat token.txt)). |
--require-auth | false | Отказ от запуска без bearer-токена, даже на loopback. Усиливает стандартную настройку разработчика 127.0.0.1 для общих dev-хостов / CI-раннеров / многопользовательских рабочих станций, где любой локальный пользователь может обратиться к слушателю. Запускается только при наличии --token или QWEN_SERVER_TOKEN; также защищает /health bearer-токеном. |
--tls-cert <path> | — | Путь к файлу PEM-сертификата. Обслуживание по HTTPS вместо HTTP. Должен использоваться вместе с --tls-key (запуск завершится ошибкой, если указан только один). Открывает доступ к API браузера в безопасном контексте — голосовой ввод (getUserMedia), WebRTC — по LAN IP, которые в противном случае блокируются браузерами на обычном http://. Только терминация TLS; без автогенерации / ACME. См. HTTPS / TLS ниже. |
--tls-key <path> | — | Путь к файлу закрытого ключа PEM. Должен использоваться вместе с --tls-cert. |
--max-sessions <n> | 32 | Лимит одновременных активных сессий. Новые запросы POST /session, которые должны создать новый дочерний процесс, возвращают 503 (с Retry-After: 5) при достижении лимита; подключения к существующим сессиям НЕ учитываются. Установите 0 для отключения. Рассчитано на одного пользователя / небольшую команду; увеличьте значение, если ваше развертывание имеет запас по RAM/FD (~30–50 МБ на сессию). |
--max-total-sessions <n> | derived | Опциональный неотрицательный целочисленный общедемонный лимит создания новых сессий across всех зарегистрированных сред выполнения рабочих пространств. Применяется к новым дочерним сессиям, восстановлению сессий и сессиям, созданным через branch/fork; подключение к существующей живой сессии не потребляет слот. Установите 0 для без ограничений. Если пропущен при нескольких рабочих пространствах при запуске/восстановлении, демон выводит фиксированный лимит из лимита на рабочее пространство и количества рабочих пространств при запуске; последующая динамическая регистрация не пересчитывает его. |
--max-pending-prompts-per-session <n> | 5 | Лимит промптов на сессию, принятых POST /session/:id/prompt, но еще не завершенных, включая промпты в очереди и активный промпт. Bridge отклоняет превышение лимита синхронно с кодом 503, Retry-After: 5 и code: "prompt_queue_full" до возврата promptId. Установите 0 для отключения. branchSession сериализуется в той же FIFO-очереди, но не учитывается в этом лимите промптов. |
--workspace <path> | process.cwd() | Абсолютный каталог рабочего пространства, регистрируемый этим демоном. Повторите флаг для размещения нескольких рабочих пространств в одном процессе; первое является основным и остается по умолчанию, когда запрос опускает cwd. Относительные значения отклоняются. Запросы сессий, чей канонический cwd не зарегистрирован, возвращают 400 workspace_mismatch. |
--memory-project-scope <mode> | workspace | Режим разделения памяти проекта. workspace (по умолчанию) ключирует память по точному зарегистрированному каталогу рабочего пространства, так что каждое рабочее пространство демона получает свою изолированную память; git-root — это режим обратной совместимости, общий для рабочих пространств, разрешённых в один и тот же корень Git. Переопределяет QWEN_CODE_MEMORY_PROJECT_SCOPE, если указан; пустое значение env трактуется как неустановленное, тогда как нераспознанное непустое значение игнорируется с одноразовым предупреждением и сохраняет устаревшее поведение git-root. Новый стандарт не мигрирует существующую память проекта git-root — используйте явную область git-root для чтения этих записей во время миграции. |
--channel <name|all> | — | Экспериментальный воркер каналов, управляемый демоном. Повторите флаг, чтобы выбрать несколько настроенных каналов, или передайте all, чтобы запустить все настроенные каналы. all нельзя комбинировать с именованными каналами. Значения cwd выбранных каналов должны разрешаться в зарегистрированное рабочее пространство; демон с несколькими рабочими пространствами запускает один воркер на каждое рабочее пространство-владелец. Воркер принадлежит qwen serve; остановите демон, чтобы остановить каналы, управляемые serve. |
--max-connections <n> | 256 | Лимит TCP-соединений на уровне слушателя (server.maxConnections). Ограничивает количество сырых сокетов независимо от количества сессий — медленные / фантомные SSE-клиенты отклоняются на этапе accept при заполнении. Увеличьте вместе с --max-sessions, если ваше развертывание предполагает множество SSE-подписчиков на сессию. |
--memory-budget-mb <n> | 50% cgroup/хост | Общий бюджет памяти в МБ для всего дерева процессов демона. Если не задан, вычисляется как 50% от лимита cgroup или памяти хоста; в любом случае эффективное значение ограничивается разрешённой доступной памятью, и сообщаются как настроенное, так и эффективное значения. Он не изменяет размер дочернего процесса qwen --acp; единственный потребитель сегодня — адаптивный рост живого журнала: один общий для демона пул роста, вычисляемый как 5% эффективного бюджета (ограничен 1024 МБ; на хостах с insufficientMemory пул равен 0 и адаптивный рост отключён), разделяется каждым мостом рабочего пространства — см. --max-journal-bytes. Разрешённые значения отображаются в limits.memory в GET /daemon/status, вместе с количеством зарегистрированных и живых дочерних процессов и рекомендательными долями на дочерний процесс в runtime.memory. Хост, слишком маленький для минимума, сообщает insufficientMemory вместо принудительного увеличения; поскольку доля по умолчанию 50%, любой хост менее ~2 ГБ попадает под это. Передайте явный --memory-budget-mb 1024 на таком хосте, чтобы переопределить вычисленное значение (флаг всё ещё требует минимум 1024 МБ доступной памяти для снятия предупреждения). Должен быть целым числом в [1024, 1048576]. |
--memory-pressure-mode <mode> | observe | Преобразует ли демон собственное чтение памяти в вердикт. observe (по умолчанию) сообщает уровень давления в runtime.memory.pressure в GET /daemon/status и генерирует проблему daemon_memory_pressure — warning, так что общий status остаётся ok — когда уровень выходит за пределы normal. off по-прежнему сообщает все значения, включая уровень, но не генерирует проблему, так что общий status не изменяется; используйте при калибровке или если вы сигнализируете по верхнеуровневому статусу. Уровень — худшее из двух соотношений: RSS против доступной памяти (что отслеживает cgroup OOM killer) и использованная куча V8 против потолка кучи этого процесса. Охватывает только корневой процесс демона; сравните с runtime.memory.children.rssBytes для дочерних процессов. Ничто не ремедирует в любом режиме. Один из off, observe. |
--child-heap-mode <mode> | observe | Моделирует ли демон разбиение кучи на дочерние процессы --memory-budget-mb. observe (по умолчанию) сообщает, что бы он применил — limits.memory.childHeap.perChildCeilingMb и maxConcurrentChildren — и считает запуски, которые превысили бы лимит. Ничего не применяется: ни один дочерний процесс не получает размер из бюджета и ни один запуск не отклоняется. off ничего не моделирует и сообщает об этом: maxConcurrentChildren и perChildCeilingMb оба null, а не несут отключённое разбиение. Счётчик отказов 0 не означает, что разбиение безопасно применять: дочерние процессы всё ещё работают на гораздо большем потолке, выведенном из хоста, поэтому рабочая нагрузка, требующая больше старого пространства, чем смоделированный потолок, выглядит здесь совершенно здоровой. Применение разбиения поставляется с измерением, которое может ответить на этот вопрос. |
--event-ring-size <n> | 8000 | Глубина кольцевого буфера воспроизведения SSE на сессию (цель #3803 §02). Устанавливает размер очереди, доступной для GET /session/:id/events с Last-Event-ID: N. Больше значение = больше запаса для переподключения за счет нескольких сотен КБ дополнительной RAM на сессию. SDK-клиенты могут дополнительно запросить больший лимит очереди на подписчика для конкретной подписки через ?maxQueued=N (диапазон [16, 2048], по умолчанию 256). Демоны также отправляют нетерминальный SSE-фрейм slow_client_warning при заполнении очереди на 75%, чтобы клиенты могли обработать данные / переподключиться до отключения. Pre-flight caps.features.slow_client_warning. |
--compacted-replay-max-bytes <n> | 4194304 | Побайтовый лимит на живую сессию для сохранённых событий воспроизведения в ограниченном снимке, возвращаемом POST /session/:id/load. Лимит применяется к compactedReplay; текущий живой журнал liveJournal отдельно ограничивается --max-journal-events и --max-journal-bytes (базовые лимиты, которые может повысить адаптивный рост — см. --max-journal-bytes). Значения должны быть положительными безопасными целыми числами; невалидные значения вызывают сбой при запуске, а жёсткий потолок — 256 МиБ. Когда старые сохранённые события воспроизведения отбрасываются, снимок начинается с history_truncated. Это не ограничивает транскрипт на диске. |
--max-journal-events <n> | 10000 | Базовый лимит на сессию для записей воспроизведения, сохраняемых в живом журнале liveJournal в полёте для текущего незавершённого хода. Последовательные совместимые фрагменты текста или мыслей разделяют одну запись, не более 256 исходных событий на запись; другие границы событий сохраняются. При превышении демон сначала пытается адаптивный рост (см. --max-journal-bytes); если запас не предоставлен или не покрывает превышение, самые старые записи отбрасываются и добавляется маркер history_truncated. Счётчики truncatedEvents и retainedEvents маркера описывают исходные события. Должен быть положительным безопасным целым числом. Фиксация этого флага (или --max-journal-bytes) отключает адаптивный рост. |
--max-journal-bytes <n> | 8388608 | Базовый побайтовый лимит на сессию для живого журнала liveJournal в полёте, учитывается по сериализованным исходным событиям, даже когда совместимые фрагменты разделяют запись воспроизведения. Когда ход превышает лимит, адаптивный рост повышает лимиты сессии к удвоению (до жёсткого потолка 256 МиБ на сессию, ограниченного оставшимся запасом пула), пока рост, предоставленный всем живым сессиям демона, помещается в одном общем пуле роста размером 5% эффективного бюджета памяти демона — значения --memory-budget-mb, если передано, ограниченного разрешённой доступной памятью, иначе 50% автоопределённой памяти (см. --memory-budget-mb) — ограниченного 1024 МБ; на хостах с insufficientMemory пул равен 0 и адаптивный рост отключён. Рост происходит по требованию и только настолько, насколько позволяет пул; когда он отказан, пул исчерпан или предоставление не покрывает превышение, самые старые записи отбрасываются целиком (всегда сохраняется хотя бы одна запись), поэтому сохранённый хвост может быть значительно меньше лимита. Фиксация этого флага (или --max-journal-events) отключает адаптивный рост. Должен быть положительным безопасным целым числом. По умолчанию 8 МиБ. |
--mcp-client-budget <n> | — | Лимит в виде положительного целого числа на количество активных MCP-клиентов. Когда рекламируется mcp_workspace_pool, лимит и транспорты разделяются на среду выполнения рабочего пространства; когда тег отсутствует, устаревший менеджер на сессию обеспечивает его. Комбинируется с --mcp-budget-mode. Если не задано, принудительное ограничение на основе учета не применяется (но GET /workspace/mcp по-прежнему сообщает clientCount). Отличается от MCP_SERVER_CONNECTION_BATCH_SIZE в claude-code, который ограничивает параллелизм при запуске, а не общее количество живых клиентов. Pre-flight caps.features.mcp_guardrails и caps.features.mcp_workspace_pool. |
--mcp-budget-mode <m> | warn / off | Как применяется --mcp-client-budget. warn (по умолчанию, если бюджет задан): без отказов, budgets[0].status в снапшоте переключается на warning при ≥75% бюджета. enforce: подключения сверх лимита отклоняются, ячейка на сервер показывает disabledReason: 'budget', детерминировано по порядку объявления mcpServers. off (по умолчанию, если бюджет не задан): только наблюдаемость. Запуск отклоняет enforce без бюджета. |
--external-tool-guard-mode <m> | off | Управляемая политика предварительного выполнения внешних инструментов ACP. off не вызывает провайдеров и не рекламирует возможность. required завершает запуск с ошибкой, если совместимый провайдер не завершает рукопожатие v1, затем завершает каждый поддерживаемый вызов инструмента верхнего уровня с отказом, если его единственный запрос подготовки не разрешен. |
--external-tool-guard-endpoint <url> | — | URL провайдера HTTP(S) loopback только с origin, используемый в режиме required, например http://127.0.0.1:8787. Пути, учетные данные URL, редиректы, не-loopback хосты и маршрутизация через прокси не принимаются. |
--external-tool-guard-timeout-ms <n> | 3000 | Целое число 100..30000; применяется независимо к рукопожатию при запуске и каждому запросу подготовки. |
--http-bridge | true | Режим этапа 1: продакшен пытается предварительно прогреть один основной дочерний процесс qwen --acp для совместимости и повторяет попытку при первом использовании после сбоя, а каждая доверенная вторичная может запустить один дочерний процесс по требованию. Сессии, нацеленные на среду выполнения, мультиплексируются в её дочерний процесс через ACP newSession(); недоверенные вторичные не могут запускать ACP. Нативный in-process режим этапа 2 станет доступен позже. |
--initialize-timeout-ms <n> | 10000 | Таймаут запроса дочернего процесса ACP, включая рукопожатие initialize (мс). Должен быть положительным целым числом до 2147483647. Значения выше потолка таймера JS (2^31-1) отклоняются при запуске, потому что Node тихо сжимает их до 1 мс. Развертывания в холодных контейнерах, которым нужен дополнительный запас для запуска дочернего процесса, могут увеличить это значение; то же значение управляет дедлайнами newSession, опросов статуса рабочего пространства и других расширенных методов ACP. |
--session-restore-timeout-ms <n> | 60000 | Дедлайн загрузки/возобновления сессии ACP в миллисекундах. Должен быть положительным целым числом до 2147483647; 0 недопустим. Если не указан, по умолчанию 60 секунд, повышается до явно переданного --initialize-timeout-ms, если это значение больше; более короткий таймаут инициализации никогда не снижает бюджет восстановления. SDK и WebUI добавляют 10 и 15 секунд клиентского запаса. Таймаут возвращает повторяемый 504 session_restore_timeout; это не означает, что сам демон завершился. |
--allow-origin <pat> | — | T2.4 (#4514 ). Allowlist cross-origin для браузерных webui-клиентов. Флаг можно повторять. Каждое значение — это * (любой origin — запуск отклоняется, если bearer-токен не настроен; рекомендуется --require-auth на loopback, чтобы /health также защищался bearer-токеном, так как по умолчанию он доступен без аутентификации на loopback; статические ассеты Web Shell остаются pre-auth в любом режиме, поэтому передайте --no-web для их удаления) или канонический URL origin (<scheme>://<host>[:<port>], без завершающего слэша / пути / userinfo / query). Поддоменные wildcards (https://*.example.com) намеренно не поддерживаются — перечислите каждый поддомен явно или используйте * с настроенным токеном (и --require-auth для полного усиления). Совпавшие origins получают CORS-заголовки ответа (Access-Control-Allow-Origin, Vary: Origin, methods, headers, max-age и exposed Retry-After); несовпавшие origins по-прежнему получают 403 с той же оберткой, что и сегодня. Origin: null (песочные iframes, документы file://) всегда отклоняется, даже при *. Pre-flight через caps.features.allow_origin. Попадания self-origin на loopback не затрагиваются. |
--web / --no-web | true | Обслуживание собранного Web Shell SPA в корне демона (GET /, /assets/* и навигации документов GET /session/<id>). Эти точки входа регистрируются до шлюза bearer-аутентификации — браузер не может прикрепить токен к субресурсу <script> или навигации в адресной строке, а оболочка не содержит секретов. Каждый API-маршрут по-прежнему защищен токеном, и fallback deep-link SPA для всех остальных путей тоже находится за шлюзом bearer. При привязке к не-loopback интерфейсам в stderr выводится предупреждение в одну строку о том, что UI доступен без аутентификации. Используйте --no-web для демона только с API. Не действует, если сборка не включает ассеты Web Shell (демон логирует breadcrumb и работает только с API). |
--open | false | После запуска слушателя открывает Web Shell в браузере по умолчанию по URL демона (с добавлением #token= в качестве фрагмента URL, если токен настроен — фрагмент никогда не отправляется на сервер, что защищает токен от попадания в логи доступа и заголовки Referer). Не действует с --no-web или в headless / CI / SSH окружениях, где браузер недоступен. |
--open-with-auth | false | Открыть Web Shell с bearer-аутентификацией на loopback. Требует включённого Web Shell и собранных ассетов. Переиспользует настроенный токен или генерирует 256-битный bearer на время жизни процесса и передаёт его в фрагменте URL Web Shell. Окружения, недоступные для браузера, печатают секретный URL для ручного открытия. Другим клиентам нужен тот же явно настроенный общий токен. |
Предостережения по области памяти проекта.
- Демон vs. автономный CLI. Флаг или среда запуска демона устанавливает одну замороженную область для каждого runtime, принадлежащего этому демону.
.envрабочего пространства илиsettings.envне могут переопределить её для одного зарегистрированного рабочего пространства. Автономный TUIqwenпо-прежнему использует область git-root; для согласованности обеих точек входа экспортируйтеQWEN_CODE_MEMORY_PROJECT_SCOPEв оболочке или среде сервиса, которая их запускает.- Коллизии имен каталогов. Ключ хранилища выводится через
sanitizeCwd, который заменяет каждый не буквенно-цифровой символ на-. Соседние каталоги, различающиеся только пунктуацией (например,feature_1иfeature-1), отображаются в один каталог памяти даже в областиworkspace. Избегайте такого именования при использовании изоляции рабочих пространств.- Нормализация различается между флагом и переменной env. Переменная окружения обрезается и приводится к нижнему регистру (
" Workspace "работает); флаг CLI сопоставляется с учетом регистра черезchoicesyargs (--memory-project-scope Workspaceотклоняется). Используйте значения в нижнем регистре при копировании между ними.
Встроенный страж перемещения Git для демона
Каждая управляемая сессия ACP демона применяет встроенный страж предварительного выполнения
для shell-команд модели, независимо от --external-tool-guard-mode и без
рекламы какой-либо возможности. Демон владеет привязанным рабочим пространством и
текущим эффективным рабочим каталогом сессии; оба поставляются из доверенного
состояния сессии и никогда не принимаются от дочернего процесса ACP.
Страж проверяет инструменты, выполняющие командную строку оболочки — run_shell_command
и monitor — и отклоняет мутирующую команду Git
до выполнения, когда её расположение репозитория разрешается за пределами
эффективного рабочего каталога сессии. Перемещение распознаётся для литеральных
форм git -C <path>, git --git-dir[=]<path>,
git --work-tree[=]<path>, ведущих
назначений GIT_DIR/GIT_WORK_TREE/GIT_COMMON_DIR/GIT_INDEX_FILE (также
когда они сделаны через export/declare/readonly, которые сохраняют их в
окружении каждой последующей команды в цепочке),
флаги смены каталога-обёртки (env -C, sudo -D) и встроенные команды cd, pushd или
popd ранее в той же цепочке команд. Общие префиксы-обёртки
(sh -c, bash -c, eval, sudo, nohup, timeout, exec, command,
builtin,
env, квалифицированные путём бинарные файлы git и синтаксис оболочки { …; } / ! …)
разворачиваются, чтобы та же политика применялась к внутреннему вызову Git, и тела подстановок $(…)
или обратных кавычек анализируются как самостоятельные команды.
Субагент, привязанный к собственному worktree, содержится в этом worktree, а не в каталоге сессии; shell-вызов, каталог выполнения которого демон не может определить, отклоняется.
Относительные цели разрешаются от эффективного начального каталога команды
(arguments.directory, если присутствует, иначе текущий эффективный рабочий
каталог сессии) после канонического разрешения пути, включая перенаправления gitfile .git,
символические ссылки и административные каталоги для каждого worktree. Перемещённая
цель, которая не может быть полностью разрешена до выполнения — динамическая цель
($VAR, обратные кавычки, ~, globs), путь, который ещё не существует, или
нечитаемое перенаправление — отклоняется для мутирующих или неклассифицируемых подкоманд.
Перемещённая цель, которая не может быть разрешена, отклоняется независимо от подкоманды —
включая только для чтения. Перемещённые команды, чья подкоманда входит
в небольшую проверенную набор только для чтения (rev-parse, cat-file), остаются разрешёнными
после разрешения цели, если только команда не несёт исполняющую команду конфигурацию -c
или не несёт флаг --output, --textconv или --filters: те записывают файл
или запускают настроенные драйверы целевого репозитория. Команды без распознанного
перемещения сохраняют своё существующее поведение.
Отказы являются окончательными и сообщаются модели как
Daemon shell guard denied a mutating Git command… для разрешённого, динамического
или неразрешимого расположения репозитория и как
Daemon shell guard denied a shell command…, когда команда не может быть
разобрана, её содержимое не может быть разрешено или нераспознанная программа может выполнить
перемещённую команду Git.
Страж надёжен против перемещения Git, записанного в литеральных формах
выше — именно от таких ошибочно нацеленных команд и существует этот контроль — и
является best-effort, а не границей, против текста оболочки, написанного для его обхода:
конструкции, скрывающие перемещение от статического читателя, могут пройти, и новые
будут продолжать находиться. Не предоставляйте демону более широкое доверие на
основе этого. Он не интерпретирует файлы скриптов,
не отслеживает значения переменных окружения между командами и не анализирует тела heredoc
(текст в форме Git внутри heredoc может быть отклонён, даже если оболочка никогда
его не выполняет). /fork и память рабочего пространства на базе агента remember/dream остаются
доступными под встроенным стражем; они ограничены только пока активен
режим внешнего провайдера ниже. Опциональный внешний страж инструментов
остаётся дополнительной политикой и получает тот же запрос только после того, как
встроенная политика его разрешит.
Обязательный страж внешних инструментов
Этот opt-in предназначен для управляемых развертываний ACP, которым нужно внешнее решение разрешить/запретить
на границе окончательного выполнения инструмента. Он полностью неактивен, если
отсутствует --external-tool-guard-mode=required:
export QWEN_CODE_EXTERNAL_TOOL_GUARD_TOKEN='replace-with-local-secret'
qwen serve \
--external-tool-guard-mode=required \
--external-tool-guard-endpoint=http://127.0.0.1:8787 \
--external-tool-guard-timeout-ms=3000Провайдер должен предоставить POST /v1/handshake и POST /v1/prepare, требовать
Authorization: Bearer <token>, возвращать JSON, повторять supplied nonce или
ID запроса и использовать версию протокола 1. Токен должен быть непустым, не более
8192 кодовых единиц UTF-16 и не содержать управляющих символов. Запросы ограничены
до 1 МиБ, ответы до 64 КиБ, а опциональные причины отказа до 500 кодовых единиц UTF-16
без управляющих символов. Успешный ответ prepare:
{ "protocolVersion": 1, "requestId": "<echo>", "allowed": true }Отказ использует allowed:false и может добавить короткую reason. Для каждого поддерживаемого
вызова инструмента верхнего уровня, который проходит существующие разрешения и PreToolUse
и достигает границы окончательного выполнения, Qwen Code отправляет один запрос prepare
и никогда его не повторяет. Более ранний отказ разрешения/хука не отправляет
запрос prepare. Таймаут, отмена, сбой транспорта, неправильные или
несоответствующие ответы и явный запрет предотвращают выполнение.
Каждый порожденный канал ACP также должен подтвердить, что он установил требуемый
обратный вызов; отсутствующее или несовместимое подтверждение отклоняет канал до
создания сессии.
Запрос провайдера несет sessionId, promptId, toolCallId, каноническое
toolName и окончательные arguments; toolCallId — это метка корреляции, а не
аутентификационная идентичность или самостоятельный ключ идемпотентности.
Окончательные аргументы могут содержать чувствительные данные приложения. Обрабатывайте их соответственно в логах провайдера и хранилище аудита.
Хуки PreToolUse выполняются до этого окончательного решения исполнителя. Режим Required Guard
не авторизует и не ограничивает поведение хуков; развертывания, которым нужна граница
вокруг каждого возможного побочного эффекта, должны отключить хуки или управлять их
реализациями отдельно.
Действия слеш-команд также выполняются до планирования модели/инструмента и не являются
вызовами Guard. Некоторые встроенные команды могут напрямую изменять файлы или настройки. Управляемое
развертывание, которому нужна граница всех эффектов, должно отклонять ввод слеш-команд
или отключать каждую неодобренную команду через slashCommands.disabled или
--disabled-slash-commands.
Область управления v1 — это инструменты верхнего уровня, вызываемые активным фоновым
управляемым промптом. Вложенные или делегирующие agent, workflow,
create_sub_session, send_message, прямой /fork и элементы памяти рабочего пространства
на базе агента remember/dream отклоняются, пока активен режим required. Фоновая оболочка верхнего уровня или запуск монитора — это все еще один защищенный вызов, и его окончательные аргументы достигают провайдера, но эта функция непрерывно не авторизует процесс и не добавляет
протокол аудита завершения процесса; политика, требующая завершения на переднем плане, должна отклонять эти
формы. Защищенные вызовы MCP также отключают автоматическое переподключение/воспроизведение после
ошибки транспорта. После успешного рукопожатия при запуске /capabilities
рекламирует external_tool_guard; его отсутствие означает, что клиенты не должны предполагать
принудительное применение.
Эта функция не авторизует явные вызовы управления REST/ACP демона; они продолжают использовать существующую аутентификацию демона и контракты маршрутов. Она также не делает разрешенный инструмент или команду оболочки детерминированными или не ограничивает их внутреннее содержимое; управляемые развертывания должны комбинировать решение провайдера с их обычной политикой инструментов и границей изоляции.
Настройка лимитов нагрузки.
--max-sessions— это лимит новых сессий на рабочее пространство.--max-total-sessions, если установлен, — это общедемонный лимит новых сессий. Еще три уровня также ограничивают нагрузку — при настройке для высоконагруженного развертывания с высокой конкурентностью настраивайте их совместно:
- уровень listener:
--max-connections/server.maxConnections=256ограничивает количество сырых TCP-соединений (back-pressure для медленных клиентов).- подписчики на сессию: EventBus по умолчанию ограничивает количество SSE-подписчиков до 64 на сессию; 65-й клиент получает терминальный
stream_errorи отключается.- прием промптов на сессию:
--max-pending-prompts-per-session=5ограничивает количество промптов в очереди + активных промптов, принимаемых для одной сессии. При переполнении возвращается503сRetry-After: 5.- общедемонные новые сессии:
--max-total-sessions=Nограничивает создание новых сессий по всему демону. При переполнении возвращается та же формаsession_limit_exceededсscope: "total".- бэклог на подписчика: очередь из 256 фреймов на SSE-клиент; клиент, превысивший емкость, получает терминальный фрейм
client_evictedи отключается (один медленный потребитель не может “повесить” демон).Эти лимиты взаимосвязаны: каждое рабочее пространство ограничено
--max-sessions, а--max-total-sessionsограничивает их совокупность. Эффективный потолок сессий — это минимум из любого конечного общедемонного лимита и совокупного лимита на среду выполнения (считайте эту совокупность неограниченной, если лимит на рабочее пространство неограничен). Если ни один не конечен, конечного потолка сессий нет. Конечный потолок × 64 подписчика × 256 фреймов — это худший случай памяти в полете на уровне EventBus; умножение на--max-pending-prompts-per-sessionограничивает принятую работу промптов на слое приёма. Стандартный размер предполагает нагрузку одного пользователя / небольшой команды; увеличивайте прогрессивно (и следите за RSS) для больших развертываний.
Ограничения для MCP-клиентов (issue #4175 PR 14). Рабочее пространство, объявляющее 30 MCP-серверов в
mcpServers, запустит 30 клиентов без верхнего лимита, если вы его не зададите.--mcp-client-budget=Nограничивает количество активных MCP-клиентов;--mcp-budget-mode={enforce,warn,off}выбирает поведение. По умолчаниюwarn, если задан бюджет (снапшот выводит предупреждение, но ни один клиент не отклоняется — полезно для измерения реального fanout перед включением принудительного режима). Отклоненные серверы в режимеenforceполучаютdisabledReason: 'budget'в своей ячейке на сервер, а ячейкаbudgets[0]показываетstatus: 'error'+errorKind: 'budget_exhausted'. Резервирование слота происходит по имени сервера и сохраняется при переподключениях / таймаутах обнаружения — отклоненный сервер не может занять слот у работающего.Текущая область определяется возможностями. Когда присутствует
mcp_workspace_pool, все сессии в одной среде выполнения рабочего пространства разделяют его пул транспортов MCP и контроллер бюджета;GET /workspace/mcpвыдаетscope: 'workspace'. Второе рабочее пространство имеет независимый пул и бюджет. Когда тег отсутствует (включаяQWEN_SERVE_NO_MCP_POOL=1), демон использует устаревшийMcpClientManagerна сессию и выдаетscope: 'session'; в этом фолбэке N сессий могут каждая потреблять настроенный лимит.qwen serve --mcp-client-budget=10 --mcp-budget-mode=warn # позже, после того как телеметрия покажет ваше реальное распределение: qwen serve --mcp-client-budget=10 --mcp-budget-mode=enforceЭто не то же самое, что
MCP_SERVER_CONNECTION_BATCH_SIZEв claude-code (который ограничивает конкурентность при запуске); они ортогональны. Клиенты должны ветвиться поmcp_workspace_pool, а не предполагать область только из версии протокола.Push-события (issue #4175 PR 14b). SDK-клиенты, подписанные на
GET /session/:id/events, получают типизированные фреймы при пересечении порогов бюджета —mcp_budget_warning(синтетический, срабатывает один раз при пересечении 75% в сторону увеличения с повторным взведением гистерезиса на 37.5%, анонсируется черезmcp_guardrail_events) иmcp_child_refused_batch(объединяется один раз за проход обнаружения в режимеenforce; длина 1 при отказе в ленивом создании изreadResource). Снапшот поGET /workspace/mcpпо-прежнему является источником истины для состояния после переподключения; события — это фронты изменений. Полезно для построения дашбордов в реальном времени без опроса.
Модель угроз для деплоя по умолчанию
- Только 127.0.0.1 — привязка к loopback, аутентификация не требуется.
--hostname 0.0.0.0требует токен — запуск будет отклонен без него.LOOPBACK_BINDSвключает IPv6 —::1и[::1]считаются loopback для правила без токена.- Allowlist заголовка Host — при привязке к loopback демон проверяет, что
Host:совпадает сlocalhost:port/127.0.0.1:port/[::1]:port/host.docker.internal:port(без учета регистра согласно RFC 7230 §5.4) для защиты от DNS rebinding. Привязки не к loopback (--hostname 0.0.0.0) намеренно обходят allowlist Host — оператор сам выбрал поверхность атаки, поэтому проверка bearer-токена является единственным уровнем аутентификации; обратные прокси / SNI / привязка клиентских сертификатов — это ответственность оператора, а не демона. Если вам нужна изоляция на основе Host при привязке не к loopback, завершайте TLS + проверяйте Host на фронтальном прокси. - CORS по умолчанию отклоняет любой Origin браузера — возвращает
403JSON. Передайте--allow-origin <pattern>(можно повторять, T2.4 #4514), чтобы пропустить определенные Origins браузеров. Каждое значение — это либо литерал*(любой origin — запуск отклоняется, если не настроен bearer-токен; для полного усиления защиты рекомендуется--require-authна loopback, так как/healthпо умолчанию остается pre-auth на loopback — обратите внимание, что статические ассеты Web Shell (/,/assets/*, навигации документов/session/:id) монтируются до bearer-аутентификации в любом режиме и остаются pre-auth даже при--require-auth, поэтому используйте--no-web, когда остаточная браузерная поверхность важна), либо канонический URL origin (<scheme>://<host>[:<port>], без завершающего слэша / пути / userinfo). Совпавшие origins получают правильные заголовки ответа CORS (Access-Control-Allow-Origin: <echoed>,Vary: Origin, а также стандартные методы / заголовки / max-age и выставленныйRetry-After); несовпавшие origins по-прежнему получают 403 с тем же конвертом, что и при стандартной блокировке.caps.features.allow_originанонсируется условно, чтобы SDK / webui клиенты могли предварительно проверить, поддерживает ли демон кросс-доменные запросы, перед их выполнением. Пример:qwen serve --allow-origin http://localhost:3000 --allow-origin http://localhost:5173. Запросы с loopback на себя (например, Web Shell UI) не затрагиваются — отдельная заглушка для удаления Origin обрабатывает их независимо от--allow-origin. Браузерные webui без настроенного--allow-originпо-прежнему возвращаются к тем же опциям Stage 1, что и раньше: упаковывайте как нативную оболочку (Electron/Tauri), чтобы заголовокOriginне отправлялся, или ставьте перед демоном обратный прокси с тем же origin. - Автоматизация браузера через расширение Chrome отделена от фрейминга.
qwen serve --allow-origin chrome-extension://<id>позволяет расширению встраивать Web Shell и подключаться к демону. Инструменты console/network/screenshot/click требуют внешней команды CDP MCP-адаптера:QWEN_CDP_MCP_COMMAND=/path/to/cdp-mcp-adapter qwen serve --allow-origin chrome-extension://<id>. Основной пакет CLI не включает адаптер автоматизации браузера; клиенты могут проверитьcaps.features.includes('browser_automation_mcp')перед отображением этих инструментов как доступных. - Порожденный дочерний процесс
qwen --acpполучает эффективное окружение своей среды выполнения. Демон замораживает базовое окружение процесса, применяет наложение settings/env-file этого рабочего пространства к локальному снимку среды выполнения и никогда не записывает наложение обратно вprocess.env; одноименные ключи в другой среде выполнения не пересекаются.QWEN_SERVER_TOKENочищается перед порождением, потому что агенту не нужен bearer демона. Переменные, влияющие на загрузчик (NODE_OPTIONS,npm_config_node_optionsи редиректы конфигурационных файлов npm,NODE_PATH,OPENSSL_CONF,NODE_REPL_EXTERNAL_MODULE,npm_config_node_gyp,npm_config_init_module,LD_PRELOAD,LD_AUDIT,DYLD_INSERT_LIBRARIES,BASH_ENV,ZDOTDIR, экспортированные определения функций bashBASH_FUNC_*), также никогда не передаются подпроцессам сессии — демон очищает их из собственногоprocess.envи из замороженного базового окружения, с которым порождаются дочерние процессы хостинга сессий (базовое окружение сохраняет их только приDEV=true, чьи.tsзаписи все еще нуждаются в загрузчике tsx), а источники.env/settings.jsonenvотклоняют их (см. settings); это применяется к каждой сессии, которую обслуживает демон. Базовые учетные данные, такие какOPENAI_API_KEY,ANTHROPIC_API_KEY,QWEN_*иDASHSCOPE_API_KEY, передаются, если только наложение среды выполнения их не изменяет. Это сделано намеренно, а не является песочницей. Агент запускается с тем же UID и имеет доступ к shell-инструментам, поэтому что угодно в~/.bashrc/~/.aws/credentials/~/.npmrcвсе равно будет доступно через инъекцию промпта. Изоляция окружения между средами выполнения не является границей безопасности ОС; не запускайтеqwen serveот имени, имеющего учетные данные, которые вы бы не доверили агенту. - Чтение текста агентом локализовано в дочернем процессе и следует обычным правилам разрешений CLI, а не границе файловой системы рабочего пространства. Прямой
read_fileможет достигать текстовых путей хоста за пределами каждого зарегистрированного рабочего пространства: внешние пути по умолчанию требуют подтверждения, а правила разрешения или режимы одобрения могут одобрить их автоматически. Одобренные чтения используют настраиваемые лимиты вывода CLI, а не лимиты возвращенного вывода, полного снимка и сканирования большого текста файловой системы рабочего пространства. Это применяется к каждому общему потребителю чтения текста, поэтому предварительные чтения, выполняемые операциями write, edit, notebook, sed и artifact, теряют эти лимиты вместе с аудитом чтения, отклонением символических ссылок и защитой от TOCTOU при чтении файловой системы рабочего пространства — см. документ по дизайну для точного списка. Поскольку полезная нагрузка подтверждения формируется путем чтения файла, diff за пределами рабочего пространства рассылается каждому подключенному SSE-подписчику до того, как кто-либо его одобрит — в интерактивном CLI этот контент видит только человек за терминалом. Относитесь к аутентифицированным клиентам демона как к тому же принципалу безопасности. HTTP-маршруты файловой системы остаются в рамках рабочего пространства, поведение инструментов обнаружения агента не изменяется. - Одобренные финальные записи от встроенных текстовых инструментов имеют узкий маршрут на том же хосте.
write_file,edit,notebook_editи симулированный sed-редактор shell-инструмента прикрепляют внутренний provenance только после того, как существующая политика разрешений допускает выполнение. Их финальная текстовая запись ACP может поэтому указывать на абсолютный путь за пределами владельца рабочего пространства без повторного подтверждения; правила разрешения, AUTO/AUTO_EDIT и YOLO ведут себя как в CLI, тогда как отклонение, Plan, отказ Hook/Guard и отмена до выполнения не отправляют финальную запись. Отмена после того, как инструмент уже вошёл в не отменяемую файловую операцию, сохраняет существующее поведение этого инструмента. Цели в рабочем пространстве по-прежнему используют WFS. Внешние цели используют хост-писатель демона с тем же снимком доверия, лимитом кодирования 5 МиБ, отклонением leaf-symlink, фиксацией канонического пути, атомарным переименованием, сохранением режима, режимом новых файлов0600по умолчанию (настраивается — см. Режим новых файлов для текстовых записей агента), стражем поколения и файловым аудитом. HTTP-записи, общие или немаркированные записи ACP, инжектированные интеграции bridge/workspace-registry/factory и произвольные перенаправления оболочки не получают это исключение. См. дизайн внешних записей. - Ограниченные SSE-очереди на подписчика — медленный клиент, переполнивший свою очередь, получает терминальный фрейм
client_evictedи отключается; один зависший потребитель не может “повесить” демон. - Лимит приема промптов на сессию — по умолчанию 5 принятых, но еще не завершенных промптов на сессию. Сбойный клиент не может поставить в очередь неограниченное количество промптов или временных ожиданий SSE для одной сессии.
- Корректное завершение работы — SIGINT/SIGTERM ожидают завершения дочерних процессов агента перед закрытием listener (10 секунд на каждый дочерний процесс).
⚠️ Известный пробел Stage 1 — разрешения глобальны для демона, а не для каждой сессии (BUy4H).
pendingPermissionsнаходится в области действия демона; любой клиент, владеющий bearer-токеном, может голосовать за любойrequestIdдля любой видимой им сессии (и SSE-событияpermission_requestсодержат requestId в своих данных). Это приемлемо в модели доверия для одного пользователя / небольшой команды, где каждый аутентифицированный клиент — это один и тот же человек или коллеги, которым он доверяет. В Stage 1.5 будет осуществлен переход наPOST /session/:id/permission/:requestId+ карту ожиданий в области действия сессии + идентификацию на клиента (must-have #3 из ревью downstream); до этого не запускайтеqwen serveс bearer-токеном, которым пользуются ненадежные стороны.⚠️ Известный пробел Stage 1 — тело
POST /session/:id/promptограничено 10 МБ (BUy4L). Мультимодальные промпты, содержащие изображения / PDF / аудио, превышающие 10 МБ, завершатся ошибкой на этапе разбора тела до запуска логики маршрута (без потоковой передачи, без прерывания в процессе загрузки). Обходной путь: уменьшите размер контента на стороне клиента или передайте ссылку на путь и позвольте агенту прочитать файл черезreadTextFile. В Stage 1.5 будет поддерживатьсяmultipart/form-dataили chunked encoding на/prompt, чтобы большие промпты не упирались в лимит.⚠️ Известный пробел Stage 1 — фантомные SSE-соединения за NAT. Демон обнаруживает мертвых клиентов через TCP back-pressure на хартбитах (интервал 15 с). Клиент, который исчезает БЕЗ TCP RST (например, NAT-коробка, тихо отбрасывающая неактивные потоки), оставляет сокет на уровне ядра “живым”, пока не истечет время ожидания keepalive-зондов Node — обычно около 2 часов при настройках Linux по умолчанию. В деплоях с
--hostname 0.0.0.0за такими NAT, фантомные SSE-соединения могут накапливаться и в конечном итоге достичь потолка в 256server.maxConnections.Установите
--writer-idle-timeout-ms <n>(issue #4514 T2.9), чтобы закрыть этот пробел явным дедлайном простоя на уровне приложения: если ни одна запись не была успешно сброшена (flushed) заnмс, демон отправляет терминальный фреймclient_evictedсreason: 'writer_idle_timeout'и закрывает поток. Флаг выключен по умолчанию для сохранения legacy-контракта — операторам в сетях, которые проглатывают RST, следует выбрать значение значительно выше 15-секундного интервала хартбитов (например,60000–300000), чтобы легитимные неактивные соединения не отключались, в то время как реально зависшие писатели быстро удалялись. Выполните pre-flightcaps.features.includes('writer_idle_timeout')из вашего SDK, чтобы убедиться, что демон это поддерживает.
Дедлайны и таймаут простоя writer
Issue #4514 T2.9 добавляет два opt-in флага, которые закрывают пробелы для долго выполняющихся / удаленных деплоев, не покрываемые 15-секундным хартбитом + AbortSignal. Общий таймаут ответа на разрешение перечислен здесь же. Все три выключены по умолчанию.
| Флаг | Переменная окружения | По умолчанию | Что делает |
|---|---|---|---|
--prompt-deadline-ms <n> | QWEN_SERVE_PROMPT_DEADLINE_MS | не задано | Серверное ограничение по настенным часам (wallclock) для одного POST /session/:id/prompt. По истечении срока демон прерывает AbortController промпта и возвращает HTTP 504 с {code:"prompt_deadline_exceeded", errorKind:"prompt_deadline_exceeded", deadlineMs:n}. Поле deadlineMs в теле запроса для каждого промпта может СОКРАТИТЬ эффективный дедлайн ниже значения флага, но никогда не продлить его. Тег возможности (условный): prompt_absolute_deadline. |
--writer-idle-timeout-ms <n> | QWEN_SERVE_WRITER_IDLE_TIMEOUT_MS | не задано | Дедлайн простоя для каждого SSE-соединения. Если ни одна запись не была УСПЕШНО сброшена (flushed) за n мс — ни реальное событие, ни 15-секундный хартбит — демон отправляет терминальный фрейм client_evicted с data.reason = 'writer_idle_timeout' (дублируется в data.errorKind) и закрывает поток. Выбирайте значение с запасом выше 15-секундного хартбита (например, 30000–300000), чтобы легитимные неактивные потоки не отключались; значения < 15000 БУДУТ отключать иначе здоровые неактивные соединения до срабатывания первого хартбита (предназначено только для тестов / коротких dev-сессий). Тег возможности (условный): writer_idle_timeout. |
--permission-response-timeout-ms <n> | — | 0 | Общий wallclock для ответов на обычные разрешения и ask_user_question в режиме демона. 0 или пропущенный флаг ожидает бесконечно; положительное целое число устанавливает дедлайн для обоих. Отмена голосования, отмена сессии и завершение работы демона по-прежнему разрешают ожидающие взаимодействия, когда таймер отключен. |
Флаги промпта и writer принимают положительное целое число в миллисекундах; 0, NaN, нецелые или отрицательные значения отклоняются при запуске с понятным сообщением об ошибке. Таймаут ответа на разрешение принимает 0 или положительное целое число. Для двух дедлайнов с поддержкой переменных окружения явные поля ServeOptions имеют приоритет над значениями переменных окружения. Потребители SDK должны выполнять pre-flight соответствующего тега возможности перед использованием поведений промпта и writer.
Режим новых файлов для текстовых записей агента
Текстовые записи агента (write_file, edit, notebook_edit и симулированный sed-редактор shell-инструмента) публикуются через атомарный писатель демона, который сохраняет режим существующего целевого файла, а для новых файлов по умолчанию устанавливает 0600 только для владельца, игнорируя umask процесса демона. Этот fail-closed стандарт является намеренным: свежий созданный агентом файл никогда не будет случайно доступен для группы/остальных, независимо от того, насколько разрешителен umask супервизора.
Операторы, чье соглашение о развертывании основано на umask (например, юнит systemd с UMask=0002, репозитории с общими группами), могут включить стандартную обработку POSIX для новых файлов с помощью:
| Переменная окружения | Значения | По умолчанию | Что делает |
|---|---|---|---|
QWEN_SERVE_NEW_FILE_MODE | owner | system | owner | system создает НОВЫЕ файлы с режимом 0o666 & ~umask, поэтому файлы, созданные агентом, следуют за umask процесса демона, как и любой другой процесс на машине. owner сохраняет независимый от umask стандарт 0600. Значения регистронезависимы; литерал 0600 принимается как псевдоним для owner (другие восьмеричные режимы не поддерживаются), а любое другое значение отклоняется с предупреждением в stderr и сохраняется стандарт 0600. |
Область действия и ограничения:
- Применяется к НОВЫМ файлам, созданным маршрутами текстовой записи (цели рабочего пространства, внешний хост-писатель на том же хосте и текстовые записи HTTP). Существующие файлы всегда сохраняют свой режим на диске — редактирование секрета
0600оставляет его0600, исполняемый файл сохраняет+x. - Бинарные загрузки (
POST /file/upload) всегда создаются с режимом0600независимо от этой настройки. - Демон читает переменную при построении файловой системы рабочего пространства; перезапустите демон после изменения.
Деплой с несколькими сессиями и воркспейсами
Передайте --workspace более одного раза для регистрации нескольких непересекающихся рабочих пространств в одном процессе qwen serve. Первый путь является основным. Каждое зарегистрированное рабочее пространство владеет изолированной границей среды выполнения, а общедемонный слушатель, политика аутентификации и лимит общих сессий являются общими. Продакшен пытается предварительно прогреть основной дочерний процесс ACP для совместимости и повторяет попытку при первом использовании после сбоя; доверенные вторичные запускают свой дочерний процесс по требованию, а недоверенные вторичные не запускают ACP. Запросы могут выбирать зарегистрированное рабочее пространство по каноническому cwd; запросы, опускающие cwd, используют основное рабочее пространство. Используйте один демон на пользователя или принцип безопасности; доверие рабочему пространству — это гейт выполнения, а не ACL.
Недоверенное вторичное рабочее пространство видно в Web Shell как untrusted и read-only. Его можно развернуть для просмотра сохраненного каталога сессий, но его пока нельзя выбрать или открыть в Web Shell, возобновить, использовать для создания сессий или полностью экспортировать. REST API следует существующей политике ограниченного чтения файловой системы и также предоставляет свой сохраненный каталог групп сессий и, когда рекламируется workspace_persisted_transcript, свой активный сохраненный транскрипт через ограниченный постраничный пейджер с квалификацией рабочего пространства. Эти чтения не включают живое состояние среды выполнения и не запускают дочерний процесс ACP. Полный экспорт с квалификацией рабочего пространства требует доверенного рабочего пространства и отдельной возможности workspace_session_export. Доверьте рабочее пространство и перезапустите демон перед использованием функций выполнения, мутации или экспорта. Недоверенное основное рабочее пространство остается отключенным в Web Shell.
Используйте отдельные процессы демона, когда вам нужна меньшая граница сбоя или безопасности, независимые bearer-токены, квоты, границы аудита, изоляция операционной системы или независимый контроль ресурсов. Режим нескольких рабочих пространств предназначен для одного оператора, размещающего несколько репозиториев; это не граница изоляции мультитенантности. Один токен демона авторизует каждый маршрут, который открывает демон, включая разрешенный каталог только для чтения для всех зарегистрированных рабочих пространств.
Подписывайтесь ДО отправки
modelServiceIdпри подключении. Если клиент делаетPOST /sessionсmodelServiceId, а в рабочем пространстве уже есть сессия, работающая с другой моделью, демон выполняет внутренний вызовsetSessionModel— сбои НЕ передаются как HTTP-ошибка (сессия остается работоспособной на своей текущей модели). Видимый сигнал сбоя — это событиеmodel_switch_failedв SSE-потоке сессии. Если вы вызоветеPOST /sessionи ТОЛЬКО ПОТОМ откроетеGET /session/:id/events, вы пропустите событие сбоя и будете тихо продолжать общаться с неправильной моделью. Сначала откройте SSE-поток или передайтеLast-Event-ID: 0при подписке, чтобы воспроизвести самое старое доступное событие из кольца.
Для обработки нескольких пользователей или принципов безопасности (каждый с независимым токеном, квотой, журналом аудита, песочницей или границей сбоя процесса) или для масштабирования за пределы возможностей одного процесса (бюджет холодного старта, количество FD, RSS) запускайте по одному демону на принцип безопасности за внешним оркестратором. Каждый такой демон может по-прежнему размещать несколько рабочих пространств для этого принципа безопасности. Оркестратор (мультитенантность / OIDC / квоты / аудит / k8s) выходит за рамки проекта qwen-code — см. issue #3803 “External Reference Architecture” для указателей по архитектуре.
Загрузка и возобновление сохраненной сессии
Демон предоставляет поток session/load и возобновления ACP по HTTP, а также отдельный постраничный пейджер транскриптов только для чтения:
| Маршрут | Когда использовать |
|---|---|
POST /session/:id/load | У клиента нет полезной локальной отрендеренной истории (холодное переподключение, выбор и затем открытие). Для живой сессии демон возвращает и внедряет текущее ограниченное окно снимка воспроизведения; если старое воспроизведение было отброшено, снимок начинается с history_truncated. Тег возможности: session_load. |
POST /session/:id/resume | У клиента уже есть ходы на экране, и ему нужен только дескриптор на стороне демона. Контекст модели восстанавливается на стороне агента без воспроизведения UI — SSE-поток остается чистым. Тег возможности: session_resume (unstable_session_resume остается устаревшим псевдонимом для старых клиентов). |
GET /session/:id/transcript | Клиенту нужен полный активный сохраненный транскрипт. Он возвращает безыдентные фреймы воспроизведения в постраничных страницах с курсором и не вызывает /load, не подключает клиент, не засеивает живой EventBus, не создает живую сессию и не изменяет живое окно воспроизведения. Тег возможности: session_transcript. |
GET /workspaces/:workspace/session/:id/transcript | Клиенту нужен активный сохраненный транскрипт из выбранного рабочего пространства без запуска ACP или загрузки настроек рабочего пространства. Зарегистрированные недоверенные вторичные рабочие пространства могут использовать этот путь только для чтения. Тег возможности: workspace_persisted_transcript. |
GET /workspaces/:workspace/session/:id/export | Клиенту нужно полное вложение html, md, json или jsonl из выбранного доверенного рабочего пространства. Оно читает активное сохраненное хранилище без запуска ACP или возврата к основному. Тег возможности: workspace_session_export. |
GET /workspaces/:workspace/session/:id/archive/export | Клиенту нужны те же форматы вложений из архивированного сохраненного хранилища в выбранном доверенном рабочем пространстве. Оно не разархивирует, не запускает ACP и не возвращается к активной или основной сессии. Тег возможности: workspace_archived_session_export. |
Для load и resume TypeScript SDK предоставляет статические фабрики в
DaemonSessionClient:
import { DaemonClient, DaemonSessionClient } from '@qwen-code/sdk';
const client = new DaemonClient({ baseUrl: 'http://127.0.0.1:4170' });
// Холодное переподключение — демон воспроизведет историю через SSE.
const session = await DaemonSessionClient.load(client, 'persisted-id');
// Или, если в вашем UI уже есть история, пропустите воспроизведение:
// const session = await DaemonSessionClient.resume(client, 'persisted-id');
for await (const event of session.events()) {
// Сначала воспроизведенные фреймы `session_update` (только для load),
// затем live-события.
}Выполняйте pre-flight caps.features.session_load, caps.features.session_resume или caps.features.session_transcript перед вызовом соответствующего маршрута — старые демоны возвращают 404. unstable_session_resume по-прежнему анонсируется как устаревший псевдоним для совместимости. Параллельные запросы с одинаковым действием для одного и того же id объединяются; при гонках с разными действиями (load конкурирует с resume) возвращается 409 restore_in_progress с Retry-After: 5. Восстановление, превышающее limits.sessionRestoreTimeoutMs, возвращает повторяемый 504 session_restore_timeout с выведенным из бюджета Retry-After (ограниченным 5–120 с); всё ещё выполняющийся дочерний запрос остаётся ограждённым до завершения очистки, а повторные запросы с тем же id в этом окне получают 409 restore_in_progress с reason: awaiting_abandoned_cleanup и выведенным из бюджета Retry-After, ограниченным 5–120 секундами, вместо фиксированной задержки 5 секунд. Если очистка неопределённа или заброшенное восстановление всё ещё не завершило полный бюджет восстановления после своего дедлайна, новая работа сессии временно получает 503 acp_channel_unavailable с reason: restore_cleanup_failed или restore_settlement_overdue, тогда как уже активные сессии остаются пригодными. См. справочник по протоколу для полного конверта ошибки.
Для полного сохраненного воспроизведения используйте постраничный просмотр с DaemonClient.getSessionTranscriptPage(sessionId, { cursor, limit }) или сырой REST-маршрут:
curl "http://127.0.0.1:4170/session/$SESSION_ID/transcript?limit=100"Для зарегистрированного рабочего пространства используйте client.workspaceById(workspaceId).getSessionTranscriptPage(sessionId, { cursor, limit }) или /workspaces/:workspace/session/:id/transcript. Метод с квалификацией рабочего пространства всегда использует нативный REST, даже если SDK-клиент имеет заменяемый транспорт ACP. Его курсоры действуют только в течение жизни демона и должны перезапускаться с первой страницы после перезапуска демона.
Для полного вложения из доверенного зарегистрированного рабочего пространства выполните pre-flight workspace_session_export и вызовите client.workspaceById(workspaceId).exportSession(sessionId, { format: 'html' }) или сырой маршрут /workspaces/:workspace/session/:id/export. Не выводите поддержку из session_export или workspace_qualified_rest_core: старые демоны могут рекламировать оба, сохраняя экспорт только для основного рабочего пространства. Текущее действие экспорта Web Shell остается только для основного рабочего пространства; используйте SDK или REST-маршрут для другого рабочего пространства.
Для архивированного вложения выполните pre-flight workspace_archived_session_export и вызовите client.workspaceById(workspaceId).exportArchivedSession(sessionId, { format: 'html' }) или /workspaces/:workspace/session/:id/archive/export. Этот путь читает архивированное хранилище на месте и возвращает 409 session_not_archived для id только активной сессии; он не разархивирует сессию. Web Shell предоставляет тот же экспорт для архивированных строк в доверенных основных и вторичных рабочих пространствах, когда возможность присутствует.
limit считает активные записи чата, а не отправленные фреймы воспроизведения; одна запись может создать несколько событий session_update. Первый ответ фиксирует размер снимка JSONL и возвращает nextCursor, пока hasMore истинно. Последующие страницы игнорируют добавления после страницы 1, но возвращают 409, если файл удален, усечен, заменен, архивирован или иным образом конфликтует с фиксированным курсором. Очень большие снимки возвращают 413 transcript_too_large перед индексацией, чтобы демон не сканировал неограниченные файлы транскриптов на пути запроса.
Для повторного постраничного просмотра через устаревший единичный маршрут установите --channel-idle-timeout-ms в положительное значение. При стандартном 0 неактивный дочерний процесс ACP рабочего пространства — и кэш индекса транскриптов в процессе, который он хранит — утилизируется после каждой страницы, поэтому каждая страница повторно порождает дочерний процесс и перестраивает индекс повторным сканированием всего фиксированного префикса (O(snapshotSize) на страницу). Положительный таймаут сохраняет дочерний процесс активным при обходе курсора, чтобы он повторно использовал свой кэшированный индекс транскриптов и конфигурацию воспроизведения. Сохраненный маршрут с квалификацией рабочего пространства никогда не запускает дочерний процесс ACP и не зависит от этого таймаута.
Примечание: воспроизведение истории живой сессии ограничено дважды: SSE-кольцом для переподключений через Last-Event-ID и --compacted-replay-max-bytes для снимка, возвращаемого POST /session/:id/load. Длинные истории с многословными ходами могут превысить любую границу. Демон показывает усечение снимка через history_truncated; используйте /transcript, когда вам нужна полная активная сохраненная история.
Модель долговечности
Сессии по-прежнему эфемерны в Stage 1 при перезапусках демона, но сохраненные на диск сессии можно перезагрузить:
- При сбое дочернего процесса публикуется
session_died, и live-сессия удаляется из карт демона. Сохраненную на диске сессию можно перезагрузить черезPOST /session/:id/load, если можно породить новый дочерний процесс агента. - При перезапуске демона теряется каждая live-сессия, находящаяся в процессе выполнения. Сохраненные сессии остаются на диске и могут быть загружены для нового процесса демона с учетом тех же правил привязки к рабочему пространству.
- Длительные отключения клиента (>5 мин при многословном ходе) могут обогнать кольцо воспроизведения SSE (по умолчанию 8000 фреймов) — переподключение через
Last-Event-IDвызываетstate_resync_required. Для мобильных клиентов / клиентов с нестабильной сетью планируйте повторное открытие SSE при длительных обрывах или вызывайтеPOST /session/:id/loadдля восстановления текущего ограниченного снимка воспроизведения; не предполагайте, что этот маршрут возвращает полный транскрипт. - Файловые операции (
writeTextFile) атомарны при сбоях (запись, затем переименование); они не атомарны при перезапусках демона в смысле воспроизведения — запись файла либо произошла, либо нет.
Если вашей интеграции требуется долговечность на стороне сервера при перезапусках, выходящая за рамки того, что покрывает session/load (например, очереди повторных попыток, управляемые сервером), вам по-прежнему необходимо восстановление состояния на уровне приложения. Не храните долго выполняющееся, чувствительное к перезапуску состояние внутри сессии демона.
Гарантии времени выполнения в Stage 1.5+
Контракт Stage 1 рассчитан на прототипирование. Согласно #3889 chiga0 downstream-consumer review , следующее не входит в Stage 1 — интеграциям производственного уровня требуется Stage 1.5+ перед тем, как полагаться на них: Блокеры для полноценного использования в downstream-проектах:
loadSession/unstable_resumeSessionover HTTP — без этого ни одна интеграция не переживет падение дочернего процесса или перезапуск демона, и ни один оркестратор, координирующий демон, также не сможет восстановить состояние.- Постоянная идентификация клиента (парные токены + отзыв для каждого клиента) — на этапе 1 (Stage 1) используется один общий bearer-токен; утечка токена отзывает доступ у всех, а
originatorClientIdзадается самим клиентом, а не проставляется демоном на основе аутентифицированной идентичности.
Базовая надежность:
Heartbeat-путь, инициируемый клиентом— реализовано в #4175 PR 9.POST /session/:id/heartbeatзаписывает метки времени последнего обращения (last-seen timestamps) в демоне (тег возможностиclient_heartbeat); хелперы SDK:DaemonClient.heartbeat()/DaemonSessionClient.heartbeat().- Событие
permission_already_resolved, когда голосование проигрывает гонку первого ответа — в настоящее время UI вынуждены определять состояние по коду404. Увеличенное кольцо повтора (replay ring)— увеличено до 8000. Настраиваемое для каждой сессии кольцо остается открытым вопросом — для мобильных / многословных рабочих нагрузок могут потребоваться переопределения для каждой сессии.- Событие
slow_client_warningпередclient_evicted— мягкое обратное давление (soft backpressure), чтобы корректно работающие медленные клиенты могли самостоятельно ограничивать скорость (уменьшать глубину рендеринга, отбрасывать чанки) перед тем, как будут отключены.
Эргономика интеграции:
POST /session/:id/_metaдля контекста в стиле IM — пары ключ-значение для каждой сессии, прикрепляемые к последующим промптам (id чата, отправитель, id треда), заменяют импровизацию для каждого канала.- Реальные переговоры о возможностях через
/capabilities—protocol_versions: { acp: '0.14.x', daemon_envelope: 1 }, чтобы клиенты могли обнаруживать расхождения (drift) вместо того, чтобы скатываться к “неизвестный фрейм, игнорировать”. - Первоклассная документация по долговечности (durability) (этот раздел) — уже реализована выше.
Полный план сближения (convergence roadmap) отслеживается в #3803 .
Границы области Stage 1 — что мы не будем исправлять в Stage 1.5
Два структурных решения являются явными целями-исключениями (non-goals) для основного плана Stage 1 / 1.5 / 2. Если ваш сценарий использования зависит от любого из них, планируйте работу в обход них, а не ждите наших изменений.
Состояние сессии изменяется только локально (согласно LaZzyMan review #4270256721 )
План Stage 1.5 описывает TUI как подписчика на EventBus в рамках процесса. На практике UI TUI строго шире, чем wire-протокол:
- Только локальный UI — около 15 диалоговых компонентов Ink (
ModelDialog,MemoryDialog,PermissionsDialog,SessionPicker,WelcomeBackDialog,FolderTrustDialog, …) и slash-командыlocal-jsx(/ide,/auth,/init,/resume,/rename,/delete,/language,/arena, …) рендерят специфичный для терминала Ink JSX. Удаленные клиенты по HTTP/SSE не могут эквивалентно рендерить Ink, и эти потоки не генерируют wire-событий. - Изменения состояния сессии без wire-событий —
/approval-mode,/memory add,/mcp add-server,/agents,/tools enable/disable,/auth,/init(записьCLAUDE.md) — все они меняют поведение агента, но только/modelв настоящее время публикует событие (model_switched).
Выбор для Stage 1 — вариант (A) из ревью: не повышать эти мутации до wire-событий. Два режима развертывания имеют разные последствия.
Режим 1 — headless qwen serve (этот PR)
Внутри демона не запускается оболочка TUI. Перечисленные выше slash-команды в этом режиме не существуют — нет терминального UI для их вызова. Следовательно, состояние сессии:
- Замораживается при загрузке для
approval-mode/memory/agents/toolsallowlist /auth— все загружается из настроек и с диска при запуске дочернего процессаqwen --acpдемона; неизменно в течение всего времени жизни сессии. Определенные в настройках MCP-серверы также замораживаются при загрузке, но серверы, добавленные во время выполнения (черезPOST /workspace/mcp/servers), могут быть добавлены или удалены без перезапуска. - Изменяется по HTTP через
POST /session/:id/model(публикуетmodel_switched),POST /workspace/mcp/servers/DELETE /workspace/mcp/servers/:name(публикуетmcp_server_added/mcp_server_removed) и голоса разрешений (POST /permission/:requestId).
Последствие: удаленные клиенты в headless-режиме видят полное состояние сессии. Никакой TUI не скрывает дополнительное состояние; расхождений (drift) быть не может. Если вы хотите изменить approval-mode, перезапустите демон с новыми настройками. MCP-серверы теперь можно добавлять/удалять во время выполнения через маршруты мутации (POST /workspace/mcp/servers, DELETE /workspace/mcp/servers/:name) — см. Управление MCP-серверами во время выполнения.
Режим 2 — совместно размещенный TUI qwen --serve в Stage 1.5 (не в этом PR)
Когда в Stage 1.5 появится qwen --serve (процесс TUI совместно размещает тот же HTTP-сервер), TUI будет существовать наряду с удаленными клиентами. Локальный оператор, вводящий /approval-mode yolo или /mcp add-server, изменяет состояние сессии, и удаленные клиенты по HTTP не получают события для наблюдения за этим изменением.
В этом режиме TUI является “супер-клиентом” — он наблюдает за тем же разговором агента, что и удаленные клиенты, И может изменять состояние сессии, чего удаленные клиенты не могут. Асимметрия заключается в следующем:
- ✅ И TUI, и удаленные клиенты видят одни и те же сообщения агента, вызовы инструментов, диффы файлов, запросы разрешений.
- ❌ Только TUI видит / изменяет approval-mode / memory / список MCP-серверов / агентов / tools allowlist / состояние аутентификации.
Последствие в Режиме 2: если UI удаленного клиента пытается зеркалить настройки сессии, он может рассинхронизироваться (drift) после любой slash-команды TUI. Удаленные клиенты должны повторно загружать состояние при подключении / переподключении (используйте Last-Event-ID: 0, чтобы повторить самое старое событие в кольце для таких вещей, как model_switched); им НЕ следует полагаться на инкрементальные события для мутаций на стороне TUI.
Почему (A), а не (B) (повышение мутаций до семейства событий session_state_changed)
(B) — более амбициозный ответ, но он запирает Stage 1.5 на существенно большей wire-поверхности, которая также должна чисто пройти через запланированный in-process рефакторинг. Мы предпочли бы честно идти в рамках меньшей области. Работа по таксономии событий состояния сессии — перечисление того, какие потоки TUI являются локальными по дизайну, а какие могут правдоподобно перейти на wire-уровень в будущем расширении с явным включением (B)-варианта — переносится в #3803 , а не в код Stage 1.5.
N параллельных сессий используют один дочерний процесс qwen --acp на среду выполнения рабочего пространства
Несколько сессий в одном доверенном рабочем пространстве используют дочерний процесс qwen --acp этой среды выполнения благодаря нативной поддержке мульти-сессий агентом (packages/cli/src/acp-integration/acpAgent.ts:194: private sessions: Map<string, Session>). Мост вызывает connection.newSession({cwd, mcpServers}) для каждой сессии — агент сохраняет их в своей карте сессий и демультиплексирует sessionId для каждого вызова. Продакшен может владеть до одного основного дочернего процесса (предварительный прогрев выполняется по умолчанию) плюс один дочерний процесс по требованию для каждой доверенной вторичной; недоверенные вторичные не владеют ни одним.
Конкретные затраты при N=5 сессиях в одном рабочем пространстве:
| Ресурс | На сессию | При N=5 |
|---|---|---|
| Node-процесс демона | один | 30–50 МБ (один демон) |
Дочерний процесс qwen --acp | общий | 60–100 МБ (один дочерний процесс) |
| Дочерние процессы MCP-серверов | пул рабочего пространства, если рекламируется; иначе на сессию | совместно используемые совпадающими записями пула, или до 3×N в устаревшем фолбэке |
FileReadCache (в куче дочернего процесса) | общий | парсится один раз |
Парсинг памяти CLAUDE.md / иерархии | общий | парсится один раз |
| Состояние OAuth refresh-токена | общий | один путь обновления |
| Изученные факты Auto-memory | общие | одна база знаний на дочерний процесс |
| Холодный старт | только первый | <200 мс после первой сессии |
Каждая активная среда выполнения рабочего пространства сохраняет одну границу моста. Продакшен пытается предварительно прогреть основной канал и повторяет попытку при первом использовании после сбоя; доверенная вторичная открывает свой канал и дочерний процесс по требованию, а недоверенная вторичная никогда этого не делает. Канал остается активным, пока жива хотя бы одна сессия. После последнего killSession среда выполнения убивает свой дочерний процесс немедленно по умолчанию или после настроенной задержки простоя канала; крах на уровне канала также разбирает его без выбора другой среды выполнения.
Дочерние процессы MCP-серверов используют пул транспортов рабочего пространства, когда рекламируется mcp_workspace_pool: совпадающие записи (среда выполнения рабочего пространства, имя сервера, отпечаток конфигурации) подсчитываются по ссылкам между сессиями. Если возможность отсутствует, устаревший менеджер на сессию независимо порождает их.
Аналогичные агенты (Cursor / Continue / Claude Code / OpenCode / Gemini CLI) все используют мульти-сессии в одном процессе. qwen-code соответствует им на уровне агента; мост Stage 1 в этом PR делает ту же архитектуру видимой по HTTP.
Вход в удаленный демон (issue #4175 PR 21)
Когда демон работает на удаленном поде (без общего с вами экрана), клиент может запустить OAuth device flow по HTTP. Демон сам опрашивает IdP; ваша задача — просто открыть URL на любом устройстве с браузером.
Бесплатный уровень Qwen OAuth был отменен 15.04.2026. Приведенные ниже примеры qwen-oauth документируют форму протокола device-flow и устаревший идентификатор провайдера; для новых установок следует использовать текущий поддерживаемый провайдер аутентификации.
# 1. Запускаем flow. Демон обращается к IdP, возвращает код + URL.
curl -X POST http://127.0.0.1:4170/workspace/auth/device-flow \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"providerId":"qwen-oauth"}'
# → 201 {
# "deviceFlowId": "fa07c61b-…",
# "userCode": "USER-1",
# "verificationUri": "https://chat.qwen.ai/api/v1/oauth2/device",
# "verificationUriComplete": "https://chat.qwen.ai/...?user_code=USER-1",
# "expiresAt": 1700000600000,
# "intervalMs": 5000,
# "attached": false
# }
# 2. Откройте URL на телефоне / ноутбуке, введите пользовательский код.
# 3. Опрашивайте для завершения (или подпишитесь на SSE для события auth_device_flow_authorized):
curl http://127.0.0.1:4170/workspace/auth/device-flow/fa07c61b-… \
-H "Authorization: Bearer $TOKEN"
# → переходы статуса: pending → authorizedTypeScript SDK оборачивает оба шага в один хелпер:
import { DaemonClient } from '@qwen-code/sdk';
const client = new DaemonClient({ baseUrl, token });
const flow = await client.auth.start({ providerId: 'qwen-oauth' });
console.log(`Откройте ${flow.verificationUri}\nКод: ${flow.userCode}`);
const result = await flow.awaitCompletion({ signal: abortCtrl.signal });
// result.status === 'authorized'Демон никогда не открывает браузер от вашего имени. Даже при локальном запуске демон остается пассивным — он возвращает URL и позволяет SDK / пользователю выбрать, где его открыть. Это сделано намеренно: демон на headless-поде, вызвавший xdg-open, молча завершится с ошибкой, скрывая фактическую поверхность аутентификации. Скопируйте UX gh auth login с “Press Enter to open browser” в своем клиенте.
--require-auth и удобство разработки. Маршруты device-flow используют строгий шлюз мутации (PR 15), что означает, что loopback по умолчанию без токена возвращает 401 token_required. Локально самый простой способ обойти это во время разработки — qwen serve --token=dev-token; --require-auth не нужен, если вы не ужесточаете loopback по умолчанию.
Ограничение между демонами. oauth_creds.json является общим для демонов (~/.qwen/oauth_creds.json), поэтому успешный вход в демоне A автоматически подхватывается при следующем обновлении токена демоном B — но SDK-клиенты демона B не получат событие auth_device_flow_authorized (события привязаны к демону).
Перехват между клиентами. Два SDK-клиента на одном демоне, оба вызывающие POST /workspace/auth/device-flow для одного и того же провайдера, получают синглтон для провайдера: первый вызов запускает новый запрос к IdP и возвращает attached: false; второй вызов возвращает СУЩЕСТВУЮЩУЮ выполняющуюся запись с attached: true. Перехват записывается в журнал аудита (под X-Qwen-Client-Id второго клиента), но НЕ генерирует отдельное событие — оба клиента в конечном итоге наблюдают ОДНО И ТО ЖЕ auth_device_flow_authorized, как только пользователь завершит работу на странице IdP. Если ваш UI различает “я начал это” и “flow кого-то еще, к которому я присоединился”, используйте ветвление по полю attached, возвращаемому start().
Файл лога демона
qwen serve добавляет диагностические записи при обычных перезапусках по стабильному
активному пути:
${QWEN_RUNTIME_DIR or ~/.qwen}/debug/daemon/daemon.logКаждая файловая запись включает случайный runId для каждого запуска и PID демона. Успешный
стабильный владелец также обновляет debug/daemon/latest на daemon.log на
платформах, поддерживающих символические ссылки. На macOS/Linux следите за ротацией:
tail -F ~/.qwen/debug/daemon/daemon.logНа других платформах настройте просмотрщик на повторное открытие пути после его замены. Просмотрщик, сохраняющий только старый файловый дескриптор, останется на архиве после ротации.
Лог фиксирует сообщения жизненного цикла, ошибки маршрутов (с контекстом route= и sessionId=), stderr дочернего процесса ACP и — при установленном QWEN_SERVE_DEBUG=1 — дополнительные хлебные крошки моста. Строки, которые сегодня идут в stderr, по-прежнему идут в stderr; файловый лог является дополнительным, а не заменой.
Активный файл ротируется до превышения 10 МиБ. Каждое семейство сохраняет
четыре архива в archive/, и каждая файловая запись ограничена 256 КиБ. Очередь
в памяти принимает не более 4 МиБ неурегулированной файловой нагрузки. Давление очереди,
сбои ротации или сбои файловой системы могут поэтому приводить к потере файловых копий;
GET /daemon/status?detail=full показывает здоровье логгера, проблемы и счетчики отброшенных
записей/байтов.
Только один демон может владеть стабильным семейством в пространстве имен логов. Параллельный
демон пишет в debug/daemon/runs/run-<runId>/daemon.log; баннер запуска
и полный статус содержат авторитетный путь. runs/recent-fallback — это
лучшее усилие для locating недавнего семейства fallback и может указывать на то, которое
все еще активно. Здоровое пространство имен сходится примерно к 100 МиБ: около 50 МиБ для
стабильного плюс одно неактивное семейство fallback. Активные или еще не устаревшие семейства fallback
сохраняются, поэтому параллельные демоны или штормы сбоев/перезапусков могут
временно использовать больше.
Один каталог среды выполнения — это одно пространство имен владения и хранения. Используйте отдельные
значения QWEN_RUNTIME_DIR, когда демонам нужна независимая история. Новые каталоги логов демона
являются приватными для пользователя (0700), и новые файлы используют 0600 на POSIX. Возрастное истечение отсутствует.
Отключение
Установите QWEN_DAEMON_LOG_FILE=0 (или false/off/no), чтобы полностью пропустить логирование в файл. Вывод в stderr не затрагивается.
Связь с дебаг-логами сессий
Дебаг-логи уровня сессии (~/.qwen/debug/<sessionId>.txt и символическая ссылка ~/.qwen/debug/latest) независимы. Лог демона находится в соседнем подкаталоге daemon/; семантика дебаг-логов для каждой сессии не изменяется этой функцией.
Внешняя ротация
Не направляйте внешнее правило logrotate на активный daemon.log. Демон
является единственным поддерживаемым писателем и ротатором; внешнее переименование, удаление или
усечение инвалидирует его модель размера. Копирование или отправка записей без
изменения семейства безопасны. Старые файлы serve-<pid>.log и
serve-<pid>-<workspaceHash>.log остаются нетронутыми и не учитываются
новой политикой хранения.
Управление MCP-серверами во время выполнения (issue #4514 )
Добавляйте или удаляйте MCP-серверы во время выполнения без перезапуска демона. Записи времени выполнения находятся во временном оверлее, который затеняет (shadows) серверы с тем же именем, определенные в настройках; базовая конфигурация settings.json / mcpServers никогда не записывается.
Предварительная проверка: проверьте caps.features на наличие mcp_server_runtime_mutation перед вызовом любого из маршрутов. Более старые демоны без этого тега возвращают 404.
POST /workspace/mcp/servers — добавление MCP-сервера во время выполнения
Строгий шлюз (требуется bearer-токен). Немедленно подключает сервер через активный McpClientManager и обнаруживает его инструменты.
Запрос:
{
"name": "my-server",
"config": {
"command": "npx",
"args": ["-y", "@my-org/mcp-server"]
}
}name должен быть буквенно-цифровым, плюс _ и - (макс. 256 символов). config — это тот же объект конфигурации MCP-сервера, который используется в записях mcpServers файла settings.json (зависящие от транспорта поля: command/args для stdio, url для SSE/HTTP). Чувствительные к безопасности поля (trust, env, cwd, oauth, headers, authProviderType, includeTools, excludeTools, type) удаляются демоном и игнорируются.
Ответ (200) — успех:
{
"name": "my-server",
"transport": "stdio",
"replaced": false,
"shadowedSettings": false,
"toolCount": 3,
"originatorClientId": "client-1"
}replaced: true— запись времени выполнения с тем же именем уже существовала, и отпечаток конфигурации отличается; старое соединение разорвано, установлено новое. Если отпечаток совпадает (идемпотентное повторное добавление),replacedравноfalse.shadowedSettings: true— существует сервер с тем же именем, определенный в настройках; запись времени выполнения теперь затеняет его. Запись в настройках не затрагивается и снова вступит в силу, если запись времени выполнения будет позже удалена.toolCount— количество инструментов, обнаруженных на только что подключенном сервере.
Ответ (200) — мягкий отказ (режим предупреждения о бюджете):
{
"name": "my-server",
"skipped": true,
"reason": "budget_warning_only"
}Возвращается, когда установлен --mcp-budget-mode=warn и добавление сервера превысит настроенный --mcp-client-budget. Сервер НЕ подключается. Вызывающая сторона должна сообщить пользователю о нехватке бюджета.
Ошибки:
| Статус | Код | Когда |
|---|---|---|
400 | invalid_server_name | Имя пустое, превышает 256 символов или содержит символы вне [A-Za-z0-9_-] |
400 | missing_required_field | config отсутствует или не является объектом non-null |
400 | invalid_client_id | Заголовок X-Qwen-Client-Id присутствует, но не зарегистрирован для этого рабочего пространства |
400 | invalid_config | Форма конфигурации отклонена валидатором транспорта MCP |
401 | token_required | Bearer-токен не настроен (строгий шлюз) |
409 | mcp_budget_would_exceed | Установлен --mcp-budget-mode=enforce и бюджет исчерпан |
502 | mcp_server_spawn_failed | Процесс сервера завершился или истек тайм-аут при подключении; тело содержит serverName, exitCode, stderr |
503 | acp_channel_unavailable | Нет активного дочернего процесса ACP (ни одна сессия еще не была создана) |
DELETE /workspace/mcp/servers/:name — удаление MCP-сервера времени выполнения
Строгий шлюз. Отключает сервер и удаляет его из оверлея времени выполнения. Идмпотентно — удаление имени, которое никогда не добавлялось, возвращает ответ с пропуском (не ошибку).
Параметр пути :name — это URL-кодированное имя сервера.
Ответ (200) — успех:
{
"name": "my-server",
"removed": true,
"wasShadowingSettings": false,
"originatorClientId": "client-1"
}wasShadowingSettings: true— удаленная запись времени выполнения затеняла сервер с тем же именем, определенный в настройках. Эта запись в настройках теперь не затеняется и будет использоваться при следующем обнаружении/перезапуске.
Ответ (200) — идемпотентный пропуск:
{
"name": "ghost",
"skipped": true,
"reason": "not_present"
}Возвращается, если имени не было в оверлее времени выполнения (оно может по-прежнему существовать в настройках — записи настроек не могут быть удалены через этот маршрут).
Ошибки:
| Статус | Код | Когда |
|---|---|---|
400 | invalid_server_name | Имя пустое, превышает 256 символов или содержит символы вне [A-Za-z0-9_-] |
400 | invalid_client_id | Заголовок X-Qwen-Client-Id присутствует, но не зарегистрирован для этого рабочего пространства |
401 | token_required | Bearer-токен не настроен (строгий шлюз) |
503 | acp_channel_unavailable | Нет активного дочернего процесса ACP |
Семантика затенения
Записи времени выполнения образуют временный оверлей поверх MCP-серверов, определенных в настройках:
- Добавление сервера времени выполнения с тем же именем, что и запись в настройках, затеняет её — конфигурация времени выполнения имеет приоритет. Исходная запись в настройках не изменяется.
- Удаление сервера времени выполнения, который затенял запись в настройках, снимает затенение — конфигурация, определенная в настройках, снова становится активной при следующем подключении.
- Перезапуск демона теряет все записи времени выполнения. Только серверы, определенные в настройках, переживают перезапуски. Серверы времени выполнения ограничены временем жизни сессии.
GET /workspace/mcpсообщает объединенное представление — как серверы, определенные в настройках, так и серверы времени выполнения, появляются в массивеservers[]. Сегодня на уровне wire нет различий между этими двумя источниками в снимке.
События
Оба маршрута генерируют SSE-события уровня рабочего пространства (их получают все активные шины сессий):
| Событие | Генерируется, когда | Поля payload |
|---|---|---|
mcp_server_added | POST успешен (не пропущен) | name, transport, replaced, shadowedSettings, toolCount, originatorClientId |
mcp_server_removed | DELETE успешен (не пропущен) | name, wasShadowingSettings, originatorClientId |
Пропущенные ответы (budget_warning_only, not_present) НЕ генерируют события. |
События, связанные с бюджетом, из существующей поверхности mcp_guardrail_events (mcp_budget_warning, mcp_child_refused_batch) также срабатывают, когда добавления во время выполнения превышают порог бюджета.
Что дальше
- Настраиваете долгоживущий демон? Локальные шаблоны запуска (systemd / launchd / nohup / tmux) для v0.16-alpha (только локально).
- Создаете клиент? См. краткое руководство по DaemonClient для TypeScript и справочник по протоколу HTTP.
- Читаете исходный код? Код моста находится в
packages/cli/src/serve/; SDK-клиент — вpackages/sdk-typescript/src/daemon/. - Отслеживаете дорожную карту? Прогресс на этапах Stage 1.5 / Stage 2 отслеживается в issue #3803 .