Среда выполнения Serve
Обзор
packages/cli/src/serve/ — это загрузочный слой для qwen serve. Он преобразует флаги CLI в ServeOptions, проверяет конфигурацию запуска, собирает приложение Express, подключает middleware, регистрирует маршруты, предоставляет провайдеры preflight/статуса для хоста демона, поддерживает кольцо аудита разрешений и управляет двухфазным процессом корректного завершения работы (graceful shutdown). Работа с HTTP находится в этом слое; работа с ACP находится на уровень ниже в @qwen-code/acp-bridge (см. 03-acp-bridge.md).
Обязанности
- Парсинг и валидация
ServeOptions: адрес прослушивания, аутентификация, рабочее пространство (workspace), лимиты сессий/подключений, бюджет/пул MCP, CORS, таймауты простоя для промптов/SSE/сессий, rate limit и связанные переключатели. - Канонизация основного рабочего пространства ровно один раз, а также канонизация каждого повторного
--workspaceперед регистрацией сессионных сред выполнения. Основная каноническая форма используется в/capabilities.workspaceCwd, fallback дляPOST /sessionи в основном bridge. - Разрешение bearer:
--token, затемQWEN_SERVER_TOKEN, затем — если ни один источник не задан и запрошенный--hostnameне является loopback (буквальныйlocalhostразрешается первым) — генерируется эфемерный 128-битный base64url bearer (22 символа), выводимый один раз при запуске. Loopback-записи никогда не генерируют токен и сохраняют доверенный режим без токена, если не установлен--require-auth. Генерация зависит от записи, а отказ при загрузке читает разрешённый адрес, что даёт два исключения:localhost, разрешающийся вне loopback, никогда не генерирует токен и загружается только при наличии разрешённого источника токена (иначеRefusing to bind …); а не-буквальное имя, разрешающееся в loopback, генерирует токен, теряя доверенный режим без токена, поэтому его bearer выводит только токен. - Отклонение небезопасных или невалидных конфигураций запуска: привязка не к loopback-интерфейсу без токена,
--require-authбез токена, wildcard или не-loopback HTTP(S)--allow-originбез токена,mcpBudgetMode='enforce'без положительногоmcpClientBudget, несуществующий или не являющийся директорией--workspace, а также невалидные значения таймаутов или rate-limit. - Создание фабрики
WorkspaceFileSystem, издателя аудита разрешений (permission audit publisher),DaemonStatusProviderиacp-bridge. - Сборка приложения Express, подключение middleware (удаление loopback
Origin-> access log -> захват входящего trace-id ->hostAllowlist-> удаление remote same-originOrigin->allowOriginCorsповерх мутабельного allowlist origin -> pre-auth/health-> pre-auth ресурсы Web Shell -> вебхуки каналов ->bearerAuth-> rate limit -> JSON parser -> telemetry -> per-routemutationGate) и монтирование маршрутов для сессий, CRUD-операций с рабочим пространством, файлами, аутентификации через device-flow, голосования за разрешения и ACP HTTP. (Безусловная стенаdenyBrowserOriginCorsсохраняется только в бутстрап-приложенииrun-qwen-serve.ts.) - Привязка порта прослушивания и регистрация обработчиков сигналов.
- Запуск двухфазного завершения работы по SIGINT/SIGTERM; принудительный выход при получении второго сигнала.
Архитектура
Точка входа: runQwenServe(opts, deps) в packages/cli/src/serve/run-qwen-serve.ts. Возвращает RunHandle ({ url, port, close, ... }).
Фабрика приложения: createServeApp(opts, getPort, deps) в packages/cli/src/serve/server.ts. Собирает Express Application. Прямые встраивающие модули и тесты вызывают её без обёртки бутстрапа.
Реестр возможностей: SERVE_CAPABILITY_REGISTRY в packages/cli/src/serve/capabilities.ts. Каждый тег имеет версию since и опциональные modes. Условные теги исключаются, если их деплойментный или рантаймный предикат ложен; реестр и карта предикатов являются источником истины. См. 11-capabilities-versioning.md.
Middleware (packages/cli/src/serve/auth.ts, server.ts, server/self-origin.ts и server/access-log.ts):
| Middleware, в порядке регистрации | Назначение | Примечания |
|---|---|---|
installSelfOriginStripMiddleware | Удаляет заголовок Origin, совпадающий с loopback same-origin значением для привязанного порта, чтобы собственные POST/fetch-вызовы loopback Web Shell никогда не считались cross-origin. | Первый middleware в runtime-приложении. Сопоставляет обе схемы и привязанный loopback-хост, опускает порты по умолчанию для схемы согласно RFC 7230 §5.4. |
| Access-log middleware | Записывает метод, путь, статус, durationMs, sessionId и clientId в DaemonLogger при завершении запроса. | Регистрируется перед каждым гейтом, поэтому 401 / 403 / 429 короткие замыкания также логируются. Исключён по точному пути: GET /health и POST */heartbeat, чтобы эти зонды жизнеспособности никогда не логировались — включая случаи, когда гейт ниже их отклоняет (HEAD /health и GET /health/ логируются как обычный запрос). Успешные потоки GET */events также исключаются. Ограничен burst-лимитом 60 строк с пополнением 2/с; переполнение сворачивается в предупреждение access logs suppressed. Отказы pre-authentication гейта (Host allowlist, CORS-стена, проверка same-origin credential для remote) используют отдельный бюджет 30/1 с, чтобы credential-less флуд этих отказов не мог лишить оператора собственных строк за этим предупреждением; bearerAuth 401 (флуд без Origin) не помечаются и по-прежнему расходуют бюджет оператора — без изменений с момента изменения порядка. |
| Захват входящего trace-id | Захватывает trace id traceparent вызывающей стороны до того, как любой гейт сможет выполнить короткое замыкание. | Позволяет access log связать строку 401 / 429 / 400 / 404 с трассировкой вызывающей стороны в деплойментах с выключенной телеметрией, где это единственная такая связь. |
hostAllowlist(bind, getPort) | На loopback проверяет, что Host принадлежит localhost, 127.0.0.1, [::1], host.docker.internal или точному привязанному loopback-адресу, а также фактическому порту; формы без порта принимаются на портах 80 и 443. | Защита от DNS rebinding; поэтому также покрывает pre-auth маршруты /health ниже. Сравнение без учёта регистра, кэшируется для каждого порта. Намеренно не работает на не-loopback привязках, где bearer является слоем аутентификации. LAN-листенер Local Control всегда применяет проверку Host по рекламируемому authority, независимо от основной привязки. |
installRemoteSelfOriginMiddleware | На не-loopback основном слушателе с токеном выполняет bearer-аутентификацию запроса, чей Origin равен схеме сокета плюс нормализованный Host, затем удаляет этот Origin. | Это позволяет same-origin HTTP-мутациям встроенного Web Shell проходить без --allow-origin. Не работает на loopback привязках и когда токен не настроен. Pre-auth маршруты Web Shell (/, //, /assets*, /mcp-app-sandbox, точные навигации /session/:id) пропускают проверку credential. Forwarded-заголовки никогда не используются. |
allowOriginCors | Всегда устанавливается на runtime-приложение поверх MutableOriginAllowlist: записи --allow-origin <pattern> засевают его, Local Control добавляет LAN origin, пока включён; несовпадающие origin получают 403 deny envelope. | См. 12-auth-security.md. Его отказы логируются access log выше, кроме исключений health/heartbeat. |
Pre-auth /health | Маршрут жизнеспособности, регистрируемый перед bearerAuth на обычной loopback привязке. | Отключается при --require-auth и никогда не регистрируется pre-auth на не-loopback привязке; в этих случаях /health регистрируется после bearerAuth. Local Control слушатель аутентифицирует собственный /health даже в pre-auth позиции. |
| Статические ресурсы Web Shell и песочница MCP App | /, /assets*, /mcp-app-sandbox и точные навигации /session/:id, монтируемые перед bearerAuth. | Браузер не может добавить Authorization к навигации или подресурсу <script src>, а статическая оболочка не содержит секретов. Fallback SPA deep-link регистрируется после всех API-маршрутов. --no-web отключает. |
| Маршруты вебхуков каналов | POST /channels/:channelName/webhooks/:source, регистрируется перед bearerAuth. | Аутентифицируется собственным x-qwen-webhook-secret; ротация bearer токена демона не ротирует секреты вебхуков. |
bearerAuth(token) | SHA-256 плюс timingSafeEqual — константное сравнение bearer. | Открытый пропуск, когда токен не настроен (loopback dev по умолчанию). Схема Bearer без учёта регистра. |
| Rate-limit middleware | Опциональный token bucket для каждого уровня для маршрутов промптов, мутаций и чтения. | Регистрируется после bearerAuth и до парсинга JSON; возвращает 429 до парсинга, когда bucket исчерпан. |
express.json({ limit: '10mb' }) | Парсинг тела JSON. | Ошибки парсинга возвращают 400. |
daemonTelemetryMiddleware | Оборачивает классифицированные запросы к daemon API, достигшие этой точки, в OpenTelemetry span через withDaemonRequestSpan. | Атрибуты включают канонический маршрут, хеш разрешённого рабочего пространства, sessionId, clientId и код статуса. Более ранние отказы auth, rate-limit и body-parser находятся за пределами этой span-границы. |
createMutationGate (per-route) | Гейт на уровне маршрута (opt-in) для мутаций, требующих полномочий оператора. Допускаются запросы от доверенного основного слушателя, запросы с bearer-аутентификацией и сопряжённые запросы Local Control. | Запрос от основного слушателя без токена, достигший строгого гейта без доверенного loopback-authority, возвращает 401 { code: 'token_required' }. Отсутствующие или невалидные настроенные учётные данные отклоняются ранее bearer middleware с обычным 401 Unauthorized. Не глобальный app.use; маршруты вызывают mutate({ strict: true }) по мере необходимости. |
Подсистемы:
| Путь | Роль |
|---|---|
serve/fs/ | Фабрика WorkspaceFileSystem плюс policy.ts (проверки размера/доверия/бинарности), paths.ts (канонизация, resolveWithin, отклонение symlink), audit.ts и типизированные значения FsError. |
serve/routes/workspace-file-read.ts, workspace-file-write.ts | HTTP-обработчики для GET /file, GET /file/bytes, POST /file/write и POST /file/edit. |
serve/workspace-memory.ts | GET/POST /workspace/memory (CRUD QWEN.md). |
serve/workspace-agents.ts | GET/POST/DELETE /workspace/agents (CRUD субагентов). |
serve/daemon-status-provider.ts | Снимок окружения и preflight-ячейки хоста демона: версия Node, точка входа CLI, статистика рабочего пространства, ripgrep, git, npm. |
serve/permission-audit.ts | PermissionAuditRing (FIFO на 512 записей) и createPermissionAuditPublisher. |
serve/auth/device-flow.ts, qwen-device-flow-provider.ts | Маршруты OAuth device-flow. См. 12-auth-security.md. |
serve/daemon-logger.ts | Структурированные файловые логи DaemonLogger. См. 19-observability.md. |
serve/debug-mode.ts | Общий предикат isServeDebugMode(), управляющий подробным контекстом ошибок в HTTP-ответах. |
serve/acp-http/ | Транспорт ACP Streamable HTTP (RFD #721), монтируется в /acp. Семь файлов реализуют JSON-RPC POST, SSE GET, DELETE teardown и общее использование bridge параллельно с REST-поверхностью. |
serve/web-shell-static.ts, serve/web-shell-resolver.ts | Поиск и монтирование собранных ресурсов Web Shell (браузерный UI демона) в /, /assets и /session/:id, а также fallback SPA deep-link, регистрируемый после всех API-маршрутов. Монтируется до bearerAuth в любом режиме запуска, поскольку браузер не может добавить Authorization к навигации или подресурсу. API-вызовы следуют обычной политике authority: настроенные токены контролируют обычные API-маршруты, кроме loopback /health, если не установлен --require-auth, а входные вебхуки каналов всегда используют свой собственный общий секрет, а основной доверенный loopback-слушатель без токена имеет полный доступ оператора. Деградирует до API-only, если ресурсы отсутствуют; --no-web отключает. |
Импорты из пакета ACP bridge:
- Примитивы event-bus импортируются из
@qwen-code/acp-bridge/eventBus. - Примитивы статуса импортируются из
@qwen-code/acp-bridge/status. serve/acp-session-bridge.tsостаётся как локальный для CLI фасад совместимости для более широкой поверхности bridge.
Поток выполнения
Последовательность загрузки
До того как runQwenServe() запускает эту последовательность, режим CLI --open-with-auth проверяет возможность работы с loopback/Web Shell и заполняет ServeOptions.token выбранным настроенным токеном или 32 случайными байтами (256-битный bearer) в кодировке base64url, если выбор пуст. Это сгенерированное значение является обычным настроенным токеном для всех шагов ниже — именно поэтому --require-auth --open-with-auth загружается — и это отдельный генератор от эфемерного bearer для не-loopback в шаге 1. Прямые встраивающие модули, вызывающие createServeApp самостоятельно, никогда не генерируют токен.
- Разрешение токена из
opts.tokenилиQWEN_SERVER_TOKENс обрезкой, чтобы завершающий символ новой строки отcat token.txtне мог скрыто нарушить сравнение bearer. Когда запрошенный--hostnameне является loopback (буквальныйlocalhostразрешается первым) и ни один источник не присутствует, генерируется эфемерный 128-битный (16-байтный) bearer как 22 символа base64url вместо отказа; он выводится один раз удалённым быстрым стартом послеlisten()и ротируется при каждом перезапуске. Loopback-записи никогда не генерируют, поэтому сохраняют доверенный режим без токена. Явно пустой источник (--token ''илиQWEN_SERVER_TOKENустановленный в пустое или только пробельное значение) не считается «отсутствующим», поэтому всегда подавляет генерацию — но пустота определяет разрешённый токен только в одном направлении: пустой--tokenзатеняет установленное значение env и разрешается в отсутствие токена, тогда как пустое env разрешается в отсутствие токена только когда--tokenне передан (непустой--tokenвсё равно побеждает); в обоих случаях не-loopback привязка без разрешённого токена всё равно не проходит проверки ниже. - Защита от опечаток в hostname:
--hostname localhost:4170вызывает ошибку и предлагает использовать--port. - Preflight аутентификации: не-loopback привязка без разрешённого токена отклоняется — это достижимо через явно пустой источник или через привязку
localhost, чьё одноразовое разрешение попадает вне loopback (генерация зависит от записи, поэтому там ничего не было сгенерировано);--require-authотклоняет привязку без токена, что после шага 1 означает привязку к loopback без настроенного источника. Защитные механизмы wildcard и не-loopback HTTP(S)--allow-originчитают тот же разрешённый токен, поэтому на не-loopback привязке сгенерированный bearer удовлетворяет им, и эти отказы тоже только для loopback. - Валидация рабочего пространства: абсолютный путь, существует, является директорией.
EACCES/EPERMоборачиваются, чтобы указать на флаг. - Канонизация рабочего пространства:
canonicalizeWorkspace(rawWorkspace)один раз запускаетrealpathSync.nativeи передаёт результат в/capabilities, fallback дляPOST /sessionи в bridge. - Валидация бюджета MCP: положительное целое число;
enforceтребует указания бюджета. - Вывод переключателя пула MCP: родительская переменная окружения
QWEN_SERVE_NO_MCP_POOL=1устанавливаетmcpPoolActive=false, поэтому возможности честно исключаютmcp_workspace_poolиmcp_pool_restart. - Валидация CORS / таймаутов / rate-limit: wildcard и не-loopback HTTP(S) значения
--allow-originтребуют разрешённый токен (см. шаг 3, почему эти отказы только для loopback); значения таймаутов простоя промптов, writer, канала, сессии, reaper и окна rate-limit приводят к быстрому сбою при невалидности. childEnvOverridesдля каждого handle: передачаQWEN_SERVE_MCP_CLIENT_BUDGETиQWEN_SERVE_MCP_BUDGET_MODEдочернему процессу ACP черезBridgeOptions.childEnvOverridesвместо мутацииprocess.env.- Однократная загрузка
settings.json: чтениеcontext.fileName,policy.permissionStrategyиpolicy.consensusQuorum. Повреждённые файлы откатываются к значениям по умолчанию.validatePolicyConfig()проверяетpolicy.*на соответствиеSERVE_CAPABILITY_REGISTRY.permission_mediation.modes; неизвестные стратегии или неположительныйconsensusQuorumвыбрасываютInvalidPolicyConfigError. Кворум, установленный для стратегии, отличной отconsensus, логирует предупреждение в stderr. - Выделение
PermissionAuditRing(512 записей). - Сборка
fsFactory:runQwenServeпо умолчанию используетtrusted: true; прямые вызовыcreateServeAppпо умолчанию используютtrusted: falseи выводят одно предупреждение. createHttpAcpBridge, см.03-acp-bridge.md.createServeAppсобирает Express.- Создание и привязка HTTP(S)-сервера к жизненному циклу перед прослушиванием, затем вызов
server.listen(port, hostname)и разрешение фактическогоgetPort()для allowlist хоста. Владение Conversations не может начаться, пока этот слушатель и оставшиеся хост-гейты запуска не будут готовы. - Регистрация обработчиков SIGINT / SIGTERM для корректного завершения работы через общий жизненный цикл приложения.
Корректное завершение работы
- Закрытие допуска и начало всех дренирований по первому сигналу:
- Удаление реестра device-flow и отмена ожидающих потоков.
bridge.shutdown()помечает каждый каналisDying = true, отправляет корректное закрытие в stdin каждого дочернего процесса ACP, ждётKILL_HARD_DEADLINE_MS(10 с) для каждого канала, затем при необходимости вызываетchannel.kill().
- Закрытие слушателя, пока выполняются дренирования приложения и хоста:
server.close()прекращает приём новых соединений и позволяет завершиться выполняющимся запросам.SHUTDOWN_FORCE_CLOSE_MS(5 с) инициируетserver.closeAllConnections().- Второй дедлайн в 2 с при необходимости снова эскалирует завершение.
- Освобождение владения Conversations только после положительного доказательства завершения от слушателя, локальной работы приложения, работы хоста, очистки Live discovery и дренирований среды выполнения. Любое неполное доказательство отклоняет завершение вместо небезопасной передачи.
- Второй сигнал во время выхода:
bridge.killAllSync()+process.exit(1), чтобы избежать блокировки выхода демона осиротевшими дочерними процессами.
Состояние и жизненный цикл
RunHandle предоставляет:
url: разрешённый URL прослушивания, после разрешения эфемерного порта.port: фактический порт, включая разрешение0.close(): программное завершение работы для встраивающих модулей и тестов.
Прямой вызов createServeApp возвращает только Application. Встраивающему модулю, которому нужны Live/Conversations, необходимо создать фактический Node-сервер, вызвать getServeAppLifecycle(app).bindServer(server) перед первым listen() и ожидать lifecycle.close() во время завершения работы. Без привязки обычные маршруты остаются доступными, но Live/Conversations завершаются с ошибкой (fail closed). Вызов обычного server.close() запускает очистку на основе событий, но встраивающий модуль всё равно должен ожидать lifecycle.close(), чтобы получить информацию о сбоях дренирования или освобождения владения.
Зависимости
Используется serve/ (upstream) | Использует serve/ (downstream) |
|---|---|
@qwen-code/acp-bridge: bridge, event bus, типы status | Обработчик подкоманды serve CLI qwen |
packages/core: getAllMemoryFilenames, Config, WorkspaceContext | Прямые встраивающие модули, тесты |
ACP SDK (@agentclientprotocol/sdk): PROTOCOL_VERSION, ClientSideConnection через bridge | |
Express + body-parser, node:crypto, node:fs, node:path |
Конфигурация
| Источник | Ключ | Эффект |
|---|---|---|
| Env | QWEN_SERVER_TOKEN | Bearer-токен после обрезки. |
| Env | QWEN_SERVE_NO_MCP_POOL=1 | Принудительно устанавливает mcpPoolActive=false. |
| Env дочернего процесса ACP | QWEN_SERVE_MCP_CLIENT_BUDGET / QWEN_SERVE_MCP_BUDGET_MODE | Генерируется из --mcp-client-budget / --mcp-budget-mode и передаётся через childEnvOverrides. |
| Env | QWEN_SERVE_PROMPT_DEADLINE_MS / QWEN_SERVE_WRITER_IDLE_TIMEOUT_MS | Таймауты простоя промпта / SSE по умолчанию. |
| Env | QWEN_SERVE_RATE_LIMIT* | Переключатель rate-limit, лимиты промптов/мутаций/чтения и окно по умолчанию. |
| Env | QWEN_SERVE_DEBUG=1 | Подробные логи stderr. См. 19-observability.md. |
| Флаги | --hostname, --port | Привязка прослушивания. |
| Флаги | --token, --require-auth, --enable-session-shell | Bearer-токен, усиление аутентификации на loopback и явный переключатель выполнения shell. |
| CLI-флаги | --open-with-auth | Запуск loopback Web Shell по умолчанию в выключенном состоянии, повторно использующий или генерирующий bearer-токен на время жизни процесса до запуска. |
| Флаг | --workspace | Переопределяет process.cwd(); повторите для регистрации дополнительных изолированных сред выполнения рабочего пространства. |
| Флаги | --max-sessions, --max-pending-prompts-per-session, --max-connections, --event-ring-size | Лимиты Bridge / Express. |
| Флаги | --mcp-client-budget=N, --mcp-budget-mode={off,warn,enforce} | Передаётся дочернему процессу ACP. |
| Флаги | --allow-origin, --allow-private-auth-base-url | Allowlist CORS для браузера и переключатель установки провайдера аутентификации localhost/private. |
| Флаг | --web / --no-web | Обслуживание или пропуск Web Shell UI в корне демона (по умолчанию обслуживается). --no-web оставляет демон в режиме API-only. |
| Флаги | --prompt-deadline-ms, --writer-idle-timeout-ms, --channel-idle-timeout-ms, --initialize-timeout-ms | Управление таймаутом промпта, SSE writer, жизненным циклом простоя дочернего процесса ACP и таймаутом запросов дочернего процесса ACP. |
| Флаги | --session-reap-interval-ms, --session-idle-timeout-ms | Управление очисткой (reaping) отключённых сессий. |
| Флаги | --rate-limit* | HTTP rate limit для каждого уровня. |
settings.json | policy.permissionStrategy, policy.consensusQuorum | Политика и кворум MultiClientPermissionMediator. |
settings.json | context.fileName | Имя файла памяти рабочего пространства, передаваемое в /workspace/init через contextFilename сервиса рабочего пространства. |
См. 17-configuration.md для сводной документации.
Важные замечания и известные ограничения
- Прямой вызов
createServeAppбезdeps.fsFactoryилиdeps.bridgeпо умолчанию устанавливаетtrusted: false; ACPwriteTextFileна стороне агента отклоняет запрос с ошибкойuntrusted_workspace. Предупреждение выводится один раз. - Runtime-приложение запускает
allowOriginCorsповерх мутабельного allowlist; несовпадающие значенияOriginполучают 403 deny envelope (безусловная стенаdenyBrowserOriginCorsсохраняется только в бутстрап-приложении). Loopback Web Shell работает, потому что другой middleware сначала удаляет совпадающие значения same-origin для loopback; на не-loopback привязке с токеном same-origin XHR оболочки bearer-аутентифицированы и ихOriginудаляется перед стеной, поэтому им не нужен--allow-origin. Три случая всё ещё требуют записи в allowlist: WebSocket-обновления (terminal, voice), TLS-терминирующий фронт-прокси, чейhttpsorigin никогда не совпадает с открытым сокетом, и любой plain-HTTP посредник, переписывающий заголовокHost— nginx по умолчанию сproxy_set_header Host $proxy_hostи k8s Ingress оба делают это. Только трансляция порта не требует ничего на не-loopback привязке (docker -p 8080:4170): проверка сравниваетOriginтолько с нормализованным пересланнымHostи никогда не обращается к порту прослушивания (только порты по умолчанию для схемы:80/:443удаляются; порт не по умолчанию должен сохраняться в нём дословно). На привязке loopback по умолчанию это не так: Host allowlist от DNS-rebinding принимает только собственный порт демона, поэтому туннель с трансляцией порта (ssh -L 8080:localhost:4170) отклоняется с403 Invalid Host headerдля каждого запроса, включая документ оболочки, и--allow-originне может это переопределить — перенаправляйте тот же порт или привязывайтесь к не-loopback. Решение для случаев WebSocket и TLS-терминирующего прокси —--allow-origin <origin>; посредник с переписыванием Host может быть настроен на пересылкуHostдословно — что не может помочь, когда TLS терминируется на прокси, потому что схема читается из собственного сокета демона. - Порядок body-parser: маршруты, использующие
mutate({ strict: true }), возвращают 401 только послеexpress.json(). В худшем случае это--max-connections × express.json({limit: '10mb'}), что может привести к выделению до 2,5 ГБ временной памяти при максимальной нагрузке на loopback-листенер; этот компромисс является осознанным. - Несколько демонов в одном процессе должны использовать
childEnvOverridesдля каждого handle; мутацияprocess.envприводит к гонкам, посколькуdefaultSpawnChannelFactoryделает снимок окружения в момент spawn.
Ссылки
packages/cli/src/serve/run-qwen-serve.ts(бутстрап, валидация загрузки, корректное завершение работы)packages/cli/src/serve/server.ts(createServeApp(), сборка middleware и маршрутов)packages/cli/src/serve/auth.ts(CORS, allowlist Host, bearer-аутентификация, mutation gate)packages/cli/src/serve/rate-limit.ts(HTTP rate limit для каждого уровня)packages/cli/src/serve/capabilities.ts(реестр возможностей и условное объявление)packages/cli/src/serve/types.ts(ServeOptions,CapabilitiesEnvelope)packages/cli/src/serve/daemon-status-provider.tspackages/cli/src/serve/permission-audit.ts- Issues: #3803 , #4175