Режим демона (qwen serve)
Запустите Qwen Code как HTTP-демон, чтобы несколько клиентов (плагины для IDE, веб-интерфейсы, скрипты CI, пользовательские CLI) могли совместно использовать сессии агента через HTTP + Server-Sent Events вместо запуска собственного подпроцесса.
Статус: Экспериментальный. Демон поддерживает локальные и удаленные развертывания Web Shell/API. Перед выходом за пределы loopback ознакомьтесь с Аутентификацией и моделью угроз развертывания.
Что это дает
-
Встроенный веб-интерфейс 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-less loopback-слушатель по умолчанию может использовать все их; строгие мутации рабочего пространства дополнительно требуют учетных данных оператора в других режимах развертывания. -
Краткое содержание сессии (#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-конфигурации без токена сбойный или вредоносный локальный клиент может спамить этот маршрут, чтобы сжечь токены. Настройте
Примечания по развертыванию и безопасности
- Loopback доверяется по умолчанию —
127.0.0.1:4170запускается без bearer-аутентификации, и каждый локальный процесс, имеющий к нему доступ, получает полные права API, включая выполнение кода от имени пользователя демона. На общих или недоверенных хостах используйте стабильныйQWEN_SERVER_TOKENвместе с--require-auth. - Не-loopback слушатели всегда используют bearer-токен — предоставьте свой токен или используйте эфемерный токен, выводимый при запуске. Генерируемые токены меняются при перезапуске, поэтому повторно используемым клиентам требуется стабильный токен. HTTP не шифруется; настройте TLS перед передачей данных по недоверенным сетям.
- Фоновый запуск имеет два пути — используйте
qwen daemon startдля встроенного управления локальным жизненным циклом или шаблоны systemd / launchd / tmux / nohup, если вам нужен надзор со стороны менеджера служб или запуск после перезагрузки. - Сохранение токена включается явно —
qwen daemon start --save-tokenсохраняет явно указанный bearer-токен в боковом файле, доступном только владельцу, дляqwen daemon token; обычныеqwen serveи генерируемые токены этот файл не используют. См. Инспекция и управление локальным демоном. - Многопользовательская работа с несколькими рабочими пространствами рассчитана на одного оператора — зарегистрированные рабочие пространства разделяют bearer-токен демона, слушатель, квоты и границы отказоустойчивости процессов. Для независимых пользователей или субъектов безопасности используйте отдельные демоны и внешний оркестратор; см. Развертывание с несколькими сессиями и рабочими пространствами.
Более подробный перечень «что мы не будем исправлять на Этапе 1» (модель мутации состояния сессии на одном хосте + N параллельных сессий, использующих один дочерний процесс ACP) см. в разделе Границы области Этапа 1 — что мы не будем исправлять на Этапе 1.5 ниже.
Быстрый старт
Удаленный доступ через Web Shell и телефон
Чтобы подключиться к запущенному локальному демону с другой машины, откройте его Web Shell и перейдите в Settings → Connections → Allow remote connections. Удаленный доступ по умолчанию отключен. Включите его, выберите сетевой интерфейс при запросе и скопируйте отображаемый адрес и bearer-токен в форму подключения другого Web Shell. Дополнительные терминальные команды или перезапуск демона не требуются.
Удаленный слушатель обслуживает те же рабочие пространства и сессии, что и локальный. Сначала он использует тот же числовой порт на выбранном интерфейсе. Если этот адрес занят, выберите другой порт в расширенных настройках; Qwen не меняет порты незаметно и не останавливает другие службы. Разрешены HTTP-страницы на localhost и 127.0.0.1 на любом порту. Расширенные настройки принимают дополнительные точные браузерные origins, включая HTTPS-страницы.
Отключение доступа или сброс токена аннулируют существующие удаленные соединения; локальные клиенты и задачи, выполняемые демоном, продолжают работу. Повторное включение генерирует новый токен. Настройка не сохраняется между перезапусками демона. Токен доступен только авторизованному локальному оператору. Основной слушатель, уже открытый явной конфигурацией запуска/прокси, помечается отдельно и не закрывается этим переключателем.
Адрес должен быть доступен через сеть и брандмауэр. HTTP не шифрует токен; используйте доверенную сеть или настройте HTTPS. HTTPS-страница не может подключиться к удаленной HTTP-конечной точке. UI различает ошибки аутентификации, политики и смешанного содержимого; общая сетевая ошибка браузера не может надежно отличить CORS, проблемы с сертификатом и сбои подключения.
qwen serve --hostname 0.0.0.0Если ни --token, ни QWEN_SERVER_TOKEN не указаны, не-loopback слушатель генерирует 128-битный эфемерный bearer-токен (22 URL-safe символа) и выводит его один раз при запуске. Явные значения флагов имеют приоритет над переменными окружения; при привязке к не-loopback адресу явное пустое значение приводит к безопасному отказу в запуске, тогда как на loopback оно перекрывает QWEN_SERVER_TOKEN и оставляет привязку в доверенном режиме без токена, описанном в разделе --require-auth. Поведение по умолчанию для loopback не меняется.
При привязке к wildcard-адресу при запуске перечисляются локальные/сетевые URL без токена с фактических интерфейсов и — для сгенерированного токена или вашего собственного токена в интерактивном терминале — QR-код с секретом для одного помеченного адреса частной LAN. Перечисление wildcard показывает только тот же набор частных/link-local адресов LAN, который рекламирует Local Control (IPv4 RFC 1918 / RFC 3927, а также IPv6 ULA fc00::/7 при dual-stack привязках); фильтр программных интерфейсов, исключающий VPN-адаптеры и мосты контейнеров, является эвристикой по имени (#9158 ), а не гарантией, поэтому имена оверлеев k8s (cbr0, cali*, vxlan.*) могут просочиться. Явная непривязка к loopback вместо этого выводит и кодирует в QR именно выбранный вами адрес — включая маршрутизируемый. Внутри контейнера перечисленный адрес — это собственный интерфейс контейнера, поэтому доступ к опубликованному порту требует подстановки адреса хоста; контейнер с выделенным pty считается интерактивным терминалом, поэтому его логи остаются с секретами. Сканируйте QR, чтобы открыть встроенный Web Shell с bearer-токеном во фрагменте URL. Браузер удаляет учетные данные из URL и хранит их в отдельном для каждой вкладки sessionStorage. Альтернативно, откройте напечатанный URL и введите токен; отсутствующие или истекшие учетные данные показывают форму повторной попытки. Сгенерированные токены ротируются при перезапуске. --no-web пропускает QR, а stdout через pipe/редирект никогда не публикует стабильный операторский токен внутри QR — в этом случае печатается подсказка о подавлении с указанием --token-qr плюс QR только с адресом (Web Shell запрашивает токен при подключении) вместо этого; передайте --token-qr или установите serve.tokenQr в настройках user, system или system-defaults, чтобы все равно печатать полный QR с токеном; --no-token-qr подавляет QR быстрого старта токена при запуске для каждого пути quickstart, включая интерактивный терминал и сгенерированный токен (который все еще печатается как собственная строка обычного текста); он не управляет QR сопряжения Local Control, который --local-control печатает по дизайну. Запрос QR там, где его нельзя напечатать — --no-web, неразрешенные ресурсы Web Shell или привязка к loopback — сообщает об этом в stderr, вместо того чтобы молча ничего не делать. Адреса не подразумевают доступность через firewall или NAT; публичный IP не обнаруживается автоматически.
Однопроходные HTTP-запросы встроенного Web Shell работают без --allow-origin; другим origin браузера все еще нужен этот allowlist, и Origin: null остается отклоненным. Функции на базе WebSocket (терминал, голос) и браузеры, обращающиеся к демону через TLS-терминирующий обратный прокси, все еще требуют явной записи --allow-origin <origin> — пересылаемые заголовки никогда не вызывают доверия для установления same-origin. То же самое касается любого plain-HTTP промежуточного узла, переписывающего заголовок Host: nginx по умолчанию использует proxy_set_header Host $proxy_host, а Ingress в k8s заставляет демон видеть Host, который больше не совпадает с Origin браузера. Передайте --allow-origin <origin> для origin, который фактически видит ваш браузер, или настройте прокси на передачу Host дословно. При привязке к не-loopback адресу одна лишь трансляция порта (docker -p 8080:4170) ничего не требует — проверка same-origin сравнивает Origin с пересылаемым Host и никогда не обращается к слушающему порту. При привязке по умолчанию к loopback это не так: allowlist Host против DNS-rebinding принимает только собственный порт демона, поэтому туннель с трансляцией порта (ssh -L 8080:localhost:4170) отклоняется с 403 Invalid Host header для каждого запроса, включая документ shell, и --allow-origin не может это переопределить — пересылайте тот же порт (ssh -L 4170:localhost:4170) или привязывайтесь к не-loopback адресу. QR сопряжения Mobile access формируется из схемы и Host запроса, который его запрашивает — никогда из пересылаемых заголовков — поэтому за TLS-терминирующим прокси он сохраняет собственную схему демона http, и обмен отклоняет страницу телефона https, потому что приглашение привязано к origin, для которого оно было выпущено. Сопрягайтесь, просматривая прямой адрес демона, или вводите токен на телефоне. Bearer предоставляет полные права API демона, включая выполнение кода. Относитесь к токенам терминала и снимкам QR как к секретам. HTTP не шифруется; существующие опции --tls-cert и --tls-key предоставляют HTTPS с вашим собственным сертификатом.
Чтобы использовать удаленный демон из Web Shell на вашей собственной машине, введите origin удаленного демона (например, https://dev-box.example:4170) и его bearer-токен на экране подключения или в Daemon Status. Удаленный демон должен разрешать origin вашей локальной страницы. Вкладка браузера на фиксированном порту может перечислять этот origin, например qwen serve --hostname 0.0.0.0 --allow-origin http://localhost:4170; WebSocket терминалу тоже нужна эта запись. Клиент рабочего стола отличается: он загружает свой собственный shell из http://127.0.0.1:<OS-assigned port>, поэтому origin его страницы меняется при каждом запуске, и никакая фиксированная запись не может ему соответствовать. Разрешите этот класс origin явно с помощью --allow-origin http://127.0.0.1:*, который допускает любой порт на точно этом loopback-хосте как для REST API, так и для WebSocket терминала/голоса. Предпочтите его *: * допускает каждый origin браузера и покрывает только REST, а не шлюз WebSocket. Токен хранится отдельно для каждой вкладки и для каждого origin демона и никогда не переиспользуется для другого демона. Боковая панель перечисляет рабочие пространства от одного выбранного демона за раз; переключение демонов перезагружает страницу и не объединяет списки локальных и удаленных проектов. Используйте существующую форму Add workspace, чтобы зарегистрировать абсолютный путь на выбранном демоне. Ссылка, чей ?daemon= указывает на origin, к которому этот браузер ранее не подключался, не контактирует с ним, пока вы не подтвердите это на экране подключения. Междоменные подключения не предлагают браузерный локальный файловый мост, ни в автономном, ни во встроенном Web Shell. Страница HTTPS не может подключиться к HTTP-демону, поэтому обслуживайте оба по HTTPS вне доверенных сетей.
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: trusted loopback mode; local callers have full API access without bearer authentication, including code execution as the daemon user. Use --require-auth with QWEN_SERVER_TOKEN on shared or untrusted hosts.Привязка по умолчанию — 127.0.0.1:4170. Bearer-аутентификация отключена, и основной слушатель является доверенным, поэтому любой локальный процесс, который может достичь порта, может использовать полный API оператора, включая выполнение кода от имени пользователя демона. Проверки доверия рабочего пространства для конкретных маршрутов, владение сессией, X-Qwen-Client-Id, разрешения, возможности, валидация и ресурсы по-прежнему применяются. Демон регистрирует текущий рабочий каталог как основное рабочее пространство; используйте абсолютный путь --workspace /path/to/dir, чтобы переопределить его, и повторите флаг для регистрации дополнительных изолированных сред выполнения.
Для демона только с API отключите Web Shell. Маршруты session, prompt, workspace, permission и SSE остаются неизменными; поверхности, привязанные к Web Shell, исчезают вместе с ним — активация Local Control завершается ошибкой закрытым способом на всех платформах, и маршруты /live/* (плюс, на macOS с QWEN_SERVE_LIVE_NATIVE_HOST=1, WebSocket /live/host) не регистрируются:
qwen serve --no-webОткройте веб-интерфейс 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 этой записи при явном указании runtime. Поле limits.maxPendingPromptsPerSession сообщает активный лимит приема промптов для каждой сессии; null означает, что лимит отключен. limits.maxTotalSessions сообщает необязательный глобальный лимит новых сессий для демона; null означает неограниченность.
Инспектирование и управление локальным демоном
qwen serve публикует локальную запись обнаружения для каждого активного слушателя, поэтому второму терминалу не нужно парсить stdout или искать в ps:
qwen daemon start --port 4170 # detached; waits for the discovery record
qwen daemon status # ports, PIDs, workspaces, and health
qwen daemon status --json # one JSON object per daemon
qwen daemon status --detail # version, uptime, sessions, and issues
qwen daemon logs --port 4170 -f # follow the recorded log file
qwen daemon stop --port 4170 # SIGTERM the recorded process
qwen daemon restart --port 4170 # stop, then start with the recorded settingsЗаписи находятся в директории runtime ($QWEN_RUNTIME_DIR, $QWEN_HOME или ~/.qwen) в daemon/daemon-<port>.json. Они имеют права 0600 в POSIX и никогда не содержат bearer-токен. Запись, оставшаяся после падения демона, сообщается как stale; status --prune или stop удаляет ее. stop отправляет SIGTERM только после того, как слушатель ответит на /health или его PID все еще жив; --force добавляет SIGKILL после таймаута graceful shutdown. Когда зарегистрировано более одного демона, передайте --port; stop также принимает --all.
status --detail запрашивает у демона его собственный статус: версию, время работы, активные сессии и количество проблем. Он отправляет QWEN_SERVER_TOKEN только если в записи указано, что демон использует bearer-токен, а записанный процесс жив, доступен по адресу и запущен тогда, когда это указано в записи; запись, PID-пространство которой недоступно этому процессу, никогда не получает bearer-токен. Без токена демон, требующий bearer-токен, сообщает о необходимости аутентификации.
qwen daemon start запускает qwen serve как отдельный процесс (detached), ожидает его запись обнаружения и выводит привязанный URL, PID, путь к логу и команду остановки. Он удаляет идентификаторы и приватные привязки возможностей из запускающей сессии, чтобы дочерний процесс был новым демоном, сохраняет переменные окружения модели/аутентификации оператора и передает --no-token-qr, чтобы стартовый лог не мог содержать токен. Рабочие пространства канонизируются, относительные каталоги среды выполнения закрепляются за директорией запуска до старта, а Windows-корни, относительные к диску, разрешаются относительно диска запуска. Привязка не к loopback требует QWEN_SERVER_TOKEN; используйте qwen serve напрямую, если вам нужен эфемерный токен. Стартовый лог — это 0600, он удаляется после успешного запуска. --wait ждет, пока среда выполнения начнет обслуживать запросы (отсутствие проблемы daemon_runtime_starting и не error); при тайм-ауте стартовый лог сохраняется, и программа завершается с ненулевым кодом.
Для авторизованного фонового демона с переиспользуемым локальным учетным данным передайте стабильный токен через окружение и включите sidecar для токена:
export QWEN_SERVER_TOKEN="$(openssl rand -hex 32)"
qwen daemon start --require-auth --save-token --wait
qwen daemon token --port 4170 # print the saved bearer; treat the output as a secret
qwen daemon token --port 4170 --remove # delete only the local sidecarПредпочитайте QWEN_SERVER_TOKEN вместо --token, который виден в списке процессов оболочки. --save-token требует явно переданный токен и отказывается запускаться без него. Sidecar находится в daemon/daemon-<port>-<pid>.token в том же каталоге среды выполнения, что и запись обнаружения; на POSIX он имеет права 0600 и никогда не включается в запись обнаружения или лог демона. qwen daemon stop удаляет его, а qwen daemon restart переносит его в заменяющий экземпляр. qwen daemon token --remove удаляет локальную копию, но не изменяет и не отзывает учетные данные работающего демона; остановите и запустите демон с новым токеном для их ротации. Обычные qwen serve, включая --open-with-auth, не записывают этот sidecar.
qwen daemon restart останавливает один зарегистрированный демон через daemon stop и снова запускает его через daemon start --wait на записанных порту и имени хоста, с записанными рабочими пространствами запуска, корнем рабочего каталога Conversations, режимом Web Shell и настройкой --require-auth; --web/--no-web и --require-auth/--no-require-auth переопределяют запись. Сохраненные регистрации рабочих пространств восстанавливаются самим новым демоном, а не воспроизводятся как --workspace. Все остальные параметры qwen serve (например, --profile, --channel, --allow-origin или лимиты частоты запросов) сбрасываются к значениям по умолчанию, поэтому перезапустите демон, настроенный с ними, через qwen serve. Перед остановкой чего-либо команда restart отвергает HTTPS-демона, рабочее пространство запуска, которое больше не существует, запуск изнутри оболочки, которую хостит демон (например, его терминал Web Shell), и демон с bearer-токеном без --token или QWEN_SERVER_TOKEN; работающий демон с bearer-токеном должен принять токен, поэтому restart никогда не ротирует учетные данные. Токен, хранящийся только в .env рабочего пространства или файле настроек, не считывается командой qwen daemon. --force передается в daemon stop только если вы его указали. Если записанный порт занят, демон запускается на следующем свободном порту, и restart завершается с ненулевым кодом.
Корень Conversations, записанный текущим демоном, сохраняется командой daemon restart, даже если QWEN_CONVERSATIONS_DIR в перезапускающей оболочке изменился. Более старые записи обнаружения без этого поля снова используют окружение запуска или значение по умолчанию; они не могут гарантировать предыдущий выбор. Чтобы выбрать другой корень, остановите демон и запустите его с новым значением. См. Рабочий каталог Conversations для требований к сохранению и восстановлению.
Емкость регистрации рабочих пространств
Лимит регистрации по умолчанию равен 256. Установите QWEN_SERVE_MAX_WORKSPACES=25 qwen serve --workspace /path/to/project, чтобы уменьшить его; допустимые значения — десятичные целые числа от 1 до 256. Недопустимые значения приводят к ошибке запуска. Это настройка запуска демона: проект .env и проект settings.env не могут ее изменить. Встроенные вызывающие стороны могут установить ServeOptions.maxRegisteredWorkspaces, который имеет приоритет над средой запуска.
Лимит учитывает основное рабочее пространство, зарегистрированные вторичные рабочие пространства и пользовательские временные (scratch) рабочие пространства. Внутренняя среда выполнения Conversations освобождена от этого ограничения. Динамическая регистрация возвращает 409 workspace_limit_reached при достижении емкости. GET /capabilities и GET /daemon/status раскрывают limits.maxRegisteredWorkspaces.
При стандартной емкости регистрации 256 обычный демон по умолчанию использует фиксированный глобальный лимит сессий 800, включая демон, запущенный только с одним рабочим пространством. Тот же лимит сессий по умолчанию применяется к любой настроенной емкости регистрации выше 25. --max-total-sessions все еще может его переопределить; 0 отключает его. При емкостях 25 или меньше действует существующее поведение по умолчанию: одно рабочее пространство запуска или восстановления не ограничено, а несколькоderive общий лимит из лимита на рабочее пространство и их количества. Изменения регистрации не пересчитывают общий лимит. Это не меняет политику памяти дочерних процессов ACP и не гарантирует 800 безопасных одновременных сессий.
Управление каналами независимо поддерживает максимум 25 владельцев рабочих пространств, раскрываемых как limits.maxChannelControlWorkspaces. Переход также учитывает владельцев, сохраненных для восстановления; при достижении емкости успешно остановите все выбранные каналы на старом владельце перед добавлением нового владельца. Слишком большой переход возвращает 409 channel_control_workspace_limit_reached. --channel all продолжает выбирать только основное рабочее пространство. Тайм-аут SDK канала не изменен.
Сохраненные файлы регистрации сохраняют схему версии 1 и поддерживают до 255 вторичных записей в пределах 8 МиБ. Необработанные записи, включая устаревшие псевдонимы, занимают постоянные слоты до тех пор, пока не будут забыты. Если полный набор допустимых записей при запуске превышает настроенный лимит, запуск завершается ошибкой, и файл остается неизменным. Чтобы уменьшить емкость, перезапустите с предыдущей емкостью, забудьте достаточно регистраций и уменьшите явные аргументы --workspace перед перезапуском с более низким лимитом. Перед понижением версии бинарного файла сделайте резервную копию файла и используйте новый бинарный файл, чтобы сократить его до максимум 24 вторичных записей и 256 КиБ. Иначе более старые бинарные файлы отвергнут это хранилище и запустятся только с явными рабочими пространствами; изменения хранилища в них завершатся ошибкой до записи.
Настройка опроса состояния сессий в реальном времени
Web Shell обновляет состояние сессий рабочего пространства в реальном времени каждые 5 секунд по умолчанию. Чтобы использовать другой интервал, установите переменную окружения демона перед его запуском:
QWEN_SESSION_LIVE_STATE_POLL_INTERVAL_MS=10000 qwen serve --webЗначение представляет собой целое число в миллисекундах, от 1000 до 2147483647. Отсутствующие или недопустимые значения приводят к использованию 5000. Интервал применяется ко всем рабочим пространствам, обслуживаемым этим демоном; перезапустите демон и перезагрузите открытые страницы Web Shell после изменения. Локальные действия с сессиями и возврат к видимой вкладке могут по-прежнему вызывать немедленное обновление. Это изменяет только опрос состояния в реальном времени, а не опрос полного каталога сессий.
Демон публикует эффективный интервал как sessionLiveStatePollIntervalMs в /capabilities. Web Shell использует это значение и возвращается к пяти секундам при подключении к более старому демону, который его не предоставляет. Потребители SDK могут прочитать это значение при выборе собственного графика опроса.
Запуск каналов из демона
# 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 явный выбор имеет приоритет, демон резервирует аренду сервисного канала перед прослушиванием, а запуск завершается ошибкой, если начальный воркер не может стать готовым. При запуске без флагов демон восстанавливает serve.channels из каждого доверенного зарегистрированного рабочего пространства, поэтому канал, настроенный для запуска в не основном рабочем пространстве, возвращается вместе с демоном, а не требует вызова API во время выполнения. Если ни один источник не выбирает каналы, тяжелая среда выполнения каналов остается незагруженной, и аренда сервисного канала не резервируется до первого PUT во время выполнения или до регистрации рабочего пространства, которое настраивает свой собственный serve.channels. Доверенное рабочее пространство, зарегистрированное после запуска демона, также восстанавливает свои собственные serve.channels один раз за сеанс работы демона: повторная регистрация позже — после удаления или после перенастройки доверия — не повторяет восстановление, поэтому канал, остановленный вами в промежутке, остается остановленным. Запуск демона с явным --channel полностью пропускает этот шаг, как и остановка каналов с помощью qwen channel stop (DELETE /workspace/channel) до тех пор, пока qwen channel set снова их не включит: регистрация рабочего пространства не является запросом на повторное включение хостинга. В отличие от восстановления при загрузке, которое пропускает имя, которое оно не может разместить, и сохраняет остальные, позднее добавленное рабочее пространство, список которого содержит такое имя, не получает обратно ни одного своего канала, а журнал демона записывает причину. Регистрация рабочего пространства не ждет запуска его каналов. Имя канала, указанное в списке рабочего пространства, которое не может его разместить, пропускается с записью в журнале, оставляя каналы других рабочих пространств работающими; all по-прежнему применяется только к основному рабочему пространству. Если готовый воркер позже падает, демон продолжает работу, перезапускает его согласно ограниченной политике перезапуска и сообщает о своем состоянии (включая предупреждения channel_worker_exited) в GET /daemon/status.
Автоматическое восстановление пропускает недопустимое поле запуска или ошибку проверки либо аренды перед запуском воркеров, записывает в журнал свой источник serve.channels и сохраняет действие несвязанных настроек. После сбоя запуска воркера демон продолжает работу только после успешной очистки. Глобальные тайм-ауты запуска среды выполнения и неподтвержденные остановки воркеров сохраняют обычное поведение при сбое запуска; аренда сервиса удерживается, пока завершение работы воркера не подтверждено. Проверьте журнал демона на наличие пропущенных или неудачных восстановлений.
Сохраненные имена запуска должны быть непустыми, не содержать пробелов в начале или конце и не содержать небезопасных управляющих или невидимых символов. Недопустимые записи пропускаются по отдельности и записываются в журнал по индексу массива. Запуск не обрезает их до другого имени экземпляра и не переписывает настройки. Переключатель запуска канала отражает сохраненную конфигурацию, тогда как состояние среды выполнения сообщает, работает ли канал.
Управление средой выполнения доступно через GET, PUT и DELETE /workspace/channel; вспомогательные функции SDK — это getChannelWorkerControl(), setChannelWorkerSelection() и stopChannelWorker(). Методы PUT/DELETE/reload используют строгий шлюз мутаций: основные вызывающие стороны с доверенного локального адреса могут использовать их без токена, в то время как другие развертывания требуют учетных данных оператора. Выборки во время выполнения намеренно эфемерны: PUT не редактирует настройки или параметры загрузки, а перезапуск возвращает явный выбор qwen serve --channel, иначе — выбор serve.channels основного доверенного рабочего пространства, или отключенное состояние, если ни одно из них не существует. Явные CLI и именованные выборки во время выполнения обрезаются и дедуплицируются в порядке первого появления; порядок сохраняется, поскольку первый канал может повлиять на общий выбор модели.
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 приводит к сбою запуска, тогда как автоматическое восстановление записывает ошибку владения в журнал и пропускает эту выборку перед запуском воркеров. --channel all остается только для основного рабочего пространства (он размещает каналы основного рабочего пространства) и не может сочетаться с именованными каналами.
Замена выборки предварительно проверяет конфигурацию, владение и доверие перед остановкой чего-либо. Она сохраняет воркеры рабочих пространств, чья упорядоченная выборка не изменилась. Если измененный воркер не может запуститься, демон останавливает новые воркеры и восстанавливает старую выборку. Если демон не может подтвердить выход старого дочернего процесса даже после SIGKILL, он сохраняет аренду PID и отказывается создавать дублирующий воркер. Воркер считается готовым, когда хотя бы один запрошенный адаптер подключается; затем PUT возвращает partial: true, а /daemon/status сообщает channel_worker_partial_connect для отсутствующих адаптеров. Имя serve.channels, которое демон не смог поднять — настроенная выборка, чей воркер не запустился, имя, которое загрузка не смогла атрибутировать, или то, что осталось недоступным после восстановления поздней регистрации — сообщается как channel_restore_failed в /daemon/status и отображается как error со своим lastError в списке каналов этого рабочего пространства, если его собственные настройки определяют его. Другие причины, по которым имя может остаться неразмещенным, указаны только в журнале демона.
Когда адаптер отклоняет 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 … по-прежнему завершается, когда ни один адаптер не подключается.
Демон также предоставляет клиентским UI и операторам снимки runtime только для чтения: 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/resources и GET /session/:id/transcript.
GET /workspace/:id/session-info (и парная множественная форма GET /workspaces/:workspace/session-info) возвращает агрегированные счетчики сессий для рабочего пространства: сохраненные active / archived / total, плюс текущий счетчик в памяти live, когда live-состояние доступно. Зарегистрированные недоверенные вторичные рабочие пространства пропускают 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 /session/:id/resources возвращает очищенные снимки Skill и MCP из собственного Config этой live-сессии. Используйте его, когда сессия может запускаться из worktree или другой эффективной директории, ресурсы которой отличаются от базового рабочего пространства. Детали аутентификации MCP, пула, бюджета и ошибок обнаружения, принадлежащие рабочему пространству, опущены в этом представлении сессии. Проверьте возможность session_resources перед вызовом.
GET /daemon/status — это сводный снимок для устранения неполадок. Команда detail=summary по умолчанию читает только состояние демона в памяти (сессии, разрешения, счетчики транспорта SSE/ACP, отклонения из-за лимитов частоты запросов, память процесса, разрешенные лимиты) и не запускает дочерний процесс ACP. Используйте GET /daemon/status?detail=full для диагностики отдельных сессий, сведений о подключении ACP, счетчиков устройства авторизации через flow и разделов статуса рабочего пространства при активном расследовании проблемы.
GET /workspace/mcp сообщает о текущем состоянии среды выполнения ACP и не запускает дочерний процесс ACP в режиме простоя; неактивный демон возвращает initialized: false с пустым снимком. После того как сессия становится активной, он переключается на initialized: true и отображает реальное состояние.
На запрос GET /workspace/providers отвечает локально демон на основе настроек рабочего пространства и переменных окружения, поэтому ответ не зависит от активности дочернего процесса ACP, а acpChannelLive является сигналом работоспособности. Если настройки рабочего пространства или конфигурация модели не могут быть разрешены, ответ содержит initialized: false, пустой массив providers и запись errors. Маршрут обращается к живому снимку ACP только тогда, когда демон сконструирован без построителя статуса провайдера на уровне демона; локальный ответ демона никогда не включает модели, зарегистрированные во время выполнения.
GET /workspace/skills немедленно возвращает сохраненное наблюдение демона и ревалидирует его в фоновом режиме, поэтому «теплое» чтение не ожидает дочерний процесс ACP. Необязательный объект snapshot сообщает о observedAt, ageMs, stale и refreshing; проверьте ageMs перед тем, как считать каталог актуальным. До первого успешного наблюдения чтение внутри окна троттлинга запуска длительностью одна секунда может вернуть initialized: false с записью errors вместо ожидания; это временная недоступность, которую можно повторить, а не пустой каталог.
Чтобы обновить настройки Skill рабочего пространства по имени, вызовите POST /workspace/skills/:name/enable с { "enabled": true | false } после проверки возможности workspace_skill_settings_toggle. Чтобы изменить несколько имен, проверьте workspace_skill_settings_batch_toggle и вызовите POST /workspace/skills/enable с { "skillNames": ["review", "deploy"], "enabled": false }; он обрабатывает все структурно корректные имена вместе, сохраняет все resulting изменения деклараций в одной заблокированной записи настроек и обновляет активные сессии ACP один раз, если что-то изменилось. Эти маршруты записывают skills.disabled и skills.enabled рабочего пространства, не консультируясь с загруженным каталогом Skill, поэтому имена могут быть отключены до установки или когда их Extension неактивен. Включение удаляет соответствующее отключение рабочего пространства и записывает явный opt-in skills.enabled, даже до установки, так что эта запись может переопределить внутреннее отключение Skill в Extension. Повторение той же декларации является операцией no-op (changed: false). Их пути и тела запросов остаются неизменными; специфические для настроек возможности заменяют устаревшие теги переключения Skill, проверенные по каталогу. Массив errors пакетного режима сохраняется для совместимости с протоколом и пуст для структурно корректных имен. Жесткая запись skills.disabled, унаследованная из более высокого уровня, остается авторитетной для эффективной доступности, но не блокирует возможность рабочего пространства записывать или удалять свою собственную декларацию. Ячейки статуса Skill показывают disabledReason (hard, default или inactive_extension) и необязательный lockedScope. Записи в недоверенном рабочем пространстве по-прежнему отвергаются. Ответ deferred означает, что ни один дочерний процесс не был активен при проверке работоспособности, или измененный запрос потерял свой дочерний процесс/сессию во время требуемого обновления; когда changed равно true, сохраненная декларация применяется при запуске дочернего процесса. Факт изменения декларации сообщается через changed (в каждом результате для пакетного запроса). skills.disabled отключает как ручное, так и модельное использование, в отличие от disable-model-invocation: true, который оставляет доступным прямой вызов /skill-name. Для пакетных операций V2 Extension проверьте extension_batch_activation_v2: PUT /extensions/activation изменяет глобальные значения по умолчанию, а PUT /workspaces/:workspace/extensions/activation изменяет точные переопределения для выбранного рабочего пространства и принимает "inherit" для их очистки. Оба принимают имена в extensionNames; enabled и disabled могут быть объявлены до установки, тогда как inherit для неизвестного имени является операцией no-op. Каждый запрос возвращает одну операцию для опроса. Когда extension_activation_explicit_refresh рекламируется, эта операция завершается после фиксации политики активации и не обновляет напрямую активные сессии. Чтобы применить ее немедленно, отправьте независимое первичное или квалифицированное по рабочему пространству обновление Extension после успешной активации. Без этого явного обновления другие рабочие пространства могут продолжать использовать предыдущую активацию до следующего прохода генерационного согласователя (до 30 секунд); неудачное согласование повторяется последующими проходами.
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 без токена предоставляет операторские права; ненадежные встраивания без учетных данных возвращают 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
},
"tools": {
"todoWrite": {
"enabled": true
}
}
}Гард добавляет максимум два последовательных вызова основной модели без нового пользовательского ввода. Сообщение пользователя в середине хода обрабатывается первым и начинает новый этап из двух попыток; повтор/продолжение и связанные фоновые результаты сохраняют бюджет текущего этапа. Каждый вызов и финальное состояние исчерпания появляются как воспроизводимые события session_update с _meta.source: "todo_stop_guard"; метаданные включают номер попытки и количество незавершенных элементов, но никогда текст Todo. Запрошенный полный промпт также выполняется первым, а существующие правила разрешений/отмены остаются неизменными.
Пока вооруженная цепочка ожидает связанную фоновую работу, несвязанные срабатывания cron/loop и уведомления о старых задачах откладываются. Повторяющаяся работа ограничена и объединяется по задачам до тех пор, пока цепочка не уступит место.
Параметр по умолчанию имеет значение false, требует перезапуска и принудительно отключается в безопасном режиме, bare-режиме и режиме Approval plan. Он хранится только в памяти: загрузка состояния Todo с диска или перезапуск демона не активирует его. Установите tools.todoWrite.enabled в settings.json и перезапустите Qwen Code, затем новый обычный запрос должен успешно выполнить свой собственный вызов верхнего уровня todo_write; повторные попытки/продолжение и переподключение активного клиента сохраняют текущую цепочку задач в памяти. Успешное изменение рабочей директории сессии сбрасывает её, чтобы старое Todo не могло возобновиться в новом рабочем пространстве.
Аутентификация
Для всего, выходящего за пределы loopback, каждый API-запрос требует bearer token. Предоставьте стабильный токен для повторно используемых клиентов или запустите без него и используйте эфемерный токен, который демон генерирует и печатает:
export QWEN_SERVER_TOKEN="$(openssl rand -hex 32)"
qwen serve --hostname 0.0.0.0 --port 4170
# → uses QWEN_SERVER_TOKEN; generates an ephemeral token if neither token source is suppliedКлиенты затем отправляют Authorization: Bearer <token> в обычных API-запросах — $QWEN_SERVER_TOKEN, если вы экспортировали его, или bearer, напечатанный при запуске, если демон сгенерировал его (этот путь никогда не устанавливает переменную окружения, поэтому скопируйте значение из вывода при запуске). /health исключено только при обычных привязках loopback, чтобы liveness-пробы k8s/Compose внутри пода не требовали учетных данных. При непривязках loopback (например, --hostname 0.0.0.0) /health требует токен вместе с другими обычными API-маршрутами — иначе злоумышленник может зондировать произвольные адреса, чтобы подтвердить существование демона. Вход webhook канала отделен в любом режиме и использует настроенный x-qwen-webhook-secret вместо bearer демона. Используйте /capabilities, чтобы проверить, что ваш настроенный токен верен end-to-end:
Усиленный loopback (
--require-auth). Поведение по умолчанию для loopback без токена подходит для ноутбука с одним пользователем, но небезопасно на общих хостах разработки, CI-раннерах или многопользовательских рабочих станциях, где любой локальный пользователь можетcurl 127.0.0.1:4170. Передайте--require-auth, чтобы сделать токен носителя обязательным для каждого обычного API-маршрута — включая/healthи/capabilities— даже при привязке к127.0.0.1. Входящие вебхуки каналов остаются независимо аутентифицированными черезx-qwen-webhook-secret. Маршруты документов и ресурсов Web Shell остаются доступными до аутентификации; используйте--no-web, чтобы удалить их. Загрузка завершается ошибкой, если не удается определить источник токена, а в случае loopback это означает--token,QWEN_SERVER_TOKENили--open-with-auth— последний устанавливает собственный сгенерированный токен перед загрузкой, поэтому--require-auth --open-with-authзапускается. Генерация зависит от написания--hostname: варианты написания loopback (127.0.0.0/8,localhost,::1,[::1]) никогда не генерируют токен, тогда как любое другое написание — включая DNS-имя, которое случайно разрешается в loopback, — генерирует эфемерный токен носителя, который удовлетворяет--require-authи выводится при запуске (только токен, если сокет привязан к loopback), ротируясь при каждой перезагрузке, поэтому закрепите--tokenилиQWEN_SERVER_TOKENдля переиспользуемых клиентов. При включенном флаге неаутентифицированный клиент не может прочитать/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)
# Paste the bearer the daemon printed at startup, or your own QWEN_SERVER_TOKEN.
TOKEN='<printed bearer>'
curl -H "Authorization: Bearer $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. Install a local CA and trust it (one-time). The mobile device must
# also trust this CA — mkcert prints where the root cert lives.
mkcert -install
# 2. Generate a cert for your machine's LAN IP. Add localhost / 127.0.0.1 to
# the SANs too: with `--open`, the daemon rewrites the browser URL to
# 127.0.0.1, so a cert scoped to only the LAN IP would be rejected with
# ERR_CERT_COMMON_NAME_INVALID. (mkcert names the output after all hosts.)
mkcert 192.168.1.100 localhost 127.0.0.1
# 3. Start the daemon over HTTPS. Non-loopback binds still require a token,
# and the browser Origin must be allowed through 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 listening on https://0.0.0.0:4170Примечания:
- Оба флага или ни одного — загрузка завершается ошибкой, если указан только один (сертификат без ключа не может запустить HTTPS-прослушиватель).
- TLS ортогонален аутентификации — HTTPS шифрует транспорт; когда настроен токен носителя, он ограничивает обычные API-маршруты, кроме
/health, при обычной привязке к loopback. Передайте--require-auth, чтобы также ограничить loopback/health. Входящие вебхуки каналов всегда используют свой общий секрет. Привязки вне loopback всегда требуют токен, независимо от наличия TLS — тот, который вы передали, или эфемерный, который демон генерирует и выводит при запуске. - Только терминация TLS — без автогенерации, без ACME / Let’s Encrypt. Это удобство для LAN / разработки; для публичных развертываний терминацию TLS следует выполнять на reverse proxy (см. модель угроз ниже).
- Воркеры каналов подключаются к демону по
https://— поэтому им тоже нужно доверять серверному сертификату. Самоподписанный сертификат (или fullchain с собственным корневым) не требует ничего дополнительного: демон инжектирует его вNODE_EXTRA_CA_CERTSкаждого воркера. Поток mkcert выше — сертификат от CA, поэтому только листовый сертификат не может якорить цепочку — экспортируйтеNODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem"в среде запуска демона перед запуском с--channel. Значение, установленное оператором, объединяется с сертификатом демона, а не заменяет его. Без этого демон запускается без ошибок, а каждый воркер каналов входит в цикл перезапуска сUNABLE_TO_VERIFY_LEAF_SIGNATURE; демоновый лог называет этот пробел при запуске. - Привязка к IPv6 wildcard корректируется на loopback, фактически назначаемый этому хосту —
--hostname ::(или[::]) привязывает сокет IPv6. (Пустой--hostnameникогда не достигает прослушивателя: загрузка отвергает его как ошибку оператора до привязки, поэтому файл compose с неустановленнымHOSTNAMEзавершится явной ошибкой вместо тихого использования wildcard.) Этот сокет является двухстековым (Node фиксируетIPV6_V6ONLY=0на нем, поэтому sysctlnet.ipv6.bindv6onlyне меняет этого), и оба loopback обычно достигают его — но хост без IPv4 вообще имеет только::1, тогда как хост, привязывающий::, но не имеющий::1на своем loopback (например,net.ipv6.conf.lo.disable_ipv6=1), имеет только127.0.0.1. Воркеры направляются в[::1], если этот хост назначает его, и в127.0.0.1в противном случае. Сертификат сервера для привязки к IPv6 wildcard должен содержать оба адреса loopback в своих SANs (mkcert localhost 127.0.0.1 ::1покрывает это); диагностическая проверка доверия при загрузке проверяет точный URL, который будут набирать воркеры, и указывает на пробел.--hostname 0.0.0.0остается неизменным и по-прежнему требует127.0.0.1. - Ротация
--tls-certна месте требует перезапуска демона — демон обслуживает байты, прочитанные при запуске, поэтому до перезапуска возрождённые воркеры могут загрузить более новое содержимое, чем представляет демон, и их рукопожатия завершатся ошибкой.
Установка Web Shell как приложения
В поддерживаемом браузере откройте Web Shell демона и используйте действие браузера Установить приложение или Добавить на главный экран. Установленное приложение открывает тот же сервер в отдельном окне с названием и значком Qwen Code. На iOS используйте меню «Поделиться» браузера и пункт Добавить на главный экран. Доступность зависит от браузера и устройства.
Демон сам предоставляет метаданные PWA: GET /manifest.webmanifest и GET /sw.js являются маршрутами до авторизации (браузер запрашивает их оба до появления заголовка Authorization), а sw.js предоставляется с Service-Worker-Allowed: /, так что его область действия охватывает весь источник. Worker кэширует только ассеты сборки Vite с хешем содержимого, используя стратегию cache-first. Не захешированные публичные иконки и манифест ревалидируются через HTTP. Запросы к API, потоки событий и запросы с заголовком авторизации остаются на сетевом уровне.
Используйте доверенное HTTPS-соединение для телефона или другого удалённого устройства; HTTP на localhost или адресе обратной петли поддерживается для локальной разработки. Обычный HTTP на IP-адресе локальной сети не включает service worker. См. инструкции по TLS выше; установка не обходит проверки сертификатов или аутентификации.
Установка всегда запускает корень сервера. Токен, ссылка на сессию или рабочее пространство в текущем адресе никогда не включаются в URL запуска приложения. Возможно, вам потребуется снова ввести токен в установленном приложении: оно использует существующий процесс аутентификации Web Shell и не добавляет постоянное хранение учётных данных.
Сервер должен оставаться запущенным и доступным. После активации service worker и взятия им управления страницей, открытие корня или ссылки на сессию при сбое соединения показывает страницу повтора. Никакие диалоги, файлы, ответы API или учётные данные не сохраняются в кэше service worker. Существующие ошибки HTTP по-прежнему поступают от сервера. Этот первый шаг не обеспечивает офлайн-работу, Web Push или уведомления при закрытом приложении. Обновлённые workers активируются немедленно с помощью skipWaiting() и clients.claim(), заменяя предыдущий worker для открытых вкладок без принудительной перезагрузки страницы. Активация удаляет старые кэши оболочки Qwen и сохраняет несвязанные кэши.
Метаданные установки используют встроенную идентичность Qwen Code; пользовательское брендинг во время выполнения всё ещё применяется внутри Web Shell. Встроенные потребители библиотеки не регистрируют worker. Для удаления установки используйте управление приложениями вашего браузера или операционной системы; для удаления его service worker и сохранённых данных сайта используйте настройки сайтов браузера.
Qwen Live (экспериментально)
Qwen Live работает через Web Shell на всех платформах: вкладка браузера выступает в роли микрофона и динамика (WS /live/web), поэтому откройте Web Shell через https или localhost для доступа к микрофону. Нативное приложение macOS Qwen Live Host по умолчанию отключено; чтобы попробовать его, запустите демон macOS с помощью QWEN_SERVE_LIVE_NATIVE_HOST=1, что восстанавливает WebSocket /live/host, возможность realtime_voice и загрузку Host при включении Live. Без него уже установленный Qwen Live Host.app не сможет подключиться (демон не регистрирует /live/host); завершите его работу и используйте Web Shell.
Флаги CLI
| Флаг | По умолчанию | Назначение |
|---|---|---|
--port <n> | 4170 | TCP-порт. 0 = эфемерный порт, назначаемый ОС. |
--hostname <addr> | 127.0.0.1 | Интерфейс привязки. Все, кроме loopback, требует токен; если ни --token, ни QWEN_SERVER_TOKEN не предоставляют его, генерируется эфемерный токен и выводится при запуске. |
--local-control | false | Публикует Web Shell на одном выбранном частном IPv4 интерфейсе с отзывным токеном сопряжения, принадлежащим демону, QR-кодом в терминале, точным origin браузера и best-effort блокировкой сна. Совместимо с --token, --allow-origin и --port 0; конфликтует с --no-web и не дефолтным --hostname. Используйте --local-control-address, когда доступно несколько кандидатов LAN, и добавьте --tls-cert + --tls-key для API браузера безопасного контекста, таких как голосовой ввод. |
--local-control-address <ip> | — | Какой IPv4-адрес LAN публиковать, если у хоста более одного кандидата. Требуется только если --local-control сообщает о неоднозначном выборе. |
--token-qr | — | Выводить QR-код с токеном даже при перехвате stdout (не интерактивный терминал) и использовании операторского (стабильного) токена, который по умолчанию скрыт, чтобы стабильные учетные данные не попадали в собираемые логи. Также можно задать через настройку serve.tokenQr, которая учитывается только в областях user, system и system-defaults; флаг имеет приоритет при передаче. Отсутствие флага не равно --no-token-qr: отсутствие оставляет политику по умолчанию (QR в интерактивном терминале или для сгенерированного токена), тогда как --no-token-qr подавляет стартовый quickstart QR с токеном для этого запуска на всех путях quickstart, так что постоянную настройку можно отклонить для конкретного запуска. Ни то, ни другое не относится к QR-коду сопряжения Local Control. |
--token <str> | — | Bearer-токен. Откатывается к переменной окружения QWEN_SERVER_TOKEN (с удалением пробелов по краям — удобно для $(cat token.txt)). Привязка вне loopback без источника генерирует эфемерный токен. Явно пустое значение все равно отказывает в запуске при привязке вне loopback; на loopback оно перекрывает переменную окружения и выбирает доверенный режим без токена (или полностью отказывает при --require-auth). |
--require-auth | false | Отказывать в запуске без bearer-токена, даже на loopback. Ужесточает разработческое значение по умолчанию 127.0.0.1 для общих dev-хостов / CI раннеров / многопользовательских рабочих станций, где любой локальный пользователь может обратиться к слушателю. Этот fail-fast действует только для loopback: loopback запускается только с одним из трех источников токена — --token, QWEN_SERVER_TOKEN или --open-with-auth, который устанавливает свой сгенерированный токен перед запуском (генерация подавлена для loopback) — тогда как при привязке вне loopback сгенерированный эфемерный токен удовлетворяет флагу. Также ограничивает /health требованием bearer-токена. |
--enable-session-shell | false | Включает прямое выполнение shell-сессий. Эффективно либо с настроенным bearer-токеном, либо в режиме доверенного loopback; вызовы все еще требуют действительного привязанного к сессии X-Qwen-Client-Id. |
--tls-cert <path> | — | Путь к файлу сертификата PEM. Обслуживание через HTTPS вместо HTTP. Должно быть в паре с --tls-key (запуск завершается ошибкой, если указан только один). Разблокирует API браузера безопасного контекста — голосовой ввод (getUserMedia), WebRTC — через IP-адрес LAN, которые браузеры иначе блокируют на простом http://. Только терминирование TLS; без автогенерации / ACME. См. HTTPS / TLS ниже. |
--tls-key <path> | — | Путь к файлу закрытого ключа PEM. Должно быть в паре с --tls-cert. |
--max-sessions <n> | 32 | Ограничение на количество одновременных живых сессий. Новые запросы POST /session, которые создадут новый дочерний процесс, возвращают 503 (с Retry-After: 5) при достижении лимита; подключение к существующим сессиям НЕ учитывается. Установите 0 для отключения. Также можно задать через serve.maxSessions (только области user, system и system-defaults); флаг имеет приоритет при передаче. Рассчитано на использование одним пользователем / небольшой командой; увеличьте, если ваше развертывание имеет запас RAM/FD (~30–50 МБ на сессию). |
--max-total-sessions <n> | 800 или производное | Необязательное неотрицательное целое число, задающее глобальный лимит демона на создание новых сессий во всех зарегистрированных runtime рабочих пространств. Применяется к новым дочерним сессиям, восстановлению сессий и сессиям, созданным ветвлением/форком; подключение к существующей живой сессии не потребляет слот. Установите 0 для неограниченного количества. По умолчанию 800, когда емкость регистрации превышает 25 (включая стандартные 256). При емкости 25 или меньше одно рабочее пространство при запуске или восстановлении не ограничено; несколько получают общий лимит из расчета на рабочее пространство и их количества. Поздняя динамическая регистрация не пересчитывает это. |
--max-pending-prompts-per-session <n> | 5 | Лимит на количество принятых, но еще не завершенных промптов на сессию через POST /session/:id/prompt, включая промпты в очереди и активный промпт. Мост синхронно отвергает переполнение с 503, Retry-After: 5 и code: "prompt_queue_full" перед возвратом promptId. Установите 0 для отключения. branchSession сериализуется в той же FIFO, но не учитывается в этом лимите промптов. |
--workspace <path> | process.cwd() | Абсолютный путь к рабочему пространству, зарегистрированному этим демоном. Повторите флаг, чтобы разместить несколько рабочих пространств в одном процессе; первое является основным и остается по умолчанию, когда запрос не указывает cwd. Относительные значения отвергаются. Запросы сессий, чей канонический cwd не зарегистрирован, возвращают 400 workspace_mismatch. |
--conversations-dir <path> | — | Абсолютный корень рабочего каталога Conversations для автономных и Live сессий. Переопределяет QWEN_CONVERSATIONS_DIR; если ни то, ни другое не задано, используется ~/Documents/Qwen Code/Conversations. Также принимается qwen daemon start. См. Рабочий каталог Conversations. |
--memory-project-scope <mode> | workspace | Режим разделения памяти проекта. workspace (по умолчанию) привязывает память к точному зарегистрированному рабочему каталогу, так что каждый рабочий процесс демона получает изолированную память; git-root — это устаревший режим совместимости, общий для рабочих процессов, разрешенных в один и тот же корень Git. Переопределяет QWEN_CODE_MEMORY_PROJECT_SCOPE при наличии; пустое значение переменной окружения считается незаданным, а нераспознанное непустое значение игнорируется с одноразовым предупреждением, сохраняя устаревшее поведение git-root. Новый параметр по умолчанию не переносит существующую память проекта на уровне корня Git — используйте явную область git-root для чтения этих записей во время миграции. |
--channel <name|all> | — | Экспериментальный канал worker, управляемый демоном. Повторите флаг для выбора нескольких настроенных каналов или передайте all для запуска всех настроенных каналов. all нельзя комбинировать с именованными каналами. Значения выбранного канала cwd должны разрешаться в зарегистрированный рабочий процесс; демон с несколькими рабочими процессами запускает одного worker для каждого владельца. Worker принадлежит qwen serve; остановите демон, чтобы остановить каналы, управляемые serve. |
--max-connections <n> | 256 | Ограничение уровня слушателя на количество TCP-соединений (server.maxConnections). Ограничивает количество сырых сокетов независимо от количества сессий — медленные / фантомные SSE-клиенты отклоняются при принятии соединения, когда лимит исчерпан. Увеличивайте вместе с --max-sessions, если ваше развертывание предполагает много подписчиков SSE на одну сессию. |
--memory-budget-mb <n> | 50% от cgroup/хоста | Общий бюджет памяти в МБ для всего дерева процессов демона. Если не задано, вычисляется как 50% от лимита cgroup или памяти хоста; в любом случае эффективное значение ограничено доступной памятью, и сообщаются как настроенные, так и эффективные цифры. Оно управляет фиксированным разделом ACP, выбранным через --child-heap-mode, и адаптивным ростом живого журнала; это не общее ограничение RSS: пул роста, общий для всего демона, вычисляемый как 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 против доступной памяти (за чем следит убийца OOM cgroup) и используемая куча V8 против потолка кучи этого процесса. Он охватывает только корневой процесс демона; сравните его с runtime.memory.children.rssBytes для дочерних процессов. Ничто не исправляется ни в одном режиме. Одно из значений off, observe. |
--child-heap-mode <mode> | observe | off отключает разделение; observe сообщает limits.memory.childHeap без изменения запуска процессов; admit обеспечивает только моделируемое количество одновременных дочерних процессов. Экспериментальный opt-in enforce также дает каждому управляемому дочернему процессу ACP фиксированный потолок old-space perChildCeilingMb и сообщает limits.memory.enforced: true. Оба режима допуска могут освободить один подходящий простаивающий ACP перед отказом с acp_child_capacity_exhausted; активные сессии остаются защищенными. Регистрация независима от емкости дочерних процессов. Конфигурации допуска с нулевым слотом приводят к ошибке запуска. Фиксированные унаследованные флаги кучи Node нормализуются в enforce; процентные флаги конфликтуют и отвергаются. Потолок ограничивает old space, а не RSS или потомков MCP. Откалибруйте свою нагрузку перед включением; ноль отказов не доказывает безопасность кучи. Изменения режима и бюджета требуют перезапуска демона; перезапустите с помощью admit для отката применения ограничений кучи. |
--event-ring-size <n> | 8000 | Глубина кольца воспроизведения SSE на сессию (цель §02 #3803). Устанавливает объем накопленных данных, доступный для GET /session/:id/events с Last-Event-ID: N. Больше = больше запаса для переподключения ценой дополнительных нескольких сотен КБ RAM на сессию. Клиенты SDK могут дополнительно запрашивать больший предел накопленных данных для конкретного подписчика через ?maxQueued=N (диапазон [16, 2048], по умолчанию 256). Демоны также отправляют нетерминальный кадр SSE slow_client_warning при заполнении очереди на 75%, чтобы клиенты могли очистить данные / переподключиться до удаления. Предварительная проверка 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, который ограничивает параллельность запуска, а не общее число активных клиентов. Предварительные проверки: 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), используемый в режиме required, например http://127.0.0.1:8787. Пути, учетные данные в URL, перенаправления, не-локальные хосты и маршрутизация через прокси не принимаются. |
--external-tool-guard-timeout-ms <n> | 3000 | Целочисленный 100..30000; применяется независимо к рукопожатию при запуске и каждому запросу на подготовку. |
--http-bridge | true | Режим этапа 1: в производственной среде предпринимается попытка предварительно запустить один основной дочерний процесс qwen --acp для обеспечения совместимости, с повторной попыткой при первом использовании после сбоя, тогда как каждый доверенный вторичный процесс может запускать дочерний процесс по требованию. Сессии, нацеленные на среду выполнения, мультиплексируются в ее дочерний процесс через ACP newSession(); недоверенные вторичные процессы не могут запускать ACP. Нативная внутрипроцессная реализация этапа 2 станет доступна позже. |
--initialize-timeout-ms <n> | 10000 | Тайм-аут запроса к дочернему процессу ACP, включая рукопожатие initialize (в мс). Должно быть положительным целым числом до 2147483647. Значения выше предела таймера JS (2^31-1) отвергаются при запуске, так как Node.js молча сжимает их до 1 мс. Развертывания в холодных контейнерах, которым требуется дополнительное время для запуска дочерних процессов, могут увеличить это значение; то же значение управляет newSession, опросами статуса рабочего пространства и другими сроками методов расширения ACP. |
--session-restore-timeout-ms <n> | 60000 | Срок загрузки/возобновления сессии ACP в миллисекундах. Должно быть положительным целым числом до 2147483647; 0 недопустимо. Если не указано, по умолчанию используется 60 секунд, увеличиваемое до явно переданного значения --initialize-timeout-ms, если оно больше; более короткий тайм-аут инициализации никогда не уменьшает бюджет восстановления. SDK и Web Shell добавляют 10 и 15 секунд клиентского запаса соответственно. По истечении времени возвращается повторяемая ошибка 504 session_restore_timeout; это не означает, что демон завершился сам. |
--allow-origin <pat> | — | T2.4 (#4514 ). Список разрешений для браузерных клиентов из разных источников. Повторяемый параметр. Каждое значение представляет собой * (любой источник — при привязке к loopback запуск прерывается, если токен носителя не настроен; при привязке к не-loopback адресам генерируемый эфемерный токен удовлетворяет этим требованиям, поэтому отказы касаются только loopback; рекомендуется использовать --require-auth на loopback, чтобы /health также контролировался токеном носителя, так как по умолчанию он доступен без авторизации на loopback; статические ресурсы Web Shell всегда доступны без авторизации во всех режимах, поэтому передайте --no-web, чтобы удалить их) или канонический URL-источник (<scheme>://<host>[:<port>], без слэша в конце, пути, userinfo или query). Канонический loopback-источник может использовать :* для порта (<scheme>://<loopback-host>:*, например http://127.0.0.1:*), чтобы разрешить этот хост на любом порту; требуется токен носителя, точное совпадение хоста (127.0.0.1, localhost и [::1] — разные записи), и это учитывается как матчером CORS REST, так и защитой CSRF WebSocket. Без токена HTTP(S)-источники должны быть loopback при привязке к loopback; при привязке к не-loopback адресам генерируемый токен удовлетворяет тому же требованию. Удаленные HTTP(S)-источники проходят аутентификацию с токеном носителя на каждом API-маршруте; исключения без авторизации — только маршруты документа/ресурсов Web Shell, песочница приложения MCP, вход вебхука канала, который аутентифицируется собственным x-qwen-webhook-secret, и /health при привязке к loopback (контролируется токеном носителя в других случаях). Расширения браузера сохраняют доступ без токена локально. Подстановки хостов и поддоменов (https://*.example.com) намеренно не поддерживаются — перечисляйте каждый хост явно или используйте * с настроенным токеном (и --require-auth для полной защиты); подстановка порта loopback выше — единственная форма подстановки. Совпавшие источники получают заголовки ответа CORS (Access-Control-Allow-Origin, Vary: Origin, методы, заголовки, max-age и открытые Retry-After); несовпавшие источники все равно получают 403 с тем же форматом ошибки, что и сейчас. Origin: null (изолированные iframe, документы file://) всегда отвергается, даже при *. Предварительная проверка через caps.features.allow_origin. Запросы к собственному источнику loopback не затрагиваются. |
--web / --no-web | true | Обслуживание собранного SPA Web Shell в корне демона (навигации документов GET /, /assets/*, /manifest.webmanifest, /sw.js и GET /session/<id>). Эти точки входа регистрируются до шлюза авторизации по токену носителя — браузер не может прикрепить токен к субресурсу <script> или навигации в адресной строке, и оболочка не содержит секретов. API-запросы следуют обычной политике полномочий демона: настроенные токены защищают обычные API-маршруты, кроме loopback /health, если не установлено --require-auth, вход вебхука канала всегда использует свой общий секрет, а доверенный основной слушатель loopback без токена имеет полный операторский доступ. Резервный вариант глубокой ссылки SPA для всех остальных путей находится за middleware авторизации по токену носителя. При привязке к не-loopback адресам однострочное предупреждение в stderr указывает, что UI доступен без авторизации. Используйте --no-web для демона только с API. Нет эффекта, если сборка исключает ресурсы Web Shell (демон записывает маркер и работает только с API). |
--open | false | После запуска слушателя откройте Web Shell в браузере по умолчанию по URL демона (с добавлением #token= как фрагмента URL, когда токен определен — настроен или сгенерирован — поскольку фрагмент никогда не отправляется на сервер, что сохраняет токен вне журналов доступа и заголовков Referer). Не действует при --no-web или в средах headless / CI / SSH, где браузер недоступен. |
--open-with-auth | false | Открыть Web Shell с авторизацией по токену носителя на loopback. Требует включенного Web Shell и собранных ресурсов. Переиспользует настроенный токен или генерирует 256-битный токен носителя на время жизни процесса и доставляет его во фрагменте Web Shell. В средах, не подходящих для браузера, выводится ручной URL с секретом. Другим клиентам требуется тот же явно настроенный общий токен. |
Примечания об области проекта памяти.
- Демон против автономного CLI. Флаг или среда запуска демона устанавливают одну замороженную область для всех runtime, принадлежащих этому демонy. Рабочее пространство
.envилиsettings.envне может переопределить его для одного зарегистрированного рабочего пространства. Автономный TUIqwenпо-прежнему по умолчанию использует область git-root; чтобы сохранить согласованность обоих точек входа, экспортируйтеQWEN_CODE_MEMORY_PROJECT_SCOPEв shell или среде сервиса, которые их запускают.- Коллизии имён директорий. Ключ хранения выводится с помощью
sanitizeCwd, который заменяет каждый неалфавитно-цифровой символ на-. Родственные директории, отличающиеся только пунктуацией (например,feature_1иfeature-1), отображаются в одну и ту же директорию памяти даже в областиworkspace. Избегайте таких имён, если полагаетесь на изоляцию рабочих пространств.- Нормализация различается между флагом и переменной окружения. Переменная окружения обрезается и приводится к нижнему регистру (
" Workspace "работает); CLI-флаг сопоставляется с учётом регистра yargschoices(--memory-project-scope Workspaceотвергается). Используйте значения в нижнем регистре при копировании между ними.
Встроенный страж перемещения Git для демона
Каждая управляемая ACP-сессия демона применяет встроенный предвыполнительный guard для команд модели в 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, а не каталогом сессии; вызов оболочки, рабочий каталог выполнения которого демон не может определить, отклоняется.
Относительные цели разрешаются от эффективного начального каталога команды (arguments.directory, если присутствует, иначе текущий эффективный рабочий каталог сессии) после канонического разрешения пути, включая перенаправления gitfile .git, символьные ссылки и административные каталоги каждого worktree. Перемещённая цель, которую невозможно полностью разрешить до выполнения — динамическая цель ($VAR, обратные кавычки, ~, глобы), путь, который ещё не существует, или нечитаемая косвенная ссылка — отклоняется для изменяющих или некласифицируемых подкоманд. Перемещённая цель, которую невозможно разрешить, отклоняется независимо от подкоманды — включая только для чтения. Перемещённые команды, чья подкоманда входит в небольшой проверенный набор только для чтения (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, написанного в вышеуказанных литеральных формах — неправильно нацеленной команды, ради которой существует этот контроль — и является лучшим усилием, а не границей, против текста оболочки, написанного для его обхода: конструкции, скрывающие перемещение от статического читателя, могут пройти, и новые будут продолжать обнаруживаться. Не предоставляйте демону более широкое доверие на основании этого. Он не интерпретирует файлы скриптов, не отслеживает значения переменных окружения между командами и не анализирует тела heredoc (текст в форме Git внутри heredoc может быть отклонён, даже если оболочка никогда его не выполняет). /fork и память рабочего пространства агента remember/dream остаются доступными под встроенным защитником; они ограничены только при активном режиме внешнего провайдера ниже. Необязательный внешний защитник инструментов остаётся дополнительной политикой и получает тот же запрос только после того, как встроенная политика его разрешит.
Обязательный страж внешних инструментов
Этот выбор предназначен для управляемых ACP-развёртываний, которым требуется внешнее решение allow/deny на финальной границе исполнителя инструментов. Он полностью скрыт, если отсутствует --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 также должен подтвердить, что он установил требуемый callback; отсутствующее или несовместимое подтверждение отвергает канал до создания Session. Запрос провайдера содержит sessionId, promptId, toolCallId, канонический toolName и финальный arguments; toolCallId является меткой корреляции, а не идентификатором аутентификации или автономным ключом идемпотентности.
Финальные аргументы могут содержать конфиденциальные данные приложения. Относитесь к ним соответственно в логах провайдера и хранилище аудита.
Хуки PreToolUse выполняются перед этим финальным решением исполнителя. Режим Required Guard не авторизует и не изолирует поведение хуков; развёртывания, которым нужна граница вокруг каждого возможного побочного эффекта, должны отключать хуки или управлять их реализациями отдельно.
Действия слэш-команд также выполняются до планирования модели/инструментов и не являются вызовами Guard. Некоторые встроенные команды могут напрямую изменять файлы или настройки. Управляемое развертывание, которому требуется граница для всех эффектов, должно отклонять ввод слэш-команд или отключать все неутвержденные команды через slashCommands.disabled или --disabled-slash-commands.
Область управления v1 охватывает инструменты верхнего уровня, вызываемые активным управляемым Prompt в переднем плане. Вложенные или делегирующие agent, workflow, create_sub_session, send_message, прямые /fork и элементы управления памятью рабочего пространства с поддержкой агента (remember/dream) отклоняются, пока активен требуемый режим. Запуск фонового шелла или монитора верхнего уровня по-прежнему является одним защищенным вызовом, и его финальные аргументы достигают провайдера, но эта функция не предоставляет непрерывную авторизацию процесса и не добавляет протокол аудита завершения процесса; политика, требующая завершения в переднем плане, должна запрещать такие сценарии. Защищенные вызовы MCP также отключают автоматическое переподключение/повтор после ошибки транспорта. После успешного рукопожатия при запуске /capabilities объявляет external_tool_guard; его отсутствие означает, что клиенты не должны предполагать наличие принудительного применения.
Эта функция не авторизует явные вызовы управления daemon REST/ACP; они продолжают использовать существующие контракты аутентификации и маршрутизации демона. Она также не делает разрешенный инструмент или команду шелла детерминированными и не изолирует их внутренности; управляемые развертывания должны сочетать решение провайдера с обычной политикой инструментов и границей изоляции.
Настройка параметров нагрузки.
--max-sessions— это лимит новых сессий на рабочее пространство.--max-total-sessions— это общий лимит новых сессий для демона; если он не указан, по умолчанию используется 800 при стандартной емкости регистрации 256. Три других слоя также ограничивают нагрузку — при настройке для высококонкурентного развертывания регулируйте их вместе:
- уровень слушателя:
--max-connections/server.maxConnections=256ограничивают необработанные TCP-соединения (обратное давление на медленных клиентов).- подписчики на сессию: EventBus по умолчанию ограничивает количество подписчиков SSE до 64 на сессию; 65-й клиент получает терминальный
stream_errorи закрывается.- допуск prompt на сессию:
--max-pending-prompts-per-session=5ограничивает количество принятых queued + active prompts для одной сессии. При переполнении возвращается503сRetry-After: 5.- новые сессии на уровне демона:
--max-total-sessions=Nограничивает создание новых сессий во всем демоне. При переполнении возвращается та же структураsession_limit_exceededсscope: "total".- очередь на подписчика: очередь из 256 кадров для каждого клиента SSE; клиент, превышающий емкость, получает терминальный кадр
client_evictedи закрывается (один медленный потребитель не может заблокировать демон).Эти лимиты взаимодействуют: каждая среда выполнения ограничена
--max-sessions, тогда как--max-total-sessionsограничивает их совокупность. Эффективный потолок сессий — это меньшее из любого конечного общего лимита демона и совокупного лимита на среду выполнения (считайте эту совокупность неограниченной, если лимит на рабочее пространство неограничен). Если ни один из них не является конечным — что при стандартной емкости регистрации требует--max-total-sessions 0— конечного потолка сессий нет. Конечный потолок × 64 подписчика × 256 кадра — это худший случай памяти в полете на уровне EventBus; умножение этого значения на--max-pending-prompts-per-sessionограничивает принятую работу prompt на уровне допуска. Стандартная настройка предполагает нагрузку для одного пользователя / небольшой команды; постепенно увеличивайте значения (и следите за 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 # later, after telemetry shows your real-world distribution: 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по-прежнему является источником истины для состояния после переподключения; события — это фронты изменений. Полезно для построения дашбордов в реальном времени без опроса.
Модель угроз для деплоя по умолчанию
- Только loopback —
127.0.0.0/8,localhostили::1; аутентификация не нужна. localhostразрешается один раз, фиксируется и проверяется после прослушивания — если его фактический адрес слушателя находится за пределами127.0.0.0/8или::1, запуск без токена закрывает слушатель и завершается ошибкой.--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или точным привязанным loopback-адресом и портом (без учета регистра согласно RFC 7230 §5.4) для защиты от DNS rebinding. При прослушивании на порту 80 или 443 также принимаются соответствующие формы без порта, потому что браузеры опускают порты по умолчанию схемы. Это поддерживает полный диапазон loopback IPv4 (127.0.0.0/8) без пропуска посторонних Host. Привязки не к loopback (--hostname 0.0.0.0) намеренно обходят allowlist Host — оператор сам выбрал поверхность атаки, поэтому проверка bearer-токена является единственным уровнем аутентификации для обычных API-маршрутов; обратные прокси / SNI / привязка клиентских сертификатов — это ответственность оператора, а не демона. Если вам нужна изоляция на основе Host при привязке не к loopback, завершайте TLS + проверяйте Host на фронтальном прокси. - CORS по умолчанию отклоняет любой браузерный Origin — возвращает
403JSON. Передайте--allow-origin <pattern>(повторяемый, T2.4 #4514), чтобы разрешить доступ для конкретных браузерных origins. Каждое значение представляет собой либо литерал*(любой origin — при привязке к loopback загрузка отказывается, если не настроен bearer-токен, тогда как привязка вне loopback выполняется успешно, поскольку генерация токена предоставляет его; использование--require-authна loopback рекомендуется для полного усиления безопасности, так как/healthпо умолчанию остается пред-аутентификационным на loopback — обратите внимание, что статические ресурсы Web Shell (/,/assets/*,/manifest.webmanifest,/sw.js,/session/:id,/plugins,/channels,/live,/scheduled-tasks,/goalsи навигации по документам/settings) монтируются до проверки bearer-токена во всех режимах и остаются пред-аутентификационными даже при использовании--require-auth, поэтому используйте--no-web, когда остаточная поверхность браузера имеет значение) или канонический URL-origin (<scheme>://<host>[:<port>], без завершающего слэша / пути / userinfo). Хост loopback может вместо этого использовать подстановку порта (<scheme>://<loopback-host>:*); это требует настроенного bearer-токена (загрузка отказывается без него), точно совпадает со схемой и хостом (127.0.0.1,localhostи[::1]являются отдельными записями) и является единственной формой подстановки, кроме предназначенной только для REST*. При привязке к loopback без токена записи HTTP(S) должны использовать хост loopback; привязка вне loopback всегда содержит токен — предоставленный или сгенерированный — именно потому, что удаленно размещенные браузерные origins могут выполнять код через API оператора от имени пользователя демона. Явные origins расширений браузера сохраняют свой путь локальной автоматизации без токена. Совпавшие 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-origin (например, UI Web Shell) не затрагиваются — отдельный шим удаления Origin обрабатывает их независимо от--allow-origin. Однодоменные HTTP-запросы встроенного удаленного Web Shell также работают без--allow-origin: на первичном слушателе вне loopbackOrigin, равный схеме прямого сокета плюс нормализованному авторитетуHost, проходит аутентификацию по bearer-токену, а затем удаляется перед стеной CORS, в то время как его существующие публичные статические маршруты остаются неаутентифицированными. Два исключения по-прежнему требуют--allow-origin— перенаправленные заголовки никогда не устанавливают доверия однодоменности, поэтому браузерный origin TLS-терминирующего обратного проксиhttpsне является однодоменным (как и посредник plain-HTTP, переписывающий заголовокHost, такой как nginx по умолчаниюproxy_set_header Host $proxy_hostили k8s Ingress; решение там — указать--allow-origin <origin>для origin, который видит ваш браузер, или использовать прокси, передающийHostдословно — перевод портов сам по себе ничего не требует, потому что сравнивается только перенаправленныйHost, из которого удаляются только значения по умолчанию для схемы:80/:443), а маршруты обновления WebSocket (терминал, голос) имеют отдельный шлюз CSWSH, который допускает только однодоменные origins loopback, явно разрешенные origins (включая подстановки портов loopback) и собственный origin слушателя Local Control; литерал*там не применяется. - Автоматизация браузера через расширение 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 секунд на каждый дочерний процесс).
⚠️ Голоса разрешений теперь имеют область действия сессии — плюс постоянная оговорка об общем bearer. Предпочтите
POST /session/:id/permission/:requestId(Wave 4 / F3): он маршрутизирует в runtime, владеющий названной живой сессией, и никогда не возвращается к основному мосту — новые многопространственные интеграции должны всегда использовать этот маршрут (pre-flightcaps.features.session_permission_vote). Устаревший глобальный для процессаPOST /permission/:requestIdостаётся для клиентов single-workspace до F3: он привязан к основному и404для сессии, принадлежащей другому зарегистрированному runtime — то же тело, что и потерянный голос при политике по умолчаниюfirst-responder— поэтому404там сам по себе не означает, что запрос уже был обработан. Таблица маршрутов: daemon REST API reference. Постоянная оговорка: каждый клиент, держащий bearer-токен, является тем же субъектом безопасности — не запускайте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 на heartbeat (интервал 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), чтобы закрыть пробел с помощью явного дедлайна простоя на уровне приложения: когда запись успешно не сбрасывается в течениеnмс, демон выдает терминальный кадрclient_evictedсreason: 'writer_idle_timeout'и закрывает поток. Флаг по умолчанию выключен для сохранения устаревшего контракта — операторы в сетях, которые поглощают RST, должны выбрать значение значительно выше интервала heartbeat 15 секунд (например,60000–300000), чтобы легитимные простаивающие соединения не удалялись, а действительно зависшие писатели своевременно очищались. Выполните предпроверкуcaps.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независимо от этой настройки. - Демон читает переменную при построении файловой системы рабочего пространства; перезапустите демон после изменения.
Рабочий каталог бесед
Установите корень рабочего каталога для автономных и Live сессий независимо от домашней директории операционной системы:
qwen serve --conversations-dir /persistent/qwen-conversations
# Or configure the launch environment:
QWEN_CONVERSATIONS_DIR=/persistent/qwen-conversations qwen serve
# The same option is available for a detached daemon:
qwen daemon start --conversations-dir /persistent/qwen-conversationsЯвный флаг имеет приоритет над QWEN_CONVERSATIONS_DIR, который может поступать из среды запуска или файла .env уровня пользователя. Встроенные лаунчеры могут выбрать тот же корень с помощью ServeOptions.conversationsDir, который также имеет приоритет над средой. Когда ни то, ни другое не настроено, корнем остаётся ~/Documents/Qwen Code/Conversations. Монтирование постоянного тома по этому пути по умолчанию или изменение домашней директории процесса также являются допустимыми вариантами развёртывания.
Явное значение должно быть непустым, полностью квалифицированным абсолютным путём без символов NUL. Относительные пути, неразвёрнутый ~ и пути Windows, зависящие от текущего диска, отвергаются. Значения не обрезаются, а некорректная конфигурация приводит к сбою запуска вместо использования значений по умолчанию. Валидный явный флаг или параметр API переопределяет невалидное значение переменной окружения. Синтаксис пути проверяется перед прослушиванием; создание каталога и проверки файловой системы остаются ленивыми, поэтому неиспользуемый корень Conversations не создаётся при запуске. Сбой файловой системы при первом использовании не приводит к выбору другого корня.
Выбор фиксируется на всё время жизни демона. Файлы проекта .env и проекта .qwen/.env, верхнеуровневые файлы settings.json env на всех уровнях области действия, перезагрузка переменных окружения и запросы браузера/сессии не могут его изменить. Соответствующая настройка проекта или опция корня для отдельной сессии отсутствует.
Standalone и Live продолжают использовать существующую среду выполнения Conversations. Каждая сессия использует свой детерминированный приватный дочерний каталог conversation-<sha256(normalized session ID)> в качестве рабочего каталога для инструментов и оболочки. Существующие проверки владения, приватных прав POSIX, канонической идентичности, прямого дочернего элемента, симлинков/reparse-точек, аренды записи (writer-lease) и восстановления по-прежнему применяются, включая их платформенно-зависимые ограничения. Windows не получает гарантий владельца/режима POSIX или ACL, а файловые системы без проверяемой идентичности inode по-прежнему не могут обнаружить все замены того же пути. Выбранное дерево Conversations остаётся зарезервированным от обычной регистрации рабочих областей; другие рабочие области сохраняют свои существующие правила.
Рабочий корень и хранилище среды выполнения имеют отдельные обязанности:
| Данные | Расположение |
|---|---|
| Рабочие файлы сессии | Приватные дочерние каталоги сессий под выбранным корнем Conversations. |
| Конфигурация общего корня | Пользовательская конфигурация под выбранным корнем, разделяемая между его сессиями. |
| Транскрипты и sidecar-файлы | Существующее хранилище базы среды выполнения демона, ключом является канонический cwd среды выполнения Conversations. |
| Журналы восстановления после удаления | Стабильное пространство имён <home>/.qwen/conversations/deletions. |
| Обнаружение Live | Стабильный локатор <home>/.qwen/live/daemon.json и, где применимо, его копия в базе среды выполнения демона. |
| Локальное обнаружение демона | daemon/daemon-<port>.json под базой среды выполнения демона; записывает выбранный путь Conversations для перезапуска. |
QWEN_HOME выбирает глобальный каталог конфигурации Qwen. QWEN_RUNTIME_DIR и advanced.runtimeOutputDir выбирают хранилище вывода среды выполнения через свои существующие правила приоритета. Ни один из них не выбирает корень рабочего каталога Conversations. Сохранение только хранилища среды выполнения не сохраняет файлы, созданные в приватных каталогах сессий; настройка только рабочего корня также не сохраняет хранилище среды выполнения или состояние восстановления и обнаружения в домашнем каталоге.
Для перезапуска с тем же корнем сохраните тот же канонический путь, базу среды выполнения и необходимое управляющее состояние. qwen daemon restart сохраняет корень из текущей записи обнаружения, а автоматический перезапуск обновления также сохраняет выбранный корень. Обычное принятие идентичности файловой системы, разрешения, аренды записи и проверки восстановления по-прежнему выполняются; это не гарантия восстановления после любого перезапуска контейнера или хоста. См. Блокировки записи и восстановление Conversations.
Чтобы намеренно изменить корни, завершите ожидающее восстановление после удаления относительно исходного корня, остановите всех писателей, использующих его, затем остановите и запустите демон с новой конфигурацией. Другой канонический корень выбирает другое пространство имён хранения истории/обнаружения и другую общую конфигурацию корня. Никакие рабочие файлы, записи сессий, конфигурация, журналы восстановления или состояние обнаружения автоматически не мигрируют, и видимость старой истории не гарантирована. Не удаляйте доказательства восстановления для принудительного принятия: если восстановление относительно исходного корня не может завершиться, изменение этой опции не является процедурой ремонта.
Хранилище вложений сессий
Вложения сессий (файлы и изображения, загруженные через Web Shell с помощью POST /session/:id/attachments) по умолчанию хранятся во временном каталоге среды выполнения рабочего пространства: <runtimeBaseDir>/tmp/<projectHash>/attachments, с ключом на сессию session-<sessionId>. Операторы, которые хотят, чтобы вложения сохранялись вне временного каталога среды выполнения (например, на выделенном томе), могут переопределить корень:
| Переменная окружения | Значения | По умолчанию | Что делает |
|---|---|---|---|
QWEN_SERVE_SESSION_ATTACHMENTS_ROOT | путь | не задано | Хранит вложения сессий в этом каталоге вместо стандартного временного каталога среды выполнения. Принимает абсолютный путь, путь относительно cwd демона или ~ / ~/…, расширяемый относительно домашнего каталога. |
Область действия и ограничения:
- Односторонняя миграция. Когда переменная окружения установлена, новые вложения записываются только в настроенный корень. Чтения и удаления, которые не попадают в настроенный корень, возвращаются к стандартному временному каталогу среды выполнения, поэтому вложения, загруженные до переключения, остаются читаемыми и удаляемыми, пока стандартный каталог fallback остается записываемым — удаление, чья устаревшая копия не может быть удалена (например, том fallback только для чтения), возвращает ошибку вместо сообщения об успехе. Обратное направление — удаление переменной окружения после записи вложений в настроенный корень — делает эти вложения недоступными; сохраняйте переменную стабильной для данного рабочего пространства.
- Разметка на сессию. Файлы находятся в
<root>/<projectHash>/attachments/session-<sessionId>/в обоих расположениях, где<projectHash>— это тот же хэш рабочего пространства, что используется стандартным временным каталогом среды выполнения; поиск fallback использует ту же разметку сессии в стандартном каталоге. Два рабочих пространства, указывающих на один настроенный корень, остаются изолированными друг от друга. - Очистка при удалении. Когда сессия удаляется, ее каталог вложений удаляется как из настроенного корня, так из стандартного каталога fallback. Архивирование сессии сохраняет ее вложения, чтобы они пережили разархивирование.
- Демон читает переменную при запуске; перезапустите демон после изменения. Каталог должен быть записываемым процессом демона.
Деплой с несколькими сессиями и воркспейсами
Передайте --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 | Клиенту нужен полный активный сохранённый транскрипт. Он возвращает кадры воспроизведения без id в страницах курсора и не вызывает /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. |
Для загрузки и возобновления TypeScript SDK предоставляет статические фабрики в DaemonSessionClient:
import { DaemonClient, DaemonSessionClient } from '@qwen-code/sdk';
const client = new DaemonClient({ baseUrl: 'http://127.0.0.1:4170' });
// Cold reconnect — daemon will replay the bounded snapshot window through SSE.
const session = await DaemonSessionClient.load(client, 'persisted-id');
// Or, if your UI already has the history, skip the replay:
// const session = await DaemonSessionClient.resume(client, 'persisted-id');
for await (const event of session.events()) {
// First the replayed `session_update` frames (load only),
// then live events.
}Выполните предпроверку caps.features.session_load, caps.features.session_resume или caps.features.session_transcript перед вызовом соответствующего маршрута — более старые демоны возвращают 404. unstable_session_resume по-прежнему публикуется как устаревший псевдоним совместимости. Параллельные запросы одного действия для одного и того же id объединяются; гонки между разными действиями (например, load конкурирует с resume) и запуски с пользовательским id, конкурирующие с восстановлением, получают 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. Инициализация сессии, которая прерывается по таймауту, но быстро не завершается, может аналогично вернуть reason: new_session_cleanup_failed, если поздняя очистка неубедительна, или new_session_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. Его курсоры существуют только в течение жизни демона и должны быть перезапущены с первой страницы после перезапуска демона. Обратная первая страница для живой сессии может запустить 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. Обратная страница может расширяться максимум до 3 * limit записей для сохранения границ хода диалога и вызова/результата инструмента. Первый ответ фиксирует размер снимка JSONL и возвращает nextCursor, пока hasMore истинно. Последующие страницы игнорируют добавления после страницы 1, но возвращают 409, если файл удален, усечен, заменен, архивирован или иным образом конфликтует с зафиксированным курсором. Очень большие снимки возвращают 413 transcript_too_large до индексирования, чтобы демон не сканировал неограниченные файлы транскриптов на пути запроса.
Для повторяющейся пагинации через устаревший одиночный маршрут установите --channel-idle-timeout-ms в положительное значение. При значении по умолчанию 0 дочерний процесс ACP простаивающего рабочего пространства — и кэш индекса транскриптов в процессе, который он содержит — уничтожается после каждой страницы, поэтому каждая страница заново запускает дочерний процесс и перестраивает индекс путем повторного сканирования всего зафиксированного префикса (O(snapshotSize) на страницу). Положительный таймаут сохраняет дочерний процесс живым во время обхода курсора, чтобы он повторно использовал свой кэшированный индекс транскриптов и конфигурацию воспроизведения, если только --child-heap-mode admit или enforce не освобождают его для удовлетворения запуска с ограничением емкости в другом рабочем пространстве. В этом случае следующая страница заново запускает дочерний процесс и перестраивает индекс. Прямые страницы и страницы курсора с указанием рабочего пространства остаются локальными для демона; только обратная первая страница для живой сессии пересекает барьер сброса.
Примечание: воспроизведение истории живой сессии ограничено дважды: SSE-кольцом для переподключений через Last-Event-ID и --compacted-replay-max-bytes для снимка, возвращаемого POST /session/:id/load. Длинные истории с многословными ходами могут превысить любую границу. Демон показывает усечение снимка через history_truncated; используйте /transcript, когда вам нужна полная активная сохраненная история.
Модель долговечности
Живые сессии прерываются при перезапуске демона, но постоянные сессии остаются на диске и могут быть перезагружены:
- При сбое дочернего процесса публикуется
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+ перед тем, как полагаться на них:
Блокеры для серьезного последующего использования:
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 /session/:id/permission/:requestId; глобальный для процесса устаревшийPOST /permission/:requestIdсохраняется для клиентов до F3 с одним рабочим пространством).
Последствие: удаленные клиенты в 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: совпадающие записи (workspace runtime, server name, config fingerprint) подсчитываются по ссылкам между сессиями. Если возможность отсутствует, устаревший менеджер для каждой сессии независимо запускает их.
Агентские аналоги (Cursor / Continue / Claude Code / OpenCode / Gemini CLI) все используют многопользовательский режим в одном процессе. qwen-code соответствует им на уровне агента; мост Стадии 1 в этом PR делает ту же архитектуру видимой через HTTP.
Вход в удаленный демон (issue #4175 PR 21)
Когда демон работает на удаленном поде (без общего дисплея с вами), клиент может инициировать OAuth device flow по HTTP. Сам демон опрашивает IdP; ваша задача — просто открыть URL на любом устройстве с браузером.
Для новой настройки используйте Qwen Account with Subscription с подпиской Plus или Pro. Используйте одну и ту же учетную запись Qwen для подписки и авторизации в браузере.
- Начните вход с помощью
POST /auth/subscription/flows?paywallScene=cliи сохранитеidиз ответа. - Опрашивайте
GET /auth/subscription/flows/:id. Когда появитсяauthorizationUrl, откройте этот адрес и завершите вход в браузере. - Когда статус изменится на
ready, вызовитеPOST /workspaces/:workspace/auth/subscriptionдля рабочего пространства, в котором будет использоваться подписка.
Бесплатный тариф Qwen OAuth закрыт 15 апреля 2026 года. Примеры qwen-oauth ниже приведены для справки по прежнему протоколу авторизации устройств.
# 1. Start a flow. The daemon contacts the IdP, returns a code + 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?entry=qwen_code&paywallScene=cli",
# "verificationUriComplete": "https://chat.qwen.ai/...?user_code=USER-1&entry=qwen_code&paywallScene=cli",
# "expiresAt": 1700000600000,
# "intervalMs": 5000,
# "attached": false
# }
# 2. Visit the URL on your phone / laptop, enter the user code.
# 3. Poll for completion (or subscribe to SSE for the auth_device_flow_authorized event):
curl http://127.0.0.1:4170/workspace/auth/device-flow/fa07c61b-… \
-H "Authorization: Bearer $TOKEN"
# → status transitions: 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(`Open ${flow.verificationUri}\nCode: ${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 используют строгий шлюз мутации. Token-less доверенный loopback по умолчанию может запускать и управлять flow напрямую; не-доверенный embed без учетных данных возвращает 401 token_required. Установите управляемый QWEN_SERVER_TOKEN и передайте --require-auth на общих хостах, CI-раннерах, удаленных машинах для разработки или везде, где локальные процессы не являются взаимно доверенными.
Ограничение между демонами. 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; файловый лог является дополнительным, а не заменой.
Неудачное автономное создание добавляет Standalone session creation failed. с проверенной целью sessionId, неудачным phase, безопасным reason, dispatchState и cleanupOutcome. Создание дочернего элемента сохраняет ID дочернего элемента и отдельно записывает проверенный родительский элемент как relatedSessionId. Сопоставляйте строки source_persistence_failed по ID сессии для подтверждения приватного источника или деталей транспорта; write_not_confirmed не указывает на причину, связанную с файловой системой. Завершенный карантин отличается от отката, и unknown не устанавливает успешность очистки. Публичные коды ошибок и поведение повторных попыток остаются неизменными. Эти диагностические данные исключают необработанные причины, промпты и полезные нагрузки источников.
Активный файл ротируется до того, как превысит 10 МиБ. Каждая группа сохраняет четыре архива под archive/, а размер каждой записи файла ограничен 256 КиБ. Очередь в памяти принимает не более 4 МиБ незавершенной полезной нагрузки файлов. Давление в очереди, сбои ротации или ошибки файловой системы могут привести к потере копий файлов; GET /daemon/status?detail=full предоставляет информацию о состоянии логгера, проблемах и счетчиках потерянных записей/байт.
Только один демон может владеть стабильной группой в пространстве имен логов. Параллельно запущенный демон пишет в debug/daemon/runs/run-<runId>/daemon.log; приветственный баннер при запуске и полный статус содержат авторитетный путь. runs/recent-fallback является поисковым указателем (best-effort) на последнюю резервную группу и может указывать на ту, которая все еще активна. Здоровое пространство имен сходится примерно к 100 МиБ: около 50 МиБ для стабильной плюс одна неактивная резервная группа. Активные или еще не устаревшие резервные группы сохраняются, поэтому параллельные демоны или штормы аварийных перезапусков могут временно использовать больше места.
Один каталог среды выполнения представляет собой одно пространство имен владения и хранения. Используйте различные значения 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-сервера во время выполнения
Строгий шлюз (требуется основной доверенный loopback или учетные данные оператора). Немедленно подключает сервер через активный McpClientManager и обнаруживает его инструменты.
Запрос:
{
"name": "my-server",
"config": {
"command": "npx",
"args": ["-y", "@my-org/mcp-server"]
}
}name должен состоять из буквенно-цифровых символов плюс _ и - (максимум 256 символов). config представляет собой тот же объект конфигурации сервера MCP, который используется в записях settings.json mcpServers (поля, зависящие от транспорта: 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 | Не-доверенный запрос без токена отклонен строгим шлюзом |
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 | Не-доверенный запрос без токена отклонен строгим шлюзом |
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) для контроля сервис-менеджером и запуска после перезагрузки.
- Создаете клиент? См. краткое руководство по DaemonClient для TypeScript и справочник по протоколу HTTP.
- Читаете исходный код? Код моста находится в
packages/cli/src/serve/; SDK-клиент — вpackages/sdk-typescript/src/daemon/. - Отслеживаете дорожную карту? Прогресс на этапах Stage 1.5 / Stage 2 отслеживается в issue #3803 .