Skip to Content
Guia do DesenvolvedorModo Daemon (Aprofundamento para Desenvolvedores)Serve Runtime

Serve Runtime

Visão geral

packages/cli/src/serve/ é a camada de inicialização do qwen serve. Ele traduz as flags da CLI em ServeOptions, valida a configuração de inicialização, constrói o app Express, conecta os middlewares, registra as rotas, expõe os provedores de preflight/status do daemon-host, mantém o anel de auditoria de permissões e é responsável pela sequência de desligamento gracioso em duas fases. O trabalho voltado para HTTP fica nesta camada; o trabalho voltado para ACP fica uma camada abaixo em @qwen-code/acp-bridge (consulte 03-acp-bridge.md).

Responsabilidades

  • Analisar e validar ServeOptions: endereço de escuta, autenticação, workspace, limites de sessão/conexão, orçamento/pool de MCP, CORS, timeouts de inatividade de prompt/SSE/sessão, limite de taxa (rate limit) e toggles relacionados.
  • Canonicalizar o workspace primário exatamente uma vez e canonicalizar cada --workspace repetido antes de registrar os runtimes de sessão. A forma canônica primária é compartilhada por /capabilities.workspaceCwd, o fallback de POST /session e a bridge primária.
  • Resolver o bearer: --token, depois QWEN_SERVER_TOKEN, e então — quando nenhuma fonte estiver presente e o --hostname solicitado for fora do loopback (o literal localhost resolvido uma vez primeiro) — um bearer efêmero gerado de 128 bits em base64url (22 caracteres) impresso uma vez na inicialização. Grafias de loopback nunca geram e mantêm o modo confiável sem token, a menos que --require-auth esteja definido. A geração baseia-se na grafia enquanto a recusa de inicialização lê o endereço resolvido, o que gera duas exceções: um localhost que resolve fora do loopback nunca gera e inicializa apenas quando uma fonte de token foi resolvida (Refusing to bind … caso contrário); e um nome não literal que resolve para loopback gera, perdendo o modo confiável sem token para que seu bearer imprima apenas o token.
  • Rejeitar configurações de inicialização inseguras ou inválidas: um bind fora do loopback cuja fonte de token está explicitamente vazia, --require-auth em um bind de loopback sem token, --allow-origin HTTP(S) wildcard ou fora do loopback em um bind de loopback sem token, mcpBudgetMode='enforce' sem um mcpClientBudget positivo, um --workspace inexistente ou que não seja um diretório, e valores inválidos de timeout ou rate limit.
  • Construir a factory WorkspaceFileSystem, o publisher de auditoria de permissões, o DaemonStatusProvider e a acp-bridge.
  • Construir o app Express, conectar os middlewares (strip de Origin no loopback -> access log -> captura de trace-id de entrada -> hostAllowlist -> strip de Origin de mesma origem remota -> allowOriginCors sobre a allowlist mutável de origem -> /health pré-autenticação -> assets estáticos do Web Shell pré-autenticação -> webhooks de canal -> bearerAuth -> rate limit -> parser JSON -> telemetria -> mutationGate por rota) e montar as rotas de sessão, CRUD de workspace, arquivo, autenticação device-flow, votação de permissão e HTTP ACP. (A barreira incondicional denyBrowserOriginCors permanece apenas no app de bootstrap, run-qwen-serve.ts.)
  • Vincular a porta de escuta e registrar os manipuladores de sinais (signal handlers).
  • Executar o desligamento em duas fases no SIGINT/SIGTERM; forçar a saída (force-exit) em um segundo sinal.

Arquitetura

Entrada: runQwenServe(opts, deps) em packages/cli/src/serve/run-qwen-serve.ts. Retorna um RunHandle ({ url, port, close, ... }).

Factory do app: createServeApp(opts, getPort, deps) em packages/cli/src/serve/server.ts. Constrói o Application do Express. Embedders diretos e testes o chamam sem o wrapper de bootstrap.

