Skip to Content
Guia do DesenvolvedorDaemonServe 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.
  • Rejeitar configurações de inicialização inseguras ou inválidas: bind fora do loopback sem token, --require-auth sem token, --allow-origin '*' 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 (allowOriginCors sobre a allowlist mutável de origem -> hostAllowlist -> access log -> bearerAuth -> rate limit -> JSON parser -> telemetry -> mutationGate por rota) e montar as rotas HTTP de sessão, CRUD de workspace, arquivo, autenticação de device-flow, votação de permissão e 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 e server.ts):

Middleware, em ordem de registroPropósitoNotas
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.
hostAllowlist(bind, getPort)No loopback, validar se o Host pertence a localhost, 127.0.0.1, [::1] ou host.docker.internal mais a porta real.Defesa contra DNS rebinding. A comparação não diferencia maiúsculas de minúsculas e é armazenada em cache por porta. O listener LAN do Local Control sempre aplica sua verificação de Host de autoridade anunciada, independentemente do bind primário.
Access-log middlewareRegistra método, caminho (path), status, durationMs, sessionId e clientId no DaemonLogger quando uma requisição termina.Registrado antes do bearerAuth, para que as negações 401 também sejam registradas. Ignora /health e heartbeat.
bearerAuth(token)Comparação de bearer em tempo constante usando SHA-256 mais 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; retorna 429 antes da análise quando um bucket é esgotado.
express.json({ limit: '10mb' })Análise do corpo (body) 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 rotas de mutação que exigem token mesmo no loopback.Retorna 401 { code: 'token_required' }. Não é um app.use global; as rotas chamam mutate({ strict: true }) conforme necessário.

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 — um navegador não pode anexar Authorization a uma navegação ou sub-recurso — enquanto toda rota de API que ele chama permanece protegida por token. 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 codificados em base64url quando essa seleção estiver vazia. Embedders diretos e invocações sem essa flag (desativada por padrão) não geram um token.

  1. Resolver e remover espaços do token em opts.token ou QWEN_SERVER_TOKEN; isso evita que uma nova linha final de cat token.txt quebre silenciosamente a comparação do bearer.
  2. Proteção contra erro de digitação no hostname: --hostname localhost:4170 gera um erro e sugere --port.
  3. Preflight de autenticação: fora do loopback sem token é recusado; --require-auth sem token é recusado.
  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: --allow-origin '*' exige token; valores de prompt, writer, channel idle, session idle, reaper e janela de rate limit falham rapidamente (fail fast) 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: loadSettings, getCurrentGeminiMdFilename, 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.fileNameSubstituição de getCurrentGeminiMdFilename para a bridge.

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 — binds fora do loopback exigem --allow-origin para os XHRs do shell.
  • 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