Registro de capacidades: SERVE_CAPABILITY_REGISTRY em packages/cli/src/serve/capabilities.ts. Cada tag tem uma versão since e modes opcionais. Tags condicionais são omitidas quando seu predicado de deployment ou runtime é falso; o registro e o mapa de predicados são a fonte da verdade. Consulte 11-capabilities-versioning.md.

Middleware (packages/cli/src/serve/auth.ts, server.ts, server/self-origin.ts e server/access-log.ts):

Middleware, em ordem de registroPropósitoNotas
installSelfOriginStripMiddlewareRemove um header Origin que corresponde a um valor de mesma origem do loopback para a porta vinculada, para que as chamadas POST/fetch do próprio Web Shell no loopback nunca sejam tratadas como cross-origin.Primeiro middleware no app de runtime. Corresponde a ambos os schemes e ao host de loopback vinculado, e omite portas padrão do scheme conforme RFC 7230 §5.4.
Access-log middlewareRegistra método, caminho, status, durationMs, sessionId e clientId no DaemonLogger quando uma requisição termina.Registrado antes de cada gate, para que os short-circuits 401 / 403 / 429 também sejam registrados. Isento por caminho exato: GET /health e POST */heartbeat, para que essas sondas de atividade nunca sejam registradas — inclusive quando um gate abaixo os rejeita (HEAD /health e GET /health/ são registrados como qualquer requisição). Streams GET */events bem-sucedidos também são omitidos. Limitado a burst de 60 linhas com preenchimento a 2/s; o overflow coalesce em um aviso access logs suppressed. Rejeições do gate pré-autenticação (allowlist de Host, a barreira CORS, a verificação de credencial de mesma origem remota) usam um orçamento separado de 30/1s, para que uma inundação sem credenciais dessas rejeições não possa eliminar as linhas do próprio operador atrás desse aviso; bearerAuth 401s (uma inundação sem Origin) não são marcados e ainda consomem o orçamento do operador — inalterado desde antes da reordenação.
Captura de trace-id de entradaCaptura o trace id traceparent do chamador antes que qualquer gate possa fazer short-circuit.Permite que o access log junte uma linha 401 / 429 / 400 / 404 ao trace do chamador em deployments com telemetria desativada, onde é o único vínculo desse tipo.
hostAllowlist(bind, getPort)No loopback, valida se o Host pertence a localhost, 127.0.0.1, [::1], host.docker.internal ou o endereço de loopback vinculado exato, além da porta real; formas sem porta são aceitas nas portas 80 e 443.Defesa contra DNS rebinding; portanto, também cobre as rotas /health pré-autenticação abaixo dele. A comparação não diferencia maiúsculas de minúsculas e é armazenada em cache por porta. Um no-op deliberado em binds fora do loopback, onde o bearer é a camada de autenticação. O listener LAN do Local Control sempre aplica sua verificação de Host de autoridade anunciada, independentemente do bind primário.
installRemoteSelfOriginMiddlewareEm um listener primário fora do loopback que tem um token, autentica por bearer uma requisição cujo Origin é igual ao scheme do socket direto mais o Host normalizado, e então remove esse Origin.É isso que permite as mutações HTTP de mesma origem do Web Shell embutido sem --allow-origin. No-op em binds de loopback e quando nenhum token está configurado. Rotas pré-autenticação do Web Shell (/, //, /assets*, /mcp-app-sandbox, navegações de documento exatas /session/:id) pulam a verificação de credencial. Headers encaminhados nunca são consultados.
allowOriginCorsSempre instalado no app de runtime sobre uma MutableOriginAllowlist: entradas --allow-origin <pattern> a inicializam, o Local Control adiciona a origem LAN enquanto habilitado; origens sem match recebem o envelope de negação 403.Consulte 12-auth-security.md. Suas rejeições são registradas pelo access log acima, exceto nas isenções de health/heartbeat.
/health pré-autenticaçãoRota de atividade registrada antes do bearerAuth em um bind de loopback comum.Removida sob --require-auth, e nunca registrada pré-autenticação em um bind fora do loopback; esses casos registram /health após o bearerAuth em vez disso. Um listener do Local Control autentica seu próprio /health mesmo na posição pré-autenticação.
Assets estáticos do Web Shell e sandbox MCP App/, /assets*, /mcp-app-sandbox e navegações de documento exatas /session/:id, montados antes do bearerAuth.Um navegador não pode anexar Authorization a uma navegação ou sub-recurso <script src>, e o shell estático não carrega segredos. O fallback de deep-link do SPA é registrado após todas as rotas de API. --no-web desativa.
Rotas de webhook de canalPOST /channels/:channelName/webhooks/:source, registrada antes do bearerAuth.Autentica com seu próprio x-qwen-webhook-secret; rotacionar o bearer do daemon não rotaciona os segredos de webhook.
bearerAuth(token)SHA-256 mais comparação de bearer em tempo constante com timingSafeEqual.Passthrough aberto quando nenhum token está configurado (padrão de dev no loopback). O esquema Bearer não diferencia maiúsculas de minúsculas.
Rate-limit middlewareToken bucket opcional por tier para rotas de prompt, mutação e leitura.Registrado após o bearerAuth e antes da análise do JSON, para que apenas requisições autenticadas sejam contadas; retorna 429 antes da análise quando um bucket é esgotado. Rotas de webhook usam seu próprio gate de segredo compartilhado em vez disso.
express.json({ limit: '10mb' })Análise de corpo JSON.Erros de análise retornam 400.
daemonTelemetryMiddlewareEnvolve as requisições classificadas da API do daemon que chegam até aqui em um span do OpenTelemetry através do withDaemonRequestSpan.Os atributos incluem rota canônica, hash do workspace resolvido, sessionId, clientId e código de status. Rejeições anteriores de autenticação, rate limit e body-parser estão fora desse limite de span.
createMutationGate (por rota)Gate de opt-in no nível da rota para mutações que exigem autoridade do operador. Requisições confiáveis do listener primário, requisições autenticadas por bearer e requisições pareadas do Local Control se qualificam.Uma requisição primária sem token que alcança o gate estrito sem autoridade de loopback confiável retorna 401 { code: 'token_required' }. Credenciais configuradas ausentes ou inválidas são rejeitadas antes pelo middleware bearer com 401 Unauthorized simples. Não é um app.use global; as rotas chamam mutate({ strict: true }) conforme necessário.

O app de bootstrap que responde requisições durante a janela fria (createBootstrapServeApp em run-qwen-serve.ts) executa uma cadeia mais curta nesta ordem: strip de Origin no loopback -> hostAllowlist -> strip de Origin de mesma origem remota -> a barreira CORS (allowOriginCors quando --allow-origin está definido, caso contrário o denyBrowserOriginCors incondicional) -> /health pré-autenticação em um bind de loopback comum -> bearerAuth -> as rotas com gate /health, /capabilities e /daemon/status. Não instala access log, então apenas as requisições que ele mesmo responde — /health, /capabilities e /daemon/status — não são registradas. O wrapper delegante (createDelegatingServeApp) fica à frente com seu próprio gate de bearer: qualquer outro caminho na janela fria inicia o runtime e é despachado para o app de runtime, que o registra, com durationMs medido a partir desse hand-off em vez do início visível pelo cliente. Com --open o app de runtime responde diretamente, então não há janela fria.

Subsistemas:

CaminhoFunção
serve/fs/Factory WorkspaceFileSystem mais policy.ts (verificações de tamanho/confiança/binário), paths.ts (canonicalizar, resolveWithin, rejeição de symlink), audit.ts e valores tipados de FsError.
serve/routes/workspace-file-read.ts, workspace-file-write.tsHandlers HTTP para GET /file, GET /file/bytes, POST /file/write e POST /file/edit.
serve/workspace-memory.tsGET/POST /workspace/memory (CRUD do QWEN.md).
serve/workspace-agents.tsGET/POST/DELETE /workspace/agents (CRUD de subagentes).
serve/daemon-status-provider.tsSnapshot do ambiente (env) mais células de preflight do daemon-host: versão do Node, entrada da CLI, stat do workspace, ripgrep, git, npm.
serve/permission-audit.tsPermissionAuditRing (FIFO de 512 entradas) e createPermissionAuditPublisher.
serve/auth/device-flow.ts, qwen-device-flow-provider.tsRotas OAuth de device-flow. Consulte 12-auth-security.md.
serve/daemon-logger.tsLogs de arquivo estruturados do DaemonLogger. Consulte 19-observability.md.
serve/debug-mode.tsPredicado compartilhado isServeDebugMode() que controla o contexto de erro detalhado nas respostas HTTP.
serve/acp-http/Transporte ACP Streamable HTTP (RFD #721), montado em /acp. Sete arquivos implementam JSON-RPC POST, SSE GET, teardown DELETE e uso compartilhado da bridge em paralelo com a superfície REST.
serve/web-shell-static.ts, serve/web-shell-resolver.tsLocaliza e monta os assets compilados do Web Shell (a UI do navegador do daemon) em /, /assets e /session/:id, além do fallback de deep-link SPA registrado após todas as rotas de API. Montado antes de bearerAuth em qualquer modo de inicialização porque um navegador não pode anexar Authorization a uma navegação ou sub-recurso. As chamadas de API seguem a política de autoridade normal: tokens configurados controlam as rotas de API normais exceto /health no loopback a menos que --require-auth esteja definido, enquanto a entrada de webhook de canal sempre usa seu próprio segredo compartilhado e o listener primário confiável de loopback sem token tem acesso total de operador. Degrada para apenas API quando os assets estão ausentes; --no-web desativa.

Imports do pacote ACP bridge:

  • Primitivas de event-bus são importadas de @qwen-code/acp-bridge/eventBus.
  • Primitivas de status são importadas de @qwen-code/acp-bridge/status.
  • serve/acp-session-bridge.ts permanece como a facade de compatibilidade local da CLI para a superfície mais ampla da bridge.

Fluxo

Sequência de inicialização

Antes de runQwenServe() iniciar esta sequência, o modo --open-with-auth (exclusivo da CLI) valida a elegibilidade de loopback/Web Shell e preenche ServeOptions.token com o token configurado selecionado, ou com 32 bytes aleatórios (um bearer de 256 bits) codificados em base64url quando essa seleção estiver vazia. Esse valor gerado é um token configurado comum para todos os passos abaixo — é por isso que --require-auth --open-with-auth inicializa — e é um gerador separado do bearer efêmero fora do loopback no passo 1. Embedders diretos que chamam createServeApp por conta própria nunca geram um token.

  1. Resolver o token a partir de opts.token ou QWEN_SERVER_TOKEN, removendo espaços para que uma nova linha final de cat token.txt não quebre silenciosamente a comparação do bearer. Quando o --hostname solicitado é fora do loopback (o literal localhost resolvido uma vez primeiro) e nenhuma fonte está presente, gerar um bearer efêmero de 128 bits (16 bytes) como 22 caracteres base64url em vez de recusar; ele é impresso uma vez pelo quickstart remoto após listen() e rotaciona a cada reinício. Grafias de loopback nunca geram, então mantêm o modo confiável sem token. Uma fonte explicitamente em branco (--token '', ou QWEN_SERVER_TOKEN definido como vazio ou apenas espaços em branco) não é “ausente”, então sempre suprime a geração — mas a brancura decide o token resolvido em apenas uma direção: um --token em branco sobrepõe um valor de env definido e resolve para nenhum token, enquanto um env em branco resolve para nenhum token apenas quando --token não é passado (um --token não em branco ainda vence); em ambos os casos, um bind fora do loopback sem token resolvido ainda falha nos guards abaixo.
  2. Proteção contra erro de digitação no hostname: --hostname localhost:4170 gera um erro e sugere --port.
  3. Preflight de autenticação: um bind fora do loopback sem token resolvido é recusado — alcançável através de uma fonte explicitamente vazia, ou através de um bind localhost cuja resolução única cai fora do loopback (a geração baseia-se na grafia, então nada foi gerado lá); --require-auth é recusado em um bind sem token, que após o passo 1 significa um bind de loopback sem fonte configurada. Os guards de --allow-origin HTTP(S) wildcard e fora do loopback leem o mesmo token resolvido, então em um bind fora do loopback o bearer gerado os satisfaz e essas recusas também são apenas de loopback.
  4. Validação do workspace: caminho absoluto, existe, é diretório. EACCES / EPERM são encapsulados para apontar para a flag.
  5. Canonicalizar workspace: canonicalizeWorkspace(rawWorkspace) executa realpathSync.native uma vez e alimenta /capabilities, o fallback de POST /session e a bridge.
  6. Validação do orçamento do MCP: inteiro positivo; enforce exige um orçamento.
  7. Inferência do toggle do pool MCP: a env pai QWEN_SERVE_NO_MCP_POOL=1 torna mcpPoolActive=false, para que as capacidades omitam honestamente mcp_workspace_pool e mcp_pool_restart.
  8. Validação de CORS / timeout / rate limit: valores --allow-origin wildcard e HTTP(S) fora do loopback exigem um token resolvido (consulte o passo 3 para entender por que essas recusas são apenas de loopback); valores de prompt, writer, channel idle, session idle, reaper e janela de rate limit falham rapidamente quando inválidos.
  9. childEnvOverrides por handle: passa QWEN_SERVE_MCP_CLIENT_BUDGET e QWEN_SERVE_MCP_BUDGET_MODE para o filho ACP através de BridgeOptions.childEnvOverrides em vez de mutar process.env.
  10. Carregar settings.json uma vez: lê context.fileName, policy.permissionStrategy e policy.consensusQuorum. Arquivos corrompidos recorrem aos padrões. validatePolicyConfig() verifica policy.* contra SERVE_CAPABILITY_REGISTRY.permission_mediation.modes; estratégias desconhecidas ou consensusQuorum não positivo lançam InvalidPolicyConfigError. Um quórum definido sob uma estratégia não consensus registra um aviso no stderr.
  11. Alocar PermissionAuditRing (512 entradas).
  12. Construir fsFactory: runQwenServe usa como padrão trusted: true; chamadores diretos de createServeApp usam como padrão trusted: false e avisam uma vez.
  13. createHttpAcpBridge, consulte 03-acp-bridge.md.
  14. createServeApp monta o Express.
  15. Criar e vincular o ciclo de vida do servidor HTTP(S) antes de ouvir (listen), depois chamar server.listen(port, hostname) e resolver o getPort() real para a allowlist de hosts. A propriedade (ownership) do Conversations não pode iniciar até que esse listener e os demais gates de inicialização do host estejam prontos.
  16. Registrar manipuladores SIGINT / SIGTERM para desligamento gracioso através do ciclo de vida compartilhado do app.

Desligamento gracioso

  1. Selar a admissão e iniciar todas as drenagens no primeiro sinal:
    • Descartar o registro de device-flow e cancelar fluxos pendentes.
    • bridge.shutdown() marca cada canal como isDying = true, envia um fechamento gracioso para o stdin de cada filho ACP, aguarda KILL_HARD_DEADLINE_MS (10s) por canal e então chama channel.kill() se necessário.
  2. Fechar o listener enquanto o app e o host drenam:
    • server.close() para de aceitar novas conexões e deixa as requisições em andamento terminarem.
    • SHUTDOWN_FORCE_CLOSE_MS (5s) aciona server.closeAllConnections().
    • Um segundo prazo de 2s escala novamente se necessário.
  3. Liberar a propriedade (ownership) do Conversations somente após prova positiva de desligamento do listener, trabalho local do app, trabalho de propriedade do host, limpeza do Live discovery e drenagens do runtime. Qualquer prova incompleita rejeita o desligamento em vez de permitir uma transferência insegura.
  4. Segundo sinal durante a saída:
    • bridge.killAllSync() + process.exit(1) para evitar que filhos órfãos bloqueiem a saída do daemon.

Estado e ciclo de vida

RunHandle expõe:

  • url: URL de escuta resolvida, após a resolução da porta efêmera.
  • port: porta real, incluindo a resolução de 0.
  • close(): desligamento programático para embedders e testes.

Chamar createServeApp diretamente ainda retorna apenas um Application. Um embedder que precisa de Live/Conversations deve criar o servidor Node real, chamar getServeAppLifecycle(app).bindServer(server) antes do seu primeiro listen() e aguardar lifecycle.close() durante o desligamento. Sem a vinculação, as rotas comuns permanecem disponíveis, mas Live/Conversations falham com fail closed. Chamar server.close() diretamente aciona a limpeza orientada por eventos, mas o embedder ainda deve aguardar lifecycle.close() para observar falhas de drenagem ou liberação de propriedade.

Dependências

Upstream usado por serve/Downstream usando serve/
@qwen-code/acp-bridge: bridge, event bus, tipos de statusO handler do subcomando serve da CLI qwen
packages/core: getAllMemoryFilenames, Config, WorkspaceContextEmbedders diretos, testes
ACP SDK (@agentclientprotocol/sdk): PROTOCOL_VERSION, ClientSideConnection através da bridge
Express + body-parser, node:crypto, node:fs, node:path

Configuração

OrigemChaveEfeito
EnvQWEN_SERVER_TOKENToken bearer após remoção de espaços.
EnvQWEN_SERVE_NO_MCP_POOL=1Força mcpPoolActive=false.
Env do filho ACPQWEN_SERVE_MCP_CLIENT_BUDGET / QWEN_SERVE_MCP_BUDGET_MODEGerado a partir de --mcp-client-budget / --mcp-budget-mode e encaminhado através de childEnvOverrides.
EnvQWEN_SERVE_PROMPT_DEADLINE_MS / QWEN_SERVE_WRITER_IDLE_TIMEOUT_MSTimeouts padrão de inatividade de prompt / SSE.
EnvQWEN_SERVE_RATE_LIMIT*Switch de rate limit, limites de prompt / mutação / leitura e padrão de janela.
EnvQWEN_SERVE_DEBUG=1Logs detalhados no stderr. Consulte 19-observability.md.
Flags--hostname, --portVinculação de escuta.
Flags--token, --require-auth, --enable-session-shellToken bearer, reforço de autenticação no loopback e switch explícito de execução de shell.
CLI flags--open-with-authLaunch do Web Shell no loopback (desativado por padrão) que reutiliza ou gera um bearer com vida útil do processo antes do runtime.
Flag--workspaceSubstitui process.cwd(); repita para registrar runtimes de workspace isolados adicionais.
Flags--max-sessions, --max-pending-prompts-per-session, --max-connections, --event-ring-sizeLimites da Bridge / Express.
Flags--mcp-client-budget=N, --mcp-budget-mode={off,warn,enforce}Encaminhado para o filho ACP.
Flags--allow-origin, --allow-private-auth-base-urlAllowlist de CORS do navegador e switch de instalação do provedor de autenticação localhost/privado.
Flag--web / --no-webServe ou pula a UI do Web Shell na raiz do daemon (o padrão é servir). --no-web deixa o daemon apenas API.
Flags--prompt-deadline-ms, --writer-idle-timeout-ms, --channel-idle-timeout-ms, --initialize-timeout-msControle do ciclo de vida de inatividade de prompt, writer SSE e filho ACP, e timeout de requisição do filho ACP.
Flags--session-reap-interval-ms, --session-idle-timeout-msControle de reaping de sessões desconectadas.
Flags--rate-limit*Limite de taxa HTTP por tier.
settings.jsonpolicy.permissionStrategy, policy.consensusQuorumPolítica e quórum do MultiClientPermissionMediator.
settings.jsoncontext.fileNameNome do arquivo de memória do workspace passado para /workspace/init através do contextFilename do workspace-service.

Consulte 17-configuration.md para a referência consolidada.

Ressalvas e limites conhecidos

  • O uso direto de createServeApp sem deps.fsFactory ou deps.bridge tem como padrão trusted: false; o writeTextFile do ACP no lado do agente rejeita como untrusted_workspace. O aviso é impresso apenas uma vez.
  • O app de runtime executa allowOriginCors sobre a allowlist mutável; valores de Origin sem match recebem o envelope de negação 403 (a barreira incondicional denyBrowserOriginCors sobrevive apenas no app de bootstrap). O Web Shell de loopback funciona porque outro middleware remove primeiro os valores correspondentes de mesma origem do loopback; em um bind fora do loopback com token, os XHRs de mesma origem do shell são autenticados por bearer e seu Origin removido antes da barreira, então não precisam de --allow-origin. Três casos ainda exigem uma entrada na allowlist: upgrades WebSocket (terminal, voz), um front proxy com terminação TLS cuja origem https nunca corresponde ao socket plain, e qualquer intermediário HTTP plain que reescreve o header Host — o proxy_set_header Host $proxy_host padrão do nginx e o k8s Ingress ambos fazem. A tradução de porta sozinha não precisa de nada em um bind fora do loopback (docker -p 8080:4170): a verificação compara Origin apenas com o Host encaminhado normalizado e nunca consulta a porta de escuta (apenas as portas padrão do scheme :80/:443 são removidas; uma porta não padrão deve sobreviver verbatim nele). No bind loopback padrão não é assim: a allowlist de Host contra DNS rebinding aceita apenas a porta do próprio daemon, então um túnel com tradução de porta (ssh -L 8080:localhost:4170) é rejeitado com 403 Invalid Host header para cada requisição, incluindo o documento do shell, e --allow-origin não pode sobrescrever isso — encaminhe a mesma porta ou vincule fora do loopback. A solução para os casos WebSocket e terminação TLS é --allow-origin <origin>; um intermediário que reescreve Host pode ser configurado para encaminhar Host verbatim — o que não pode ajudar quando a TLS termina no proxy, porque o scheme é lido do próprio socket do daemon.
  • Ordem do body-parser: rotas que usam mutate({ strict: true }) retornam 401 apenas após o express.json(). O pior caso é --max-connections × express.json({limit: '10mb'}), chegando a cerca de 2,5 GB de memória transitória em um listener de loopback saturado; esse tradeoff é intencional.
  • Múltiplos daemons em um único processo devem usar childEnvOverrides por handle; a mutação de process.env causa race conditions porque o defaultSpawnChannelFactory faz um snapshot do env no momento do spawn.

Referências

  • packages/cli/src/serve/run-qwen-serve.ts (bootstrap, validação de inicialização, encerramento gracioso)
  • packages/cli/src/serve/server.ts (createServeApp(), montagem de middleware e rotas)
  • packages/cli/src/serve/auth.ts (CORS, allowlist de Host, bearer auth, gate de mutação)
  • packages/cli/src/serve/rate-limit.ts (rate limit HTTP por tier)
  • packages/cli/src/serve/capabilities.ts (registro de capabilities e anúncio condicional)
  • packages/cli/src/serve/types.ts (ServeOptions, CapabilitiesEnvelope)
  • packages/cli/src/serve/daemon-status-provider.ts
  • packages/cli/src/serve/permission-audit.ts
  • Issues: #3803 , #4175 
Last updated on